Web Inspector: Fix assert in QuickConsole - main frame execution context path component
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / QuickConsole.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.QuickConsole = function(element)
27 {
28     WebInspector.Object.call(this);
29
30     this._toggleOrFocusKeyboardShortcut = new WebInspector.KeyboardShortcut(null, WebInspector.KeyboardShortcut.Key.Escape, this._toggleOrFocus.bind(this));
31
32     var mainFrameExecutionContext = new WebInspector.ExecutionContext(WebInspector.QuickConsole.MainFrameContextExecutionIdentifier, WebInspector.UIString("Main Frame"), true, null);
33     this._mainFrameExecutionContextPathComponent = this._createExecutionContextPathComponent(mainFrameExecutionContext.name, mainFrameExecutionContext.identifier);
34     this._selectedExecutionContextPathComponent = this._mainFrameExecutionContextPathComponent;
35
36     this._otherExecutionContextPathComponents = [];
37     this._frameIdentifierToExecutionContextPathComponentMap = {};
38
39     this._element = element || document.createElement("div");
40     this._element.classList.add(WebInspector.QuickConsole.StyleClassName);
41
42     this.prompt = new WebInspector.ConsolePrompt(null, "text/javascript");
43     this.prompt.element.classList.add(WebInspector.QuickConsole.TextPromptStyleClassName);
44     this._element.appendChild(this.prompt.element);
45
46     // FIXME: CodeMirror 4 has a default "Esc" key handler that always prevents default.
47     // Our keyboard shortcut above will respect the default prevented and ignore the event
48     // and not toggle the console. Install our own Escape key handler that will trigger
49     // when the ConsolePrompt is empty, to restore toggling behavior. A better solution
50     // would be for CodeMirror's event handler to pass if it doesn't do anything.
51     this.prompt.escapeKeyHandlerWhenEmpty = function() { WebInspector.toggleSplitConsole(); };
52
53     this.prompt.shown();
54
55     this._navigationBar = new WebInspector.QuickConsoleNavigationBar;
56     this._element.appendChild(this._navigationBar.element);
57
58     this._executionContextSelectorItem = new WebInspector.HierarchicalPathNavigationItem;
59     this._executionContextSelectorItem.showSelectorArrows = true;
60     this._navigationBar.addNavigationItem(this._executionContextSelectorItem);
61
62     this._executionContextSelectorDivider = new WebInspector.DividerNavigationItem;
63     this._navigationBar.addNavigationItem(this._executionContextSelectorDivider);
64
65     this._rebuildExecutionContextPathComponents();
66
67     // COMPATIBILITY (iOS 6): Execution contexts did not exist, evaluation worked with frame ids.
68     if (WebInspector.ExecutionContext.supported()) {
69         WebInspector.Frame.addEventListener(WebInspector.Frame.Event.PageExecutionContextChanged, this._framePageExecutionContextsChanged, this);
70         WebInspector.Frame.addEventListener(WebInspector.Frame.Event.ExecutionContextsCleared, this._frameExecutionContextsCleared, this);
71     } else {
72         WebInspector.frameResourceManager.addEventListener(WebInspector.FrameResourceManager.Event.FrameWasAdded, this._frameAdded, this);
73         WebInspector.frameResourceManager.addEventListener(WebInspector.FrameResourceManager.Event.FrameWasRemoved, this._frameRemoved, this);
74         WebInspector.Frame.addEventListener(WebInspector.Frame.Event.MainResourceDidChange, this._frameMainResourceChanged, this);
75     }
76
77     WebInspector.debuggerManager.addEventListener(WebInspector.DebuggerManager.Event.ActiveCallFrameDidChange, this._debuggerActiveCallFrameDidChange, this);
78 };
79
80 WebInspector.QuickConsole.StyleClassName = "quick-console";
81 WebInspector.QuickConsole.ShowingLogClassName = "showing-log";
82 WebInspector.QuickConsole.NavigationBarContainerStyleClassName = "navigation-bar-container";
83 WebInspector.QuickConsole.NavigationBarSpacerStyleClassName = "navigation-bar-spacer";
84 WebInspector.QuickConsole.TextPromptStyleClassName = "text-prompt";
85
86 WebInspector.QuickConsole.ToolbarSingleLineHeight = 21;
87 WebInspector.QuickConsole.ToolbarPromptPadding = 4;
88 WebInspector.QuickConsole.ToolbarTopBorder = 1;
89
90 WebInspector.QuickConsole.MainFrameContextExecutionIdentifier = undefined;
91
92 WebInspector.QuickConsole.Event = {
93     DidResize: "quick-console-did-resize"
94 };
95
96 WebInspector.QuickConsole.prototype = {
97     constructor: WebInspector.QuickConsole,
98
99     // Public
100
101     get element()
102     {
103         return this._element;
104     },
105
106     get navigationBar()
107     {
108         return this._navigationBar;
109     },
110
111     get executionContextIdentifier()
112     {
113         return this._selectedExecutionContextPathComponent._executionContextIdentifier;
114     },
115
116     updateLayout: function()
117     {
118         // A hard maximum size of 33% of the window.
119         const maximumAllowedHeight = Math.round(window.innerHeight * 0.33);
120         this.prompt.element.style.maxHeight = maximumAllowedHeight + "px";
121     },
122
123     consoleLogVisibilityChanged: function(visible)
124     {
125         if (visible)
126             this.element.classList.add(WebInspector.QuickConsole.ShowingLogClassName);
127         else
128             this.element.classList.remove(WebInspector.QuickConsole.ShowingLogClassName);
129
130         this.dispatchEventToListeners(WebInspector.QuickConsole.Event.DidResize);
131     },
132
133     // Private
134
135     _executionContextPathComponentsToDisplay: function()
136     {
137         // If we are in the debugger the console will use the active call frame, don't show the selector.
138         if (WebInspector.debuggerManager.activeCallFrame)
139             return [];
140
141         // If there is only the Main Frame, don't show the selector.
142         if (!this._otherExecutionContextPathComponents.length)
143             return [];
144
145         return [this._selectedExecutionContextPathComponent];
146     },
147
148     _rebuildExecutionContextPathComponents: function()
149     {
150         var components = this._executionContextPathComponentsToDisplay();
151         var isEmpty = !components.length;
152
153         this._executionContextSelectorItem.components = components;
154
155         this._executionContextSelectorItem.hidden = isEmpty;
156         this._executionContextSelectorDivider.hidden = isEmpty;
157     },
158
159     _framePageExecutionContextsChanged: function(event)
160     {
161         var frame = event.target;
162
163         var shouldAutomaticallySelect = this._restoreSelectedExecutionContextForFrame === frame;
164
165         var newExecutionContextPathComponent = this._insertExecutionContextPathComponentForFrame(frame, shouldAutomaticallySelect);
166
167         if (shouldAutomaticallySelect) {
168             delete this._restoreSelectedExecutionContextForFrame;
169             this._selectedExecutionContextPathComponent = newExecutionContextPathComponent;
170             this._rebuildExecutionContextPathComponents();
171         }
172     },
173
174     _frameExecutionContextsCleared: function(event)
175     {
176         var frame = event.target;
177
178         // If this frame is navigating and it is selected in the UI we want to reselect its new item after navigation.
179         if (event.data.committingProvisionalLoad && !this._restoreSelectedExecutionContextForFrame) {
180             var executionContextPathComponent = this._frameIdentifierToExecutionContextPathComponentMap[frame.id];
181             if (this._selectedExecutionContextPathComponent === executionContextPathComponent) {
182                 this._restoreSelectedExecutionContextForFrame = frame;
183                 // As a fail safe, if the frame never gets an execution context, clear the restore value.
184                 setTimeout(function() { delete this._restoreSelectedExecutionContextForFrame; }.bind(this), 10);
185             }
186         }
187
188         this._removeExecutionContextPathComponentForFrame(frame);
189     },
190
191     _frameAdded: function(event)
192     {
193         var frame = event.data.frame;
194         this._insertExecutionContextPathComponentForFrame(frame);
195     },
196
197     _frameRemoved: function(event)
198     {
199         var frame = event.data.frame;
200         this._removeExecutionContextPathComponentForFrame(frame);
201     },
202
203     _frameMainResourceChanged: function(event)
204     {
205         var frame = event.target;
206         this._updateExecutionContextPathComponentForFrame(frame);
207     },
208
209     _createExecutionContextPathComponent: function(name, identifier)
210     {
211         var pathComponent = new WebInspector.HierarchicalPathComponent(name, "execution-context", identifier, true, true);
212         pathComponent.addEventListener(WebInspector.HierarchicalPathComponent.Event.SiblingWasSelected, this._pathComponentSelected, this);
213         pathComponent.addEventListener(WebInspector.HierarchicalPathComponent.Event.Clicked, this._pathComponentClicked, this);
214         pathComponent.truncatedDisplayNameLength = 50;
215         pathComponent._executionContextIdentifier = identifier;
216         return pathComponent;
217     },
218
219     _createExecutionContextPathComponentFromFrame: function(frame)
220     {
221         var name = frame.name ? frame.name + " \u2014 " + frame.mainResource.displayName : frame.mainResource.displayName;
222         var identifier = WebInspector.ExecutionContext.supported() ? frame.pageExecutionContext.id : frame.id;
223
224         var pathComponent = this._createExecutionContextPathComponent(name, identifier);
225         pathComponent._frame = frame;
226
227         return pathComponent;
228     },
229
230     _compareExecutionContextPathComponents: function(a, b)
231     {
232         // "Main Frame" always on top.
233         if (!a._frame)
234             return -1;
235         if (!b._frame)
236             return 1;
237
238         // Frames with a name above frames without a name.
239         if (a._frame.name && !b._frame.name)
240             return -1;
241         if (!a._frame.name && b._frame.name)
242             return 1;
243
244         return a.displayName.localeCompare(b.displayName);
245     },
246
247     _insertExecutionContextPathComponentForFrame: function(frame, skipRebuild)
248     {
249         if (frame.isMainFrame())
250             return this._mainFrameExecutionContextPathComponent;
251
252         console.assert(!this._frameIdentifierToExecutionContextPathComponentMap[frame.id]);
253         if (this._frameIdentifierToExecutionContextPathComponentMap[frame.id])
254             return null;
255
256         var executionContextPathComponent = this._createExecutionContextPathComponentFromFrame(frame);
257
258         var index = insertionIndexForObjectInListSortedByFunction(executionContextPathComponent, this._otherExecutionContextPathComponents, this._compareExecutionContextPathComponents);
259
260         var prev = index > 0 ? this._otherExecutionContextPathComponents[index - 1] : this._mainFrameExecutionContextPathComponent;
261         var next = this._otherExecutionContextPathComponents[index] || null;
262         if (prev) {
263             prev.nextSibling = executionContextPathComponent;
264             executionContextPathComponent.previousSibling = prev;
265         }
266         if (next) {
267             next.previousSibling = executionContextPathComponent;
268             executionContextPathComponent.nextSibling = next;
269         }
270
271         this._otherExecutionContextPathComponents.splice(index, 0, executionContextPathComponent);
272         this._frameIdentifierToExecutionContextPathComponentMap[frame.id] = executionContextPathComponent;
273
274         if (!skipRebuild)
275             this._rebuildExecutionContextPathComponents();
276
277         return executionContextPathComponent;
278     },
279
280     _removeExecutionContextPathComponentForFrame: function(frame, skipRebuild)
281     {
282         if (frame.isMainFrame())
283             return;
284
285         var executionContextPathComponent = this._frameIdentifierToExecutionContextPathComponentMap[frame.id];
286         console.assert(executionContextPathComponent);
287         if (!executionContextPathComponent)
288             return;
289
290         executionContextPathComponent.removeEventListener(WebInspector.HierarchicalPathComponent.Event.SiblingWasSelected, this._pathComponentSelected, this);
291         executionContextPathComponent.removeEventListener(WebInspector.HierarchicalPathComponent.Event.Clicked, this._pathComponentClicked, this);
292
293         var prev = executionContextPathComponent.previousSibling;
294         var next = executionContextPathComponent.nextSibling;
295         if (prev)
296             prev.nextSibling = next;
297         if (next)
298             next.previousSibling = prev;
299
300         if (this._selectedExecutionContextPathComponent === executionContextPathComponent)
301             this._selectedExecutionContextPathComponent = this._mainFrameExecutionContextPathComponent;
302
303         this._otherExecutionContextPathComponents.remove(executionContextPathComponent, true);
304         delete this._frameIdentifierToExecutionContextPathComponentMap[frame.id];
305
306         if (!skipRebuild)
307             this._rebuildExecutionContextPathComponents();
308     },
309
310     _updateExecutionContextPathComponentForFrame: function(frame)
311     {
312         if (frame.isMainFrame())
313             return;
314
315         var executionContextPathComponent = this._frameIdentifierToExecutionContextPathComponentMap[frame.id];
316         if (!executionContextPathComponent)
317             return;
318
319         var wasSelected = this._selectedExecutionContextPathComponent === executionContextPathComponent;
320
321         this._removeExecutionContextPathComponentForFrame(frame, true);
322         var newExecutionContextPathComponent = this._insertExecutionContextPathComponentForFrame(frame, true);
323
324         if (wasSelected)
325             this._selectedExecutionContextPathComponent = newExecutionContextPathComponent;
326
327         this._rebuildExecutionContextPathComponents();
328     },
329
330     _pathComponentSelected: function(event)
331     {
332         if (event.data.pathComponent === this._selectedExecutionContextPathComponent)
333             return;
334
335         this._selectedExecutionContextPathComponent = event.data.pathComponent;
336
337         this._rebuildExecutionContextPathComponents();
338     },
339
340     _pathComponentClicked: function(event)
341     {
342         this.prompt.focus();
343     },
344
345     _debuggerActiveCallFrameDidChange: function(event)
346     {
347         this._rebuildExecutionContextPathComponents();
348     },
349
350     _toggleOrFocus: function(event)
351     {
352         if (this.prompt.focused)
353             WebInspector.toggleSplitConsole();
354         else if (!WebInspector.isEditingAnyField() && !WebInspector.isEventTargetAnEditableField(event))
355             this.prompt.focus();
356     }
357 };
358
359 WebInspector.QuickConsole.prototype.__proto__ = WebInspector.Object.prototype;