Backed out changeset e7acb4e12051 (bug 1893666) for causing xpcshell failures on...
[gecko.git] / devtools / client / inspector / rules / models / text-property.js
blobd3861759c78a9720c1014b1f5627d7181408da02
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 "use strict";
7 const { generateUUID } = require("resource://devtools/shared/generate-uuid.js");
8 const {
9   COMPATIBILITY_TOOLTIP_MESSAGE,
10 } = require("resource://devtools/client/inspector/rules/constants.js");
12 loader.lazyRequireGetter(
13   this,
14   "escapeCSSComment",
15   "resource://devtools/shared/css/parsing-utils.js",
16   true
19 loader.lazyRequireGetter(
20   this,
21   "getCSSVariables",
22   "resource://devtools/client/inspector/rules/utils/utils.js",
23   true
26 /**
27  * TextProperty is responsible for the following:
28  *   Manages a single property from the authoredText attribute of the
29  *     relevant declaration.
30  *   Maintains a list of computed properties that come from this
31  *     property declaration.
32  *   Changes to the TextProperty are sent to its related Rule for
33  *     application.
34  */
35 class TextProperty {
36   /**
37    * @param {Rule} rule
38    *        The rule this TextProperty came from.
39    * @param {String} name
40    *        The text property name (such as "background" or "border-top").
41    * @param {String} value
42    *        The property's value (not including priority).
43    * @param {String} priority
44    *        The property's priority (either "important" or an empty string).
45    * @param {Boolean} enabled
46    *        Whether the property is enabled.
47    * @param {Boolean} invisible
48    *        Whether the property is invisible. In an inherited rule, only show
49    *        the inherited declarations. The other declarations are considered
50    *        invisible and does not show up in the UI. These are needed so that
51    *        the index of a property in Rule.textProps is the same as the index
52    *        coming from parseDeclarations.
53    */
54   constructor(rule, name, value, priority, enabled = true, invisible = false) {
55     this.id = name + "_" + generateUUID().toString();
56     this.rule = rule;
57     this.name = name;
58     this.value = value;
59     this.priority = priority;
60     this.enabled = !!enabled;
61     this.invisible = invisible;
62     this.elementStyle = this.rule.elementStyle;
63     this.cssProperties = this.elementStyle.ruleView.cssProperties;
64     this.panelDoc = this.elementStyle.ruleView.inspector.panelDoc;
65     this.userProperties = this.elementStyle.store.userProperties;
66     // Names of CSS variables used in the value of this declaration.
67     this.usedVariables = new Set();
69     this.updateComputed();
70     this.updateUsedVariables();
71   }
73   get computedProperties() {
74     return this.computed
75       .filter(computed => computed.name !== this.name)
76       .map(computed => {
77         return {
78           isOverridden: computed.overridden,
79           name: computed.name,
80           priority: computed.priority,
81           value: computed.value,
82         };
83       });
84   }
86   /**
87    * Returns whether or not the declaration's name is known.
88    *
89    * @return {Boolean} true if the declaration name is known, false otherwise.
90    */
91   get isKnownProperty() {
92     return this.cssProperties.isKnown(this.name);
93   }
95   /**
96    * Returns whether or not the declaration is changed by the user.
97    *
98    * @return {Boolean} true if the declaration is changed by the user, false
99    * otherwise.
100    */
101   get isPropertyChanged() {
102     return this.userProperties.contains(this.rule.domRule, this.name);
103   }
105   /**
106    * Update the editor associated with this text property,
107    * if any.
108    */
109   updateEditor() {
110     // When the editor updates, reset the saved
111     // compatibility issues list as any updates
112     // may alter the compatibility status of declarations
113     this.rule.compatibilityIssues = null;
114     if (this.editor) {
115       this.editor.update();
116     }
117   }
119   /**
120    * Update the list of computed properties for this text property.
121    */
122   updateComputed() {
123     if (!this.name) {
124       return;
125     }
127     // This is a bit funky.  To get the list of computed properties
128     // for this text property, we'll set the property on a dummy element
129     // and see what the computed style looks like.
130     const dummyElement = this.elementStyle.ruleView.dummyElement;
131     const dummyStyle = dummyElement.style;
132     dummyStyle.cssText = "";
133     dummyStyle.setProperty(this.name, this.value, this.priority);
135     this.computed = [];
137     // Manually get all the properties that are set when setting a value on
138     // this.name and check the computed style on dummyElement for each one.
139     // If we just read dummyStyle, it would skip properties when value === "".
140     const subProps = this.cssProperties.getSubproperties(this.name);
142     for (const prop of subProps) {
143       this.computed.push({
144         textProp: this,
145         name: prop,
146         value: dummyStyle.getPropertyValue(prop),
147         priority: dummyStyle.getPropertyPriority(prop),
148       });
149     }
150   }
152   /**
153    * Extract all CSS variable names used in this declaration's value into a Set for
154    * easy querying. Call this method any time the declaration's value changes.
155    */
156   updateUsedVariables() {
157     this.usedVariables.clear();
159     for (const variable of getCSSVariables(this.value)) {
160       this.usedVariables.add(variable);
161     }
162   }
164   /**
165    * Set all the values from another TextProperty instance into
166    * this TextProperty instance.
167    *
168    * @param {TextProperty} prop
169    *        The other TextProperty instance.
170    */
171   set(prop) {
172     let changed = false;
173     for (const item of ["name", "value", "priority", "enabled"]) {
174       if (this[item] !== prop[item]) {
175         this[item] = prop[item];
176         changed = true;
177       }
178     }
180     if (changed) {
181       this.updateUsedVariables();
182       this.updateEditor();
183     }
184   }
186   setValue(value, priority, force = false) {
187     if (value !== this.value || force) {
188       this.userProperties.setProperty(this.rule.domRule, this.name, value);
189     }
190     return this.rule.setPropertyValue(this, value, priority).then(() => {
191       this.updateUsedVariables();
192       this.updateEditor();
193     });
194   }
196   /**
197    * Called when the property's value has been updated externally, and
198    * the property and editor should update to reflect that value.
199    *
200    * @param {String} value
201    *        Property value
202    */
203   updateValue(value) {
204     if (value !== this.value) {
205       this.value = value;
206       this.updateUsedVariables();
207       this.updateEditor();
208     }
209   }
211   async setName(name) {
212     if (name !== this.name) {
213       this.userProperties.setProperty(this.rule.domRule, name, this.value);
214     }
216     await this.rule.setPropertyName(this, name);
217     this.updateEditor();
218   }
220   setEnabled(value) {
221     this.rule.setPropertyEnabled(this, value);
222     this.updateEditor();
223   }
225   remove() {
226     this.rule.removeProperty(this);
227   }
229   /**
230    * Return a string representation of the rule property.
231    */
232   stringifyProperty() {
233     // Get the displayed property value
234     let declaration = this.name + ": " + this.value;
236     if (this.priority) {
237       declaration += " !" + this.priority;
238     }
240     declaration += ";";
242     // Comment out property declarations that are not enabled
243     if (!this.enabled) {
244       declaration = "/* " + escapeCSSComment(declaration) + " */";
245     }
247     return declaration;
248   }
250   /**
251    * Returns the associated StyleRule declaration if it exists
252    *
253    * @returns {Object|undefined}
254    */
255   #getDomRuleDeclaration() {
256     const selfIndex = this.rule.textProps.indexOf(this);
257     return this.rule.domRule.declarations?.[selfIndex];
258   }
260   /**
261    * Validate this property. Does it make sense for this value to be assigned
262    * to this property name?
263    *
264    * @return {Boolean} true if the whole CSS declaration is valid, false otherwise.
265    */
266   isValid() {
267     const declaration = this.#getDomRuleDeclaration();
269     // When adding a new property in the rule-view, the TextProperty object is
270     // created right away before the rule gets updated on the server, so we're
271     // not going to find the corresponding declaration object yet. Default to
272     // true.
273     if (!declaration) {
274       return true;
275     }
277     return declaration.isValid;
278   }
280   isUsed() {
281     const declaration = this.#getDomRuleDeclaration();
283     // StyleRuleActor's declarations may have a isUsed flag (if the server is the right
284     // version). Just return true if the information is missing.
285     if (!declaration?.isUsed) {
286       return { used: true };
287     }
289     return declaration.isUsed;
290   }
292   /**
293    * Get compatibility issue linked with the textProp.
294    *
295    * @returns  A JSON objects with compatibility information in following form:
296    *    {
297    *      // A boolean to denote the compatibility status
298    *      isCompatible: <boolean>,
299    *      // The CSS declaration that has compatibility issues
300    *      property: <string>,
301    *      // The un-aliased root CSS declaration for the given property
302    *      rootProperty: <string>,
303    *      // The l10n message id for the tooltip message
304    *      msgId: <string>,
305    *      // Link to MDN documentation for the rootProperty
306    *      url: <string>,
307    *      // An array of all the browsers that don't support the given CSS rule
308    *      unsupportedBrowsers: <Array>,
309    *    }
310    */
311   async isCompatible() {
312     // This is a workaround for Bug 1648339
313     // https://bugzilla.mozilla.org/show_bug.cgi?id=1648339
314     // that makes the tooltip icon inconsistent with the
315     // position of the rule it is associated with. Once solved,
316     // the compatibility data can be directly accessed from the
317     // declaration and this logic can be used to set isCompatible
318     // property directly to domRule in StyleRuleActor's form() method.
319     if (!this.enabled) {
320       return { isCompatible: true };
321     }
323     const compatibilityIssues = await this.rule.getCompatibilityIssues();
324     if (!compatibilityIssues.length) {
325       return { isCompatible: true };
326     }
328     const property = this.name;
329     const indexOfProperty = compatibilityIssues.findIndex(
330       issue => issue.property === property || issue.aliases?.includes(property)
331     );
333     if (indexOfProperty < 0) {
334       return { isCompatible: true };
335     }
337     const {
338       property: rootProperty,
339       deprecated,
340       experimental,
341       specUrl,
342       url,
343       unsupportedBrowsers,
344     } = compatibilityIssues[indexOfProperty];
346     let msgId = COMPATIBILITY_TOOLTIP_MESSAGE.default;
347     if (deprecated && experimental && !unsupportedBrowsers.length) {
348       msgId =
349         COMPATIBILITY_TOOLTIP_MESSAGE["deprecated-experimental-supported"];
350     } else if (deprecated && experimental) {
351       msgId = COMPATIBILITY_TOOLTIP_MESSAGE["deprecated-experimental"];
352     } else if (deprecated && !unsupportedBrowsers.length) {
353       msgId = COMPATIBILITY_TOOLTIP_MESSAGE["deprecated-supported"];
354     } else if (deprecated) {
355       msgId = COMPATIBILITY_TOOLTIP_MESSAGE.deprecated;
356     } else if (experimental && !unsupportedBrowsers.length) {
357       msgId = COMPATIBILITY_TOOLTIP_MESSAGE["experimental-supported"];
358     } else if (experimental) {
359       msgId = COMPATIBILITY_TOOLTIP_MESSAGE.experimental;
360     }
362     return {
363       isCompatible: false,
364       property,
365       rootProperty,
366       msgId,
367       specUrl,
368       url,
369       unsupportedBrowsers,
370     };
371   }
373   /**
374    * Validate the name of this property.
375    *
376    * @return {Boolean} true if the property name is valid, false otherwise.
377    */
378   isNameValid() {
379     const declaration = this.#getDomRuleDeclaration();
381     // When adding a new property in the rule-view, the TextProperty object is
382     // created right away before the rule gets updated on the server, so we're
383     // not going to find the corresponding declaration object yet. Default to
384     // true.
385     if (!declaration) {
386       return true;
387     }
389     return declaration.isNameValid;
390   }
392   /**
393    * Returns whether the property is invalid at computed-value time.
394    * For now, it's only computed on the server for declarations of
395    * registered properties.
396    *
397    * @return {Boolean}
398    */
399   isInvalidAtComputedValueTime() {
400     const declaration = this.#getDomRuleDeclaration();
401     // When adding a new property in the rule-view, the TextProperty object is
402     // created right away before the rule gets updated on the server, so we're
403     // not going to find the corresponding declaration object yet. Default to
404     // false.
405     if (!declaration) {
406       return false;
407     }
409     return declaration.invalidAtComputedValueTime;
410   }
412   /**
413    * Returns the expected syntax for this property.
414    * For now, it's only sent from the server for invalid at computed-value time declarations.
415    *
416    * @return {String|null} The expected syntax, or null.
417    */
418   getExpectedSyntax() {
419     const declaration = this.#getDomRuleDeclaration();
420     // When adding a new property in the rule-view, the TextProperty object is
421     // created right away before the rule gets updated on the server, so we're
422     // not going to find the corresponding declaration object yet. Default to
423     // null.
424     if (!declaration) {
425       return null;
426     }
428     return declaration.syntax;
429   }
431   /**
432    * Returns true if the property value is a CSS variables and contains the given variable
433    * name, and false otherwise.
434    *
435    * @param {String}
436    *        CSS variable name (e.g. "--color")
437    * @return {Boolean}
438    */
439   hasCSSVariable(name) {
440     return this.usedVariables.has(name);
441   }
444 module.exports = TextProperty;