Web Inspector: Charles Proxy errors opening har files exported from Safari (invalid...
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / NetworkTableContentView.js
1 /*
2  * Copyright (C) 2017 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.NetworkTableContentView = class NetworkTableContentView extends WI.ContentView
27 {
28     constructor(representedObject, extraArguments)
29     {
30         super(representedObject);
31
32         this._entries = [];
33         this._entriesSortComparator = null;
34         this._filteredEntries = [];
35         this._pendingInsertions = [];
36         this._pendingUpdates = [];
37         this._pendingFilter = false;
38         this._showingRepresentedObjectCookie = null;
39
40         this._table = null;
41         this._nameColumnWidthSetting = new WI.Setting("network-table-content-view-name-column-width", 250);
42
43         this._selectedObject = null;
44         this._detailView = null;
45         this._detailViewMap = new Map;
46
47         this._domNodeEntries = new Map;
48
49         this._waterfallStartTime = NaN;
50         this._waterfallEndTime = NaN;
51         this._waterfallTimelineRuler = null;
52         this._waterfallPopover = null;
53
54         // FIXME: Network Timeline.
55         // FIXME: Throttling.
56
57         this._typeFilterScopeBarItemAll = new WI.ScopeBarItem("network-type-filter-all", WI.UIString("All"), {exclusive: true});
58         let typeFilterScopeBarItems = [this._typeFilterScopeBarItemAll];
59
60         let uniqueTypes = [
61             ["Document", (type) => type === WI.Resource.Type.Document],
62             ["Stylesheet", (type) => type === WI.Resource.Type.Stylesheet],
63             ["Image", (type) => type === WI.Resource.Type.Image],
64             ["Font", (type) => type === WI.Resource.Type.Font],
65             ["Script", (type) => type === WI.Resource.Type.Script],
66             ["XHR", (type) => type === WI.Resource.Type.XHR || type === WI.Resource.Type.Fetch],
67             ["Other", (type) => {
68                 return type !== WI.Resource.Type.Document
69                     && type !== WI.Resource.Type.Stylesheet
70                     && type !== WI.Resource.Type.Image
71                     && type !== WI.Resource.Type.Font
72                     && type !== WI.Resource.Type.Script
73                     && type !== WI.Resource.Type.XHR
74                     && type !== WI.Resource.Type.Fetch;
75             }],
76         ];
77         for (let [key, checker] of uniqueTypes) {
78             let type = WI.Resource.Type[key];
79             let scopeBarItem = new WI.ScopeBarItem("network-type-filter-" + key, WI.NetworkTableContentView.shortDisplayNameForResourceType(type));
80             scopeBarItem.__checker = checker;
81             typeFilterScopeBarItems.push(scopeBarItem);
82         }
83
84         this._typeFilterScopeBar = new WI.ScopeBar("network-type-filter-scope-bar", typeFilterScopeBarItems, typeFilterScopeBarItems[0]);
85         this._typeFilterScopeBar.addEventListener(WI.ScopeBar.Event.SelectionChanged, this._typeFilterScopeBarSelectionChanged, this);
86
87         if (WI.MediaInstrument.supported()) {
88             this._groupByDOMNodeNavigationItem = new WI.CheckboxNavigationItem("group-by-node", WI.UIString("Group Media Requests"), WI.settings.groupByDOMNode.value);
89             this._groupByDOMNodeNavigationItem.addEventListener(WI.CheckboxNavigationItem.Event.CheckedDidChange, this._handleGroupByDOMNodeCheckedDidChange, this);
90         } else
91             WI.settings.groupByDOMNode.value = false;
92
93         this._urlFilterSearchText = null;
94         this._urlFilterSearchRegex = null;
95         this._urlFilterIsActive = false;
96
97         this._urlFilterNavigationItem = new WI.FilterBarNavigationItem;
98         this._urlFilterNavigationItem.filterBar.addEventListener(WI.FilterBar.Event.FilterDidChange, this._urlFilterDidChange, this);
99         this._urlFilterNavigationItem.filterBar.placeholder = WI.UIString("Filter Full URL");
100
101         this._activeTypeFilters = this._generateTypeFilter();
102         this._activeURLFilterResources = new Set;
103
104         this._emptyFilterResultsMessageElement = null;
105
106         this._clearOnLoadNavigationItem = new WI.CheckboxNavigationItem("perserve-log", WI.UIString("Preserve Log"), !WI.settings.clearNetworkOnNavigate.value);
107         this._clearOnLoadNavigationItem.tooltip = WI.UIString("Do not clear network items on new page loads");
108         this._clearOnLoadNavigationItem.addEventListener(WI.CheckboxNavigationItem.Event.CheckedDidChange, () => { WI.settings.clearNetworkOnNavigate.value = !WI.settings.clearNetworkOnNavigate.value; });
109         WI.settings.clearNetworkOnNavigate.addEventListener(WI.Setting.Event.Changed, this._clearNetworkOnNavigateSettingChanged, this);
110
111         this._harExportNavigationItem = new WI.ButtonNavigationItem("har-export", WI.UIString("Export"), "Images/Export.svg", 15, 15);
112         this._harExportNavigationItem.buttonStyle = WI.ButtonNavigationItem.Style.ImageAndText;
113         this._harExportNavigationItem.tooltip = WI.UIString("HAR Export (%s)").format(WI.saveKeyboardShortcut.displayName);
114         this._harExportNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, () => { this._exportHAR(); });
115
116         this._checkboxsNavigationItemGroup = new WI.GroupNavigationItem([this._clearOnLoadNavigationItem, new WI.DividerNavigationItem]);
117         this._checkboxsNavigationItemGroup.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
118
119         this._buttonsNavigationItemGroup = new WI.GroupNavigationItem([this._harExportNavigationItem, new WI.DividerNavigationItem]);
120         this._buttonsNavigationItemGroup.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
121
122         // COMPATIBILITY (iOS 10.3): Network.setDisableResourceCaching did not exist.
123         if (window.NetworkAgent && NetworkAgent.setResourceCachingDisabled) {
124             let toolTipForDisableResourceCache = WI.UIString("Ignore the resource cache when loading resources");
125             let activatedToolTipForDisableResourceCache = WI.UIString("Use the resource cache when loading resources");
126             this._disableResourceCacheNavigationItem = new WI.ActivateButtonNavigationItem("disable-resource-cache", toolTipForDisableResourceCache, activatedToolTipForDisableResourceCache, "Images/IgnoreCaches.svg", 16, 16);
127             this._disableResourceCacheNavigationItem.activated = WI.settings.resourceCachingDisabled.value;
128
129             this._disableResourceCacheNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._toggleDisableResourceCache, this);
130             WI.settings.resourceCachingDisabled.addEventListener(WI.Setting.Event.Changed, this._resourceCachingDisabledSettingChanged, this);
131         }
132
133         this._clearNetworkItemsNavigationItem = new WI.ButtonNavigationItem("clear-network-items", WI.UIString("Clear Network Items (%s)").format(WI.clearKeyboardShortcut.displayName), "Images/NavigationItemTrash.svg", 15, 15);
134         this._clearNetworkItemsNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, () => { this.reset(); });
135
136         WI.Target.addEventListener(WI.Target.Event.ResourceAdded, this._handleResourceAdded, this);
137         WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
138         WI.Frame.addEventListener(WI.Frame.Event.ResourceWasAdded, this._handleResourceAdded, this);
139         WI.Resource.addEventListener(WI.Resource.Event.LoadingDidFinish, this._resourceLoadingDidFinish, this);
140         WI.Resource.addEventListener(WI.Resource.Event.LoadingDidFail, this._resourceLoadingDidFail, this);
141         WI.Resource.addEventListener(WI.Resource.Event.TransferSizeDidChange, this._resourceTransferSizeDidChange, this);
142         WI.networkManager.addEventListener(WI.NetworkManager.Event.MainFrameDidChange, this._mainFrameDidChange, this);
143
144         this._needsInitialPopulate = true;
145
146         // FIXME: This is working around the order of events. Normal page navigation
147         // triggers a MainResource change and then a MainFrame change. Page Transition
148         // triggers a MainFrame change then a MainResource change.
149         this._transitioningPageTarget = false;
150
151         WI.notifications.addEventListener(WI.Notification.TransitionPageTarget, this._transitionPageTarget, this);
152     }
153
154     // Static
155
156     static displayNameForResource(resource)
157     {
158         if (resource.type === WI.Resource.Type.Image || resource.type === WI.Resource.Type.Font || resource.type === WI.Resource.Type.Other) {
159             let fileExtension;
160             if (resource.mimeType)
161                 fileExtension = WI.fileExtensionForMIMEType(resource.mimeType);
162             if (!fileExtension)
163                 fileExtension = WI.fileExtensionForURL(resource.url);
164             if (fileExtension)
165                 return fileExtension;
166         }
167
168         return WI.NetworkTableContentView.shortDisplayNameForResourceType(resource.type).toLowerCase();
169     }
170
171     static shortDisplayNameForResourceType(type)
172     {
173         switch (type) {
174         case WI.Resource.Type.Document:
175             return WI.UIString("Document");
176         case WI.Resource.Type.Stylesheet:
177             return "CSS";
178         case WI.Resource.Type.Image:
179             return WI.UIString("Image");
180         case WI.Resource.Type.Font:
181             return WI.UIString("Font");
182         case WI.Resource.Type.Script:
183             return "JS";
184         case WI.Resource.Type.XHR:
185             return "XHR";
186         case WI.Resource.Type.Fetch:
187             return WI.UIString("Fetch");
188         case WI.Resource.Type.Ping:
189             return WI.UIString("Ping");
190         case WI.Resource.Type.Beacon:
191             return WI.UIString("Beacon");
192         case WI.Resource.Type.WebSocket:
193         case WI.Resource.Type.Other:
194             return WI.UIString("Other");
195         default:
196             console.error("Unknown resource type", type);
197             return null;
198         }
199     }
200
201     // Public
202
203     get selectionPathComponents()
204     {
205         return null;
206     }
207
208     get navigationItems()
209     {
210         let items = [this._checkboxsNavigationItemGroup, this._buttonsNavigationItemGroup];
211         if (this._disableResourceCacheNavigationItem)
212             items.push(this._disableResourceCacheNavigationItem);
213         items.push(this._clearNetworkItemsNavigationItem);
214         return items;
215     }
216
217     get filterNavigationItems()
218     {
219         let navigationItems = [this._urlFilterNavigationItem, this._typeFilterScopeBar];
220         if (WI.MediaInstrument.supported())
221             navigationItems.push(this._groupByDOMNodeNavigationItem);
222         return navigationItems;
223     }
224
225     get supportsSave()
226     {
227         return this._filteredEntries.some((entry) => entry.resource.finished);
228     }
229
230     get saveData()
231     {
232         return {customSaveHandler: () => { this._exportHAR(); }};
233     }
234
235     shown()
236     {
237         super.shown();
238
239         if (this._detailView)
240             this._detailView.shown();
241
242         if (this._table)
243             this._table.restoreScrollPosition();
244     }
245
246     hidden()
247     {
248         this._hidePopover();
249
250         if (this._detailView)
251             this._detailView.hidden();
252
253         super.hidden();
254     }
255
256     closed()
257     {
258         for (let detailView of this._detailViewMap.values())
259             detailView.dispose();
260         this._detailViewMap.clear();
261
262         this._domNodeEntries.clear();
263
264         this._hidePopover();
265         this._hideDetailView();
266
267         WI.Target.removeEventListener(null, null, this);
268         WI.Frame.removeEventListener(null, null, this);
269         WI.Resource.removeEventListener(null, null, this);
270         WI.settings.resourceCachingDisabled.removeEventListener(null, null, this);
271         WI.settings.clearNetworkOnNavigate.removeEventListener(null, null, this);
272         WI.networkManager.removeEventListener(WI.NetworkManager.Event.MainFrameDidChange, this._mainFrameDidChange, this);
273
274         super.closed();
275     }
276
277     reset()
278     {
279         this._entries = [];
280         this._filteredEntries = [];
281         this._pendingInsertions = [];
282
283         for (let detailView of this._detailViewMap.values())
284             detailView.dispose();
285         this._detailViewMap.clear();
286
287         this._domNodeEntries.clear();
288
289         this._waterfallStartTime = NaN;
290         this._waterfallEndTime = NaN;
291         this._updateWaterfallTimelineRuler();
292         this._updateExportButton();
293
294         if (this._table) {
295             this._selectedObject = null;
296             this._table.reloadData();
297             this._hidePopover();
298             this._hideDetailView();
299         }
300     }
301
302     showRepresentedObject(representedObject, cookie)
303     {
304         console.assert(representedObject instanceof WI.Resource);
305
306         let rowIndex = this._rowIndexForRepresentedObject(representedObject);
307         if (rowIndex === -1) {
308             this._selectedObject = null;
309             this._table.deselectAll();
310             this._hideDetailView();
311             return;
312         }
313
314         this._showingRepresentedObjectCookie = cookie;
315         this._table.selectRow(rowIndex);
316         this._showingRepresentedObjectCookie = null;
317     }
318
319     // NetworkDetailView delegate
320
321     networkDetailViewClose(networkDetailView)
322     {
323         this._selectedObject = null;
324         this._table.deselectAll();
325         this._hideDetailView();
326     }
327
328     // Table dataSource
329
330     tableNumberOfRows(table)
331     {
332         return this._filteredEntries.length;
333     }
334
335     tableSortChanged(table)
336     {
337         this._generateSortComparator();
338
339         if (!this._entriesSortComparator)
340             return;
341
342         this._hideDetailView();
343
344         for (let nodeEntry of this._domNodeEntries.values())
345             nodeEntry.initiatedResourceEntries.sort(this._entriesSortComparator);
346
347         this._updateSort();
348         this._updateFilteredEntries();
349         this._reloadTable();
350     }
351
352     // Table delegate
353
354     tableCellContextMenuClicked(table, cell, column, rowIndex, event)
355     {
356         if (column !== this._nameColumn)
357             return;
358
359         this._table.selectRow(rowIndex);
360
361         let entry = this._filteredEntries[rowIndex];
362         let contextMenu = WI.ContextMenu.createFromEvent(event);
363         WI.appendContextMenuItemsForSourceCode(contextMenu, entry.resource);
364
365         contextMenu.appendSeparator();
366         contextMenu.appendItem(WI.UIString("Export HAR"), () => { this._exportHAR(); }, !this._canExportHAR());
367     }
368
369     tableShouldSelectRow(table, cell, column, rowIndex)
370     {
371         return column === this._nameColumn;
372     }
373
374     tableSelectionDidChange(table)
375     {
376         let rowIndex = table.selectedRow;
377         if (isNaN(rowIndex)) {
378             this._selectedObject = null;
379             this._hideDetailView();
380             return;
381         }
382
383         let entry = this._filteredEntries[rowIndex];
384         if (entry.resource === this._selectedObject || entry.domNode === this._selectedObject)
385             return;
386
387         this._selectedObject = entry.resource || entry.domNode;
388         if (this._selectedObject)
389             this._showDetailView(this._selectedObject);
390         else
391             this._hideDetailView();
392     }
393
394     tablePopulateCell(table, cell, column, rowIndex)
395     {
396         let entry = this._filteredEntries[rowIndex];
397
398         if (entry.resource)
399             cell.classList.toggle("error", entry.resource.hadLoadingError());
400
401         let setTextContent = (accessor) => {
402             let uniqueValues = this._uniqueValuesForDOMNodeEntry(entry, accessor);
403             if (uniqueValues) {
404                 if (uniqueValues.size > 1) {
405                     cell.classList.add("multiple");
406                     cell.textContent = WI.UIString("(multiple)");
407                     return;
408                 }
409
410                 cell.textContent = uniqueValues.values().next().value || emDash;
411                 return;
412             }
413
414             cell.textContent = accessor(entry) || emDash;
415         };
416
417         switch (column.identifier) {
418         case "name":
419             this._populateNameCell(cell, entry);
420             break;
421         case "domain":
422             this._populateDomainCell(cell, entry);
423             break;
424         case "type":
425             setTextContent((resourceEntry) => resourceEntry.displayType);
426             break;
427         case "mimeType":
428             setTextContent((resourceEntry) => resourceEntry.mimeType);
429             break;
430         case "method":
431             setTextContent((resourceEntry) => resourceEntry.method);
432             break;
433         case "scheme":
434             setTextContent((resourceEntry) => resourceEntry.scheme);
435             break;
436         case "status":
437             setTextContent((resourceEntry) => resourceEntry.status);
438             break;
439         case "protocol":
440             setTextContent((resourceEntry) => resourceEntry.protocol);
441             break;
442         case "initiator":
443             this._populateInitiatorCell(cell, entry);
444             break;
445         case "priority":
446             setTextContent((resourceEntry) => WI.Resource.displayNameForPriority(resourceEntry.priority));
447             break;
448         case "remoteAddress":
449             setTextContent((resourceEntry) => resourceEntry.remoteAddress);
450             break;
451         case "connectionIdentifier":
452             setTextContent((resourceEntry) => resourceEntry.connectionIdentifier);
453             break;
454         case "resourceSize": {
455             let resourceSize = entry.resourceSize;
456             let resourceEntries = entry.initiatedResourceEntries;
457             if (resourceEntries)
458                 resourceSize = resourceEntries.reduce((accumulator, current) => accumulator + (current.resourceSize || 0), 0);
459             cell.textContent = isNaN(resourceSize) ? emDash : Number.bytesToString(resourceSize);
460             break;
461         }
462         case "transferSize":
463             this._populateTransferSizeCell(cell, entry);
464             break;
465         case "time": {
466             // FIXME: <https://webkit.org/b/176748> Web Inspector: Frontend sometimes receives resources with negative duration (responseEnd - requestStart)
467             let time = entry.time;
468             let resourceEntries = entry.initiatedResourceEntries;
469             if (resourceEntries)
470                 time = resourceEntries.reduce((accumulator, current) => accumulator + (current.time || 0), 0);
471             cell.textContent = isNaN(time) ? emDash : Number.secondsToString(Math.max(time, 0));
472             break;
473         }
474         case "waterfall":
475             this._populateWaterfallGraph(cell, entry);
476             break;
477         }
478
479         return cell;
480     }
481
482     // Private
483
484     _populateNameCell(cell, entry)
485     {
486         console.assert(!cell.firstChild, "We expect the cell to be empty.", cell, cell.firstChild);
487
488         function createIconElement() {
489             let iconElement = cell.appendChild(document.createElement("img"));
490             iconElement.className = "icon";
491         }
492
493         let domNode = entry.domNode;
494         if (domNode) {
495             this._table.element.classList.add("grouped");
496
497             cell.classList.add("parent");
498
499             let disclosureElement = cell.appendChild(document.createElement("img"));
500             disclosureElement.classList.add("disclosure");
501             disclosureElement.classList.toggle("expanded", !!entry.expanded);
502             disclosureElement.addEventListener("click", (event) => {
503                 entry.expanded = !entry.expanded;
504
505                 this._updateFilteredEntries();
506                 this._reloadTable();
507             });
508
509             createIconElement();
510
511             cell.classList.add("dom-node");
512             cell.appendChild(WI.linkifyNodeReference(domNode));
513             return;
514         }
515
516         let resource = entry.resource;
517         if (resource.isLoading()) {
518             let statusElement = cell.appendChild(document.createElement("div"));
519             statusElement.className = "status";
520             let spinner = new WI.IndeterminateProgressSpinner;
521             statusElement.appendChild(spinner.element);
522         }
523
524         createIconElement();
525
526         cell.classList.add(WI.ResourceTreeElement.ResourceIconStyleClassName);
527
528         if (WI.settings.groupByDOMNode.value && resource.initiatorNode) {
529             let nodeEntry = this._domNodeEntries.get(resource.initiatorNode);
530             if (nodeEntry.initiatedResourceEntries.length > 1 || nodeEntry.domNode.domEvents.length)
531                 cell.classList.add("child");
532         }
533
534         let nameElement = cell.appendChild(document.createElement("span"));
535         nameElement.textContent = entry.name;
536
537         let range = resource.requestedByteRange;
538         if (range) {
539             let rangeElement = nameElement.appendChild(document.createElement("span"));
540             rangeElement.classList.add("range");
541             rangeElement.textContent = WI.UIString("Byte Range %s\u2013%s").format(range.start, range.end);
542         }
543
544         cell.title = resource.url;
545         cell.classList.add(WI.Resource.classNameForResource(resource));
546     }
547
548     _populateDomainCell(cell, entry)
549     {
550         console.assert(!cell.firstChild, "We expect the cell to be empty.", cell, cell.firstChild);
551
552         function createIconAndText(scheme, domain) {
553             let secure = scheme === "https" || scheme === "wss";
554             if (secure) {
555                 let lockIconElement = cell.appendChild(document.createElement("img"));
556                 lockIconElement.className = "lock";
557             }
558
559             cell.append(domain || emDash);
560         }
561
562         let uniqueSchemeValues = this._uniqueValuesForDOMNodeEntry(entry, (resourceEntry) => resourceEntry.scheme);
563         let uniqueDomainValues = this._uniqueValuesForDOMNodeEntry(entry, (resourceEntry) => resourceEntry.domain);
564         if (uniqueSchemeValues && uniqueDomainValues) {
565             if (uniqueSchemeValues.size > 1 || uniqueDomainValues.size > 1) {
566                 cell.classList.add("multiple");
567                 cell.textContent = WI.UIString("(multiple)");
568                 return;
569             }
570
571             createIconAndText(uniqueSchemeValues.values().next().value, uniqueDomainValues.values().next().value);
572             return;
573         }
574
575         createIconAndText(entry.scheme, entry.domain);
576     }
577
578     _populateInitiatorCell(cell, entry)
579     {
580         let domNode = entry.domNode;
581         if (domNode) {
582             cell.textContent = emDash;
583             return;
584         }
585
586         let initiatorLocation = entry.resource.initiatorSourceCodeLocation;
587         if (!initiatorLocation) {
588             cell.textContent = emDash;
589             return;
590         }
591
592         const options = {
593             dontFloat: true,
594             ignoreSearchTab: true,
595         };
596         cell.appendChild(WI.createSourceCodeLocationLink(initiatorLocation, options));
597     }
598
599     _populateTransferSizeCell(cell, entry)
600     {
601         let resourceEntries = entry.initiatedResourceEntries;
602         if (resourceEntries) {
603             if (resourceEntries.every((resourceEntry) => resourceEntry.resource.responseSource === WI.Resource.ResponseSource.MemoryCache)) {
604                 cell.classList.add("cache-type");
605                 cell.textContent = WI.UIString("(memory)");
606                 return;
607             }
608             if (resourceEntries.every((resourceEntry) => resourceEntry.resource.responseSource === WI.Resource.ResponseSource.DiskCache)) {
609                 cell.classList.add("cache-type");
610                 cell.textContent = WI.UIString("(disk)");
611                 return;
612             }
613             if (resourceEntries.every((resourceEntry) => resourceEntry.resource.responseSource === WI.Resource.ResponseSource.ServiceWorker)) {
614                 cell.classList.add("cache-type");
615                 cell.textContent = WI.UIString("(service worker)");
616                 return;
617             }
618             let transferSize = resourceEntries.reduce((accumulator, current) => accumulator + (current.transferSize || 0), 0);
619             if (isNaN(transferSize))
620                 cell.textContent = emDash;
621             else
622                 cell.textContent = Number.bytesToString(transferSize);
623             return;
624         }
625
626         let responseSource = entry.resource.responseSource;
627         if (responseSource === WI.Resource.ResponseSource.MemoryCache) {
628             cell.classList.add("cache-type");
629             cell.textContent = WI.UIString("(memory)");
630             return;
631         }
632         if (responseSource === WI.Resource.ResponseSource.DiskCache) {
633             cell.classList.add("cache-type");
634             cell.textContent = WI.UIString("(disk)");
635             return;
636         }
637         if (responseSource === WI.Resource.ResponseSource.ServiceWorker) {
638             cell.classList.add("cache-type");
639             cell.textContent = WI.UIString("(service worker)");
640             return;
641         }
642
643         let transferSize = entry.transferSize;
644         cell.textContent = isNaN(transferSize) ? emDash : Number.bytesToString(transferSize);
645         console.assert(!cell.classList.contains("cache-type"), "Should not have cache-type class on cell.");
646     }
647
648     _populateWaterfallGraph(cell, entry)
649     {
650         cell.removeChildren();
651
652         let container = cell.appendChild(document.createElement("div"));
653         container.className = "waterfall-container";
654
655         let graphStartTime = this._waterfallTimelineRuler.startTime;
656         let secondsPerPixel = this._waterfallTimelineRuler.secondsPerPixel;
657
658         function positionByStartOffset(element, timestamp) {
659             let styleAttribute = WI.resolvedLayoutDirection() === WI.LayoutDirection.LTR ? "left" : "right";
660             element.style.setProperty(styleAttribute, ((timestamp - graphStartTime) / secondsPerPixel) + "px");
661         }
662
663         function setWidthForDuration(element, startTimestamp, endTimestamp) {
664             element.style.setProperty("width", ((endTimestamp - startTimestamp) / secondsPerPixel) + "px");
665         }
666
667         let domNode = entry.domNode;
668         if (domNode) {
669             const domEventElementSize = 8; // Keep this in sync with `--node-waterfall-dom-event-size`.
670
671             let groupedDOMEvents = [];
672             for (let domEvent of domNode.domEvents) {
673                 if (domEvent.originator)
674                     continue;
675
676                 if (!groupedDOMEvents.length || (domEvent.timestamp - groupedDOMEvents.lastValue.endTimestamp) >= (domEventElementSize * secondsPerPixel)) {
677                     groupedDOMEvents.push({
678                         startTimestamp: domEvent.timestamp,
679                         domEvents: [],
680                     });
681                 }
682                 groupedDOMEvents.lastValue.endTimestamp = domEvent.timestamp;
683                 groupedDOMEvents.lastValue.domEvents.push(domEvent);
684             }
685
686             let fullscreenDOMEvents = WI.DOMNode.getFullscreenDOMEvents(domNode.domEvents);
687             if (fullscreenDOMEvents.length) {
688                 if (!fullscreenDOMEvents[0].data.enabled)
689                     fullscreenDOMEvents.unshift({timestamp: graphStartTime});
690
691                 if (fullscreenDOMEvents.lastValue.data.enabled)
692                     fullscreenDOMEvents.push({timestamp: this._waterfallEndTime});
693
694                 console.assert((fullscreenDOMEvents.length % 2) === 0, "Every enter/exit of fullscreen should have a corresponding exit/enter.");
695
696                 for (let i = 0; i < fullscreenDOMEvents.length; i += 2) {
697                     let fullscreenElement = container.appendChild(document.createElement("div"));
698                     fullscreenElement.classList.add("area", "dom-fullscreen");
699                     positionByStartOffset(fullscreenElement, fullscreenDOMEvents[i].timestamp);
700                     setWidthForDuration(fullscreenElement, fullscreenDOMEvents[i].timestamp, fullscreenDOMEvents[i + 1].timestamp);
701
702                     let originator = fullscreenDOMEvents[i].originator || fullscreenDOMEvents[i + 1].originator;
703                     if (originator)
704                         fullscreenElement.title = WI.UIString("Full-Screen from \u201C%s\u201D").format(originator.displayName);
705                     else
706                         fullscreenElement.title = WI.UIString("Full-Screen");
707                 }
708             }
709
710             for (let lowPowerRange of domNode.lowPowerRanges) {
711                 let startTimestamp = lowPowerRange.startTimestamp || graphStartTime;
712                 let endTimestamp = lowPowerRange.endTimestamp || this._waterfallEndTime;
713
714                 let lowPowerElement = container.appendChild(document.createElement("div"));
715                 lowPowerElement.classList.add("area", "low-power");
716                 lowPowerElement.title = WI.UIString("Low-Power Mode");
717                 positionByStartOffset(lowPowerElement, startTimestamp);
718                 setWidthForDuration(lowPowerElement, startTimestamp, endTimestamp);
719             }
720
721             let playing = false;
722
723             function createDOMEventLine(domEvents, startTimestamp, endTimestamp) {
724                 if (domEvents.lastValue.eventName === "ended")
725                     return;
726
727                 for (let i = domEvents.length - 1; i >= 0; --i) {
728                     let domEvent = domEvents[i];
729                     if (domEvent.eventName === "play" || domEvent.eventName === "playing") {
730                         playing = true;
731                         break;
732                     }
733
734                     if (domEvent.eventName === "pause" || domEvent.eventName === "stall") {
735                         playing = false;
736                         break;
737                     }
738                 }
739
740                 let lineElement = container.appendChild(document.createElement("div"));
741                 lineElement.classList.add("dom-activity");
742                 lineElement.classList.toggle("playing", playing);
743                 positionByStartOffset(lineElement, startTimestamp);
744                 setWidthForDuration(lineElement, startTimestamp, endTimestamp);
745             }
746
747             for (let [a, b] of groupedDOMEvents.adjacencies())
748                 createDOMEventLine(a.domEvents, a.endTimestamp, b.startTimestamp);
749
750             if (groupedDOMEvents.length)
751                 createDOMEventLine(groupedDOMEvents.lastValue.domEvents, groupedDOMEvents.lastValue.endTimestamp, this._waterfallEndTime);
752
753             for (let {startTimestamp, endTimestamp, domEvents} of groupedDOMEvents) {
754                 let paddingForCentering = domEventElementSize * secondsPerPixel / 2;
755
756                 let eventElement = container.appendChild(document.createElement("div"));
757                 eventElement.classList.add("dom-event");
758                 positionByStartOffset(eventElement, startTimestamp - paddingForCentering);
759                 setWidthForDuration(eventElement, startTimestamp, endTimestamp + paddingForCentering);
760                 eventElement.addEventListener("mousedown", (event) => {
761                     if (event.button !== 0 || event.ctrlKey)
762                         return;
763                     this._handleNodeEntryMousedownWaterfall(entry, domEvents);
764                 });
765
766                 for (let domEvent of domEvents)
767                     entry.domEventElements.set(domEvent, eventElement);
768             }
769
770             return;
771         }
772
773         let resource = entry.resource;
774         if (!resource.hasResponse()) {
775             cell.textContent = zeroWidthSpace;
776             return;
777         }
778
779         let {startTime, redirectStart, redirectEnd, fetchStart, domainLookupStart, domainLookupEnd, connectStart, connectEnd, secureConnectionStart, requestStart, responseStart, responseEnd} = resource.timingData;
780         if (isNaN(startTime) || isNaN(responseEnd) || startTime > responseEnd) {
781             cell.textContent = zeroWidthSpace;
782             return;
783         }
784
785         if (responseEnd < graphStartTime) {
786             cell.textContent = zeroWidthSpace;
787             return;
788         }
789
790         let graphEndTime = this._waterfallTimelineRuler.endTime;
791         if (startTime > graphEndTime) {
792             cell.textContent = zeroWidthSpace;
793             return;
794         }
795
796         let lastEndTimestamp = NaN;
797         function appendBlock(startTimestamp, endTimestamp, className) {
798             if (isNaN(startTimestamp) || isNaN(endTimestamp))
799                 return null;
800
801             if (Math.abs(startTimestamp - lastEndTimestamp) < secondsPerPixel * 2)
802                 startTimestamp = lastEndTimestamp;
803             lastEndTimestamp = endTimestamp;
804
805             let block = container.appendChild(document.createElement("div"));
806             block.classList.add("block", className);
807             positionByStartOffset(block, startTimestamp);
808             setWidthForDuration(block, startTimestamp, endTimestamp);
809             return block;
810         }
811
812         // Mouse block sits on top and accepts mouse events on this group.
813         let padSeconds = 10 * secondsPerPixel;
814         let mouseBlock = appendBlock(startTime - padSeconds, responseEnd + padSeconds, "mouse-tracking");
815         mouseBlock.addEventListener("mousedown", (event) => {
816             if (event.button !== 0 || event.ctrlKey)
817                 return;
818             this._handleResourceEntryMousedownWaterfall(entry);
819         });
820
821         // Super small visualization.
822         let totalWidth = (responseEnd - startTime) / secondsPerPixel;
823         if (totalWidth <= 3) {
824             let twoPixels = secondsPerPixel * 2;
825             appendBlock(startTime, startTime + twoPixels, "queue");
826             appendBlock(startTime + twoPixels, startTime + (2 * twoPixels), "response");
827             return;
828         }
829
830         appendBlock(startTime, responseEnd, "filler");
831
832         // FIXME: <https://webkit.org/b/190214> Web Inspector: expose full load metrics for redirect requests
833         appendBlock(redirectStart, redirectEnd, "redirect");
834
835         if (domainLookupStart) {
836             appendBlock(fetchStart, domainLookupStart, "queue");
837             appendBlock(domainLookupStart, domainLookupEnd || connectStart || requestStart, "dns");
838         } else if (connectStart)
839             appendBlock(fetchStart, connectStart, "queue");
840         else if (requestStart)
841             appendBlock(fetchStart, requestStart, "queue");
842         if (connectStart)
843             appendBlock(connectStart, secureConnectionStart || connectEnd, "connect");
844         if (secureConnectionStart)
845             appendBlock(secureConnectionStart, connectEnd, "secure");
846         appendBlock(requestStart, responseStart, "request");
847         appendBlock(responseStart, responseEnd, "response");
848     }
849
850     _generateSortComparator()
851     {
852         let sortColumnIdentifier = this._table.sortColumnIdentifier;
853         if (!sortColumnIdentifier) {
854             this._entriesSortComparator = null;
855             return;
856         }
857
858         let comparator;
859
860         switch (sortColumnIdentifier) {
861         case "name":
862         case "domain":
863         case "mimeType":
864         case "method":
865         case "scheme":
866         case "protocol":
867         case "initiator":
868         case "remoteAddress":
869             // Simple string.
870             comparator = (a, b) => (a[sortColumnIdentifier] || "").extendedLocaleCompare(b[sortColumnIdentifier] || "");
871             break;
872
873         case "status":
874         case "connectionIdentifier":
875         case "resourceSize":
876         case "time":
877             // Simple number.
878             comparator = (a, b) => {
879                 let aValue = a[sortColumnIdentifier];
880                 if (isNaN(aValue))
881                     return 1;
882                 let bValue = b[sortColumnIdentifier];
883                 if (isNaN(bValue))
884                     return -1;
885                 return aValue - bValue;
886             };
887             break;
888
889         case "priority":
890             // Resource.NetworkPriority enum.
891             comparator = (a, b) => WI.Resource.comparePriority(a.priority, b.priority);
892             break;
893
894         case "type":
895             // Sort by displayType string.
896             comparator = (a, b) => (a.displayType || "").extendedLocaleCompare(b.displayType || "");
897             break;
898
899         case "transferSize":
900             // Handle (memory) and (disk) values.
901             comparator = (a, b) => {
902                 let transferSizeA = a.transferSize;
903                 let transferSizeB = b.transferSize;
904
905                 // Treat NaN as the largest value.
906                 if (isNaN(transferSizeA))
907                     return 1;
908                 if (isNaN(transferSizeB))
909                     return -1;
910
911                 // Treat memory cache and disk cache as small values.
912                 let sourceA = a.resource.responseSource;
913                 if (sourceA === WI.Resource.ResponseSource.MemoryCache)
914                     transferSizeA = -20;
915                 else if (sourceA === WI.Resource.ResponseSource.DiskCache)
916                     transferSizeA = -10;
917                 else if (sourceA === WI.Resource.ResponseSource.ServiceWorker)
918                     transferSizeA = -5;
919
920                 let sourceB = b.resource.responseSource;
921                 if (sourceB === WI.Resource.ResponseSource.MemoryCache)
922                     transferSizeB = -20;
923                 else if (sourceB === WI.Resource.ResponseSource.DiskCache)
924                     transferSizeB = -10;
925                 else if (sourceB === WI.Resource.ResponseSource.ServiceWorker)
926                     transferSizeB = -5;
927
928                 return transferSizeA - transferSizeB;
929             };
930             break;
931
932         case "waterfall":
933             // Sort by startTime number.
934             comparator = (a, b) => a.startTime - b.startTime;
935             break;
936
937         default:
938             console.assert("Unexpected sort column", sortColumnIdentifier);
939             return;
940         }
941
942         let reverseFactor = this._table.sortOrder === WI.Table.SortOrder.Ascending ? 1 : -1;
943
944         // If the entry has an `initiatorNode`, use that node's "first" resource as the value of
945         // `entry`, so long as the entry being compared to doesn't have the same `initiatorNode`.
946         // This will ensure that all resource entries for a given `initiatorNode` will appear right
947         // next to each other, as they will all effectively be sorted by the first resource.
948         let substitute = (entry, other) => {
949             if (WI.settings.groupByDOMNode.value && entry.resource.initiatorNode) {
950                 let nodeEntry = this._domNodeEntries.get(entry.resource.initiatorNode);
951                 if (!nodeEntry.initiatedResourceEntries.includes(other))
952                     return nodeEntry.initiatedResourceEntries[0];
953             }
954             return entry;
955         };
956
957         this._entriesSortComparator = (a, b) => reverseFactor * comparator(substitute(a, b), substitute(b, a));
958     }
959
960     // Protected
961
962     initialLayout()
963     {
964         this._waterfallTimelineRuler = new WI.TimelineRuler;
965         this._waterfallTimelineRuler.allowsClippedLabels = true;
966
967         this._nameColumn = new WI.TableColumn("name", WI.UIString("Name"), {
968             minWidth: WI.Sidebar.AbsoluteMinimumWidth,
969             maxWidth: 500,
970             initialWidth: this._nameColumnWidthSetting.value,
971             resizeType: WI.TableColumn.ResizeType.Locked,
972         });
973
974         this._domainColumn = new WI.TableColumn("domain", WI.UIString("Domain"), {
975             minWidth: 120,
976             maxWidth: 200,
977             initialWidth: 150,
978         });
979
980         this._typeColumn = new WI.TableColumn("type", WI.UIString("Type"), {
981             minWidth: 70,
982             maxWidth: 120,
983             initialWidth: 90,
984         });
985
986         this._mimeTypeColumn = new WI.TableColumn("mimeType", WI.UIString("MIME Type"), {
987             hidden: true,
988             minWidth: 100,
989             maxWidth: 150,
990             initialWidth: 120,
991         });
992
993         this._methodColumn = new WI.TableColumn("method", WI.UIString("Method"), {
994             hidden: true,
995             minWidth: 55,
996             maxWidth: 80,
997             initialWidth: 65,
998         });
999
1000         this._schemeColumn = new WI.TableColumn("scheme", WI.UIString("Scheme"), {
1001             hidden: true,
1002             minWidth: 55,
1003             maxWidth: 80,
1004             initialWidth: 65,
1005         });
1006
1007         this._statusColumn = new WI.TableColumn("status", WI.UIString("Status"), {
1008             hidden: true,
1009             minWidth: 50,
1010             maxWidth: 50,
1011             align: "left",
1012         });
1013
1014         this._protocolColumn = new WI.TableColumn("protocol", WI.UIString("Protocol"), {
1015             hidden: true,
1016             minWidth: 65,
1017             maxWidth: 80,
1018             initialWidth: 75,
1019         });
1020
1021         this._initiatorColumn = new WI.TableColumn("initiator", WI.UIString("Initiator"), {
1022             hidden: true,
1023             minWidth: 75,
1024             maxWidth: 175,
1025             initialWidth: 125,
1026         });
1027
1028         this._priorityColumn = new WI.TableColumn("priority", WI.UIString("Priority"), {
1029             hidden: true,
1030             minWidth: 65,
1031             maxWidth: 80,
1032             initialWidth: 70,
1033         });
1034
1035         this._remoteAddressColumn = new WI.TableColumn("remoteAddress", WI.UIString("IP Address"), {
1036             hidden: true,
1037             minWidth: 150,
1038         });
1039
1040         this._connectionIdentifierColumn = new WI.TableColumn("connectionIdentifier", WI.UIString("Connection ID"), {
1041             hidden: true,
1042             minWidth: 50,
1043             maxWidth: 120,
1044             initialWidth: 80,
1045             align: "right",
1046         });
1047
1048         this._resourceSizeColumn = new WI.TableColumn("resourceSize", WI.UIString("Resource Size"), {
1049             hidden: true,
1050             minWidth: 80,
1051             maxWidth: 100,
1052             initialWidth: 80,
1053             align: "right",
1054         });
1055
1056         this._transferSizeColumn = new WI.TableColumn("transferSize", WI.UIString("Transfer Size"), {
1057             minWidth: 100,
1058             maxWidth: 150,
1059             initialWidth: 100,
1060             align: "right",
1061         });
1062
1063         this._timeColumn = new WI.TableColumn("time", WI.UIString("Time"), {
1064             minWidth: 65,
1065             maxWidth: 90,
1066             initialWidth: 65,
1067             align: "right",
1068         });
1069
1070         this._waterfallColumn = new WI.TableColumn("waterfall", WI.UIString("Waterfall"), {
1071             minWidth: 230,
1072             headerView: this._waterfallTimelineRuler,
1073         });
1074
1075         this._nameColumn.addEventListener(WI.TableColumn.Event.WidthDidChange, this._tableNameColumnDidChangeWidth, this);
1076         this._waterfallColumn.addEventListener(WI.TableColumn.Event.WidthDidChange, this._tableWaterfallColumnDidChangeWidth, this);
1077
1078         this._table = new WI.Table("network-table", this, this, 20);
1079
1080         this._table.addColumn(this._nameColumn);
1081         this._table.addColumn(this._domainColumn);
1082         this._table.addColumn(this._typeColumn);
1083         this._table.addColumn(this._mimeTypeColumn);
1084         this._table.addColumn(this._methodColumn);
1085         this._table.addColumn(this._schemeColumn);
1086         this._table.addColumn(this._statusColumn);
1087         this._table.addColumn(this._protocolColumn);
1088         this._table.addColumn(this._initiatorColumn);
1089         this._table.addColumn(this._priorityColumn);
1090         this._table.addColumn(this._remoteAddressColumn);
1091         this._table.addColumn(this._connectionIdentifierColumn);
1092         this._table.addColumn(this._resourceSizeColumn);
1093         this._table.addColumn(this._transferSizeColumn);
1094         this._table.addColumn(this._timeColumn);
1095         this._table.addColumn(this._waterfallColumn);
1096
1097         if (!this._table.sortColumnIdentifier) {
1098             this._table.sortOrder = WI.Table.SortOrder.Ascending;
1099             this._table.sortColumnIdentifier = "waterfall";
1100         }
1101
1102         this.addSubview(this._table);
1103     }
1104
1105     layout()
1106     {
1107         this._updateWaterfallTimelineRuler();
1108         this._processPendingEntries();
1109         this._positionDetailView();
1110         this._positionEmptyFilterMessage();
1111         this._updateExportButton();
1112     }
1113
1114     didLayoutSubtree()
1115     {
1116         super.didLayoutSubtree();
1117
1118         if (this._waterfallPopover)
1119             this._waterfallPopover.resize();
1120     }
1121
1122     handleClearShortcut(event)
1123     {
1124         this.reset();
1125     }
1126
1127     // Private
1128
1129     _updateWaterfallTimeRange(startTimestamp, endTimestamp)
1130     {
1131         if (isNaN(this._waterfallStartTime) || startTimestamp < this._waterfallStartTime)
1132             this._waterfallStartTime = startTimestamp;
1133
1134         if (isNaN(this._waterfallEndTime) || endTimestamp > this._waterfallEndTime)
1135             this._waterfallEndTime = endTimestamp;
1136     }
1137
1138     _updateWaterfallTimelineRuler()
1139     {
1140         if (!this._waterfallTimelineRuler)
1141             return;
1142
1143         if (isNaN(this._waterfallStartTime)) {
1144             this._waterfallTimelineRuler.zeroTime = 0;
1145             this._waterfallTimelineRuler.startTime = 0;
1146             this._waterfallTimelineRuler.endTime = 0.250;
1147         } else {
1148             this._waterfallTimelineRuler.zeroTime = this._waterfallStartTime;
1149             this._waterfallTimelineRuler.startTime = this._waterfallStartTime;
1150             this._waterfallTimelineRuler.endTime = this._waterfallEndTime;
1151
1152             // Add a little bit of padding on the each side.
1153             const paddingPixels = 5;
1154             let padSeconds = paddingPixels * this._waterfallTimelineRuler.secondsPerPixel;
1155             this._waterfallTimelineRuler.zeroTime = this._waterfallStartTime - padSeconds;
1156             this._waterfallTimelineRuler.startTime = this._waterfallStartTime - padSeconds;
1157             this._waterfallTimelineRuler.endTime = this._waterfallEndTime + padSeconds;
1158         }
1159     }
1160
1161     _canExportHAR()
1162     {
1163         let mainFrame = WI.networkManager.mainFrame;
1164         if (!mainFrame)
1165             return false;
1166
1167         let mainResource = mainFrame.mainResource;
1168         if (!mainResource)
1169             return false;
1170
1171         if (!mainResource.requestSentDate)
1172             return false;
1173
1174         if (!this._HARResources().length)
1175             return false;
1176
1177         return true;
1178     }
1179
1180     _updateExportButton()
1181     {
1182         this._harExportNavigationItem.enabled = this._canExportHAR();
1183     }
1184
1185     _processPendingEntries()
1186     {
1187         let needsSort = this._pendingUpdates.length > 0;
1188         let needsFilter = this._pendingFilter;
1189
1190         // No global sort or filter is needed, so just insert new records into their sorted position.
1191         if (!needsSort && !needsFilter) {
1192             let originalLength = this._pendingInsertions.length;
1193             for (let resource of this._pendingInsertions)
1194                 this._insertResourceAndReloadTable(resource);
1195             console.assert(this._pendingInsertions.length === originalLength);
1196             this._pendingInsertions = [];
1197             return;
1198         }
1199
1200         for (let resource of this._pendingInsertions) {
1201             let resourceEntry = this._entryForResource(resource);
1202             this._tryLinkResourceToDOMNode(resourceEntry);
1203             this._entries.push(resourceEntry);
1204         }
1205         this._pendingInsertions = [];
1206
1207         for (let updateObject of this._pendingUpdates) {
1208             if (updateObject instanceof WI.Resource)
1209                 this._updateEntryForResource(updateObject);
1210         }
1211         this._pendingUpdates = [];
1212
1213         this._pendingFilter = false;
1214
1215         this._updateSort();
1216         this._updateFilteredEntries();
1217         this._reloadTable();
1218     }
1219
1220     _populateWithInitialResourcesIfNeeded()
1221     {
1222         if (!this._needsInitialPopulate)
1223             return;
1224
1225         this._needsInitialPopulate = false;
1226
1227         let populateResourcesForFrame = (frame) => {
1228             if (frame.provisionalMainResource)
1229                 this._pendingInsertions.push(frame.provisionalMainResource);
1230             else if (frame.mainResource)
1231                 this._pendingInsertions.push(frame.mainResource);
1232
1233             for (let resource of frame.resourceCollection)
1234                 this._pendingInsertions.push(resource);
1235
1236             for (let childFrame of frame.childFrameCollection)
1237                 populateResourcesForFrame(childFrame);
1238         };
1239
1240         let populateResourcesForTarget = (target) => {
1241             if (target.mainResource instanceof WI.Resource)
1242                 this._pendingInsertions.push(target.mainResource);
1243             for (let resource of target.resourceCollection)
1244                 this._pendingInsertions.push(resource);
1245         };
1246
1247         for (let target of WI.targets) {
1248             if (target === WI.pageTarget)
1249                 populateResourcesForFrame(WI.networkManager.mainFrame);
1250             else
1251                 populateResourcesForTarget(target);
1252         }
1253
1254         this.needsLayout();
1255     }
1256
1257     _checkURLFilterAgainstResource(resource)
1258     {
1259         if (this._urlFilterSearchRegex.test(resource.url)) {
1260             this._activeURLFilterResources.add(resource);
1261             return;
1262         }
1263
1264         for (let redirect of resource.redirects) {
1265             if (this._urlFilterSearchRegex.test(redirect.url)) {
1266                 this._activeURLFilterResources.add(resource);
1267                 return;
1268             }
1269         }
1270     }
1271
1272     _rowIndexForRepresentedObject(object)
1273     {
1274         return this._filteredEntries.findIndex((x) => {
1275             if (x.resource === object)
1276                 return true;
1277             if (x.domNode === object)
1278                 return true;
1279             return false;
1280         });
1281     }
1282
1283     _updateEntryForResource(resource)
1284     {
1285         let index = this._entries.findIndex((x) => x.resource === resource);
1286         if (index === -1)
1287             return;
1288
1289         // Don't wipe out the previous entry, as it may be used by a node entry.
1290         function updateExistingEntry(existingEntry, newEntry) {
1291             for (let key in newEntry)
1292                 existingEntry[key] = newEntry[key];
1293         }
1294
1295         let entry = this._entryForResource(resource);
1296         updateExistingEntry(this._entries[index], entry);
1297
1298         let rowIndex = this._rowIndexForRepresentedObject(resource);
1299         if (rowIndex === -1)
1300             return;
1301
1302         updateExistingEntry(this._filteredEntries[rowIndex], entry);
1303     }
1304
1305     _hidePopover()
1306     {
1307         if (this._waterfallPopover)
1308             this._waterfallPopover.dismiss();
1309     }
1310
1311     _hideDetailView()
1312     {
1313         if (!this._detailView)
1314             return;
1315
1316         this.element.classList.remove("showing-detail");
1317         this._table.scrollContainer.style.removeProperty("width");
1318
1319         this.removeSubview(this._detailView);
1320
1321         this._detailView.hidden();
1322         this._detailView = null;
1323
1324         this._table.updateLayout(WI.View.LayoutReason.Resize);
1325         this._table.reloadVisibleColumnCells(this._waterfallColumn);
1326     }
1327
1328     _showDetailView(object)
1329     {
1330         let oldDetailView = this._detailView;
1331
1332         this._detailView = this._detailViewMap.get(object);
1333         if (this._detailView === oldDetailView)
1334             return;
1335
1336         if (!this._detailView) {
1337             if (object instanceof WI.Resource)
1338                 this._detailView = new WI.NetworkResourceDetailView(object, this);
1339             else if (object instanceof WI.DOMNode) {
1340                 this._detailView = new WI.NetworkDOMNodeDetailView(object, this);
1341             }
1342
1343             this._detailViewMap.set(object, this._detailView);
1344         }
1345
1346         if (oldDetailView) {
1347             oldDetailView.hidden();
1348             this.replaceSubview(oldDetailView, this._detailView);
1349         } else
1350             this.addSubview(this._detailView);
1351
1352         if (this._showingRepresentedObjectCookie)
1353             this._detailView.willShowWithCookie(this._showingRepresentedObjectCookie);
1354
1355         this._detailView.shown();
1356
1357         this.element.classList.add("showing-detail");
1358         this._table.scrollContainer.style.width = this._nameColumn.width + "px";
1359
1360         // FIXME: It would be nice to avoid this.
1361         // Currently the ResourceDetailView is in the heirarchy but has not yet done a layout so we
1362         // end up seeing the table behind it. This forces us to layout now instead of after a beat.
1363         this.updateLayout();
1364     }
1365
1366     _positionDetailView()
1367     {
1368         if (!this._detailView)
1369             return;
1370
1371         let side = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL ? "right" : "left";
1372         this._detailView.element.style[side] = this._nameColumn.width + "px";
1373         this._table.scrollContainer.style.width = this._nameColumn.width + "px";
1374     }
1375
1376     _updateURLFilterActiveIndicator()
1377     {
1378         this._urlFilterNavigationItem.filterBar.indicatingActive = this._hasURLFilter();
1379     }
1380
1381     _updateEmptyFilterResultsMessage()
1382     {
1383         if (this._hasActiveFilter() && !this._filteredEntries.length)
1384             this._showEmptyFilterResultsMessage();
1385         else
1386             this._hideEmptyFilterResultsMessage();
1387     }
1388
1389     _showEmptyFilterResultsMessage()
1390     {
1391         if (!this._emptyFilterResultsMessageElement) {
1392             let buttonElement = document.createElement("button");
1393             buttonElement.textContent = WI.UIString("Clear Filters");
1394             buttonElement.addEventListener("click", () => { this._resetFilters(); });
1395
1396             this._emptyFilterResultsMessageElement = WI.createMessageTextView(WI.UIString("No Filter Results"));
1397             this._emptyFilterResultsMessageElement.appendChild(buttonElement);
1398         }
1399
1400         this.element.appendChild(this._emptyFilterResultsMessageElement);
1401         this._positionEmptyFilterMessage();
1402     }
1403
1404     _hideEmptyFilterResultsMessage()
1405     {
1406         if (!this._emptyFilterResultsMessageElement)
1407             return;
1408
1409         this._emptyFilterResultsMessageElement.remove();
1410     }
1411
1412     _positionEmptyFilterMessage()
1413     {
1414         if (!this._emptyFilterResultsMessageElement)
1415             return;
1416
1417         let width = this._nameColumn.width - 1; // For the 1px border.
1418         this._emptyFilterResultsMessageElement.style.width = width + "px";
1419     }
1420
1421     _clearNetworkOnNavigateSettingChanged()
1422     {
1423         this._clearOnLoadNavigationItem.checked = !WI.settings.clearNetworkOnNavigate.value;
1424     }
1425
1426     _resourceCachingDisabledSettingChanged()
1427     {
1428         this._disableResourceCacheNavigationItem.activated = WI.settings.resourceCachingDisabled.value;
1429     }
1430
1431     _toggleDisableResourceCache()
1432     {
1433         WI.settings.resourceCachingDisabled.value = !WI.settings.resourceCachingDisabled.value;
1434     }
1435
1436     _mainResourceDidChange(event)
1437     {
1438         let frame = event.target;
1439         if (!frame.isMainFrame() || !WI.settings.clearNetworkOnNavigate.value)
1440             return;
1441
1442         this.reset();
1443
1444         if (this._transitioningPageTarget) {
1445             this._transitioningPageTarget = false;
1446             this._needsInitialPopulate = true;
1447             this._populateWithInitialResourcesIfNeeded();
1448             return;
1449         }
1450
1451         this._insertResourceAndReloadTable(frame.mainResource);
1452     }
1453
1454     _mainFrameDidChange()
1455     {
1456         this._populateWithInitialResourcesIfNeeded();
1457     }
1458
1459     _resourceLoadingDidFinish(event)
1460     {
1461         let resource = event.target;
1462         this._pendingUpdates.push(resource);
1463
1464         this._updateWaterfallTimeRange(resource.firstTimestamp, resource.timingData.responseEnd);
1465
1466         if (this._hasURLFilter())
1467             this._checkURLFilterAgainstResource(resource);
1468
1469         this.needsLayout();
1470     }
1471
1472     _resourceLoadingDidFail(event)
1473     {
1474         let resource = event.target;
1475         this._pendingUpdates.push(resource);
1476
1477         this._updateWaterfallTimeRange(resource.firstTimestamp, resource.timingData.responseEnd);
1478
1479         if (this._hasURLFilter())
1480             this._checkURLFilterAgainstResource(resource);
1481
1482         this.needsLayout();
1483     }
1484
1485     _resourceTransferSizeDidChange(event)
1486     {
1487         if (!this._table)
1488             return;
1489
1490         let resource = event.target;
1491
1492         // In the unlikely event that this is the sort column, we may need to resort.
1493         if (this._table.sortColumnIdentifier === "transferSize") {
1494             this._pendingUpdates.push(resource);
1495             this.needsLayout();
1496             return;
1497         }
1498
1499         let index = this._entries.findIndex((x) => x.resource === resource);
1500         if (index === -1)
1501             return;
1502
1503         let entry = this._entries[index];
1504         entry.transferSize = !isNaN(resource.networkTotalTransferSize) ? resource.networkTotalTransferSize : resource.estimatedTotalTransferSize;
1505
1506         let rowIndex = this._rowIndexForRepresentedObject(resource);
1507         if (rowIndex === -1)
1508             return;
1509
1510         this._table.reloadCell(rowIndex, "transferSize");
1511     }
1512
1513     _handleResourceAdded(event)
1514     {
1515         this._insertResourceAndReloadTable(event.data.resource);
1516     }
1517
1518     _isDefaultSort()
1519     {
1520         return this._table.sortColumnIdentifier === "waterfall" && this._table.sortOrder === WI.Table.SortOrder.Ascending;
1521     }
1522
1523     _insertResourceAndReloadTable(resource)
1524     {
1525         this._updateWaterfallTimeRange(resource.firstTimestamp, resource.timingData.responseEnd);
1526
1527         if (!this._table || !(WI.tabBrowser.selectedTabContentView instanceof WI.NetworkTabContentView)) {
1528             this._pendingInsertions.push(resource);
1529             this.needsLayout();
1530             return;
1531         }
1532
1533         let resourceEntry = this._entryForResource(resource);
1534
1535         this._tryLinkResourceToDOMNode(resourceEntry);
1536
1537         if (WI.settings.groupByDOMNode.value && resource.initiatorNode) {
1538             if (!this._entriesSortComparator)
1539                 this._generateSortComparator();
1540         } else if (this._isDefaultSort() || !this._entriesSortComparator) {
1541             // Default sort has fast path.
1542             this._entries.push(resourceEntry);
1543             if (this._passFilter(resourceEntry)) {
1544                 this._filteredEntries.push(resourceEntry);
1545                 this._table.reloadDataAddedToEndOnly();
1546             }
1547             return;
1548         }
1549
1550         insertObjectIntoSortedArray(resourceEntry, this._entries, this._entriesSortComparator);
1551
1552         if (this._passFilter(resourceEntry)) {
1553             if (WI.settings.groupByDOMNode.value)
1554                 this._updateFilteredEntries();
1555             else
1556                 insertObjectIntoSortedArray(resourceEntry, this._filteredEntries, this._entriesSortComparator);
1557
1558             // Probably a useless optimization here, but if we only added this row to the end
1559             // we may avoid recreating all visible rows by saying as such.
1560             if (this._filteredEntries.lastValue === resourceEntry)
1561                 this._table.reloadDataAddedToEndOnly();
1562             else
1563                 this._reloadTable();
1564         }
1565     }
1566
1567     _entryForResource(resource)
1568     {
1569         // FIXME: <https://webkit.org/b/143632> Web Inspector: Resources with the same name in different folders aren't distinguished
1570         // FIXME: <https://webkit.org/b/176765> Web Inspector: Resource names should be less ambiguous
1571
1572         return {
1573             resource,
1574             name: WI.displayNameForURL(resource.url, resource.urlComponents),
1575             domain: WI.displayNameForHost(resource.urlComponents.host),
1576             scheme: resource.urlComponents.scheme ? resource.urlComponents.scheme.toLowerCase() : "",
1577             method: resource.requestMethod,
1578             type: resource.type,
1579             displayType: WI.NetworkTableContentView.displayNameForResource(resource),
1580             mimeType: resource.mimeType,
1581             status: resource.statusCode,
1582             cached: resource.cached,
1583             resourceSize: resource.size,
1584             transferSize: !isNaN(resource.networkTotalTransferSize) ? resource.networkTotalTransferSize : resource.estimatedTotalTransferSize,
1585             time: resource.totalDuration,
1586             protocol: resource.protocol,
1587             initiator: resource.initiatorSourceCodeLocation ? resource.initiatorSourceCodeLocation.displayLocationString() : "",
1588             priority: resource.priority,
1589             remoteAddress: resource.remoteAddress,
1590             connectionIdentifier: resource.connectionIdentifier,
1591             startTime: resource.firstTimestamp,
1592         };
1593     }
1594
1595     _entryForDOMNode(domNode)
1596     {
1597         return {
1598             domNode,
1599             initiatedResourceEntries: [],
1600             domEventElements: new Map,
1601             expanded: true,
1602         };
1603     }
1604
1605     _tryLinkResourceToDOMNode(resourceEntry)
1606     {
1607         let resource = resourceEntry.resource;
1608         if (!resource || !resource.initiatorNode)
1609             return;
1610
1611         let nodeEntry = this._domNodeEntries.get(resource.initiatorNode);
1612         if (!nodeEntry) {
1613             nodeEntry = this._entryForDOMNode(resource.initiatorNode, Object.keys(resourceEntry));
1614             this._domNodeEntries.set(resource.initiatorNode, nodeEntry);
1615
1616             resource.initiatorNode.addEventListener(WI.DOMNode.Event.DidFireEvent, this._handleNodeDidFireEvent, this);
1617             if (resource.initiatorNode.canEnterLowPowerMode())
1618                 resource.initiatorNode.addEventListener(WI.DOMNode.Event.LowPowerChanged, this._handleNodeLowPowerChanged, this);
1619         }
1620
1621         if (!this._entriesSortComparator)
1622             this._generateSortComparator();
1623
1624         insertObjectIntoSortedArray(resourceEntry, nodeEntry.initiatedResourceEntries, this._entriesSortComparator);
1625     }
1626
1627     _uniqueValuesForDOMNodeEntry(nodeEntry, accessor)
1628     {
1629         let resourceEntries = nodeEntry.initiatedResourceEntries;
1630         if (!resourceEntries)
1631             return null;
1632
1633         return resourceEntries.reduce((accumulator, current) => {
1634             let value = accessor(current);
1635             if (value || typeof value === "number")
1636                 accumulator.add(value);
1637             return accumulator;
1638         }, new Set);
1639     }
1640
1641     _handleNodeDidFireEvent(event)
1642     {
1643         let domNode = event.target;
1644         let {domEvent} = event.data;
1645
1646         this._pendingUpdates.push(domNode);
1647
1648         this._updateWaterfallTimeRange(NaN, domEvent.timestamp + (this._waterfallTimelineRuler.secondsPerPixel * 10));
1649
1650         this.needsLayout();
1651     }
1652
1653     _handleNodeLowPowerChanged(event)
1654     {
1655         let domNode = event.target;
1656         let {timestamp} = event.data;
1657
1658         this._pendingUpdates.push(domNode);
1659
1660         this._updateWaterfallTimeRange(NaN, timestamp + (this._waterfallTimelineRuler.secondsPerPixel * 10));
1661
1662         this.needsLayout();
1663     }
1664
1665     _hasTypeFilter()
1666     {
1667         return !!this._activeTypeFilters;
1668     }
1669
1670     _hasURLFilter()
1671     {
1672         return this._urlFilterIsActive;
1673     }
1674
1675     _hasActiveFilter()
1676     {
1677         return this._hasTypeFilter()
1678             || this._hasURLFilter();
1679     }
1680
1681     _passTypeFilter(entry)
1682     {
1683         if (!this._hasTypeFilter())
1684             return true;
1685         return this._activeTypeFilters.some((checker) => checker(entry.resource.type));
1686     }
1687
1688     _passURLFilter(entry)
1689     {
1690         if (!this._hasURLFilter())
1691             return true;
1692         return this._activeURLFilterResources.has(entry.resource);
1693     }
1694
1695     _passFilter(entry)
1696     {
1697         return this._passTypeFilter(entry)
1698             && this._passURLFilter(entry);
1699     }
1700
1701     _updateSort()
1702     {
1703         if (this._entriesSortComparator)
1704             this._entries = this._entries.sort(this._entriesSortComparator);
1705     }
1706
1707     _updateFilteredEntries()
1708     {
1709         if (this._hasActiveFilter())
1710             this._filteredEntries = this._entries.filter(this._passFilter, this);
1711         else
1712             this._filteredEntries = this._entries.slice();
1713
1714         if (WI.settings.groupByDOMNode.value) {
1715             for (let nodeEntry of this._domNodeEntries.values()) {
1716                 if (nodeEntry.initiatedResourceEntries.length < 2 && !nodeEntry.domNode.domEvents.length)
1717                     continue;
1718
1719                 let firstIndex = Infinity;
1720                 for (let resourceEntry of nodeEntry.initiatedResourceEntries) {
1721                     if (this._hasActiveFilter() && !this._passFilter(resourceEntry))
1722                         continue;
1723
1724                     let index = this._filteredEntries.indexOf(resourceEntry);
1725                     if (index >= 0 && index < firstIndex)
1726                         firstIndex = index;
1727                 }
1728
1729                 if (!isFinite(firstIndex))
1730                     continue;
1731
1732                 this._filteredEntries.insertAtIndex(nodeEntry, firstIndex);
1733             }
1734
1735             this._filteredEntries = this._filteredEntries.filter((entry) => {
1736                 if (entry.resource && entry.resource.initiatorNode) {
1737                     let nodeEntry = this._domNodeEntries.get(entry.resource.initiatorNode);
1738                     if (!nodeEntry.expanded)
1739                         return false;
1740                 }
1741                 return true;
1742             });
1743         }
1744
1745         this._updateURLFilterActiveIndicator();
1746         this._updateEmptyFilterResultsMessage();
1747     }
1748
1749     _reloadTable()
1750     {
1751         this._table.reloadData();
1752         this._restoreSelectedRow();
1753     }
1754
1755     _generateTypeFilter()
1756     {
1757         let selectedItems = this._typeFilterScopeBar.selectedItems;
1758         if (!selectedItems.length || selectedItems.includes(this._typeFilterScopeBarItemAll))
1759             return null;
1760
1761         return selectedItems.map((item) => item.__checker);
1762     }
1763
1764     _resetFilters()
1765     {
1766         console.assert(this._hasActiveFilter());
1767
1768         // Clear url filter.
1769         this._urlFilterSearchText = null;
1770         this._urlFilterSearchRegex = null;
1771         this._urlFilterIsActive = false;
1772         this._activeURLFilterResources.clear();
1773         this._urlFilterNavigationItem.filterBar.clear();
1774         console.assert(!this._hasURLFilter());
1775
1776         // Clear type filter.
1777         this._typeFilterScopeBar.resetToDefault();
1778         console.assert(!this._hasTypeFilter());
1779
1780         console.assert(!this._hasActiveFilter());
1781
1782         this._updateFilteredEntries();
1783         this._reloadTable();
1784     }
1785
1786     _areFilterListsIdentical(listA, listB)
1787     {
1788         if (listA && listB) {
1789             if (listA.length !== listB.length)
1790                 return false;
1791
1792             for (let i = 0; i < listA.length; ++i) {
1793                 if (listA[i] !== listB[i])
1794                     return false;
1795             }
1796
1797             return true;
1798         }
1799
1800         return false;
1801     }
1802
1803     _typeFilterScopeBarSelectionChanged(event)
1804     {
1805         // FIXME: <https://webkit.org/b/176763> Web Inspector: ScopeBar SelectionChanged event may dispatch multiple times for a single logical change
1806         // We can't use shallow equals here because the contents are functions.
1807         let oldFilter = this._activeTypeFilters;
1808         let newFilter = this._generateTypeFilter();
1809         if (this._areFilterListsIdentical(oldFilter, newFilter))
1810             return;
1811
1812         // Even if the selected resource would still be visible, lets close the detail view if a filter changes.
1813         this._hideDetailView();
1814
1815         this._activeTypeFilters = newFilter;
1816         this._updateFilteredEntries();
1817         this._reloadTable();
1818     }
1819
1820     _handleGroupByDOMNodeCheckedDidChange(event)
1821     {
1822         WI.settings.groupByDOMNode.value = this._groupByDOMNodeNavigationItem.checked;
1823
1824         if (!WI.settings.groupByDOMNode.value) {
1825             this._table.element.classList.remove("grouped");
1826
1827             if (this._selectedObject && this._selectedObject instanceof WI.DOMNode) {
1828                 this._selectedObject = null;
1829                 this._hideDetailView();
1830             }
1831         }
1832
1833         this._updateSort();
1834         this._updateFilteredEntries();
1835         this._reloadTable();
1836     }
1837
1838     _urlFilterDidChange(event)
1839     {
1840         let searchQuery = this._urlFilterNavigationItem.filterBar.filters.text;
1841         if (searchQuery === this._urlFilterSearchText)
1842             return;
1843
1844         // Even if the selected resource would still be visible, lets close the detail view if a filter changes.
1845         this._hideDetailView();
1846
1847         // Search cleared.
1848         if (!searchQuery) {
1849             this._urlFilterSearchText = null;
1850             this._urlFilterSearchRegex = null;
1851             this._urlFilterIsActive = false;
1852             this._activeURLFilterResources.clear();
1853
1854             this._updateFilteredEntries();
1855             this._reloadTable();
1856             return;
1857         }
1858
1859         this._urlFilterIsActive = true;
1860         this._urlFilterSearchText = searchQuery;
1861         this._urlFilterSearchRegex = new RegExp(searchQuery.escapeForRegExp(), "i");
1862
1863         this._activeURLFilterResources.clear();
1864
1865         for (let entry of this._entries)
1866             this._checkURLFilterAgainstResource(entry.resource);
1867
1868         this._updateFilteredEntries();
1869         this._reloadTable();
1870     }
1871
1872     _restoreSelectedRow()
1873     {
1874         if (!this._selectedObject)
1875             return;
1876
1877         let rowIndex = this._rowIndexForRepresentedObject(this._selectedObject);
1878         if (rowIndex === -1) {
1879             this._selectedObject = null;
1880             this._table.deselectAll();
1881             return;
1882         }
1883
1884         this._table.selectRow(rowIndex);
1885         this._showDetailView(this._selectedObject);
1886     }
1887
1888     _HARResources()
1889     {
1890         let resources = this._filteredEntries.map((x) => x.resource);
1891         const supportedHARSchemes = new Set(["http", "https", "ws", "wss"]);
1892         return resources.filter((resource) => {
1893             if (!resource.finished)
1894                 return false;
1895             if (!resource.requestSentDate)
1896                 return false;
1897             if (!supportedHARSchemes.has(resource.urlComponents.scheme))
1898                 return false;
1899             return true;
1900         });
1901     }
1902
1903     _exportHAR()
1904     {
1905         let resources = this._HARResources();
1906         if (!resources.length) {
1907             InspectorFrontendHost.beep();
1908             return;
1909         }
1910
1911         WI.HARBuilder.buildArchive(resources).then((har) => {
1912             let mainFrame = WI.networkManager.mainFrame;
1913             let archiveName = mainFrame.mainResource.urlComponents.host || mainFrame.mainResource.displayName || "Archive";
1914             let url = "web-inspector:///" + encodeURI(archiveName) + ".har";
1915             WI.FileUtilities.save({
1916                 url,
1917                 content: JSON.stringify(har, null, 2),
1918                 forceSaveAs: true,
1919             });
1920         }).catch(handlePromiseException);
1921     }
1922
1923     _waterfallPopoverContent()
1924     {
1925         let contentElement = document.createElement("div");
1926         contentElement.classList.add("waterfall-popover-content");
1927         return contentElement;
1928     }
1929
1930     _waterfallPopoverContentForResourceEntry(resourceEntry)
1931     {
1932         let contentElement = this._waterfallPopoverContent();
1933
1934         let resource = resourceEntry.resource;
1935         if (!resource.hasResponse() || !resource.firstTimestamp || !resource.lastTimestamp) {
1936             contentElement.textContent = WI.UIString("Resource has no timing data");
1937             return contentElement;
1938         }
1939
1940         let breakdownView = new WI.ResourceTimingBreakdownView(resource, 300);
1941         contentElement.appendChild(breakdownView.element);
1942         breakdownView.updateLayout();
1943
1944         return contentElement;
1945     }
1946
1947     _waterfallPopoverContentForNodeEntry(nodeEntry, domEvents)
1948     {
1949         let contentElement = this._waterfallPopoverContent();
1950
1951         let breakdownView = new WI.DOMEventsBreakdownView(domEvents);
1952         contentElement.appendChild(breakdownView.element);
1953         breakdownView.updateLayout();
1954
1955         return contentElement;
1956     }
1957
1958     _handleResourceEntryMousedownWaterfall(resourceEntry)
1959     {
1960         let popoverContentElement = this._waterfallPopoverContentForResourceEntry(resourceEntry);
1961         this._handleMousedownWaterfall(resourceEntry, popoverContentElement, (cell) => {
1962             return cell.querySelector(".block.mouse-tracking");
1963         });
1964     }
1965
1966     _handleNodeEntryMousedownWaterfall(nodeEntry, domEvents)
1967     {
1968         let popoverContentElement = this._waterfallPopoverContentForNodeEntry(nodeEntry, domEvents);
1969         this._handleMousedownWaterfall(nodeEntry, popoverContentElement, (cell) => {
1970             let domEventElement = nodeEntry.domEventElements.get(domEvents[0]);
1971
1972             // Show any additional DOM events that have been merged into the range.
1973             if (domEventElement && this._waterfallPopover.visible) {
1974                 let newDOMEvents = Array.from(nodeEntry.domEventElements)
1975                 .filter(([domEvent, element]) => element === domEventElement)
1976                 .map(([domEvent, element]) => domEvent);
1977
1978                 this._waterfallPopover.content = this._waterfallPopoverContentForNodeEntry(nodeEntry, newDOMEvents);
1979             }
1980
1981             return domEventElement;
1982         });
1983     }
1984
1985     _handleMousedownWaterfall(entry, popoverContentElement, updateTargetAndContentFunction)
1986     {
1987         if (!this._waterfallPopover) {
1988             this._waterfallPopover = new WI.Popover;
1989             this._waterfallPopover.element.classList.add("waterfall-popover");
1990         }
1991
1992         if (this._waterfallPopover.visible)
1993             return;
1994
1995         let calculateTargetFrame = () => {
1996             let rowIndex = this._rowIndexForRepresentedObject(entry.resource || entry.domNode);
1997             let cell = this._table.cellForRowAndColumn(rowIndex, this._waterfallColumn);
1998             if (cell) {
1999                 let targetElement = updateTargetAndContentFunction(cell);
2000                 if (targetElement)
2001                     return WI.Rect.rectFromClientRect(targetElement.getBoundingClientRect());
2002             }
2003
2004             this._waterfallPopover.dismiss();
2005             return null;
2006         };
2007
2008         let targetFrame = calculateTargetFrame();
2009         if (!targetFrame)
2010             return;
2011         if (!targetFrame.size.width && !targetFrame.size.height)
2012             return;
2013
2014         let isRTL = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL;
2015         let preferredEdges = isRTL ? [WI.RectEdge.MAX_Y, WI.RectEdge.MIN_Y, WI.RectEdge.MAX_X] : [WI.RectEdge.MAX_Y, WI.RectEdge.MIN_Y, WI.RectEdge.MIN_X];
2016         this._waterfallPopover.windowResizeHandler = () => {
2017             let bounds = calculateTargetFrame();
2018             if (bounds)
2019                 this._waterfallPopover.present(bounds, preferredEdges);
2020         };
2021
2022         this._waterfallPopover.presentNewContentWithFrame(popoverContentElement, targetFrame, preferredEdges);
2023     }
2024
2025     _tableNameColumnDidChangeWidth(event)
2026     {
2027         this._nameColumnWidthSetting.value = event.target.width;
2028
2029         this._positionDetailView();
2030         this._positionEmptyFilterMessage();
2031     }
2032
2033     _tableWaterfallColumnDidChangeWidth(event)
2034     {
2035         this._table.reloadVisibleColumnCells(this._waterfallColumn);
2036     }
2037
2038     _transitionPageTarget(event)
2039     {
2040         this._transitioningPageTarget = true;
2041     }
2042 };