diff options
Diffstat (limited to 'devtools/client/inspector/rules/models/element-style.js')
-rw-r--r-- | devtools/client/inspector/rules/models/element-style.js | 412 |
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; |