Bug 1501962 - Expose a method from rule view that scrolls to a rule property's name and value. r=gl
authorMicah Tigley <mtigley@mozilla.com>
Thu, 21 Feb 2019 23:04:25 +0000
changeset 460557 0a4aefb36aff504e179121c8de0039c0df9e8106
parent 460556 ef2adef9bbccc3c90945ff3cc1648488bcf58b7e
child 460558 03aece6cda47ff158485d986e19ba230ea4c8834
push id78763
push usermtigley@mozilla.com
push dateFri, 22 Feb 2019 15:26:00 +0000
treeherderautoland@0a4aefb36aff [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgl
bugs1501962
milestone67.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1501962 - Expose a method from rule view that scrolls to a rule property's name and value. r=gl Differential Revision: https://phabricator.services.mozilla.com/D15932
devtools/client/inspector/rules/rules.js
devtools/client/inspector/rules/test/browser.ini
devtools/client/inspector/rules/test/browser_rules_highlight-element-rule.js
devtools/client/inspector/rules/test/browser_rules_highlight-property.js
devtools/client/themes/rules.css
--- a/devtools/client/inspector/rules/rules.js
+++ b/devtools/client/inspector/rules/rules.js
@@ -26,26 +26,29 @@ const {
   VIEW_NODE_VARIABLE_TYPE,
   VIEW_NODE_FONT_TYPE,
 } = require("devtools/client/inspector/shared/node-types");
 const TooltipsOverlay = require("devtools/client/inspector/shared/tooltips-overlay");
 const {createChild, promiseWarn} = require("devtools/client/inspector/shared/utils");
 const {debounce} = require("devtools/shared/debounce");
 const EventEmitter = require("devtools/shared/event-emitter");
 
+loader.lazyRequireGetter(this, "flashElementOn", "devtools/client/inspector/markup/utils", true);
+loader.lazyRequireGetter(this, "flashElementOff", "devtools/client/inspector/markup/utils", true);
 loader.lazyRequireGetter(this, "ClassListPreviewer", "devtools/client/inspector/rules/views/class-list-previewer");
 loader.lazyRequireGetter(this, "StyleInspectorMenu", "devtools/client/inspector/shared/style-inspector-menu");
 loader.lazyRequireGetter(this, "AutocompletePopup", "devtools/client/shared/autocomplete-popup");
 loader.lazyRequireGetter(this, "KeyShortcuts", "devtools/client/shared/key-shortcuts");
 loader.lazyRequireGetter(this, "clipboardHelper", "devtools/shared/platform/clipboard");
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const PREF_UA_STYLES = "devtools.inspector.showUserAgentStyles";
 const PREF_DEFAULT_COLOR_UNIT = "devtools.defaultColorUnit";
 const FILTER_CHANGED_TIMEOUT = 150;
+const REMOVE_FLASH_ELEMENT_DURATION = 500;
 
 // This is used to parse user input when filtering.
 const FILTER_PROP_RE = /\s*([^:\s]*)\s*:\s*(.*?)\s*;?$/;
 // This is used to parse the filter search value to see if the filter
 // should be strict or not
 const FILTER_STRICT_RE = /\s*`(.*?)`\s*$/;
 const INSET_POINT_TYPES = ["top", "right", "bottom", "left"];
 
@@ -118,16 +121,18 @@ function CssRuleView(inspector, document
   this._onAddRule = this._onAddRule.bind(this);
   this._onContextMenu = this._onContextMenu.bind(this);
   this._onCopy = this._onCopy.bind(this);
   this._onFilterStyles = this._onFilterStyles.bind(this);
   this._onClearSearch = this._onClearSearch.bind(this);
   this._onTogglePseudoClassPanel = this._onTogglePseudoClassPanel.bind(this);
   this._onTogglePseudoClass = this._onTogglePseudoClass.bind(this);
   this._onToggleClassPanel = this._onToggleClassPanel.bind(this);
+  this.highlightElementRule = this.highlightElementRule.bind(this);
+  this.highlightProperty = this.highlightProperty.bind(this);
 
   const doc = this.styleDocument;
   this.element = doc.getElementById("ruleview-container-focusable");
   this.addRuleButton = doc.getElementById("ruleview-add-rule-button");
   this.searchField = doc.getElementById("ruleview-searchbox");
   this.searchClearButton = doc.getElementById("ruleview-searchinput-clear");
   this.pseudoClassPanel = doc.getElementById("pseudo-class-panel");
   this.pseudoClassToggle = doc.getElementById("pseudo-class-panel-toggle");
@@ -1046,16 +1051,18 @@ CssRuleView.prototype = {
     this.element.appendChild(container);
 
     header.addEventListener("click", () => {
       this._toggleContainerVisibility(twisty, container, isPseudo,
         !this.showPseudoElements);
     });
 
     if (isPseudo) {
+      container.id = "pseudo-elements-container";
+      twisty.id = "pseudo-elements-header-twisty";
       this._toggleContainerVisibility(twisty, container, isPseudo,
         this.showPseudoElements);
     }
 
     return container;
   },
 
   /**
@@ -1603,16 +1610,180 @@ CssRuleView.prototype = {
                event.target === this.searchField &&
                this._onClearSearch()) {
       // Handle the search box's keypress event. If the escape key is pressed,
       // clear the search box field.
       event.preventDefault();
       event.stopPropagation();
     }
   },
+
+  /**
+   * Temporarily flash the given element.
+   *
+   * @param  {Element} element
+   *         The element.
+   */
+  _flashElement(element) {
+    flashElementOn(element);
+    flashElementOff(element);
+
+    if (this._removeFlashOutTimer) {
+      clearTimeout(this._removeFlashOutTimer);
+      this._removeFlashOutTimer = null;
+    }
+
+    // Remove the flash-out class to prevent the element from re-flashing when the view
+    // is resized.
+    this._removeFlashOutTimer = setTimeout(() => {
+      element.classList.remove("flash-out");
+      // Emit "scrolled-to-property" for use by tests.
+      this.emit("scrolled-to-element");
+    }, REMOVE_FLASH_ELEMENT_DURATION);
+  },
+
+  /**
+   * Scrolls to the top of either the rule or declaration. The view will try to scroll to
+   * the rule if both can fit in the viewport. If not, then scroll to the declaration.
+   *
+   * @param  {Element} rule
+   *         The rule to scroll to.
+   * @param  {Element|null} declaration
+   *         Optional. The declaration to scroll to.
+   * @param  {String} scrollBehavior
+   *         Optional. The transition animation when scrolling.
+   */
+  _scrollToElement(rule, declaration, scrollBehavior = "smooth") {
+    let elementToScrollTo = rule;
+
+    if (declaration) {
+      const { offsetTop, offsetHeight } = declaration;
+      // Get the distance between both the rule and declaration. If the distance is
+      // greater than the height of the rule view, then only scroll to the declaration.
+      const distance = (offsetTop + offsetHeight) - rule.offsetTop;
+
+      if (this.element.parentNode.offsetHeight <= distance) {
+        elementToScrollTo = declaration;
+      }
+    }
+
+    elementToScrollTo.scrollIntoView({ behavior: scrollBehavior });
+  },
+
+  /**
+   * Toggles the visibility of the pseudo element rule's container.
+   */
+  _togglePseudoElementRuleContainer() {
+    const container = this.styleDocument.getElementById("pseudo-elements-container");
+    const twisty = this.styleDocument.getElementById("pseudo-elements-header-twisty");
+    this._toggleContainerVisibility(twisty, container, true, true);
+  },
+
+  /**
+   * Finds the rule with the matching actorID and highlights it.
+   *
+   * @param  {String} ruleId
+   *         The actorID of the rule.
+   */
+  highlightElementRule: function(ruleId) {
+    let scrollBehavior = "smooth";
+
+    const rule = this.rules.find(r => r.domRule.actorID === ruleId);
+
+    if (!rule) {
+      return;
+    }
+
+    if (rule.domRule.actorID === ruleId) {
+      // If using 2-Pane mode, then switch to the Rules tab first.
+      if (!this.inspector.is3PaneModeEnabled) {
+        this.inspector.sidebar.select("ruleview");
+      }
+
+      if (rule.pseudoElement.length && !this.showPseudoElements) {
+        scrollBehavior = "auto";
+        this._togglePseudoElementRuleContainer();
+      }
+
+      const { editor: { element } } = rule;
+
+      // Scroll to the top of the rule and highlight it.
+      this._scrollToElement(element, null, scrollBehavior);
+      this._flashElement(element);
+    }
+  },
+
+  /**
+   * Finds the specified TextProperty name in the rule view. If found, scroll to and
+   * flash the TextProperty.
+   *
+   * @param  {String} name
+   *         The property name to scroll to and highlight.
+   * @return {Boolean} true if the TextProperty name is found, and false otherwise.
+   */
+  highlightProperty: function(name) {
+    for (const rule of this.rules) {
+      for (const textProp of rule.textProps) {
+        if (textProp.overridden || textProp.invisible || !textProp.enabled) {
+          continue;
+        }
+
+        const { editor: { selectorText } } = rule;
+        let scrollBehavior = "smooth";
+
+        // First, search for a matching authored property.
+        if (textProp.name === name) {
+          // If using 2-Pane mode, then switch to the Rules tab first.
+          if (!this.inspector.is3PaneModeEnabled) {
+            this.inspector.sidebar.select("ruleview");
+          }
+
+          // If the property is being applied by a pseudo element rule, expand the pseudo
+          // element list container.
+          if (rule.pseudoElement.length && !this.showPseudoElements) {
+            // Set the scroll behavior to "auto" to avoid timing issues between toggling
+            // the pseudo element container and scrolling smoothly to the rule.
+            scrollBehavior = "auto";
+            this._togglePseudoElementRuleContainer();
+          }
+
+          // Scroll to the top of the property's rule so that both the property and its
+          // rule are visible.
+          this._scrollToElement(selectorText, textProp.editor.element, scrollBehavior);
+          this._flashElement(textProp.editor.element);
+
+          return true;
+        }
+
+        // If there is no matching property, then look in computed properties.
+        for (const computed of textProp.computed) {
+          if (computed.name === name) {
+            if (!this.inspector.is3PaneModeEnabled) {
+              this.inspector.sidebar.select("ruleview");
+            }
+
+            if (textProp.rule.pseudoElement.length && !this.showPseudoElements) {
+              scrollBehavior = "auto";
+              this._togglePseudoElementRuleContainer();
+            }
+
+            // Expand the computed list.
+            textProp.editor.expandForFilter();
+
+            this._scrollToElement(selectorText, computed.element, scrollBehavior);
+            this._flashElement(computed.element);
+
+            return true;
+          }
+        }
+      }
+    }
+
+    return false;
+  },
 };
 
 /**
  * Helper functions
  */
 
 /**
  * Walk up the DOM from a given node until a parent property holder is found.
--- a/devtools/client/inspector/rules/test/browser.ini
+++ b/devtools/client/inspector/rules/test/browser.ini
@@ -182,16 +182,18 @@ skip-if = (os == "win" && debug) # bug 9
 [browser_rules_grid-toggle_01.js]
 [browser_rules_grid-toggle_01b.js]
 [browser_rules_grid-toggle_02.js]
 [browser_rules_grid-toggle_03.js]
 [browser_rules_grid-toggle_04.js]
 [browser_rules_grid-toggle_05.js]
 [browser_rules_gridline-names-autocomplete.js]
 [browser_rules_guessIndentation.js]
+[browser_rules_highlight-element-rule.js]
+[browser_rules_highlight-property.js]
 [browser_rules_highlight-used-fonts.js]
 [browser_rules_inherited-properties_01.js]
 [browser_rules_inherited-properties_02.js]
 [browser_rules_inherited-properties_03.js]
 [browser_rules_inherited-properties_04.js]
 [browser_rules_inline-source-map.js]
 [browser_rules_inline-style-order.js]
 [browser_rules_invalid.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_highlight-element-rule.js
@@ -0,0 +1,42 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+  http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the rule view's highlightElementRule scrolls to the specified rule.
+
+const TEST_URI = `
+  <style type="text/css">
+    .test::after {
+      content: "!";
+      color: red;
+    }
+  </style>
+  <div class="test">Hello</div>
+`;
+
+add_task(async function() {
+  await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  const { inspector, view } = await openRuleView();
+
+  await selectNode(".test", inspector);
+  const { rules, styleWindow } = view;
+
+  info("Highlight .test::after rule.");
+  const ruleId = rules[0].domRule.actorID;
+
+  info("Wait for the view to scroll to the property.");
+  const onHighlightProperty = view.once("scrolled-to-element");
+
+  view.highlightElementRule(ruleId);
+
+  await onHighlightProperty;
+
+  ok(isInViewport(rules[0].editor.element, styleWindow), ".test::after is in view.");
+});
+
+function isInViewport(element, win) {
+  const { top, left, bottom, right } = element.getBoundingClientRect();
+  return top >= 0 && bottom <= win.innerHeight && left >= 0 && right <= win.innerWidth;
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_highlight-property.js
@@ -0,0 +1,69 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+  http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the rule view's highlightProperty scrolls to the specified declaration.
+
+const TEST_URI = `
+  <style type="text/css">
+    .test {
+      font-size: 12px;
+      border: 1px solid blue;
+    }
+
+    .test::after {
+      content: "!";
+      color: red;
+    }
+  </style>
+  <div class="test">Hello this is a test</div>
+`;
+
+add_task(async function() {
+  await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  const { inspector, view } = await openRuleView();
+
+  await selectNode(".test", inspector);
+  const { rules, styleWindow } = view;
+
+  info("Highlight the computed border-left-width declaration in the rule view.");
+  const borderLeftWidthStyle = rules[2].textProps[1].computed
+    .find(({ name }) => name === "border-left-width");
+
+  let onHighlightProperty = view.once("scrolled-to-element");
+  let isHighlighted = view.highlightProperty("border-left-width");
+  await onHighlightProperty;
+
+  ok(isHighlighted, "border-left-property is highlighted.");
+  ok(isInViewport(borderLeftWidthStyle.element, styleWindow),
+    "border-left-width is in view.");
+
+  info("Highlight the computed font-size declaration in the rule view.");
+  const fontSize = rules[2].textProps[0].editor;
+
+  info("Wait for the view to scroll to the property.");
+  onHighlightProperty = view.once("scrolled-to-element");
+  isHighlighted = view.highlightProperty("font-size");
+  await onHighlightProperty;
+
+  ok(isHighlighted, "font-size property is highlighted.");
+  ok(isInViewport(fontSize.element, styleWindow), "font-size is in view.");
+
+  info("Highlight the pseudo-element's color declaration in the rule view.");
+  const color = rules[0].textProps[1].editor;
+
+  info("Wait for the view to scroll to the property.");
+  onHighlightProperty = view.once("scrolled-to-element");
+  isHighlighted = view.highlightProperty("color");
+  await onHighlightProperty;
+
+  ok(isHighlighted, "color property is highlighted.");
+  ok(isInViewport(color.element, styleWindow), "color property is in view.");
+});
+
+function isInViewport(element, win) {
+  const { top, left, bottom, right } = element.getBoundingClientRect();
+  return top >= 0 && bottom <= win.innerHeight && left >= 0 && right <= win.innerWidth;
+}
--- a/devtools/client/themes/rules.css
+++ b/devtools/client/themes/rules.css
@@ -635,8 +635,18 @@
 
 #pseudo-class-panel-toggle::before {
   background-image: url("chrome://devtools/skin/images/pseudo-class.svg");
 }
 
 #class-panel-toggle::before {
   content: ".cls";
 }
+
+.flash-out {
+  animation: flash-out 1s ease-out;
+}
+
+@keyframes flash-out {
+  from {
+    background: var(--theme-contrast-background);
+  }
+}