Web Inspector: Changes are not applied in CSS sidebar when switching to Resources...
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / RulesStyleDetailsPanel.js
1 /*
2  * Copyright (C) 2013, 2015 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.RulesStyleDetailsPanel = class RulesStyleDetailsPanel extends WebInspector.StyleDetailsPanel
27 {
28     constructor(delegate)
29     {
30         super(delegate, "rules", "rules", WebInspector.UIString("Styles \u2014 Rules"));
31
32         this._sections = [];
33         this._inspectorSection = null;
34         this._isInspectorSectionPendingFocus = false;
35         this._ruleMediaAndInherticanceList = [];
36         this._propertyToSelectAndHighlight = null;
37
38         this._emptyFilterResultsElement = document.createElement("div");
39         this._emptyFilterResultsElement.classList.add("no-filter-results");
40
41         this._emptyFilterResultsMessage = document.createElement("div");
42         this._emptyFilterResultsMessage.classList.add("no-filter-results-message");
43         this._emptyFilterResultsMessage.textContent = WebInspector.UIString("No Results Found");
44         this._emptyFilterResultsElement.appendChild(this._emptyFilterResultsMessage);
45
46         this._boundRemoveSectionWithActiveEditor = this._removeSectionWithActiveEditor.bind(this);
47     }
48
49     // Public
50
51     refresh(significantChange)
52     {
53         // We only need to do a rebuild on significant changes. Other changes are handled
54         // by the sections and text editors themselves.
55         if (!significantChange) {
56             super.refresh();
57             return;
58         }
59
60         if (!this._forceSignificantChange) {
61             this._sectionWithActiveEditor = null;
62             for (var section of this._sections) {
63                 if (!section.editorActive)
64                     continue;
65
66                 this._sectionWithActiveEditor = section;
67                 break;
68             }
69
70             if (this._sectionWithActiveEditor) {
71                 this._sectionWithActiveEditor.addEventListener(WebInspector.CSSStyleDeclarationSection.Event.Blurred, this._boundRemoveSectionWithActiveEditor);
72                 return;
73             }
74         }
75
76         var newSections = [];
77         var newDOMFragment = document.createDocumentFragment();
78
79         var previousMediaList = [];
80         var previousSection = null;
81
82         var pseudoElements = this.nodeStyles.pseudoElements;
83         var pseudoElementsStyle = [];
84         for (var pseudoIdentifier in pseudoElements)
85             pseudoElementsStyle = pseudoElementsStyle.concat(pseudoElements[pseudoIdentifier].orderedStyles);
86
87         var orderedPseudoStyles = uniqueOrderedStyles(pseudoElementsStyle);
88         // Reverse the array to allow ensure that splicing the array will not mess with the order.
89         if (orderedPseudoStyles.length)
90             orderedPseudoStyles.reverse();
91
92         function mediaListsEqual(a, b)
93         {
94             a = a || [];
95             b = b || [];
96
97             if (a.length !== b.length)
98                 return false;
99
100             for (var i = 0; i < a.length; ++i) {
101                 var aMedia = a[i];
102                 var bMedia = b[i];
103
104                 if (aMedia.type !== bMedia.type)
105                     return false;
106
107                 if (aMedia.text !== bMedia.text)
108                     return false;
109
110                 if (!aMedia.sourceCodeLocation && bMedia.sourceCodeLocation)
111                     return false;
112
113                 if (aMedia.sourceCodeLocation && !aMedia.sourceCodeLocation.isEqual(bMedia.sourceCodeLocation))
114                     return false;
115             }
116
117             return true;
118         }
119
120         function uniqueOrderedStyles(orderedStyles)
121         {
122             var uniqueStyles = [];
123
124             for (var style of orderedStyles) {
125                 var rule = style.ownerRule;
126                 if (!rule) {
127                     uniqueStyles.push(style);
128                     continue;
129                 }
130
131                 var found = false;
132                 for (var existingStyle of uniqueStyles) {
133                     if (rule.isEqualTo(existingStyle.ownerRule)) {
134                         found = true;
135                         break;
136                     }
137                 }
138                 if (!found)
139                     uniqueStyles.push(style);
140             }
141
142             return uniqueStyles;
143         }
144
145         function appendStyleSection(style)
146         {
147             var section = style.__rulesSection;
148
149             if (!section) {
150                 section = new WebInspector.CSSStyleDeclarationSection(this, style);
151                 style.__rulesSection = section;
152             } else
153                 section.refresh();
154
155             if (this._isInspectorSectionPendingFocus && style.isInspectorRule())
156                 this._inspectorSection = section;
157
158             // Reset lastInGroup in case the order/grouping changed.
159             section.lastInGroup = false;
160
161             newDOMFragment.appendChild(section.element);
162             newSections.push(section);
163
164             previousSection = section;
165         }
166
167         function insertMediaOrInheritanceLabel(style)
168         {
169             if (previousSection && previousSection.style.type === WebInspector.CSSStyleDeclaration.Type.Inline)
170                 previousSection.lastInGroup = true;
171
172             var hasMediaOrInherited = [];
173
174             if (previousSection && previousSection.style.node !== style.node) {
175                 previousSection.lastInGroup = true;
176
177                 var prefixElement = document.createElement("strong");
178                 prefixElement.textContent = WebInspector.UIString("Inherited From: ");
179
180                 var inheritedLabel = document.createElement("div");
181                 inheritedLabel.className = "label";
182                 inheritedLabel.appendChild(prefixElement);
183                 inheritedLabel.appendChild(WebInspector.linkifyNodeReference(style.node, 100));
184                 newDOMFragment.appendChild(inheritedLabel);
185
186                 hasMediaOrInherited.push(inheritedLabel);
187             }
188
189             // Only include the media list if it is different from the previous media list shown.
190             var currentMediaList = (style.ownerRule && style.ownerRule.mediaList) || [];
191             if (!mediaListsEqual(previousMediaList, currentMediaList)) {
192                 previousMediaList = currentMediaList;
193
194                 // Break the section group even if the media list is empty. That way the user knows
195                 // the previous displayed media list does not apply to the next section.
196                 if (previousSection)
197                     previousSection.lastInGroup = true;
198
199                 for (var media of currentMediaList) {
200                     var prefixElement = document.createElement("strong");
201                     prefixElement.textContent = WebInspector.UIString("Media: ");
202
203                     var mediaLabel = document.createElement("div");
204                     mediaLabel.className = "label";
205                     mediaLabel.append(prefixElement, media.text);
206
207                     if (media.sourceCodeLocation) {
208                         const options = {
209                             dontFloat: true,
210                             ignoreNetworkTab: true,
211                             ignoreSearchTab: true,
212                         };
213                         mediaLabel.append(" \u2014 ", WebInspector.createSourceCodeLocationLink(media.sourceCodeLocation, options));
214                     }
215
216                     newDOMFragment.appendChild(mediaLabel);
217
218                     hasMediaOrInherited.push(mediaLabel);
219                 }
220             }
221
222             if (!hasMediaOrInherited.length && style.type !== WebInspector.CSSStyleDeclaration.Type.Inline) {
223                 if (previousSection && !previousSection.lastInGroup)
224                     hasMediaOrInherited = this._ruleMediaAndInherticanceList.lastValue;
225                 else {
226                     var prefixElement = document.createElement("strong");
227                     prefixElement.textContent = WebInspector.UIString("Media: ");
228
229                     var mediaLabel = document.createElement("div");
230                     mediaLabel.className = "label";
231                     mediaLabel.append(prefixElement, "all");
232
233                     newDOMFragment.appendChild(mediaLabel);
234                     hasMediaOrInherited.push(mediaLabel);
235                 }
236             }
237
238             this._ruleMediaAndInherticanceList.push(hasMediaOrInherited);
239         }
240
241         function insertAllMatchingPseudoStyles(force)
242         {
243             if (!orderedPseudoStyles.length)
244                 return;
245
246             if (force) {
247                 for (var j = orderedPseudoStyles.length - 1; j >= 0; --j) {
248                     var pseudoStyle = orderedPseudoStyles[j];
249                     insertMediaOrInheritanceLabel.call(this, pseudoStyle);
250                     appendStyleSection.call(this, pseudoStyle);
251                 }
252                 orderedPseudoStyles = [];
253             }
254
255             if (!previousSection)
256                 return;
257
258             var ownerRule = previousSection.style.ownerRule;
259             if (!ownerRule)
260                 return;
261
262             for (var j = orderedPseudoStyles.length - 1; j >= 0; --j) {
263                 var pseudoStyle = orderedPseudoStyles[j];
264                 if (!pseudoStyle.ownerRule.selectorIsGreater(ownerRule.mostSpecificSelector))
265                     continue;
266
267                 insertMediaOrInheritanceLabel.call(this, pseudoStyle);
268                 appendStyleSection.call(this, pseudoStyle);
269                 ownerRule = pseudoStyle.ownerRule;
270                 orderedPseudoStyles.splice(j, 1);
271             }
272         }
273
274         this._ruleMediaAndInherticanceList = [];
275         var orderedStyles = uniqueOrderedStyles(this.nodeStyles.orderedStyles);
276         for (var style of orderedStyles) {
277             var isUserAgentStyle = style.ownerRule && style.ownerRule.type === WebInspector.CSSStyleSheet.Type.UserAgent;
278             insertAllMatchingPseudoStyles.call(this, isUserAgentStyle || style.inherited);
279
280             insertMediaOrInheritanceLabel.call(this, style);
281             appendStyleSection.call(this, style);
282         }
283
284         // Just in case there are any pseudo-selectors left that haven't been added.
285         insertAllMatchingPseudoStyles.call(this, true);
286
287         if (previousSection)
288             previousSection.lastInGroup = true;
289
290         this.element.removeChildren();
291         this.element.appendChild(newDOMFragment);
292         this.element.appendChild(this._emptyFilterResultsElement);
293
294         this._sections = newSections;
295
296         for (var i = 0; i < this._sections.length; ++i)
297             this._sections[i].updateLayout();
298
299         super.refresh();
300     }
301
302     scrollToSectionAndHighlightProperty(property)
303     {
304         if (!this._visible) {
305             this._propertyToSelectAndHighlight = property;
306             return false;
307         }
308
309         for (var section of this._sections) {
310             if (section.highlightProperty(property))
311                 return true;
312         }
313
314         return false;
315     }
316
317     cssStyleDeclarationSectionEditorFocused(focusedSection)
318     {
319         for (let section of this._sections) {
320             if (section !== focusedSection)
321                 section.clearSelection();
322         }
323
324         this._sectionWithActiveEditor = focusedSection;
325     }
326
327     cssStyleDeclarationSectionEditorNextRule(currentSection)
328     {
329         currentSection.clearSelection();
330
331         var index = this._sections.indexOf(currentSection);
332         this._sections[index < this._sections.length - 1 ? index + 1 : 0].focusRuleSelector();
333     }
334
335     cssStyleDeclarationSectionEditorPreviousRule(currentSection, selectLastProperty) {
336         currentSection.clearSelection();
337
338         if (selectLastProperty || !currentSection.selectorEditable) {
339             var index = this._sections.indexOf(currentSection);
340             index = index > 0 ? index - 1 : this._sections.length - 1;
341
342             var section = this._sections[index];
343             while (section.locked) {
344                 index = index > 0 ? index - 1 : this._sections.length - 1;
345                 section = this._sections[index];
346             }
347
348             section.focus();
349             section.selectLastProperty();
350             return;
351         }
352
353         currentSection.focusRuleSelector(true);
354     }
355
356     filterDidChange(filterBar)
357     {
358         for (var labels of this._ruleMediaAndInherticanceList) {
359             for (var i = 0; i < labels.length; ++i) {
360                 labels[i].classList.toggle(WebInspector.CSSStyleDetailsSidebarPanel.NoFilterMatchInSectionClassName, filterBar.hasActiveFilters());
361
362                 if (i === labels.length - 1)
363                     labels[i].classList.toggle("filter-matching-label", filterBar.hasActiveFilters());
364             }
365         }
366
367         var matchFound = !filterBar.hasActiveFilters();
368         for (var i = 0; i < this._sections.length; ++i) {
369             var section = this._sections[i];
370
371             if (section.findMatchingPropertiesAndSelectors(filterBar.filters.text) && filterBar.hasActiveFilters()) {
372                 if (this._ruleMediaAndInherticanceList[i].length) {
373                     for (var label of this._ruleMediaAndInherticanceList[i])
374                         label.classList.remove(WebInspector.CSSStyleDetailsSidebarPanel.NoFilterMatchInSectionClassName);
375                 } else
376                     section.element.classList.add(WebInspector.CSSStyleDetailsSidebarPanel.FilterMatchingSectionHasLabelClassName);
377
378                 matchFound = true;
379             }
380         }
381
382         this.element.classList.toggle("filter-non-matching", !matchFound);
383     }
384
385     newRuleButtonClicked()
386     {
387         if (this.nodeStyles.node.isInUserAgentShadowTree())
388             return;
389
390         for (let existingRule of this.nodeStyles.rulesForSelector()) {
391             if (this.focusEmptySectionWithStyle(existingRule.style))
392                 return;
393         }
394
395         this._isInspectorSectionPendingFocus = true;
396         let newInspectorRuleSelector = this.nodeStyles.node.appropriateSelectorFor(true);
397         this.nodeStyles.addRule(newInspectorRuleSelector);
398     }
399
400     sectionForStyle(style)
401     {
402         if (style.__rulesSection)
403             return style.__rulesSection;
404
405         for (let section of this._sections) {
406             if (section.style === style)
407                 return section;
408         }
409         return null;
410     }
411
412     focusEmptySectionWithStyle(style)
413     {
414         if (style.hasProperties())
415             return false;
416
417         let section = this.sectionForStyle(style);
418         if (!section)
419             return false;
420
421         section.focus();
422         return true;
423     }
424
425     // Protected
426
427     shown()
428     {
429         super.shown();
430
431         // Associate the style and section objects so they can be reused.
432         // Also update the layout in case we changed widths while hidden.
433         for (var i = 0; i < this._sections.length; ++i) {
434             var section = this._sections[i];
435             section.style.__rulesSection = section;
436             section.updateLayout();
437         }
438
439         // If there was an active section and the panel was hidden, refresh the section in case
440         // changes were made to the underlying resource.
441         if (this._sectionWithActiveEditor)
442             this._sectionWithActiveEditor.refreshEditor();
443     }
444
445     hidden()
446     {
447         super.hidden();
448
449         // Disconnect the style and section objects so they have a chance
450         // to release their objects when this panel is not visible.
451         for (var i = 0; i < this._sections.length; ++i)
452             delete this._sections[i].style.__rulesSection;
453     }
454
455     sizeDidChange()
456     {
457         for (var i = 0; i < this._sections.length; ++i)
458             this._sections[i].updateLayout();
459     }
460
461     nodeStylesRefreshed(event)
462     {
463         super.nodeStylesRefreshed(event);
464
465         if (this._propertyToSelectAndHighlight) {
466             this.scrollToSectionAndHighlightProperty(this._propertyToSelectAndHighlight);
467             this._propertyToSelectAndHighlight = null;
468         }
469
470         if (this._isInspectorSectionPendingFocus) {
471             this._isInspectorSectionPendingFocus = false;
472
473             if (this._inspectorSection) {
474                 this._inspectorSection.focus();
475                 this._inspectorSection = null;
476             }
477         }
478     }
479
480     // Private
481
482     _removeSectionWithActiveEditor(event)
483     {
484         this._sectionWithActiveEditor.removeEventListener(WebInspector.CSSStyleDeclarationSection.Event.Blurred, this._boundRemoveSectionWithActiveEditor);
485         this.refresh(true);
486     }
487 };