964bd004ef1b8db7edf52ea068b059420681c90b
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / LogContentView.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 // FIXME: <https://webkit.org/b/143545> Web Inspector: LogContentView should use higher level objects
27
28 WI.LogContentView = class LogContentView extends WI.ContentView
29 {
30     constructor(representedObject)
31     {
32         super(representedObject);
33
34         this._nestingLevel = 0;
35         this._selectedMessages = [];
36
37         // FIXME: Try to use a marker, instead of a list of messages that get re-added.
38         this._provisionalMessages = [];
39
40         this.element.classList.add("log");
41
42         this.messagesElement = document.createElement("div");
43         this.messagesElement.classList.add("console-messages");
44         this.messagesElement.tabIndex = 0;
45         this.messagesElement.setAttribute("role", "log");
46         this.messagesElement.addEventListener("mousedown", this._mousedown.bind(this));
47         this.messagesElement.addEventListener("keydown", this._keyDown.bind(this));
48         this.messagesElement.addEventListener("keypress", this._keyPress.bind(this));
49         this.messagesElement.addEventListener("dragstart", this._ondragstart.bind(this), true);
50         this.element.appendChild(this.messagesElement);
51
52         this.prompt = WI.quickConsole.prompt;
53
54         this._keyboardShortcutCommandA = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl, "A");
55         this._keyboardShortcutEsc = new WI.KeyboardShortcut(null, WI.KeyboardShortcut.Key.Escape);
56
57         this._logViewController = new WI.JavaScriptLogViewController(this.messagesElement, this.element, this.prompt, this, "console-prompt-history");
58         this._lastMessageView = null;
59
60         const fixed = true;
61         this._findBanner = new WI.FindBanner(this, "console-find-banner", fixed);
62         this._findBanner.targetElement = this.element;
63
64         this._currentSearchQuery = "";
65         this._searchMatches = [];
66         this._selectedSearchMatch = null;
67         this._selectedSearchMatchIsValid = false;
68
69         this._preserveLogNavigationItem = new WI.CheckboxNavigationItem("preserve-log", WI.UIString("Preserve Log"), !WI.settings.clearLogOnNavigate.value);
70         this._preserveLogNavigationItem.tooltip = WI.UIString("Do not clear the console on new page loads");
71         this._preserveLogNavigationItem.addEventListener(WI.CheckboxNavigationItem.Event.CheckedDidChange, () => {
72             WI.settings.clearLogOnNavigate.value = !WI.settings.clearLogOnNavigate.value;
73         });
74         WI.settings.clearLogOnNavigate.addEventListener(WI.Setting.Event.Changed, this._handleClearLogOnNavigateSettingChanged, this);
75
76         this._emulateInUserGestureNavigationItem = new WI.CheckboxNavigationItem("emulate-in-user-gesture", WI.UIString("Emulate User Gesture"), WI.settings.emulateInUserGesture.value);
77         this._emulateInUserGestureNavigationItem.tooltip = WI.UIString("Run console commands as if inside a user gesture");
78         this._emulateInUserGestureNavigationItem.addEventListener(WI.CheckboxNavigationItem.Event.CheckedDidChange, () => {
79             WI.settings.emulateInUserGesture.value = !WI.settings.emulateInUserGesture.value;
80         });
81         WI.settings.emulateInUserGesture.addEventListener(WI.Setting.Event.Changed, this._handleEmulateInUserGestureSettingChanged, this);
82
83         this._checkboxesNavigationItemGroup = new WI.GroupNavigationItem([this._preserveLogNavigationItem, this._emulateInUserGestureNavigationItem, new WI.DividerNavigationItem]);
84
85         let scopeBarItems = [
86             new WI.ScopeBarItem(WI.LogContentView.Scopes.All, WI.UIString("All"), {exclusive: true}),
87             new WI.ScopeBarItem(WI.LogContentView.Scopes.Errors, WI.UIString("Errors"), {className: "errors"}),
88             new WI.ScopeBarItem(WI.LogContentView.Scopes.Warnings, WI.UIString("Warnings"), {className: "warnings"}),
89             new WI.ScopeBarItem(WI.LogContentView.Scopes.Logs, WI.UIString("Logs"), {className: "logs"}),
90             new WI.ScopeBarItem(WI.LogContentView.Scopes.Infos, WI.UIString("Infos"), {className: "infos", hidden: true}),
91             new WI.ScopeBarItem(WI.LogContentView.Scopes.Debugs, WI.UIString("Debugs"), {className: "debugs", hidden: true}),
92         ];
93
94         this._scopeBar = new WI.ScopeBar("log-scope-bar", scopeBarItems, scopeBarItems[0]);
95         this._scopeBar.addEventListener(WI.ScopeBar.Event.SelectionChanged, this._scopeBarSelectionDidChange, this);
96
97         this._hasNonDefaultLogChannelMessage = false;
98         if (WI.ConsoleManager.supportsLogChannels()) {
99             let messageChannelBarItems = [
100                 new WI.ScopeBarItem(WI.LogContentView.Scopes.AllChannels, WI.UIString("All"), {exclusive: true}),
101                 new WI.ScopeBarItem(WI.LogContentView.Scopes.Media, WI.UIString("Media"), {className: "media"}),
102                 new WI.ScopeBarItem(WI.LogContentView.Scopes.WebRTC, WI.UIString("WebRTC"), {className: "webrtc"}),
103             ];
104
105             this._messageSourceBar = new WI.ScopeBar("message-channel-scope-bar", messageChannelBarItems, messageChannelBarItems[0]);
106             this._messageSourceBar.addEventListener(WI.ScopeBar.Event.SelectionChanged, this._messageSourceBarSelectionDidChange, this);
107         }
108
109         this._garbageCollectNavigationItem = new WI.ButtonNavigationItem("garbage-collect", WI.UIString("Collect garbage"), "Images/NavigationItemGarbageCollect.svg", 16, 16);
110         this._garbageCollectNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
111         this._garbageCollectNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._garbageCollect, this);
112
113         this._clearLogNavigationItem = new WI.ButtonNavigationItem("clear-log", WI.UIString("Clear log (%s or %s)").format(WI.clearKeyboardShortcut.displayName, this._logViewController.messagesAlternateClearKeyboardShortcut.displayName), "Images/NavigationItemTrash.svg", 15, 15);
114         this._clearLogNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
115         this._clearLogNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._clearLog, this);
116
117         this._showConsoleTabNavigationItem = new WI.ButtonNavigationItem("show-tab", WI.UIString("Show Console tab"), "Images/SplitToggleUp.svg", 16, 16);
118         this._showConsoleTabNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.High;
119         this._showConsoleTabNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._showConsoleTab, this);
120
121         this.messagesElement.addEventListener("contextmenu", this._handleContextMenuEvent.bind(this), false);
122
123         WI.consoleManager.addEventListener(WI.ConsoleManager.Event.SessionStarted, this._sessionStarted, this);
124         WI.consoleManager.addEventListener(WI.ConsoleManager.Event.MessageAdded, this._messageAdded, this);
125         WI.consoleManager.addEventListener(WI.ConsoleManager.Event.PreviousMessageRepeatCountUpdated, this._previousMessageRepeatCountUpdated, this);
126         WI.consoleManager.addEventListener(WI.ConsoleManager.Event.Cleared, this._logCleared, this);
127
128         WI.Frame.addEventListener(WI.Frame.Event.ProvisionalLoadStarted, this._provisionalLoadStarted, this);
129     }
130
131     // Public
132
133     get navigationItems()
134     {
135         let navigationItems = [this._scopeBar, new WI.DividerNavigationItem];
136
137         if (this._hasNonDefaultLogChannelMessage && this._messageSourceBar)
138             navigationItems.push(this._messageSourceBar, new WI.DividerNavigationItem);
139
140         if (window.HeapAgent && HeapAgent.gc)
141             navigationItems.push(this._garbageCollectNavigationItem);
142
143         navigationItems.push(this._clearLogNavigationItem);
144
145         if (WI.isShowingSplitConsole())
146             navigationItems.push(new WI.DividerNavigationItem, this._showConsoleTabNavigationItem);
147         else if (WI.isShowingConsoleTab())
148             navigationItems.unshift(this._findBanner, this._checkboxesNavigationItemGroup);
149
150         return navigationItems;
151     }
152
153     get scopeBar()
154     {
155         return this._scopeBar;
156     }
157
158     get logViewController()
159     {
160         return this._logViewController;
161     }
162
163     get scrollableElements()
164     {
165         return [this.element];
166     }
167
168     get shouldKeepElementsScrolledToBottom()
169     {
170         return true;
171     }
172
173     shown()
174     {
175         super.shown();
176
177         this._logViewController.renderPendingMessages();
178     }
179
180     closed()
181     {
182         // While it may be possible to get here, this is a singleton ContentView instance
183         // that is often re-inserted back into different ContentBrowsers, so we shouldn't
184         // remove the event listeners. The singleton will never go away anyways.
185         console.assert(this === WI.consoleContentView);
186
187         super.closed();
188     }
189
190     didAppendConsoleMessageView(messageView)
191     {
192         console.assert(messageView instanceof WI.ConsoleMessageView || messageView instanceof WI.ConsoleCommandView);
193
194         // Nest the message.
195         var type = messageView instanceof WI.ConsoleCommandView ? null : messageView.message.type;
196         if (this._nestingLevel && type !== WI.ConsoleMessage.MessageType.EndGroup) {
197             var x = 16 * this._nestingLevel;
198             var messageElement = messageView.element;
199             messageElement.style.left = x + "px";
200             messageElement.style.width = "calc(100% - " + x + "px)";
201         }
202
203         // Update the nesting level.
204         switch (type) {
205         case WI.ConsoleMessage.MessageType.StartGroup:
206         case WI.ConsoleMessage.MessageType.StartGroupCollapsed:
207             ++this._nestingLevel;
208             break;
209         case WI.ConsoleMessage.MessageType.EndGroup:
210             if (this._nestingLevel > 0)
211                 --this._nestingLevel;
212             break;
213         }
214
215         this._clearFocusableChildren();
216
217         // Some results don't populate until further backend dispatches occur (like the DOM tree).
218         // We want to remove focusable children after those pending dispatches too.
219         let target = messageView.message ? messageView.message.target : WI.runtimeManager.activeExecutionContext.target;
220         target.connection.runAfterPendingDispatches(this._clearFocusableChildren.bind(this));
221
222         if (type && type !== WI.ConsoleMessage.MessageType.EndGroup) {
223             console.assert(messageView.message instanceof WI.ConsoleMessage);
224             if (!(messageView.message instanceof WI.ConsoleCommandResultMessage))
225                 this._markScopeBarItemUnread(messageView.message.level);
226
227             console.assert(messageView.element instanceof Element);
228             this._filterMessageElements([messageView.element]);
229         }
230     }
231
232     get supportsSearch() { return true; }
233     get numberOfSearchResults() { return this.hasPerformedSearch ? this._searchMatches.length : null; }
234     get hasPerformedSearch() { return this._currentSearchQuery !== ""; }
235
236     get supportsCustomFindBanner()
237     {
238         return WI.isShowingConsoleTab();
239     }
240
241     showCustomFindBanner()
242     {
243         if (!this.visible)
244             return;
245
246         this._findBanner.focus();
247     }
248
249     get supportsSave()
250     {
251         if (!this.visible)
252             return false;
253
254         if (WI.isShowingSplitConsole())
255             return false;
256
257         return true;
258     }
259
260     get saveData()
261     {
262         return {url: "web-inspector:///Console.txt", content: this._formatMessagesAsData(false), forceSaveAs: true};
263     }
264
265     handleCopyEvent(event)
266     {
267         if (!this._selectedMessages.length)
268             return;
269
270         event.clipboardData.setData("text/plain", this._formatMessagesAsData(true));
271         event.stopPropagation();
272         event.preventDefault();
273     }
274
275     handleClearShortcut(event)
276     {
277         this._logViewController.requestClearMessages();
278     }
279
280     handlePopulateFindShortcut()
281     {
282         let searchQuery = this.searchQueryWithSelection();
283         if (!searchQuery)
284             return;
285
286         this._findBanner.searchQuery = searchQuery;
287
288         this.performSearch(this._findBanner.searchQuery);
289     }
290
291     handleFindNextShortcut()
292     {
293         this.findBannerRevealNextResult(this._findBanner);
294     }
295
296     handleFindPreviousShortcut()
297     {
298         this.findBannerRevealPreviousResult(this._findBanner);
299     }
300
301     findBannerRevealPreviousResult()
302     {
303         this.highlightPreviousSearchMatch();
304     }
305
306     highlightPreviousSearchMatch()
307     {
308         if (!this.hasPerformedSearch || isEmptyObject(this._searchMatches))
309             return;
310
311         var index = this._selectedSearchMatch ? this._searchMatches.indexOf(this._selectedSearchMatch) : this._searchMatches.length;
312         this._highlightSearchMatchAtIndex(index - 1);
313     }
314
315     findBannerRevealNextResult()
316     {
317         this.highlightNextSearchMatch();
318     }
319
320     highlightNextSearchMatch()
321     {
322         if (!this.hasPerformedSearch || isEmptyObject(this._searchMatches))
323             return;
324
325         var index = this._selectedSearchMatch ? this._searchMatches.indexOf(this._selectedSearchMatch) + 1 : 0;
326         this._highlightSearchMatchAtIndex(index);
327     }
328
329     findBannerWantsToClearAndBlur(findBanner)
330     {
331         if (this._selectedMessages.length)
332             this.messagesElement.focus();
333         else
334             this.prompt.focus();
335     }
336
337     // Protected
338
339     layout()
340     {
341         this._scrollElementHeight = this.messagesElement.getBoundingClientRect().height;
342     }
343
344     // Private
345
346     _formatMessagesAsData(onlySelected)
347     {
348         var messages = this._allMessageElements();
349
350         if (onlySelected) {
351             messages = messages.filter(function(message) {
352                 return message.classList.contains(WI.LogContentView.SelectedStyleClassName);
353             });
354         }
355
356         var data = "";
357
358         var isPrefixOptional = messages.length <= 1 && onlySelected;
359         messages.forEach(function(messageElement, index) {
360             var messageView = messageElement.__messageView || messageElement.__commandView;
361             if (!messageView)
362                 return;
363
364             if (index > 0)
365                 data += "\n";
366             data += messageView.toClipboardString(isPrefixOptional);
367         });
368
369         return data;
370     }
371
372     _sessionStarted(event)
373     {
374         if (WI.settings.clearLogOnNavigate.value) {
375             this._reappendProvisionalMessages();
376             return;
377         }
378
379         const isFirstSession = false;
380         const newSessionReason = event.data.wasReloaded ? WI.ConsoleSession.NewSessionReason.PageReloaded : WI.ConsoleSession.NewSessionReason.PageNavigated;
381         this._logViewController.startNewSession(isFirstSession, {newSessionReason, timestamp: event.data.timestamp});
382
383         this._clearProvisionalState();
384     }
385
386     _scopeFromMessageSource(source)
387     {
388         switch (source) {
389         case WI.ConsoleMessage.MessageSource.Media:
390             return WI.LogContentView.Scopes.Media;
391         case WI.ConsoleMessage.MessageSource.WebRTC:
392             return WI.LogContentView.Scopes.WebRTC;
393         }
394
395         return undefined;
396     }
397
398     _scopeFromMessageLevel(level)
399     {
400         switch (level) {
401         case WI.ConsoleMessage.MessageLevel.Warning:
402             return WI.LogContentView.Scopes.Warnings;
403         case WI.ConsoleMessage.MessageLevel.Error:
404             return WI.LogContentView.Scopes.Errors;
405         case WI.ConsoleMessage.MessageLevel.Log:
406             return WI.LogContentView.Scopes.Logs;
407         case WI.ConsoleMessage.MessageLevel.Info:
408             return this._hasNonDefaultLogChannelMessage ? WI.LogContentView.Scopes.Infos : WI.LogContentView.Scopes.Logs;
409         case WI.ConsoleMessage.MessageLevel.Debug:
410             return this._hasNonDefaultLogChannelMessage ? WI.LogContentView.Scopes.Debugs : WI.LogContentView.Scopes.Logs;
411         }
412         console.assert(false, "This should not be reached.");
413
414         return undefined;
415     }
416
417     _markScopeBarItemUnread(level)
418     {
419         let messageLevel = this._scopeFromMessageLevel(level);
420         if (!messageLevel)
421             return;
422
423         let item = this._scopeBar.item(messageLevel);
424         if (item && !item.selected && !this._scopeBar.item(WI.LogContentView.Scopes.All).selected)
425             item.element.classList.add("unread");
426     }
427
428     _messageAdded(event)
429     {
430         let message = event.data.message;
431         if (this._startedProvisionalLoad)
432             this._provisionalMessages.push(message);
433
434         if (!this._hasNonDefaultLogChannelMessage && WI.consoleManager.logChannelSources.includes(message.source)) {
435             this._hasNonDefaultLogChannelMessage = true;
436             this.dispatchEventToListeners(WI.ContentView.Event.NavigationItemsDidChange);
437             this._scopeBar.item(WI.LogContentView.Scopes.Infos).hidden = false;
438             this._scopeBar.item(WI.LogContentView.Scopes.Debugs).hidden = false;
439         }
440
441         this._logViewController.appendConsoleMessage(message);
442     }
443
444     _previousMessageRepeatCountUpdated(event)
445     {
446         if (this._logViewController.updatePreviousMessageRepeatCount(event.data.count) && this._lastMessageView)
447             this._markScopeBarItemUnread(this._lastMessageView.message.level);
448     }
449
450     _handleContextMenuEvent(event)
451     {
452         if (!window.getSelection().isCollapsed) {
453             // If there is a selection, we want to show our normal context menu
454             // (with Copy, etc.), and not Clear Log.
455             return;
456         }
457
458         // In the case that there are selected messages, only clear that selection if the right-click
459         // is not on the element or descendants of the selected messages.
460         if (this._selectedMessages.length && !this._selectedMessages.some(element => element.contains(event.target))) {
461             this._clearMessagesSelection();
462             this._mousedown(event);
463         }
464
465         // If there are no selected messages, right-clicking will not reset the current mouse state
466         // meaning that when the context menu is dismissed, console messages will be selected when
467         // the user moves the mouse even though no buttons are pressed.
468         if (!this._selectedMessages.length)
469             this._mouseup(event);
470
471         // We don't want to show the custom menu for links in the console.
472         if (event.target.enclosingNodeOrSelfWithNodeName("a"))
473             return;
474
475         let contextMenu = WI.ContextMenu.createFromEvent(event);
476
477         if (this._selectedMessages.length) {
478             contextMenu.appendItem(WI.UIString("Copy Selected"), () => {
479                 InspectorFrontendHost.copyText(this._formatMessagesAsData(true));
480             });
481
482             contextMenu.appendItem(WI.UIString("Save Selected"), () => {
483                 const forceSaveAs = true;
484                 WI.FileUtilities.save({
485                     url: "web-inspector:///Console.txt",
486                     content: this._formatMessagesAsData(true),
487                 }, forceSaveAs);
488             });
489
490             contextMenu.appendSeparator();
491         }
492
493         contextMenu.appendItem(WI.UIString("Clear Log"), this._clearLog.bind(this));
494         contextMenu.appendSeparator();
495     }
496
497     _mousedown(event)
498     {
499         if (this._selectedMessages.length && (event.button !== 0 || event.ctrlKey))
500             return;
501
502         if (event.defaultPrevented) {
503             // Default was prevented on the event, so this means something deeper (like a disclosure triangle)
504             // handled the mouse down. In this case we want to clear the selection and don't make a new selection.
505             this._clearMessagesSelection();
506             return;
507         }
508
509         this._mouseDownWrapper = event.target.enclosingNodeOrSelfWithClass(WI.LogContentView.ItemWrapperStyleClassName);
510         this._mouseDownShiftKey = event.shiftKey;
511         this._mouseDownCommandKey = event.metaKey;
512         this._mouseMoveIsRowSelection = false;
513
514         window.addEventListener("mousemove", this);
515         window.addEventListener("mouseup", this);
516     }
517
518     _targetInMessageCanBeSelected(target, message)
519     {
520         if (target.enclosingNodeOrSelfWithNodeName("a"))
521             return false;
522         return true;
523     }
524
525     _mousemove(event)
526     {
527         var selection = window.getSelection();
528         var wrapper = event.target.enclosingNodeOrSelfWithClass(WI.LogContentView.ItemWrapperStyleClassName);
529
530         if (!wrapper) {
531             // No wrapper under the mouse, so look at the selection to try and find one.
532             if (!selection.isCollapsed) {
533                 wrapper = selection.focusNode.parentNode.enclosingNodeOrSelfWithClass(WI.LogContentView.ItemWrapperStyleClassName);
534                 selection.removeAllRanges();
535             }
536
537             if (!wrapper)
538                 return;
539         }
540
541         if (!selection.isCollapsed)
542             this._clearMessagesSelection();
543
544         if (wrapper === this._mouseDownWrapper && !this._mouseMoveIsRowSelection)
545             return;
546
547         selection.removeAllRanges();
548
549         if (!this._mouseMoveIsRowSelection)
550             this._updateMessagesSelection(this._mouseDownWrapper, this._mouseDownCommandKey, this._mouseDownShiftKey, false);
551
552         this._updateMessagesSelection(wrapper, false, true, false);
553
554         this._mouseMoveIsRowSelection = true;
555
556         event.preventDefault();
557         event.stopPropagation();
558     }
559
560     _mouseup(event)
561     {
562         window.removeEventListener("mousemove", this);
563         window.removeEventListener("mouseup", this);
564
565         var selection = window.getSelection();
566         var wrapper = event.target.enclosingNodeOrSelfWithClass(WI.LogContentView.ItemWrapperStyleClassName);
567
568         if (wrapper && (selection.isCollapsed || event.shiftKey)) {
569             selection.removeAllRanges();
570
571             if (this._targetInMessageCanBeSelected(event.target, wrapper)) {
572                 var sameWrapper = wrapper === this._mouseDownWrapper;
573                 this._updateMessagesSelection(wrapper, sameWrapper ? this._mouseDownCommandKey : false, sameWrapper ? this._mouseDownShiftKey : true, false);
574             }
575         } else if (!selection.isCollapsed) {
576             // There is a text selection, clear the row selection.
577             this._clearMessagesSelection();
578         } else if (!this._mouseDownWrapper) {
579             // The mouse didn't hit a console item, so clear the row selection.
580             this._clearMessagesSelection();
581
582             // Focus the prompt. Focusing the prompt needs to happen after the click to work.
583             setTimeout(() => { this.prompt.focus(); }, 0);
584         }
585
586         delete this._mouseMoveIsRowSelection;
587         delete this._mouseDownWrapper;
588         delete this._mouseDownShiftKey;
589         delete this._mouseDownCommandKey;
590     }
591
592     _ondragstart(event)
593     {
594         if (event.target.enclosingNodeOrSelfWithClass(WI.DOMTreeOutline.StyleClassName)) {
595             event.stopPropagation();
596             event.preventDefault();
597         }
598     }
599
600     handleEvent(event)
601     {
602         switch (event.type) {
603         case "mousemove":
604             this._mousemove(event);
605             break;
606         case "mouseup":
607             this._mouseup(event);
608             break;
609         }
610     }
611
612     _updateMessagesSelection(message, multipleSelection, rangeSelection, shouldScrollIntoView)
613     {
614         console.assert(message);
615         if (!message)
616             return;
617
618         var alreadySelectedMessage = this._selectedMessages.includes(message);
619         if (alreadySelectedMessage && this._selectedMessages.length && multipleSelection) {
620             message.classList.remove(WI.LogContentView.SelectedStyleClassName);
621             this._selectedMessages.remove(message);
622             return;
623         }
624
625         if (!multipleSelection && !rangeSelection)
626             this._clearMessagesSelection();
627
628         if (rangeSelection) {
629             var messages = this._visibleMessageElements();
630
631             var refIndex = this._referenceMessageForRangeSelection ? messages.indexOf(this._referenceMessageForRangeSelection) : 0;
632             var targetIndex = messages.indexOf(message);
633
634             var newRange = [Math.min(refIndex, targetIndex), Math.max(refIndex, targetIndex)];
635
636             if (this._selectionRange && this._selectionRange[0] === newRange[0] && this._selectionRange[1] === newRange[1])
637                 return;
638
639             var startIndex = this._selectionRange ? Math.min(this._selectionRange[0], newRange[0]) : newRange[0];
640             var endIndex = this._selectionRange ? Math.max(this._selectionRange[1], newRange[1]) : newRange[1];
641
642             for (var i = startIndex; i <= endIndex; ++i) {
643                 var messageInRange = messages[i];
644                 if (i >= newRange[0] && i <= newRange[1] && !messageInRange.classList.contains(WI.LogContentView.SelectedStyleClassName)) {
645                     messageInRange.classList.add(WI.LogContentView.SelectedStyleClassName);
646                     this._selectedMessages.push(messageInRange);
647                 } else if (i < newRange[0] || i > newRange[1] && messageInRange.classList.contains(WI.LogContentView.SelectedStyleClassName)) {
648                     messageInRange.classList.remove(WI.LogContentView.SelectedStyleClassName);
649                     this._selectedMessages.remove(messageInRange);
650                 }
651             }
652
653             this._selectionRange = newRange;
654         } else {
655             message.classList.add(WI.LogContentView.SelectedStyleClassName);
656             this._selectedMessages.push(message);
657         }
658
659         if (!rangeSelection)
660             this._referenceMessageForRangeSelection = message;
661
662         if (shouldScrollIntoView && !alreadySelectedMessage)
663             this._ensureMessageIsVisible(this._selectedMessages.lastValue);
664     }
665
666     _ensureMessageIsVisible(message)
667     {
668         if (!message)
669             return;
670
671         var y = this._positionForMessage(message).y;
672         if (y < 0) {
673             this.element.scrollTop += y;
674             return;
675         }
676
677         var nextMessage = this._nextMessage(message);
678         if (nextMessage) {
679             y = this._positionForMessage(nextMessage).y;
680             if (y > this._scrollElementHeight)
681                 this.element.scrollTop += y - this._scrollElementHeight;
682         } else {
683             y += message.getBoundingClientRect().height;
684             if (y > this._scrollElementHeight)
685                 this.element.scrollTop += y - this._scrollElementHeight;
686         }
687     }
688
689     _positionForMessage(message)
690     {
691         var pagePoint = window.webkitConvertPointFromNodeToPage(message, new WebKitPoint(0, 0));
692         return window.webkitConvertPointFromPageToNode(this.element, pagePoint);
693     }
694
695     _isMessageVisible(message)
696     {
697         var node = message;
698
699         if (node.classList.contains(WI.LogContentView.FilteredOutStyleClassName))
700             return false;
701
702         if (this.hasPerformedSearch && node.classList.contains(WI.LogContentView.FilteredOutBySearchStyleClassName))
703             return false;
704
705         if (message.classList.contains("console-group-title"))
706             node = node.parentNode.parentNode;
707
708         while (node && node !== this.messagesElement) {
709             if (node.classList.contains("collapsed"))
710                 return false;
711             node = node.parentNode;
712         }
713
714         return true;
715     }
716
717     _isMessageSelected(message)
718     {
719         return message.classList.contains(WI.LogContentView.SelectedStyleClassName);
720     }
721
722     _clearMessagesSelection()
723     {
724         this._selectedMessages.forEach(function(message) {
725             message.classList.remove(WI.LogContentView.SelectedStyleClassName);
726         });
727         this._selectedMessages = [];
728         delete this._referenceMessageForRangeSelection;
729     }
730
731     _selectAllMessages()
732     {
733         this._clearMessagesSelection();
734
735         var messages = this._visibleMessageElements();
736         for (var i = 0; i < messages.length; ++i) {
737             var message = messages[i];
738             message.classList.add(WI.LogContentView.SelectedStyleClassName);
739             this._selectedMessages.push(message);
740         }
741     }
742
743     _allMessageElements()
744     {
745         return Array.from(this.messagesElement.querySelectorAll(".console-message, .console-user-command"));
746     }
747
748     _unfilteredMessageElements()
749     {
750         return this._allMessageElements().filter(function(message) {
751             return !message.classList.contains(WI.LogContentView.FilteredOutStyleClassName);
752         });
753     }
754
755     _visibleMessageElements()
756     {
757         var unfilteredMessages = this._unfilteredMessageElements();
758
759         if (!this.hasPerformedSearch)
760             return unfilteredMessages;
761
762         return unfilteredMessages.filter(function(message) {
763             return !message.classList.contains(WI.LogContentView.FilteredOutBySearchStyleClassName);
764         });
765     }
766
767     _logCleared(event)
768     {
769         for (let item of this._scopeBar.items)
770             item.element.classList.remove("unread");
771
772         this._logViewController.clear();
773         this._nestingLevel = 0;
774
775         if (this._currentSearchQuery)
776             this.performSearch(this._currentSearchQuery);
777     }
778
779     _showConsoleTab()
780     {
781         WI.showConsoleTab();
782     }
783
784     _clearLog()
785     {
786         WI.consoleManager.requestClearMessages();
787     }
788
789     _garbageCollect()
790     {
791         // COMPATIBILITY (iOS 10.3): Worker targets did not support HeapAgent.
792         for (let target of WI.targets) {
793             if (target.HeapAgent)
794                 target.HeapAgent.gc();
795         }
796     }
797
798     _messageShouldBeVisible(message)
799     {
800         let messageSource = this._messageSourceBar && this._scopeFromMessageSource(message.source);
801         if (messageSource && !this._messageSourceBar.item(messageSource).selected && !this._messageSourceBar.item(WI.LogContentView.Scopes.AllChannels).selected)
802             return false;
803
804         let messageLevel = this._scopeFromMessageLevel(message.level);
805         if (messageLevel)
806             return this._scopeBar.item(messageLevel).selected || this._scopeBar.item(WI.LogContentView.Scopes.All).selected;
807
808         return true;
809     }
810
811     _messageSourceBarSelectionDidChange(event)
812     {
813         let selectedItem = this._messageSourceBar.selectedItems[0];
814         if (selectedItem.id === WI.LogContentView.Scopes.AllChannels) {
815             for (let item of this._messageSourceBar.items)
816                 item.element.classList.remove("unread");
817         } else
818             selectedItem.element.classList.remove("unread");
819
820         this._filterMessageElements(this._allMessageElements());
821     }
822
823     _scopeBarSelectionDidChange(event)
824     {
825         let selectedItem = this._scopeBar.selectedItems[0];
826
827         if (selectedItem.id === WI.LogContentView.Scopes.All) {
828             for (let item of this._scopeBar.items)
829                 item.element.classList.remove("unread");
830         } else
831             selectedItem.element.classList.remove("unread");
832
833         this._filterMessageElements(this._allMessageElements());
834     }
835
836     _filterMessageElements(messageElements)
837     {
838         messageElements.forEach(function(messageElement) {
839             let visible = messageElement.__commandView instanceof WI.ConsoleCommandView || messageElement.__message instanceof WI.ConsoleCommandResultMessage;
840             if (!visible)
841                 visible = this._messageShouldBeVisible(messageElement.__message);
842
843             let classList = messageElement.classList;
844             if (visible)
845                 classList.remove(WI.LogContentView.FilteredOutStyleClassName);
846             else {
847                 this._selectedMessages.remove(messageElement);
848                 classList.remove(WI.LogContentView.SelectedStyleClassName);
849                 classList.add(WI.LogContentView.FilteredOutStyleClassName);
850             }
851         }, this);
852
853         this.performSearch(this._currentSearchQuery);
854     }
855
856     _handleClearLogOnNavigateSettingChanged()
857     {
858         this._preserveLogNavigationItem.checked = !WI.settings.clearLogOnNavigate.value;
859     }
860
861     _handleEmulateInUserGestureSettingChanged()
862     {
863         this._emulateInUserGestureNavigationItem.checked = WI.settings.emulateInUserGesture.value;
864     }
865
866     _keyDown(event)
867     {
868         let isRTL = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL;
869
870         if (this._keyboardShortcutCommandA.matchesEvent(event))
871             this._commandAWasPressed(event);
872         else if (this._keyboardShortcutEsc.matchesEvent(event))
873             this._escapeWasPressed(event);
874         else if (event.keyIdentifier === "Up")
875             this._upArrowWasPressed(event);
876         else if (event.keyIdentifier === "Down")
877             this._downArrowWasPressed(event);
878         else if ((!isRTL && event.keyIdentifier === "Left") || (isRTL && event.keyIdentifier === "Right"))
879             this._leftArrowWasPressed(event);
880         else if ((!isRTL && event.keyIdentifier === "Right") || (isRTL && event.keyIdentifier === "Left"))
881             this._rightArrowWasPressed(event);
882         else if (event.keyIdentifier === "Enter" && event.metaKey)
883             this._commandEnterWasPressed(event);
884     }
885
886     _keyPress(event)
887     {
888         const isCommandC = event.metaKey && event.keyCode === 99;
889         if (!isCommandC)
890             this.prompt.focus();
891     }
892
893     _commandAWasPressed(event)
894     {
895         this._selectAllMessages();
896         event.preventDefault();
897     }
898
899     _escapeWasPressed(event)
900     {
901         if (this._selectedMessages.length)
902             this._clearMessagesSelection();
903         else
904             this.prompt.focus();
905
906         event.preventDefault();
907     }
908
909     _upArrowWasPressed(event)
910     {
911         var messages = this._visibleMessageElements();
912
913         if (!this._selectedMessages.length) {
914             if (messages.length)
915                 this._updateMessagesSelection(messages.lastValue, false, false, true);
916             return;
917         }
918
919         var lastMessage = this._selectedMessages.lastValue;
920         var previousMessage = this._previousMessage(lastMessage);
921         if (previousMessage)
922             this._updateMessagesSelection(previousMessage, false, event.shiftKey, true);
923         else if (!event.shiftKey) {
924             this._clearMessagesSelection();
925             this._updateMessagesSelection(messages[0], false, false, true);
926         }
927
928         event.preventDefault();
929     }
930
931     _downArrowWasPressed(event)
932     {
933         var messages = this._visibleMessageElements();
934
935         if (!this._selectedMessages.length) {
936             if (messages.length)
937                 this._updateMessagesSelection(messages[0], false, false, true);
938             return;
939         }
940
941         var lastMessage = this._selectedMessages.lastValue;
942         var nextMessage = this._nextMessage(lastMessage);
943         if (nextMessage)
944             this._updateMessagesSelection(nextMessage, false, event.shiftKey, true);
945         else if (!event.shiftKey) {
946             this._clearMessagesSelection();
947             this._updateMessagesSelection(messages.lastValue, false, false, true);
948         }
949
950         event.preventDefault();
951     }
952
953     _leftArrowWasPressed(event)
954     {
955         if (this._selectedMessages.length !== 1)
956             return;
957
958         var currentMessage = this._selectedMessages[0];
959         if (currentMessage.classList.contains("console-group-title")) {
960             currentMessage.parentNode.classList.add("collapsed");
961             event.preventDefault();
962         } else if (currentMessage.__messageView && currentMessage.__messageView.expandable) {
963             currentMessage.__messageView.collapse();
964             event.preventDefault();
965         }
966     }
967
968     _rightArrowWasPressed(event)
969     {
970         if (this._selectedMessages.length !== 1)
971             return;
972
973         var currentMessage = this._selectedMessages[0];
974         if (currentMessage.classList.contains("console-group-title")) {
975             currentMessage.parentNode.classList.remove("collapsed");
976             event.preventDefault();
977         } else if (currentMessage.__messageView && currentMessage.__messageView.expandable) {
978             currentMessage.__messageView.expand();
979             event.preventDefault();
980         }
981     }
982
983     _commandEnterWasPressed(event)
984     {
985         if (this._selectedMessages.length !== 1)
986             return;
987
988         let message = this._selectedMessages[0];
989         if (message.__commandView && message.__commandView.commandText) {
990             this._logViewController.consolePromptTextCommitted(null, message.__commandView.commandText);
991             event.preventDefault();
992         }
993     }
994
995     _previousMessage(message)
996     {
997         var messages = this._visibleMessageElements();
998         for (var i = messages.indexOf(message) - 1; i >= 0; --i) {
999             if (this._isMessageVisible(messages[i]))
1000                 return messages[i];
1001         }
1002         return null;
1003     }
1004
1005     _nextMessage(message)
1006     {
1007         var messages = this._visibleMessageElements();
1008         for (var i = messages.indexOf(message) + 1; i < messages.length; ++i) {
1009             if (this._isMessageVisible(messages[i]))
1010                 return messages[i];
1011         }
1012         return null;
1013     }
1014
1015     _clearFocusableChildren()
1016     {
1017         var focusableElements = this.messagesElement.querySelectorAll("[tabindex]");
1018         for (var i = 0, count = focusableElements.length; i < count; ++i)
1019             focusableElements[i].removeAttribute("tabindex");
1020     }
1021
1022     findBannerPerformSearch(findBanner, searchQuery)
1023     {
1024         this.performSearch(searchQuery);
1025     }
1026
1027     findBannerSearchCleared()
1028     {
1029         this.searchCleared();
1030     }
1031
1032     revealNextSearchResult()
1033     {
1034         this.findBannerRevealNextResult();
1035     }
1036
1037     revealPreviousSearchResult()
1038     {
1039         this.findBannerRevealPreviousResult();
1040     }
1041
1042     performSearch(searchQuery)
1043     {
1044         if (!isEmptyObject(this._searchHighlightDOMChanges))
1045             WI.revertDOMChanges(this._searchHighlightDOMChanges);
1046
1047         this._currentSearchQuery = searchQuery;
1048         this._searchHighlightDOMChanges = [];
1049         this._searchMatches = [];
1050         this._selectedSearchMatchIsValid = false;
1051         this._selectedSearchMatch = null;
1052         let numberOfResults = 0;
1053
1054         if (this._currentSearchQuery === "") {
1055             this.element.classList.remove(WI.LogContentView.SearchInProgressStyleClassName);
1056             this.dispatchEventToListeners(WI.ContentView.Event.NumberOfSearchResultsDidChange);
1057             return;
1058         }
1059
1060         this.element.classList.add(WI.LogContentView.SearchInProgressStyleClassName);
1061
1062         let searchRegex = new RegExp(this._currentSearchQuery.escapeForRegExp(), "gi");
1063         this._unfilteredMessageElements().forEach(function(message) {
1064             let matchRanges = [];
1065             let text = message.textContent;
1066             let match = searchRegex.exec(text);
1067             while (match) {
1068                 numberOfResults++;
1069                 matchRanges.push({offset: match.index, length: match[0].length});
1070                 match = searchRegex.exec(text);
1071             }
1072
1073             if (!isEmptyObject(matchRanges))
1074                 this._highlightRanges(message, matchRanges);
1075
1076             let classList = message.classList;
1077             if (!isEmptyObject(matchRanges) || message.__commandView instanceof WI.ConsoleCommandView || message.__message instanceof WI.ConsoleCommandResultMessage)
1078                 classList.remove(WI.LogContentView.FilteredOutBySearchStyleClassName);
1079             else
1080                 classList.add(WI.LogContentView.FilteredOutBySearchStyleClassName);
1081         }, this);
1082
1083         this.dispatchEventToListeners(WI.ContentView.Event.NumberOfSearchResultsDidChange);
1084
1085         this._findBanner.numberOfResults = numberOfResults;
1086
1087         if (!this._selectedSearchMatchIsValid && this._selectedSearchMatch) {
1088             this._selectedSearchMatch.highlight.classList.remove(WI.LogContentView.SelectedStyleClassName);
1089             this._selectedSearchMatch = null;
1090         }
1091     }
1092
1093     searchHidden()
1094     {
1095         this.searchCleared();
1096     }
1097
1098     searchCleared()
1099     {
1100         this.performSearch("");
1101     }
1102
1103     _highlightRanges(message, matchRanges)
1104     {
1105         var highlightedElements = WI.highlightRangesWithStyleClass(message, matchRanges, WI.LogContentView.HighlightedStyleClassName, this._searchHighlightDOMChanges);
1106
1107         console.assert(highlightedElements.length === matchRanges.length);
1108
1109         matchRanges.forEach(function(range, index) {
1110             this._searchMatches.push({message, range, highlight: highlightedElements[index]});
1111
1112             if (this._selectedSearchMatch && !this._selectedSearchMatchIsValid && this._selectedSearchMatch.message === message) {
1113                 this._selectedSearchMatchIsValid = this._rangesOverlap(this._selectedSearchMatch.range, range);
1114                 if (this._selectedSearchMatchIsValid) {
1115                     delete this._selectedSearchMatch;
1116                     this._highlightSearchMatchAtIndex(this._searchMatches.length - 1);
1117                 }
1118             }
1119         }, this);
1120     }
1121
1122     _rangesOverlap(range1, range2)
1123     {
1124         return range1.offset <= range2.offset + range2.length && range2.offset <= range1.offset + range1.length;
1125     }
1126
1127     _highlightSearchMatchAtIndex(index)
1128     {
1129         if (index >= this._searchMatches.length)
1130             index = 0;
1131         else if (index < 0)
1132             index = this._searchMatches.length - 1;
1133
1134         if (this._selectedSearchMatch)
1135             this._selectedSearchMatch.highlight.classList.remove(WI.LogContentView.SelectedStyleClassName);
1136
1137         this._selectedSearchMatch = this._searchMatches[index];
1138         this._selectedSearchMatch.highlight.classList.add(WI.LogContentView.SelectedStyleClassName);
1139
1140         this._ensureMessageIsVisible(this._selectedSearchMatch.message);
1141     }
1142
1143     _provisionalLoadStarted()
1144     {
1145         this._startedProvisionalLoad = true;
1146     }
1147
1148     _reappendProvisionalMessages()
1149     {
1150         if (!this._startedProvisionalLoad)
1151             return;
1152
1153         this._startedProvisionalLoad = false;
1154
1155         for (let provisionalMessage of this._provisionalMessages)
1156             this._logViewController.appendConsoleMessage(provisionalMessage);
1157
1158         this._provisionalMessages = [];
1159     }
1160
1161     _clearProvisionalState()
1162     {
1163         this._startedProvisionalLoad = false;
1164         this._provisionalMessages = [];
1165     }
1166 };
1167
1168 WI.LogContentView.Scopes = {
1169     All: "log-all",
1170     Errors: "log-errors",
1171     Warnings: "log-warnings",
1172     Logs: "log-logs",
1173     Infos: "log-infos",
1174     Debugs: "log-debugs",
1175     AllChannels: "log-all-channels",
1176     Media: "log-media",
1177     WebRTC: "log-webrtc",
1178 };
1179
1180 WI.LogContentView.ItemWrapperStyleClassName = "console-item";
1181 WI.LogContentView.FilteredOutStyleClassName = "filtered-out";
1182 WI.LogContentView.SelectedStyleClassName = "selected";
1183 WI.LogContentView.SearchInProgressStyleClassName = "search-in-progress";
1184 WI.LogContentView.FilteredOutBySearchStyleClassName = "filtered-out-by-search";
1185 WI.LogContentView.HighlightedStyleClassName = "highlighted";