Web Inspector: Styles Redesign: Pasting multiple properties should create properties...
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Models / CSSProperty.js
1 /*
2  * Copyright (C) 2013 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WI.CSSProperty = class CSSProperty extends WI.Object
27 {
28     constructor(index, text, name, value, priority, enabled, overridden, implicit, anonymous, valid, styleSheetTextRange)
29     {
30         super();
31
32         this._ownerStyle = null;
33         this._index = index;
34
35         this.update(text, name, value, priority, enabled, overridden, implicit, anonymous, valid, styleSheetTextRange, true);
36     }
37
38     // Static
39
40     static isInheritedPropertyName(name)
41     {
42         console.assert(typeof name === "string");
43         if (name in WI.CSSKeywordCompletions.InheritedProperties)
44             return true;
45         // Check if the name is a CSS variable.
46         return name.startsWith("--");
47     }
48
49     // Public
50
51     get ownerStyle()
52     {
53         return this._ownerStyle;
54     }
55
56     set ownerStyle(ownerStyle)
57     {
58         this._ownerStyle = ownerStyle || null;
59     }
60
61     get index()
62     {
63         return this._index;
64     }
65
66     set index(index)
67     {
68         this._index = index;
69     }
70
71     update(text, name, value, priority, enabled, overridden, implicit, anonymous, valid, styleSheetTextRange, dontFireEvents)
72     {
73         // Locked CSSProperty can still be updated from the back-end when the text matches.
74         // We need to do this to keep attributes such as valid and overridden up to date.
75         if (this._ownerStyle && this._ownerStyle.locked && text !== this._text)
76             return;
77
78         text = text || "";
79         name = name || "";
80         value = value || "";
81         priority = priority || "";
82         enabled = enabled || false;
83         overridden = overridden || false;
84         implicit = implicit || false;
85         anonymous = anonymous || false;
86         valid = valid || false;
87
88         var changed = false;
89
90         if (!dontFireEvents) {
91             changed = this._name !== name || this._rawValue !== value || this._priority !== priority ||
92                 this._enabled !== enabled || this._implicit !== implicit || this._anonymous !== anonymous || this._valid !== valid;
93         }
94
95         // Use the setter for overridden if we want to fire events since the
96         // OverriddenStatusChanged event coalesces changes before it fires.
97         if (!dontFireEvents)
98             this.overridden = overridden;
99         else
100             this._overridden = overridden;
101
102         this._text = text;
103         this._name = name;
104         this._rawValue = value;
105         this._priority = priority;
106         this._enabled = enabled;
107         this._implicit = implicit;
108         this._anonymous = anonymous;
109         this._inherited = WI.CSSProperty.isInheritedPropertyName(name);
110         this._valid = valid;
111         this._variable = name.startsWith("--");
112         this._styleSheetTextRange = styleSheetTextRange || null;
113
114         this._relatedShorthandProperty = null;
115         this._relatedLonghandProperties = [];
116
117         // Clear computed properties.
118         delete this._styleDeclarationTextRange;
119         delete this._canonicalName;
120         delete this._hasOtherVendorNameOrKeyword;
121
122         if (changed)
123             this.dispatchEventToListeners(WI.CSSProperty.Event.Changed);
124     }
125
126     remove()
127     {
128         // Setting name or value to an empty string removes the entire CSSProperty.
129         this._name = "";
130         const forceRemove = true;
131         this._updateStyleText(forceRemove);
132     }
133
134     replaceWithText(text)
135     {
136         this._updateOwnerStyleText(this._text, text, true);
137     }
138
139     commentOut(disabled)
140     {
141         console.assert(this._enabled === disabled, "CSS property is already " + (disabled ? "disabled" : "enabled"));
142         if (this._enabled === !disabled)
143             return;
144
145         this._enabled = !disabled;
146
147         if (disabled)
148             this.text = "/* " + this._text + " */";
149         else
150             this.text = this._text.slice(2, -2).trim();
151     }
152
153     get synthesizedText()
154     {
155         var name = this.name;
156         if (!name)
157             return "";
158
159         var priority = this.priority;
160         return name + ": " + this.value.trim() + (priority ? " !" + priority : "") + ";";
161     }
162
163     get text()
164     {
165         return this._text || this.synthesizedText;
166     }
167
168     set text(newText)
169     {
170         if (this._text === newText)
171             return;
172
173         this._updateOwnerStyleText(this._text, newText);
174         this._text = newText;
175     }
176
177     get name()
178     {
179         return this._name;
180     }
181
182     set name(name)
183     {
184         if (name === this._name)
185             return;
186
187         this._name = name;
188         this._updateStyleText();
189     }
190
191     get canonicalName()
192     {
193         if (this._canonicalName)
194             return this._canonicalName;
195
196         this._canonicalName = WI.cssStyleManager.canonicalNameForPropertyName(this.name);
197
198         return this._canonicalName;
199     }
200
201     // FIXME: Remove current value getter and rename rawValue to value once the old styles sidebar is removed.
202     get value()
203     {
204         if (!this._value)
205             this._value = this._rawValue.replace(/\s*!important\s*$/, "");
206
207         return this._value;
208     }
209
210     get rawValue()
211     {
212         return this._rawValue;
213     }
214
215     set rawValue(value)
216     {
217         if (value === this._rawValue)
218             return;
219
220         this._rawValue = value;
221         this._value = undefined;
222         this._updateStyleText();
223     }
224
225     get important()
226     {
227         return this.priority === "important";
228     }
229
230     get priority() { return this._priority; }
231
232     get attached()
233     {
234         return this._enabled && this._ownerStyle && (!isNaN(this._index) || this._ownerStyle.type === WI.CSSStyleDeclaration.Type.Computed);
235     }
236
237     // Only commented out properties are disabled.
238     get enabled() { return this._enabled; }
239
240     get overridden() { return this._overridden; }
241     set overridden(overridden)
242     {
243         overridden = overridden || false;
244
245         if (this._overridden === overridden)
246             return;
247
248         var previousOverridden = this._overridden;
249
250         this._overridden = overridden;
251
252         if (this._overriddenStatusChangedTimeout)
253             return;
254
255         function delayed()
256         {
257             delete this._overriddenStatusChangedTimeout;
258
259             if (this._overridden === previousOverridden)
260                 return;
261
262             this.dispatchEventToListeners(WI.CSSProperty.Event.OverriddenStatusChanged);
263         }
264
265         this._overriddenStatusChangedTimeout = setTimeout(delayed.bind(this), 0);
266     }
267
268     get implicit() { return this._implicit; }
269     set implicit(implicit) { this._implicit = implicit; }
270
271     get anonymous() { return this._anonymous; }
272     get inherited() { return this._inherited; }
273     get valid() { return this._valid; }
274     get variable() { return this._variable; }
275     get styleSheetTextRange() { return this._styleSheetTextRange; }
276
277     get editable()
278     {
279         return !!(this._styleSheetTextRange && this._ownerStyle && this._ownerStyle.styleSheetTextRange);
280     }
281
282     get styleDeclarationTextRange()
283     {
284         if ("_styleDeclarationTextRange" in this)
285             return this._styleDeclarationTextRange;
286
287         if (!this._ownerStyle || !this._styleSheetTextRange)
288             return null;
289
290         var styleTextRange = this._ownerStyle.styleSheetTextRange;
291         if (!styleTextRange)
292             return null;
293
294         var startLine = this._styleSheetTextRange.startLine - styleTextRange.startLine;
295         var endLine = this._styleSheetTextRange.endLine - styleTextRange.startLine;
296
297         var startColumn = this._styleSheetTextRange.startColumn;
298         if (!startLine)
299             startColumn -= styleTextRange.startColumn;
300
301         var endColumn = this._styleSheetTextRange.endColumn;
302         if (!endLine)
303             endColumn -= styleTextRange.startColumn;
304
305         this._styleDeclarationTextRange = new WI.TextRange(startLine, startColumn, endLine, endColumn);
306
307         return this._styleDeclarationTextRange;
308     }
309
310     get relatedShorthandProperty() { return this._relatedShorthandProperty; }
311     set relatedShorthandProperty(property)
312     {
313         this._relatedShorthandProperty = property || null;
314     }
315
316     get relatedLonghandProperties() { return this._relatedLonghandProperties; }
317
318     addRelatedLonghandProperty(property)
319     {
320         this._relatedLonghandProperties.push(property);
321     }
322
323     clearRelatedLonghandProperties(property)
324     {
325         this._relatedLonghandProperties = [];
326     }
327
328     hasOtherVendorNameOrKeyword()
329     {
330         if ("_hasOtherVendorNameOrKeyword" in this)
331             return this._hasOtherVendorNameOrKeyword;
332
333         this._hasOtherVendorNameOrKeyword = WI.cssStyleManager.propertyNameHasOtherVendorPrefix(this.name) || WI.cssStyleManager.propertyValueHasOtherVendorKeyword(this.value);
334
335         return this._hasOtherVendorNameOrKeyword;
336     }
337
338     // Private
339
340     _updateStyleText(forceRemove = false)
341     {
342         let text = "";
343
344         if (this._name && this._rawValue)
345             text = this._name + ": " + this._rawValue + ";";
346
347         let oldText = this._text;
348         this._text = text;
349         this._updateOwnerStyleText(oldText, this._text, forceRemove);
350     }
351
352     _updateOwnerStyleText(oldText, newText, forceRemove = false)
353     {
354         if (oldText === newText) {
355             if (forceRemove) {
356                 const lineDelta = 0;
357                 const columnDelta = 0;
358                 this._ownerStyle.shiftPropertiesAfter(this, lineDelta, columnDelta, forceRemove);
359             }
360             return;
361         }
362
363         let styleText = this._ownerStyle.text || "";
364
365         // _styleSheetTextRange is the position of the property within the stylesheet.
366         // range is the position of the property within the rule.
367         let range = this._styleSheetTextRange.relativeTo(this._ownerStyle.styleSheetTextRange.startLine, this._ownerStyle.styleSheetTextRange.startColumn);
368
369         // Append a line break to count the last line of styleText towards endOffset.
370         range.resolveOffsets(styleText + "\n");
371
372         console.assert(oldText === styleText.slice(range.startOffset, range.endOffset), "_styleSheetTextRange data is invalid.");
373
374         let newStyleText = this._appendSemicolonIfNeeded(styleText.slice(0, range.startOffset)) + newText + styleText.slice(range.endOffset);
375
376         let lineDelta = newText.lineCount - oldText.lineCount;
377         let columnDelta = newText.lastLine.length - oldText.lastLine.length;
378         this._styleSheetTextRange = this._styleSheetTextRange.cloneAndModify(0, 0, lineDelta, columnDelta);
379
380         this._ownerStyle.text = newStyleText;
381
382         let propertyWasRemoved = !newText;
383         this._ownerStyle.shiftPropertiesAfter(this, lineDelta, columnDelta, propertyWasRemoved);
384     }
385
386     _appendSemicolonIfNeeded(styleText)
387     {
388         if (/[^;\s]\s*$/.test(styleText))
389             return styleText.trimRight() + "; ";
390
391         return styleText;
392     }
393 };
394
395 WI.CSSProperty.Event = {
396     Changed: "css-property-changed",
397     OverriddenStatusChanged: "css-property-overridden-status-changed"
398 };