Bug 1521013 - Implement selector editing in the new rules view. r=rcaliman
authorGabriel Luong <gabriel.luong@gmail.com>
Tue, 22 Jan 2019 16:38:43 -0500
changeset 514915 2a721c78e648516a8dc97df2755ceb7a00f7658f
parent 514914 347238f5ae0a30eb86909da23b99badd9ad479ca
child 514930 80d073eee75b5a5333c9293aa990ee8415837bc8
push id1953
push userffxbld-merge
push dateMon, 11 Mar 2019 12:10:20 +0000
treeherdermozilla-release@9c35dcbaa899 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrcaliman
bugs1521013
milestone66.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 1521013 - Implement selector editing in the new rules view. r=rcaliman
devtools/client/inspector/rules/components/Rule.js
devtools/client/inspector/rules/components/Rules.js
devtools/client/inspector/rules/components/RulesApp.js
devtools/client/inspector/rules/components/Selector.js
devtools/client/inspector/rules/models/element-style.js
devtools/client/inspector/rules/new-rules.js
--- a/devtools/client/inspector/rules/components/Rule.js
+++ b/devtools/client/inspector/rules/components/Rule.js
@@ -16,27 +16,30 @@ const SourceLink = createFactory(require
 const Types = require("../types");
 
 class Rule extends PureComponent {
   static get propTypes() {
     return {
       onToggleDeclaration: PropTypes.func.isRequired,
       onToggleSelectorHighlighter: PropTypes.func.isRequired,
       rule: PropTypes.shape(Types.rule).isRequired,
+      showSelectorEditor: PropTypes.func.isRequired,
     };
   }
 
   render() {
     const {
       onToggleDeclaration,
       onToggleSelectorHighlighter,
       rule,
+      showSelectorEditor,
     } = this.props;
     const {
       declarations,
+      id,
       isUnmatched,
       isUserAgentStyle,
       selector,
       sourceLink,
       type,
     } = rule;
 
     return (
@@ -45,17 +48,20 @@ class Rule extends PureComponent {
           className: "ruleview-rule devtools-monospace" +
                      (isUnmatched ? " unmatched" : "") +
                      (isUserAgentStyle ? " uneditable" : ""),
         },
         SourceLink({ sourceLink }),
         dom.div({ className: "ruleview-code" },
           dom.div({},
             Selector({
+              id,
+              isUserAgentStyle,
               selector,
+              showSelectorEditor,
               type,
             }),
             type !== CSSRule.KEYFRAME_RULE ?
               SelectorHighlighter({
                 onToggleSelectorHighlighter,
                 selector,
               })
               :
--- a/devtools/client/inspector/rules/components/Rules.js
+++ b/devtools/client/inspector/rules/components/Rules.js
@@ -12,30 +12,33 @@ const Rule = createFactory(require("./Ru
 const Types = require("../types");
 
 class Rules extends PureComponent {
   static get propTypes() {
     return {
       onToggleDeclaration: PropTypes.func.isRequired,
       onToggleSelectorHighlighter: PropTypes.func.isRequired,
       rules: PropTypes.arrayOf(PropTypes.shape(Types.rule)).isRequired,
+      showSelectorEditor: PropTypes.func.isRequired,
     };
   }
 
   render() {
     const {
       onToggleDeclaration,
       onToggleSelectorHighlighter,
       rules,
+      showSelectorEditor,
     } = this.props;
 
     return rules.map(rule => {
       return Rule({
         key: rule.id,
         onToggleDeclaration,
         onToggleSelectorHighlighter,
         rule,
+        showSelectorEditor,
       });
     });
   }
 }
 
 module.exports = Rules;
--- a/devtools/client/inspector/rules/components/RulesApp.js
+++ b/devtools/client/inspector/rules/components/RulesApp.js
@@ -30,16 +30,25 @@ class RulesApp extends PureComponent {
     return {
       onAddClass: PropTypes.func.isRequired,
       onSetClassState: PropTypes.func.isRequired,
       onToggleClassPanelExpanded: PropTypes.func.isRequired,
       onToggleDeclaration: PropTypes.func.isRequired,
       onTogglePseudoClass: PropTypes.func.isRequired,
       onToggleSelectorHighlighter: PropTypes.func.isRequired,
       rules: PropTypes.arrayOf(PropTypes.shape(Types.rule)).isRequired,
+      showSelectorEditor: PropTypes.func.isRequired,
+    };
+  }
+
+  getRuleProps() {
+    return {
+      onToggleDeclaration: this.props.onToggleDeclaration,
+      onToggleSelectorHighlighter: this.props.onToggleSelectorHighlighter,
+      showSelectorEditor: this.props.showSelectorEditor,
     };
   }
 
   renderInheritedRules(rules) {
     if (!rules.length) {
       return null;
     }
 
@@ -51,18 +60,17 @@ class RulesApp extends PureComponent {
         lastInherited = rule.inheritance.inherited;
 
         output.push(
           dom.div({ className: "ruleview-header" }, rule.inheritance.inheritedSource)
         );
       }
 
       output.push(Rule({
-        onToggleDeclaration: this.props.onToggleDeclaration,
-        onToggleSelectorHighlighter: this.props.onToggleSelectorHighlighter,
+        ...this.getRuleProps(),
         rule,
       }));
     }
 
     return output;
   }
 
   renderKeyframesRules(rules) {
@@ -79,18 +87,17 @@ class RulesApp extends PureComponent {
       }
 
       lastKeyframes = rule.keyframesRule.id;
 
       const items = [
         {
           component: Rules,
           componentProps: {
-            onToggleDeclaration: this.props.onToggleDeclaration,
-            onToggleSelectorHighlighter: this.props.onToggleSelectorHighlighter,
+            ...this.getRuleProps(),
             rules: rules.filter(r => r.keyframesRule.id === lastKeyframes),
           },
           header: rule.keyframesRule.keyframesName,
           opened: true,
         },
       ];
 
       output.push(Accordion({ items }));
@@ -100,33 +107,31 @@ class RulesApp extends PureComponent {
   }
 
   renderStyleRules(rules) {
     if (!rules.length) {
       return null;
     }
 
     return Rules({
-      onToggleDeclaration: this.props.onToggleDeclaration,
-      onToggleSelectorHighlighter: this.props.onToggleSelectorHighlighter,
+      ...this.getRuleProps(),
       rules,
     });
   }
 
   renderPseudoElementRules(rules) {
     if (!rules.length) {
       return null;
     }
 
     const items = [
       {
         component: Rules,
         componentProps: {
-          onToggleDeclaration: this.props.onToggleDeclaration,
-          onToggleSelectorHighlighter: this.props.onToggleSelectorHighlighter,
+          ...this.getRuleProps(),
           rules,
         },
         header: getStr("rule.pseudoElement"),
         opened: Services.prefs.getBoolPref(SHOW_PSEUDO_ELEMENTS_PREF),
         onToggled: () => {
           const opened = Services.prefs.getBoolPref(SHOW_PSEUDO_ELEMENTS_PREF);
           Services.prefs.setBoolPref(SHOW_PSEUDO_ELEMENTS_PREF, !opened);
         },
--- a/devtools/client/inspector/rules/components/Selector.js
+++ b/devtools/client/inspector/rules/components/Selector.js
@@ -1,39 +1,62 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const { PureComponent } = require("devtools/client/shared/vendor/react");
+const { createRef, PureComponent } = require("devtools/client/shared/vendor/react");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
-
+const { editableItem } = require("devtools/client/shared/inplace-editor");
 const {
   parsePseudoClassesAndAttributes,
   SELECTOR_ATTRIBUTE,
   SELECTOR_ELEMENT,
   SELECTOR_PSEUDO_CLASS,
 } = require("devtools/shared/css/parsing-utils");
 const {
   ELEMENT_STYLE,
   PSEUDO_CLASSES,
 } = require("devtools/client/inspector/rules/constants");
 
 const Types = require("../types");
 
 class Selector extends PureComponent {
   static get propTypes() {
     return {
+      id: PropTypes.string.isRequired,
+      isUserAgentStyle: PropTypes.bool.isRequired,
       selector: PropTypes.shape(Types.selector).isRequired,
+      showSelectorEditor: PropTypes.func.isRequired,
       type: PropTypes.number.isRequired,
     };
   }
 
+  constructor(props) {
+    super(props);
+    this.selectorRef = createRef();
+  }
+
+  componentDidMount() {
+    if (this.props.isUserAgentStyle ||
+        this.props.type === ELEMENT_STYLE ||
+        this.props.type === CSSRule.KEYFRAME_RULE) {
+      // Selector is not editable.
+      return;
+    }
+
+    editableItem({
+      element: this.selectorRef.current,
+    }, element => {
+      this.props.showSelectorEditor(element, this.props.id);
+    });
+  }
+
   renderSelector() {
     // Show the text directly for custom selector text (such as the inline "element"
     // style and Keyframes rules).
     if (this.props.type === ELEMENT_STYLE || this.props.type === CSSRule.KEYFRAME_RULE) {
       return this.props.selector.selectorText;
     }
 
     const { matchedSelectors, selectors } = this.props.selector;
@@ -82,16 +105,17 @@ class Selector extends PureComponent {
     return output;
   }
 
   render() {
     return (
       dom.span(
         {
           className: "ruleview-selectorcontainer",
+          ref: this.selectorRef,
           tabIndex: 0,
         },
         this.renderSelector()
       )
     );
   }
 }
 
--- a/devtools/client/inspector/rules/models/element-style.js
+++ b/devtools/client/inspector/rules/models/element-style.js
@@ -328,16 +328,83 @@ ElementStyle.prototype = {
       // overridden state has changed for the text property.
       if (this._updatePropertyOverridden(textProp)) {
         textProp.updateEditor();
       }
     }
   },
 
   /**
+   * Modifies the existing rule's selector to the new given value.
+   *
+   * @param {String} ruleId
+   *        The id of the Rule to be modified.
+   * @param {String} selector
+   *        The new selector value.
+   */
+  modifySelector: async function(ruleId, selector) {
+    try {
+      const rule = this.getRule(ruleId);
+      if (!rule) {
+        return;
+      }
+
+      const response = await rule.domRule.modifySelector(this.element, selector);
+      const { ruleProps, isMatching } = response;
+
+      if (!ruleProps) {
+        // Notify for changes, even when nothing changes, just to allow tests
+        // being able to track end of this request.
+        this.ruleView.emit("ruleview-invalid-selector");
+        return;
+      }
+
+      const newRule = new Rule(this, {
+        ...ruleProps,
+        isUnmatched: !isMatching,
+      });
+
+      // Recompute the list of applied styles because editing a
+      // selector might cause this rule's position to change.
+      const appliedStyles = await this.pageStyle.getApplied(this.element, {
+        inherited: true,
+        matchedSelectors: true,
+        filter: this.showUserAgentStyles ? "ua" : undefined,
+      });
+      const newIndex = appliedStyles.findIndex(r => r.rule == ruleProps.rule);
+      const oldIndex = this.rules.indexOf(rule);
+
+      // Remove the old rule and insert the new rule according to where it appears
+      // in the list of applied styles.
+      this.rules.splice(oldIndex, 1);
+      // If the selector no longer matches, then we leave the rule in
+      // the same relative position.
+      this.rules.splice(newIndex === -1 ? oldIndex : newIndex, 0, newRule);
+
+      // Mark any properties that are overridden according to the new list of rules.
+      this.markOverriddenAll();
+
+      // In order to keep the new rule in place of the old in the rules view, we need
+      // to remove the rule again if the rule was inserted to its new index according
+      // to the list of applied styles.
+      // Note: you might think we would replicate the list-modification logic above,
+      // but that is complicated due to the way the UI installs pseudo-element rules
+      // and the like.
+      if (newIndex !== -1) {
+        this.rules.splice(newIndex, 1);
+        this.rules.splice(oldIndex, 0, newRule);
+      }
+
+      this._changed();
+    } catch (e) {
+      console.error(e);
+    }
+  },
+
+  /**
    * Toggles the enabled state of the given CSS declaration.
    *
    * @param {String} ruleId
    *        The Rule id of the given CSS declaration.
    * @param {String} declarationId
    *        The TextProperty id for the CSS declaration.
    */
   toggleDeclaration: function(ruleId, declarationId) {
--- a/devtools/client/inspector/rules/new-rules.js
+++ b/devtools/client/inspector/rules/new-rules.js
@@ -26,16 +26,17 @@ const {
 
 const RulesApp = createFactory(require("./components/RulesApp"));
 
 const { LocalizationHelper } = require("devtools/shared/l10n");
 const INSPECTOR_L10N =
   new LocalizationHelper("devtools/client/locales/inspector.properties");
 
 loader.lazyRequireGetter(this, "ClassList", "devtools/client/inspector/rules/models/class-list");
+loader.lazyRequireGetter(this, "InplaceEditor", "devtools/client/shared/inplace-editor", true);
 
 const PREF_UA_STYLES = "devtools.inspector.showUserAgentStyles";
 
 class RulesView {
   constructor(inspector, window) {
     this.cssProperties = inspector.cssProperties;
     this.doc = window.document;
     this.inspector = inspector;
@@ -49,16 +50,17 @@ class RulesView {
 
     this.onAddClass = this.onAddClass.bind(this);
     this.onSelection = this.onSelection.bind(this);
     this.onSetClassState = this.onSetClassState.bind(this);
     this.onToggleClassPanelExpanded = this.onToggleClassPanelExpanded.bind(this);
     this.onToggleDeclaration = this.onToggleDeclaration.bind(this);
     this.onTogglePseudoClass = this.onTogglePseudoClass.bind(this);
     this.onToggleSelectorHighlighter = this.onToggleSelectorHighlighter.bind(this);
+    this.showSelectorEditor = this.showSelectorEditor.bind(this);
     this.updateClassList = this.updateClassList.bind(this);
     this.updateRules = this.updateRules.bind(this);
 
     this.inspector.sidebar.on("select", this.onSelection);
     this.selection.on("detached-front", this.onSelection);
     this.selection.on("new-node-front", this.onSelection);
 
     this.init();
@@ -73,16 +75,17 @@ class RulesView {
 
     const rulesApp = RulesApp({
       onAddClass: this.onAddClass,
       onSetClassState: this.onSetClassState,
       onToggleClassPanelExpanded: this.onToggleClassPanelExpanded,
       onToggleDeclaration: this.onToggleDeclaration,
       onTogglePseudoClass: this.onTogglePseudoClass,
       onToggleSelectorHighlighter: this.onToggleSelectorHighlighter,
+      showSelectorEditor: this.showSelectorEditor,
     });
 
     const provider = createElement(Provider, {
       id: "ruleview",
       key: "ruleview",
       store: this.store,
       title: INSPECTOR_L10N.getStr("inspector.sidebar.ruleViewTitle"),
     }, rulesApp);
@@ -320,16 +323,45 @@ class RulesView {
       this.highlighters.selectorHighlighterShown = null;
       this.store.dispatch(updateHighlightedSelector(""));
       // This event is emitted for testing purposes.
       this.emit("ruleview-selectorhighlighter-toggled", false);
     }
   }
 
   /**
+   * Shows the inplace editor for the a selector.
+   *
+   * @param  {DOMNode} element
+   *         The selector's span element to show the inplace editor.
+   * @param  {String} ruleId
+   *         The id of the Rule to be modified.
+   */
+  showSelectorEditor(element, ruleId) {
+    new InplaceEditor({
+      element,
+      done: async (value, commit) => {
+        if (!value || !commit) {
+          return;
+        }
+
+        // Hide the selector highlighter if it matches the selector being edited.
+        if (this.highlighters.selectorHighlighterShown) {
+          const selector = await this.elementStyle.getRule(ruleId).getUniqueSelector();
+          if (this.highlighters.selectorHighlighterShown === selector) {
+            this.onToggleSelectorHighlighter(this.highlighters.selectorHighlighterShown);
+          }
+        }
+
+        await this.elementStyle.modifySelector(ruleId, value);
+      },
+    });
+  }
+
+  /**
    * Updates the rules view by dispatching the new rules data of the newly selected
    * element. This is called when the rules view becomes visible or upon new node
    * selection.
    *
    * @param  {NodeFront|null} element
    *         The NodeFront of the current selected element.
    */
   async update(element) {