diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /devtools/client/inspector/rules/models | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-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.js | 412 | ||||
-rw-r--r-- | devtools/client/inspector/rules/models/moz.build | 11 | ||||
-rw-r--r-- | devtools/client/inspector/rules/models/rule.js | 686 | ||||
-rw-r--r-- | devtools/client/inspector/rules/models/text-property.js | 215 |
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; |