blob: 375a6287b13df1d420551d4871ba9d1be8de7977 [file] [log] [blame] [edit]
/*
* 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;