| /* |
| * Copyright (C) 2018 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.LegacyTabBar = class LegacyTabBar extends WI.View |
| { |
| constructor(element, tabBarItems) |
| { |
| super(element); |
| |
| this.element.classList.add("tab-bar"); |
| this.element.setAttribute("role", "tablist"); |
| this.element.addEventListener("mousedown", this._handleMouseDown.bind(this)); |
| this.element.addEventListener("click", this._handleClick.bind(this)); |
| this.element.addEventListener("mouseleave", this._handleMouseLeave.bind(this)); |
| this.element.addEventListener("contextmenu", this._handleContextMenu.bind(this)); |
| |
| this.element.createChild("div", "top-border"); |
| |
| this._tabBarItems = []; |
| this._hiddenTabBarItems = []; |
| |
| if (tabBarItems) { |
| for (let tabBarItem in tabBarItems) |
| this.addTabBarItem(tabBarItem); |
| } |
| |
| this.addTabBarItem(WI.settingsTabContentView.tabBarItem, {suppressAnimations: true}); |
| |
| this._newTabTabBarItem = new WI.PinnedTabBarItem("Images/NewTabPlus.svg", WI.UIString("Create a new tab")); |
| this._newTabTabBarItem.element.addEventListener("mouseenter", this._handleNewTabMouseEnter.bind(this)); |
| this._newTabTabBarItem.element.addEventListener("click", this._handleNewTabClick.bind(this)); |
| this.addTabBarItem(this._newTabTabBarItem, {suppressAnimations: true}); |
| |
| this._tabPickerTabBarItem = new WI.PinnedTabBarItem("Images/TabPicker.svg", WI.UIString("Show hidden tabs")); |
| this._tabPickerTabBarItem.element.classList.add("tab-picker"); |
| this.addTabBarItem(this._tabPickerTabBarItem, {suppressAnimations: true}); |
| } |
| |
| // Public |
| |
| get newTabTabBarItem() { return this._newTabTabBarItem; } |
| |
| updateNewTabTabBarItemState() |
| { |
| let newTabExists = !WI.isNewTabWithTypeAllowed(WI.NewTabContentView.Type); |
| this._newTabTabBarItem.disabled = newTabExists; |
| } |
| |
| addTabBarItem(tabBarItem, options = {}) |
| { |
| return this.insertTabBarItem(tabBarItem, this._tabBarItems.length, options); |
| } |
| |
| insertTabBarItem(tabBarItem, index, options = {}) |
| { |
| console.assert(tabBarItem instanceof WI.TabBarItem); |
| if (!(tabBarItem instanceof WI.TabBarItem)) |
| return null; |
| |
| if (tabBarItem.parentTabBar === this) |
| return null; |
| |
| if (this._tabAnimatedClosedSinceMouseEnter) { |
| // Delay adding the new tab until we can expand the tabs after a closed tab. |
| this._finishExpandingTabsAfterClose().then(() => { |
| this.insertTabBarItem(tabBarItem, index, options); |
| }); |
| return null; |
| } |
| |
| if (tabBarItem.parentTabBar) |
| tabBarItem.parentTabBar.removeTabBarItem(tabBarItem); |
| |
| tabBarItem.parentTabBar = this; |
| |
| index = Number.constrain(index, 0, this.normalTabCount); |
| |
| if (this.element.classList.contains("animating")) { |
| requestAnimationFrame(removeStyles.bind(this)); |
| options.suppressAnimations = true; |
| } |
| |
| var beforeTabSizesAndPositions; |
| if (!options.suppressAnimations) |
| beforeTabSizesAndPositions = this._recordTabBarItemSizesAndPositions(); |
| |
| this._tabBarItems.splice(index, 0, tabBarItem); |
| |
| var nextSibling = this._tabBarItems[index + 1]; |
| let nextSiblingElement = nextSibling ? nextSibling.element : this._tabBarItems.lastValue.element; |
| |
| if (this.element.contains(nextSiblingElement)) |
| this.element.insertBefore(tabBarItem.element, nextSiblingElement); |
| else |
| this.element.appendChild(tabBarItem.element); |
| |
| this.element.classList.toggle("single-tab", !this._hasMoreThanOneNormalTab()); |
| |
| tabBarItem.element.style.left = null; |
| tabBarItem.element.style.width = null; |
| |
| function animateTabs() |
| { |
| this.element.classList.add("animating"); |
| this.element.classList.add("inserting-tab"); |
| |
| this._applyTabBarItemSizesAndPositions(afterTabSizesAndPositions); |
| |
| this.element.addEventListener("webkitTransitionEnd", removeStylesListener); |
| } |
| |
| function removeStyles() |
| { |
| this.element.classList.remove("static-layout"); |
| this.element.classList.remove("animating"); |
| this.element.classList.remove("inserting-tab"); |
| |
| tabBarItem.element.classList.remove("being-inserted"); |
| |
| this._clearTabBarItemSizesAndPositions(); |
| |
| this.element.removeEventListener("webkitTransitionEnd", removeStylesListener); |
| } |
| |
| if (!options.suppressAnimations) { |
| var afterTabSizesAndPositions = this._recordTabBarItemSizesAndPositions(); |
| |
| this.updateLayout(); |
| |
| let tabBarItems = this._tabBarItemsFromLeftToRight(); |
| let previousTabBarItem = tabBarItems[tabBarItems.indexOf(tabBarItem) - 1] || null; |
| let previousTabBarItemSizeAndPosition = previousTabBarItem ? beforeTabSizesAndPositions.get(previousTabBarItem) : null; |
| |
| if (previousTabBarItemSizeAndPosition) |
| beforeTabSizesAndPositions.set(tabBarItem, {left: previousTabBarItemSizeAndPosition.left + previousTabBarItemSizeAndPosition.width, width: 0}); |
| else |
| beforeTabSizesAndPositions.set(tabBarItem, {left: 0, width: 0}); |
| |
| this.element.classList.add("static-layout"); |
| tabBarItem.element.classList.add("being-inserted"); |
| |
| this._applyTabBarItemSizesAndPositions(beforeTabSizesAndPositions); |
| |
| var removeStylesListener = removeStyles.bind(this); |
| |
| requestAnimationFrame(animateTabs.bind(this)); |
| } else |
| this.needsLayout(); |
| |
| if (!(tabBarItem instanceof WI.PinnedTabBarItem)) |
| this.updateNewTabTabBarItemState(); |
| |
| this.dispatchEventToListeners(WI.TabBar.Event.TabBarItemAdded, {tabBarItem}); |
| |
| return tabBarItem; |
| } |
| |
| removeTabBarItem(tabBarItemOrIndex, options = {}) |
| { |
| let tabBarItem = this._findTabBarItem(tabBarItemOrIndex); |
| if (!tabBarItem || tabBarItem instanceof WI.PinnedTabBarItem) |
| return null; |
| |
| tabBarItem.parentTabBar = null; |
| |
| if (this._selectedTabBarItem === tabBarItem) { |
| var index = this._tabBarItems.indexOf(tabBarItem); |
| var nextTabBarItem = this._tabBarItems[index + 1]; |
| if (!nextTabBarItem || nextTabBarItem instanceof WI.PinnedTabBarItem) |
| nextTabBarItem = this._tabBarItems[index - 1]; |
| |
| this.selectedTabBarItem = nextTabBarItem; |
| } |
| |
| if (this.element.classList.contains("animating")) { |
| requestAnimationFrame(removeStyles.bind(this)); |
| options.suppressAnimations = true; |
| } |
| |
| var beforeTabSizesAndPositions; |
| if (!options.suppressAnimations) |
| beforeTabSizesAndPositions = this._recordTabBarItemSizesAndPositions(); |
| |
| // Subtract 1 from normalTabCount since arrays begin indexing at 0. |
| let wasLastNormalTab = this._tabBarItems.indexOf(tabBarItem) === this.normalTabCount - 1; |
| |
| this._tabBarItems.remove(tabBarItem); |
| tabBarItem.element.remove(); |
| |
| var hasMoreThanOneNormalTab = this._hasMoreThanOneNormalTab(); |
| this.element.classList.toggle("single-tab", !hasMoreThanOneNormalTab); |
| |
| const shouldOpenDefaultTab = !tabBarItem.isDefaultTab && !this.normalTabCount; |
| if (shouldOpenDefaultTab) |
| options.suppressAnimations = true; |
| |
| if (!hasMoreThanOneNormalTab || wasLastNormalTab || !options.suppressExpansion) { |
| if (!options.suppressAnimations) { |
| this._tabAnimatedClosedSinceMouseEnter = true; |
| this._finishExpandingTabsAfterClose(beforeTabSizesAndPositions); |
| } else |
| this.needsLayout(); |
| |
| this.updateNewTabTabBarItemState(); |
| |
| this.dispatchEventToListeners(WI.TabBar.Event.TabBarItemRemoved, {tabBarItem}); |
| |
| if (shouldOpenDefaultTab) |
| this._openDefaultTab(); |
| |
| return tabBarItem; |
| } |
| |
| var lastNormalTabBarItem; |
| |
| function animateTabs() |
| { |
| this.element.classList.add("animating"); |
| this.element.classList.add("closing-tab"); |
| |
| // For RTL, we need to place extra space between pinned tab and first normal tab. |
| // From left to right there is pinned tabs, extra space, then normal tabs. Compute |
| // how much extra space we need to additionally add for normal tab items. |
| let extraSpaceBetweenNormalAndPinnedTabs = 0; |
| if (WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL) { |
| extraSpaceBetweenNormalAndPinnedTabs = this.element.getBoundingClientRect().width; |
| for (let currentTabBarItem of this._tabBarItemsFromLeftToRight()) |
| extraSpaceBetweenNormalAndPinnedTabs -= currentTabBarItem.element.getBoundingClientRect().width; |
| } |
| |
| let left = 0; |
| for (let currentTabBarItem of this._tabBarItemsFromLeftToRight()) { |
| let sizeAndPosition = beforeTabSizesAndPositions.get(currentTabBarItem); |
| |
| if (!(currentTabBarItem instanceof WI.PinnedTabBarItem)) { |
| currentTabBarItem.element.style.left = extraSpaceBetweenNormalAndPinnedTabs + left + "px"; |
| left += sizeAndPosition.width; |
| lastNormalTabBarItem = currentTabBarItem; |
| } else |
| left = sizeAndPosition.left + sizeAndPosition.width; |
| } |
| |
| // The selected tab and last tab need to draw a right border as well, so make them 1px wider. |
| if (this._selectedTabBarItem) |
| this._selectedTabBarItem.element.style.width = (parseFloat(this._selectedTabBarItem.element.style.width) + 1) + "px"; |
| |
| if (lastNormalTabBarItem !== this._selectedTabBarItem) |
| lastNormalTabBarItem.element.style.width = (parseFloat(lastNormalTabBarItem.element.style.width) + 1) + "px"; |
| |
| this.element.addEventListener("webkitTransitionEnd", removeStylesListener); |
| } |
| |
| function removeStyles() |
| { |
| // The selected tab needs to stop drawing the right border, so make it 1px smaller. Only if it isn't the last. |
| if (this._selectedTabBarItem && this._selectedTabBarItem !== lastNormalTabBarItem) |
| this._selectedTabBarItem.element.style.width = (parseFloat(this._selectedTabBarItem.element.style.width) - 1) + "px"; |
| |
| this.element.classList.remove("animating"); |
| this.element.classList.remove("closing-tab"); |
| |
| this.updateLayout(); |
| |
| this.element.removeEventListener("webkitTransitionEnd", removeStylesListener); |
| } |
| |
| if (!options.suppressAnimations) { |
| this.element.classList.add("static-layout"); |
| |
| this._tabAnimatedClosedSinceMouseEnter = true; |
| |
| this._applyTabBarItemSizesAndPositions(beforeTabSizesAndPositions); |
| |
| var removeStylesListener = removeStyles.bind(this); |
| |
| requestAnimationFrame(animateTabs.bind(this)); |
| } else |
| this.needsLayout(); |
| |
| this.updateNewTabTabBarItemState(); |
| |
| this.dispatchEventToListeners(WI.TabBar.Event.TabBarItemRemoved, {tabBarItem}); |
| |
| if (shouldOpenDefaultTab) |
| this._openDefaultTab(); |
| |
| return tabBarItem; |
| } |
| |
| selectPreviousTab() |
| { |
| if (this._tabBarItems.length <= 1) |
| return; |
| |
| var startIndex = this._tabBarItems.indexOf(this._selectedTabBarItem); |
| var newIndex = startIndex; |
| do { |
| if (newIndex === 0) |
| newIndex = this._tabBarItems.length - 1; |
| else |
| newIndex--; |
| |
| if (!(this._tabBarItems[newIndex] instanceof WI.PinnedTabBarItem)) |
| break; |
| } while (newIndex !== startIndex); |
| |
| if (newIndex === startIndex) |
| return; |
| |
| this.selectedTabBarItem = this._tabBarItems[newIndex]; |
| } |
| |
| selectNextTab() |
| { |
| if (this._tabBarItems.length <= 1) |
| return; |
| |
| var startIndex = this._tabBarItems.indexOf(this._selectedTabBarItem); |
| var newIndex = startIndex; |
| do { |
| if (newIndex === this._tabBarItems.length - 1) |
| newIndex = 0; |
| else |
| newIndex++; |
| |
| if (!(this._tabBarItems[newIndex] instanceof WI.PinnedTabBarItem)) |
| break; |
| } while (newIndex !== startIndex); |
| |
| if (newIndex === startIndex) |
| return; |
| |
| this.selectedTabBarItem = this._tabBarItems[newIndex]; |
| } |
| |
| get selectedTabBarItem() |
| { |
| return this._selectedTabBarItem; |
| } |
| |
| set selectedTabBarItem(tabBarItemOrIndex) |
| { |
| let tabBarItem = this._findTabBarItem(tabBarItemOrIndex); |
| if (tabBarItem === this._newTabTabBarItem || tabBarItem === this._tabPickerTabBarItem) { |
| // Get the last normal tab item if the item is not selectable. |
| tabBarItem = this._tabBarItems[this.normalTabCount - 1]; |
| } |
| |
| if (this._selectedTabBarItem === tabBarItem) |
| return; |
| |
| if (this._selectedTabBarItem) |
| this._selectedTabBarItem.selected = false; |
| |
| this._selectedTabBarItem = tabBarItem || null; |
| |
| if (this._selectedTabBarItem) { |
| this._selectedTabBarItem.selected = true; |
| if (this._selectedTabBarItem.element.classList.contains("hidden")) |
| this.needsLayout(); |
| } |
| |
| this.dispatchEventToListeners(WI.TabBar.Event.TabBarItemSelected); |
| } |
| |
| get tabBarItems() |
| { |
| return this._tabBarItems; |
| } |
| |
| get normalTabCount() |
| { |
| return this._tabBarItems.filter((item) => !(item instanceof WI.PinnedTabBarItem)).length; |
| } |
| |
| get saveableTabCount() |
| { |
| return this._tabBarItems.filter((item) => item.representedObject && item.representedObject.constructor.shouldSaveTab()).length; |
| } |
| |
| // Protected |
| |
| layout() |
| { |
| if (this.element.classList.contains("static-layout")) |
| return; |
| |
| this.element.classList.add("calculate-width"); |
| this.element.classList.remove("collapsed"); |
| |
| function forceItemHidden(item, hidden) { |
| item.element.classList.toggle("hidden", !!hidden); |
| } |
| |
| for (let item of this._tabBarItems) |
| forceItemHidden(item, item === this._tabPickerTabBarItem); |
| |
| function measureItemWidth(item) { |
| if (!item[WI.TabBar.CachedWidthSymbol]) |
| item[WI.TabBar.CachedWidthSymbol] = item.element.realOffsetWidth; |
| return item[WI.TabBar.CachedWidthSymbol]; |
| } |
| |
| let recalculateItemWidths = () => { |
| return this._tabBarItems.reduce((total, item) => { |
| item[WI.TabBar.CachedWidthSymbol] = undefined; |
| return total + measureItemWidth(item); |
| }, 0); |
| }; |
| |
| this._hiddenTabBarItems = []; |
| |
| let totalItemWidth = recalculateItemWidths(); |
| let barWidth = this.element.realOffsetWidth; |
| |
| if (totalItemWidth > barWidth) { |
| this.element.classList.add("collapsed"); |
| totalItemWidth = recalculateItemWidths(); |
| if (totalItemWidth > barWidth) { |
| forceItemHidden(this._tabPickerTabBarItem, false); |
| totalItemWidth += measureItemWidth(this._tabPickerTabBarItem); |
| } |
| |
| let tabBarItems = this._tabBarItemsFromLeftToRight(); |
| let index = tabBarItems.length; |
| while (totalItemWidth > barWidth && --index >= 0) { |
| let item = tabBarItems[index]; |
| if (item === this.selectedTabBarItem || item instanceof WI.PinnedTabBarItem) |
| continue; |
| |
| totalItemWidth -= measureItemWidth(item); |
| forceItemHidden(item, true); |
| |
| this._hiddenTabBarItems.push(item); |
| } |
| } |
| |
| this.element.classList.remove("calculate-width"); |
| } |
| |
| // Private |
| |
| _tabBarItemsFromLeftToRight() |
| { |
| return WI.resolvedLayoutDirection() === WI.LayoutDirection.LTR ? this._tabBarItems : this._tabBarItems.slice().reverse(); |
| } |
| |
| _findTabBarItem(tabBarItemOrIndex) |
| { |
| if (typeof tabBarItemOrIndex === "number") |
| return this._tabBarItems[tabBarItemOrIndex] || null; |
| |
| if (tabBarItemOrIndex instanceof WI.TabBarItem) { |
| if (this._tabBarItems.includes(tabBarItemOrIndex)) |
| return tabBarItemOrIndex; |
| } |
| |
| return null; |
| } |
| |
| _hasMoreThanOneNormalTab() |
| { |
| let normalTabCount = 0; |
| for (let tabBarItem of this._tabBarItems) { |
| if (tabBarItem instanceof WI.PinnedTabBarItem) |
| continue; |
| |
| ++normalTabCount; |
| if (normalTabCount >= 2) |
| return true; |
| } |
| |
| return false; |
| } |
| |
| _openDefaultTab() |
| { |
| this.dispatchEventToListeners(WI.TabBar.Event.OpenDefaultTab); |
| } |
| |
| _recordTabBarItemSizesAndPositions() |
| { |
| var tabBarItemSizesAndPositions = new Map; |
| |
| const barRect = this.element.getBoundingClientRect(); |
| |
| for (var tabBarItem of this._tabBarItems) { |
| var boundingRect = tabBarItem.element.getBoundingClientRect(); |
| tabBarItemSizesAndPositions.set(tabBarItem, {left: boundingRect.left - barRect.left, width: boundingRect.width}); |
| } |
| |
| return tabBarItemSizesAndPositions; |
| } |
| |
| _applyTabBarItemSizesAndPositions(tabBarItemSizesAndPositions, skipTabBarItem) |
| { |
| for (var [tabBarItem, sizeAndPosition] of tabBarItemSizesAndPositions) { |
| if (skipTabBarItem && tabBarItem === skipTabBarItem) |
| continue; |
| tabBarItem.element.style.left = sizeAndPosition.left + "px"; |
| tabBarItem.element.style.width = sizeAndPosition.width + "px"; |
| } |
| } |
| |
| _clearTabBarItemSizesAndPositions(skipTabBarItem) |
| { |
| for (var tabBarItem of this._tabBarItems) { |
| if (skipTabBarItem && tabBarItem === skipTabBarItem) |
| continue; |
| tabBarItem.element.style.left = null; |
| tabBarItem.element.style.width = null; |
| } |
| } |
| |
| _finishExpandingTabsAfterClose(beforeTabSizesAndPositions) |
| { |
| return new Promise(function(resolve, reject) { |
| console.assert(this._tabAnimatedClosedSinceMouseEnter); |
| this._tabAnimatedClosedSinceMouseEnter = false; |
| |
| if (!beforeTabSizesAndPositions) |
| beforeTabSizesAndPositions = this._recordTabBarItemSizesAndPositions(); |
| |
| this.element.classList.remove("static-layout"); |
| this._clearTabBarItemSizesAndPositions(); |
| |
| var afterTabSizesAndPositions = this._recordTabBarItemSizesAndPositions(); |
| |
| this._applyTabBarItemSizesAndPositions(beforeTabSizesAndPositions); |
| this.element.classList.add("static-layout"); |
| |
| function animateTabs() |
| { |
| this.element.classList.add("static-layout"); |
| this.element.classList.add("animating"); |
| this.element.classList.add("expanding-tabs"); |
| |
| this._applyTabBarItemSizesAndPositions(afterTabSizesAndPositions); |
| |
| this.element.addEventListener("webkitTransitionEnd", removeStylesListener); |
| } |
| |
| function removeStyles() |
| { |
| this.element.classList.remove("static-layout"); |
| this.element.classList.remove("animating"); |
| this.element.classList.remove("expanding-tabs"); |
| |
| this._clearTabBarItemSizesAndPositions(); |
| |
| this.updateLayout(); |
| |
| this.element.removeEventListener("webkitTransitionEnd", removeStylesListener); |
| |
| resolve(); |
| } |
| |
| var removeStylesListener = removeStyles.bind(this); |
| |
| requestAnimationFrame(animateTabs.bind(this)); |
| }.bind(this)); |
| } |
| |
| _handleMouseDown(event) |
| { |
| // Only consider left mouse clicks for tab movement. |
| if (event.button !== 0 || event.ctrlKey) |
| return; |
| |
| let itemElement = event.target.closest("." + WI.TabBarItem.StyleClassName); |
| if (!itemElement) |
| return; |
| |
| let tabBarItem = itemElement[WI.TabBarItem.ElementReferenceSymbol]; |
| if (!tabBarItem) |
| return; |
| |
| if (tabBarItem.disabled) |
| return; |
| |
| if (tabBarItem === this._newTabTabBarItem) |
| return; |
| |
| if (tabBarItem === this._tabPickerTabBarItem) { |
| if (!this._hiddenTabBarItems.length) |
| return; |
| |
| if (this._ignoreTabPickerMouseDown) |
| return; |
| |
| this._ignoreTabPickerMouseDown = true; |
| |
| let contextMenu = WI.ContextMenu.createFromEvent(event); |
| contextMenu.addBeforeShowCallback(() => { |
| this._ignoreTabPickerMouseDown = false; |
| }); |
| |
| for (let item of this._hiddenTabBarItems) { |
| contextMenu.appendItem(item.title, () => { |
| this.selectedTabBarItem = item; |
| }); |
| } |
| |
| contextMenu.show(); |
| return; |
| } |
| |
| let closeButtonElement = event.target.closest("." + WI.TabBarItem.CloseButtonStyleClassName); |
| if (closeButtonElement) |
| return; |
| |
| this.selectedTabBarItem = tabBarItem; |
| |
| if (tabBarItem instanceof WI.PinnedTabBarItem || !this._hasMoreThanOneNormalTab()) |
| return; |
| |
| this._firstNormalTabItemIndex = 0; |
| for (let i = 0; i < this._tabBarItems.length; ++i) { |
| if (this._tabBarItems[i] instanceof WI.PinnedTabBarItem) |
| continue; |
| |
| this._firstNormalTabItemIndex = i; |
| break; |
| } |
| |
| this._mouseIsDown = true; |
| |
| this._mouseMovedEventListener = this._handleMouseMoved.bind(this); |
| this._mouseUpEventListener = this._handleMouseUp.bind(this); |
| |
| // Register these listeners on the document so we can track the mouse if it leaves the tab bar. |
| document.addEventListener("mousemove", this._mouseMovedEventListener, true); |
| document.addEventListener("mouseup", this._mouseUpEventListener, true); |
| |
| event.preventDefault(); |
| event.stopPropagation(); |
| } |
| |
| _handleClick(event) |
| { |
| var itemElement = event.target.closest("." + WI.TabBarItem.StyleClassName); |
| if (!itemElement) |
| return; |
| |
| var tabBarItem = itemElement[WI.TabBarItem.ElementReferenceSymbol]; |
| if (!tabBarItem) |
| return; |
| |
| if (tabBarItem.disabled) |
| return; |
| |
| const clickedMiddleButton = event.button === 1; |
| |
| var closeButtonElement = event.target.closest("." + WI.TabBarItem.CloseButtonStyleClassName); |
| if (closeButtonElement || clickedMiddleButton) { |
| // Disallow closing the default tab if it is the only tab. |
| if (tabBarItem.isDefaultTab && this.element.classList.contains("single-tab")) |
| return; |
| |
| if (!event.altKey) { |
| this.removeTabBarItem(tabBarItem, {suppressExpansion: true}); |
| return; |
| } |
| |
| for (let i = this._tabBarItems.length - 1; i >= 0; --i) { |
| let item = this._tabBarItems[i]; |
| if (item === tabBarItem || item instanceof WI.PinnedTabBarItem) |
| continue; |
| this.removeTabBarItem(item); |
| } |
| } |
| } |
| |
| _handleMouseMoved(event) |
| { |
| console.assert(event.button === 0); |
| console.assert(this._mouseIsDown); |
| if (!this._mouseIsDown) |
| return; |
| |
| console.assert(this._selectedTabBarItem); |
| if (!this._selectedTabBarItem) |
| return; |
| |
| event.preventDefault(); |
| event.stopPropagation(); |
| |
| if (!this.element.classList.contains("static-layout")) { |
| this._applyTabBarItemSizesAndPositions(this._recordTabBarItemSizesAndPositions()); |
| this.element.classList.add("static-layout"); |
| this.element.classList.add("dragging-tab"); |
| } |
| |
| if (this._mouseOffset === undefined) |
| this._mouseOffset = event.pageX - this._selectedTabBarItem.element.totalOffsetLeft; |
| |
| var tabBarMouseOffset = event.pageX - this.element.totalOffsetLeft; |
| var newLeft = tabBarMouseOffset - this._mouseOffset; |
| |
| this._selectedTabBarItem.element.style.left = newLeft + "px"; |
| |
| var selectedTabMidX = newLeft + (this._selectedTabBarItem.element.realOffsetWidth / 2); |
| |
| var currentIndex = this._tabBarItems.indexOf(this._selectedTabBarItem); |
| var newIndex = currentIndex; |
| |
| for (let tabBarItem of this._tabBarItems) { |
| if (tabBarItem === this._selectedTabBarItem) |
| continue; |
| |
| var tabBarItemRect = tabBarItem.element.getBoundingClientRect(); |
| |
| if (selectedTabMidX < tabBarItemRect.left || selectedTabMidX > tabBarItemRect.right) |
| continue; |
| |
| newIndex = this._tabBarItems.indexOf(tabBarItem); |
| break; |
| } |
| |
| // Subtract 1 from normalTabCount since arrays begin indexing at 0. |
| newIndex = Number.constrain(newIndex, this._firstNormalTabItemIndex, this.normalTabCount - 1); |
| |
| if (currentIndex === newIndex) |
| return; |
| |
| this._tabBarItems.splice(currentIndex, 1); |
| this._tabBarItems.splice(newIndex, 0, this._selectedTabBarItem); |
| |
| let nextSibling = this._tabBarItems[newIndex + 1]; |
| let nextSiblingElement = nextSibling ? nextSibling.element : this._newTabTabBarItem.element; |
| |
| this.element.insertBefore(this._selectedTabBarItem.element, nextSiblingElement); |
| |
| // FIXME: Animate the tabs that move to make room for the selected tab. This was causing me trouble when I tried. |
| |
| let left = 0; |
| for (let tabBarItem of this._tabBarItemsFromLeftToRight()) { |
| if (tabBarItem !== this._selectedTabBarItem && tabBarItem !== this._newTabTabBarItem && parseFloat(tabBarItem.element.style.left) !== left) |
| tabBarItem.element.style.left = left + "px"; |
| left += parseFloat(tabBarItem.element.style.width); |
| } |
| } |
| |
| _handleMouseUp(event) |
| { |
| console.assert(event.button === 0); |
| console.assert(this._mouseIsDown); |
| if (!this._mouseIsDown) |
| return; |
| |
| this.element.classList.remove("dragging-tab"); |
| |
| if (!this._tabAnimatedClosedSinceMouseEnter) { |
| this.element.classList.remove("static-layout"); |
| this._clearTabBarItemSizesAndPositions(); |
| } else { |
| let left = 0; |
| for (let tabBarItem of this._tabBarItemsFromLeftToRight()) { |
| if (tabBarItem === this._selectedTabBarItem) |
| tabBarItem.element.style.left = left + "px"; |
| left += parseFloat(tabBarItem.element.style.width); |
| } |
| } |
| |
| this._mouseIsDown = false; |
| this._mouseOffset = undefined; |
| |
| document.removeEventListener("mousemove", this._mouseMovedEventListener, true); |
| document.removeEventListener("mouseup", this._mouseUpEventListener, true); |
| |
| this._mouseMovedEventListener = null; |
| this._mouseUpEventListener = null; |
| |
| event.preventDefault(); |
| event.stopPropagation(); |
| |
| this.dispatchEventToListeners(WI.TabBar.Event.TabBarItemsReordered); |
| } |
| |
| _handleMouseLeave(event) |
| { |
| if (this._mouseIsDown || !this._tabAnimatedClosedSinceMouseEnter || !this.element.classList.contains("static-layout") || this.element.classList.contains("animating")) |
| return; |
| |
| // This event can still fire when the mouse is inside the element if DOM nodes are added, removed or generally change inside. |
| // Check if the mouse really did leave the element by checking the bounds. |
| // FIXME: Is this a WebKit bug or correct behavior? |
| const barRect = this.element.getBoundingClientRect(); |
| const newTabItemRect = this._newTabTabBarItem.element.getBoundingClientRect(); |
| if (event.pageY > barRect.top && event.pageY < barRect.bottom && event.pageX > barRect.left && event.pageX < (newTabItemRect ? newTabItemRect.right : barRect.right)) |
| return; |
| |
| this._finishExpandingTabsAfterClose(); |
| } |
| |
| _handleContextMenu(event) |
| { |
| let contextMenu = WI.ContextMenu.createFromEvent(event); |
| |
| for (let tabClass of WI.knownTabClasses()) { |
| if (!tabClass.isTabAllowed() || tabClass.tabInfo().isEphemeral) |
| continue; |
| |
| let openTabBarItem = null; |
| for (let tabBarItem of this._tabBarItems) { |
| let tabContentView = tabBarItem.representedObject; |
| if (!(tabContentView instanceof WI.TabContentView)) |
| continue; |
| |
| if (tabContentView.type === tabClass.Type) { |
| openTabBarItem = tabBarItem; |
| break; |
| } |
| } |
| |
| contextMenu.appendCheckboxItem(tabClass.tabInfo().title, () => { |
| if (openTabBarItem) |
| this.removeTabBarItem(openTabBarItem); |
| else |
| WI.createNewTabWithType(tabClass.Type, {shouldShowNewTab: true}); |
| }, !!openTabBarItem); |
| } |
| } |
| |
| _handleNewTabClick(event) |
| { |
| WI.showNewTabTab(); |
| } |
| |
| _handleNewTabMouseEnter(event) |
| { |
| if (!this._tabAnimatedClosedSinceMouseEnter || !this.element.classList.contains("static-layout") || this.element.classList.contains("animating")) |
| return; |
| |
| this._finishExpandingTabsAfterClose(); |
| } |
| }; |