56c10677f8a535fdd78fc3f12540411d755cf000
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Models / CSSStyleDeclaration.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.CSSStyleDeclaration = class CSSStyleDeclaration extends WI.Object
27 {
28     constructor(nodeStyles, ownerStyleSheet, id, type, node, inherited, text, properties, styleSheetTextRange)
29     {
30         super();
31
32         console.assert(nodeStyles);
33         this._nodeStyles = nodeStyles;
34
35         this._ownerRule = null;
36
37         this._ownerStyleSheet = ownerStyleSheet || null;
38         this._id = id || null;
39         this._type = type || null;
40         this._node = node || null;
41         this._inherited = inherited || false;
42
43         this._initialState = null;
44         this._locked = false;
45         this._pendingProperties = [];
46         this._propertyNameMap = {};
47
48         this._properties = [];
49         this._enabledProperties = null;
50         this._visibleProperties = null;
51
52         this.update(text, properties, styleSheetTextRange, {dontFireEvents: true});
53     }
54
55     // Public
56
57     get initialState() { return this._initialState; }
58
59     get id()
60     {
61         return this._id;
62     }
63
64     get ownerStyleSheet()
65     {
66         return this._ownerStyleSheet;
67     }
68
69     get type()
70     {
71         return this._type;
72     }
73
74     get inherited()
75     {
76         return this._inherited;
77     }
78
79     get node()
80     {
81         return this._node;
82     }
83
84     get editable()
85     {
86         if (!this._id)
87             return false;
88
89         if (this._type === WI.CSSStyleDeclaration.Type.Rule)
90             return this._ownerRule && this._ownerRule.editable;
91
92         if (this._type === WI.CSSStyleDeclaration.Type.Inline)
93             return !this._node.isInUserAgentShadowTree();
94
95         return false;
96     }
97
98     get selectorEditable()
99     {
100         return this._ownerRule && this._ownerRule.editable;
101     }
102
103     get locked() { return this._locked; }
104     set locked(value) { this._locked = value; }
105
106     update(text, properties, styleSheetTextRange, options = {})
107     {
108         let dontFireEvents = options.dontFireEvents || false;
109         let suppressLock = options.suppressLock || false;
110
111         if (this._locked && !suppressLock && text !== this._text)
112             return;
113
114         text = text || "";
115         properties = properties || [];
116
117         let oldProperties = this._properties || [];
118         let oldText = this._text;
119
120         this._text = text;
121         this._properties = properties;
122
123         this._styleSheetTextRange = styleSheetTextRange;
124         this._propertyNameMap = {};
125
126         this._enabledProperties = null;
127         this._visibleProperties = null;
128
129         let editable = this.editable;
130
131         for (let property of this._properties) {
132             property.ownerStyle = this;
133
134             // Store the property in a map if we aren't editable. This
135             // allows for quick lookup for computed style. Editable
136             // styles don't use the map since they need to account for
137             // overridden properties.
138             if (!editable)
139                 this._propertyNameMap[property.name] = property;
140             else {
141                 // Remove from pendingProperties (if it was pending).
142                 this._pendingProperties.remove(property);
143             }
144         }
145
146         for (let oldProperty of oldProperties) {
147             if (this.enabledProperties.includes(oldProperty))
148                 continue;
149
150             // Clear the index, since it is no longer valid.
151             oldProperty.index = NaN;
152
153             // Keep around old properties in pending in case they
154             // are needed again during editing.
155             if (editable)
156                 this._pendingProperties.push(oldProperty);
157         }
158
159         if (dontFireEvents)
160             return;
161
162         // Don't fire the event if there is text and it hasn't changed.
163         if (oldText && this._text && oldText === this._text)
164             return;
165
166         function delayed()
167         {
168             this.dispatchEventToListeners(WI.CSSStyleDeclaration.Event.PropertiesChanged);
169         }
170
171         // Delay firing the PropertiesChanged event so DOMNodeStyles has a chance to mark overridden and associated properties.
172         setTimeout(delayed.bind(this), 0);
173     }
174
175     get ownerRule()
176     {
177         return this._ownerRule;
178     }
179
180     set ownerRule(rule)
181     {
182         this._ownerRule = rule || null;
183     }
184
185     get text()
186     {
187         return this._text;
188     }
189
190     set text(text)
191     {
192         if (this._text === text)
193             return;
194
195         let trimmedText = text.trim();
196         if (this._text === trimmedText)
197             return;
198
199         if (!trimmedText.length || this._type === WI.CSSStyleDeclaration.Type.Inline)
200             text = trimmedText;
201
202         // Update text immediately when it was modified via the styles sidebar.
203         if (this._locked)
204             this._text = text;
205
206         this._nodeStyles.changeStyleText(this, text);
207     }
208
209     get enabledProperties()
210     {
211         if (!this._enabledProperties)
212             this._enabledProperties = this._properties.filter((property) => property.enabled);
213
214         return this._enabledProperties;
215     }
216
217     get properties()
218     {
219         return this._properties;
220     }
221
222     set properties(properties)
223     {
224         if (properties === this._properties)
225             return;
226
227         this._properties = properties;
228         this._enabledProperties = null;
229         this._visibleProperties = null;
230     }
231
232     get visibleProperties()
233     {
234         if (!this._visibleProperties)
235             this._visibleProperties = this._properties.filter((property) => !!property.styleDeclarationTextRange);
236
237         return this._visibleProperties;
238     }
239
240     get pendingProperties()
241     {
242         return this._pendingProperties;
243     }
244
245     get styleSheetTextRange()
246     {
247         return this._styleSheetTextRange;
248     }
249
250     get mediaList()
251     {
252         if (this._ownerRule)
253             return this._ownerRule.mediaList;
254         return [];
255     }
256
257     get selectorText()
258     {
259         if (this._ownerRule)
260             return this._ownerRule.selectorText;
261         return this._node.appropriateSelectorFor(true);
262     }
263
264     propertyForName(name, dontCreateIfMissing)
265     {
266         console.assert(name);
267         if (!name)
268             return null;
269
270         if (!this.editable)
271             return this._propertyNameMap[name] || null;
272
273         // Editable styles don't use the map since they need to
274         // account for overridden properties.
275
276         function findMatch(properties)
277         {
278             for (var i = 0; i < properties.length; ++i) {
279                 var property = properties[i];
280                 if (property.canonicalName !== name && property.name !== name)
281                     continue;
282                 if (bestMatchProperty && !bestMatchProperty.overridden && property.overridden)
283                     continue;
284                 bestMatchProperty = property;
285             }
286         }
287
288         var bestMatchProperty = null;
289
290         findMatch(this.enabledProperties);
291
292         if (bestMatchProperty)
293             return bestMatchProperty;
294
295         if (dontCreateIfMissing || !this.editable)
296             return null;
297
298         findMatch(this._pendingProperties, true);
299
300         if (bestMatchProperty)
301             return bestMatchProperty;
302
303         var newProperty = new WI.CSSProperty(NaN, null, name);
304         newProperty.ownerStyle = this;
305
306         this._pendingProperties.push(newProperty);
307
308         return newProperty;
309     }
310
311     newBlankProperty(propertyIndex)
312     {
313         let text, name, value, priority, overridden, implicit, anonymous;
314         let enabled = true;
315         let valid = false;
316         let styleSheetTextRange = this._rangeAfterPropertyAtIndex(propertyIndex - 1);
317
318         this.markModified();
319         let property = new WI.CSSProperty(propertyIndex, text, name, value, priority, enabled, overridden, implicit, anonymous, valid, styleSheetTextRange);
320
321         this._properties.insertAtIndex(property, propertyIndex);
322         for (let index = propertyIndex + 1; index < this._properties.length; index++)
323             this._properties[index].index = index;
324
325         this.update(this._text, this._properties, this._styleSheetTextRange, {dontFireEvents: true, suppressLock: true});
326
327         return property;
328     }
329
330     markModified()
331     {
332         let properties = this._initialState ? this._initialState.properties : this._properties;
333
334         if (!this._initialState) {
335             this._initialState = new WI.CSSStyleDeclaration(
336                     this._nodeStyles,
337                     this._ownerStyleSheet,
338                     this._id,
339                     this._type,
340                     this._node,
341                     this._inherited,
342                     this._text,
343                     [], // Passing CSS properties here would change their ownerStyle.
344                     this._styleSheetTextRange);
345         }
346
347         this._initialState.properties = properties.map((property) => { return property.initialState || property });
348
349         if (this._ownerRule)
350             this._ownerRule.markModified();
351     }
352
353     shiftPropertiesAfter(cssProperty, lineDelta, columnDelta, propertyWasRemoved)
354     {
355         // cssProperty.index could be set to NaN by WI.CSSStyleDeclaration.prototype.update.
356         let realIndex = this._properties.indexOf(cssProperty);
357         if (realIndex === -1)
358             return;
359
360         let endLine = cssProperty.styleSheetTextRange.endLine;
361
362         for (let i = realIndex + 1; i < this._properties.length; i++) {
363             let property = this._properties[i];
364
365             if (property._styleSheetTextRange) {
366                 if (property.styleSheetTextRange.startLine === endLine) {
367                     // Only update column data if it's on the same line.
368                     property._styleSheetTextRange = property._styleSheetTextRange.cloneAndModify(lineDelta, columnDelta, lineDelta, columnDelta);
369                 } else
370                     property._styleSheetTextRange = property._styleSheetTextRange.cloneAndModify(lineDelta, 0, lineDelta, 0);
371             }
372
373             if (propertyWasRemoved && !isNaN(property._index))
374                 property._index--;
375         }
376
377         if (propertyWasRemoved)
378             this._properties.splice(realIndex, 1);
379
380         // Invalidate cached properties.
381         this._visibleProperties = null;
382     }
383
384     // Protected
385
386     get nodeStyles()
387     {
388         return this._nodeStyles;
389     }
390
391     // Private
392
393     _rangeAfterPropertyAtIndex(index)
394     {
395         if (index < 0)
396             return this._styleSheetTextRange.collapseToStart();
397
398         if (index >= this.visibleProperties.length)
399             return this._styleSheetTextRange.collapseToEnd();
400
401         let property = this.visibleProperties[index];
402         return property.styleSheetTextRange.collapseToEnd();
403     }
404 };
405
406 WI.CSSStyleDeclaration.Event = {
407     PropertiesChanged: "css-style-declaration-properties-changed",
408 };
409
410 WI.CSSStyleDeclaration.Type = {
411     Rule: "css-style-declaration-type-rule",
412     Inline: "css-style-declaration-type-inline",
413     Attribute: "css-style-declaration-type-attribute",
414     Computed: "css-style-declaration-type-computed"
415 };