Web Inspector: save and restore source positions in back/forward history
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / 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     WebInspector.Object.call(this);
29
30     this._element = element || document.createElement("div");
31     this._element.classList.add(WebInspector.ContentBrowser.StyleClassName);
32
33     this._navigationBar = new WebInspector.NavigationBar;
34     this._element.appendChild(this._navigationBar.element);
35
36     this._contentViewContainer = new WebInspector.ContentViewContainer;
37     this._contentViewContainer.addEventListener(WebInspector.ContentViewContainer.Event.CurrentContentViewDidChange, this._currentContentViewDidChange, this);
38     this._element.appendChild(this._contentViewContainer.element);
39
40     this._findBanner = new WebInspector.FindBanner(this);
41     this._findKeyboardShortcut = new WebInspector.KeyboardShortcut(WebInspector.KeyboardShortcut.Modifier.CommandOrControl, "F", this._showFindBanner.bind(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._saveKeyboardShortcut = new WebInspector.KeyboardShortcut(WebInspector.KeyboardShortcut.Modifier.CommandOrControl, "S", this._save.bind(this));
46     this._saveAsKeyboardShortcut = new WebInspector.KeyboardShortcut(WebInspector.KeyboardShortcut.Modifier.Shift | WebInspector.KeyboardShortcut.Modifier.CommandOrControl, "S", this._saveAs.bind(this));
47
48     if (!disableBackForward) {
49         this._backKeyboardShortcut = new WebInspector.KeyboardShortcut(WebInspector.KeyboardShortcut.Modifier.CommandOrControl | WebInspector.KeyboardShortcut.Modifier.Control, WebInspector.KeyboardShortcut.Key.Left, this._backButtonClicked.bind(this));
50         this._forwardKeyboardShortcut = new WebInspector.KeyboardShortcut(WebInspector.KeyboardShortcut.Modifier.CommandOrControl | WebInspector.KeyboardShortcut.Modifier.Control, WebInspector.KeyboardShortcut.Key.Right, this._forwardButtonClicked.bind(this));
51
52         this._backButtonNavigationItem = new WebInspector.ButtonNavigationItem("back", WebInspector.UIString("Back (%s)").format(this._backKeyboardShortcut.displayName), "Images/BackArrow.svg", 9, 9);
53         this._backButtonNavigationItem.addEventListener(WebInspector.ButtonNavigationItem.Event.Clicked, this._backButtonClicked, this);
54         this._backButtonNavigationItem.enabled = false;
55         this._navigationBar.addNavigationItem(this._backButtonNavigationItem);
56
57         this._forwardButtonNavigationItem = new WebInspector.ButtonNavigationItem("forward", WebInspector.UIString("Forward (%s)").format(this._forwardKeyboardShortcut.displayName), "Images/ForwardArrow.svg", 9, 9);
58         this._forwardButtonNavigationItem.addEventListener(WebInspector.ButtonNavigationItem.Event.Clicked, this._forwardButtonClicked, this);
59         this._forwardButtonNavigationItem.enabled = false;
60         this._navigationBar.addNavigationItem(this._forwardButtonNavigationItem);
61
62         this._navigationBar.addNavigationItem(new WebInspector.DividerNavigationItem);
63     }
64
65     this._hierarchicalPathNavigationItem = new WebInspector.HierarchicalPathNavigationItem;
66     this._hierarchicalPathNavigationItem.addEventListener(WebInspector.HierarchicalPathNavigationItem.Event.PathComponentWasSelected, this._hierarchicalPathComponentWasSelected, this);
67     this._navigationBar.addNavigationItem(this._hierarchicalPathNavigationItem);
68
69     this._contentViewSelectionPathNavigationItem = new WebInspector.HierarchicalPathNavigationItem;
70
71     this._navigationBar.addNavigationItem(new WebInspector.FlexibleSpaceNavigationItem);
72
73     WebInspector.ContentView.addEventListener(WebInspector.ContentView.Event.SelectionPathComponentsDidChange, this._contentViewSelectionPathComponentDidChange, this);
74     WebInspector.ContentView.addEventListener(WebInspector.ContentView.Event.SupplementalRepresentedObjectsDidChange, this._contentViewSupplementalRepresentedObjectsDidChange, this);
75     WebInspector.ContentView.addEventListener(WebInspector.ContentView.Event.NumberOfSearchResultsDidChange, this._contentViewNumberOfSearchResultsDidChange, this);
76     WebInspector.ContentView.addEventListener(WebInspector.ContentView.Event.NavigationItemsDidChange, this._contentViewNavigationItemsDidChange, this);
77
78     this._delegate = delegate || null;
79
80     this._currentContentViewNavigationItems = [];
81 };
82
83 WebInspector.Object.addConstructorFunctions(WebInspector.ContentBrowser);
84
85 WebInspector.ContentBrowser.StyleClassName = "content-browser";
86
87 WebInspector.ContentBrowser.Event = {
88     CurrentRepresentedObjectsDidChange: "content-browser-current-represented-objects-did-change",
89     CurrentContentViewDidChange: "content-browser-current-content-view-did-change"
90 };
91
92 WebInspector.ContentBrowser.prototype = {
93     constructor: WebInspector.ContentBrowser,
94
95     // Public
96
97     get element()
98     {
99         return this._element;
100     },
101
102     get navigationBar()
103     {
104         return this._navigationBar;
105     },
106
107     get contentViewContainer()
108     {
109         return this._contentViewContainer;
110     },
111
112     get delegate()
113     {
114         return this._delegate;
115     },
116
117     set delegate(newDelegate)
118     {
119         this._delegate = newDelegate || null;
120     },
121
122     get currentContentView()
123     {
124         return this._contentViewContainer.currentContentView;
125     },
126
127     get currentRepresentedObjects()
128     {
129         var representedObjects = [];
130
131         var lastComponent = this._hierarchicalPathNavigationItem.lastComponent;
132         if (lastComponent && lastComponent.representedObject)
133             representedObjects.push(lastComponent.representedObject);
134
135         lastComponent = this._contentViewSelectionPathNavigationItem.lastComponent;
136         if (lastComponent && lastComponent.representedObject)
137             representedObjects.push(lastComponent.representedObject);
138
139         var currentContentView = this.currentContentView;
140         if (currentContentView) {
141             var supplementalRepresentedObjects = currentContentView.supplementalRepresentedObjects;
142             if (supplementalRepresentedObjects && supplementalRepresentedObjects.length)
143                 representedObjects = representedObjects.concat(supplementalRepresentedObjects);
144         }
145
146         return representedObjects;
147     },
148
149     updateLayout: function()
150     {
151         this._navigationBar.updateLayout();
152         this._contentViewContainer.updateLayout();
153     },
154
155     showContentViewForRepresentedObject: function(representedObject)
156     {
157         return this._contentViewContainer.showContentViewForRepresentedObject(representedObject);
158     },
159
160     showContentView: function(contentView, cookie, restoreCallback)
161     {
162         return this._contentViewContainer.showContentView(contentView, cookie, restoreCallback);
163     },
164
165     contentViewForRepresentedObject: function(representedObject, onlyExisting)
166     {
167         return this._contentViewContainer.contentViewForRepresentedObject(representedObject, onlyExisting);
168     },
169
170     canGoBack: function()
171     {
172         var currentContentView = this.currentContentView;
173         if (currentContentView && currentContentView.canGoBack())
174             return true;
175         return this._contentViewContainer.canGoBack();
176     },
177
178     canGoForward: function()
179     {
180         var currentContentView = this.currentContentView;
181         if (currentContentView && currentContentView.canGoForward())
182             return true;
183         return this._contentViewContainer.canGoForward();
184     },
185
186     goBack: function()
187     {
188         var currentContentView = this.currentContentView;
189         if (currentContentView && currentContentView.canGoBack()) {
190             currentContentView.goBack();
191             this._updateBackForwardButtons();
192             return;
193         }
194
195         this._contentViewContainer.goBack();
196
197         // The _updateBackForwardButtons function is called by _currentContentViewDidChange,
198         // so it does not need to be called here.
199     },
200
201     goForward: function()
202     {
203         var currentContentView = this.currentContentView;
204         if (currentContentView && currentContentView.canGoForward()) {
205             currentContentView.goForward();
206             this._updateBackForwardButtons();
207             return;
208         }
209
210         this._contentViewContainer.goForward();
211
212         // The _updateBackForwardButtons function is called by _currentContentViewDidChange,
213         // so it does not need to be called here.
214     },
215
216     findBannerPerformSearch: function(findBanner, query)
217     {
218         var currentContentView = this.currentContentView;
219         if (!currentContentView || !currentContentView.supportsSearch)
220             return;
221
222         currentContentView.performSearch(query);
223     },
224
225     findBannerSearchCleared: function(findBanner)
226     {
227         var currentContentView = this.currentContentView;
228         if (!currentContentView || !currentContentView.supportsSearch)
229             return;
230
231         currentContentView.searchCleared();
232     },
233
234     findBannerSearchQueryForSelection: function(findBanner)
235     {
236         var currentContentView = this.currentContentView;
237         if (!currentContentView || !currentContentView.supportsSearch)
238             return null;
239
240         return currentContentView.searchQueryWithSelection();
241     },
242
243     findBannerRevealPreviousResult: function(findBanner)
244     {
245         var currentContentView = this.currentContentView;
246         if (!currentContentView || !currentContentView.supportsSearch)
247             return;
248
249         currentContentView.revealPreviousSearchResult(!findBanner.showing);
250     },
251
252     findBannerRevealNextResult: function(findBanner)
253     {
254         var currentContentView = this.currentContentView;
255         if (!currentContentView || !currentContentView.supportsSearch)
256             return;
257
258         currentContentView.revealNextSearchResult(!findBanner.showing);
259     },
260
261     // Private
262
263     _backButtonClicked: function(event)
264     {
265         this.goBack();
266     },
267
268     _forwardButtonClicked: function(event)
269     {
270         this.goForward();
271     },
272
273     _saveDataToFile: function(saveData, forceSaveAs)
274     {
275         console.assert(saveData);
276         if (!saveData)
277             return;
278
279         if (typeof saveData.customSaveHandler === "function") {
280             saveData.customSaveHandler(forceSaveAs);
281             return;
282         }
283
284         console.assert(saveData.url);
285         console.assert(typeof saveData.content === "string");
286         if (!saveData.url || typeof saveData.content !== "string")
287             return;
288
289         InspectorFrontendHost.save(saveData.url, saveData.content, false, forceSaveAs || saveData.forceSaveAs);
290     },
291
292     _save: function(event)
293     {
294         var currentContentView = this.currentContentView;
295         if (!currentContentView || !currentContentView.supportsSave)
296             return;
297
298         this._saveDataToFile(currentContentView.saveData);
299     },
300
301     _saveAs: function(event)
302     {
303         var currentContentView = this.currentContentView;
304         if (!currentContentView || !currentContentView.supportsSave)
305             return;
306
307         this._saveDataToFile(currentContentView.saveData, true);
308     },
309
310     _showFindBanner: function(event)
311     {
312         var currentContentView = this.currentContentView;
313         if (!currentContentView || !currentContentView.supportsSearch)
314             return;
315
316         this._findBanner.show();
317     },
318
319     _findBannerDidShow: function(event)
320     {
321         var currentContentView = this.currentContentView;
322         if (!currentContentView || !currentContentView.supportsSearch)
323             return;
324
325         currentContentView.automaticallyRevealFirstSearchResult = true;
326     },
327
328     _findBannerDidHide: function(event)
329     {
330         var currentContentView = this.currentContentView;
331         if (!currentContentView || !currentContentView.supportsSearch)
332             return;
333
334         currentContentView.automaticallyRevealFirstSearchResult = false;
335     },
336
337     _contentViewNumberOfSearchResultsDidChange: function(event)
338     {
339         if (event.target !== this.currentContentView)
340             return;
341
342         this._findBanner.numberOfResults = this.currentContentView.numberOfSearchResults;
343     },
344
345     _updateHierarchicalPathNavigationItem: function(representedObject)
346     {
347         if (!this.delegate || typeof this.delegate.contentBrowserTreeElementForRepresentedObject !== "function")
348             return;
349
350         var treeElement = representedObject ? this.delegate.contentBrowserTreeElementForRepresentedObject(this, representedObject) : null;
351         var pathComponents = [];
352
353         while (treeElement && !treeElement.root) {
354             var pathComponent = new WebInspector.GeneralTreeElementPathComponent(treeElement);
355             pathComponents.unshift(pathComponent);
356             treeElement = treeElement.parent;
357         }
358
359         this._hierarchicalPathNavigationItem.components = pathComponents;
360     },
361
362     _updateContentViewSelectionPathNavigationItem: function(contentView)
363     {
364         var selectionPathComponents = contentView ? contentView.selectionPathComponents || [] : [];
365         this._contentViewSelectionPathNavigationItem.components = selectionPathComponents;
366
367         if (!selectionPathComponents.length) {
368             this._hierarchicalPathNavigationItem.alwaysShowLastPathComponentSeparator = false;
369             this._navigationBar.removeNavigationItem(this._contentViewSelectionPathNavigationItem);
370             return;
371         }
372
373         // Insert the _contentViewSelectionPathNavigationItem after the _hierarchicalPathNavigationItem, if needed.
374         if (!this._navigationBar.navigationItems.contains(this._contentViewSelectionPathNavigationItem)) {
375             var hierarchicalPathItemIndex = this._navigationBar.navigationItems.indexOf(this._hierarchicalPathNavigationItem);
376             console.assert(hierarchicalPathItemIndex !== -1);
377             this._navigationBar.insertNavigationItem(this._contentViewSelectionPathNavigationItem, hierarchicalPathItemIndex + 1);
378             this._hierarchicalPathNavigationItem.alwaysShowLastPathComponentSeparator = true;
379         }
380     },
381
382     _updateBackForwardButtons: function()
383     {
384         if (!this._backButtonNavigationItem || !this._forwardButtonNavigationItem)
385             return;
386
387         this._backButtonNavigationItem.enabled = this.canGoBack();
388         this._forwardButtonNavigationItem.enabled = this.canGoForward();
389     },
390
391     _updateContentViewNavigationItems: function()
392     {
393         var navigationBar = this.navigationBar;
394
395         // First, we remove the navigation items added by the previous content view.
396         this._currentContentViewNavigationItems.forEach(function(navigationItem) {
397             navigationBar.removeNavigationItem(navigationItem);
398         });
399
400         var currentContentView = this.currentContentView;
401         if (!currentContentView) {
402             this._currentContentViewNavigationItems = [];
403             return;
404         }
405
406         var insertionIndex = navigationBar.navigationItems.length;
407         console.assert(insertionIndex >= 0);
408
409         // Keep track of items we'll be adding to the navigation bar.
410         var newNavigationItems = [];
411
412         // Go through each of the items of the new content view and add a divider before them.
413         currentContentView.navigationItems.forEach(function(navigationItem, index) {
414             // Add dividers before items unless it's the first item and not a button.
415             if (index !== 0 || navigationItem instanceof WebInspector.ButtonNavigationItem) {
416                 var divider = new WebInspector.DividerNavigationItem;
417                 navigationBar.insertNavigationItem(divider, insertionIndex++);
418                 newNavigationItems.push(divider);
419             }
420             navigationBar.insertNavigationItem(navigationItem, insertionIndex++);
421             newNavigationItems.push(navigationItem);
422         });
423
424         // Remember the navigation items we inserted so we can remove them
425         // for the next content view.
426         this._currentContentViewNavigationItems = newNavigationItems;
427     },
428
429     _updateFindBanner: function(currentContentView)
430     {
431         if (!currentContentView) {
432             this._findBanner.targetElement = null;
433             this._findBanner.numberOfResults = null;
434             return;
435         }
436
437         this._findBanner.targetElement = currentContentView.element;
438         this._findBanner.numberOfResults = currentContentView.hasPerformedSearch ? currentContentView.numberOfSearchResults : null;
439
440         if (currentContentView.supportsSearch && this._findBanner.searchQuery) {
441             currentContentView.automaticallyRevealFirstSearchResult = this._findBanner.showing;
442             currentContentView.performSearch(this._findBanner.searchQuery);
443         }
444     },
445
446     _dispatchCurrentRepresentedObjectsDidChangeEventSoon: function()
447     {
448         if (this._currentRepresentedObjectsDidChangeTimeout)
449             return;
450         this._currentRepresentedObjectsDidChangeTimeout = setTimeout(this._dispatchCurrentRepresentedObjectsDidChangeEvent.bind(this), 0);
451     },
452
453     _dispatchCurrentRepresentedObjectsDidChangeEvent: function()
454     {
455         if (this._currentRepresentedObjectsDidChangeTimeout) {
456             clearTimeout(this._currentRepresentedObjectsDidChangeTimeout);
457             delete this._currentRepresentedObjectsDidChangeTimeout;
458         }
459
460         this.dispatchEventToListeners(WebInspector.ContentBrowser.Event.CurrentRepresentedObjectsDidChange);
461     },
462
463     _contentViewSelectionPathComponentDidChange: function(event)
464     {
465         if (event.target !== this.currentContentView)
466             return;
467
468         this._updateContentViewSelectionPathNavigationItem(event.target);
469         this._updateBackForwardButtons();
470
471         this._updateContentViewNavigationItems();
472
473         this._navigationBar.updateLayout();
474
475         this._dispatchCurrentRepresentedObjectsDidChangeEventSoon();
476     },
477
478     _contentViewSupplementalRepresentedObjectsDidChange: function(event)
479     {
480         if (event.target !== this.currentContentView)
481             return;
482
483         this._dispatchCurrentRepresentedObjectsDidChangeEventSoon();
484     },
485
486     _currentContentViewDidChange: function(event)
487     {
488         var currentContentView = this.currentContentView;
489
490         this._updateHierarchicalPathNavigationItem(currentContentView ? currentContentView.representedObject : null);
491         this._updateContentViewSelectionPathNavigationItem(currentContentView);
492         this._updateBackForwardButtons();
493
494         this._updateContentViewNavigationItems();
495         this._updateFindBanner(currentContentView);
496
497         this._navigationBar.updateLayout();
498
499         this.dispatchEventToListeners(WebInspector.ContentBrowser.Event.CurrentContentViewDidChange);
500
501         this._dispatchCurrentRepresentedObjectsDidChangeEvent();
502     },
503
504     _contentViewNavigationItemsDidChange: function(event)
505     {
506         if (event.target !== this.currentContentView)
507             return;
508
509         this._updateContentViewNavigationItems();
510         this._navigationBar.updateLayout();
511     },
512
513     _hierarchicalPathComponentWasSelected: function(event)
514     {
515         console.assert(event.data.pathComponent instanceof WebInspector.GeneralTreeElementPathComponent);
516
517         var treeElement = event.data.pathComponent.generalTreeElement;
518         var originalTreeElement = treeElement;
519
520         // Some tree elements (like folders) are not viewable. Find the first descendant that is viewable.
521         while (treeElement && !WebInspector.ContentView.isViewable(treeElement.representedObject))
522             treeElement = treeElement.traverseNextTreeElement(false, originalTreeElement, false);
523
524         if (!treeElement)
525             return;
526
527         this.showContentViewForRepresentedObject(treeElement.representedObject);
528     }
529 };
530
531 WebInspector.ContentBrowser.prototype.__proto__ = WebInspector.Object.prototype;