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
--- 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;
},