Bug 1531230 - Stop using the TextPropertyEditor value in the TextProperty model....
[gecko.git] / devtools / client / inspector / rules / models / text-property.js
blobfa58642624ae015d179ad8ebeeb2749ba543c516
1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
2 /* vim: set ts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 "use strict";
9 const { generateUUID } = require("devtools/shared/generate-uuid");
11 loader.lazyRequireGetter(this, "escapeCSSComment", "devtools/shared/css/parsing-utils", true);
13 /**
14  * TextProperty is responsible for the following:
15  *   Manages a single property from the authoredText attribute of the
16  *     relevant declaration.
17  *   Maintains a list of computed properties that come from this
18  *     property declaration.
19  *   Changes to the TextProperty are sent to its related Rule for
20  *     application.
21  */
22 class TextProperty {
23   /**
24    * @param {Rule} rule
25    *        The rule this TextProperty came from.
26    * @param {String} name
27    *        The text property name (such as "background" or "border-top").
28    * @param {String} value
29    *        The property's value (not including priority).
30    * @param {String} priority
31    *        The property's priority (either "important" or an empty string).
32    * @param {Boolean} enabled
33    *        Whether the property is enabled.
34    * @param {Boolean} invisible
35    *        Whether the property is invisible. In an inherited rule, only show
36    *        the inherited declarations. The other declarations are considered
37    *        invisible and does not show up in the UI. These are needed so that
38    *        the index of a property in Rule.textProps is the same as the index
39    *        coming from parseDeclarations.
40    */
41   constructor(rule, name, value, priority, enabled = true, invisible = false) {
42     this.id = name + "_" + generateUUID().toString();
43     this.rule = rule;
44     this.name = name;
45     this.value = value;
46     this.priority = priority;
47     this.enabled = !!enabled;
48     this.invisible = invisible;
49     this.cssProperties = this.rule.elementStyle.ruleView.cssProperties;
50     this.panelDoc = this.rule.elementStyle.ruleView.inspector.panelDoc;
52     this.updateComputed();
53   }
55   get computedProperties() {
56     return this.computed
57       .filter(computed => computed.name !== this.name)
58       .map(computed => {
59         return {
60           isOverridden: computed.overridden,
61           name: computed.name,
62           priority: computed.priority,
63           value: computed.value,
64         };
65       });
66   }
68   /**
69    * See whether this property's name is known.
70    *
71    * @return {Boolean} true if the property name is known, false otherwise.
72    */
73   get isKnownProperty() {
74     return this.cssProperties.isKnown(this.name);
75   }
77   /**
78    * Update the editor associated with this text property,
79    * if any.
80    */
81   updateEditor() {
82     if (this.editor) {
83       this.editor.update();
84     }
85   }
87   /**
88    * Update the list of computed properties for this text property.
89    */
90   updateComputed() {
91     if (!this.name) {
92       return;
93     }
95     // This is a bit funky.  To get the list of computed properties
96     // for this text property, we'll set the property on a dummy element
97     // and see what the computed style looks like.
98     const dummyElement = this.rule.elementStyle.ruleView.dummyElement;
99     const dummyStyle = dummyElement.style;
100     dummyStyle.cssText = "";
101     dummyStyle.setProperty(this.name, this.value, this.priority);
103     this.computed = [];
105     // Manually get all the properties that are set when setting a value on
106     // this.name and check the computed style on dummyElement for each one.
107     // If we just read dummyStyle, it would skip properties when value === "".
108     const subProps = this.cssProperties.getSubproperties(this.name);
110     for (const prop of subProps) {
111       this.computed.push({
112         textProp: this,
113         name: prop,
114         value: dummyStyle.getPropertyValue(prop),
115         priority: dummyStyle.getPropertyPriority(prop),
116       });
117     }
118   }
120   /**
121    * Set all the values from another TextProperty instance into
122    * this TextProperty instance.
123    *
124    * @param {TextProperty} prop
125    *        The other TextProperty instance.
126    */
127   set(prop) {
128     let changed = false;
129     for (const item of ["name", "value", "priority", "enabled"]) {
130       if (this[item] !== prop[item]) {
131         this[item] = prop[item];
132         changed = true;
133       }
134     }
136     if (changed) {
137       this.updateEditor();
138     }
139   }
141   setValue(value, priority, force = false) {
142     const store = this.rule.elementStyle.store;
144     if (value !== this.value || force) {
145       store.userProperties.setProperty(this.rule.domRule, this.name, value);
146     }
148     return this.rule.setPropertyValue(this, value, priority)
149       .then(() => this.updateEditor());
150   }
152   /**
153    * Called when the property's value has been updated externally, and
154    * the property and editor should update to reflect that value.
155    *
156    * @param {String} value
157    *        Property value
158    */
159   updateValue(value) {
160     if (value !== this.value) {
161       this.value = value;
162       this.updateEditor();
163     }
164   }
166   async setName(name) {
167     if (name !== this.name) {
168       const store = this.rule.elementStyle.store;
169       store.userProperties.setProperty(this.rule.domRule, name, this.value);
170     }
172     await this.rule.setPropertyName(this, name);
173     this.updateEditor();
174   }
176   setEnabled(value) {
177     this.rule.setPropertyEnabled(this, value);
178     this.updateEditor();
179   }
181   remove() {
182     this.rule.removeProperty(this);
183   }
185   /**
186    * Return a string representation of the rule property.
187    */
188   stringifyProperty() {
189     // Get the displayed property value
190     let declaration = this.name + ": " + this.value;
192     if (this.priority) {
193       declaration += " !" + this.priority;
194     }
196     declaration += ";";
198     // Comment out property declarations that are not enabled
199     if (!this.enabled) {
200       declaration = "/* " + escapeCSSComment(declaration) + " */";
201     }
203     return declaration;
204   }
206   /**
207    * Validate this property. Does it make sense for this value to be assigned
208    * to this property name?
209    *
210    * @return {Boolean} true if the whole CSS declaration is valid, false otherwise.
211    */
212   isValid() {
213     const selfIndex = this.rule.textProps.indexOf(this);
215     // When adding a new property in the rule-view, the TextProperty object is
216     // created right away before the rule gets updated on the server, so we're
217     // not going to find the corresponding declaration object yet. Default to
218     // true.
219     if (!this.rule.domRule.declarations[selfIndex]) {
220       return true;
221     }
223     return this.rule.domRule.declarations[selfIndex].isValid;
224   }
226   /**
227    * Validate the name of this property.
228    *
229    * @return {Boolean} true if the property name is valid, false otherwise.
230    */
231   isNameValid() {
232     const selfIndex = this.rule.textProps.indexOf(this);
234     // When adding a new property in the rule-view, the TextProperty object is
235     // created right away before the rule gets updated on the server, so we're
236     // not going to find the corresponding declaration object yet. Default to
237     // true.
238     if (!this.rule.domRule.declarations[selfIndex]) {
239       return true;
240     }
242     // Starting with FF61, StyleRuleActor provides an accessor to signal if the property
243     // name is valid. If we don't have this, assume the name is valid. In use, rely on
244     // isValid() as a guard against false positives.
245     return (this.rule.domRule.declarations[selfIndex].isNameValid !== undefined)
246       ? this.rule.domRule.declarations[selfIndex].isNameValid
247       : true;
248   }
251 module.exports = TextProperty;