| /* |
| * Copyright (C) 2013 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| WebInspector.ContentViewContainer = function(element) |
| { |
| WebInspector.Object.call(this); |
| |
| this._element = element || document.createElement("div"); |
| this._element.classList.add(WebInspector.ContentViewContainer.StyleClassName); |
| |
| this._backForwardList = []; |
| this._currentIndex = -1; |
| }; |
| |
| WebInspector.ContentViewContainer.StyleClassName = "content-view-container"; |
| |
| WebInspector.ContentViewContainer.Event = { |
| CurrentContentViewDidChange: "content-view-container-current-content-view-did-change" |
| }; |
| |
| WebInspector.ContentViewContainer.prototype = { |
| constructor: WebInspector.ContentViewContainer, |
| |
| // Public |
| |
| get element() |
| { |
| return this._element; |
| }, |
| |
| get currentIndex() |
| { |
| return this._currentIndex; |
| }, |
| |
| get backForwardList() |
| { |
| return this._backForwardList; |
| }, |
| |
| get currentContentView() |
| { |
| if (this._currentIndex < 0 || this._currentIndex > this._backForwardList.length - 1) |
| return null; |
| return this._backForwardList[this._currentIndex]; |
| }, |
| |
| updateLayout: function() |
| { |
| var currentContentView = this.currentContentView; |
| if (currentContentView) |
| currentContentView.updateLayout(); |
| }, |
| |
| contentViewForRepresentedObject: function(representedObject, onlyExisting) |
| { |
| console.assert(representedObject); |
| if (!representedObject) |
| return null; |
| |
| // Iterate over all the known content views for the representedObject (if any) and find one that doesn't |
| // have a parent container or has this container as its parent. |
| var contentView = null; |
| for (var i = 0; representedObject.__contentViews && i < representedObject.__contentViews.length; ++i) { |
| var currentContentView = representedObject.__contentViews[i]; |
| if (!currentContentView._parentContainer || currentContentView._parentContainer === this) { |
| contentView = currentContentView; |
| break; |
| } |
| } |
| |
| console.assert(!contentView || contentView instanceof WebInspector.ContentView); |
| if (contentView instanceof WebInspector.ContentView) |
| return contentView; |
| |
| // Return early to avoid creating a new content view when onlyExisting is true. |
| if (onlyExisting) |
| return null; |
| |
| try { |
| // No existing content view found, make a new one. |
| contentView = new WebInspector.ContentView(representedObject); |
| } catch (e) { |
| console.error(e); |
| return null; |
| } |
| |
| // Remember this content view for future calls. |
| if (!representedObject.__contentViews) |
| representedObject.__contentViews = []; |
| representedObject.__contentViews.push(contentView); |
| |
| return contentView; |
| }, |
| |
| showContentViewForRepresentedObject: function(representedObject) |
| { |
| var contentView = this.contentViewForRepresentedObject(representedObject); |
| if (!contentView) |
| return null; |
| |
| this.showContentView(contentView); |
| |
| return contentView; |
| }, |
| |
| showContentView: function(contentView) |
| { |
| console.assert(contentView instanceof WebInspector.ContentView); |
| if (!(contentView instanceof WebInspector.ContentView)) |
| return null; |
| |
| // Don't allow showing a content view that is already associated with another container. |
| // Showing a content view that is already associated with this container is allowed. |
| console.assert(!contentView.parentContainer || contentView.parentContainer === this); |
| if (contentView.parentContainer && contentView.parentContainer !== this) |
| return null; |
| |
| // Don't do anything if the content view is already the current content view. |
| var currentContentView = this.currentContentView; |
| if (currentContentView === contentView) |
| return contentView; |
| |
| // Showing a content view will truncate the back/forward list after the current index and insert the content view |
| // at the end of the list. Finally, the current index will be updated to point to the end of the back/forward list. |
| |
| // Increment the current index to where we will insert the content view. |
| var newIndex = this._currentIndex + 1; |
| |
| // Insert the content view at the new index. This will remove any content views greater than or equal to the index. |
| var removedItems = this._backForwardList.splice(newIndex, this._backForwardList.length - newIndex, contentView); |
| |
| console.assert(newIndex === this._backForwardList.length - 1); |
| console.assert(this._backForwardList[newIndex] === contentView); |
| |
| // Disassociate with the removed content views. |
| for (var i = 0; i < removedItems.length; ++i) { |
| // Skip disassociation if this content view is still in the back/forward list. |
| if (this._backForwardList.contains(contentView)) |
| continue; |
| |
| this._disassociateFromContentView(removedItems[i]); |
| } |
| |
| // Associate with the new content view. |
| contentView._parentContainer = this; |
| |
| this.showBackForwardEntry(newIndex); |
| |
| return contentView; |
| }, |
| |
| showBackForwardEntry: function(index) |
| { |
| console.assert(index >= 0 && index <= this._backForwardList.length - 1); |
| if (index < 0 || index > this._backForwardList.length - 1) |
| return; |
| |
| if (this._currentIndex === index) |
| return; |
| |
| // Hide the currently visible content view. |
| var currentContentView = this.currentContentView; |
| if (currentContentView) |
| this._hideContentView(currentContentView); |
| |
| this._currentIndex = index; |
| |
| this._showContentView(this.currentContentView); |
| |
| this.dispatchEventToListeners(WebInspector.ContentViewContainer.Event.CurrentContentViewDidChange); |
| }, |
| |
| replaceContentView: function(oldContentView, newContentView) |
| { |
| console.assert(oldContentView instanceof WebInspector.ContentView); |
| if (!(oldContentView instanceof WebInspector.ContentView)) |
| return; |
| |
| console.assert(newContentView instanceof WebInspector.ContentView); |
| if (!(newContentView instanceof WebInspector.ContentView)) |
| return; |
| |
| console.assert(oldContentView.parentContainer === this); |
| if (oldContentView.parentContainer !== this) |
| return; |
| |
| console.assert(!newContentView.parentContainer || newContentView.parentContainer === this); |
| if (newContentView.parentContainer && newContentView.parentContainer !== this) |
| return; |
| |
| var currentlyShowing = (this.currentContentView === oldContentView); |
| if (currentlyShowing) |
| this._hideContentView(oldContentView); |
| |
| // Disassociate with the old content view. |
| this._disassociateFromContentView(oldContentView); |
| |
| // Associate with the new content view. |
| newContentView._parentContainer = this; |
| |
| // Replace all occurrences of oldContentView with newContentView in the back/forward list. |
| for (var i = 0; i < this._backForwardList.length; ++i) { |
| if (this._backForwardList[i] === oldContentView) |
| this._backForwardList[i] = newContentView; |
| } |
| |
| if (currentlyShowing) { |
| this._showContentView(newContentView); |
| this.dispatchEventToListeners(WebInspector.ContentViewContainer.Event.CurrentContentViewDidChange); |
| } |
| }, |
| |
| closeAllContentViewsOfPrototype: function(constructor) |
| { |
| if (!this._backForwardList.length) { |
| console.assert(this._currentIndex === -1); |
| return; |
| } |
| |
| // Do a check to see if all the content views are instances of this prototype. |
| // If they all are we can use the quicker closeAllContentViews method. |
| var allSamePrototype = true; |
| for (var i = this._backForwardList.length - 1; i >= 0; --i) { |
| if (!(this._backForwardList[i] instanceof constructor)) { |
| allSamePrototype = false; |
| break; |
| } |
| } |
| |
| if (allSamePrototype) { |
| this.closeAllContentViews(); |
| return; |
| } |
| |
| var oldCurrentContentView = this.currentContentView; |
| |
| // Hide and disassociate with all the content views that are instances of the constructor. |
| for (var i = this._backForwardList.length - 1; i >= 0; --i) { |
| var contentView = this._backForwardList[i]; |
| if (!(contentView instanceof constructor)) |
| continue; |
| |
| if (contentView === oldCurrentContentView) |
| this._hideContentView(contentView); |
| |
| if (this._currentIndex >= i) { |
| // Decrement the currentIndex since we will remove an item in the back/forward array |
| // that it the current index or comes before it. |
| --this._currentIndex; |
| } |
| |
| this._disassociateFromContentView(contentView); |
| |
| // Remove the item from the back/forward list. |
| this._backForwardList.splice(i, 1); |
| } |
| |
| var currentContentView = this.currentContentView; |
| console.assert(currentContentView || (!currentContentView && this._currentIndex === -1)); |
| |
| if (currentContentView && currentContentView !== oldCurrentContentView) { |
| this._showContentView(currentContentView); |
| this.dispatchEventToListeners(WebInspector.ContentViewContainer.Event.CurrentContentViewDidChange); |
| } |
| }, |
| |
| closeAllContentViews: function() |
| { |
| if (!this._backForwardList.length) { |
| console.assert(this._currentIndex === -1); |
| return; |
| } |
| |
| // Hide and disassociate with all the content views. |
| for (var i = 0; i < this._backForwardList.length; ++i) { |
| var contentView = this._backForwardList[i]; |
| if (i === this._currentIndex) |
| this._hideContentView(contentView); |
| this._disassociateFromContentView(contentView); |
| } |
| |
| this._backForwardList = []; |
| this._currentIndex = -1; |
| |
| this.dispatchEventToListeners(WebInspector.ContentViewContainer.Event.CurrentContentViewDidChange); |
| }, |
| |
| canGoBack: function() |
| { |
| return this._currentIndex > 0; |
| }, |
| |
| canGoForward: function() |
| { |
| return this._currentIndex < this._backForwardList.length - 1; |
| }, |
| |
| goBack: function() |
| { |
| if (!this.canGoBack()) |
| return; |
| this.showBackForwardEntry(this._currentIndex - 1); |
| }, |
| |
| goForward: function() |
| { |
| if (!this.canGoForward()) |
| return; |
| this.showBackForwardEntry(this._currentIndex + 1); |
| }, |
| |
| shown: function() |
| { |
| var currentContentView = this.currentContentView; |
| if (!currentContentView) |
| return; |
| |
| this._showContentView(currentContentView); |
| }, |
| |
| hidden: function() |
| { |
| var currentContentView = this.currentContentView; |
| if (!currentContentView) |
| return; |
| |
| this._hideContentView(currentContentView); |
| }, |
| |
| // Private |
| |
| _addContentViewElement: function(contentView) |
| { |
| if (contentView.element.parentNode !== this._element) |
| this._element.appendChild(contentView.element); |
| }, |
| |
| _removeContentViewElement: function(contentView) |
| { |
| if (contentView.element.parentNode) |
| contentView.element.parentNode.removeChild(contentView.element); |
| }, |
| |
| _disassociateFromContentView: function(contentView) |
| { |
| console.assert(!contentView.visible); |
| |
| contentView._parentContainer = null; |
| |
| var representedObject = contentView.representedObject; |
| if (!representedObject || !representedObject.__contentViews) |
| return; |
| |
| representedObject.__contentViews.remove(contentView); |
| |
| contentView.closed(); |
| }, |
| |
| _saveScrollPositionsForContentView: function(contentView) |
| { |
| var scrollableElements = contentView.scrollableElements || []; |
| for (var i = 0; i < scrollableElements.length; ++i) { |
| var element = scrollableElements[i]; |
| if (!element) |
| continue; |
| if (contentView.shouldKeepElementsScrolledToBottom) |
| element._savedIsScrolledToBottom = element.isScrolledToBottom(); |
| element._savedScrollTop = element.scrollTop; |
| element._savedScrollLeft = element.scrollLeft; |
| } |
| }, |
| |
| _restoreScrollPositionsForContentView: function(contentView) |
| { |
| var scrollableElements = contentView.scrollableElements || []; |
| for (var i = 0; i < scrollableElements.length; ++i) { |
| var element = scrollableElements[i]; |
| if (!element) |
| continue; |
| |
| // Restore the top scroll position by either scrolling to the bottom or to the saved position. |
| element.scrollTop = element._savedIsScrolledToBottom ? element.scrollHeight : element._savedScrollTop; |
| |
| // Don't restore the left scroll position when scrolled to the bottom. This way the when content changes |
| // the user won't be left in a weird horizontal position. |
| element.scrollLeft = element._savedIsScrolledToBottom ? 0 : element._savedScrollLeft; |
| } |
| }, |
| |
| _showContentView: function(contentView) |
| { |
| if (contentView.visible) |
| return; |
| |
| this._addContentViewElement(contentView); |
| |
| this._prepareContentViewToShow(contentView); |
| }, |
| |
| _prepareContentViewToShow: function(contentView) |
| { |
| this._restoreScrollPositionsForContentView(contentView); |
| |
| contentView.visible = true; |
| contentView.shown(); |
| contentView.updateLayout(); |
| }, |
| |
| _hideContentView: function(contentView) |
| { |
| if (!contentView.visible) |
| return; |
| |
| this._prepareContentViewToHide(contentView); |
| |
| this._removeContentViewElement(contentView); |
| }, |
| |
| _prepareContentViewToHide: function(contentView) |
| { |
| contentView.visible = false; |
| contentView.hidden(); |
| |
| this._saveScrollPositionsForContentView(contentView); |
| } |
| }; |
| |
| WebInspector.ContentViewContainer.prototype.__proto__ = WebInspector.Object.prototype; |