mirror of
https://github.com/roytam1/UXP.git
synced 2026-06-10 18:38:34 +00:00
413 lines
12 KiB
JavaScript
413 lines
12 KiB
JavaScript
/* -*- 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;
|