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
26WebInspector.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
37WebInspector.ContentViewContainer.StyleClassName = "content-view-container";
38
39WebInspector.ContentViewContainer.Event = {
40    CurrentContentViewDidChange: "content-view-container-current-content-view-did-change"
41};
42
43WebInspector.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)
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);
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
413WebInspector.ContentViewContainer.prototype.__proto__ = WebInspector.Object.prototype;
414