533 lines
19 KiB
JavaScript
533 lines
19 KiB
JavaScript
/*
|
||
* 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");
|