Web Inspector: replace TypeVerifier with subclasses of WI.Collection
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / ResourceSidebarPanel.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 WI.ResourceSidebarPanel = class ResourceSidebarPanel extends WI.NavigationSidebarPanel
27 {
28     constructor(contentBrowser)
29     {
30         super("resource", WI.UIString("Resources"), true);
31
32         this.contentBrowser = contentBrowser;
33
34         this._navigationBar = new WI.NavigationBar;
35         this.addSubview(this._navigationBar);
36
37         this._targetTreeElementMap = new Map;
38
39         var scopeItemPrefix = "resource-sidebar-";
40         var scopeBarItems = [];
41
42         scopeBarItems.push(new WI.ScopeBarItem(scopeItemPrefix + "type-all", WI.UIString("All Resources"), true));
43
44         for (var key in WI.Resource.Type) {
45             var value = WI.Resource.Type[key];
46             var scopeBarItem = new WI.ScopeBarItem(scopeItemPrefix + value, WI.Resource.displayNameForType(value, true));
47             scopeBarItem[WI.ResourceSidebarPanel.ResourceTypeSymbol] = value;
48             scopeBarItems.push(scopeBarItem);
49         }
50
51         this._scopeBar = new WI.ScopeBar("resource-sidebar-scope-bar", scopeBarItems, scopeBarItems[0], true);
52         this._scopeBar.addEventListener(WI.ScopeBar.Event.SelectionChanged, this._scopeBarSelectionDidChange, this);
53
54         this._navigationBar.addNavigationItem(this._scopeBar);
55
56         WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
57
58         WI.frameResourceManager.addEventListener(WI.FrameResourceManager.Event.MainFrameDidChange, this._mainFrameDidChange, this);
59
60         WI.debuggerManager.addEventListener(WI.DebuggerManager.Event.ScriptAdded, this._scriptWasAdded, this);
61         WI.debuggerManager.addEventListener(WI.DebuggerManager.Event.ScriptRemoved, this._scriptWasRemoved, this);
62         WI.debuggerManager.addEventListener(WI.DebuggerManager.Event.ScriptsCleared, this._scriptsCleared, this);
63
64         WI.cssStyleManager.addEventListener(WI.CSSStyleManager.Event.StyleSheetAdded, this._styleSheetAdded, this);
65
66         WI.targetManager.addEventListener(WI.TargetManager.Event.TargetRemoved, this._targetRemoved, this);
67
68         WI.notifications.addEventListener(WI.Notification.ExtraDomainsActivated, this._extraDomainsActivated, this);
69
70         this.contentTreeOutline.addEventListener(WI.TreeOutline.Event.SelectionDidChange, this._treeSelectionDidChange, this);
71         this.contentTreeOutline.includeSourceMapResourceChildren = true;
72
73         if (WI.sharedApp.debuggableType === WI.DebuggableType.JavaScript) {
74             this.contentTreeOutline.disclosureButtons = false;
75             WI.SourceCode.addEventListener(WI.SourceCode.Event.SourceMapAdded, () => { this.contentTreeOutline.disclosureButtons = true; }, this);
76         }
77     }
78
79     // Public
80
81     get minimumWidth()
82     {
83         return this._navigationBar.minimumWidth;
84     }
85
86     closed()
87     {
88         super.closed();
89
90         WI.Frame.removeEventListener(null, null, this);
91         WI.frameResourceManager.removeEventListener(null, null, this);
92         WI.debuggerManager.removeEventListener(null, null, this);
93         WI.notifications.removeEventListener(null, null, this);
94     }
95
96     showDefaultContentView()
97     {
98         if (WI.frameResourceManager.mainFrame) {
99             this.contentBrowser.showContentViewForRepresentedObject(WI.frameResourceManager.mainFrame);
100             return;
101         }
102
103         var firstTreeElement = this.contentTreeOutline.children[0];
104         if (firstTreeElement)
105             this.showDefaultContentViewForTreeElement(firstTreeElement);
106     }
107
108     treeElementForRepresentedObject(representedObject)
109     {
110         // A custom implementation is needed for this since the frames are populated lazily.
111
112         if (!this._mainFrameTreeElement && (representedObject instanceof WI.Resource || representedObject instanceof WI.Frame || representedObject instanceof WI.Collection)) {
113             // All resources are under the main frame, so we need to return early if we don't have the main frame yet.
114             return null;
115         }
116
117         // The Frame is used as the representedObject instead of the main resource in our tree.
118         if (representedObject instanceof WI.Resource && representedObject.parentFrame && representedObject.parentFrame.mainResource === representedObject)
119             representedObject = representedObject.parentFrame;
120
121         function isAncestor(ancestor, resourceOrFrame)
122         {
123             // SourceMapResources are descendants of another SourceCode object.
124             if (resourceOrFrame instanceof WI.SourceMapResource) {
125                 if (resourceOrFrame.sourceMap.originalSourceCode === ancestor)
126                     return true;
127
128                 // Not a direct ancestor, so check the ancestors of the parent SourceCode object.
129                 resourceOrFrame = resourceOrFrame.sourceMap.originalSourceCode;
130             }
131
132             var currentFrame = resourceOrFrame.parentFrame;
133             while (currentFrame) {
134                 if (currentFrame === ancestor)
135                     return true;
136                 currentFrame = currentFrame.parentFrame;
137             }
138
139             return false;
140         }
141
142         function getParent(resourceOrFrame)
143         {
144             // SourceMapResources are descendants of another SourceCode object.
145             if (resourceOrFrame instanceof WI.SourceMapResource)
146                 return resourceOrFrame.sourceMap.originalSourceCode;
147             return resourceOrFrame.parentFrame;
148         }
149
150         var treeElement = this.contentTreeOutline.findTreeElement(representedObject, isAncestor, getParent);
151         if (treeElement)
152             return treeElement;
153
154         // Only special case Script objects.
155         if (!(representedObject instanceof WI.Script)) {
156             console.error("Didn't find a TreeElement for representedObject", representedObject);
157             return null;
158         }
159
160         // If the Script has a URL we should have found it earlier.
161         if (representedObject.url) {
162             console.error("Didn't find a ScriptTreeElement for a Script with a URL.");
163             return null;
164         }
165
166         // Since the Script does not have a URL we consider it an 'anonymous' script. These scripts happen from calls to
167         // window.eval() or browser features like Auto Fill and Reader. They are not normally added to the sidebar, but since
168         // we have a ScriptContentView asking for the tree element we will make a ScriptTreeElement on demand and add it.
169
170         if (!this._anonymousScriptsFolderTreeElement) {
171             let collection = new WI.ScriptCollection;
172             this._anonymousScriptsFolderTreeElement = new WI.FolderTreeElement(WI.UIString("Anonymous Scripts"), collection);
173         }
174
175         if (!this._anonymousScriptsFolderTreeElement.parent) {
176             var index = insertionIndexForObjectInListSortedByFunction(this._anonymousScriptsFolderTreeElement, this.contentTreeOutline.children, this._compareTreeElements);
177             this.contentTreeOutline.insertChild(this._anonymousScriptsFolderTreeElement, index);
178         }
179
180         this._anonymousScriptsFolderTreeElement.representedObject.add(representedObject);
181
182         var scriptTreeElement = new WI.ScriptTreeElement(representedObject);
183         this._anonymousScriptsFolderTreeElement.appendChild(scriptTreeElement);
184
185         return scriptTreeElement;
186     }
187
188     // Protected
189
190     initialLayout()
191     {
192         super.initialLayout();
193
194         if (WI.frameResourceManager.mainFrame)
195             this._mainFrameMainResourceDidChange(WI.frameResourceManager.mainFrame);
196
197         for (let script of WI.debuggerManager.knownNonResourceScripts) {
198             this._addScript(script);
199
200             if (script.sourceMaps.length && WI.sharedApp.debuggableType === WI.DebuggableType.JavaScript)
201                 this.contentTreeOutline.disclosureButtons = true;
202         }
203     }
204
205     hasCustomFilters()
206     {
207         console.assert(this._scopeBar.selectedItems.length === 1);
208         var selectedScopeBarItem = this._scopeBar.selectedItems[0];
209         return selectedScopeBarItem && !selectedScopeBarItem.exclusive;
210     }
211
212     matchTreeElementAgainstCustomFilters(treeElement, flags)
213     {
214         console.assert(this._scopeBar.selectedItems.length === 1);
215         var selectedScopeBarItem = this._scopeBar.selectedItems[0];
216
217         // Show everything if there is no selection or "All Resources" is selected (the exclusive item).
218         if (!selectedScopeBarItem || selectedScopeBarItem.exclusive)
219             return true;
220
221         // Folders are hidden on the first pass, but visible childen under the folder will force the folder visible again.
222         if (treeElement instanceof WI.FolderTreeElement)
223             return false;
224
225         function match()
226         {
227             if (treeElement instanceof WI.FrameTreeElement)
228                 return selectedScopeBarItem[WI.ResourceSidebarPanel.ResourceTypeSymbol] === WI.Resource.Type.Document;
229
230             if (treeElement instanceof WI.ScriptTreeElement)
231                 return selectedScopeBarItem[WI.ResourceSidebarPanel.ResourceTypeSymbol] === WI.Resource.Type.Script;
232
233             if (treeElement instanceof WI.CSSStyleSheetTreeElement)
234                 return selectedScopeBarItem[WI.ResourceSidebarPanel.ResourceTypeSymbol] === WI.Resource.Type.Stylesheet;
235
236             console.assert(treeElement instanceof WI.ResourceTreeElement, "Unknown treeElement", treeElement);
237             if (!(treeElement instanceof WI.ResourceTreeElement))
238                 return false;
239
240             return treeElement.resource.type === selectedScopeBarItem[WI.ResourceSidebarPanel.ResourceTypeSymbol];
241         }
242
243         var matched = match();
244         if (matched)
245             flags.expandTreeElement = true;
246         return matched;
247     }
248
249     // Private
250
251     _mainResourceDidChange(event)
252     {
253         if (!event.target.isMainFrame())
254             return;
255
256         this._mainFrameMainResourceDidChange(event.target);
257     }
258
259     _mainFrameDidChange(event)
260     {
261         this._mainFrameMainResourceDidChange(WI.frameResourceManager.mainFrame);
262     }
263
264     _mainFrameMainResourceDidChange(mainFrame)
265     {
266         this.contentBrowser.contentViewContainer.closeAllContentViews();
267
268         if (this._mainFrameTreeElement) {
269             this.contentTreeOutline.removeChild(this._mainFrameTreeElement);
270             this._mainFrameTreeElement = null;
271         }
272
273         if (!mainFrame)
274             return;
275
276         this._mainFrameTreeElement = new WI.FrameTreeElement(mainFrame);
277         this.contentTreeOutline.insertChild(this._mainFrameTreeElement, 0);
278
279         function delayedWork()
280         {
281             if (!this.contentTreeOutline.selectedTreeElement) {
282                 var currentContentView = this.contentBrowser.currentContentView;
283                 var treeElement = currentContentView ? this.treeElementForRepresentedObject(currentContentView.representedObject) : null;
284                 if (!treeElement)
285                     treeElement = this._mainFrameTreeElement;
286                 this.showDefaultContentViewForTreeElement(treeElement);
287             }
288         }
289
290         // Cookie restoration will attempt to re-select the resource we were showing.
291         // Give it time to do that before selecting the main frame resource.
292         setTimeout(delayedWork.bind(this));
293     }
294
295     _scriptWasAdded(event)
296     {
297         this._addScript(event.data.script);
298     }
299
300     _addScript(script)
301     {
302         // We don't add scripts without URLs here. Those scripts can quickly clutter the interface and
303         // are usually more transient. They will get added if/when they need to be shown in a content view.
304         if (!script.url && !script.sourceURL)
305             return;
306
307         // Worker script.
308         if (script.target !== WI.mainTarget) {
309             if (script.isMainResource())
310                 this._addTargetWithMainResource(script.target);
311             return;
312         }
313
314         // If the script URL matches a resource we can assume it is part of that resource and does not need added.
315         if (script.resource || script.dynamicallyAddedScriptElement)
316             return;
317
318         let insertIntoTopLevel = false;
319         let parentFolderTreeElement = null;
320
321         if (script.injected) {
322             if (!this._extensionScriptsFolderTreeElement) {
323                 let collection = new WI.ScriptCollection;
324                 this._extensionScriptsFolderTreeElement = new WI.FolderTreeElement(WI.UIString("Extension Scripts"), collection);
325             }
326
327             parentFolderTreeElement = this._extensionScriptsFolderTreeElement;
328         } else {
329             if (WI.sharedApp.debuggableType === WI.DebuggableType.JavaScript && !WI.sharedApp.hasExtraDomains)
330                 insertIntoTopLevel = true;
331             else {
332                 if (!this._extraScriptsFolderTreeElement) {
333                     let collection = new WI.ScriptCollection;
334                     this._extraScriptsFolderTreeElement = new WI.FolderTreeElement(WI.UIString("Extra Scripts"), collection);
335                 }
336
337                 parentFolderTreeElement = this._extraScriptsFolderTreeElement;
338             }
339         }
340
341         if (parentFolderTreeElement)
342             parentFolderTreeElement.representedObject.add(script);
343
344         var scriptTreeElement = new WI.ScriptTreeElement(script);
345
346         if (insertIntoTopLevel) {
347             var index = insertionIndexForObjectInListSortedByFunction(scriptTreeElement, this.contentTreeOutline.children, this._compareTreeElements);
348             this.contentTreeOutline.insertChild(scriptTreeElement, index);
349         } else {
350             if (!parentFolderTreeElement.parent) {
351                 var index = insertionIndexForObjectInListSortedByFunction(parentFolderTreeElement, this.contentTreeOutline.children, this._compareTreeElements);
352                 this.contentTreeOutline.insertChild(parentFolderTreeElement, index);
353             }
354
355             parentFolderTreeElement.appendChild(scriptTreeElement);
356         }
357     }
358
359     _scriptWasRemoved(event)
360     {
361         let script = event.data.script;
362         let scriptTreeElement = this.contentTreeOutline.getCachedTreeElement(script);
363         if (!scriptTreeElement)
364             return;
365
366         let parentTreeElement = scriptTreeElement.parent;
367         parentTreeElement.removeChild(scriptTreeElement);
368
369         if (parentTreeElement instanceof WI.FolderTreeElement) {
370             parentTreeElement.representedObject.remove(script);
371
372             if (!parentTreeElement.children.length)
373                 parentTreeElement.parent.removeChild(parentTreeElement);
374         }
375     }
376
377     _scriptsCleared(event)
378     {
379         const suppressOnDeselect = true;
380         const suppressSelectSibling = true;
381
382         if (this._extensionScriptsFolderTreeElement) {
383             if (this._extensionScriptsFolderTreeElement.parent)
384                 this._extensionScriptsFolderTreeElement.parent.removeChild(this._extensionScriptsFolderTreeElement, suppressOnDeselect, suppressSelectSibling);
385
386             this._extensionScriptsFolderTreeElement.representedObject.clear();
387             this._extensionScriptsFolderTreeElement = null;
388         }
389
390         if (this._extraScriptsFolderTreeElement) {
391             if (this._extraScriptsFolderTreeElement.parent)
392                 this._extraScriptsFolderTreeElement.parent.removeChild(this._extraScriptsFolderTreeElement, suppressOnDeselect, suppressSelectSibling);
393
394             this._extraScriptsFolderTreeElement.representedObject.clear();
395             this._extraScriptsFolderTreeElement = null;
396         }
397
398         if (this._anonymousScriptsFolderTreeElement) {
399             if (this._anonymousScriptsFolderTreeElement.parent)
400                 this._anonymousScriptsFolderTreeElement.parent.removeChild(this._anonymousScriptsFolderTreeElement, suppressOnDeselect, suppressSelectSibling);
401
402             this._anonymousScriptsFolderTreeElement.representedObject.clear();
403             this._anonymousScriptsFolderTreeElement = null;
404         }
405
406         if (this._targetTreeElementMap.size) {
407             for (let treeElement of this._targetTreeElementMap)
408                 treeElement.parent.removeChild(treeElement, suppressOnDeselect, suppressSelectSibling);
409             this._targetTreeElementMap.clear();
410         }
411     }
412
413     _styleSheetAdded(event)
414     {
415         let styleSheet = event.data.styleSheet;
416         if (!styleSheet.isInspectorStyleSheet())
417             return;
418
419         let frameTreeElement = this.treeElementForRepresentedObject(styleSheet.parentFrame);
420         if (!frameTreeElement)
421             return;
422
423         frameTreeElement.addRepresentedObjectToNewChildQueue(styleSheet);
424     }
425
426     _addTargetWithMainResource(target)
427     {
428         console.assert(target.type === WI.Target.Type.Worker);
429
430         let targetTreeElement = new WI.WorkerTreeElement(target);
431         this._targetTreeElementMap.set(target, targetTreeElement);
432
433         let index = insertionIndexForObjectInListSortedByFunction(targetTreeElement, this.contentTreeOutline.children, this._compareTreeElements);
434         this.contentTreeOutline.insertChild(targetTreeElement, index);
435     }
436
437     _targetRemoved(event)
438     {
439         let removedTarget = event.data.target;
440
441         let targetTreeElement = this._targetTreeElementMap.take(removedTarget);
442         if (targetTreeElement)
443             targetTreeElement.parent.removeChild(targetTreeElement);
444     }
445
446     _treeSelectionDidChange(event)
447     {
448         if (!this.visible)
449             return;
450
451         let treeElement = event.data.selectedElement;
452         if (!treeElement)
453             return;
454
455         if (treeElement instanceof WI.FolderTreeElement
456             || treeElement instanceof WI.ResourceTreeElement
457             || treeElement instanceof WI.ScriptTreeElement
458             || treeElement instanceof WI.CSSStyleSheetTreeElement) {
459             const cookie = null;
460             const options = {
461                 ignoreNetworkTab: true,
462                 ignoreSearchTab: true,
463             };
464             WI.showRepresentedObject(treeElement.representedObject, cookie, options);
465             return;
466         }
467
468         console.error("Unknown tree element", treeElement);
469     }
470
471     _compareTreeElements(a, b)
472     {
473         // Always sort the main frame element first.
474         if (a instanceof WI.FrameTreeElement)
475             return -1;
476         if (b instanceof WI.FrameTreeElement)
477             return 1;
478
479         console.assert(a.mainTitle);
480         console.assert(b.mainTitle);
481
482         return (a.mainTitle || "").extendedLocaleCompare(b.mainTitle || "");
483     }
484
485     _extraDomainsActivated()
486     {
487         if (WI.sharedApp.debuggableType === WI.DebuggableType.JavaScript)
488             this.contentTreeOutline.disclosureButtons = true;
489     }
490
491     _scopeBarSelectionDidChange(event)
492     {
493         this.updateFilter();
494     }
495 };
496
497 WI.ResourceSidebarPanel.ResourceTypeSymbol = Symbol("resource-type");