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
26WebInspector.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
80WebInspector.QuickConsole.StyleClassName = "quick-console";
81WebInspector.QuickConsole.ShowingLogClassName = "showing-log";
82WebInspector.QuickConsole.NavigationBarContainerStyleClassName = "navigation-bar-container";
83WebInspector.QuickConsole.NavigationBarSpacerStyleClassName = "navigation-bar-spacer";
84WebInspector.QuickConsole.TextPromptStyleClassName = "text-prompt";
85
86WebInspector.QuickConsole.ToolbarSingleLineHeight = 21;
87WebInspector.QuickConsole.ToolbarPromptPadding = 4;
88WebInspector.QuickConsole.ToolbarTopBorder = 1;
89
90WebInspector.QuickConsole.MainFrameContextExecutionIdentifier = undefined;
91
92WebInspector.QuickConsole.Event = {
93    DidResize: "quick-console-did-resize"
94};
95
96WebInspector.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;
251
252        console.assert(!this._frameIdentifierToExecutionContextPathComponentMap[frame.id]);
253        if (this._frameIdentifierToExecutionContextPathComponentMap[frame.id])
254            return;
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
359WebInspector.QuickConsole.prototype.__proto__ = WebInspector.Object.prototype;
360