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