summaryrefslogtreecommitdiffstats
path: root/devtools/client/inspector/rules/models
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /devtools/client/inspector/rules/models
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'devtools/client/inspector/rules/models')
-rw-r--r--devtools/client/inspector/rules/models/element-style.js412
-rw-r--r--devtools/client/inspector/rules/models/moz.build11
-rw-r--r--devtools/client/inspector/rules/models/rule.js686
-rw-r--r--devtools/client/inspector/rules/models/text-property.js215
4 files changed, 1324 insertions, 0 deletions
diff --git a/devtools/client/inspector/rules/models/element-style.js b/devtools/client/inspector/rules/models/element-style.js
new file mode 100644
index 000000000..7f015ba08
--- /dev/null
+++ b/devtools/client/inspector/rules/models/element-style.js
@@ -0,0 +1,412 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set 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 promise = require("promise");
+const {Rule} = require("devtools/client/inspector/rules/models/rule");
+const {promiseWarn} = require("devtools/client/inspector/shared/utils");
+const {ELEMENT_STYLE} = require("devtools/shared/specs/styles");
+const {getCssProperties} = require("devtools/shared/fronts/css-properties");
+
+/**
+ * ElementStyle is responsible for the following:
+ * Keeps track of which properties are overridden.
+ * Maintains a list of Rule objects for a given element.
+ *
+ * @param {Element} element
+ * The element whose style we are viewing.
+ * @param {CssRuleView} ruleView
+ * The instance of the rule-view panel.
+ * @param {Object} store
+ * The ElementStyle can use this object to store metadata
+ * that might outlast the rule view, particularly the current
+ * set of disabled properties.
+ * @param {PageStyleFront} pageStyle
+ * Front for the page style actor that will be providing
+ * the style information.
+ * @param {Boolean} showUserAgentStyles
+ * Should user agent styles be inspected?
+ */
+function ElementStyle(element, ruleView, store, pageStyle,
+ showUserAgentStyles) {
+ this.element = element;
+ this.ruleView = ruleView;
+ this.store = store || {};
+ this.pageStyle = pageStyle;
+ this.showUserAgentStyles = showUserAgentStyles;
+ this.rules = [];
+ this.cssProperties = getCssProperties(this.ruleView.inspector.toolbox);
+
+ // We don't want to overwrite this.store.userProperties so we only create it
+ // if it doesn't already exist.
+ if (!("userProperties" in this.store)) {
+ this.store.userProperties = new UserProperties();
+ }
+
+ if (!("disabled" in this.store)) {
+ this.store.disabled = new WeakMap();
+ }
+}
+
+ElementStyle.prototype = {
+ // The element we're looking at.
+ element: null,
+
+ destroy: function () {
+ if (this.destroyed) {
+ return;
+ }
+ this.destroyed = true;
+
+ for (let rule of this.rules) {
+ if (rule.editor) {
+ rule.editor.destroy();
+ }
+ }
+ },
+
+ /**
+ * Called by the Rule object when it has been changed through the
+ * setProperty* methods.
+ */
+ _changed: function () {
+ if (this.onChanged) {
+ this.onChanged();
+ }
+ },
+
+ /**
+ * Refresh the list of rules to be displayed for the active element.
+ * Upon completion, this.rules[] will hold a list of Rule objects.
+ *
+ * Returns a promise that will be resolved when the elementStyle is
+ * ready.
+ */
+ populate: function () {
+ let populated = this.pageStyle.getApplied(this.element, {
+ inherited: true,
+ matchedSelectors: true,
+ filter: this.showUserAgentStyles ? "ua" : undefined,
+ }).then(entries => {
+ if (this.destroyed) {
+ return promise.resolve(undefined);
+ }
+
+ if (this.populated !== populated) {
+ // Don't care anymore.
+ return promise.resolve(undefined);
+ }
+
+ // Store the current list of rules (if any) during the population
+ // process. They will be reused if possible.
+ let existingRules = this.rules;
+
+ this.rules = [];
+
+ for (let entry of entries) {
+ this._maybeAddRule(entry, existingRules);
+ }
+
+ // Mark overridden computed styles.
+ this.markOverriddenAll();
+
+ this._sortRulesForPseudoElement();
+
+ // We're done with the previous list of rules.
+ for (let r of existingRules) {
+ if (r && r.editor) {
+ r.editor.destroy();
+ }
+ }
+
+ return undefined;
+ }).then(null, e => {
+ // populate is often called after a setTimeout,
+ // the connection may already be closed.
+ if (this.destroyed) {
+ return promise.resolve(undefined);
+ }
+ return promiseWarn(e);
+ });
+ this.populated = populated;
+ return this.populated;
+ },
+
+ /**
+ * Put pseudo elements in front of others.
+ */
+ _sortRulesForPseudoElement: function () {
+ this.rules = this.rules.sort((a, b) => {
+ return (a.pseudoElement || "z") > (b.pseudoElement || "z");
+ });
+ },
+
+ /**
+ * Add a rule if it's one we care about. Filters out duplicates and
+ * inherited styles with no inherited properties.
+ *
+ * @param {Object} options
+ * Options for creating the Rule, see the Rule constructor.
+ * @param {Array} existingRules
+ * Rules to reuse if possible. If a rule is reused, then it
+ * it will be deleted from this array.
+ * @return {Boolean} true if we added the rule.
+ */
+ _maybeAddRule: function (options, existingRules) {
+ // If we've already included this domRule (for example, when a
+ // common selector is inherited), ignore it.
+ if (options.rule &&
+ this.rules.some(rule => rule.domRule === options.rule)) {
+ return false;
+ }
+
+ if (options.system) {
+ return false;
+ }
+
+ let rule = null;
+
+ // If we're refreshing and the rule previously existed, reuse the
+ // Rule object.
+ if (existingRules) {
+ let ruleIndex = existingRules.findIndex((r) => r.matches(options));
+ if (ruleIndex >= 0) {
+ rule = existingRules[ruleIndex];
+ rule.refresh(options);
+ existingRules.splice(ruleIndex, 1);
+ }
+ }
+
+ // If this is a new rule, create its Rule object.
+ if (!rule) {
+ rule = new Rule(this, options);
+ }
+
+ // Ignore inherited rules with no visible properties.
+ if (options.inherited && !rule.hasAnyVisibleProperties()) {
+ return false;
+ }
+
+ this.rules.push(rule);
+ return true;
+ },
+
+ /**
+ * Calls markOverridden with all supported pseudo elements
+ */
+ markOverriddenAll: function () {
+ this.markOverridden();
+ for (let pseudo of this.cssProperties.pseudoElements) {
+ this.markOverridden(pseudo);
+ }
+ },
+
+ /**
+ * Mark the properties listed in this.rules for a given pseudo element
+ * with an overridden flag if an earlier property overrides it.
+ *
+ * @param {String} pseudo
+ * Which pseudo element to flag as overridden.
+ * Empty string or undefined will default to no pseudo element.
+ */
+ markOverridden: function (pseudo = "") {
+ // Gather all the text properties applied by these rules, ordered
+ // from more- to less-specific. Text properties from keyframes rule are
+ // excluded from being marked as overridden since a number of criteria such
+ // as time, and animation overlay are required to be check in order to
+ // determine if the property is overridden.
+ let textProps = [];
+ for (let rule of this.rules) {
+ if ((rule.matchedSelectors.length > 0 ||
+ rule.domRule.type === ELEMENT_STYLE) &&
+ rule.pseudoElement === pseudo && !rule.keyframes) {
+ for (let textProp of rule.textProps.slice(0).reverse()) {
+ if (textProp.enabled) {
+ textProps.push(textProp);
+ }
+ }
+ }
+ }
+
+ // Gather all the computed properties applied by those text
+ // properties.
+ let computedProps = [];
+ for (let textProp of textProps) {
+ computedProps = computedProps.concat(textProp.computed);
+ }
+
+ // Walk over the computed properties. As we see a property name
+ // for the first time, mark that property's name as taken by this
+ // property.
+ //
+ // If we come across a property whose name is already taken, check
+ // its priority against the property that was found first:
+ //
+ // If the new property is a higher priority, mark the old
+ // property overridden and mark the property name as taken by
+ // the new property.
+ //
+ // If the new property is a lower or equal priority, mark it as
+ // overridden.
+ //
+ // _overriddenDirty will be set on each prop, indicating whether its
+ // dirty status changed during this pass.
+ let taken = {};
+ for (let computedProp of computedProps) {
+ let earlier = taken[computedProp.name];
+
+ // Prevent -webkit-gradient from being selected after unchecking
+ // linear-gradient in this case:
+ // -moz-linear-gradient: ...;
+ // -webkit-linear-gradient: ...;
+ // linear-gradient: ...;
+ if (!computedProp.textProp.isValid()) {
+ computedProp.overridden = true;
+ continue;
+ }
+ let overridden;
+ if (earlier &&
+ computedProp.priority === "important" &&
+ earlier.priority !== "important" &&
+ (earlier.textProp.rule.inherited ||
+ !computedProp.textProp.rule.inherited)) {
+ // New property is higher priority. Mark the earlier property
+ // overridden (which will reverse its dirty state).
+ earlier._overriddenDirty = !earlier._overriddenDirty;
+ earlier.overridden = true;
+ overridden = false;
+ } else {
+ overridden = !!earlier;
+ }
+
+ computedProp._overriddenDirty =
+ (!!computedProp.overridden !== overridden);
+ computedProp.overridden = overridden;
+ if (!computedProp.overridden && computedProp.textProp.enabled) {
+ taken[computedProp.name] = computedProp;
+ }
+ }
+
+ // For each TextProperty, mark it overridden if all of its
+ // computed properties are marked overridden. Update the text
+ // property's associated editor, if any. This will clear the
+ // _overriddenDirty state on all computed properties.
+ for (let textProp of textProps) {
+ // _updatePropertyOverridden will return true if the
+ // overridden state has changed for the text property.
+ if (this._updatePropertyOverridden(textProp)) {
+ textProp.updateEditor();
+ }
+ }
+ },
+
+ /**
+ * Mark a given TextProperty as overridden or not depending on the
+ * state of its computed properties. Clears the _overriddenDirty state
+ * on all computed properties.
+ *
+ * @param {TextProperty} prop
+ * The text property to update.
+ * @return {Boolean} true if the TextProperty's overridden state (or any of
+ * its computed properties overridden state) changed.
+ */
+ _updatePropertyOverridden: function (prop) {
+ let overridden = true;
+ let dirty = false;
+ for (let computedProp of prop.computed) {
+ if (!computedProp.overridden) {
+ overridden = false;
+ }
+ dirty = computedProp._overriddenDirty || dirty;
+ delete computedProp._overriddenDirty;
+ }
+
+ dirty = (!!prop.overridden !== overridden) || dirty;
+ prop.overridden = overridden;
+ return dirty;
+ }
+};
+
+/**
+ * Store of CSSStyleDeclarations mapped to properties that have been changed by
+ * the user.
+ */
+function UserProperties() {
+ this.map = new Map();
+}
+
+UserProperties.prototype = {
+ /**
+ * Get a named property for a given CSSStyleDeclaration.
+ *
+ * @param {CSSStyleDeclaration} style
+ * The CSSStyleDeclaration against which the property is mapped.
+ * @param {String} name
+ * The name of the property to get.
+ * @param {String} value
+ * Default value.
+ * @return {String}
+ * The property value if it has previously been set by the user, null
+ * otherwise.
+ */
+ getProperty: function (style, name, value) {
+ let key = this.getKey(style);
+ let entry = this.map.get(key, null);
+
+ if (entry && name in entry) {
+ return entry[name];
+ }
+ return value;
+ },
+
+ /**
+ * Set a named property for a given CSSStyleDeclaration.
+ *
+ * @param {CSSStyleDeclaration} style
+ * The CSSStyleDeclaration against which the property is to be mapped.
+ * @param {String} bame
+ * The name of the property to set.
+ * @param {String} userValue
+ * The value of the property to set.
+ */
+ setProperty: function (style, bame, userValue) {
+ let key = this.getKey(style, bame);
+ let entry = this.map.get(key, null);
+
+ if (entry) {
+ entry[bame] = userValue;
+ } else {
+ let props = {};
+ props[bame] = userValue;
+ this.map.set(key, props);
+ }
+ },
+
+ /**
+ * Check whether a named property for a given CSSStyleDeclaration is stored.
+ *
+ * @param {CSSStyleDeclaration} style
+ * The CSSStyleDeclaration against which the property would be mapped.
+ * @param {String} name
+ * The name of the property to check.
+ */
+ contains: function (style, name) {
+ let key = this.getKey(style, name);
+ let entry = this.map.get(key, null);
+ return !!entry && name in entry;
+ },
+
+ getKey: function (style, name) {
+ return style.actorID + ":" + name;
+ },
+
+ clear: function () {
+ this.map.clear();
+ }
+};
+
+exports.ElementStyle = ElementStyle;
diff --git a/devtools/client/inspector/rules/models/moz.build b/devtools/client/inspector/rules/models/moz.build
new file mode 100644
index 000000000..1c5c0f89f
--- /dev/null
+++ b/devtools/client/inspector/rules/models/moz.build
@@ -0,0 +1,11 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'element-style.js',
+ 'rule.js',
+ 'text-property.js',
+)
diff --git a/devtools/client/inspector/rules/models/rule.js b/devtools/client/inspector/rules/models/rule.js
new file mode 100644
index 000000000..1a3fa057a
--- /dev/null
+++ b/devtools/client/inspector/rules/models/rule.js
@@ -0,0 +1,686 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set 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 promise = require("promise");
+const CssLogic = require("devtools/shared/inspector/css-logic");
+const {ELEMENT_STYLE} = require("devtools/shared/specs/styles");
+const {TextProperty} =
+ require("devtools/client/inspector/rules/models/text-property");
+const {promiseWarn} = require("devtools/client/inspector/shared/utils");
+const {parseDeclarations} = 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);
+
+/**
+ * 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
+ * The ElementStyle to which this rule belongs.
+ * @param {Object} options
+ * The information used to construct this rule. Properties include:
+ * 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) {
+ this.elementStyle = elementStyle;
+ this.domRule = options.rule || null;
+ this.style = options.rule;
+ this.matchedSelectors = options.matchedSelectors || [];
+ this.pseudoElement = options.pseudoElement || "";
+
+ this.isSystem = options.isSystem;
+ this.isUnmatched = options.isUnmatched || false;
+ this.inherited = options.inherited || null;
+ this.keyframes = options.keyframes || null;
+ this._modificationDepth = 0;
+
+ if (this.domRule && this.domRule.mediaText) {
+ this.mediaText = this.domRule.mediaText;
+ }
+
+ this.cssProperties = this.elementStyle.ruleView.cssProperties;
+
+ // Populate the text properties with the style's current authoredText
+ // value, and add in any disabled properties from the store.
+ this.textProps = this._getTextProperties();
+ this.textProps = this.textProps.concat(this._getDisabledProperties());
+}
+
+Rule.prototype = {
+ mediaText: "",
+
+ get title() {
+ let title = CssLogic.shortSource(this.sheet);
+ if (this.domRule.type !== ELEMENT_STYLE && this.ruleLine > 0) {
+ title += ":" + this.ruleLine;
+ }
+
+ return title + (this.mediaText ? " @media " + this.mediaText : "");
+ },
+
+ get inheritedSource() {
+ if (this._inheritedSource) {
+ return this._inheritedSource;
+ }
+ this._inheritedSource = "";
+ if (this.inherited) {
+ let eltText = this.inherited.displayName;
+ if (this.inherited.id) {
+ eltText += "#" + this.inherited.id;
+ }
+ this._inheritedSource =
+ STYLE_INSPECTOR_L10N.getFormatStr("rule.inheritedFrom", eltText);
+ }
+ return this._inheritedSource;
+ },
+
+ get keyframesName() {
+ if (this._keyframesName) {
+ return this._keyframesName;
+ }
+ this._keyframesName = "";
+ if (this.keyframes) {
+ this._keyframesName =
+ STYLE_INSPECTOR_L10N.getFormatStr("rule.keyframe", this.keyframes.name);
+ }
+ return this._keyframesName;
+ },
+
+ get selectorText() {
+ return this.domRule.selectors ? this.domRule.selectors.join(", ") :
+ CssLogic.l10n("rule.sourceElement");
+ },
+
+ /**
+ * The rule's stylesheet.
+ */
+ get sheet() {
+ return this.domRule ? this.domRule.parentStyleSheet : null;
+ },
+
+ /**
+ * The rule's line within a stylesheet
+ */
+ get ruleLine() {
+ return this.domRule ? this.domRule.line : "";
+ },
+
+ /**
+ * The rule's column within a stylesheet
+ */
+ get ruleColumn() {
+ return this.domRule ? this.domRule.column : null;
+ },
+
+ /**
+ * Get display name for this rule based on the original source
+ * for this rule's style sheet.
+ *
+ * @return {Promise}
+ * Promise which resolves with location as an object containing
+ * both the full and short version of the source string.
+ */
+ getOriginalSourceStrings: function () {
+ return this.domRule.getOriginalLocation().then(({href,
+ line, mediaText}) => {
+ let mediaString = mediaText ? " @" + mediaText : "";
+ let linePart = line > 0 ? (":" + line) : "";
+
+ let sourceStrings = {
+ full: (href || CssLogic.l10n("rule.sourceInline")) + linePart +
+ mediaString,
+ short: CssLogic.shortSource({href: href}) + linePart + mediaString
+ };
+
+ return sourceStrings;
+ });
+ },
+
+ /**
+ * Returns true if the rule matches the creation options
+ * specified.
+ *
+ * @param {Object} options
+ * Creation options. See the Rule constructor for documentation.
+ */
+ matches: function (options) {
+ return this.style === options.rule;
+ },
+
+ /**
+ * Create a new TextProperty to include in the rule.
+ *
+ * @param {String} name
+ * The text property name (such as "background" or "border-top").
+ * @param {String} value
+ * The property's value (not including priority).
+ * @param {String} priority
+ * The property's priority (either "important" or an empty string).
+ * @param {Boolean} enabled
+ * True if the property should be enabled.
+ * @param {TextProperty} siblingProp
+ * Optional, property next to which the new property will be added.
+ */
+ createProperty: function (name, value, priority, enabled, siblingProp) {
+ let prop = new TextProperty(this, name, value, priority, enabled);
+
+ let ind;
+ if (siblingProp) {
+ ind = this.textProps.indexOf(siblingProp) + 1;
+ this.textProps.splice(ind, 0, prop);
+ } else {
+ ind = this.textProps.length;
+ this.textProps.push(prop);
+ }
+
+ 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();
+ });
+
+ 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.
+ */
+ _applyPropertiesNoAuthored: function (modifications) {
+ this.elementStyle.markOverriddenAll();
+
+ let disabledProps = [];
+
+ for (let prop of this.textProps) {
+ if (prop.invisible) {
+ continue;
+ }
+ if (!prop.enabled) {
+ disabledProps.push({
+ name: prop.name,
+ value: prop.value,
+ priority: prop.priority
+ });
+ continue;
+ }
+ if (prop.value.trim() === "") {
+ continue;
+ }
+
+ modifications.setProperty(-1, prop.name, prop.value, prop.priority);
+
+ prop.updateComputed();
+ }
+
+ // Store disabled properties in the disabled store.
+ let disabled = this.elementStyle.store.disabled;
+ if (disabledProps.length > 0) {
+ disabled.set(this.style, disabledProps);
+ } else {
+ disabled.delete(this.style);
+ }
+
+ return modifications.apply().then(() => {
+ let cssProps = {};
+ // Note that even though StyleRuleActors normally provide parsed
+ // declarations already, _applyPropertiesNoAuthored is only used when
+ // connected to older backend that do not provide them. So parse here.
+ for (let cssProp of parseDeclarations(this.cssProperties.isKnown,
+ this.style.authoredText)) {
+ cssProps[cssProp.name] = cssProp;
+ }
+
+ for (let textProp of this.textProps) {
+ if (!textProp.enabled) {
+ continue;
+ }
+ let cssProp = cssProps[textProp.name];
+
+ if (!cssProp) {
+ cssProp = {
+ name: textProp.name,
+ value: "",
+ priority: ""
+ };
+ }
+
+ textProp.priority = cssProp.priority;
+ }
+ });
+ },
+
+ /**
+ * A helper for applyProperties that applies properties in the "as
+ * authored" case; that is, when the StyleRuleActor supports
+ * setRuleText.
+ */
+ _applyPropertiesAuthored: function (modifications) {
+ return modifications.apply().then(() => {
+ // The rewriting may have required some other property values to
+ // change, e.g., to insert some needed terminators. Update the
+ // relevant properties here.
+ for (let index in modifications.changedDeclarations) {
+ let newValue = modifications.changedDeclarations[index];
+ this.textProps[index].noticeNewValue(newValue);
+ }
+ // Recompute and redisplay the computed properties.
+ for (let prop of this.textProps) {
+ if (!prop.invisible && prop.enabled) {
+ prop.updateComputed();
+ prop.updateEditor();
+ }
+ }
+ });
+ },
+
+ /**
+ * Reapply all the properties in this rule, and update their
+ * computed styles. Will re-mark overridden properties. Sets the
+ * |_applyingModifications| property to a promise which will resolve
+ * when the edit has completed.
+ *
+ * @param {Function} modifier a function that takes a RuleModificationList
+ * (or RuleRewriter) as an argument and that modifies it
+ * to apply the desired edit
+ * @return {Promise} a promise which will resolve when the edit
+ * is complete
+ */
+ applyProperties: function (modifier) {
+ // If there is already a pending modification, we have to wait
+ // until it settles before applying the next modification.
+ let resultPromise =
+ promise.resolve(this._applyingModifications).then(() => {
+ let modifications = this.style.startModifyingProperties(
+ this.cssProperties);
+ modifier(modifications);
+ if (this.style.canSetRuleText) {
+ return this._applyPropertiesAuthored(modifications);
+ }
+ return this._applyPropertiesNoAuthored(modifications);
+ }).then(() => {
+ this.elementStyle.markOverriddenAll();
+
+ if (resultPromise === this._applyingModifications) {
+ this._applyingModifications = null;
+ this.elementStyle._changed();
+ }
+ }).catch(promiseWarn);
+
+ this._applyingModifications = resultPromise;
+ return resultPromise;
+ },
+
+ /**
+ * Renames a property.
+ *
+ * @param {TextProperty} property
+ * The property to rename.
+ * @param {String} name
+ * The new property name (such as "background" or "border-top").
+ */
+ setPropertyName: function (property, name) {
+ if (name === property.name) {
+ return;
+ }
+
+ let oldName = property.name;
+ property.name = name;
+ let index = this.textProps.indexOf(property);
+ this.applyProperties((modifications) => {
+ modifications.renameProperty(index, oldName, name);
+ });
+ },
+
+ /**
+ * Sets the value and priority of a property, then reapply all properties.
+ *
+ * @param {TextProperty} property
+ * The property to manipulate.
+ * @param {String} value
+ * The property's value (not including priority).
+ * @param {String} priority
+ * The property's priority (either "important" or an empty string).
+ */
+ setPropertyValue: function (property, value, priority) {
+ if (value === property.value && priority === property.priority) {
+ return;
+ }
+
+ property.value = value;
+ property.priority = priority;
+
+ let index = this.textProps.indexOf(property);
+ this.applyProperties((modifications) => {
+ modifications.setProperty(index, property.name, value, priority);
+ });
+ },
+
+ /**
+ * Just sets the value and priority of a property, in order to preview its
+ * effect on the content document.
+ *
+ * @param {TextProperty} property
+ * The property which value will be previewed
+ * @param {String} value
+ * The value to be used for the preview
+ * @param {String} priority
+ * The property's priority (either "important" or an empty string).
+ */
+ previewPropertyValue: function (property, value, priority) {
+ let modifications = this.style.startModifyingProperties(this.cssProperties);
+ modifications.setProperty(this.textProps.indexOf(property),
+ property.name, value, priority);
+ modifications.apply().then(() => {
+ // Ensure dispatching a ruleview-changed event
+ // also for previews
+ this.elementStyle._changed();
+ });
+ },
+
+ /**
+ * Disables or enables given TextProperty.
+ *
+ * @param {TextProperty} property
+ * The property to enable/disable
+ * @param {Boolean} value
+ */
+ setPropertyEnabled: function (property, value) {
+ if (property.enabled === !!value) {
+ return;
+ }
+ property.enabled = !!value;
+ let index = this.textProps.indexOf(property);
+ this.applyProperties((modifications) => {
+ modifications.setPropertyEnabled(index, property.name, property.enabled);
+ });
+ },
+
+ /**
+ * Remove a given TextProperty from the rule and update the rule
+ * accordingly.
+ *
+ * @param {TextProperty} property
+ * The property to be removed
+ */
+ removeProperty: function (property) {
+ let 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);
+ });
+ },
+
+ /**
+ * Get the list of TextProperties from the style. Needs
+ * to parse the style's authoredText.
+ */
+ _getTextProperties: function () {
+ let textProps = [];
+ let store = this.elementStyle.store;
+
+ // Starting with FF49, StyleRuleActors provide parsed declarations.
+ let props = this.style.declarations;
+ if (!props.length) {
+ props = parseDeclarations(this.cssProperties.isKnown,
+ this.style.authoredText, true);
+ }
+
+ for (let prop of props) {
+ let name = prop.name;
+ // If the authored text has an invalid property, it will show up
+ // as nameless. Skip these as we don't currently have a good
+ // way to display them.
+ if (!name) {
+ continue;
+ }
+ // In an inherited rule, we only show inherited properties.
+ // However, we must keep all properties in order for rule
+ // rewriting to work properly. So, compute the "invisible"
+ // property here.
+ let invisible = this.inherited && !this.cssProperties.isInherited(name);
+ let value = store.userProperties.getProperty(this.style, name,
+ prop.value);
+ let textProp = new TextProperty(this, name, value, prop.priority,
+ !("commentOffsets" in prop),
+ invisible);
+ textProps.push(textProp);
+ }
+
+ return textProps;
+ },
+
+ /**
+ * Return the list of disabled properties from the store for this rule.
+ */
+ _getDisabledProperties: function () {
+ let store = this.elementStyle.store;
+
+ // Include properties from the disabled property store, if any.
+ let disabledProps = store.disabled.get(this.style);
+ if (!disabledProps) {
+ return [];
+ }
+
+ let textProps = [];
+
+ for (let prop of disabledProps) {
+ let value = store.userProperties.getProperty(this.style, prop.name,
+ prop.value);
+ let textProp = new TextProperty(this, prop.name, value, prop.priority);
+ textProp.enabled = false;
+ textProps.push(textProp);
+ }
+
+ return textProps;
+ },
+
+ /**
+ * Reread the current state of the rules and rebuild text
+ * properties as needed.
+ */
+ refresh: function (options) {
+ this.matchedSelectors = options.matchedSelectors || [];
+ let newTextProps = this._getTextProperties();
+
+ // Update current properties for each property present on the style.
+ // This will mark any touched properties with _visited so we
+ // can detect properties that weren't touched (because they were
+ // removed from the style).
+ // Also keep track of properties that didn't exist in the current set
+ // of properties.
+ let brandNewProps = [];
+ for (let newProp of newTextProps) {
+ if (!this._updateTextProperty(newProp)) {
+ brandNewProps.push(newProp);
+ }
+ }
+
+ // Refresh editors and disabled state for all the properties that
+ // were updated.
+ for (let prop of this.textProps) {
+ // Properties that weren't touched during the update
+ // process must no longer exist on the node. Mark them disabled.
+ if (!prop._visited) {
+ prop.enabled = false;
+ prop.updateEditor();
+ } else {
+ delete prop._visited;
+ }
+ }
+
+ // Add brand new properties.
+ this.textProps = this.textProps.concat(brandNewProps);
+
+ // Refresh the editor if one already exists.
+ if (this.editor) {
+ this.editor.populate();
+ }
+ },
+
+ /**
+ * Update the current TextProperties that match a given property
+ * from the authoredText. Will choose one existing TextProperty to update
+ * with the new property's value, and will disable all others.
+ *
+ * When choosing the best match to reuse, properties will be chosen
+ * by assigning a rank and choosing the highest-ranked property:
+ * Name, value, and priority match, enabled. (6)
+ * Name, value, and priority match, disabled. (5)
+ * Name and value match, enabled. (4)
+ * Name and value match, disabled. (3)
+ * Name matches, enabled. (2)
+ * Name matches, disabled. (1)
+ *
+ * If no existing properties match the property, nothing happens.
+ *
+ * @param {TextProperty} newProp
+ * The current version of the property, as parsed from the
+ * authoredText in Rule._getTextProperties().
+ * @return {Boolean} true if a property was updated, false if no properties
+ * were updated.
+ */
+ _updateTextProperty: function (newProp) {
+ let match = { rank: 0, prop: null };
+
+ for (let prop of this.textProps) {
+ if (prop.name !== newProp.name) {
+ continue;
+ }
+
+ // Mark this property visited.
+ prop._visited = true;
+
+ // Start at rank 1 for matching name.
+ let rank = 1;
+
+ // Value and Priority matches add 2 to the rank.
+ // Being enabled adds 1. This ranks better matches higher,
+ // with priority breaking ties.
+ if (prop.value === newProp.value) {
+ rank += 2;
+ if (prop.priority === newProp.priority) {
+ rank += 2;
+ }
+ }
+
+ if (prop.enabled) {
+ rank += 1;
+ }
+
+ if (rank > match.rank) {
+ if (match.prop) {
+ // We outrank a previous match, disable it.
+ match.prop.enabled = false;
+ match.prop.updateEditor();
+ }
+ match.rank = rank;
+ match.prop = prop;
+ } else if (rank) {
+ // A previous match outranks us, disable ourself.
+ prop.enabled = false;
+ prop.updateEditor();
+ }
+ }
+
+ // If we found a match, update its value with the new text property
+ // value.
+ if (match.prop) {
+ match.prop.set(newProp);
+ return true;
+ }
+
+ return false;
+ },
+
+ /**
+ * Jump between editable properties in the UI. If the focus direction is
+ * forward, begin editing the next property name if available or focus the
+ * new property editor otherwise. If the focus direction is backward,
+ * begin editing the previous property value or focus the selector editor if
+ * this is the first element in the property list.
+ *
+ * @param {TextProperty} textProperty
+ * The text property that will be left to focus on a sibling.
+ * @param {Number} direction
+ * The move focus direction number.
+ */
+ editClosestTextProperty: function (textProperty, direction) {
+ let index = this.textProps.indexOf(textProperty);
+
+ if (direction === Services.focus.MOVEFOCUS_FORWARD) {
+ for (++index; index < this.textProps.length; ++index) {
+ if (!this.textProps[index].invisible) {
+ break;
+ }
+ }
+ if (index === this.textProps.length) {
+ textProperty.rule.editor.closeBrace.click();
+ } else {
+ this.textProps[index].editor.nameSpan.click();
+ }
+ } else if (direction === Services.focus.MOVEFOCUS_BACKWARD) {
+ for (--index; index >= 0; --index) {
+ if (!this.textProps[index].invisible) {
+ break;
+ }
+ }
+ if (index < 0) {
+ textProperty.editor.ruleEditor.selectorText.click();
+ } else {
+ this.textProps[index].editor.valueSpan.click();
+ }
+ }
+ },
+
+ /**
+ * Return a string representation of the rule.
+ */
+ stringifyRule: function () {
+ let selectorText = this.selectorText;
+ let cssText = "";
+ let terminator = Services.appinfo.OS === "WINNT" ? "\r\n" : "\n";
+
+ for (let textProp of this.textProps) {
+ if (!textProp.invisible) {
+ cssText += "\t" + textProp.stringifyProperty() + terminator;
+ }
+ }
+
+ return selectorText + " {" + terminator + cssText + "}";
+ },
+
+ /**
+ * See whether this rule has any non-invisible properties.
+ * @return {Boolean} true if there is any visible property, or false
+ * if all properties are invisible
+ */
+ hasAnyVisibleProperties: function () {
+ for (let prop of this.textProps) {
+ if (!prop.invisible) {
+ return true;
+ }
+ }
+ return false;
+ }
+};
+
+exports.Rule = Rule;
diff --git a/devtools/client/inspector/rules/models/text-property.js b/devtools/client/inspector/rules/models/text-property.js
new file mode 100644
index 000000000..3bbe6e91d
--- /dev/null
+++ b/devtools/client/inspector/rules/models/text-property.js
@@ -0,0 +1,215 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set 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 {escapeCSSComment} = require("devtools/shared/css/parsing-utils");
+const {getCssProperties} = require("devtools/shared/fronts/css-properties");
+
+/**
+ * TextProperty is responsible for the following:
+ * Manages a single property from the authoredText attribute of the
+ * relevant declaration.
+ * Maintains a list of computed properties that come from this
+ * property declaration.
+ * Changes to the TextProperty are sent to its related Rule for
+ * application.
+ *
+ * @param {Rule} rule
+ * The rule this TextProperty came from.
+ * @param {String} name
+ * The text property name (such as "background" or "border-top").
+ * @param {String} value
+ * The property's value (not including priority).
+ * @param {String} priority
+ * The property's priority (either "important" or an empty string).
+ * @param {Boolean} enabled
+ * Whether the property is enabled.
+ * @param {Boolean} invisible
+ * Whether the property is invisible. An invisible property
+ * does not show up in the UI; these are needed so that the
+ * index of a property in Rule.textProps is the same as the index
+ * coming from parseDeclarations.
+ */
+function TextProperty(rule, name, value, priority, enabled = true,
+ invisible = false) {
+ this.rule = rule;
+ this.name = name;
+ this.value = value;
+ this.priority = priority;
+ this.enabled = !!enabled;
+ this.invisible = invisible;
+ this.panelDoc = this.rule.elementStyle.ruleView.inspector.panelDoc;
+
+ const toolbox = this.rule.elementStyle.ruleView.inspector.toolbox;
+ this.cssProperties = getCssProperties(toolbox);
+
+ this.updateComputed();
+}
+
+TextProperty.prototype = {
+ /**
+ * Update the editor associated with this text property,
+ * if any.
+ */
+ updateEditor: function () {
+ if (this.editor) {
+ this.editor.update();
+ }
+ },
+
+ /**
+ * Update the list of computed properties for this text property.
+ */
+ updateComputed: function () {
+ if (!this.name) {
+ return;
+ }
+
+ // This is a bit funky. To get the list of computed properties
+ // for this text property, we'll set the property on a dummy element
+ // and see what the computed style looks like.
+ let dummyElement = this.rule.elementStyle.ruleView.dummyElement;
+ let dummyStyle = dummyElement.style;
+ dummyStyle.cssText = "";
+ dummyStyle.setProperty(this.name, this.value, this.priority);
+
+ this.computed = [];
+
+ // Manually get all the properties that are set when setting a value on
+ // this.name and check the computed style on dummyElement for each one.
+ // If we just read dummyStyle, it would skip properties when value === "".
+ let subProps = this.cssProperties.getSubproperties(this.name);
+
+ for (let prop of subProps) {
+ this.computed.push({
+ textProp: this,
+ name: prop,
+ value: dummyStyle.getPropertyValue(prop),
+ priority: dummyStyle.getPropertyPriority(prop),
+ });
+ }
+ },
+
+ /**
+ * Set all the values from another TextProperty instance into
+ * this TextProperty instance.
+ *
+ * @param {TextProperty} prop
+ * The other TextProperty instance.
+ */
+ set: function (prop) {
+ let changed = false;
+ for (let item of ["name", "value", "priority", "enabled"]) {
+ if (this[item] !== prop[item]) {
+ this[item] = prop[item];
+ changed = true;
+ }
+ }
+
+ if (changed) {
+ this.updateEditor();
+ }
+ },
+
+ setValue: function (value, priority, force = false) {
+ let store = this.rule.elementStyle.store;
+
+ if (this.editor && value !== this.editor.committed.value || force) {
+ store.userProperties.setProperty(this.rule.style, this.name, value);
+ }
+
+ 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.
+ */
+ noticeNewValue: function (value) {
+ if (value !== this.value) {
+ this.value = value;
+ this.updateEditor();
+ }
+ },
+
+ setName: function (name) {
+ let store = this.rule.elementStyle.store;
+
+ if (name !== this.name) {
+ store.userProperties.setProperty(this.rule.style, name,
+ this.editor.committed.value);
+ }
+
+ this.rule.setPropertyName(this, name);
+ this.updateEditor();
+ },
+
+ setEnabled: function (value) {
+ this.rule.setPropertyEnabled(this, value);
+ this.updateEditor();
+ },
+
+ remove: function () {
+ this.rule.removeProperty(this);
+ },
+
+ /**
+ * Return a string representation of the rule property.
+ */
+ stringifyProperty: function () {
+ // Get the displayed property value
+ let declaration = this.name + ": " + this.editor.valueSpan.textContent +
+ ";";
+
+ // Comment out property declarations that are not enabled
+ if (!this.enabled) {
+ declaration = "/* " + escapeCSSComment(declaration) + " */";
+ }
+
+ return declaration;
+ },
+
+ /**
+ * See whether this property's name is known.
+ *
+ * @return {Boolean} true if the property name is known, false otherwise.
+ */
+ isKnownProperty: function () {
+ return this.cssProperties.isKnown(this.name);
+ },
+
+ /**
+ * Validate this property. Does it make sense for this value to be assigned
+ * to this property name?
+ *
+ * @return {Boolean} true if the property value is valid, false otherwise.
+ */
+ isValid: function () {
+ // Starting with FF49, StyleRuleActors provide a list of parsed
+ // declarations, with data about their validity, but if we don't have this,
+ // compute validity locally (which might not be correct, but better than
+ // nothing).
+ if (!this.rule.domRule.declarations) {
+ return this.cssProperties.isValidOnClient(this.name, this.value, this.panelDoc);
+ }
+
+ let selfIndex = this.rule.textProps.indexOf(this);
+
+ // When adding a new property in the rule-view, the TextProperty object is
+ // created right away before the rule gets updated on the server, so we're
+ // not going to find the corresponding declaration object yet. Default to
+ // true.
+ if (!this.rule.domRule.declarations[selfIndex]) {
+ return true;
+ }
+
+ return this.rule.domRule.declarations[selfIndex].isValid;
+ }
+};
+
+exports.TextProperty = TextProperty;