Web Inspector: save and restore source positions in back/forward history
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / ContentViewContainer.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.ContentViewContainer = function(element)
27 {
28     WebInspector.Object.call(this);
29
30     this._element = element || document.createElement("div");
31     this._element.classList.add(WebInspector.ContentViewContainer.StyleClassName);
32
33     this._backForwardList = [];
34     this._currentIndex = -1;
35 };
36
37 WebInspector.ContentViewContainer.StyleClassName = "content-view-container";
38
39 WebInspector.ContentViewContainer.Event = {
40     CurrentContentViewDidChange: "content-view-container-current-content-view-did-change"
41 };
42
43 WebInspector.ContentViewContainer.prototype = {
44     constructor: WebInspector.ContentViewContainer,
45
46     // Public
47
48     get element()
49     {
50         return this._element;
51     },
52
53     get currentIndex()
54     {
55         return this._currentIndex;
56     },
57
58     get backForwardList()
59     {
60         return this._backForwardList;
61     },
62
63     get currentContentView()
64     {
65         if (this._currentIndex < 0 || this._currentIndex > this._backForwardList.length - 1)
66             return null;
67         return this._backForwardList[this._currentIndex].contentView;
68     },
69
70     get currentBackForwardEntry()
71     {
72         if (this._currentIndex < 0 || this._currentIndex > this._backForwardList.length - 1)
73             return null;
74         return this._backForwardList[this._currentIndex];
75     },
76
77     updateLayout: function()
78     {
79         var currentContentView = this.currentContentView;
80         if (currentContentView)
81             currentContentView.updateLayout();
82     },
83
84     contentViewForRepresentedObject: function(representedObject, onlyExisting)
85     {
86         console.assert(representedObject);
87         if (!representedObject)
88             return null;
89
90         // Iterate over all the known content views for the representedObject (if any) and find one that doesn't
91         // have a parent container or has this container as its parent.
92         var contentView = null;
93         for (var i = 0; representedObject.__contentViews && i < representedObject.__contentViews.length; ++i) {
94             var currentContentView = representedObject.__contentViews[i];
95             if (!currentContentView._parentContainer || currentContentView._parentContainer === this) {
96                 contentView = currentContentView;
97                 break;
98             }
99         }
100
101         console.assert(!contentView || contentView instanceof WebInspector.ContentView);
102         if (contentView instanceof WebInspector.ContentView)
103             return contentView;
104
105         // Return early to avoid creating a new content view when onlyExisting is true.
106         if (onlyExisting)
107             return null;
108
109         try {
110             // No existing content view found, make a new one.
111             contentView = new WebInspector.ContentView(representedObject);
112         } catch (e) {
113             console.error(e);
114             return null;
115         }
116
117         // Remember this content view for future calls.
118         if (!representedObject.__contentViews)
119             representedObject.__contentViews = [];
120         representedObject.__contentViews.push(contentView);
121
122         return contentView;
123     },
124
125     showContentViewForRepresentedObject: function(representedObject)
126     {
127         var contentView = this.contentViewForRepresentedObject(representedObject);
128         if (!contentView)
129             return null;
130
131         this.showContentView(contentView);
132
133         return contentView;
134     },
135
136     showContentView: function(contentView, cookie, restoreCallback)
137     {
138         console.assert(contentView instanceof WebInspector.ContentView);
139         if (!(contentView instanceof WebInspector.ContentView))
140             return null;
141
142         // Don't allow showing a content view that is already associated with another container.
143         // Showing a content view that is already associated with this container is allowed.
144         console.assert(!contentView.parentContainer || contentView.parentContainer === this);
145         if (contentView.parentContainer && contentView.parentContainer !== this)
146             return null;
147
148         var currentEntry = this.currentBackForwardEntry;
149         var provisionalEntry = new WebInspector.BackForwardEntry(contentView, cookie, restoreCallback);
150         // Don't do anything if we would have added an identical back/forward list entry.
151         if (currentEntry && currentEntry.contentView === contentView && Object.shallowEqual(provisionalEntry.cookie, currentEntry.cookie))
152             return currentEntry.contentView;
153
154         // Showing a content view will truncate the back/forward list after the current index and insert the content view
155         // at the end of the list. Finally, the current index will be updated to point to the end of the back/forward list.
156
157         // Increment the current index to where we will insert the content view.
158         var newIndex = this._currentIndex + 1;
159
160         // Insert the content view at the new index. This will remove any content views greater than or equal to the index.
161         var removedEntries = this._backForwardList.splice(newIndex, this._backForwardList.length - newIndex, provisionalEntry);
162
163         console.assert(newIndex === this._backForwardList.length - 1);
164         console.assert(this._backForwardList[newIndex] === provisionalEntry);
165
166         // Disassociate with the removed content views.
167         for (var i = 0; i < removedEntries.length; ++i) {
168             // Skip disassociation if this content view is still in the back/forward list.
169             var shouldDissociateContentView = this._backForwardList.some(function(existingEntry) {
170                 return existingEntry.contentView === removedEntries[i].contentView;
171             });
172             if (shouldDissociateContentView)
173                 this._disassociateFromContentView(removedEntries[i]);
174         }
175
176         // Associate with the new content view.
177         contentView._parentContainer = this;
178
179         this.showBackForwardEntryForIndex(newIndex);
180
181         return contentView;
182     },
183
184     showBackForwardEntryForIndex: function(index)
185     {
186         console.assert(index >= 0 && index <= this._backForwardList.length - 1);
187         if (index < 0 || index > this._backForwardList.length - 1)
188             return;
189
190         if (this._currentIndex === index)
191             return;
192
193         // Hide the currently visible content view.
194         var previousEntry = this.currentBackForwardEntry;
195         if (previousEntry)
196             this._hideEntry(previousEntry);
197
198         this._currentIndex = index;
199         var currentEntry = this.currentBackForwardEntry;
200         console.assert(currentEntry);
201
202         this._showEntry(currentEntry);
203
204         this.dispatchEventToListeners(WebInspector.ContentViewContainer.Event.CurrentContentViewDidChange);
205     },
206
207     replaceContentView: function(oldContentView, newContentView)
208     {
209         console.assert(oldContentView instanceof WebInspector.ContentView);
210         if (!(oldContentView instanceof WebInspector.ContentView))
211             return;
212
213         console.assert(newContentView instanceof WebInspector.ContentView);
214         if (!(newContentView instanceof WebInspector.ContentView))
215             return;
216
217         console.assert(oldContentView.parentContainer === this);
218         if (oldContentView.parentContainer !== this)
219             return;
220
221         console.assert(!newContentView.parentContainer || newContentView.parentContainer === this);
222         if (newContentView.parentContainer && newContentView.parentContainer !== this)
223             return;
224
225         var currentlyShowing = (this.currentContentView === oldContentView);
226         if (currentlyShowing)
227             this._hideEntry(this.currentBackForwardEntry);
228
229         // Disassociate with the old content view.
230         this._disassociateFromContentView(oldContentView);
231
232         // Associate with the new content view.
233         newContentView._parentContainer = this;
234
235         // Replace all occurrences of oldContentView with newContentView in the back/forward list.
236         for (var i = 0; i < this._backForwardList.length; ++i) {
237             if (this._backForwardList[i].contentView === oldContentView)
238                 this._backForwardList[i].contentView = newContentView;
239         }
240
241         // Re-show the current entry, because its content view instance was replaced.
242         if (currentlyShowing) {
243             this._showEntry(this.currentBackForwardEntry);
244             this.dispatchEventToListeners(WebInspector.ContentViewContainer.Event.CurrentContentViewDidChange);
245         }
246     },
247
248     closeAllContentViewsOfPrototype: function(constructor)
249     {
250         if (!this._backForwardList.length) {
251             console.assert(this._currentIndex === -1);
252             return;
253         }
254
255         // Do a check to see if all the content views are instances of this prototype.
256         // If they all are we can use the quicker closeAllContentViews method.
257         var allSamePrototype = true;
258         for (var i = this._backForwardList.length - 1; i >= 0; --i) {
259             if (!(this._backForwardList[i].contentView instanceof constructor)) {
260                 allSamePrototype = false;
261                 break;
262             }
263         }
264
265         if (allSamePrototype) {
266             this.closeAllContentViews();
267             return;
268         }
269
270         var oldCurrentContentView = this.currentContentView;
271
272         var backForwardListDidChange = false;
273         // Hide and disassociate with all the content views that are instances of the constructor.
274         for (var i = this._backForwardList.length - 1; i >= 0; --i) {
275             var entry = this._backForwardList[i];
276             if (!(entry.contentView instanceof constructor))
277                 continue;
278
279             if (entry.contentView === oldCurrentContentView)
280                 this._hideEntry(entry);
281
282             if (this._currentIndex >= i) {
283                 // Decrement the currentIndex since we will remove an item in the back/forward array
284                 // that it the current index or comes before it.
285                 --this._currentIndex;
286             }
287
288             this._disassociateFromContentView(entry.contentView);
289
290             // Remove the item from the back/forward list.
291             this._backForwardList.splice(i, 1);
292             backForwardListDidChange = true;
293         }
294
295         var currentEntry = this.currentBackForwardEntry;
296         console.assert(currentEntry || (!currentEntry && this._currentIndex === -1));
297
298         if (currentEntry && currentEntry.contentView !== oldCurrentContentView || backForwardListDidChange) {
299             this._showEntry(currentEntry);
300             this.dispatchEventToListeners(WebInspector.ContentViewContainer.Event.CurrentContentViewDidChange);
301         }
302     },
303
304     closeAllContentViews: function()
305     {
306         if (!this._backForwardList.length) {
307             console.assert(this._currentIndex === -1);
308             return;
309         }
310
311         // Hide and disassociate with all the content views.
312         for (var i = 0; i < this._backForwardList.length; ++i) {
313             var entry = this._backForwardList[i];
314             if (i === this._currentIndex)
315                 this._hideEntry(entry);
316             this._disassociateFromContentView(entry.contentView);
317         }
318
319         this._backForwardList = [];
320         this._currentIndex = -1;
321
322         this.dispatchEventToListeners(WebInspector.ContentViewContainer.Event.CurrentContentViewDidChange);
323     },
324
325     canGoBack: function()
326     {
327         return this._currentIndex > 0;
328     },
329
330     canGoForward: function()
331     {
332         return this._currentIndex < this._backForwardList.length - 1;
333     },
334
335     goBack: function()
336     {
337         if (!this.canGoBack())
338             return;
339         this.showBackForwardEntryForIndex(this._currentIndex - 1);
340     },
341
342     goForward: function()
343     {
344         if (!this.canGoForward())
345             return;
346         this.showBackForwardEntryForIndex(this._currentIndex + 1);
347     },
348
349     shown: function()
350     {
351         var currentEntry = this.currentBackForwardEntry;
352         if (!currentEntry)
353             return;
354
355         this._showEntry(currentEntry);
356     },
357
358     hidden: function()
359     {
360         var currentEntry = this.currentBackForwardEntry;
361         if (!currentEntry)
362             return;
363
364         this._hideEntry(currentEntry);
365     },
366
367     // Private
368
369     _addContentViewElement: function(contentView)
370     {
371         if (contentView.element.parentNode !== this._element)
372             this._element.appendChild(contentView.element);
373     },
374
375     _removeContentViewElement: function(contentView)
376     {
377         if (contentView.element.parentNode)
378             contentView.element.parentNode.removeChild(contentView.element);
379     },
380
381     _disassociateFromContentView: function(contentView)
382     {
383         console.assert(!contentView.visible);
384
385         contentView._parentContainer = null;
386
387         var representedObject = contentView.representedObject;
388         if (!representedObject || !representedObject.__contentViews)
389             return;
390
391         representedObject.__contentViews.remove(contentView);
392
393         contentView.closed();
394     },
395
396     _showEntry: function(entry)
397     {
398         console.assert(entry instanceof WebInspector.BackForwardEntry);
399
400         this._addContentViewElement(entry.contentView);
401         entry.prepareToShow();
402     },
403
404     _hideEntry: function(entry)
405     {
406         console.assert(entry instanceof WebInspector.BackForwardEntry);
407
408         entry.prepareToHide();
409         this._removeContentViewElement(entry.contentView);
410     }
411 };
412
413 WebInspector.ContentViewContainer.prototype.__proto__ = WebInspector.Object.prototype;