Bug 1478612 - 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 822922 d53d52fbd2f930dcffff727c1a05e8c06e35f23d
parent 822828 7ba07ef0e4532b644b812942aa38af4510dbc74f
push id117525
push userbmo:rcaliman@mozilla.com
push dateThu, 26 Jul 2018 11:19:31 +0000
reviewersgl
bugs1478612
milestone63.0a1
Bug 1478612 - Emit change events from Rule and TextProperty models. r=gl This is a basic client-side implementation for tracking changes occurring through the Rule view. 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/changes/change-manager.js
devtools/client/inspector/rules/models/rule.js
devtools/client/inspector/rules/models/text-property.js
devtools/client/inspector/rules/views/rule-editor.js
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/changes/change-manager.js
@@ -0,0 +1,67 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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 {
+  trackChange,
+} = require("./actions/changes");
+
+class ChangeManager {
+  constructor(inspector) {
+    this.inspector = inspector;
+    this.store = this.inspector.store;
+  }
+
+  async track(change) {
+    const defaults = { rule: null, add: null, remove: null };
+    change = { ...defaults, ...change };
+    const { rule, add, remove } = change;
+
+    if (!rule) {
+      console.warn(`Missing rule for change: ${change}`);
+      return;
+    }
+
+    // Do not track partial property additions.
+    if (add && add.property && !add.value) {
+      return;
+    }
+
+    // Do not track partial property removals.
+    if (remove && remove.property && !remove.value) {
+      return;
+    }
+
+    // Source is the stylesheet URL if defined. Otherwise, assume it's an inline <style>
+    let href = (rule.sheet && rule.sheet.href) ? rule.sheet.href : "inline stylesheet";
+    let selector = rule.selectorText;
+    const tag = this.inspector.selection.nodeFront.tagName;
+
+    // If the rule is actually and inline style, correct previous assumptions.
+    if (rule.isInlineStyle) {
+      href = "inline";
+      selector = await this.inspector.selection.nodeFront.getUniqueSelector();
+    }
+
+    const data = {
+      href,
+      selector,
+      add,
+      remove,
+      tag,
+    };
+
+    this.store.dispatch(trackChange(data));
+  }
+
+  destroy() {
+    this.inspector = null;
+    this.store = null;
+  }
+}
+
+module.exports = ChangeManager;
--- 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;
   },