Bug 1478448 - (Part 4) Emit change events from Rule and TextProperty models. r=gl draft
authorRazvan Caliman <rcaliman@mozilla.com>
Wed, 25 Jul 2018 20:29:35 +0200
changeset 822692 1006e24d399834c3e9e76011a5dc58a8b9ee945d
parent 822691 3aa3617becc05acf5f7e4c845f5e780db5111df0
push id117448
push userbmo:rcaliman@mozilla.com
push dateWed, 25 Jul 2018 18:46:36 +0000
reviewersgl
bugs1478448
milestone63.0a1
Bug 1478448 - (Part 4) Emit change events from Rule and TextProperty models. r=gl This is a basic client-side implementation for tracking changes. It is likely to be removed. For now it serves as base for testing that the Changes panel and corresponding React - Redux setup works. MozReview-Commit-ID: FVxVjjarM47
devtools/client/inspector/rules/models/rule.js
devtools/client/inspector/rules/models/text-property.js
devtools/client/inspector/rules/views/rule-editor.js
--- a/devtools/client/inspector/rules/models/rule.js
+++ b/devtools/client/inspector/rules/models/rule.js
@@ -12,16 +12,17 @@ const {ELEMENT_STYLE} = require("devtool
 const TextProperty = require("devtools/client/inspector/rules/models/text-property");
 const {promiseWarn} = require("devtools/client/inspector/shared/utils");
 const {parseNamedDeclarations} = require("devtools/shared/css/parsing-utils");
 const Services = require("Services");
 
 const STYLE_INSPECTOR_PROPERTIES = "devtools/shared/locales/styleinspector.properties";
 const {LocalizationHelper} = require("devtools/shared/l10n");
 const STYLE_INSPECTOR_L10N = new LocalizationHelper(STYLE_INSPECTOR_PROPERTIES);
+const EventEmitter = require("devtools/shared/event-emitter");
 
 /**
  * Rule is responsible for the following:
  *   Manages a single style declaration or rule.
  *   Applies changes to the properties in a rule.
  *   Maintains a list of TextProperty objects.
  *
  * @param {ElementStyle} elementStyle
@@ -31,16 +32,18 @@ const STYLE_INSPECTOR_L10N = new Localiz
  *          rule: A StyleRuleActor
  *          inherited: An element this rule was inherited from.  If omitted,
  *            the rule applies directly to the current element.
  *          isSystem: Is this a user agent style?
  *          isUnmatched: True if the rule does not match the current selected
  *            element, otherwise, false.
  */
 function Rule(elementStyle, options) {
+  EventEmitter.decorate(this);
+
   this.elementStyle = elementStyle;
   this.domRule = options.rule;
   this.matchedSelectors = options.matchedSelectors || [];
   this.pseudoElement = options.pseudoElement || "";
 
   this.isSystem = options.isSystem;
   this.isUnmatched = options.isUnmatched || false;
   this.inherited = options.inherited || null;
@@ -81,16 +84,20 @@ Rule.prototype = {
         eltText += "#" + this.inherited.id;
       }
       this._inheritedSource =
         STYLE_INSPECTOR_L10N.getFormatStr("rule.inheritedFrom", eltText);
     }
     return this._inheritedSource;
   },
 
+  get isInlineStyle() {
+    return this.domRule.type === ELEMENT_STYLE;
+  },
+
   get keyframesName() {
     if (this._keyframesName) {
       return this._keyframesName;
     }
     this._keyframesName = "";
     if (this.keyframes) {
       this._keyframesName =
         STYLE_INSPECTOR_L10N.getFormatStr("rule.keyframe", this.keyframes.name);
@@ -163,16 +170,21 @@ Rule.prototype = {
 
     this.applyProperties((modifications) => {
       modifications.createProperty(ind, name, value, priority, enabled);
       // Now that the rule has been updated, the server might have given us data
       // that changes the state of the property. Update it now.
       prop.updateEditor();
     });
 
+    const change = {
+      rule: this,
+      add: { property: name, value }
+    };
+    this.emit("track-change", change);
     return prop;
   },
 
   /**
    * Helper function for applyProperties that is called when the actor
    * does not support as-authored styles.  Store disabled properties
    * in the element style's store.
    */
@@ -395,16 +407,21 @@ Rule.prototype = {
   removeProperty: function(property) {
     const index = this.textProps.indexOf(property);
     this.textProps.splice(index, 1);
     // Need to re-apply properties in case removing this TextProperty
     // exposes another one.
     this.applyProperties((modifications) => {
       modifications.removeProperty(index, property.name);
     });
+    const change = {
+      rule: this,
+      remove: { property: property.name, value: property.value }
+    };
+    this.emit("track-change", change);
   },
 
   /**
    * Get the list of TextProperties from the style. Needs
    * to parse the style's authoredText.
    */
   _getTextProperties: function() {
     const textProps = [];
--- a/devtools/client/inspector/rules/models/text-property.js
+++ b/devtools/client/inspector/rules/models/text-property.js
@@ -112,21 +112,34 @@ TextProperty.prototype = {
 
     if (changed) {
       this.updateEditor();
     }
   },
 
   setValue: function(value, priority, force = false) {
     const store = this.rule.elementStyle.store;
+    const isValueUpdated = this.editor && value !== this.editor.committed.value;
 
-    if (this.editor && value !== this.editor.committed.value || force) {
+    if (isValueUpdated || force) {
       store.userProperties.setProperty(this.rule.domRule, this.name, value);
     }
 
+    const change = {
+      rule: this.rule,
+      add: { property: this.name, value },
+      remove: isValueUpdated && this.editor.committed.value
+        ? {
+          property: this.name,
+          value: this.editor.committed.value
+        }
+        : null
+    };
+
+    this.rule.emit("track-change", change);
     this.rule.setPropertyValue(this, value, priority);
     this.updateEditor();
   },
 
   /**
    * Called when the property's value has been updated externally, and
    * the property and editor should update to reflect that value.
    *
@@ -153,16 +166,21 @@ TextProperty.prototype = {
   },
 
   setEnabled: function(value) {
     this.rule.setPropertyEnabled(this, value);
     this.updateEditor();
   },
 
   remove: function() {
+    const change = {
+      rule: this.rule,
+      remove: { property: this.name, value: this.value }
+    };
+    this.rule.emit("track-change", change);
     this.rule.removeProperty(this);
   },
 
   /**
    * Return a string representation of the rule property.
    */
   stringifyProperty: function() {
     // Get the displayed property value
--- a/devtools/client/inspector/rules/views/rule-editor.js
+++ b/devtools/client/inspector/rules/views/rule-editor.js
@@ -31,16 +31,17 @@ const Services = require("Services");
 const EventEmitter = require("devtools/shared/event-emitter");
 const {Tools} = require("devtools/client/definitions");
 const {gDevTools} = require("devtools/client/framework/devtools");
 const CssLogic = require("devtools/shared/inspector/css-logic");
 
 const STYLE_INSPECTOR_PROPERTIES = "devtools/shared/locales/styleinspector.properties";
 const {LocalizationHelper} = require("devtools/shared/l10n");
 const STYLE_INSPECTOR_L10N = new LocalizationHelper(STYLE_INSPECTOR_PROPERTIES);
+const TRACK_CHANGES_ENABLED = "devtools.inspector.changes.enabled";
 
 /**
  * RuleEditor is responsible for the following:
  *   Owns a Rule object and creates a list of TextPropertyEditors
  *     for its TextProperties.
  *   Manages creation of new text properties.
  *
  * @param {CssRuleView} ruleView
@@ -65,27 +66,30 @@ function RuleEditor(ruleView, rule) {
   this._onNewProperty = this._onNewProperty.bind(this);
   this._newPropertyDestroy = this._newPropertyDestroy.bind(this);
   this._onSelectorDone = this._onSelectorDone.bind(this);
   this._locationChanged = this._locationChanged.bind(this);
   this.updateSourceLink = this.updateSourceLink.bind(this);
   this._onToolChanged = this._onToolChanged.bind(this);
   this._updateLocation = this._updateLocation.bind(this);
   this._onSourceClick = this._onSourceClick.bind(this);
+  this.onTrackChange = this.onTrackChange.bind(this);
 
   this.rule.domRule.on("location-changed", this._locationChanged);
+  this.rule.on("track-change", this.onTrackChange);
   this.toolbox.on("tool-registered", this._onToolChanged);
   this.toolbox.on("tool-unregistered", this._onToolChanged);
 
   this._create();
 }
 
 RuleEditor.prototype = {
   destroy: function() {
     this.rule.domRule.off("location-changed");
+    this.rule.off("track-change", this.onTrackChange);
     this.toolbox.off("tool-registered", this._onToolChanged);
     this.toolbox.off("tool-unregistered", this._onToolChanged);
 
     let url = null;
     if (this.rule.sheet) {
       url = this.rule.sheet.href || this.rule.sheet.nodeHref;
     }
     if (url && !this.rule.isSystem && this.rule.domRule.type !== ELEMENT_STYLE) {
@@ -96,16 +100,27 @@ RuleEditor.prototype = {
 
       if (this._sourceMapURLService) {
         this._sourceMapURLService.unsubscribe(url, sourceLine, sourceColumn,
           this._updateLocation);
       }
     }
   },
 
+  /**
+   * Track metadata about a style change.
+   *
+   * @param {Object} change
+   */
+  onTrackChange(change) {
+    if (Services.prefs.getBoolPref(TRACK_CHANGES_ENABLED)) {
+      this.ruleView.inspector.changeManager.track(change);
+    }
+  },
+
   get sourceMapURLService() {
     if (!this._sourceMapURLService) {
       // sourceMapURLService is a lazy getter in the toolbox.
       this._sourceMapURLService = this.toolbox.sourceMapURLService;
     }
 
     return this._sourceMapURLService;
   },