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/. */
7 const { generateUUID } = require("resource://devtools/shared/generate-uuid.js");
9 COMPATIBILITY_TOOLTIP_MESSAGE,
10 } = require("resource://devtools/client/inspector/rules/constants.js");
12 loader.lazyRequireGetter(
15 "resource://devtools/shared/css/parsing-utils.js",
19 loader.lazyRequireGetter(
22 "resource://devtools/client/inspector/rules/utils/utils.js",
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
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.
54 constructor(rule, name, value, priority, enabled = true, invisible = false) {
55 this.id = name + "_" + generateUUID().toString();
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();
73 get computedProperties() {
75 .filter(computed => computed.name !== this.name)
78 isOverridden: computed.overridden,
80 priority: computed.priority,
81 value: computed.value,
87 * Returns whether or not the declaration's name is known.
89 * @return {Boolean} true if the declaration name is known, false otherwise.
91 get isKnownProperty() {
92 return this.cssProperties.isKnown(this.name);
96 * Returns whether or not the declaration is changed by the user.
98 * @return {Boolean} true if the declaration is changed by the user, false
101 get isPropertyChanged() {
102 return this.userProperties.contains(this.rule.domRule, this.name);
106 * Update the editor associated with this text property,
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;
115 this.editor.update();
120 * Update the list of computed properties for this text property.
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);
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) {
146 value: dummyStyle.getPropertyValue(prop),
147 priority: dummyStyle.getPropertyPriority(prop),
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.
156 updateUsedVariables() {
157 this.usedVariables.clear();
159 for (const variable of getCSSVariables(this.value)) {
160 this.usedVariables.add(variable);
165 * Set all the values from another TextProperty instance into
166 * this TextProperty instance.
168 * @param {TextProperty} prop
169 * The other TextProperty instance.
173 for (const item of ["name", "value", "priority", "enabled"]) {
174 if (this[item] !== prop[item]) {
175 this[item] = prop[item];
181 this.updateUsedVariables();
186 setValue(value, priority, force = false) {
187 if (value !== this.value || force) {
188 this.userProperties.setProperty(this.rule.domRule, this.name, value);
190 return this.rule.setPropertyValue(this, value, priority).then(() => {
191 this.updateUsedVariables();
197 * Called when the property's value has been updated externally, and
198 * the property and editor should update to reflect that value.
200 * @param {String} value
204 if (value !== this.value) {
206 this.updateUsedVariables();
211 async setName(name) {
212 if (name !== this.name) {
213 this.userProperties.setProperty(this.rule.domRule, name, this.value);
216 await this.rule.setPropertyName(this, name);
221 this.rule.setPropertyEnabled(this, value);
226 this.rule.removeProperty(this);
230 * Return a string representation of the rule property.
232 stringifyProperty() {
233 // Get the displayed property value
234 let declaration = this.name + ": " + this.value;
237 declaration += " !" + this.priority;
242 // Comment out property declarations that are not enabled
244 declaration = "/* " + escapeCSSComment(declaration) + " */";
251 * Returns the associated StyleRule declaration if it exists
253 * @returns {Object|undefined}
255 #getDomRuleDeclaration() {
256 const selfIndex = this.rule.textProps.indexOf(this);
257 return this.rule.domRule.declarations?.[selfIndex];
261 * Validate this property. Does it make sense for this value to be assigned
262 * to this property name?
264 * @return {Boolean} true if the whole CSS declaration is valid, false otherwise.
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
277 return declaration.isValid;
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 };
289 return declaration.isUsed;
293 * Get compatibility issue linked with the textProp.
295 * @returns A JSON objects with compatibility information in following form:
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
305 * // Link to MDN documentation for the rootProperty
307 * // An array of all the browsers that don't support the given CSS rule
308 * unsupportedBrowsers: <Array>,
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.
320 return { isCompatible: true };
323 const compatibilityIssues = await this.rule.getCompatibilityIssues();
324 if (!compatibilityIssues.length) {
325 return { isCompatible: true };
328 const property = this.name;
329 const indexOfProperty = compatibilityIssues.findIndex(
330 issue => issue.property === property || issue.aliases?.includes(property)
333 if (indexOfProperty < 0) {
334 return { isCompatible: true };
338 property: rootProperty,
344 } = compatibilityIssues[indexOfProperty];
346 let msgId = COMPATIBILITY_TOOLTIP_MESSAGE.default;
347 if (deprecated && experimental && !unsupportedBrowsers.length) {
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;
374 * Validate the name of this property.
376 * @return {Boolean} true if the property name is valid, false otherwise.
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
389 return declaration.isNameValid;
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.
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
409 return declaration.invalidAtComputedValueTime;
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.
416 * @return {String|null} The expected syntax, or null.
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
428 return declaration.syntax;
432 * Returns true if the property value is a CSS variables and contains the given variable
433 * name, and false otherwise.
436 * CSS variable name (e.g. "--color")
439 hasCSSVariable(name) {
440 return this.usedVariables.has(name);
444 module.exports = TextProperty;