Bug 792387 - Rule view should highlight which sections of a css selector matches an element. r=jwalker
authorDave Camp <dcamp@mozilla.com>
Thu, 20 Sep 2012 12:54:39 -0700
changeset 107494 dc326f5870d2403a6239a4fb530ea121ddc207ef
parent 107493 196ca3ef5b570e136c7fa2d4641e0d2482032e56
child 107596 f731fa718465e7cfc252c0acf1aeec35913d7123
push id1099
push userdcamp@campd.org
push dateThu, 20 Sep 2012 19:55:49 +0000
treeherderfx-team@dc326f5870d2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjwalker
bugs792387
milestone18.0a1
Bug 792387 - Rule view should highlight which sections of a css selector matches an element. r=jwalker
browser/devtools/styleinspector/CssLogic.jsm
browser/devtools/styleinspector/CssRuleView.jsm
browser/devtools/styleinspector/test/browser_ruleview_ui.js
browser/themes/gnomestripe/devtools/csshtmltree.css
browser/themes/pinstripe/devtools/csshtmltree.css
browser/themes/winstripe/devtools/csshtmltree.css
--- a/browser/devtools/styleinspector/CssLogic.jsm
+++ b/browser/devtools/styleinspector/CssLogic.jsm
@@ -827,16 +827,79 @@ CssLogic.getShortNamePath = function Css
     });
     aElement = aElement.parentNode;
   } while (aElement && aElement != doc.body && aElement != doc.head && aElement != doc);
 
   return reply;
 };
 
 /**
+ * Get a string list of selectors for a given CSSStyleRule.selectorText
+ *
+ * @param {string} aSelectorText The CSSStyleRule.selectorText to parse.
+ * @return {array} An array of string selectors.
+ */
+CssLogic.getSelectors = function CssLogic_getSelectors(aSelectorText)
+{
+  let selectors = [];
+
+  let selector = aSelectorText.trim();
+  if (!selector) {
+    return selectors;
+  }
+
+  let nesting = 0;
+  let currentSelector = [];
+
+  // Parse a selector group into selectors. Normally we could just .split(',')
+  // however Gecko allows -moz-any(a, b, c) as a selector so we ignore commas
+  // inside brackets.
+  for (let i = 0, selLen = selector.length; i < selLen; i++) {
+    let c = selector.charAt(i);
+    switch (c) {
+      case ",":
+        if (nesting == 0 && currentSelector.length > 0) {
+          let selectorStr = currentSelector.join("").trim();
+          if (selectorStr) {
+            selectors.push(selectorStr);
+          }
+          currentSelector = [];
+        } else {
+          currentSelector.push(c);
+        }
+        break;
+
+      case "(":
+        nesting++;
+        currentSelector.push(c);
+        break;
+
+      case ")":
+        nesting--;
+        currentSelector.push(c);
+        break;
+
+      default:
+        currentSelector.push(c);
+        break;
+    }
+  }
+
+  // Add the last selector.
+  if (nesting == 0 && currentSelector.length > 0) {
+    let selectorStr = currentSelector.join("").trim();
+    if (selectorStr) {
+      selectors.push(selectorStr);
+    }
+  }
+
+  return selectors;
+}
+
+/**
  * Memonized lookup of a l10n string from a string bundle.
  * @param {string} aName The key to lookup.
  * @returns A localized version of the given key.
  */
 CssLogic.l10n = function(aName) CssLogic._strings.GetStringFromName(aName);
 
 XPCOMUtils.defineLazyGetter(CssLogic, "_strings", function() Services.strings
         .createBundle("chrome://browser/locale/devtools/styleinspector.properties"));
@@ -1263,66 +1326,18 @@ CssRule.prototype = {
 
     // Parse the CSSStyleRule.selectorText string.
     this._selectors = [];
 
     if (!this._domRule.selectorText) {
       return this._selectors;
     }
 
-    let selector = this._domRule.selectorText.trim();
-    if (!selector) {
-      return this._selectors;
-    }
-
-    let nesting = 0;
-    let currentSelector = [];
-
-    // Parse a selector group into selectors. Normally we could just .split(',')
-    // however Gecko allows -moz-any(a, b, c) as a selector so we ignore commas
-    // inside brackets.
-    for (let i = 0, selLen = selector.length; i < selLen; i++) {
-      let c = selector.charAt(i);
-      switch (c) {
-        case ",":
-          if (nesting == 0 && currentSelector.length > 0) {
-            let selectorStr = currentSelector.join("").trim();
-            if (selectorStr) {
-              this._selectors.push(new CssSelector(this, selectorStr));
-            }
-            currentSelector = [];
-          } else {
-            currentSelector.push(c);
-          }
-          break;
-
-        case "(":
-          nesting++;
-          currentSelector.push(c);
-          break;
-
-        case ")":
-          nesting--;
-          currentSelector.push(c);
-          break;
-
-        default:
-          currentSelector.push(c);
-          break;
-      }
-    }
-
-    // Add the last selector.
-    if (nesting == 0 && currentSelector.length > 0) {
-      let selectorStr = currentSelector.join("").trim();
-      if (selectorStr) {
-        this._selectors.push(new CssSelector(this, selectorStr));
-      }
-    }
-
+    let selectors = CssLogic.getSelectors(this._domRule.selectorText);
+    this._selectors = [new CssSelector(this, text) for (text of selectors)];
     return this._selectors;
   },
 
   toString: function CssRule_toString()
   {
     return "[CssRule " + this._domRule.selectorText + "]";
   },
 };
--- a/browser/devtools/styleinspector/CssRuleView.jsm
+++ b/browser/devtools/styleinspector/CssRuleView.jsm
@@ -1399,17 +1399,44 @@ RuleEditor.prototype = {
     }.bind(this));
   },
 
   /**
    * Update the rule editor with the contents of the rule.
    */
   populate: function RuleEditor_populate()
   {
-    this.selectorText.textContent = this.rule.selectorText;
+    // Clear out existing viewers.
+    while (this.selectorText.hasChildNodes()) {
+      this.selectorText.removeChild(this.selectorText.lastChild);
+    }
+
+    // If selector text comes from a css rule, highlight selectors that
+    // actually match.  For custom selector text (such as for the 'element'
+    // style, just show the text directly.
+    if (this.rule.domRule && this.rule.domRule.selectorText) {
+      let selectors = CssLogic.getSelectors(this.rule.selectorText);
+      let element = this.rule.inherited || this.ruleView._viewedElement;
+      for (let i = 0; i < selectors.length; i++) {
+        let selector = selectors[i];
+        if (i != 0) {
+          createChild(this.selectorText, "span", {
+            class: "ruleview-selector-separator",
+            textContent: ", "
+          });
+        }
+        let cls = element.mozMatchesSelector(selector) ? "ruleview-selector-matched" : "ruleview-selector-unmatched";
+        createChild(this.selectorText, "span", {
+          class: cls,
+          textContent: selector
+        });
+      }
+    } else {
+      this.selectorText.textContent = this.rule.selectorText;
+    }
 
     for (let prop of this.rule.textProps) {
       if (!prop.editor) {
         new TextPropertyEditor(this, prop);
         this.propertyList.appendChild(prop.editor.element);
       }
     }
   },
--- a/browser/devtools/styleinspector/test/browser_ruleview_ui.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_ui.js
@@ -26,17 +26,17 @@ function expectChange()
 }
 
 function startTest()
 {
   let style = '' +
     '#testid {' +
     '  background-color: blue;' +
     '} ' +
-    '.testclass {' +
+    '.testclass, .unmatched {' +
     '  background-color: green;' +
     '}';
 
   let styleNode = addStyle(doc, style);
   doc.body.innerHTML = '<div id="testid" class="testclass">Styled Node</div>';
   let testElement = doc.getElementById("testid");
 
   ruleDialog = openDialog("chrome://browser/content/devtools/cssruleview.xul",
@@ -50,16 +50,20 @@ function startTest()
     ruleView.element.addEventListener("CssRuleViewChanged", ruleViewChanged, false);
     is(ruleView.element.querySelectorAll("#noResults").length, 1, "Has a no-results element.");
     ruleView.highlight(testElement);
     is(ruleView.element.querySelectorAll("#noResults").length, 0, "After a highlight, no longer has a no-results element.");
     ruleView.highlight(null);
     is(ruleView.element.querySelectorAll("#noResults").length, 1, "After highlighting null, has a no-results element again.");
     ruleView.highlight(testElement);
 
+    let classEditor = ruleView.element.children[2]._ruleEditor;
+    is(classEditor.selectorText.querySelector(".ruleview-selector-matched").textContent, ".testclass", ".textclass should be matched.");
+    is(classEditor.selectorText.querySelector(".ruleview-selector-unmatched").textContent, ".unmatched", ".unmatched should not be matched.");
+
     waitForFocus(testCancelNew, ruleDialog);
   }, true);
 }
 
 function testCancelNew()
 {
   // Start at the beginning: start to add a rule to the element's style
   // declaration, but leave it empty.
--- a/browser/themes/gnomestripe/devtools/csshtmltree.css
+++ b/browser/themes/gnomestripe/devtools/csshtmltree.css
@@ -276,8 +276,12 @@
 .ruleview-propertycontainer > .ruleview-propertyvalue {
   border-bottom: 1px dashed transparent;
 }
 
 .ruleview-namecontainer:hover > .ruleview-propertyname,
 .ruleview-propertycontainer:hover > .ruleview-propertyvalue {
   border-bottom-color: hsl(0,0%,50%);
 }
+
+.ruleview-selector-separator, .ruleview-selector-unmatched {
+  color: #888;
+}
--- a/browser/themes/pinstripe/devtools/csshtmltree.css
+++ b/browser/themes/pinstripe/devtools/csshtmltree.css
@@ -278,8 +278,13 @@
 .ruleview-propertycontainer > .ruleview-propertyvalue {
   border-bottom: 1px dashed transparent;
 }
 
 .ruleview-namecontainer:hover > .ruleview-propertyname,
 .ruleview-propertycontainer:hover > .ruleview-propertyvalue {
   border-bottom-color: hsl(0,0%,50%);
 }
+
+.ruleview-selector-separator, .ruleview-selector-unmatched {
+  color: #888;
+}
+
--- a/browser/themes/winstripe/devtools/csshtmltree.css
+++ b/browser/themes/winstripe/devtools/csshtmltree.css
@@ -278,8 +278,12 @@
 .ruleview-propertycontainer > .ruleview-propertyvalue {
   border-bottom: 1px dashed transparent;
 }
 
 .ruleview-namecontainer:hover > .ruleview-propertyname,
 .ruleview-propertycontainer:hover > .ruleview-propertyvalue {
   border-bottom-color: hsl(0,0%,50%);
 }
+
+.ruleview-selector-separator, .ruleview-selector-unmatched {
+  color: #888;
+}