9bb81708da850bed278a2f33d9bf12ae036cff95
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / CSSStyleDetailsSidebarPanel.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 WI.CSSStyleDetailsSidebarPanel = class CSSStyleDetailsSidebarPanel extends WI.DOMDetailsSidebarPanel
27 {
28     constructor()
29     {
30         const dontCreateNavigationItem = true;
31         super("css-style", WI.UIString("Styles"), dontCreateNavigationItem);
32
33         this._selectedPanel = null;
34         this._computedStyleDetailsPanel = new WI.ComputedStyleDetailsPanel(this);
35         this._rulesStyleDetailsPanel = new WI.RulesStyleDetailsPanel(this);
36         this._visualStyleDetailsPanel = new WI.VisualStyleDetailsPanel(this);
37
38         if (WI.settings.experimentalSpreadsheetStyleEditor.value)
39             this._activeRulesStyleDetailsPanel = new WI.SpreadsheetRulesStyleDetailsPanel(this);
40         else
41             this._activeRulesStyleDetailsPanel = this._rulesStyleDetailsPanel;
42
43         this._panels = [this._computedStyleDetailsPanel, this._activeRulesStyleDetailsPanel, this._visualStyleDetailsPanel];
44         this._panelNavigationInfo = [this._computedStyleDetailsPanel.navigationInfo, this._activeRulesStyleDetailsPanel.navigationInfo, this._visualStyleDetailsPanel.navigationInfo];
45
46         this._lastSelectedPanelSetting = new WI.Setting("last-selected-style-details-panel", this._activeRulesStyleDetailsPanel.navigationInfo.identifier);
47         this._classListContainerToggledSetting = new WI.Setting("class-list-container-toggled", false);
48
49         this._initiallySelectedPanel = this._panelMatchingIdentifier(this._lastSelectedPanelSetting.value) || this._activeRulesStyleDetailsPanel;
50
51         this._navigationItem = new WI.ScopeRadioButtonNavigationItem(this.identifier, this.displayName, this._panelNavigationInfo, this._initiallySelectedPanel.navigationInfo);
52         this._navigationItem.addEventListener(WI.ScopeRadioButtonNavigationItem.Event.SelectedItemChanged, this._handleSelectedItemChanged, this);
53
54         this._forcedPseudoClassCheckboxes = {};
55     }
56
57     // Public
58
59     supportsDOMNode(nodeToInspect)
60     {
61         return nodeToInspect.nodeType() === Node.ELEMENT_NODE;
62     }
63
64     visibilityDidChange()
65     {
66         super.visibilityDidChange();
67
68         if (!this._selectedPanel)
69             return;
70
71         if (!this.visible) {
72             this._selectedPanel.hidden();
73             return;
74         }
75
76         this._updateNoForcedPseudoClassesScrollOffset();
77
78         this._selectedPanel.shown();
79         this._selectedPanel.markAsNeedsRefresh(this.domNode);
80     }
81
82     computedStyleDetailsPanelShowProperty(property)
83     {
84         this._activeRulesStyleDetailsPanel.scrollToSectionAndHighlightProperty(property);
85         this._switchPanels(this._activeRulesStyleDetailsPanel);
86
87         this._navigationItem.selectedItemIdentifier = this._lastSelectedPanelSetting.value;
88     }
89
90     // Protected
91
92     layout()
93     {
94         let domNode = this.domNode;
95         if (!domNode)
96             return;
97
98         this.contentView.element.scrollTop = this._initialScrollOffset;
99
100         for (let panel of this._panels) {
101             panel.element._savedScrollTop = undefined;
102             panel.markAsNeedsRefresh(domNode);
103         }
104
105         this._updatePseudoClassCheckboxes();
106
107         if (!this._classListContainer.hidden)
108             this._populateClassToggles();
109     }
110
111     addEventListeners()
112     {
113         let effectiveDOMNode = this.domNode.isPseudoElement() ? this.domNode.parentNode : this.domNode;
114         if (!effectiveDOMNode)
115             return;
116
117         effectiveDOMNode.addEventListener(WI.DOMNode.Event.EnabledPseudoClassesChanged, this._updatePseudoClassCheckboxes, this);
118         effectiveDOMNode.addEventListener(WI.DOMNode.Event.AttributeModified, this._handleNodeAttributeModified, this);
119         effectiveDOMNode.addEventListener(WI.DOMNode.Event.AttributeRemoved, this._handleNodeAttributeRemoved, this);
120     }
121
122     removeEventListeners()
123     {
124         let effectiveDOMNode = this.domNode.isPseudoElement() ? this.domNode.parentNode : this.domNode;
125         if (!effectiveDOMNode)
126             return;
127
128         effectiveDOMNode.removeEventListener(null, null, this);
129     }
130
131     initialLayout()
132     {
133         if (WI.cssStyleManager.canForcePseudoClasses()) {
134             this._forcedPseudoClassContainer = document.createElement("div");
135             this._forcedPseudoClassContainer.className = "pseudo-classes";
136
137             let groupElement = null;
138
139             WI.CSSStyleManager.ForceablePseudoClasses.forEach(function(pseudoClass) {
140                 // We don't localize the label since it is a CSS pseudo-class from the CSS standard.
141                 let label = pseudoClass.capitalize();
142
143                 let labelElement = document.createElement("label");
144
145                 let checkboxElement = document.createElement("input");
146                 checkboxElement.addEventListener("change", this._forcedPseudoClassCheckboxChanged.bind(this, pseudoClass));
147                 checkboxElement.type = "checkbox";
148
149                 this._forcedPseudoClassCheckboxes[pseudoClass] = checkboxElement;
150
151                 labelElement.appendChild(checkboxElement);
152                 labelElement.append(label);
153
154                 if (!groupElement || groupElement.children.length === 2) {
155                     groupElement = document.createElement("div");
156                     groupElement.className = "group";
157                     this._forcedPseudoClassContainer.appendChild(groupElement);
158                 }
159
160                 groupElement.appendChild(labelElement);
161             }, this);
162
163             this.contentView.element.appendChild(this._forcedPseudoClassContainer);
164         }
165
166         this._computedStyleDetailsPanel.addEventListener(WI.StyleDetailsPanel.Event.Refreshed, this._filterDidChange, this);
167         this._rulesStyleDetailsPanel.addEventListener(WI.StyleDetailsPanel.Event.Refreshed, this._filterDidChange, this);
168
169         console.assert(this._initiallySelectedPanel, "Should have an initially selected panel.");
170
171         this._switchPanels(this._initiallySelectedPanel);
172         this._initiallySelectedPanel = null;
173
174         let optionsContainer = this.element.createChild("div", "options-container");
175
176         let newRuleButton = optionsContainer.createChild("img", "new-rule");
177         newRuleButton.title = WI.UIString("Add new rule");
178         newRuleButton.addEventListener("click", this._newRuleButtonClicked.bind(this));
179         newRuleButton.addEventListener("contextmenu", this._newRuleButtonContextMenu.bind(this));
180
181         this._filterBar = new WI.FilterBar;
182         this._filterBar.addEventListener(WI.FilterBar.Event.FilterDidChange, this._filterDidChange, this);
183         optionsContainer.appendChild(this._filterBar.element);
184
185         this._classToggleButton = optionsContainer.createChild("button", "toggle-class-toggle");
186         this._classToggleButton.textContent = WI.UIString("Classes");
187         this._classToggleButton.title = WI.UIString("Toggle Classes");
188         this._classToggleButton.addEventListener("click", this._classToggleButtonClicked.bind(this));
189
190         this._classListContainer = this.element.createChild("div", "class-list-container");
191         this._classListContainer.hidden = true;
192
193         this._addClassContainer = this._classListContainer.createChild("div", "new-class");
194         this._addClassContainer.title = WI.UIString("Add a Class");
195         this._addClassContainer.addEventListener("click", this._addClassContainerClicked.bind(this));
196
197         this._addClassInput = this._addClassContainer.createChild("input", "class-name-input");
198         this._addClassInput.setAttribute("placeholder", WI.UIString("Add New Class"));
199         this._addClassInput.addEventListener("keypress", this._addClassInputKeyPressed.bind(this));
200         this._addClassInput.addEventListener("blur", this._addClassInputBlur.bind(this));
201
202         WI.cssStyleManager.addEventListener(WI.CSSStyleManager.Event.StyleSheetAdded, this._styleSheetAddedOrRemoved, this);
203         WI.cssStyleManager.addEventListener(WI.CSSStyleManager.Event.StyleSheetRemoved, this._styleSheetAddedOrRemoved, this);
204
205         if (this._classListContainerToggledSetting.value)
206             this._classToggleButtonClicked();
207     }
208
209     sizeDidChange()
210     {
211         super.sizeDidChange();
212
213         this._updateNoForcedPseudoClassesScrollOffset();
214
215         if (this._selectedPanel)
216             this._selectedPanel.sizeDidChange();
217     }
218
219     // Private
220
221     get _initialScrollOffset()
222     {
223         if (!WI.cssStyleManager.canForcePseudoClasses())
224             return 0;
225         return this.domNode && this.domNode.enabledPseudoClasses.length ? 0 : WI.CSSStyleDetailsSidebarPanel.NoForcedPseudoClassesScrollOffset;
226     }
227
228     _updateNoForcedPseudoClassesScrollOffset()
229     {
230         if (this._forcedPseudoClassContainer)
231             WI.CSSStyleDetailsSidebarPanel.NoForcedPseudoClassesScrollOffset = this._forcedPseudoClassContainer.offsetHeight;
232     }
233
234     _panelMatchingIdentifier(identifier)
235     {
236         let selectedPanel = null;
237         for (let panel of this._panels) {
238             if (panel.navigationInfo.identifier !== identifier)
239                 continue;
240
241             selectedPanel = panel;
242             break;
243         }
244
245         return selectedPanel;
246     }
247
248     _handleSelectedItemChanged()
249     {
250         let selectedIdentifier = this._navigationItem.selectedItemIdentifier;
251         let selectedPanel = this._panelMatchingIdentifier(selectedIdentifier);
252         this._switchPanels(selectedPanel);
253     }
254
255     _switchPanels(selectedPanel)
256     {
257         console.assert(selectedPanel);
258
259         if (this._selectedPanel) {
260             this._selectedPanel.hidden();
261             this._selectedPanel.element._savedScrollTop = this.contentView.element.scrollTop;
262             this.contentView.removeSubview(this._selectedPanel);
263         }
264
265         this._selectedPanel = selectedPanel;
266         if (!this._selectedPanel)
267             return;
268
269         this.contentView.addSubview(this._selectedPanel);
270
271         if (typeof this._selectedPanel.element._savedScrollTop === "number")
272             this.contentView.element.scrollTop = this._selectedPanel.element._savedScrollTop;
273         else
274             this.contentView.element.scrollTop = this._initialScrollOffset;
275
276         let hasFilter = typeof this._selectedPanel.filterDidChange === "function";
277         this.contentView.element.classList.toggle("has-filter-bar", hasFilter);
278         if (this._filterBar)
279             this.contentView.element.classList.toggle(WI.CSSStyleDetailsSidebarPanel.FilterInProgressClassName, hasFilter && this._filterBar.hasActiveFilters());
280
281         this.contentView.element.classList.toggle("supports-new-rule", typeof this._selectedPanel.newRuleButtonClicked === "function");
282         this._selectedPanel.shown();
283
284         this._lastSelectedPanelSetting.value = selectedPanel.navigationInfo.identifier;
285     }
286
287     _forcedPseudoClassCheckboxChanged(pseudoClass, event)
288     {
289         if (!this.domNode)
290             return;
291
292         let effectiveDOMNode = this.domNode.isPseudoElement() ? this.domNode.parentNode : this.domNode;
293
294         effectiveDOMNode.setPseudoClassEnabled(pseudoClass, event.target.checked);
295     }
296
297     _updatePseudoClassCheckboxes()
298     {
299         if (!this.domNode)
300             return;
301
302         let effectiveDOMNode = this.domNode.isPseudoElement() ? this.domNode.parentNode : this.domNode;
303
304         let enabledPseudoClasses = effectiveDOMNode.enabledPseudoClasses;
305
306         for (let pseudoClass in this._forcedPseudoClassCheckboxes) {
307             let checkboxElement = this._forcedPseudoClassCheckboxes[pseudoClass];
308             checkboxElement.checked = enabledPseudoClasses.includes(pseudoClass);
309         }
310     }
311
312     _handleNodeAttributeModified(event)
313     {
314         if (event && event.data && event.data.name === "class")
315             this._populateClassToggles();
316     }
317
318     _handleNodeAttributeRemoved(event)
319     {
320         if (event && event.data && event.data.name === "class")
321             this._populateClassToggles();
322     }
323
324
325     _newRuleButtonClicked()
326     {
327         if (this._selectedPanel && typeof this._selectedPanel.newRuleButtonClicked === "function")
328             this._selectedPanel.newRuleButtonClicked();
329     }
330
331     _newRuleButtonContextMenu(event)
332     {
333         if (this._selectedPanel && typeof this._selectedPanel.newRuleButtonContextMenu === "function")
334             this._selectedPanel.newRuleButtonContextMenu(event);
335     }
336
337     _classToggleButtonClicked(event)
338     {
339         this._classToggleButton.classList.toggle("selected");
340         this._classListContainer.hidden = !this._classListContainer.hidden;
341         this._classListContainerToggledSetting.value = !this._classListContainer.hidden;
342         if (this._classListContainer.hidden)
343             return;
344
345         this._populateClassToggles();
346     }
347
348     _addClassContainerClicked(event)
349     {
350         this._addClassContainer.classList.add("active");
351         this._addClassInput.focus();
352     }
353
354     _addClassInputKeyPressed(event)
355     {
356         if (event.keyCode !== WI.KeyboardShortcut.Key.Enter.keyCode)
357             return;
358
359         this._addClassInput.blur();
360     }
361
362     _addClassInputBlur(event)
363     {
364         this.domNode.toggleClass(this._addClassInput.value, true);
365         this._addClassContainer.classList.remove("active");
366         this._addClassInput.value = null;
367     }
368
369     _populateClassToggles()
370     {
371         // Ensure that _addClassContainer is the first child of _classListContainer.
372         while (this._classListContainer.children.length > 1)
373             this._classListContainer.children[1].remove();
374
375         let classes = this.domNode.getAttribute("class");
376         let classToggledMap = this.domNode[WI.CSSStyleDetailsSidebarPanel.ToggledClassesSymbol];
377         if (!classToggledMap)
378             classToggledMap = this.domNode[WI.CSSStyleDetailsSidebarPanel.ToggledClassesSymbol] = new Map;
379
380         if (classes && classes.length) {
381             for (let className of classes.split(/\s+/))
382                 classToggledMap.set(className, true);
383         }
384
385         for (let [className, toggled] of classToggledMap) {
386             if ((toggled && !classes.includes(className)) || (!toggled && classes.includes(className))) {
387                 toggled = !toggled;
388                 classToggledMap.set(className, toggled);
389             }
390
391             this._createToggleForClassName(className);
392         }
393     }
394
395     _createToggleForClassName(className)
396     {
397         if (!className || !className.length)
398             return;
399
400         let classToggledMap = this.domNode[WI.CSSStyleDetailsSidebarPanel.ToggledClassesSymbol];
401         if (!classToggledMap)
402             return;
403
404         if (!classToggledMap.has(className))
405             classToggledMap.set(className, true);
406
407         let toggled = classToggledMap.get(className);
408
409         let classNameContainer = document.createElement("div");
410         classNameContainer.classList.add("class-toggle");
411
412         let classNameToggle = classNameContainer.createChild("input");
413         classNameToggle.type = "checkbox";
414         classNameToggle.checked = toggled;
415
416         let classNameTitle = classNameContainer.createChild("span");
417         classNameTitle.textContent = className;
418         classNameTitle.draggable = true;
419         classNameTitle.addEventListener("dragstart", (event) => {
420             event.dataTransfer.setData(WI.CSSStyleDetailsSidebarPanel.ToggledClassesDragType, className);
421             event.dataTransfer.effectAllowed = "copy";
422         });
423
424         let classNameToggleChanged = (event) => {
425             this.domNode.toggleClass(className, classNameToggle.checked);
426             classToggledMap.set(className, classNameToggle.checked);
427         };
428
429         classNameToggle.addEventListener("click", classNameToggleChanged);
430         classNameTitle.addEventListener("click", (event) => {
431             classNameToggle.checked = !classNameToggle.checked;
432             classNameToggleChanged();
433         });
434
435         this._classListContainer.appendChild(classNameContainer);
436     }
437
438     _filterDidChange()
439     {
440         this.contentView.element.classList.toggle(WI.CSSStyleDetailsSidebarPanel.FilterInProgressClassName, this._filterBar.hasActiveFilters());
441
442         this._selectedPanel.filterDidChange(this._filterBar);
443     }
444
445     _styleSheetAddedOrRemoved()
446     {
447         this.needsLayout();
448     }
449 };
450
451 WI.CSSStyleDetailsSidebarPanel.NoForcedPseudoClassesScrollOffset = 30; // Default height of the forced pseudo classes container. Updated in sizeDidChange.
452 WI.CSSStyleDetailsSidebarPanel.FilterInProgressClassName = "filter-in-progress";
453 WI.CSSStyleDetailsSidebarPanel.FilterMatchingSectionHasLabelClassName = "filter-section-has-label";
454 WI.CSSStyleDetailsSidebarPanel.FilterMatchSectionClassName = "filter-matching";
455 WI.CSSStyleDetailsSidebarPanel.NoFilterMatchInSectionClassName = "filter-section-non-matching";
456 WI.CSSStyleDetailsSidebarPanel.NoFilterMatchInPropertyClassName = "filter-property-non-matching";
457
458 WI.CSSStyleDetailsSidebarPanel.ToggledClassesSymbol = Symbol("css-style-details-sidebar-panel-toggled-classes-symbol");
459 WI.CSSStyleDetailsSidebarPanel.ToggledClassesDragType = "text/classname";