1be38e5d5fe16b8ffd314244387997094dfec875
[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         this._initialState = null;
35
36         this.update(text, name, value, priority, enabled, overridden, implicit, anonymous, valid, styleSheetTextRange, true);
37     }
38
39     // Static
40
41     static isInheritedPropertyName(name)
42     {
43         console.assert(typeof name === "string");
44         if (WI.CSSKeywordCompletions.InheritedProperties.has(name))
45             return true;
46         // Check if the name is a CSS variable.
47         return name.startsWith("--");
48     }
49
50     // Public
51
52     get ownerStyle()
53     {
54         return this._ownerStyle;
55     }
56
57     set ownerStyle(ownerStyle)
58     {
59         this._ownerStyle = ownerStyle || null;
60     }
61
62     get index()
63     {
64         return this._index;
65     }
66
67     set index(index)
68     {
69         this._index = index;
70     }
71
72     update(text, name, value, priority, enabled, overridden, implicit, anonymous, valid, styleSheetTextRange, dontFireEvents)
73     {
74         // Locked CSSProperty can still be updated from the back-end when the text matches.
75         // We need to do this to keep attributes such as valid and overridden up to date.
76         if (this._ownerStyle && this._ownerStyle.locked && text !== this._text)
77             return;
78
79         text = text || "";
80         name = name || "";
81         value = value || "";
82         priority = priority || "";
83         enabled = enabled || false;
84         overridden = overridden || false;
85         implicit = implicit || false;
86         anonymous = anonymous || false;
87         valid = valid || false;
88
89         var changed = false;
90
91         if (!dontFireEvents) {
92             changed = this._name !== name || this._rawValue !== value || this._priority !== priority ||
93                 this._enabled !== enabled || this._implicit !== implicit || this._anonymous !== anonymous || this._valid !== valid;
94         }
95
96         // Use the setter for overridden if we want to fire events since the
97         // OverriddenStatusChanged event coalesces changes before it fires.
98         if (!dontFireEvents)
99             this.overridden = overridden;
100         else
101             this._overridden = overridden;
102
103         this._text = text;
104         this._name = name;
105         this._rawValue = value;
106         this._priority = priority;
107         this._enabled = enabled;
108         this._implicit = implicit;
109         this._anonymous = anonymous;
110         this._inherited = WI.CSSProperty.isInheritedPropertyName(name);
111         this._valid = valid;
112         this._variable = name.startsWith("--");
113         this._styleSheetTextRange = styleSheetTextRange || null;
114
115         this._relatedShorthandProperty = null;
116         this._relatedLonghandProperties = [];
117
118         // Clear computed properties.
119         delete this._styleDeclarationTextRange;
120         delete this._canonicalName;
121         delete this._hasOtherVendorNameOrKeyword;
122
123         if (changed)
124             this.dispatchEventToListeners(WI.CSSProperty.Event.Changed);
125     }
126
127     remove()
128     {
129         this._markModified();
130
131         // Setting name or value to an empty string removes the entire CSSProperty.
132         this._name = "";
133         const forceRemove = true;
134         this._updateStyleText(forceRemove);
135     }
136
137     replaceWithText(text)
138     {
139         this._markModified();
140
141         this._updateOwnerStyleText(this._text, text, true);
142     }
143
144     commentOut(disabled)
145     {
146         console.assert(this._enabled === disabled, "CSS property is already " + (disabled ? "disabled" : "enabled"));
147         if (this._enabled === !disabled)
148             return;
149
150         this._markModified();
151         this._enabled = !disabled;
152
153         if (disabled)
154             this.text = "/* " + this._text + " */";
155         else
156             this.text = this._text.slice(2, -2).trim();
157     }
158
159     get text()
160     {
161         return this._text;
162     }
163
164     set text(newText)
165     {
166         if (this._text === newText)
167             return;
168
169         this._markModified();
170         this._updateOwnerStyleText(this._text, newText);
171         this._text = newText;
172     }
173
174     get formattedText()
175     {
176         if (!this._name)
177             return "";
178
179         return `${this._name}: ${this._rawValue};`;
180     }
181
182     get modified()
183     {
184         return !!this._initialState;
185     }
186
187     get name()
188     {
189         return this._name;
190     }
191
192     set name(name)
193     {
194         if (name === this._name)
195             return;
196
197         this._markModified();
198         this._name = name;
199         this._updateStyleText();
200     }
201
202     get canonicalName()
203     {
204         if (this._canonicalName)
205             return this._canonicalName;
206
207         this._canonicalName = WI.cssManager.canonicalNameForPropertyName(this.name);
208
209         return this._canonicalName;
210     }
211
212     // FIXME: Remove current value getter and rename rawValue to value once the old styles sidebar is removed.
213     get value()
214     {
215         if (!this._value)
216             this._value = this._rawValue.replace(/\s*!important\s*$/, "");
217
218         return this._value;
219     }
220
221     get rawValue()
222     {
223         return this._rawValue;
224     }
225
226     set rawValue(value)
227     {
228         if (value === this._rawValue)
229             return;
230
231         this._markModified();
232
233         this._rawValue = value;
234         this._value = undefined;
235         this._updateStyleText();
236     }
237
238     get important()
239     {
240         return this.priority === "important";
241     }
242
243     get priority() { return this._priority; }
244
245     get attached()
246     {
247         return this._enabled && this._ownerStyle && (!isNaN(this._index) || this._ownerStyle.type === WI.CSSStyleDeclaration.Type.Computed);
248     }
249
250     // Only commented out properties are disabled.
251     get enabled() { return this._enabled; }
252
253     get overridden() { return this._overridden; }
254     set overridden(overridden)
255     {
256         overridden = overridden || false;
257
258         if (this._overridden === overridden)
259             return;
260
261         var previousOverridden = this._overridden;
262
263         this._overridden = overridden;
264
265         if (this._overriddenStatusChangedTimeout)
266             return;
267
268         function delayed()
269         {
270             delete this._overriddenStatusChangedTimeout;
271
272             if (this._overridden === previousOverridden)
273                 return;
274
275             this.dispatchEventToListeners(WI.CSSProperty.Event.OverriddenStatusChanged);
276         }
277
278         this._overriddenStatusChangedTimeout = setTimeout(delayed.bind(this), 0);
279     }
280
281     get implicit() { return this._implicit; }
282     set implicit(implicit) { this._implicit = implicit; }
283
284     get anonymous() { return this._anonymous; }
285     get inherited() { return this._inherited; }
286     get valid() { return this._valid; }
287     get variable() { return this._variable; }
288     get styleSheetTextRange() { return this._styleSheetTextRange; }
289
290     get initialState()
291     {
292         return this._initialState;
293     }
294
295     get editable()
296     {
297         return !!(this._styleSheetTextRange && this._ownerStyle && this._ownerStyle.styleSheetTextRange);
298     }
299
300     get styleDeclarationTextRange()
301     {
302         if ("_styleDeclarationTextRange" in this)
303             return this._styleDeclarationTextRange;
304
305         if (!this._ownerStyle || !this._styleSheetTextRange)
306             return null;
307
308         var styleTextRange = this._ownerStyle.styleSheetTextRange;
309         if (!styleTextRange)
310             return null;
311
312         var startLine = this._styleSheetTextRange.startLine - styleTextRange.startLine;
313         var endLine = this._styleSheetTextRange.endLine - styleTextRange.startLine;
314
315         var startColumn = this._styleSheetTextRange.startColumn;
316         if (!startLine)
317             startColumn -= styleTextRange.startColumn;
318
319         var endColumn = this._styleSheetTextRange.endColumn;
320         if (!endLine)
321             endColumn -= styleTextRange.startColumn;
322
323         this._styleDeclarationTextRange = new WI.TextRange(startLine, startColumn, endLine, endColumn);
324
325         return this._styleDeclarationTextRange;
326     }
327
328     get relatedShorthandProperty() { return this._relatedShorthandProperty; }
329     set relatedShorthandProperty(property)
330     {
331         this._relatedShorthandProperty = property || null;
332     }
333
334     get relatedLonghandProperties() { return this._relatedLonghandProperties; }
335
336     addRelatedLonghandProperty(property)
337     {
338         this._relatedLonghandProperties.push(property);
339     }
340
341     clearRelatedLonghandProperties(property)
342     {
343         this._relatedLonghandProperties = [];
344     }
345
346     hasOtherVendorNameOrKeyword()
347     {
348         if ("_hasOtherVendorNameOrKeyword" in this)
349             return this._hasOtherVendorNameOrKeyword;
350
351         this._hasOtherVendorNameOrKeyword = WI.cssManager.propertyNameHasOtherVendorPrefix(this.name) || WI.cssManager.propertyValueHasOtherVendorKeyword(this.value);
352
353         return this._hasOtherVendorNameOrKeyword;
354     }
355
356     // Private
357
358     _updateStyleText(forceRemove = false)
359     {
360         let text = "";
361
362         if (this._name && this._rawValue)
363             text = this._name + ": " + this._rawValue + ";";
364
365         let oldText = this._text;
366         this._text = text;
367         this._updateOwnerStyleText(oldText, this._text, forceRemove);
368     }
369
370     _updateOwnerStyleText(oldText, newText, forceRemove = false)
371     {
372         console.assert(this.modified, "CSSProperty was modified without saving initial state.");
373
374         if (oldText === newText) {
375             if (forceRemove) {
376                 const lineDelta = 0;
377                 const columnDelta = 0;
378                 this._ownerStyle.shiftPropertiesAfter(this, lineDelta, columnDelta, forceRemove);
379             }
380             return;
381         }
382
383         this._prependSemicolonIfNeeded();
384
385         let styleText = this._ownerStyle.text || "";
386
387         // _styleSheetTextRange is the position of the property within the stylesheet.
388         // range is the position of the property within the rule.
389         let range = this._styleSheetTextRange.relativeTo(this._ownerStyle.styleSheetTextRange.startLine, this._ownerStyle.styleSheetTextRange.startColumn);
390
391         // Append a line break to count the last line of styleText towards endOffset.
392         range.resolveOffsets(styleText + "\n");
393
394         console.assert(oldText === styleText.slice(range.startOffset, range.endOffset), "_styleSheetTextRange data is invalid.");
395
396         if (WI.settings.enableStyleEditingDebugMode.value) {
397             let prefix = styleText.slice(0, range.startOffset);
398             let postfix = styleText.slice(range.endOffset);
399             console.info(`${prefix}%c${oldText}%c${newText}%c${postfix}`, `background: hsl(356, 100%, 90%); color: black`, `background: hsl(100, 100%, 91%); color: black`, `background: transparent`);
400         }
401
402         let newStyleText = styleText.slice(0, range.startOffset) + newText + styleText.slice(range.endOffset);
403
404         let lineDelta = newText.lineCount - oldText.lineCount;
405         let columnDelta = newText.lastLine.length - oldText.lastLine.length;
406         this._styleSheetTextRange = this._styleSheetTextRange.cloneAndModify(0, 0, lineDelta, columnDelta);
407
408         this._ownerStyle.text = newStyleText;
409
410         let propertyWasRemoved = !newText;
411         this._ownerStyle.shiftPropertiesAfter(this, lineDelta, columnDelta, propertyWasRemoved);
412     }
413
414     _prependSemicolonIfNeeded()
415     {
416         for (let i = this.index - 1; i >= 0; --i) {
417             let property = this._ownerStyle.properties[i];
418             if (!property.enabled)
419                 continue;
420
421             let match = property.text.match(/[^;\s](\s*)$/);
422             if (match)
423                 property.text = property.text.trimRight() + ";" + match[1];
424
425             break;
426         }
427     }
428
429     _markModified()
430     {
431         if (this.modified)
432             return;
433
434         this._initialState = new WI.CSSProperty(
435             this._index,
436             this._text,
437             this._name,
438             this._rawValue,
439             this._priority,
440             this._enabled,
441             this._overridden,
442             this._implicit,
443             this._anonymous,
444             this._valid,
445             this._styleSheetTextRange);
446
447         if (this._ownerStyle) {
448             this._ownerStyle.markModified();
449             this._initialState.ownerStyle = this._ownerStyle.initialState;
450         }
451     }
452 };
453
454 WI.CSSProperty.Event = {
455     Changed: "css-property-changed",
456     OverriddenStatusChanged: "css-property-overridden-status-changed"
457 };