Web Inspector: Styles: close unbalanced quotes and parenthesis when editing values
[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         let suffix = WI.CSSCompletions.completeUnbalancedValue(value);
234         if (suffix)
235             value += suffix;
236
237         this._rawValue = value;
238         this._value = undefined;
239         this._updateStyleText();
240     }
241
242     get important()
243     {
244         return this.priority === "important";
245     }
246
247     get priority() { return this._priority; }
248
249     get attached()
250     {
251         return this._enabled && this._ownerStyle && (!isNaN(this._index) || this._ownerStyle.type === WI.CSSStyleDeclaration.Type.Computed);
252     }
253
254     // Only commented out properties are disabled.
255     get enabled() { return this._enabled; }
256
257     get overridden() { return this._overridden; }
258     set overridden(overridden)
259     {
260         overridden = overridden || false;
261
262         if (this._overridden === overridden)
263             return;
264
265         var previousOverridden = this._overridden;
266
267         this._overridden = overridden;
268
269         if (this._overriddenStatusChangedTimeout)
270             return;
271
272         function delayed()
273         {
274             delete this._overriddenStatusChangedTimeout;
275
276             if (this._overridden === previousOverridden)
277                 return;
278
279             this.dispatchEventToListeners(WI.CSSProperty.Event.OverriddenStatusChanged);
280         }
281
282         this._overriddenStatusChangedTimeout = setTimeout(delayed.bind(this), 0);
283     }
284
285     get implicit() { return this._implicit; }
286     set implicit(implicit) { this._implicit = implicit; }
287
288     get anonymous() { return this._anonymous; }
289     get inherited() { return this._inherited; }
290     get valid() { return this._valid; }
291     get variable() { return this._variable; }
292     get styleSheetTextRange() { return this._styleSheetTextRange; }
293
294     get initialState()
295     {
296         return this._initialState;
297     }
298
299     get editable()
300     {
301         return !!(this._styleSheetTextRange && this._ownerStyle && this._ownerStyle.styleSheetTextRange);
302     }
303
304     get styleDeclarationTextRange()
305     {
306         if ("_styleDeclarationTextRange" in this)
307             return this._styleDeclarationTextRange;
308
309         if (!this._ownerStyle || !this._styleSheetTextRange)
310             return null;
311
312         var styleTextRange = this._ownerStyle.styleSheetTextRange;
313         if (!styleTextRange)
314             return null;
315
316         var startLine = this._styleSheetTextRange.startLine - styleTextRange.startLine;
317         var endLine = this._styleSheetTextRange.endLine - styleTextRange.startLine;
318
319         var startColumn = this._styleSheetTextRange.startColumn;
320         if (!startLine)
321             startColumn -= styleTextRange.startColumn;
322
323         var endColumn = this._styleSheetTextRange.endColumn;
324         if (!endLine)
325             endColumn -= styleTextRange.startColumn;
326
327         this._styleDeclarationTextRange = new WI.TextRange(startLine, startColumn, endLine, endColumn);
328
329         return this._styleDeclarationTextRange;
330     }
331
332     get relatedShorthandProperty() { return this._relatedShorthandProperty; }
333     set relatedShorthandProperty(property)
334     {
335         this._relatedShorthandProperty = property || null;
336     }
337
338     get relatedLonghandProperties() { return this._relatedLonghandProperties; }
339
340     addRelatedLonghandProperty(property)
341     {
342         this._relatedLonghandProperties.push(property);
343     }
344
345     clearRelatedLonghandProperties(property)
346     {
347         this._relatedLonghandProperties = [];
348     }
349
350     hasOtherVendorNameOrKeyword()
351     {
352         if ("_hasOtherVendorNameOrKeyword" in this)
353             return this._hasOtherVendorNameOrKeyword;
354
355         this._hasOtherVendorNameOrKeyword = WI.cssManager.propertyNameHasOtherVendorPrefix(this.name) || WI.cssManager.propertyValueHasOtherVendorKeyword(this.value);
356
357         return this._hasOtherVendorNameOrKeyword;
358     }
359
360     // Private
361
362     _updateStyleText(forceRemove = false)
363     {
364         let text = "";
365
366         if (this._name && this._rawValue)
367             text = this._name + ": " + this._rawValue + ";";
368
369         let oldText = this._text;
370         this._text = text;
371         this._updateOwnerStyleText(oldText, this._text, forceRemove);
372     }
373
374     _updateOwnerStyleText(oldText, newText, forceRemove = false)
375     {
376         console.assert(this.modified, "CSSProperty was modified without saving initial state.");
377
378         if (oldText === newText) {
379             if (forceRemove) {
380                 const lineDelta = 0;
381                 const columnDelta = 0;
382                 this._ownerStyle.shiftPropertiesAfter(this, lineDelta, columnDelta, forceRemove);
383             }
384             return;
385         }
386
387         console.assert(this._ownerStyle);
388         if (!this._ownerStyle)
389             return;
390
391         this._prependSemicolonIfNeeded();
392
393         let styleText = this._ownerStyle.text || "";
394
395         // _styleSheetTextRange is the position of the property within the stylesheet.
396         // range is the position of the property within the rule.
397         let range = this._styleSheetTextRange.relativeTo(this._ownerStyle.styleSheetTextRange.startLine, this._ownerStyle.styleSheetTextRange.startColumn);
398
399         // Append a line break to count the last line of styleText towards endOffset.
400         range.resolveOffsets(styleText + "\n");
401
402         console.assert(oldText === styleText.slice(range.startOffset, range.endOffset), "_styleSheetTextRange data is invalid.");
403
404         if (WI.settings.enableStyleEditingDebugMode.value) {
405             let prefix = styleText.slice(0, range.startOffset);
406             let postfix = styleText.slice(range.endOffset);
407             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`);
408         }
409
410         let newStyleText = styleText.slice(0, range.startOffset) + newText + styleText.slice(range.endOffset);
411
412         let lineDelta = newText.lineCount - oldText.lineCount;
413         let columnDelta = newText.lastLine.length - oldText.lastLine.length;
414         this._styleSheetTextRange = this._styleSheetTextRange.cloneAndModify(0, 0, lineDelta, columnDelta);
415
416         this._ownerStyle.text = newStyleText;
417
418         let propertyWasRemoved = !newText;
419         this._ownerStyle.shiftPropertiesAfter(this, lineDelta, columnDelta, propertyWasRemoved);
420     }
421
422     _prependSemicolonIfNeeded()
423     {
424         for (let i = this.index - 1; i >= 0; --i) {
425             let property = this._ownerStyle.properties[i];
426             if (!property.enabled)
427                 continue;
428
429             let match = property.text.match(/[^;\s](\s*)$/);
430             if (match)
431                 property.text = property.text.trimRight() + ";" + match[1];
432
433             break;
434         }
435     }
436
437     _markModified()
438     {
439         if (this.modified)
440             return;
441
442         this._initialState = new WI.CSSProperty(
443             this._index,
444             this._text,
445             this._name,
446             this._rawValue,
447             this._priority,
448             this._enabled,
449             this._overridden,
450             this._implicit,
451             this._anonymous,
452             this._valid,
453             this._styleSheetTextRange);
454
455         if (this._ownerStyle) {
456             this._ownerStyle.markModified();
457             this._initialState.ownerStyle = this._ownerStyle.initialState;
458         }
459     }
460 };
461
462 WI.CSSProperty.Event = {
463     Changed: "css-property-changed",
464     OverriddenStatusChanged: "css-property-overridden-status-changed"
465 };