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