| /* |
| * Copyright (C) 2013, 2015 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.ContentBrowser = class ContentBrowser extends WI.View |
| { |
| constructor(element, delegate, {hideBackForwardButtons, disableBackForwardNavigation, disableFindBanner, flexibleNavigationItem, contentViewNavigationItemGroup} = {}) |
| { |
| super(element); |
| |
| this.element.classList.add("content-browser"); |
| |
| this._navigationBar = new WI.NavigationBar; |
| this.addSubview(this._navigationBar); |
| |
| this._contentViewContainer = new WI.ContentViewContainer({disableBackForwardNavigation}); |
| this._contentViewContainer.addEventListener(WI.ContentViewContainer.Event.CurrentContentViewDidChange, this._currentContentViewDidChange, this); |
| this.addSubview(this._contentViewContainer); |
| |
| if (!hideBackForwardButtons) { |
| let isRTL = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL; |
| |
| let goBack = () => { this.goBack(); }; |
| let goForward = () => { this.goForward(); }; |
| |
| let backShortcutKey = isRTL ? WI.KeyboardShortcut.Key.Right : WI.KeyboardShortcut.Key.Left; |
| let forwardShortcutKey = isRTL ? WI.KeyboardShortcut.Key.Left : WI.KeyboardShortcut.Key.Right; |
| this._backKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl | WI.KeyboardShortcut.Modifier.Control, backShortcutKey, goBack, this.element); |
| this._forwardKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl | WI.KeyboardShortcut.Modifier.Control, forwardShortcutKey, goForward, this.element); |
| |
| let leftArrow = "Images/BackForwardArrows.svg#left-arrow-mask"; |
| let rightArrow = "Images/BackForwardArrows.svg#right-arrow-mask"; |
| let backButtonImage = isRTL ? rightArrow : leftArrow; |
| let forwardButtonImage = isRTL ? leftArrow : rightArrow; |
| |
| this._backNavigationItem = new WI.ButtonNavigationItem("back", WI.UIString("Back (%s)").format(this._backKeyboardShortcut.displayName), backButtonImage, 8, 13); |
| this._backNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, goBack, this); |
| this._backNavigationItem.enabled = false; |
| |
| this._forwardNavigationItem = new WI.ButtonNavigationItem("forward", WI.UIString("Forward (%s)").format(this._forwardKeyboardShortcut.displayName), forwardButtonImage, 8, 13); |
| this._forwardNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, goForward, this); |
| this._forwardNavigationItem.enabled = false; |
| |
| let navigationButtonsGroup = new WI.GroupNavigationItem([this._backNavigationItem, this._forwardNavigationItem]); |
| this._navigationBar.addNavigationItem(navigationButtonsGroup); |
| } |
| |
| if (!disableFindBanner) { |
| this._findBanner = new WI.FindBanner(this); |
| this._findBanner.addEventListener(WI.FindBanner.Event.DidShow, this._findBannerDidShow, this); |
| this._findBanner.addEventListener(WI.FindBanner.Event.DidHide, this._findBannerDidHide, this); |
| } |
| |
| this._hierarchicalPathNavigationItem = new WI.HierarchicalPathNavigationItem; |
| this._hierarchicalPathNavigationItem.addEventListener(WI.HierarchicalPathNavigationItem.Event.PathComponentWasSelected, this._hierarchicalPathComponentWasSelected, this); |
| this._navigationBar.addNavigationItem(this._hierarchicalPathNavigationItem); |
| |
| this._contentViewSelectionPathNavigationItem = new WI.HierarchicalPathNavigationItem; |
| |
| this._flexibleNavigationItem = flexibleNavigationItem || new WI.FlexibleSpaceNavigationItem; |
| this._navigationBar.addNavigationItem(this._flexibleNavigationItem); |
| |
| this._currentContentViewNavigationItemsGroup = contentViewNavigationItemGroup || null; |
| |
| WI.ContentView.addEventListener(WI.ContentView.Event.SelectionPathComponentsDidChange, this._contentViewSelectionPathComponentDidChange, this); |
| WI.ContentView.addEventListener(WI.ContentView.Event.SupplementalRepresentedObjectsDidChange, this._contentViewSupplementalRepresentedObjectsDidChange, this); |
| WI.ContentView.addEventListener(WI.ContentView.Event.NumberOfSearchResultsDidChange, this._contentViewNumberOfSearchResultsDidChange, this); |
| WI.ContentView.addEventListener(WI.ContentView.Event.NavigationItemsDidChange, this._contentViewNavigationItemsDidChange, this); |
| |
| this._delegate = delegate || null; |
| |
| this._currentContentViewNavigationItems = []; |
| |
| this._dispatchCurrentRepresentedObjectsDidChangeDebouncer = new Debouncer(() => { |
| this.dispatchEventToListeners(WI.ContentBrowser.Event.CurrentRepresentedObjectsDidChange); |
| }); |
| } |
| |
| // Public |
| |
| get navigationBar() |
| { |
| return this._navigationBar; |
| } |
| |
| get contentViewContainer() |
| { |
| return this._contentViewContainer; |
| } |
| |
| get delegate() |
| { |
| return this._delegate; |
| } |
| |
| set delegate(newDelegate) |
| { |
| this._delegate = newDelegate || null; |
| } |
| |
| get currentContentView() |
| { |
| return this._contentViewContainer.currentContentView; |
| } |
| |
| get currentRepresentedObjects() |
| { |
| var representedObjects = []; |
| |
| var lastComponent = this._hierarchicalPathNavigationItem.lastComponent; |
| if (lastComponent && lastComponent.representedObject) |
| representedObjects.push(lastComponent.representedObject); |
| |
| lastComponent = this._contentViewSelectionPathNavigationItem.lastComponent; |
| if (lastComponent && lastComponent.representedObject) |
| representedObjects.push(lastComponent.representedObject); |
| |
| var currentContentView = this.currentContentView; |
| if (currentContentView) { |
| var supplementalRepresentedObjects = currentContentView.supplementalRepresentedObjects; |
| if (supplementalRepresentedObjects && supplementalRepresentedObjects.length) |
| representedObjects.pushAll(supplementalRepresentedObjects); |
| } |
| |
| return representedObjects; |
| } |
| |
| showContentViewForRepresentedObject(representedObject, cookie, extraArguments) |
| { |
| var contentView = this.contentViewForRepresentedObject(representedObject, false, extraArguments); |
| return this._contentViewContainer.showContentView(contentView, cookie); |
| } |
| |
| showContentView(contentView, cookie) |
| { |
| return this._contentViewContainer.showContentView(contentView, cookie); |
| } |
| |
| contentViewForRepresentedObject(representedObject, onlyExisting, extraArguments) |
| { |
| return this._contentViewContainer.contentViewForRepresentedObject(representedObject, onlyExisting, extraArguments); |
| } |
| |
| updateHierarchicalPathForCurrentContentView() |
| { |
| var currentContentView = this.currentContentView; |
| this._updateHierarchicalPathNavigationItem(currentContentView ? currentContentView.representedObject : null); |
| } |
| |
| canGoBack() |
| { |
| var currentContentView = this.currentContentView; |
| if (currentContentView && currentContentView.canGoBack()) |
| return true; |
| return this._contentViewContainer.canGoBack(); |
| } |
| |
| canGoForward() |
| { |
| var currentContentView = this.currentContentView; |
| if (currentContentView && currentContentView.canGoForward()) |
| return true; |
| return this._contentViewContainer.canGoForward(); |
| } |
| |
| goBack() |
| { |
| var currentContentView = this.currentContentView; |
| if (currentContentView && currentContentView.canGoBack()) { |
| currentContentView.goBack(); |
| this._updateBackForwardButtons(); |
| return; |
| } |
| |
| this._contentViewContainer.goBack(); |
| |
| // The _updateBackForwardButtons function is called by _currentContentViewDidChange, |
| // so it does not need to be called here. |
| } |
| |
| goForward() |
| { |
| var currentContentView = this.currentContentView; |
| if (currentContentView && currentContentView.canGoForward()) { |
| currentContentView.goForward(); |
| this._updateBackForwardButtons(); |
| return; |
| } |
| |
| this._contentViewContainer.goForward(); |
| |
| // The _updateBackForwardButtons function is called by _currentContentViewDidChange, |
| // so it does not need to be called here. |
| } |
| |
| showFindBanner() |
| { |
| if (!this._findBanner) |
| return; |
| |
| var currentContentView = this.currentContentView; |
| if (!currentContentView || !currentContentView.supportsSearch) |
| return; |
| |
| if (currentContentView.supportsCustomFindBanner) { |
| currentContentView.showCustomFindBanner(); |
| return; |
| } |
| |
| this._findBanner.show(); |
| } |
| |
| attached() |
| { |
| super.attached(); |
| |
| this._updateContentViewSelectionPathNavigationItem(this.currentContentView); |
| this.updateHierarchicalPathForCurrentContentView(); |
| } |
| |
| // Global ContentBrowser KeyboardShortcut handlers |
| |
| handlePopulateFindShortcut() |
| { |
| let currentContentView = this.currentContentView; |
| if (!currentContentView?.supportsSearch) |
| return; |
| |
| if (!WI.updateFindString(currentContentView.searchQueryWithSelection())) |
| return; |
| |
| this._findBanner.searchQuery = WI.findString; |
| |
| currentContentView.performSearch(this._findBanner.searchQuery); |
| } |
| |
| async handleFindNextShortcut() |
| { |
| if (!this._findBanner.showing && this._findBanner.searchQuery !== WI.findString) { |
| let searchQuery = WI.findString; |
| this._findBanner.searchQuery = searchQuery; |
| |
| let currentContentView = this.currentContentView; |
| if (currentContentView?.supportsSearch) { |
| currentContentView.performSearch(this._findBanner.searchQuery); |
| await currentContentView.awaitEvent(WI.ContentView.Event.NumberOfSearchResultsDidChange, this); |
| if (this._findBanner.searchQuery !== searchQuery || this.currentContentView !== currentContentView) |
| return; |
| } |
| } |
| |
| this.findBannerRevealNextResult(this._findBanner); |
| } |
| |
| async handleFindPreviousShortcut() |
| { |
| if (!this._findBanner.showing && this._findBanner.searchQuery !== WI.findString) { |
| let searchQuery = WI.findString; |
| this._findBanner.searchQuery = searchQuery; |
| |
| let currentContentView = this.currentContentView; |
| if (currentContentView?.supportsSearch) { |
| currentContentView.performSearch(this._findBanner.searchQuery); |
| await currentContentView.awaitEvent(WI.ContentView.Event.NumberOfSearchResultsDidChange, this); |
| if (this._findBanner.searchQuery !== searchQuery || this.currentContentView !== currentContentView) |
| return; |
| } |
| } |
| |
| this.findBannerRevealPreviousResult(this._findBanner); |
| } |
| |
| // FindBanner delegate |
| |
| findBannerPerformSearch(findBanner, query) |
| { |
| let currentContentView = this.currentContentView; |
| if (!currentContentView || !currentContentView.supportsSearch) |
| return; |
| |
| currentContentView.performSearch(query); |
| } |
| |
| findBannerSearchCleared(findBanner) |
| { |
| let currentContentView = this.currentContentView; |
| if (!currentContentView || !currentContentView.supportsSearch) |
| return; |
| |
| currentContentView.searchCleared(); |
| } |
| |
| findBannerRevealPreviousResult(findBanner) |
| { |
| let currentContentView = this.currentContentView; |
| if (!currentContentView || !currentContentView.supportsSearch) |
| return; |
| |
| currentContentView.revealPreviousSearchResult(!findBanner.showing); |
| } |
| |
| findBannerRevealNextResult(findBanner) |
| { |
| let currentContentView = this.currentContentView; |
| if (!currentContentView || !currentContentView.supportsSearch) |
| return; |
| |
| currentContentView.revealNextSearchResult(!findBanner.showing); |
| } |
| |
| // Private |
| |
| _findBannerDidShow(event) |
| { |
| var currentContentView = this.currentContentView; |
| if (!currentContentView || !currentContentView.supportsSearch) |
| return; |
| |
| currentContentView.automaticallyRevealFirstSearchResult = true; |
| if (this._findBanner.searchQuery !== "") |
| currentContentView.performSearch(this._findBanner.searchQuery); |
| } |
| |
| _findBannerDidHide(event) |
| { |
| var currentContentView = this.currentContentView; |
| if (!currentContentView || !currentContentView.supportsSearch) |
| return; |
| |
| currentContentView.automaticallyRevealFirstSearchResult = false; |
| currentContentView.searchHidden(); |
| } |
| |
| _contentViewNumberOfSearchResultsDidChange(event) |
| { |
| if (!this._findBanner) |
| return; |
| |
| if (event.target !== this.currentContentView) |
| return; |
| |
| this._findBanner.numberOfResults = this.currentContentView.numberOfSearchResults; |
| } |
| |
| _updateHierarchicalPathNavigationItem(representedObject) |
| { |
| if (!this.delegate || typeof this.delegate.contentBrowserTreeElementForRepresentedObject !== "function") |
| return; |
| |
| var treeElement = representedObject ? this.delegate.contentBrowserTreeElementForRepresentedObject(this, representedObject) : null; |
| var pathComponents = []; |
| |
| while (treeElement && !treeElement.root) { |
| var pathComponent = new WI.GeneralTreeElementPathComponent(treeElement); |
| pathComponents.unshift(pathComponent); |
| treeElement = treeElement.parent; |
| } |
| |
| this._hierarchicalPathNavigationItem.components = pathComponents; |
| } |
| |
| _updateContentViewSelectionPathNavigationItem(contentView) |
| { |
| var selectionPathComponents = contentView ? contentView.selectionPathComponents || [] : []; |
| this._contentViewSelectionPathNavigationItem.components = selectionPathComponents; |
| |
| if (this._currentContentViewNavigationItemsGroup) |
| return; |
| |
| if (!selectionPathComponents.length) { |
| this._hierarchicalPathNavigationItem.alwaysShowLastPathComponentSeparator = false; |
| this._navigationBar.removeNavigationItem(this._contentViewSelectionPathNavigationItem); |
| return; |
| } |
| |
| // Insert the _contentViewSelectionPathNavigationItem after the _hierarchicalPathNavigationItem, if needed. |
| if (!this._navigationBar.navigationItems.includes(this._contentViewSelectionPathNavigationItem)) { |
| var hierarchicalPathItemIndex = this._navigationBar.navigationItems.indexOf(this._hierarchicalPathNavigationItem); |
| console.assert(hierarchicalPathItemIndex !== -1); |
| this._navigationBar.insertNavigationItem(this._contentViewSelectionPathNavigationItem, hierarchicalPathItemIndex + 1); |
| this._hierarchicalPathNavigationItem.alwaysShowLastPathComponentSeparator = true; |
| } |
| } |
| |
| _updateBackForwardButtons() |
| { |
| if (!this._backNavigationItem || !this._forwardNavigationItem) |
| return; |
| |
| this._backNavigationItem.enabled = this.canGoBack(); |
| this._forwardNavigationItem.enabled = this.canGoForward(); |
| } |
| |
| _updateContentViewNavigationItems(forceUpdate) |
| { |
| let currentContentView = this.currentContentView; |
| if (!currentContentView) { |
| this._removeAllNavigationItems(); |
| this._currentContentViewNavigationItems = []; |
| if (this._currentContentViewNavigationItemsGroup) |
| this._currentContentViewNavigationItems.push(this._contentViewSelectionPathNavigationItem); |
| return; |
| } |
| |
| // If the ContentView is a tombstone within our ContentViewContainer, don't steal its navigationItems. |
| // Only the owning ContentBrowser should have the navigationItems. |
| if (currentContentView.parentContainer !== this._contentViewContainer) |
| return; |
| |
| if (!forceUpdate) { |
| let previousItems = this._currentContentViewNavigationItems.filter((item) => !(item instanceof WI.DividerNavigationItem)); |
| let isUnchanged = Array.shallowEqual(previousItems, currentContentView.navigationItems); |
| |
| if (isUnchanged) |
| return; |
| } |
| |
| this._removeAllNavigationItems(); |
| |
| let navigationBar = this.navigationBar; |
| let insertionIndex = navigationBar.navigationItems.indexOf(this._flexibleNavigationItem) + 1; |
| console.assert(insertionIndex >= 0); |
| |
| // Keep track of items we'll be adding to the navigation bar. |
| let newNavigationItems = []; |
| let shouldInsert = !this._currentContentViewNavigationItemsGroup; |
| |
| // Go through each of the items of the new content view and add a divider before them. |
| currentContentView.navigationItems.forEach(function(navigationItem, index) { |
| if (shouldInsert) |
| navigationBar.insertNavigationItem(navigationItem, insertionIndex++); |
| newNavigationItems.push(navigationItem); |
| }); |
| |
| if (this._currentContentViewNavigationItemsGroup) |
| this._currentContentViewNavigationItemsGroup.navigationItems = [this._contentViewSelectionPathNavigationItem].concat(newNavigationItems); |
| |
| // Remember the navigation items we inserted so we can remove them |
| // for the next content view. |
| this._currentContentViewNavigationItems = newNavigationItems; |
| } |
| |
| _removeAllNavigationItems() |
| { |
| if (this._currentContentViewNavigationItemsGroup) |
| this._currentContentViewNavigationItemsGroup.navigationItems = []; |
| else { |
| for (let navigationItem of this._currentContentViewNavigationItems) { |
| if (navigationItem.parentNavigationBar) |
| navigationItem.parentNavigationBar.removeNavigationItem(navigationItem); |
| } |
| } |
| } |
| |
| _updateFindBanner(currentContentView) |
| { |
| if (!this._findBanner) |
| return; |
| |
| if (!currentContentView) { |
| this._findBanner.targetElement = null; |
| this._findBanner.numberOfResults = null; |
| return; |
| } |
| |
| this._findBanner.targetElement = currentContentView.element; |
| this._findBanner.numberOfResults = currentContentView.hasPerformedSearch ? currentContentView.numberOfSearchResults : null; |
| |
| if (currentContentView.supportsSearch && this._findBanner.searchQuery) { |
| currentContentView.automaticallyRevealFirstSearchResult = this._findBanner.showing; |
| currentContentView.performSearch(this._findBanner.searchQuery); |
| } |
| } |
| |
| _contentViewSelectionPathComponentDidChange(event) |
| { |
| if (event.target !== this.currentContentView) |
| return; |
| |
| // If the ContentView is a tombstone within our ContentViewContainer, do nothing. Let the owning ContentBrowser react. |
| if (event.target.parentContainer !== this._contentViewContainer) |
| return; |
| |
| this._updateContentViewSelectionPathNavigationItem(event.target); |
| this._updateBackForwardButtons(); |
| |
| this._updateContentViewNavigationItems(); |
| |
| this._navigationBar.needsLayout(); |
| |
| this._dispatchCurrentRepresentedObjectsDidChangeDebouncer.delayForTime(0); |
| } |
| |
| _contentViewSupplementalRepresentedObjectsDidChange(event) |
| { |
| if (event.target !== this.currentContentView) |
| return; |
| |
| // If the ContentView is a tombstone within our ContentViewContainer, do nothing. Let the owning ContentBrowser react. |
| if (event.target.parentContainer !== this._contentViewContainer) |
| return; |
| |
| this._dispatchCurrentRepresentedObjectsDidChangeDebouncer.delayForTime(0); |
| } |
| |
| _currentContentViewDidChange(event) |
| { |
| var currentContentView = this.currentContentView; |
| |
| this._updateHierarchicalPathNavigationItem(currentContentView ? currentContentView.representedObject : null); |
| this._updateContentViewSelectionPathNavigationItem(currentContentView); |
| this._updateBackForwardButtons(); |
| |
| this._updateContentViewNavigationItems(); |
| this._updateFindBanner(currentContentView); |
| |
| this._navigationBar.needsLayout(); |
| |
| this.dispatchEventToListeners(WI.ContentBrowser.Event.CurrentContentViewDidChange); |
| |
| this._dispatchCurrentRepresentedObjectsDidChangeDebouncer.force(); |
| } |
| |
| _contentViewNavigationItemsDidChange(event) |
| { |
| if (event.target !== this.currentContentView) |
| return; |
| |
| // If the ContentView is a tombstone within our ContentViewContainer, do nothing. Let the owning ContentBrowser react. |
| if (event.target.parentContainer !== this._contentViewContainer) |
| return; |
| |
| const forceUpdate = true; |
| this._updateContentViewNavigationItems(forceUpdate); |
| this._navigationBar.needsLayout(); |
| } |
| |
| _hierarchicalPathComponentWasSelected(event) |
| { |
| console.assert(event.data.pathComponent instanceof WI.GeneralTreeElementPathComponent); |
| |
| var treeElement = event.data.pathComponent.generalTreeElement; |
| var originalTreeElement = treeElement; |
| |
| // Some tree elements (like folders) are not viewable. Find the first descendant that is viewable. |
| while (treeElement && !WI.ContentView.isViewable(treeElement.representedObject)) |
| treeElement = treeElement.traverseNextTreeElement(false, originalTreeElement, false); |
| |
| if (!treeElement) |
| return; |
| |
| treeElement.revealAndSelect(); |
| } |
| }; |
| |
| WI.ContentBrowser.Event = { |
| CurrentRepresentedObjectsDidChange: "content-browser-current-represented-objects-did-change", |
| CurrentContentViewDidChange: "content-browser-current-content-view-did-change" |
| }; |