summaryrefslogtreecommitdiffstats
path: root/devtools/client/inspector/rules/models/text-property.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--devtools/client/inspector/rules/models/text-property.js400
1 files changed, 400 insertions, 0 deletions
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 0000000000..d7568f74f3
--- /dev/null
+++ b/devtools/client/inspector/rules/models/text-property.js
@@ -0,0 +1,400 @@
+/* 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 { generateUUID } = require("resource://devtools/shared/generate-uuid.js");
+const {
+ COMPATIBILITY_TOOLTIP_MESSAGE,
+} = require("resource://devtools/client/inspector/rules/constants.js");
+
+loader.lazyRequireGetter(
+ this,
+ "escapeCSSComment",
+ "resource://devtools/shared/css/parsing-utils.js",
+ true
+);
+
+loader.lazyRequireGetter(
+ this,
+ "getCSSVariables",
+ "resource://devtools/client/inspector/rules/utils/utils.js",
+ true
+);
+
+/**
+ * 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.
+ */
+class TextProperty {
+ /**
+ * @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. In an inherited rule, only show
+ * the inherited declarations. The other declarations are considered
+ * invisible and 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.
+ */
+ constructor(rule, name, value, priority, enabled = true, invisible = false) {
+ this.id = name + "_" + generateUUID().toString();
+ this.rule = rule;
+ this.name = name;
+ this.value = value;
+ this.priority = priority;
+ this.enabled = !!enabled;
+ this.invisible = invisible;
+ this.elementStyle = this.rule.elementStyle;
+ this.cssProperties = this.elementStyle.ruleView.cssProperties;
+ this.panelDoc = this.elementStyle.ruleView.inspector.panelDoc;
+ this.userProperties = this.elementStyle.store.userProperties;
+ // Names of CSS variables used in the value of this declaration.
+ this.usedVariables = new Set();
+
+ this.updateComputed();
+ this.updateUsedVariables();
+ }
+
+ get computedProperties() {
+ return this.computed
+ .filter(computed => computed.name !== this.name)
+ .map(computed => {
+ return {
+ isOverridden: computed.overridden,
+ name: computed.name,
+ priority: computed.priority,
+ value: computed.value,
+ };
+ });
+ }
+
+ /**
+ * Returns whether or not the declaration's name is known.
+ *
+ * @return {Boolean} true if the declaration name is known, false otherwise.
+ */
+ get isKnownProperty() {
+ return this.cssProperties.isKnown(this.name);
+ }
+
+ /**
+ * Returns whether or not the declaration is changed by the user.
+ *
+ * @return {Boolean} true if the declaration is changed by the user, false
+ * otherwise.
+ */
+ get isPropertyChanged() {
+ return this.userProperties.contains(this.rule.domRule, this.name);
+ }
+
+ /**
+ * Update the editor associated with this text property,
+ * if any.
+ */
+ updateEditor() {
+ // When the editor updates, reset the saved
+ // compatibility issues list as any updates
+ // may alter the compatibility status of declarations
+ this.rule.compatibilityIssues = null;
+ if (this.editor) {
+ this.editor.update();
+ }
+ }
+
+ /**
+ * Update the list of computed properties for this text property.
+ */
+ updateComputed() {
+ 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.
+ const dummyElement = this.elementStyle.ruleView.dummyElement;
+ const 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 === "".
+ const subProps = this.cssProperties.getSubproperties(this.name);
+
+ for (const prop of subProps) {
+ this.computed.push({
+ textProp: this,
+ name: prop,
+ value: dummyStyle.getPropertyValue(prop),
+ priority: dummyStyle.getPropertyPriority(prop),
+ });
+ }
+ }
+
+ /**
+ * Extract all CSS variable names used in this declaration's value into a Set for
+ * easy querying. Call this method any time the declaration's value changes.
+ */
+ updateUsedVariables() {
+ this.usedVariables.clear();
+
+ for (const variable of getCSSVariables(this.value)) {
+ this.usedVariables.add(variable);
+ }
+ }
+
+ /**
+ * Set all the values from another TextProperty instance into
+ * this TextProperty instance.
+ *
+ * @param {TextProperty} prop
+ * The other TextProperty instance.
+ */
+ set(prop) {
+ let changed = false;
+ for (const item of ["name", "value", "priority", "enabled"]) {
+ if (this[item] !== prop[item]) {
+ this[item] = prop[item];
+ changed = true;
+ }
+ }
+
+ if (changed) {
+ this.updateUsedVariables();
+ this.updateEditor();
+ }
+ }
+
+ setValue(value, priority, force = false) {
+ if (value !== this.value || force) {
+ this.userProperties.setProperty(this.rule.domRule, this.name, value);
+ }
+ return this.rule.setPropertyValue(this, value, priority).then(() => {
+ this.updateUsedVariables();
+ this.updateEditor();
+ });
+ }
+
+ /**
+ * Called when the property's value has been updated externally, and
+ * the property and editor should update to reflect that value.
+ *
+ * @param {String} value
+ * Property value
+ */
+ updateValue(value) {
+ if (value !== this.value) {
+ this.value = value;
+ this.updateUsedVariables();
+ this.updateEditor();
+ }
+ }
+
+ async setName(name) {
+ if (name !== this.name) {
+ this.userProperties.setProperty(this.rule.domRule, name, this.value);
+ }
+
+ await this.rule.setPropertyName(this, name);
+ this.updateEditor();
+ }
+
+ setEnabled(value) {
+ this.rule.setPropertyEnabled(this, value);
+ this.updateEditor();
+ }
+
+ remove() {
+ this.rule.removeProperty(this);
+ }
+
+ /**
+ * Return a string representation of the rule property.
+ */
+ stringifyProperty() {
+ // Get the displayed property value
+ let declaration = this.name + ": " + this.value;
+
+ if (this.priority) {
+ declaration += " !" + this.priority;
+ }
+
+ declaration += ";";
+
+ // Comment out property declarations that are not enabled
+ if (!this.enabled) {
+ declaration = "/* " + escapeCSSComment(declaration) + " */";
+ }
+
+ return declaration;
+ }
+
+ /**
+ * Validate this property. Does it make sense for this value to be assigned
+ * to this property name?
+ *
+ * @return {Boolean} true if the whole CSS declaration is valid, false otherwise.
+ */
+ isValid() {
+ const 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;
+ }
+
+ isUsed() {
+ const selfIndex = this.rule.textProps.indexOf(this);
+ const declarations = this.rule.domRule.declarations;
+
+ // StyleRuleActor's declarations may have a isUsed flag (if the server is the right
+ // version). Just return true if the information is missing.
+ if (
+ !declarations ||
+ !declarations[selfIndex] ||
+ !declarations[selfIndex].isUsed
+ ) {
+ return { used: true };
+ }
+
+ return declarations[selfIndex].isUsed;
+ }
+
+ /**
+ * Get compatibility issue linked with the textProp.
+ *
+ * @returns A JSON objects with compatibility information in following form:
+ * {
+ * // A boolean to denote the compatibility status
+ * isCompatible: <boolean>,
+ * // The CSS declaration that has compatibility issues
+ * property: <string>,
+ * // The un-aliased root CSS declaration for the given property
+ * rootProperty: <string>,
+ * // The l10n message id for the tooltip message
+ * msgId: <string>,
+ * // Link to MDN documentation for the rootProperty
+ * url: <string>,
+ * // An array of all the browsers that don't support the given CSS rule
+ * unsupportedBrowsers: <Array>,
+ * }
+ */
+ async isCompatible() {
+ // This is a workaround for Bug 1648339
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1648339
+ // that makes the tooltip icon inconsistent with the
+ // position of the rule it is associated with. Once solved,
+ // the compatibility data can be directly accessed from the
+ // declaration and this logic can be used to set isCompatible
+ // property directly to domRule in StyleRuleActor's form() method.
+ if (!this.enabled) {
+ return { isCompatible: true };
+ }
+
+ const compatibilityIssues = await this.rule.getCompatibilityIssues();
+ if (!compatibilityIssues.length) {
+ return { isCompatible: true };
+ }
+
+ const property = this.name;
+ const indexOfProperty = compatibilityIssues.findIndex(
+ issue => issue.property === property || issue.aliases?.includes(property)
+ );
+
+ if (indexOfProperty < 0) {
+ return { isCompatible: true };
+ }
+
+ const {
+ property: rootProperty,
+ deprecated,
+ experimental,
+ specUrl,
+ url,
+ unsupportedBrowsers,
+ } = compatibilityIssues[indexOfProperty];
+
+ let msgId = COMPATIBILITY_TOOLTIP_MESSAGE.default;
+ if (deprecated && experimental && !unsupportedBrowsers.length) {
+ msgId =
+ COMPATIBILITY_TOOLTIP_MESSAGE["deprecated-experimental-supported"];
+ } else if (deprecated && experimental) {
+ msgId = COMPATIBILITY_TOOLTIP_MESSAGE["deprecated-experimental"];
+ } else if (deprecated && !unsupportedBrowsers.length) {
+ msgId = COMPATIBILITY_TOOLTIP_MESSAGE["deprecated-supported"];
+ } else if (deprecated) {
+ msgId = COMPATIBILITY_TOOLTIP_MESSAGE.deprecated;
+ } else if (experimental && !unsupportedBrowsers.length) {
+ msgId = COMPATIBILITY_TOOLTIP_MESSAGE["experimental-supported"];
+ } else if (experimental) {
+ msgId = COMPATIBILITY_TOOLTIP_MESSAGE.experimental;
+ }
+
+ return {
+ isCompatible: false,
+ property,
+ rootProperty,
+ msgId,
+ specUrl,
+ url,
+ unsupportedBrowsers,
+ };
+ }
+
+ /**
+ * Validate the name of this property.
+ *
+ * @return {Boolean} true if the property name is valid, false otherwise.
+ */
+ isNameValid() {
+ const 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].isNameValid;
+ }
+
+ /**
+ * Returns true if the property value is a CSS variables and contains the given variable
+ * name, and false otherwise.
+ *
+ * @param {String}
+ * CSS variable name (e.g. "--color")
+ * @return {Boolean}
+ */
+ hasCSSVariable(name) {
+ return this.usedVariables.has(name);
+ }
+}
+
+module.exports = TextProperty;