Web Inspector: Support smart-pasting in the Rules sidebar panel
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Models / DOMNodeStyles.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 WebInspector.DOMNodeStyles = class DOMNodeStyles extends WebInspector.Object
27 {
28     constructor(node)
29     {
30         super();
31
32         console.assert(node);
33         this._node = node || null;
34
35         this._rulesMap = {};
36         this._styleDeclarationsMap = {};
37
38         this._matchedRules = [];
39         this._inheritedRules = [];
40         this._pseudoElements = {};
41         this._inlineStyle = null;
42         this._attributesStyle = null;
43         this._computedStyle = null;
44         this._orderedStyles = [];
45         this._stylesNeedingTextCommited = [];
46
47         this._propertyNameToEffectivePropertyMap = {};
48
49         this.refresh();
50     }
51
52     // Public
53
54     get node()
55     {
56         return this._node;
57     }
58
59     get needsRefresh()
60     {
61         return this._refreshPending || this._needsRefresh;
62     }
63
64     refreshIfNeeded()
65     {
66         if (!this._needsRefresh)
67             return;
68         this.refresh();
69     }
70
71     refresh()
72     {
73         if (this._refreshPending)
74             return;
75
76         this._needsRefresh = false;
77         this._refreshPending = true;
78
79         function parseRuleMatchArrayPayload(matchArray, node, inherited)
80         {
81             var result = [];
82
83             var ruleOccurrences = {};
84
85             // Iterate in reverse order to match the cascade order.
86             for (var i = matchArray.length - 1; i >= 0; --i) {
87                 // COMPATIBILITY (iOS 6): This was just an array of rules, now it is an array of matches that have
88                 // a 'rule' property. Support both here. And 'matchingSelectors' does not exist on iOS 6.
89                 var matchedSelectorIndices = matchArray[i].matchingSelectors || [];
90                 var rule = this._parseRulePayload(matchArray[i].rule || matchArray[i], matchedSelectorIndices, node, inherited, ruleOccurrences);
91                 if (!rule)
92                     continue;
93                 result.push(rule);
94             }
95
96             return result;
97         }
98
99         function fetchedMatchedStyles(error, matchedRulesPayload, pseudoElementRulesPayload, inheritedRulesPayload)
100         {
101             matchedRulesPayload = matchedRulesPayload || [];
102             pseudoElementRulesPayload = pseudoElementRulesPayload || [];
103             inheritedRulesPayload = inheritedRulesPayload || [];
104
105             // Move the current maps to previous.
106             this._previousRulesMap = this._rulesMap;
107             this._previousStyleDeclarationsMap = this._styleDeclarationsMap;
108
109             // Clear the current maps.
110             this._rulesMap = {};
111             this._styleDeclarationsMap = {};
112
113             this._matchedRules = parseRuleMatchArrayPayload.call(this, matchedRulesPayload, this._node);
114
115             this._pseudoElements = {};
116             for (var i = 0; i < pseudoElementRulesPayload.length; ++i) {
117                 var pseudoElementRulePayload = pseudoElementRulesPayload[i];
118
119                 // COMPATIBILITY (iOS 6): The entry payload had a 'rules' property, now it has a 'matches' property. Support both here.
120                 var pseudoElementRules = parseRuleMatchArrayPayload.call(this, pseudoElementRulePayload.matches || pseudoElementRulePayload.rules, this._node);
121                 this._pseudoElements[pseudoElementRulePayload.pseudoId] = {matchedRules: pseudoElementRules};
122             }
123
124             this._inheritedRules = [];
125
126             var i = 0;
127             var currentNode = this._node.parentNode;
128             while (currentNode && i < inheritedRulesPayload.length) {
129                 var inheritedRulePayload = inheritedRulesPayload[i];
130
131                 var inheritedRuleInfo = {node: currentNode};
132                 inheritedRuleInfo.inlineStyle = inheritedRulePayload.inlineStyle ? this._parseStyleDeclarationPayload(inheritedRulePayload.inlineStyle, currentNode, true, WebInspector.CSSStyleDeclaration.Type.Inline) : null;
133                 inheritedRuleInfo.matchedRules = inheritedRulePayload.matchedCSSRules ? parseRuleMatchArrayPayload.call(this, inheritedRulePayload.matchedCSSRules, currentNode, true) : [];
134
135                 if (inheritedRuleInfo.inlineStyle || inheritedRuleInfo.matchedRules.length)
136                     this._inheritedRules.push(inheritedRuleInfo);
137
138                 currentNode = currentNode.parentNode;
139                 ++i;
140             }
141         }
142
143         function fetchedInlineStyles(error, inlineStylePayload, attributesStylePayload)
144         {
145             this._inlineStyle = inlineStylePayload ? this._parseStyleDeclarationPayload(inlineStylePayload, this._node, false, WebInspector.CSSStyleDeclaration.Type.Inline) : null;
146             this._attributesStyle = attributesStylePayload ? this._parseStyleDeclarationPayload(attributesStylePayload, this._node, false, WebInspector.CSSStyleDeclaration.Type.Attribute) : null;
147
148             this._updateStyleCascade();
149         }
150
151         function fetchedComputedStyle(error, computedPropertiesPayload)
152         {
153             var properties = [];
154             for (var i = 0; computedPropertiesPayload && i < computedPropertiesPayload.length; ++i) {
155                 var propertyPayload = computedPropertiesPayload[i];
156
157                 var canonicalName = WebInspector.cssStyleManager.canonicalNameForPropertyName(propertyPayload.name);
158                 propertyPayload.implicit = !this._propertyNameToEffectivePropertyMap[canonicalName];
159
160                 var property = this._parseStylePropertyPayload(propertyPayload, NaN, this._computedStyle);
161                 properties.push(property);
162             }
163
164             if (this._computedStyle)
165                 this._computedStyle.update(null, properties);
166             else
167                 this._computedStyle = new WebInspector.CSSStyleDeclaration(this, null, null, WebInspector.CSSStyleDeclaration.Type.Computed, this._node, false, null, properties);
168
169             this._refreshPending = false;
170
171             var significantChange = this._previousSignificantChange || false;
172             if (!significantChange) {
173                 for (var key in this._styleDeclarationsMap) {
174                     // Check if the same key exists in the previous map and has the same style objects.
175                     if (key in this._previousStyleDeclarationsMap && Object.shallowEqual(this._styleDeclarationsMap[key], this._previousStyleDeclarationsMap[key]))
176                         continue;
177
178                     if (!this._includeUserAgentRulesOnNextRefresh) {
179                         // We can assume all the styles with the same key are from the same stylesheet and rule, so we only check the first.
180                         var firstStyle = this._styleDeclarationsMap[key][0];
181                         if (firstStyle && firstStyle.ownerRule && firstStyle.ownerRule.type === WebInspector.CSSRule.Type.UserAgent) {
182                             // User Agent styles get different identifiers after some edits. This would cause us to fire a significant refreshed
183                             // event more than it is helpful. And since the user agent stylesheet is static it shouldn't match differently
184                             // between refreshes for the same node. This issue is tracked by: https://webkit.org/b/110055
185                             continue;
186                         }
187                     }
188
189                     // This key is new or has different style objects than before. This is a significant change.
190                     significantChange = true;
191                     break;
192                 }
193             }
194
195             if (!significantChange) {
196                 for (var key in this._previousStyleDeclarationsMap) {
197                     // Check if the same key exists in current map. If it does exist it was already checked for equality above.
198                     if (key in this._styleDeclarationsMap)
199                         continue;
200
201                     if (!this._includeUserAgentRulesOnNextRefresh) {
202                         // See above for why we skip user agent style rules.
203                         var firstStyle = this._previousStyleDeclarationsMap[key][0];
204                         if (firstStyle && firstStyle.ownerRule && firstStyle.ownerRule.type === WebInspector.CSSRule.Type.UserAgent)
205                             continue;
206                     }
207
208                     // This key no longer exists. This is a significant change.
209                     significantChange = true;
210                     break;
211                 }
212             }
213
214             delete this._includeUserAgentRulesOnNextRefresh;
215
216             // Delete the previous maps now that any reused rules and style have been moved over.
217             delete this._previousRulesMap;
218             delete this._previousStyleDeclarationsMap;
219
220             var styleToCommit = this._stylesNeedingTextCommited.shift();
221             if (styleToCommit) {
222                 // Remember the significant change flag so we can pass it along when the pending style
223                 // changes trigger a refresh. If we wait to scan later we might not find a significant change
224                 // and fail to tell listeners about it.
225                 this._previousSignificantChange = significantChange;
226
227                 this.changeStyleText(styleToCommit, styleToCommit.__pendingText);
228
229                 return;
230             }
231
232             // Delete the previous saved significant change flag so we rescan for a significant change next time.
233             delete this._previousSignificantChange;
234
235             this.dispatchEventToListeners(WebInspector.DOMNodeStyles.Event.Refreshed, {significantChange});
236         }
237
238         CSSAgent.getMatchedStylesForNode.invoke({nodeId: this._node.id, includePseudo: true, includeInherited: true}, fetchedMatchedStyles.bind(this));
239         CSSAgent.getInlineStylesForNode.invoke({nodeId: this._node.id}, fetchedInlineStyles.bind(this));
240         CSSAgent.getComputedStyleForNode.invoke({nodeId: this._node.id}, fetchedComputedStyle.bind(this));
241     }
242
243     addEmptyRule()
244     {
245         function addedRule(error, rulePayload)
246         {
247             if (error)
248                 return;
249
250             DOMAgent.markUndoableState();
251
252             this.refresh();
253         }
254
255         var selector = this._node.appropriateSelectorFor(true);
256
257         CSSAgent.addRule.invoke({contextNodeId: this._node.id, selector}, addedRule.bind(this));
258     }
259
260     get matchedRules()
261     {
262         return this._matchedRules;
263     }
264
265     get inheritedRules()
266     {
267         return this._inheritedRules;
268     }
269
270     get inlineStyle()
271     {
272         return this._inlineStyle;
273     }
274
275     get attributesStyle()
276     {
277         return this._attributesStyle;
278     }
279
280     get pseudoElements()
281     {
282         return this._pseudoElements;
283     }
284
285     get computedStyle()
286     {
287         return this._computedStyle;
288     }
289
290     get orderedStyles()
291     {
292         return this._orderedStyles;
293     }
294
295     effectivePropertyForName(name)
296     {
297         var canonicalName = WebInspector.cssStyleManager.canonicalNameForPropertyName(name);
298         return this._propertyNameToEffectivePropertyMap[canonicalName] || null;
299     }
300
301     // Protected
302
303     mediaQueryResultDidChange()
304     {
305         this._markAsNeedsRefresh();
306     }
307
308     pseudoClassesDidChange(node)
309     {
310         this._includeUserAgentRulesOnNextRefresh = true;
311         this._markAsNeedsRefresh();
312     }
313
314     attributeDidChange(node, attributeName)
315     {
316         // Ignore the attribute we know we just changed and handled above.
317         if (this._ignoreNextStyleAttributeDidChangeEvent && node === this._node && attributeName === "style") {
318             delete this._ignoreNextStyleAttributeDidChangeEvent;
319             return;
320         }
321
322         this._markAsNeedsRefresh();
323     }
324
325     changeRule(rule, selector, text)
326     {
327         if (!rule)
328             return;
329
330         selector = selector || "";
331
332         function changeCompleted()
333         {
334             DOMAgent.markUndoableState();
335             this.refresh();
336         }
337
338         function styleChanged(error, stylePayload)
339         {
340             if (error)
341                 return;
342
343             changeCompleted.call(this);
344         }
345
346         function changeText(styleId)
347         {
348             // COMPATIBILITY (iOS 6): CSSAgent.setStyleText was not available in iOS 6.
349             if (!text || !text.length || !CSSAgent.setStyleText) {
350                 changeCompleted.call(this);
351                 return;
352             }
353
354             CSSAgent.setStyleText(styleId, text, styleChanged.bind(this));
355         }
356
357         function ruleSelectorChanged(error, rulePayload)
358         {
359             if (error)
360                 return;
361
362             changeText.call(this, rulePayload.style.styleId);
363         }
364
365         this._needsRefresh = true;
366         this._ignoreNextContentDidChangeForStyleSheet = rule.ownerStyleSheet;
367
368         CSSAgent.setRuleSelector(rule.id, selector, ruleSelectorChanged.bind(this));
369     }
370
371     changeRuleSelector(rule, selector)
372     {
373         selector = selector || "";
374         var result = new WebInspector.WrappedPromise;
375
376         function ruleSelectorChanged(error, rulePayload)
377         {
378             if (error) {
379                 result.reject(error);
380                 return;
381             }
382
383             DOMAgent.markUndoableState();
384
385             // Do a full refresh incase the rule no longer matches the node or the
386             // matched selector indices changed.
387             this.refresh();
388
389             result.resolve(rulePayload);
390         }
391
392         this._needsRefresh = true;
393         this._ignoreNextContentDidChangeForStyleSheet = rule.ownerStyleSheet;
394
395         CSSAgent.setRuleSelector(rule.id, selector, ruleSelectorChanged.bind(this));
396         return result.promise;
397     }
398
399     changeStyleText(style, text)
400     {
401         if (!style.ownerStyleSheet || !style.styleSheetTextRange)
402             return;
403
404         text = text || "";
405
406         function styleChanged(error, stylePayload)
407         {
408             if (error)
409                 return;
410             this.refresh();
411         }
412
413         if (CSSAgent.setStyleText) {
414             CSSAgent.setStyleText(style.id, text, styleChanged.bind(this));
415             return;
416         }
417
418         // COMPATIBILITY (iOS 6): CSSAgent.setStyleText was not available in iOS 6.
419
420         function attributeChanged(error)
421         {
422             if (error)
423                 return;
424             this.refresh();
425         }
426
427         // Setting the text on CSSStyleSheet for inline styles causes a crash. https://webkit.org/b/110359
428         // So we just set the style attribute to get the same affect. This also avoids SourceCodeRevisions.
429         if (style.type === WebInspector.CSSStyleDeclaration.Type.Inline) {
430             text = text.trim();
431
432             this._ignoreNextStyleAttributeDidChangeEvent = true;
433
434             if (text)
435                 style.node.setAttributeValue("style", text, attributeChanged.bind(this));
436             else
437                 style.node.removeAttribute("style", attributeChanged.bind(this));
438
439             return;
440         }
441
442         if (this._needsRefresh || this._refreshPending) {
443             // If we need refreshed then it is not safe to use the styleSheetTextRange since the range likely has
444             // changed and we need updated ranges. Store the text and remember the style so we can commit it after
445             // the next refresh.
446
447             style.__pendingText = text;
448
449             if (!this._stylesNeedingTextCommited.includes(style))
450                 this._stylesNeedingTextCommited.push(style);
451
452             return;
453         }
454
455         function fetchedStyleSheetContent(parameters)
456         {
457             var content = parameters.content;
458
459             console.assert(style.styleSheetTextRange);
460             if (!style.styleSheetTextRange)
461                 return;
462
463             var startOffset = style.styleSheetTextRange.startOffset;
464             var endOffset = style.styleSheetTextRange.endOffset;
465
466             if (isNaN(startOffset) || isNaN(endOffset)) {
467                 style.styleSheetTextRange.resolveOffsets(content);
468
469                 startOffset = style.styleSheetTextRange.startOffset;
470                 endOffset = style.styleSheetTextRange.endOffset;
471             }
472
473             console.assert(!isNaN(startOffset));
474             console.assert(!isNaN(endOffset));
475             if (isNaN(startOffset) || isNaN(endOffset))
476                 return;
477
478             function contentDidChange()
479             {
480                 style.ownerStyleSheet.removeEventListener(WebInspector.CSSStyleSheet.Event.ContentDidChange, contentDidChange, this);
481
482                 this.refresh();
483             }
484
485             style.ownerStyleSheet.addEventListener(WebInspector.CSSStyleSheet.Event.ContentDidChange, contentDidChange, this);
486
487             var newContent = content.substring(0, startOffset) + text + content.substring(endOffset);
488
489             WebInspector.branchManager.currentBranch.revisionForRepresentedObject(style.ownerStyleSheet).content = newContent;
490         }
491
492         this._stylesNeedingTextCommited.remove(style);
493         delete style.__pendingText;
494
495         this._needsRefresh = true;
496         this._ignoreNextContentDidChangeForStyleSheet = style.ownerStyleSheet;
497
498         style.ownerStyleSheet.requestContent().then(fetchedStyleSheetContent.bind(this));
499     }
500
501     // Private
502
503     _createSourceCodeLocation(sourceURL, sourceLine, sourceColumn)
504     {
505         if (!sourceURL)
506             return null;
507
508         var sourceCode;
509
510         // Try to use the node to find the frame which has the correct resource first.
511         if (this._node.ownerDocument) {
512             var mainResource = WebInspector.frameResourceManager.resourceForURL(this._node.ownerDocument.documentURL);
513             if (mainResource) {
514                 var parentFrame = mainResource.parentFrame;
515                 sourceCode = parentFrame.resourceForURL(sourceURL);
516             }
517         }
518
519         // If that didn't find the resource, then search all frames.
520         if (!sourceCode)
521             sourceCode = WebInspector.frameResourceManager.resourceForURL(sourceURL);
522
523         if (!sourceCode)
524             return null;
525
526         return sourceCode.createSourceCodeLocation(sourceLine || 0, sourceColumn || 0);
527     }
528
529     _parseSourceRangePayload(payload, text)
530     {
531         if (!payload)
532             return null;
533
534         // COMPATIBILITY (iOS 6): The range use to only contain start and end offsets. Now it
535         // has line and column for the start and end position. Support both here.
536         if ("start" in payload && "end" in payload) {
537             var textRange = new WebInspector.TextRange(payload.start, payload.end);
538             if (typeof text === "string")
539                 textRange.resolveLinesAndColumns(text);
540             return textRange;
541         }
542
543         return new WebInspector.TextRange(payload.startLine, payload.startColumn, payload.endLine, payload.endColumn);
544     }
545
546     _parseStylePropertyPayload(payload, index, styleDeclaration, styleText)
547     {
548         var text = payload.text || "";
549         var name = payload.name;
550         var value = (payload.value || "").replace(/\s*!important\s*$/, "");
551         var priority = payload.priority || "";
552
553         var enabled = true;
554         var overridden = false;
555         var implicit = payload.implicit || false;
556         var anonymous = false;
557         var valid = "parsedOk" in payload ? payload.parsedOk : true;
558
559         switch (payload.status || "style") {
560         case "active":
561             enabled = true;
562             break;
563         case "inactive":
564             overridden = true;
565             enabled = true;
566             break;
567         case "disabled":
568             enabled = false;
569             break;
570         case "style":
571             // FIXME: Is this still needed? This includes UserAgent styles and HTML attribute styles.
572             anonymous = true;
573             break;
574         }
575
576         var styleSheetTextRange = null;
577         var styleDeclarationTextRange = null;
578
579         // COMPATIBILITY (iOS 6): The range is in the style text, not the whole stylesheet.
580         // Later the range was changed to be in the whole stylesheet.
581         if (payload.range && "start" in payload.range && "end" in payload.range)
582             styleDeclarationTextRange = this._parseSourceRangePayload(payload.range, styleText);
583         else
584             styleSheetTextRange = this._parseSourceRangePayload(payload.range);
585
586         if (styleDeclaration) {
587             // Use propertyForName when the index is NaN since propertyForName is fast in that case.
588             var property = isNaN(index) ? styleDeclaration.propertyForName(name, true) : styleDeclaration.properties[index];
589
590             // Reuse a property if the index and name matches. Otherwise it is a different property
591             // and should be created from scratch. This works in the simple cases where only existing
592             // properties change in place and no properties are inserted or deleted at the beginning.
593             // FIXME: This could be smarter by ignoring index and just go by name. However, that gets
594             // tricky for rules that have more than one property with the same name.
595             if (property && property.name === name && (property.index === index || (isNaN(property.index) && isNaN(index)))) {
596                 property.update(text, name, value, priority, enabled, overridden, implicit, anonymous, valid, styleSheetTextRange, styleDeclarationTextRange);
597                 return property;
598             }
599
600             // Reuse a pending property with the same name. These properties are pending being committed,
601             // so if we find a match that likely means it got committed and we should use it.
602             var pendingProperties = styleDeclaration.pendingProperties;
603             for (var i = 0; i < pendingProperties.length; ++i) {
604                 var pendingProperty = pendingProperties[i];
605                 if (pendingProperty.name === name && isNaN(pendingProperty.index)) {
606                     pendingProperty.index = index;
607                     pendingProperty.update(text, name, value, priority, enabled, overridden, implicit, anonymous, valid, styleSheetTextRange, styleDeclarationTextRange);
608                     return pendingProperty;
609                 }
610             }
611         }
612
613         return new WebInspector.CSSProperty(index, text, name, value, priority, enabled, overridden, implicit, anonymous, valid, styleSheetTextRange, styleDeclarationTextRange);
614     }
615
616     _parseStyleDeclarationPayload(payload, node, inherited, type, rule, updateAllStyles)
617     {
618         if (!payload)
619             return null;
620
621         rule = rule || null;
622         inherited = inherited || false;
623
624         var id = payload.styleId;
625         var mapKey = id ? id.styleSheetId + ":" + id.ordinal : null;
626
627         if (type === WebInspector.CSSStyleDeclaration.Type.Attribute)
628             mapKey = node.id + ":attribute";
629
630         var styleDeclaration = rule ? rule.style : null;
631         var styleDeclarations = [];
632
633         // Look for existing styles in the previous map if there is one, otherwise use the current map.
634         var previousStyleDeclarationsMap = this._previousStyleDeclarationsMap || this._styleDeclarationsMap;
635         if (mapKey && mapKey in previousStyleDeclarationsMap) {
636             styleDeclarations = previousStyleDeclarationsMap[mapKey];
637
638             // If we need to update all styles, then stop here and call _parseStyleDeclarationPayload for each style.
639             // We need to parse multiple times so we reuse the right properties from each style.
640             if (updateAllStyles && styleDeclarations.length) {
641                 for (var i = 0; i < styleDeclarations.length; ++i) {
642                     var styleDeclaration = styleDeclarations[i];
643                     this._parseStyleDeclarationPayload(payload, styleDeclaration.node, styleDeclaration.inherited, styleDeclaration.type, styleDeclaration.ownerRule);
644                 }
645
646                 return null;
647             }
648
649             if (!styleDeclaration) {
650                 var filteredStyleDeclarations = styleDeclarations.filter(function(styleDeclaration) {
651                     // This case only applies for styles that are not part of a rule.
652                     if (styleDeclaration.ownerRule) {
653                         console.assert(!rule);
654                         return false;
655                     }
656
657                     if (styleDeclaration.node !== node)
658                         return false;
659
660                     if (styleDeclaration.inherited !== inherited)
661                         return false;
662
663                     return true;
664                 });
665
666                 console.assert(filteredStyleDeclarations.length <= 1);
667                 styleDeclaration = filteredStyleDeclarations[0] || null;
668             }
669         }
670
671         if (previousStyleDeclarationsMap !== this._styleDeclarationsMap) {
672             // If the previous and current maps differ then make sure the found styleDeclaration is added to the current map.
673             styleDeclarations = mapKey && mapKey in this._styleDeclarationsMap ? this._styleDeclarationsMap[mapKey] : [];
674
675             if (styleDeclaration && !styleDeclarations.includes(styleDeclaration)) {
676                 styleDeclarations.push(styleDeclaration);
677                 this._styleDeclarationsMap[mapKey] = styleDeclarations;
678             }
679         }
680
681         var shorthands = {};
682         for (var i = 0; payload.shorthandEntries && i < payload.shorthandEntries.length; ++i) {
683             var shorthand = payload.shorthandEntries[i];
684             shorthands[shorthand.name] = shorthand.value;
685         }
686
687         var text = payload.cssText;
688
689         var inheritedPropertyCount = 0;
690
691         var properties = [];
692         for (var i = 0; payload.cssProperties && i < payload.cssProperties.length; ++i) {
693             var propertyPayload = payload.cssProperties[i];
694
695             if (inherited && propertyPayload.name in WebInspector.CSSKeywordCompletions.InheritedProperties)
696                 ++inheritedPropertyCount;
697
698             var property = this._parseStylePropertyPayload(propertyPayload, i, styleDeclaration, text);
699             properties.push(property);
700         }
701
702         var styleSheetTextRange = this._parseSourceRangePayload(payload.range);
703
704         if (styleDeclaration) {
705             styleDeclaration.update(text, properties, styleSheetTextRange);
706             return styleDeclaration;
707         }
708
709         var styleSheet = id ? WebInspector.cssStyleManager.styleSheetForIdentifier(id.styleSheetId) : null;
710         if (styleSheet) {
711             if (type === WebInspector.CSSStyleDeclaration.Type.Inline)
712                 styleSheet.markAsInlineStyle();
713             styleSheet.addEventListener(WebInspector.CSSStyleSheet.Event.ContentDidChange, this._styleSheetContentDidChange, this);
714         }
715
716         if (inherited && !inheritedPropertyCount)
717             return null;
718
719         styleDeclaration = new WebInspector.CSSStyleDeclaration(this, styleSheet, id, type, node, inherited, text, properties, styleSheetTextRange);
720
721         if (mapKey) {
722             styleDeclarations.push(styleDeclaration);
723             this._styleDeclarationsMap[mapKey] = styleDeclarations;
724         }
725
726         return styleDeclaration;
727     }
728
729     _parseSelectorListPayload(selectorList)
730     {
731         // COMPATIBILITY (iOS 6): The payload did not have 'selectorList'.
732         if (!selectorList)
733             return [];
734
735         var selectors = selectorList.selectors;
736         if (!selectors.length)
737             return [];
738
739         // COMPATIBILITY (iOS 8): The selectorList payload was an array of selector text strings.
740         // Now they are CSSSelector objects with multiple properties.
741         if (typeof selectors[0] === "string") {
742             return selectors.map(function(selectorText) {
743                 return new WebInspector.CSSSelector(selectorText);
744             });
745         }
746
747         return selectors.map(function(selectorPayload) {
748             return new WebInspector.CSSSelector(selectorPayload.text, selectorPayload.specificity, selectorPayload.dynamic);
749         });
750     }
751
752     _parseRulePayload(payload, matchedSelectorIndices, node, inherited, ruleOccurrences)
753     {
754         if (!payload)
755             return null;
756
757         // User and User Agent rules don't have 'ruleId' in the payload. However, their style's have 'styleId' and
758         // 'styleId' is the same identifier the backend uses for Author rule identifiers, so do the same here.
759         // They are excluded by the backend because they are not editable, however our front-end does not determine
760         // editability solely based on the existence of the id like the open source front-end does.
761         var id = payload.ruleId || payload.style.styleId;
762
763         var mapKey = id ? id.styleSheetId + ":" + id.ordinal + ":" + (inherited ? "I" : "N") + ":" + node.id : null;
764
765         // Rules can match multiple times if they have multiple selectors or because of inheritance. We keep a count
766         // of occurrences so we have unique rules per occurrence, that way properties will be correctly marked as overridden.
767         var occurrence = 0;
768         if (mapKey) {
769             if (mapKey in ruleOccurrences)
770                 occurrence = ++ruleOccurrences[mapKey];
771             else
772                 ruleOccurrences[mapKey] = occurrence;
773
774             // Append the occurrence number to the map key for lookup in the rules map.
775             mapKey += ":" + occurrence;
776         }
777
778         var rule = null;
779
780         // Look for existing rules in the previous map if there is one, otherwise use the current map.
781         var previousRulesMap = this._previousRulesMap || this._rulesMap;
782         if (mapKey && mapKey in previousRulesMap) {
783             rule = previousRulesMap[mapKey];
784
785             if (previousRulesMap !== this._rulesMap) {
786                 // If the previous and current maps differ then make sure the found rule is added to the current map.
787                 this._rulesMap[mapKey] = rule;
788             }
789         }
790
791         var style = this._parseStyleDeclarationPayload(payload.style, node, inherited, WebInspector.CSSStyleDeclaration.Type.Rule, rule);
792         if (!style)
793             return null;
794
795         // COMPATIBILITY (iOS 6): The payload had 'selectorText' as a property,
796         // now it has 'selectorList' with a 'text' property. Support both here.
797         var selectorText = payload.selectorList ? payload.selectorList.text : payload.selectorText;
798         var selectors = this._parseSelectorListPayload(payload.selectorList);
799
800         // COMPATIBILITY (iOS 6): The payload did not have 'selectorList'.
801         // Fallback to using 'sourceLine' without column information.
802         if (payload.selectorList && payload.selectorList.range) {
803             var sourceRange = payload.selectorList.range;
804             var sourceCodeLocation = this._createSourceCodeLocation(payload.sourceURL, sourceRange.startLine, sourceRange.startColumn);
805         } else
806             var sourceCodeLocation = this._createSourceCodeLocation(payload.sourceURL, payload.sourceLine);
807
808         var type;
809         switch (payload.origin) {
810         case "regular":
811             type = WebInspector.CSSRule.Type.Author;
812             break;
813         case "user":
814             type = WebInspector.CSSRule.Type.User;
815             break;
816         case "user-agent":
817             type = WebInspector.CSSRule.Type.UserAgent;
818             break;
819         case "inspector":
820             type = WebInspector.CSSRule.Type.Inspector;
821             break;
822         }
823
824         var mediaList = [];
825         for (var i = 0; payload.media && i < payload.media.length; ++i) {
826             var mediaItem = payload.media[i];
827
828             var mediaType;
829             switch (mediaItem.source) {
830             case "mediaRule":
831                 mediaType = WebInspector.CSSMedia.Type.MediaRule;
832                 break;
833             case "importRule":
834                 mediaType = WebInspector.CSSMedia.Type.ImportRule;
835                 break;
836             case "linkedSheet":
837                 mediaType = WebInspector.CSSMedia.Type.LinkedStyleSheet;
838                 break;
839             case "inlineSheet":
840                 mediaType = WebInspector.CSSMedia.Type.InlineStyleSheet;
841                 break;
842             }
843
844             var mediaText = mediaItem.text;
845             var mediaSourceCodeLocation = this._createSourceCodeLocation(mediaItem.sourceURL, mediaItem.sourceLine);
846
847             mediaList.push(new WebInspector.CSSMedia(mediaType, mediaText, mediaSourceCodeLocation));
848         }
849
850         if (rule) {
851             rule.update(sourceCodeLocation, selectorText, selectors, matchedSelectorIndices, style, mediaList);
852             return rule;
853         }
854
855         var styleSheet = id ? WebInspector.cssStyleManager.styleSheetForIdentifier(id.styleSheetId) : null;
856         if (styleSheet)
857             styleSheet.addEventListener(WebInspector.CSSStyleSheet.Event.ContentDidChange, this._styleSheetContentDidChange, this);
858
859         rule = new WebInspector.CSSRule(this, styleSheet, id, type, sourceCodeLocation, selectorText, selectors, matchedSelectorIndices, style, mediaList);
860
861         if (mapKey)
862             this._rulesMap[mapKey] = rule;
863
864         return rule;
865     }
866
867     _markAsNeedsRefresh()
868     {
869         this._needsRefresh = true;
870         this.dispatchEventToListeners(WebInspector.DOMNodeStyles.Event.NeedsRefresh);
871     }
872
873     _styleSheetContentDidChange(event)
874     {
875         var styleSheet = event.target;
876         console.assert(styleSheet);
877         if (!styleSheet)
878             return;
879
880         // Ignore the stylesheet we know we just changed and handled above.
881         if (styleSheet === this._ignoreNextContentDidChangeForStyleSheet) {
882             delete this._ignoreNextContentDidChangeForStyleSheet;
883             return;
884         }
885
886         this._markAsNeedsRefresh();
887     }
888
889     _updateStyleCascade()
890     {
891         var cascadeOrderedStyleDeclarations = this._collectStylesInCascadeOrder(this._matchedRules, this._inlineStyle, this._attributesStyle);
892
893         for (var i = 0; i < this._inheritedRules.length; ++i) {
894             var inheritedStyleInfo = this._inheritedRules[i];
895             var inheritedCascadeOrder = this._collectStylesInCascadeOrder(inheritedStyleInfo.matchedRules, inheritedStyleInfo.inlineStyle, null);
896             cascadeOrderedStyleDeclarations = cascadeOrderedStyleDeclarations.concat(inheritedCascadeOrder);
897         }
898
899         this._orderedStyles = cascadeOrderedStyleDeclarations;
900
901         this._propertyNameToEffectivePropertyMap = {};
902
903         this._markOverriddenProperties(cascadeOrderedStyleDeclarations, this._propertyNameToEffectivePropertyMap);
904         this._associateRelatedProperties(cascadeOrderedStyleDeclarations, this._propertyNameToEffectivePropertyMap);
905
906         for (var pseudoIdentifier in this._pseudoElements) {
907             var pseudoElementInfo = this._pseudoElements[pseudoIdentifier];
908             pseudoElementInfo.orderedStyles = this._collectStylesInCascadeOrder(pseudoElementInfo.matchedRules, null, null);
909             this._markOverriddenProperties(pseudoElementInfo.orderedStyles);
910             this._associateRelatedProperties(pseudoElementInfo.orderedStyles);
911         }
912     }
913
914     _collectStylesInCascadeOrder(matchedRules, inlineStyle, attributesStyle)
915     {
916         var result = [];
917
918         // Inline style has the greatest specificity. So it goes first in the cascade order.
919         if (inlineStyle)
920             result.push(inlineStyle);
921
922         var userAndUserAgentStyles = [];
923
924         for (var i = 0; i < matchedRules.length; ++i) {
925             var rule = matchedRules[i];
926
927             // Only append to the result array here for author and inspector rules since attribute
928             // styles come between author rules and user/user agent rules.
929             switch (rule.type) {
930             case WebInspector.CSSRule.Type.Inspector:
931             case WebInspector.CSSRule.Type.Author:
932                 result.push(rule.style);
933                 break;
934
935             case WebInspector.CSSRule.Type.User:
936             case WebInspector.CSSRule.Type.UserAgent:
937                 userAndUserAgentStyles.push(rule.style);
938                 break;
939             }
940         }
941
942         // Style properties from HTML attributes are next.
943         if (attributesStyle)
944             result.push(attributesStyle);
945
946         // Finally add the user and user stylesheet's matched style rules we collected earlier.
947         result = result.concat(userAndUserAgentStyles);
948
949         return result;
950     }
951
952     _markOverriddenProperties(styles, propertyNameToEffectiveProperty)
953     {
954         propertyNameToEffectiveProperty = propertyNameToEffectiveProperty || {};
955
956         for (var i = 0; i < styles.length; ++i) {
957             var style = styles[i];
958             var properties = style.properties;
959
960             for (var j = 0; j < properties.length; ++j) {
961                 var property = properties[j];
962                 if (!property.enabled || !property.valid) {
963                     property.overridden = false;
964                     continue;
965                 }
966
967                 if (style.inherited && !property.inherited) {
968                     property.overridden = false;
969                     continue;
970                 }
971
972                 var canonicalName = property.canonicalName;
973                 if (canonicalName in propertyNameToEffectiveProperty) {
974                     var effectiveProperty = propertyNameToEffectiveProperty[canonicalName];
975
976                     if (effectiveProperty.ownerStyle === property.ownerStyle) {
977                         if (effectiveProperty.important && !property.important) {
978                             property.overridden = true;
979                             continue;
980                         }
981                     } else if (effectiveProperty.important || !property.important || effectiveProperty.ownerStyle.node !== property.ownerStyle.node) {
982                         property.overridden = true;
983                         continue;
984                     }
985
986                     if (!property.anonymous)
987                         effectiveProperty.overridden = true;
988                 }
989
990                 property.overridden = false;
991
992                 propertyNameToEffectiveProperty[canonicalName] = property;
993             }
994         }
995     }
996
997     _associateRelatedProperties(styles, propertyNameToEffectiveProperty)
998     {
999         for (var i = 0; i < styles.length; ++i) {
1000             var properties = styles[i].properties;
1001
1002             var knownShorthands = {};
1003
1004             for (var j = 0; j < properties.length; ++j) {
1005                 var property = properties[j];
1006
1007                 if (!property.valid)
1008                     continue;
1009
1010                 if (!WebInspector.CSSCompletions.cssNameCompletions.isShorthandPropertyName(property.name))
1011                     continue;
1012
1013                 if (knownShorthands[property.canonicalName] && !knownShorthands[property.canonicalName].overridden) {
1014                     console.assert(property.overridden);
1015                     continue;
1016                 }
1017
1018                 knownShorthands[property.canonicalName] = property;
1019             }
1020
1021             for (var j = 0; j < properties.length; ++j) {
1022                 var property = properties[j];
1023
1024                 if (!property.valid)
1025                     continue;
1026
1027                 var shorthandProperty = null;
1028
1029                 if (!isEmptyObject(knownShorthands)) {
1030                     var possibleShorthands = WebInspector.CSSCompletions.cssNameCompletions.shorthandsForLonghand(property.canonicalName);
1031                     for (var k = 0; k < possibleShorthands.length; ++k) {
1032                         if (possibleShorthands[k] in knownShorthands) {
1033                             shorthandProperty = knownShorthands[possibleShorthands[k]];
1034                             break;
1035                         }
1036                     }
1037                 }
1038
1039                 if (!shorthandProperty || shorthandProperty.overridden !== property.overridden) {
1040                     property.relatedShorthandProperty = null;
1041                     property.clearRelatedLonghandProperties();
1042                     continue;
1043                 }
1044
1045                 shorthandProperty.addRelatedLonghandProperty(property);
1046                 property.relatedShorthandProperty = shorthandProperty;
1047
1048                 if (propertyNameToEffectiveProperty && propertyNameToEffectiveProperty[shorthandProperty.canonicalName] === shorthandProperty)
1049                     propertyNameToEffectiveProperty[property.canonicalName] = property;
1050             }
1051         }
1052     }
1053 };
1054
1055 WebInspector.DOMNodeStyles.Event = {
1056     NeedsRefresh: "dom-node-styles-needs-refresh",
1057     Refreshed: "dom-node-styles-refreshed"
1058 };