11b9d33287d93bd6bcd8fcdb3a6760cb5bb2e19a
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / ContentBrowser.js
1 /*
2  * Copyright (C) 2013 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WebInspector.ContentBrowser = function(element, delegate, disableBackForward)
27 {
28     // FIXME: Convert this to a WebInspector.Object subclass, and call super().
29     // WebInspector.Object.call(this);
30
31     this._element = element || document.createElement("div");
32     this._element.classList.add(WebInspector.ContentBrowser.StyleClassName);
33
34     this._navigationBar = new WebInspector.NavigationBar;
35     this._element.appendChild(this._navigationBar.element);
36
37     this._contentViewContainer = new WebInspector.ContentViewContainer;
38     this._contentViewContainer.addEventListener(WebInspector.ContentViewContainer.Event.CurrentContentViewDidChange, this._currentContentViewDidChange, this);
39     this._element.appendChild(this._contentViewContainer.element);
40
41     this._findBanner = new WebInspector.FindBanner(this);
42     this._findBanner.addEventListener(WebInspector.FindBanner.Event.DidShow, this._findBannerDidShow, this);
43     this._findBanner.addEventListener(WebInspector.FindBanner.Event.DidHide, this._findBannerDidHide, this);
44
45     this._findKeyboardShortcut = new WebInspector.KeyboardShortcut(WebInspector.KeyboardShortcut.Modifier.CommandOrControl, "F", this._showFindBanner.bind(this));
46     this._saveKeyboardShortcut = new WebInspector.KeyboardShortcut(WebInspector.KeyboardShortcut.Modifier.CommandOrControl, "S", this._save.bind(this));
47     this._saveAsKeyboardShortcut = new WebInspector.KeyboardShortcut(WebInspector.KeyboardShortcut.Modifier.Shift | WebInspector.KeyboardShortcut.Modifier.CommandOrControl, "S", this._saveAs.bind(this));
48
49     this._findKeyboardShortcut.disabled = true;
50     this._saveKeyboardShortcut.disabled = true;
51     this._saveAsKeyboardShortcut.disabled = true;
52
53     if (!disableBackForward) {
54         this._backKeyboardShortcut = new WebInspector.KeyboardShortcut(WebInspector.KeyboardShortcut.Modifier.CommandOrControl | WebInspector.KeyboardShortcut.Modifier.Control, WebInspector.KeyboardShortcut.Key.Left, this._backButtonClicked.bind(this));
55         this._forwardKeyboardShortcut = new WebInspector.KeyboardShortcut(WebInspector.KeyboardShortcut.Modifier.CommandOrControl | WebInspector.KeyboardShortcut.Modifier.Control, WebInspector.KeyboardShortcut.Key.Right, this._forwardButtonClicked.bind(this));
56
57         this._backKeyboardShortcut.disabled = true;
58         this._forwardKeyboardShortcut.disabled = true;
59
60         var forwardArrow, backArrow;
61         if (WebInspector.Platform.isLegacyMacOS) {
62             forwardArrow = {src: "Images/Legacy/ForwardArrow.svg", width: 9, height: 9};
63             backArrow = {src: "Images/Legacy/BackArrow.svg", width: 9, height: 9};
64         } else {
65             forwardArrow = {src: "Images/ForwardArrow.svg", width: 8, height: 13};
66             backArrow = {src: "Images/BackArrow.svg", width: 8, height: 13};
67         }
68
69         this._backButtonNavigationItem = new WebInspector.ButtonNavigationItem("back", WebInspector.UIString("Back (%s)").format(this._backKeyboardShortcut.displayName), backArrow.src, backArrow.width, backArrow.height);
70         this._backButtonNavigationItem.addEventListener(WebInspector.ButtonNavigationItem.Event.Clicked, this._backButtonClicked, this);
71         this._backButtonNavigationItem.enabled = false;
72         this._navigationBar.addNavigationItem(this._backButtonNavigationItem);
73
74         this._forwardButtonNavigationItem = new WebInspector.ButtonNavigationItem("forward", WebInspector.UIString("Forward (%s)").format(this._forwardKeyboardShortcut.displayName), forwardArrow.src, forwardArrow.width, forwardArrow.height);
75         this._forwardButtonNavigationItem.addEventListener(WebInspector.ButtonNavigationItem.Event.Clicked, this._forwardButtonClicked, this);
76         this._forwardButtonNavigationItem.enabled = false;
77         this._navigationBar.addNavigationItem(this._forwardButtonNavigationItem);
78
79         this._navigationBar.addNavigationItem(new WebInspector.DividerNavigationItem);
80     }
81
82     this._hierarchicalPathNavigationItem = new WebInspector.HierarchicalPathNavigationItem;
83     this._hierarchicalPathNavigationItem.addEventListener(WebInspector.HierarchicalPathNavigationItem.Event.PathComponentWasSelected, this._hierarchicalPathComponentWasSelected, this);
84     this._navigationBar.addNavigationItem(this._hierarchicalPathNavigationItem);
85
86     this._contentViewSelectionPathNavigationItem = new WebInspector.HierarchicalPathNavigationItem;
87
88     this._dividingFlexibleSpaceNavigationItem = new WebInspector.FlexibleSpaceNavigationItem;
89     this._navigationBar.addNavigationItem(this._dividingFlexibleSpaceNavigationItem);
90
91     WebInspector.ContentView.addEventListener(WebInspector.ContentView.Event.SelectionPathComponentsDidChange, this._contentViewSelectionPathComponentDidChange, this);
92     WebInspector.ContentView.addEventListener(WebInspector.ContentView.Event.SupplementalRepresentedObjectsDidChange, this._contentViewSupplementalRepresentedObjectsDidChange, this);
93     WebInspector.ContentView.addEventListener(WebInspector.ContentView.Event.NumberOfSearchResultsDidChange, this._contentViewNumberOfSearchResultsDidChange, this);
94     WebInspector.ContentView.addEventListener(WebInspector.ContentView.Event.NavigationItemsDidChange, this._contentViewNavigationItemsDidChange, this);
95
96     this._delegate = delegate || null;
97
98     this._currentContentViewNavigationItems = [];
99 };
100
101 // FIXME: Move to a WebInspector.Object subclass and we can remove this.
102 WebInspector.Object.deprecatedAddConstructorFunctions(WebInspector.ContentBrowser);
103
104 WebInspector.ContentBrowser.StyleClassName = "content-browser";
105
106 WebInspector.ContentBrowser.Event = {
107     CurrentRepresentedObjectsDidChange: "content-browser-current-represented-objects-did-change",
108     CurrentContentViewDidChange: "content-browser-current-content-view-did-change"
109 };
110
111 WebInspector.ContentBrowser.prototype = {
112     constructor: WebInspector.ContentBrowser,
113
114     // Public
115
116     get element()
117     {
118         return this._element;
119     },
120
121     get navigationBar()
122     {
123         return this._navigationBar;
124     },
125
126     get contentViewContainer()
127     {
128         return this._contentViewContainer;
129     },
130
131     get delegate()
132     {
133         return this._delegate;
134     },
135
136     set delegate(newDelegate)
137     {
138         this._delegate = newDelegate || null;
139     },
140
141     get currentContentView()
142     {
143         return this._contentViewContainer.currentContentView;
144     },
145
146     get currentRepresentedObjects()
147     {
148         var representedObjects = [];
149
150         var lastComponent = this._hierarchicalPathNavigationItem.lastComponent;
151         if (lastComponent && lastComponent.representedObject)
152             representedObjects.push(lastComponent.representedObject);
153
154         lastComponent = this._contentViewSelectionPathNavigationItem.lastComponent;
155         if (lastComponent && lastComponent.representedObject)
156             representedObjects.push(lastComponent.representedObject);
157
158         var currentContentView = this.currentContentView;
159         if (currentContentView) {
160             var supplementalRepresentedObjects = currentContentView.supplementalRepresentedObjects;
161             if (supplementalRepresentedObjects && supplementalRepresentedObjects.length)
162                 representedObjects = representedObjects.concat(supplementalRepresentedObjects);
163         }
164
165         return representedObjects;
166     },
167
168     updateLayout: function()
169     {
170         this._navigationBar.updateLayout();
171         this._contentViewContainer.updateLayout();
172     },
173
174     showContentViewForRepresentedObject: function(representedObject, cookie, extraArguments)
175     {
176         var contentView = this.contentViewForRepresentedObject(representedObject, false, extraArguments);
177         return this._contentViewContainer.showContentView(contentView, cookie);
178     },
179
180     showContentView: function(contentView, cookie)
181     {
182         return this._contentViewContainer.showContentView(contentView, cookie);
183     },
184
185     contentViewForRepresentedObject: function(representedObject, onlyExisting, extraArguments)
186     {
187         return this._contentViewContainer.contentViewForRepresentedObject(representedObject, onlyExisting, extraArguments);
188     },
189
190     canGoBack: function()
191     {
192         var currentContentView = this.currentContentView;
193         if (currentContentView && currentContentView.canGoBack())
194             return true;
195         return this._contentViewContainer.canGoBack();
196     },
197
198     canGoForward: function()
199     {
200         var currentContentView = this.currentContentView;
201         if (currentContentView && currentContentView.canGoForward())
202             return true;
203         return this._contentViewContainer.canGoForward();
204     },
205
206     goBack: function()
207     {
208         var currentContentView = this.currentContentView;
209         if (currentContentView && currentContentView.canGoBack()) {
210             currentContentView.goBack();
211             this._updateBackForwardButtons();
212             return;
213         }
214
215         this._contentViewContainer.goBack();
216
217         // The _updateBackForwardButtons function is called by _currentContentViewDidChange,
218         // so it does not need to be called here.
219     },
220
221     goForward: function()
222     {
223         var currentContentView = this.currentContentView;
224         if (currentContentView && currentContentView.canGoForward()) {
225             currentContentView.goForward();
226             this._updateBackForwardButtons();
227             return;
228         }
229
230         this._contentViewContainer.goForward();
231
232         // The _updateBackForwardButtons function is called by _currentContentViewDidChange,
233         // so it does not need to be called here.
234     },
235
236     findBannerPerformSearch: function(findBanner, query)
237     {
238         var currentContentView = this.currentContentView;
239         if (!currentContentView || !currentContentView.supportsSearch)
240             return;
241
242         currentContentView.performSearch(query);
243     },
244
245     findBannerSearchCleared: function(findBanner)
246     {
247         var currentContentView = this.currentContentView;
248         if (!currentContentView || !currentContentView.supportsSearch)
249             return;
250
251         currentContentView.searchCleared();
252     },
253
254     findBannerSearchQueryForSelection: function(findBanner)
255     {
256         var currentContentView = this.currentContentView;
257         if (!currentContentView || !currentContentView.supportsSearch)
258             return null;
259
260         return currentContentView.searchQueryWithSelection();
261     },
262
263     findBannerRevealPreviousResult: function(findBanner)
264     {
265         var currentContentView = this.currentContentView;
266         if (!currentContentView || !currentContentView.supportsSearch)
267             return;
268
269         currentContentView.revealPreviousSearchResult(!findBanner.showing);
270     },
271
272     findBannerRevealNextResult: function(findBanner)
273     {
274         var currentContentView = this.currentContentView;
275         if (!currentContentView || !currentContentView.supportsSearch)
276             return;
277
278         currentContentView.revealNextSearchResult(!findBanner.showing);
279     },
280
281     shown: function()
282     {
283         this._contentViewContainer.shown();
284
285         if (this._backKeyboardShortcut)
286             this._backKeyboardShortcut.disabled = false;
287         if (this._forwardKeyboardShortcut)
288             this._forwardKeyboardShortcut.disabled = false;
289
290         this._findKeyboardShortcut.disabled = false;
291         this._saveKeyboardShortcut.disabled = false;
292         this._saveAsKeyboardShortcut.disabled = false;
293
294         this._findBanner.enableKeyboardShortcuts();
295     },
296
297     hidden: function()
298     {
299         this._contentViewContainer.hidden();
300
301         if (this._backKeyboardShortcut)
302             this._backKeyboardShortcut.disabled = false;
303         if (this._forwardKeyboardShortcut)
304             this._forwardKeyboardShortcut.disabled = true;
305
306         this._findKeyboardShortcut.disabled = true;
307         this._saveKeyboardShortcut.disabled = true;
308         this._saveAsKeyboardShortcut.disabled = true;
309
310         this._findBanner.disableKeyboardShortcuts();
311     },
312
313     // Private
314
315     _backButtonClicked: function(event)
316     {
317         this.goBack();
318     },
319
320     _forwardButtonClicked: function(event)
321     {
322         this.goForward();
323     },
324
325     _saveDataToFile: function(saveData, forceSaveAs)
326     {
327         console.assert(saveData);
328         if (!saveData)
329             return;
330
331         if (typeof saveData.customSaveHandler === "function") {
332             saveData.customSaveHandler(forceSaveAs);
333             return;
334         }
335
336         console.assert(saveData.url);
337         console.assert(typeof saveData.content === "string");
338         if (!saveData.url || typeof saveData.content !== "string")
339             return;
340
341         InspectorFrontendHost.save(saveData.url, saveData.content, false, forceSaveAs || saveData.forceSaveAs);
342     },
343
344     _save: function(event)
345     {
346         var currentContentView = this.currentContentView;
347         if (!currentContentView || !currentContentView.supportsSave)
348             return;
349
350         this._saveDataToFile(currentContentView.saveData);
351     },
352
353     _saveAs: function(event)
354     {
355         var currentContentView = this.currentContentView;
356         if (!currentContentView || !currentContentView.supportsSave)
357             return;
358
359         this._saveDataToFile(currentContentView.saveData, true);
360     },
361
362     _showFindBanner: function(event)
363     {
364         var currentContentView = this.currentContentView;
365         if (!currentContentView || !currentContentView.supportsSearch)
366             return;
367
368         this._findBanner.show();
369     },
370
371     _findBannerDidShow: function(event)
372     {
373         var currentContentView = this.currentContentView;
374         if (!currentContentView || !currentContentView.supportsSearch)
375             return;
376
377         currentContentView.automaticallyRevealFirstSearchResult = true;
378     },
379
380     _findBannerDidHide: function(event)
381     {
382         var currentContentView = this.currentContentView;
383         if (!currentContentView || !currentContentView.supportsSearch)
384             return;
385
386         currentContentView.automaticallyRevealFirstSearchResult = false;
387     },
388
389     _contentViewNumberOfSearchResultsDidChange: function(event)
390     {
391         if (event.target !== this.currentContentView)
392             return;
393
394         this._findBanner.numberOfResults = this.currentContentView.numberOfSearchResults;
395     },
396
397     _updateHierarchicalPathNavigationItem: function(representedObject)
398     {
399         if (!this.delegate || typeof this.delegate.contentBrowserTreeElementForRepresentedObject !== "function")
400             return;
401
402         var treeElement = representedObject ? this.delegate.contentBrowserTreeElementForRepresentedObject(this, representedObject) : null;
403         var pathComponents = [];
404
405         while (treeElement && !treeElement.root) {
406             var pathComponent = new WebInspector.GeneralTreeElementPathComponent(treeElement);
407             pathComponents.unshift(pathComponent);
408             treeElement = treeElement.parent;
409         }
410
411         this._hierarchicalPathNavigationItem.components = pathComponents;
412     },
413
414     _updateContentViewSelectionPathNavigationItem: function(contentView)
415     {
416         var selectionPathComponents = contentView ? contentView.selectionPathComponents || [] : [];
417         this._contentViewSelectionPathNavigationItem.components = selectionPathComponents;
418
419         if (!selectionPathComponents.length) {
420             this._hierarchicalPathNavigationItem.alwaysShowLastPathComponentSeparator = false;
421             this._navigationBar.removeNavigationItem(this._contentViewSelectionPathNavigationItem);
422             return;
423         }
424
425         // Insert the _contentViewSelectionPathNavigationItem after the _hierarchicalPathNavigationItem, if needed.
426         if (!this._navigationBar.navigationItems.includes(this._contentViewSelectionPathNavigationItem)) {
427             var hierarchicalPathItemIndex = this._navigationBar.navigationItems.indexOf(this._hierarchicalPathNavigationItem);
428             console.assert(hierarchicalPathItemIndex !== -1);
429             this._navigationBar.insertNavigationItem(this._contentViewSelectionPathNavigationItem, hierarchicalPathItemIndex + 1);
430             this._hierarchicalPathNavigationItem.alwaysShowLastPathComponentSeparator = true;
431         }
432     },
433
434     _updateBackForwardButtons: function()
435     {
436         if (!this._backButtonNavigationItem || !this._forwardButtonNavigationItem)
437             return;
438
439         this._backButtonNavigationItem.enabled = this.canGoBack();
440         this._forwardButtonNavigationItem.enabled = this.canGoForward();
441     },
442
443     _updateContentViewNavigationItems: function()
444     {
445         var navigationBar = this.navigationBar;
446
447         // First, we remove the navigation items added by the previous content view.
448         this._currentContentViewNavigationItems.forEach(function(navigationItem) {
449             navigationBar.removeNavigationItem(navigationItem);
450         });
451
452         var currentContentView = this.currentContentView;
453         if (!currentContentView) {
454             this._currentContentViewNavigationItems = [];
455             return;
456         }
457
458         var insertionIndex = navigationBar.navigationItems.indexOf(this._dividingFlexibleSpaceNavigationItem) + 1;
459         console.assert(insertionIndex >= 0);
460
461         // Keep track of items we'll be adding to the navigation bar.
462         var newNavigationItems = [];
463
464         // Go through each of the items of the new content view and add a divider before them.
465         currentContentView.navigationItems.forEach(function(navigationItem, index) {
466             // Add dividers before items unless it's the first item and not a button.
467             if (index !== 0 || navigationItem instanceof WebInspector.ButtonNavigationItem) {
468                 var divider = new WebInspector.DividerNavigationItem;
469                 navigationBar.insertNavigationItem(divider, insertionIndex++);
470                 newNavigationItems.push(divider);
471             }
472             navigationBar.insertNavigationItem(navigationItem, insertionIndex++);
473             newNavigationItems.push(navigationItem);
474         });
475
476         // Remember the navigation items we inserted so we can remove them
477         // for the next content view.
478         this._currentContentViewNavigationItems = newNavigationItems;
479     },
480
481     _updateFindBanner: function(currentContentView)
482     {
483         if (!currentContentView) {
484             this._findBanner.targetElement = null;
485             this._findBanner.numberOfResults = null;
486             return;
487         }
488
489         this._findBanner.targetElement = currentContentView.element;
490         this._findBanner.numberOfResults = currentContentView.hasPerformedSearch ? currentContentView.numberOfSearchResults : null;
491
492         if (currentContentView.supportsSearch && this._findBanner.searchQuery) {
493             currentContentView.automaticallyRevealFirstSearchResult = this._findBanner.showing;
494             currentContentView.performSearch(this._findBanner.searchQuery);
495         }
496     },
497
498     _dispatchCurrentRepresentedObjectsDidChangeEventSoon: function()
499     {
500         if (this._currentRepresentedObjectsDidChangeTimeout)
501             return;
502         this._currentRepresentedObjectsDidChangeTimeout = setTimeout(this._dispatchCurrentRepresentedObjectsDidChangeEvent.bind(this), 0);
503     },
504
505     _dispatchCurrentRepresentedObjectsDidChangeEvent: function()
506     {
507         if (this._currentRepresentedObjectsDidChangeTimeout) {
508             clearTimeout(this._currentRepresentedObjectsDidChangeTimeout);
509             delete this._currentRepresentedObjectsDidChangeTimeout;
510         }
511
512         this.dispatchEventToListeners(WebInspector.ContentBrowser.Event.CurrentRepresentedObjectsDidChange);
513     },
514
515     _contentViewSelectionPathComponentDidChange: function(event)
516     {
517         if (event.target !== this.currentContentView)
518             return;
519
520         this._updateContentViewSelectionPathNavigationItem(event.target);
521         this._updateBackForwardButtons();
522
523         this._updateContentViewNavigationItems();
524
525         this._navigationBar.updateLayout();
526
527         this._dispatchCurrentRepresentedObjectsDidChangeEventSoon();
528     },
529
530     _contentViewSupplementalRepresentedObjectsDidChange: function(event)
531     {
532         if (event.target !== this.currentContentView)
533             return;
534
535         this._dispatchCurrentRepresentedObjectsDidChangeEventSoon();
536     },
537
538     _currentContentViewDidChange: function(event)
539     {
540         var currentContentView = this.currentContentView;
541
542         this._updateHierarchicalPathNavigationItem(currentContentView ? currentContentView.representedObject : null);
543         this._updateContentViewSelectionPathNavigationItem(currentContentView);
544         this._updateBackForwardButtons();
545
546         this._updateContentViewNavigationItems();
547         this._updateFindBanner(currentContentView);
548
549         this._navigationBar.updateLayout();
550
551         this.dispatchEventToListeners(WebInspector.ContentBrowser.Event.CurrentContentViewDidChange);
552
553         this._dispatchCurrentRepresentedObjectsDidChangeEvent();
554     },
555
556     _contentViewNavigationItemsDidChange: function(event)
557     {
558         if (event.target !== this.currentContentView)
559             return;
560
561         this._updateContentViewNavigationItems();
562         this._navigationBar.updateLayout();
563     },
564
565     _hierarchicalPathComponentWasSelected: function(event)
566     {
567         console.assert(event.data.pathComponent instanceof WebInspector.GeneralTreeElementPathComponent);
568
569         var treeElement = event.data.pathComponent.generalTreeElement;
570         var originalTreeElement = treeElement;
571
572         // Some tree elements (like folders) are not viewable. Find the first descendant that is viewable.
573         while (treeElement && !WebInspector.ContentView.isViewable(treeElement.representedObject))
574             treeElement = treeElement.traverseNextTreeElement(false, originalTreeElement, false);
575
576         if (!treeElement)
577             return;
578
579         this.showContentViewForRepresentedObject(treeElement.representedObject);
580     }
581 };
582
583 WebInspector.ContentBrowser.prototype.__proto__ = WebInspector.Object.prototype;