blob: 109e8d73adac07ad179dc2c54185f5a94b426670 [file] [log] [blame]
/*
* Copyright (C) 2013–2021 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.
*/
WI.ContentViewContainer = class ContentViewContainer extends WI.View
{
constructor({disableBackForwardNavigation} = {})
{
super();
this._disableBackForwardNavigation = !!disableBackForwardNavigation;
this.element.classList.add("content-view-container");
this._backForwardList = [];
this._currentIndex = -1;
}
// Public
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].contentView;
}
get currentBackForwardEntry()
{
if (this._currentIndex < 0 || this._currentIndex > this._backForwardList.length - 1)
return null;
return this._backForwardList[this._currentIndex];
}
contentViewForRepresentedObject(representedObject, onlyExisting, extraArguments)
{
return WI.ContentView.contentViewForRepresentedObject(representedObject, onlyExisting, extraArguments);
}
showContentViewForRepresentedObject(representedObject, extraArguments)
{
var contentView = this.contentViewForRepresentedObject(representedObject, false, extraArguments);
if (!contentView)
return null;
this.showContentView(contentView);
return contentView;
}
showContentView(contentView, cookie)
{
if (this._disableBackForwardNavigation && this.currentContentView) {
this.replaceContentView(this.currentContentView, contentView, cookie);
return;
}
console.assert(contentView instanceof WI.ContentView);
if (!(contentView instanceof WI.ContentView))
return null;
// No change.
if (contentView === this.currentContentView && !cookie)
return contentView;
// ContentViews can be shared between containers. If this content view is
// not owned by us, it may need to be transferred to this container.
if (contentView.parentContainer !== this)
this._takeOwnershipOfContentView(contentView);
let currentEntry = this.currentBackForwardEntry;
// Try to find the last entry with the same content view so we can copy it
// to preserve the last scroll positions. The supplied cookie (if any) could
// still change the scroll position afterwards, but in most cases the cookie
// is undefined, so we want to show with a state last used.
let provisionalEntry = null;
for (let i = this._backForwardList.length - 1; i >= 0; --i) {
if (this._backForwardList[i].contentView === contentView) {
provisionalEntry = this._backForwardList[i].makeCopy(cookie);
break;
}
}
if (!provisionalEntry)
provisionalEntry = new WI.BackForwardEntry(contentView, cookie);
// Don't do anything if we would have added an identical back/forward list entry.
if (provisionalEntry.isEqual(currentEntry)) {
currentEntry.prepareToShow();
return currentEntry.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.
let 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.
let removedEntries = this._backForwardList.splice(newIndex, this._backForwardList.length - newIndex, provisionalEntry);
console.assert(newIndex === this._backForwardList.length - 1);
console.assert(this._backForwardList[newIndex] === provisionalEntry);
// Disassociate with the removed content views.
for (let i = 0; i < removedEntries.length; ++i) {
// Skip disassociation if this content view is still in the back/forward list.
let shouldDissociateContentView = !this._backForwardList.some((existingEntry) => existingEntry.contentView === removedEntries[i].contentView);
if (shouldDissociateContentView)
this._disassociateFromContentView(removedEntries[i].contentView, removedEntries[i].tombstone);
}
// Associate with the new content view.
contentView._parentContainer = this;
this.showBackForwardEntryForIndex(newIndex);
console.assert(!this._disableBackForwardNavigation || this._backForwardList.length <= 1);
return contentView;
}
showBackForwardEntryForIndex(index)
{
console.assert(index >= 0 && index <= this._backForwardList.length - 1);
if (index < 0 || index > this._backForwardList.length - 1)
return;
if (this._currentIndex === index)
return;
var previousEntry = this.currentBackForwardEntry;
this._currentIndex = index;
var currentEntry = this.currentBackForwardEntry;
console.assert(currentEntry);
if (previousEntry && (!currentEntry.contentView.isAttached || previousEntry.contentView !== currentEntry.contentView))
this._hideEntry(previousEntry);
this._showEntry(currentEntry);
this.dispatchEventToListeners(WI.ContentViewContainer.Event.CurrentContentViewDidChange);
}
replaceContentView(oldContentView, newContentView, newCookie)
{
console.assert(oldContentView instanceof WI.ContentView);
if (!(oldContentView instanceof WI.ContentView))
return;
console.assert(newContentView instanceof WI.ContentView);
if (!(newContentView instanceof WI.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._hideEntry(this.currentBackForwardEntry);
// Disassociate with the old content view.
this._disassociateFromContentView(oldContentView, false);
// 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].contentView === oldContentView) {
console.assert(!this._backForwardList[i].tombstone);
let currentCookie = newCookie ?? this._backForwardList[i].cookie;
this._backForwardList[i] = new WI.BackForwardEntry(newContentView, currentCookie);
}
}
this._removeIdenticalAdjacentBackForwardEntries();
// Re-show the current entry, because its content view instance was replaced.
if (currentlyShowing) {
this._showEntry(this.currentBackForwardEntry);
this.dispatchEventToListeners(WI.ContentViewContainer.Event.CurrentContentViewDidChange);
}
console.assert(!this._disableBackForwardNavigation || this._backForwardList.length <= 1);
}
closeContentView(contentViewToClose)
{
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 allSameContentView = true;
for (var i = this._backForwardList.length - 1; i >= 0; --i) {
if (this._backForwardList[i].contentView !== contentViewToClose) {
allSameContentView = false;
break;
}
}
if (allSameContentView) {
this.closeAllContentViews();
return;
}
var visibleContentView = this.currentContentView;
var backForwardListDidChange = false;
// Hide and disassociate with all the content views that are the same as contentViewToClose.
for (var i = this._backForwardList.length - 1; i >= 0; --i) {
var entry = this._backForwardList[i];
if (entry.contentView !== contentViewToClose)
continue;
if (entry.contentView === visibleContentView)
this._hideEntry(entry);
if (this._currentIndex >= i) {
// Decrement the currentIndex since we will remove an item in the back/forward array
// that is the current index or comes before it.
--this._currentIndex;
}
this._disassociateFromContentView(entry.contentView, entry.tombstone);
// Remove the item from the back/forward list.
this._backForwardList.splice(i, 1);
backForwardListDidChange = true;
}
if (backForwardListDidChange)
this._removeIdenticalAdjacentBackForwardEntries();
var currentEntry = this.currentBackForwardEntry;
console.assert(currentEntry || (!currentEntry && this._currentIndex === -1));
if (currentEntry && currentEntry.contentView !== visibleContentView || backForwardListDidChange) {
this._showEntry(currentEntry);
this.dispatchEventToListeners(WI.ContentViewContainer.Event.CurrentContentViewDidChange);
}
}
closeAllContentViews(filter)
{
console.assert(!filter || typeof filter === "function");
if (!this._backForwardList.length) {
console.assert(this._currentIndex === -1);
return;
}
var visibleContentView = this.currentContentView;
// Hide and disassociate with all the content views.
for (let i = 0; i < this._backForwardList.length; ++i) {
let entry = this._backForwardList[i];
if (filter && !filter(entry.contentView))
continue;
if (entry.contentView === visibleContentView)
this._hideEntry(entry);
this._disassociateFromContentView(entry.contentView, entry.tombstone);
}
this._backForwardList = [];
this._currentIndex = -1;
this.dispatchEventToListeners(WI.ContentViewContainer.Event.CurrentContentViewDidChange);
}
canGoBack()
{
return this._currentIndex > 0;
}
canGoForward()
{
return this._currentIndex < this._backForwardList.length - 1;
}
goBack()
{
if (!this.canGoBack())
return;
this.showBackForwardEntryForIndex(this._currentIndex - 1);
}
goForward()
{
if (!this.canGoForward())
return;
this.showBackForwardEntryForIndex(this._currentIndex + 1);
}
attached()
{
super.attached();
var currentEntry = this.currentBackForwardEntry;
if (currentEntry)
this._showEntry(currentEntry);
}
detached()
{
var currentEntry = this.currentBackForwardEntry;
if (currentEntry)
this._hideEntry(currentEntry);
super.detached();
}
// Private
_takeOwnershipOfContentView(contentView)
{
console.assert(contentView.parentContainer !== this, "We already have ownership of the ContentView");
if (contentView.parentContainer === this)
return;
if (contentView.parentContainer)
contentView.parentContainer._placeTombstonesForContentView(contentView);
contentView._parentContainer = this;
this._clearTombstonesForContentView(contentView);
// These contentView navigation items need to move to the new content browser.
contentView.dispatchEventToListeners(WI.ContentView.Event.NavigationItemsDidChange);
}
_placeTombstonesForContentView(contentView)
{
console.assert(contentView.parentContainer === this);
// Ensure another ContentViewContainer doesn't close this ContentView while we still have it.
let tombstoneContentViewContainers = this._tombstoneContentViewContainersForContentView(contentView);
console.assert(!tombstoneContentViewContainers.includes(this));
let visibleContentView = this.currentContentView;
for (let entry of this._backForwardList) {
if (entry.contentView !== contentView)
continue;
if (entry.contentView === visibleContentView) {
this._hideEntry(entry);
visibleContentView = null;
}
console.assert(!entry.tombstone);
entry.tombstone = true;
tombstoneContentViewContainers.push(this);
}
}
_clearTombstonesForContentView(contentView)
{
console.assert(contentView.parentContainer === this);
let tombstoneContentViewContainers = this._tombstoneContentViewContainersForContentView(contentView);
tombstoneContentViewContainers.removeAll(this);
for (let entry of this._backForwardList) {
if (entry.contentView !== contentView)
continue;
console.assert(entry.tombstone);
entry.tombstone = false;
}
}
_disassociateFromContentView(contentView, isTombstone)
{
// Just remove one of our tombstone back references.
// There may be other back/forward entries that need a reference.
if (isTombstone) {
let tombstoneContentViewContainers = this._tombstoneContentViewContainersForContentView(contentView);
tombstoneContentViewContainers.remove(this);
return;
}
if (contentView.constructor.shouldNotRemoveFromDOMWhenHidden()) {
// Hidden/non-visible extension tabs must remain attached to the DOM to avoid reloading.
if (!contentView.visible)
return;
if (contentView.isAttached)
this.removeSubview(contentView);
}
console.assert(!contentView.isAttached);
if (!contentView._parentContainer)
return;
contentView._parentContainer = null;
// If another ContentViewContainer has tombstones for this, just transfer
// ownership to that ContentViewContainer and avoid closing the ContentView.
// We don't care who we transfer this to, so just use the first.
let tombstoneContentViewContainers = this._tombstoneContentViewContainersForContentView(contentView);
if (tombstoneContentViewContainers && tombstoneContentViewContainers.length) {
tombstoneContentViewContainers[0]._takeOwnershipOfContentView(contentView);
return;
}
contentView.closed();
if (contentView.representedObject)
WI.ContentView.closedContentViewForRepresentedObject(contentView.representedObject);
}
_showEntry(entry)
{
console.assert(entry instanceof WI.BackForwardEntry);
// We may be showing a tombstone from a BackForward list or when re-showing a container
// that had previously had the content view transferred away from it.
// Take over the ContentView.
if (entry.tombstone) {
this._takeOwnershipOfContentView(entry.contentView);
console.assert(!entry.tombstone);
}
if (!this.subviews.includes(entry.contentView))
this.addSubview(entry.contentView);
else if (entry.contentView.constructor.shouldNotRemoveFromDOMWhenHidden()) {
entry.contentView.visible = true;
entry.contentView._didMoveToParent(this);
}
entry.prepareToShow();
}
_hideEntry(entry)
{
console.assert(entry instanceof WI.BackForwardEntry);
// If this was a tombstone, the content view should already have been
// hidden when we placed the tombstone.
if (entry.tombstone)
return;
entry.prepareToHide();
if (this.subviews.includes(entry.contentView)) {
if (entry.contentView.constructor.shouldNotRemoveFromDOMWhenHidden()) {
entry.contentView.visible = false;
entry.contentView._didMoveToParent(null);
} else
this.removeSubview(entry.contentView);
}
}
_tombstoneContentViewContainersForContentView(contentView)
{
let tombstoneContentViewContainers = contentView[WI.ContentViewContainer.TombstoneContentViewContainersSymbol];
if (!tombstoneContentViewContainers)
tombstoneContentViewContainers = contentView[WI.ContentViewContainer.TombstoneContentViewContainersSymbol] = [];
return tombstoneContentViewContainers;
}
_removeIdenticalAdjacentBackForwardEntries()
{
if (this._backForwardList.length < 2)
return;
let previousEntry;
for (let i = this._backForwardList.length - 1; i >= 0; --i) {
let entry = this._backForwardList[i];
if (!entry.isEqual(previousEntry)) {
previousEntry = entry;
continue;
}
if (this._currentIndex >= i) {
// Decrement the currentIndex since we will remove an item in the back/forward array
// that is the current index or comes before it.
--this._currentIndex;
}
this._backForwardList.splice(i, 1);
}
}
};
WI.ContentViewContainer.Event = {
CurrentContentViewDidChange: "content-view-container-current-content-view-did-change"
};
WI.ContentViewContainer.TombstoneContentViewContainersSymbol = Symbol("content-view-container-tombstone-content-view-containers");