summaryrefslogtreecommitdiffstats
path: root/devtools/client/inspector/rules/models/element-style.js
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/element-style.js
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/element-style.js')
-rw-r--r--devtools/client/inspector/rules/models/element-style.js412
1 files changed, 412 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;