| /* |
| * Copyright (C) 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.TabBrowser = class TabBrowser extends WI.View |
| { |
| constructor(element, tabBar, navigationSidebar, detailsSidebar) |
| { |
| console.assert(tabBar, "Must provide a TabBar."); |
| |
| super(element); |
| |
| this.element.classList.add("tab-browser"); |
| |
| this._tabBar = tabBar; |
| this._navigationSidebar = navigationSidebar || null; |
| this._detailsSidebar = detailsSidebar || null; |
| |
| if (this._navigationSidebar) { |
| this._navigationSidebar.addEventListener(WI.Sidebar.Event.CollapsedStateDidChange, this._sidebarCollapsedStateDidChange, this); |
| this._navigationSidebar.addEventListener(WI.Sidebar.Event.WidthDidChange, this._sidebarWidthDidChange, this); |
| } |
| |
| if (this._detailsSidebar) { |
| this._detailsSidebar.addEventListener(WI.Sidebar.Event.CollapsedStateDidChange, this._sidebarCollapsedStateDidChange, this); |
| this._detailsSidebar.addEventListener(WI.Sidebar.Event.SidebarPanelSelected, this._sidebarPanelSelected, this); |
| this._detailsSidebar.addEventListener(WI.Sidebar.Event.WidthDidChange, this._sidebarWidthDidChange, this); |
| } |
| |
| this._contentViewContainer = new WI.ContentViewContainer; |
| this.addSubview(this._contentViewContainer); |
| |
| let showNextTab = () => { this._showNextTab(); }; |
| let showPreviousTab = () => { this._showPreviousTab(); }; |
| |
| let isRTL = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL; |
| |
| let nextKey1 = isRTL ? WI.KeyboardShortcut.Key.LeftCurlyBrace : WI.KeyboardShortcut.Key.RightCurlyBrace; |
| let previousKey1 = isRTL ? WI.KeyboardShortcut.Key.RightCurlyBrace : WI.KeyboardShortcut.Key.LeftCurlyBrace; |
| |
| this._showNextTabKeyboardShortcut1 = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl | WI.KeyboardShortcut.Modifier.Shift, nextKey1, showNextTab); |
| this._showPreviousTabKeyboardShortcut1 = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl | WI.KeyboardShortcut.Modifier.Shift, previousKey1, showPreviousTab); |
| |
| let nextModifier2 = isRTL ? WI.KeyboardShortcut.Modifier.Shift : 0; |
| let previousModifier2 = isRTL ? 0 : WI.KeyboardShortcut.Modifier.Shift; |
| |
| this._showNextTabKeyboardShortcut2 = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.Control | nextModifier2, WI.KeyboardShortcut.Key.Tab, showNextTab); |
| this._showPreviousTabKeyboardShortcut2 = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.Control | previousModifier2, WI.KeyboardShortcut.Key.Tab, showPreviousTab); |
| |
| let previousTabKey = isRTL ? WI.KeyboardShortcut.Key.Right : WI.KeyboardShortcut.Key.Left; |
| let nextTabKey = isRTL ? WI.KeyboardShortcut.Key.Left : WI.KeyboardShortcut.Key.Right; |
| this._previousTabKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl | WI.KeyboardShortcut.Modifier.Shift, previousTabKey, this._showPreviousTabCheckingForEditableField.bind(this)); |
| this._previousTabKeyboardShortcut.implicitlyPreventsDefault = false; |
| this._nextTabKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl | WI.KeyboardShortcut.Modifier.Shift, nextTabKey, this._showNextTabCheckingForEditableField.bind(this)); |
| this._nextTabKeyboardShortcut.implicitlyPreventsDefault = false; |
| |
| this._tabBar.addEventListener(WI.TabBar.Event.TabBarItemSelected, this._tabBarItemSelected, this); |
| this._tabBar.addEventListener(WI.TabBar.Event.TabBarItemAdded, this._tabBarItemAdded, this); |
| this._tabBar.addEventListener(WI.TabBar.Event.TabBarItemRemoved, this._tabBarItemRemoved, this); |
| |
| this._recentTabContentViews = []; |
| this._closedTabClasses = new Set; |
| } |
| |
| // Public |
| |
| get tabBar() |
| { |
| return this._tabBar; |
| } |
| |
| get navigationSidebar() |
| { |
| return this._navigationSidebar; |
| } |
| |
| get detailsSidebar() |
| { |
| return this._detailsSidebar; |
| } |
| |
| get selectedTabContentView() |
| { |
| return this._contentViewContainer.currentContentView; |
| } |
| |
| bestTabContentViewForClass(constructor) |
| { |
| console.assert(!this.selectedTabContentView || this.selectedTabContentView === this._recentTabContentViews[0]); |
| |
| for (var tabContentView of this._recentTabContentViews) { |
| if (tabContentView instanceof constructor) |
| return tabContentView; |
| } |
| |
| return null; |
| } |
| |
| bestTabContentViewForRepresentedObject(representedObject, options = {}) |
| { |
| console.assert(!this.selectedTabContentView || this.selectedTabContentView === this._recentTabContentViews[0]); |
| |
| let tabContentView = this._recentTabContentViews.find((tabContentView) => tabContentView.type === options.preferredTabType); |
| if (tabContentView && tabContentView.canShowRepresentedObject(representedObject)) |
| return tabContentView; |
| |
| for (let tabContentView of this._recentTabContentViews) { |
| if (options.ignoreSearchTab && tabContentView instanceof WI.SearchTabContentView) |
| continue; |
| if (options.ignoreNetworkTab && tabContentView instanceof WI.NetworkTabContentView) |
| continue; |
| |
| if (tabContentView.canShowRepresentedObject(representedObject)) |
| return tabContentView; |
| } |
| |
| return null; |
| } |
| |
| addTabForContentView(tabContentView, options = {}) |
| { |
| console.assert(tabContentView instanceof WI.TabContentView); |
| if (!(tabContentView instanceof WI.TabContentView)) |
| return false; |
| |
| let tabBarItem = tabContentView.tabBarItem; |
| |
| console.assert(tabBarItem instanceof WI.TabBarItem); |
| if (!(tabBarItem instanceof WI.TabBarItem)) |
| return false; |
| |
| if (tabBarItem.representedObject !== tabContentView) |
| tabBarItem.representedObject = tabContentView; |
| |
| if (tabBarItem.parentTabBar === this._tabBar) |
| return true; |
| |
| // Add the tab after the first tab content view, since the first |
| // tab content view is the currently selected one. |
| if (this._recentTabContentViews.length && this.selectedTabContentView) |
| this._recentTabContentViews.splice(1, 0, tabContentView); |
| else |
| this._recentTabContentViews.push(tabContentView); |
| |
| if (typeof options.insertionIndex === "number") |
| this._tabBar.insertTabBarItem(tabBarItem, options.insertionIndex, options); |
| else |
| this._tabBar.addTabBarItem(tabBarItem, options); |
| |
| console.assert(this._recentTabContentViews.length === this._tabBar.tabCount); |
| console.assert(!this.selectedTabContentView || this.selectedTabContentView === this._recentTabContentViews[0]); |
| |
| return true; |
| } |
| |
| showTabForContentView(tabContentView, options = {}) |
| { |
| if (!this.addTabForContentView(tabContentView, options)) |
| return false; |
| |
| this._tabBar.selectTabBarItem(tabContentView.tabBarItem, options); |
| |
| // FIXME: this is a workaround for <https://webkit.org/b/151876>. |
| // Without this extra call, we might never lay out the child tab |
| // if it has already marked itself as dirty in the same run loop |
| // as it is attached. It will schedule a layout, but when the rAF |
| // fires the parent will abort the layout because the counter is |
| // out of sync. |
| this.needsLayout(); |
| return true; |
| } |
| |
| closeTabForContentView(tabContentView, options = {}) |
| { |
| console.assert(tabContentView instanceof WI.TabContentView); |
| if (!(tabContentView instanceof WI.TabContentView)) |
| return false; |
| |
| console.assert(tabContentView.tabBarItem instanceof WI.TabBarItem); |
| if (!(tabContentView.tabBarItem instanceof WI.TabBarItem)) |
| return false; |
| |
| if (tabContentView.tabBarItem.parentTabBar !== this._tabBar) |
| return false; |
| |
| this._tabBar.removeTabBarItem(tabContentView.tabBarItem, options); |
| |
| console.assert(this._recentTabContentViews.length === this._tabBar.tabCount); |
| console.assert(!this.selectedTabContentView || this.selectedTabContentView === this._recentTabContentViews[0]); |
| |
| return true; |
| } |
| |
| // Protected |
| |
| sizeDidChange() |
| { |
| super.sizeDidChange(); |
| |
| for (let tabContentView of this._recentTabContentViews) |
| tabContentView[WI.TabBrowser.NeedsResizeLayoutSymbol] = tabContentView !== this.selectedTabContentView; |
| } |
| |
| // Private |
| |
| _tabBarItemSelected(event) |
| { |
| this._saveFocusedNodeForTabContentView(event.data.previousTabBarItem ? event.data.previousTabBarItem.representedObject : null); |
| |
| let tabContentView = this._tabBar.selectedTabBarItem ? this._tabBar.selectedTabBarItem.representedObject : null; |
| |
| if (tabContentView) { |
| let tabClass = tabContentView.constructor; |
| let shouldSaveTab = tabClass.shouldSaveTab() || tabClass.shouldPinTab(); |
| if (shouldSaveTab) { |
| this._recentTabContentViews.remove(tabContentView); |
| this._recentTabContentViews.unshift(tabContentView); |
| } |
| |
| this._contentViewContainer.showContentView(tabContentView); |
| |
| console.assert(this.selectedTabContentView); |
| console.assert(this._recentTabContentViews.length === this._tabBar.tabCount); |
| console.assert(this.selectedTabContentView === this._recentTabContentViews[0] || !shouldSaveTab); |
| } else { |
| this._contentViewContainer.closeAllContentViews(); |
| |
| console.assert(!this.selectedTabContentView); |
| } |
| |
| this._showNavigationSidebarPanelForTabContentView(tabContentView); |
| this._showDetailsSidebarPanelsForTabContentView(tabContentView); |
| |
| // If the tab browser was resized prior to showing the tab, the new tab needs to perform a resize layout. |
| if (tabContentView && tabContentView[WI.TabBrowser.NeedsResizeLayoutSymbol]) { |
| tabContentView[WI.TabBrowser.NeedsResizeLayoutSymbol] = false; |
| tabContentView.updateLayout(WI.View.LayoutReason.Resize); |
| } |
| |
| let outgoingTab = event.data.previousTabBarItem ? event.data.previousTabBarItem.representedObject : null; |
| let incomingTab = tabContentView; |
| let initiator = event.data.initiatorHint || WI.TabBrowser.TabNavigationInitiator.Unknown; |
| this.dispatchEventToListeners(WI.TabBrowser.Event.SelectedTabContentViewDidChange, {outgoingTab, incomingTab, initiator}); |
| |
| this._restoreFocusedNodeForTabContentView(tabContentView); |
| } |
| |
| _tabBarItemAdded(event) |
| { |
| let tabContentView = event.data.tabBarItem.representedObject; |
| |
| console.assert(tabContentView); |
| if (!tabContentView) |
| return; |
| |
| this._closedTabClasses.delete(tabContentView.constructor); |
| } |
| |
| _tabBarItemRemoved(event) |
| { |
| let tabContentView = event.data.tabBarItem.representedObject; |
| |
| console.assert(tabContentView); |
| if (!tabContentView) |
| return; |
| |
| this._recentTabContentViews.remove(tabContentView); |
| |
| if (tabContentView.constructor.shouldSaveTab()) |
| this._closedTabClasses.add(tabContentView.constructor); |
| |
| this._contentViewContainer.closeContentView(tabContentView); |
| |
| console.assert(this._recentTabContentViews.length === this._tabBar.tabCount); |
| console.assert(!this.selectedTabContentView || this.selectedTabContentView === this._recentTabContentViews[0]); |
| } |
| |
| _sidebarPanelSelected(event) |
| { |
| if (this._ignoreSidebarEvents) |
| return; |
| |
| var tabContentView = this.selectedTabContentView; |
| if (!tabContentView) |
| return; |
| |
| console.assert(event.target === this._detailsSidebar); |
| |
| if (tabContentView.managesDetailsSidebarPanels) |
| return; |
| |
| var selectedSidebarPanel = this._detailsSidebar.selectedSidebarPanel; |
| tabContentView.detailsSidebarSelectedPanelSetting.value = selectedSidebarPanel ? selectedSidebarPanel.identifier : null; |
| } |
| |
| _sidebarCollapsedStateDidChange(event) |
| { |
| if (this._ignoreSidebarEvents) |
| return; |
| |
| var tabContentView = this.selectedTabContentView; |
| if (!tabContentView) |
| return; |
| |
| if (event.target === this._navigationSidebar && !tabContentView.managesNavigationSidebarPanel) |
| tabContentView.navigationSidebarCollapsedSetting.value = this._navigationSidebar.collapsed; |
| else if (event.target === this._detailsSidebar && !tabContentView.managesDetailsSidebarPanels) |
| tabContentView.detailsSidebarCollapsedSetting.value = this._detailsSidebar.collapsed; |
| } |
| |
| _sidebarWidthDidChange(event) |
| { |
| if (this._ignoreSidebarEvents || !event.data) |
| return; |
| |
| let tabContentView = this.selectedTabContentView; |
| if (!tabContentView) |
| return; |
| |
| switch (event.target) { |
| case this._navigationSidebar: |
| tabContentView.navigationSidebarWidthSetting.value = event.data.newWidth; |
| break; |
| |
| case this._detailsSidebar: |
| tabContentView.detailsSidebarWidthSetting.value = event.data.newWidth; |
| break; |
| } |
| } |
| |
| _saveFocusedNodeForTabContentView(tabContentView) |
| { |
| if (!tabContentView) |
| return; |
| |
| if (!WI.isContentAreaFocused()) |
| return; |
| |
| tabContentView[WI.TabBrowser.FocusedNodeSymbol] = document.activeElement; |
| } |
| |
| _restoreFocusedNodeForTabContentView(tabContentView) |
| { |
| if (!tabContentView) |
| return; |
| |
| let node = tabContentView[WI.TabBrowser.FocusedNodeSymbol]; |
| if (node && !WI.isContentAreaFocused()) |
| node.focus(); |
| |
| tabContentView[WI.TabBrowser.FocusedNodeSymbol] = null; |
| } |
| |
| _showNavigationSidebarPanelForTabContentView(tabContentView) |
| { |
| if (!this._navigationSidebar) |
| return; |
| |
| this._ignoreSidebarEvents = true; |
| |
| this._navigationSidebar.removeSidebarPanel(0); |
| |
| console.assert(!this._navigationSidebar.sidebarPanels.length); |
| |
| if (!tabContentView) { |
| this._ignoreSidebarEvents = false; |
| return; |
| } |
| |
| if (tabContentView.navigationSidebarWidthSetting.value) |
| this._navigationSidebar.width = tabContentView.navigationSidebarWidthSetting.value; |
| |
| var navigationSidebarPanel = tabContentView.navigationSidebarPanel; |
| if (!navigationSidebarPanel) { |
| this._navigationSidebar.collapsed = true; |
| this._ignoreSidebarEvents = false; |
| return; |
| } |
| |
| if (tabContentView.managesNavigationSidebarPanel) { |
| tabContentView.showNavigationSidebarPanel(); |
| this._ignoreSidebarEvents = false; |
| return; |
| } |
| |
| this._navigationSidebar.addSidebarPanel(navigationSidebarPanel); |
| this._navigationSidebar.selectedSidebarPanel = navigationSidebarPanel; |
| |
| this._navigationSidebar.collapsed = tabContentView.navigationSidebarCollapsedSetting.value; |
| |
| this._ignoreSidebarEvents = false; |
| } |
| |
| _showDetailsSidebarPanelsForTabContentView(tabContentView) |
| { |
| if (!this._detailsSidebar) |
| return; |
| |
| this._ignoreSidebarEvents = true; |
| |
| for (var i = this._detailsSidebar.sidebarPanels.length - 1; i >= 0; --i) |
| this._detailsSidebar.removeSidebarPanel(i); |
| |
| console.assert(!this._detailsSidebar.sidebarPanels.length); |
| |
| if (!tabContentView) { |
| this._ignoreSidebarEvents = false; |
| return; |
| } |
| |
| if (tabContentView.detailsSidebarWidthSetting.value) |
| this._detailsSidebar.width = tabContentView.detailsSidebarWidthSetting.value; |
| |
| if (tabContentView.managesDetailsSidebarPanels) { |
| tabContentView.showDetailsSidebarPanels(); |
| this._ignoreSidebarEvents = false; |
| return; |
| } |
| |
| var detailsSidebarPanels = tabContentView.detailsSidebarPanels; |
| if (!detailsSidebarPanels) { |
| this._detailsSidebar.collapsed = true; |
| this._ignoreSidebarEvents = false; |
| return; |
| } |
| |
| for (var detailsSidebarPanel of detailsSidebarPanels) |
| this._detailsSidebar.addSidebarPanel(detailsSidebarPanel); |
| |
| this._detailsSidebar.selectedSidebarPanel = tabContentView.detailsSidebarSelectedPanelSetting.value || detailsSidebarPanels[0]; |
| |
| this._detailsSidebar.collapsed = tabContentView.detailsSidebarCollapsedSetting.value || !detailsSidebarPanels.length; |
| |
| this._ignoreSidebarEvents = false; |
| } |
| |
| _showPreviousTab(event) |
| { |
| this._tabBar.selectPreviousTab(); |
| } |
| |
| _showNextTab(event) |
| { |
| this._tabBar.selectNextTab(); |
| } |
| |
| _showNextTabCheckingForEditableField(event) |
| { |
| if (WI.isEventTargetAnEditableField(event)) |
| return; |
| |
| this._showNextTab(event); |
| |
| event.preventDefault(); |
| } |
| |
| _showPreviousTabCheckingForEditableField(event) |
| { |
| if (WI.isEventTargetAnEditableField(event)) |
| return; |
| |
| this._showPreviousTab(event); |
| |
| event.preventDefault(); |
| } |
| }; |
| |
| WI.TabBrowser.NeedsResizeLayoutSymbol = Symbol("needs-resize-layout"); |
| WI.TabBrowser.FocusedNodeSymbol = Symbol("focused-node"); |
| |
| WI.TabBrowser.TabNavigationInitiator = { |
| // Initiated by clicking on the TabBar UI (switching, opening, closing). |
| TabClick: "tab-browser-tab-navigation-initiator-tab-click", |
| |
| // Initiated by clicking a URL, symbol, go-to-arrow, or other link to a resource/source code location. |
| LinkClick: "tab-browser-tab-navigation-initiator-link-click", |
| |
| // Initiated by clicking miscellaneous UI (i.e., Quick Console's chevron, New Tab Tab's buttons). |
| ButtonClick: "tab-browser-tab-navigation-initiator-button-click", |
| |
| // Initiated by selecting a context menu item in Web Inspector (i.e., "Reveal in Network Tab"). |
| ContextMenu: "tab-browser-tab-navigation-initiator-context-menu", |
| |
| // Initiated by clicking a dashboard element. |
| Dashboard: "tab-browser-tab-navigation-initiator-dashboard", |
| |
| // Initiated by automatically switching tabs when a breakpoint is hit. |
| Breakpoint: "tab-browser-tab-navigation-initiator-breakpoint", |
| |
| // Initiated by inspecting a DOM element, database, or other object via Console API's inspect() or live node selection. |
| Inspect: "tab-browser-tab-navigation-initiator-inspect", |
| |
| // Initiated by keyboard shortcut (tab switching, new tab, search bar). |
| KeyboardShortcut: "tab-browser-tab-navigation-initiator-keyboard-shortcut", |
| |
| // Initiated from outside of Web Inspector (Develop Menu, _WKInspector SPI). |
| FrontendAPI: "tab-browser-tab-navigation-initiator-frontend-api", |
| |
| // Uncategorized; these should be investigated and categorized as one of the above. |
| Unknown: "tab-browser-tab-navigation-initiator-unknown" |
| } |
| |
| WI.TabBrowser.Event = { |
| SelectedTabContentViewDidChange: "tab-browser-selected-tab-content-view-did-change" |
| }; |