Web Inspector: Make Console's Execution Context picker stand out when it is non-default
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / HierarchicalPathComponent.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.HierarchicalPathComponent = class HierarchicalPathComponent extends WI.Object
27 {
28     constructor(displayName, styleClassNames, representedObject, textOnly, showSelectorArrows)
29     {
30         super();
31
32         console.assert(displayName);
33         console.assert(styleClassNames);
34
35         this._representedObject = representedObject || null;
36
37         this._element = document.createElement("div");
38         this._element.className = "hierarchical-path-component";
39
40         if (!Array.isArray(styleClassNames))
41             styleClassNames = [styleClassNames];
42
43         this._element.classList.add(...styleClassNames);
44
45         if (!textOnly) {
46             this._iconElement = document.createElement("img");
47             this._iconElement.className = "icon";
48             this._element.appendChild(this._iconElement);
49         } else
50             this._element.classList.add("text-only");
51
52         this._titleElement = document.createElement("div");
53         this._titleElement.className = "title";
54         this._titleElement.setAttribute("dir", "auto");
55         this._element.appendChild(this._titleElement);
56
57         this._titleContentElement = document.createElement("div");
58         this._titleContentElement.className = "content";
59         this._titleElement.appendChild(this._titleContentElement);
60
61         this._separatorElement = document.createElement("div");
62         this._separatorElement.className = "separator";
63         this._element.appendChild(this._separatorElement);
64
65         this._selectElement = document.createElement("select");
66         this._selectElement.setAttribute("dir", "auto");
67         this._selectElement.addEventListener("mouseover", this._selectElementMouseOver.bind(this));
68         this._selectElement.addEventListener("mouseout", this._selectElementMouseOut.bind(this));
69         this._selectElement.addEventListener("mousedown", this._selectElementMouseDown.bind(this));
70         this._selectElement.addEventListener("mouseup", this._selectElementMouseUp.bind(this));
71         this._selectElement.addEventListener("change", this._selectElementSelectionChanged.bind(this));
72         this._element.appendChild(this._selectElement);
73
74         this._previousSibling = null;
75         this._nextSibling = null;
76
77         this._truncatedDisplayNameLength = 0;
78
79         this._collapsed = false;
80         this._hidden = false;
81         this._selectorArrows = false;
82
83         this.displayName = displayName;
84         this.selectorArrows = showSelectorArrows;
85     }
86
87     // Public
88
89     get selectedPathComponent()
90     {
91         let selectedOption = this._selectElement[this._selectElement.selectedIndex];
92         if (!selectedOption && this._selectElement.options.length === 1)
93             selectedOption = this._selectElement.options[0];
94         return selectedOption && selectedOption._pathComponent || null;
95     }
96
97     get element() { return this._element; }
98     get representedObject() { return this._representedObject; }
99
100     get displayName()
101     {
102         return this._displayName;
103     }
104
105     set displayName(newDisplayName)
106     {
107         console.assert(newDisplayName);
108         if (newDisplayName === this._displayName)
109             return;
110
111         this._displayName = newDisplayName;
112
113         this._updateElementTitleAndText();
114     }
115
116     get truncatedDisplayNameLength()
117     {
118         return this._truncatedDisplayNameLength;
119     }
120
121     set truncatedDisplayNameLength(truncatedDisplayNameLength)
122     {
123         truncatedDisplayNameLength = truncatedDisplayNameLength || 0;
124
125         if (truncatedDisplayNameLength === this._truncatedDisplayNameLength)
126             return;
127
128         this._truncatedDisplayNameLength = truncatedDisplayNameLength;
129
130         this._updateElementTitleAndText();
131     }
132
133     get minimumWidth()
134     {
135         if (this._collapsed)
136             return WI.HierarchicalPathComponent.MinimumWidthCollapsed;
137         if (this._selectorArrows)
138             return WI.HierarchicalPathComponent.MinimumWidth + WI.HierarchicalPathComponent.SelectorArrowsWidth;
139         return WI.HierarchicalPathComponent.MinimumWidth;
140     }
141
142     get forcedWidth()
143     {
144         let maxWidth = this._element.style.getProperty("width");
145         if (typeof maxWidth === "string")
146             return parseInt(maxWidth);
147         return null;
148     }
149
150     set forcedWidth(width)
151     {
152         if (typeof width === "number") {
153             let minimumWidthForOneCharacterTruncatedTitle = WI.HierarchicalPathComponent.MinimumWidthForOneCharacterTruncatedTitle;
154             if (this.selectorArrows)
155                 minimumWidthForOneCharacterTruncatedTitle += WI.HierarchicalPathComponent.SelectorArrowsWidth;
156
157             // If the width is less than the minimum width required to show a single character and ellipsis, then
158             // just collapse down to the bare minimum to show only the icon.
159             if (width < minimumWidthForOneCharacterTruncatedTitle)
160                 width = 0;
161
162             // Ensure the width does not go less than 1px. If the width is 0 the layout gets funky. There is a min-width
163             // in the CSS too, so as long the width is less than min-width we get the desired effect of only showing the icon.
164             this._element.style.setProperty("width", Math.max(1, width) + "px");
165         } else
166             this._element.style.removeProperty("width");
167     }
168
169     get hidden()
170     {
171         return this._hidden;
172     }
173
174     set hidden(flag)
175     {
176         if (this._hidden === flag)
177             return;
178
179         this._hidden = flag;
180         this._element.classList.toggle("hidden", this._hidden);
181     }
182
183     get collapsed()
184     {
185         return this._collapsed;
186     }
187
188     set collapsed(flag)
189     {
190         if (this._collapsed === flag)
191             return;
192
193         this._collapsed = flag;
194         this._element.classList.toggle("collapsed", this._collapsed);
195     }
196
197     get selectorArrows()
198     {
199         return this._selectorArrows;
200     }
201
202     set selectorArrows(flag)
203     {
204         if (this._selectorArrows === flag)
205             return;
206
207         this._selectorArrows = flag;
208
209         if (this._selectorArrows) {
210             this._selectorArrowsElement = WI.ImageUtilities.useSVGSymbol("Images/UpDownArrows.svg", "selector-arrows");
211             this._element.insertBefore(this._selectorArrowsElement, this._separatorElement);
212         } else if (this._selectorArrowsElement) {
213             this._selectorArrowsElement.remove();
214             this._selectorArrowsElement = null;
215         }
216
217         this._element.classList.toggle("show-selector-arrows", !!this._selectorArrows);
218     }
219
220     get previousSibling() { return this._previousSibling; }
221     set previousSibling(newSlibling) { this._previousSibling = newSlibling || null; }
222     get nextSibling() { return this._nextSibling; }
223     set nextSibling(newSlibling) { this._nextSibling = newSlibling || null; }
224
225     // Private
226
227     _updateElementTitleAndText()
228     {
229         let truncatedDisplayName = this._displayName;
230         if (this._truncatedDisplayNameLength && truncatedDisplayName.length > this._truncatedDisplayNameLength)
231             truncatedDisplayName = truncatedDisplayName.substring(0, this._truncatedDisplayNameLength) + ellipsis;
232
233         this._element.title = this._displayName;
234         this._titleContentElement.textContent = truncatedDisplayName;
235     }
236
237     _updateSelectElement()
238     {
239         this._selectElement.removeChildren();
240
241         function createOption(component)
242         {
243             let optionElement = document.createElement("option");
244             let maxPopupMenuLength = 130; // <rdar://problem/13445374> <select> with very long option has clipped text and popup menu is still very wide
245             optionElement.textContent = component.displayName.length <= maxPopupMenuLength ? component.displayName : component.displayName.substring(0, maxPopupMenuLength) + ellipsis;
246             optionElement._pathComponent = component;
247             return optionElement;
248         }
249
250         let previousSiblingCount = 0;
251         let sibling = this.previousSibling;
252         while (sibling) {
253             this._selectElement.insertBefore(createOption(sibling), this._selectElement.firstChild);
254             sibling = sibling.previousSibling;
255             ++previousSiblingCount;
256         }
257
258         this._selectElement.appendChild(createOption(this));
259
260         sibling = this.nextSibling;
261         while (sibling) {
262             this._selectElement.appendChild(createOption(sibling));
263             sibling = sibling.nextSibling;
264         }
265
266         // Since the change event only fires when the selection actually changes we are
267         // stuck with either not showing the current selection in the menu or accepting that
268         // the user can't select what is already selected again. Selecting the same item
269         // again can be desired (for selecting the main resource while looking at an image).
270         // So if there is only one option, don't make it be selected by default. This lets
271         // you select the top level item which usually has no siblings to go back.
272         // FIXME: Make this work when there are multiple options with a selectedIndex.
273         if (this._selectElement.options.length === 1)
274             this._selectElement.selectedIndex = -1;
275         else
276             this._selectElement.selectedIndex = previousSiblingCount;
277     }
278
279     _selectElementMouseOver(event)
280     {
281         if (typeof this.mouseOver === "function")
282             this.mouseOver();
283     }
284
285     _selectElementMouseOut(event)
286     {
287         if (typeof this.mouseOut === "function")
288             this.mouseOut();
289     }
290
291     _selectElementMouseDown(event)
292     {
293         this._updateSelectElement();
294     }
295
296     _selectElementMouseUp(event)
297     {
298         this.dispatchEventToListeners(WI.HierarchicalPathComponent.Event.Clicked, {pathComponent: this.selectedPathComponent});
299     }
300
301     _selectElementSelectionChanged(event)
302     {
303         this.dispatchEventToListeners(WI.HierarchicalPathComponent.Event.SiblingWasSelected, {pathComponent: this.selectedPathComponent});
304     }
305 };
306
307 WI.HierarchicalPathComponent.MinimumWidth = 32;
308 WI.HierarchicalPathComponent.MinimumWidthCollapsed = 24;
309 WI.HierarchicalPathComponent.MinimumWidthForOneCharacterTruncatedTitle = 54;
310 WI.HierarchicalPathComponent.SelectorArrowsWidth = 12;
311
312 WI.HierarchicalPathComponent.Event = {
313     SiblingWasSelected: "hierarchical-path-component-sibling-was-selected",
314     Clicked: "hierarchical-path-component-clicked"
315 };