968 lines
35 KiB
JavaScript
968 lines
35 KiB
JavaScript
/*
|
|
* 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.TabBar = class TabBar extends WI.View
|
|
{
|
|
constructor(element)
|
|
{
|
|
super(element);
|
|
|
|
this.element.classList.add("tab-bar");
|
|
this.element.addEventListener("mousedown", this._handleMouseDown.bind(this));
|
|
|
|
this.element.createChild("div", "border top");
|
|
|
|
const navigationBarBeforeElement = null;
|
|
this._navigationBarBefore = new WI.NavigationBar(navigationBarBeforeElement, {sizesToFit: true});
|
|
this.addSubview(this._navigationBarBefore);
|
|
|
|
this._tabContainer = this.element.appendChild(document.createElement("div"));
|
|
this._tabContainer.className = "tabs";
|
|
this._tabContainer.setAttribute("role", "tablist");
|
|
this._tabContainer.addEventListener("mousedown", this._handleTabContainerMouseDown.bind(this));
|
|
this._tabContainer.addEventListener("mouseleave", this._handleTabContainerMouseLeave.bind(this));
|
|
this._tabContainer.addEventListener("contextmenu", this._handleTabContainerContextMenu.bind(this));
|
|
|
|
const navigationBarAfterElement = null;
|
|
this._navigationBarAfter = new WI.NavigationBar(navigationBarAfterElement, {sizesToFit: true});
|
|
this.addSubview(this._navigationBarAfter);
|
|
|
|
this.element.createChild("div", "border bottom");
|
|
|
|
this._tabBarItems = [];
|
|
this._hiddenTabBarItems = [];
|
|
|
|
const showHiddenTabsRepresentedObject = null;
|
|
const showHiddenTabsDisplayName = null;
|
|
this._showHiddenTabsTabBarItem = new WI.PinnedTabBarItem(showHiddenTabsRepresentedObject, "Images/Overflow.svg", showHiddenTabsDisplayName, WI.UIString("Show hidden tabs\u2026"));
|
|
this._showHiddenTabsTabBarItem.hidden = true;
|
|
this.addTabBarItem(this._showHiddenTabsTabBarItem, {suppressAnimations: true});
|
|
|
|
const openClosedTabsRepresentedObject = null;
|
|
const openClosedTabsDisplayName = null;
|
|
this._openClosedTabsTabBarItem = new WI.PinnedTabBarItem(openClosedTabsRepresentedObject, "Images/Plus15.svg", openClosedTabsDisplayName, WI.UIString("Open closed tabs\u2026"));
|
|
this._openClosedTabsTabBarItem.hidden = true;
|
|
this.addTabBarItem(this._openClosedTabsTabBarItem, {suppressAnimations: true});
|
|
|
|
this._mouseDownPageX = NaN;
|
|
this._isDragging = false;
|
|
}
|
|
|
|
// Public
|
|
|
|
addNavigationItemBefore(navigationItem)
|
|
{
|
|
this._navigationBarBefore.addNavigationItem(navigationItem);
|
|
|
|
this.needsLayout();
|
|
}
|
|
|
|
addNavigationItemAfter(navigationItem)
|
|
{
|
|
this._navigationBarAfter.addNavigationItem(navigationItem);
|
|
|
|
this.needsLayout();
|
|
}
|
|
|
|
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;
|
|
|
|
if (tabBarItem instanceof WI.GeneralTabBarItem)
|
|
index = Number.constrain(index, 0, this.normalTabCount);
|
|
else
|
|
index = Number.constrain(index, this.normalTabCount, this._tabBarItems.length);
|
|
|
|
if (this._tabContainer.classList.contains("animating")) {
|
|
requestAnimationFrame(removeStyles.bind(this));
|
|
options.suppressAnimations = true;
|
|
}
|
|
|
|
var beforeTabSizesAndPositions;
|
|
if (!options.suppressAnimations)
|
|
beforeTabSizesAndPositions = this._recordTabBarItemSizesAndPositions();
|
|
|
|
this._tabBarItems.splice(index, 0, tabBarItem);
|
|
|
|
let nextSibling = this._tabBarItems[index + 1] || this._tabBarItems.lastValue;
|
|
|
|
if (this._tabContainer.contains(nextSibling.element)) {
|
|
if (!(tabBarItem instanceof WI.PinnedTabBarItem) && nextSibling instanceof WI.PinnedTabBarItem)
|
|
this._tabContainer.insertBefore(tabBarItem.element, this._pinnedButtons()[0].element);
|
|
else
|
|
this._tabContainer.insertBefore(tabBarItem.element, nextSibling.element);
|
|
} else {
|
|
if (tabBarItem instanceof WI.PinnedTabBarItem)
|
|
this._tabContainer.appendChild(tabBarItem.element);
|
|
else
|
|
this._tabContainer.insertBefore(tabBarItem.element, this._pinnedButtons()[0].element);
|
|
}
|
|
|
|
tabBarItem.element.style.left = null;
|
|
tabBarItem.element.style.width = null;
|
|
|
|
function animateTabs()
|
|
{
|
|
this._tabContainer.classList.add("animating");
|
|
this._tabContainer.classList.add("inserting-tab");
|
|
|
|
this._applyTabBarItemSizesAndPositions(afterTabSizesAndPositions);
|
|
|
|
this._tabContainer.addEventListener("transitionend", removeStylesListener);
|
|
}
|
|
|
|
function removeStyles()
|
|
{
|
|
this._tabContainer.classList.remove("static-layout");
|
|
this._tabContainer.classList.remove("animating");
|
|
this._tabContainer.classList.remove("inserting-tab");
|
|
|
|
tabBarItem.element.classList.remove("being-inserted");
|
|
|
|
this._clearTabBarItemSizesAndPositions();
|
|
|
|
this._tabContainer.removeEventListener("transitionend", 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._tabContainer.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();
|
|
|
|
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;
|
|
|
|
if (this.normalTabCount === 1)
|
|
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._tabContainer.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();
|
|
|
|
this._openClosedTabsTabBarItem.hidden = !this._closedTabClasses().length;
|
|
|
|
if (!this._hasMoreThanOneNormalTab() || wasLastNormalTab || !options.suppressExpansion) {
|
|
if (!options.suppressAnimations) {
|
|
this._tabAnimatedClosedSinceMouseEnter = true;
|
|
this._finishExpandingTabsAfterClose(beforeTabSizesAndPositions);
|
|
} else
|
|
this.needsLayout();
|
|
|
|
this.dispatchEventToListeners(WI.TabBar.Event.TabBarItemRemoved, {tabBarItem});
|
|
return tabBarItem;
|
|
}
|
|
|
|
var lastNormalTabBarItem;
|
|
|
|
function animateTabs()
|
|
{
|
|
this._tabContainer.classList.add("animating");
|
|
this._tabContainer.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._tabContainer.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._tabContainer.addEventListener("transitionend", 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._tabContainer.classList.remove("animating");
|
|
this._tabContainer.classList.remove("closing-tab");
|
|
|
|
this.updateLayout();
|
|
|
|
this._tabContainer.removeEventListener("transitionend", removeStylesListener);
|
|
}
|
|
|
|
if (!options.suppressAnimations) {
|
|
this._tabContainer.classList.add("static-layout");
|
|
|
|
this._tabAnimatedClosedSinceMouseEnter = true;
|
|
|
|
this._applyTabBarItemSizesAndPositions(beforeTabSizesAndPositions);
|
|
|
|
var removeStylesListener = removeStyles.bind(this);
|
|
|
|
requestAnimationFrame(animateTabs.bind(this));
|
|
} else
|
|
this.needsLayout();
|
|
|
|
this.dispatchEventToListeners(WI.TabBar.Event.TabBarItemRemoved, {tabBarItem});
|
|
|
|
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)
|
|
{
|
|
this.selectTabBarItem(tabBarItemOrIndex);
|
|
}
|
|
|
|
selectTabBarItem(tabBarItemOrIndex, options = {})
|
|
{
|
|
let tabBarItem = this._findTabBarItem(tabBarItemOrIndex);
|
|
if (this._pinnedButtons().includes(tabBarItem)) {
|
|
// Get the last normal tab item if the item is not selectable.
|
|
tabBarItem = this._tabBarItems[this.normalTabCount - 1];
|
|
}
|
|
|
|
if (this._selectedTabBarItem === tabBarItem)
|
|
return;
|
|
|
|
let previousTabBarItem = this._selectedTabBarItem;
|
|
|
|
if (this._selectedTabBarItem)
|
|
this._selectedTabBarItem.selected = false;
|
|
|
|
this._selectedTabBarItem = tabBarItem || null;
|
|
|
|
if (this._selectedTabBarItem) {
|
|
this._selectedTabBarItem.selected = true;
|
|
if (this._selectedTabBarItem.hidden)
|
|
this.needsLayout();
|
|
}
|
|
|
|
let initiatorHint = options.initiatorHint || WI.TabBrowser.TabNavigationInitiator.Unknown;
|
|
this.dispatchEventToListeners(WI.TabBar.Event.TabBarItemSelected, {previousTabBarItem, initiatorHint});
|
|
}
|
|
|
|
get tabBarItems()
|
|
{
|
|
return this._tabBarItems;
|
|
}
|
|
|
|
get visibleTabBarItemsFromLeftToRight()
|
|
{
|
|
return this._tabBarItemsFromLeftToRight().filter((item) => !item.hidden);
|
|
}
|
|
|
|
get tabCount()
|
|
{
|
|
return this._tabBarItems.filter((item) => item.representedObject instanceof WI.TabContentView).length;
|
|
}
|
|
|
|
get normalTabCount()
|
|
{
|
|
return this._tabBarItems.filter((item) => !(item instanceof WI.PinnedTabBarItem)).length;
|
|
}
|
|
|
|
resetCachedWidths()
|
|
{
|
|
for (let tabBarItem of this._tabBarItems)
|
|
tabBarItem[WI.TabBar.CachedWidthSymbol] = 0;
|
|
}
|
|
|
|
// Protected
|
|
|
|
layout()
|
|
{
|
|
if (this._tabContainer.classList.contains("static-layout"))
|
|
return;
|
|
|
|
let undocked = WI.dockConfiguration === WI.DockConfiguration.Undocked;
|
|
|
|
function measureWidth(tabBarItem) {
|
|
if (!tabBarItem[WI.TabBar.CachedWidthSymbol])
|
|
tabBarItem[WI.TabBar.CachedWidthSymbol] = tabBarItem.element.realOffsetWidth;
|
|
return tabBarItem[WI.TabBar.CachedWidthSymbol];
|
|
}
|
|
|
|
let availableSpace = this._tabContainer.realOffsetWidth;
|
|
|
|
this._tabContainer.classList.add("calculate-width");
|
|
|
|
this._hiddenTabBarItems = [];
|
|
|
|
let normalTabBarItems = [];
|
|
for (let tabBarItem of this._tabBarItemsFromLeftToRight()) {
|
|
switch (tabBarItem) {
|
|
case this._showHiddenTabsTabBarItem:
|
|
tabBarItem.hidden = true;
|
|
continue;
|
|
|
|
case this._openClosedTabsTabBarItem:
|
|
tabBarItem.hidden = !this._closedTabClasses().length;
|
|
if (tabBarItem.hidden)
|
|
continue;
|
|
else
|
|
break; // Make sure to calculate its width below.
|
|
}
|
|
|
|
tabBarItem.hidden = false;
|
|
|
|
if (tabBarItem instanceof WI.PinnedTabBarItem) {
|
|
availableSpace -= measureWidth(tabBarItem);
|
|
continue;
|
|
}
|
|
|
|
normalTabBarItems.push(tabBarItem);
|
|
|
|
// When undocked, `WI.TabBarItem` grow to fill any available space. As a result, if a
|
|
// `WI.TabBarItem` is added or removed, the width of all `WI.TabBarItem` will change.
|
|
if (undocked)
|
|
tabBarItem[WI.TabBar.CachedWidthSymbol] = 0;
|
|
}
|
|
|
|
// Wait to measure widths until all `WI.TabBarItem` are un-hidden for the reason above.
|
|
let normalTabBarItemsWidth = normalTabBarItems.reduce((accumulator, tabBarItem) => accumulator + measureWidth(tabBarItem), 0);
|
|
if (Math.round(normalTabBarItemsWidth) >= Math.floor(availableSpace)) {
|
|
this._showHiddenTabsTabBarItem.hidden = false;
|
|
availableSpace -= measureWidth(this._showHiddenTabsTabBarItem);
|
|
|
|
let index = normalTabBarItems.length - 1;
|
|
do {
|
|
let tabBarItem = normalTabBarItems[index];
|
|
if (tabBarItem === this._selectedTabBarItem)
|
|
continue;
|
|
|
|
normalTabBarItemsWidth -= measureWidth(tabBarItem);
|
|
|
|
tabBarItem.hidden = true;
|
|
this._hiddenTabBarItems.push(tabBarItem);
|
|
} while (normalTabBarItemsWidth >= availableSpace && --index >= 0);
|
|
}
|
|
|
|
// Tabs are marked as hidden from right to left, meaning that the right-most item will be
|
|
// first in the list. Reverse the list so that the right-most item is last.
|
|
this._hiddenTabBarItems.reverse();
|
|
|
|
this._tabContainer.classList.remove("calculate-width");
|
|
}
|
|
|
|
didLayoutSubtree()
|
|
{
|
|
super.didLayoutSubtree();
|
|
|
|
this._tabContainer.classList.toggle("hide-border-start", this._navigationBarBefore.navigationItems.every((item) => item.hidden));
|
|
this._tabContainer.classList.toggle("hide-border-end", this._navigationBarAfter.navigationItems.every((item) => item.hidden));
|
|
}
|
|
|
|
// Private
|
|
|
|
_pinnedButtons()
|
|
{
|
|
return [this._showHiddenTabsTabBarItem, this._openClosedTabsTabBarItem];
|
|
}
|
|
|
|
_tabBarItemsFromLeftToRight()
|
|
{
|
|
return WI.resolvedLayoutDirection() === WI.LayoutDirection.LTR ? this._tabBarItems : this._tabBarItems.slice().reverse();
|
|
}
|
|
|
|
_closedTabClasses()
|
|
{
|
|
return Array.from(WI.knownTabClasses()).filter((tabClass) => WI.isNewTabWithTypeAllowed(tabClass.Type));
|
|
}
|
|
|
|
_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;
|
|
}
|
|
|
|
_recordTabBarItemSizesAndPositions()
|
|
{
|
|
var tabBarItemSizesAndPositions = new Map;
|
|
|
|
let barRect = this._tabContainer.getBoundingClientRect();
|
|
|
|
for (let tabBarItem of this._tabBarItems) {
|
|
if (tabBarItem.hidden)
|
|
continue;
|
|
|
|
let 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._tabContainer.classList.remove("static-layout");
|
|
this._clearTabBarItemSizesAndPositions();
|
|
|
|
var afterTabSizesAndPositions = this._recordTabBarItemSizesAndPositions();
|
|
|
|
this._applyTabBarItemSizesAndPositions(beforeTabSizesAndPositions);
|
|
this._tabContainer.classList.add("static-layout");
|
|
|
|
function animateTabs()
|
|
{
|
|
this._tabContainer.classList.add("static-layout");
|
|
this._tabContainer.classList.add("animating");
|
|
this._tabContainer.classList.add("expanding-tabs");
|
|
|
|
this._applyTabBarItemSizesAndPositions(afterTabSizesAndPositions);
|
|
|
|
this._tabContainer.addEventListener("transitionend", removeStylesListener);
|
|
}
|
|
|
|
function removeStyles()
|
|
{
|
|
this._tabContainer.classList.remove("static-layout");
|
|
this._tabContainer.classList.remove("animating");
|
|
this._tabContainer.classList.remove("expanding-tabs");
|
|
|
|
this._clearTabBarItemSizesAndPositions();
|
|
|
|
this.updateLayout();
|
|
|
|
this._tabContainer.removeEventListener("transitionend", removeStylesListener);
|
|
|
|
resolve();
|
|
}
|
|
|
|
var removeStylesListener = removeStyles.bind(this);
|
|
|
|
requestAnimationFrame(animateTabs.bind(this));
|
|
}.bind(this));
|
|
}
|
|
|
|
_handleMouseDown(event)
|
|
{
|
|
if (event.button !== 0 || event.ctrlKey)
|
|
return;
|
|
|
|
if (event.target !== this.element)
|
|
return;
|
|
|
|
switch (WI.dockConfiguration) {
|
|
case WI.DockConfiguration.Bottom:
|
|
WI.resizeDockedFrameMouseDown(event);
|
|
break;
|
|
|
|
case WI.DockConfiguration.Undocked:
|
|
WI.moveUndockedWindowMouseDown(event);
|
|
break;
|
|
}
|
|
}
|
|
|
|
_handleTabContainerMouseDown(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;
|
|
|
|
switch (tabBarItem) {
|
|
case this._showHiddenTabsTabBarItem:
|
|
this._handleShowHiddenTabsTabBarItemMouseDown(event);
|
|
return;
|
|
|
|
case this._openClosedTabsTabBarItem:
|
|
this._handleAddClosedTabsTabBarItemMouseDown(event);
|
|
return;
|
|
}
|
|
|
|
this.selectTabBarItem(tabBarItem, {
|
|
initiatorHint: WI.TabBrowser.TabNavigationInitiator.TabClick
|
|
});
|
|
|
|
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._mouseDownPageX = event.pageX;
|
|
|
|
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();
|
|
}
|
|
|
|
_handleShowHiddenTabsTabBarItemMouseDown(event)
|
|
{
|
|
if (!this._hiddenTabBarItems.length)
|
|
return;
|
|
|
|
if (this._ignoreShowHiddenTabsTabBarItemMouseDown)
|
|
return;
|
|
|
|
this._ignoreShowHiddenTabsTabBarItemMouseDown = true;
|
|
|
|
let contextMenu = WI.ContextMenu.createFromEvent(event);
|
|
contextMenu.addBeforeShowCallback(() => {
|
|
this._ignoreShowHiddenTabsTabBarItemMouseDown = false;
|
|
});
|
|
|
|
for (let item of this._hiddenTabBarItems) {
|
|
contextMenu.appendItem(item.displayName, () => {
|
|
this.selectTabBarItem(item, {
|
|
initiator: WI.TabBrowser.TabNavigationInitiator.ContextMenu
|
|
});
|
|
});
|
|
}
|
|
|
|
contextMenu.show();
|
|
}
|
|
|
|
_handleAddClosedTabsTabBarItemMouseDown(event)
|
|
{
|
|
let closedTabClasses = this._closedTabClasses();
|
|
if (!closedTabClasses.length)
|
|
return;
|
|
|
|
if (this._ignoreAddClosedTabsTabBarItemMouseDown)
|
|
return;
|
|
|
|
this._ignoreAddClosedTabsTabBarItemMouseDown = true;
|
|
|
|
let contextMenu = WI.ContextMenu.createFromEvent(event);
|
|
contextMenu.addBeforeShowCallback(() => {
|
|
this._ignoreAddClosedTabsTabBarItemMouseDown = false;
|
|
});
|
|
|
|
for (let closedTabClass of closedTabClasses) {
|
|
// Tab types that are not restorable (i.e., extension tab) should not be added in the generic code path.
|
|
if (!closedTabClass.shouldSaveTab())
|
|
continue;
|
|
|
|
contextMenu.appendItem(closedTabClass.tabInfo().displayName, () => {
|
|
WI.createNewTabWithType(closedTabClass.Type, {shouldShowNewTab: true});
|
|
});
|
|
}
|
|
|
|
WI.sharedApp.extensionController.addContextMenuItemsForClosedExtensionTabs(contextMenu);
|
|
|
|
contextMenu.show();
|
|
}
|
|
|
|
_handleMouseMoved(event)
|
|
{
|
|
console.assert(event.button === 0);
|
|
console.assert(typeof this._mouseDownPageX === "number" && !isNaN(this._mouseDownPageX));
|
|
if (isNaN(this._mouseDownPageX))
|
|
return;
|
|
|
|
console.assert(this._selectedTabBarItem);
|
|
if (!this._selectedTabBarItem)
|
|
return;
|
|
|
|
if (this._mouseOffset === undefined)
|
|
this._mouseOffset = event.pageX - this._selectedTabBarItem.element.totalOffsetLeft;
|
|
|
|
if (!this._isDragging) {
|
|
const dragThreshold = 12;
|
|
if (Math.abs(this._mouseDownPageX - event.pageX) < dragThreshold)
|
|
return;
|
|
|
|
this._isDragging = true;
|
|
}
|
|
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
if (!this._tabContainer.classList.contains("static-layout")) {
|
|
this._applyTabBarItemSizesAndPositions(this._recordTabBarItemSizesAndPositions());
|
|
this._tabContainer.classList.add("static-layout");
|
|
this._tabContainer.classList.add("dragging-tab");
|
|
}
|
|
|
|
let containerOffset = this._tabContainer.totalOffsetLeft;
|
|
|
|
let tabBarMouseOffset = event.pageX - containerOffset;
|
|
var newLeft = tabBarMouseOffset - this._mouseOffset;
|
|
|
|
this._selectedTabBarItem.element.style.left = newLeft + "px";
|
|
|
|
let selectedTabMidX = containerOffset + newLeft + (this._selectedTabBarItem.element.realOffsetWidth / 2);
|
|
|
|
var currentIndex = this._tabBarItems.indexOf(this._selectedTabBarItem);
|
|
var newIndex = currentIndex;
|
|
|
|
for (let tabBarItem of this._tabBarItems) {
|
|
if (tabBarItem.hidden)
|
|
continue;
|
|
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 : null;
|
|
|
|
this._tabContainer.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.
|
|
|
|
function inlineStyleValue(element, property) {
|
|
return element.getComputedCSSPropertyNumberValue(property) || 0;
|
|
}
|
|
|
|
let accumulatedLeft = 0;
|
|
for (let tabBarItem of this._tabBarItemsFromLeftToRight()) {
|
|
if (tabBarItem.hidden)
|
|
continue;
|
|
|
|
if (tabBarItem !== this._selectedTabBarItem && inlineStyleValue(tabBarItem.element, "left") !== accumulatedLeft)
|
|
tabBarItem.element.style.left = accumulatedLeft + "px";
|
|
|
|
accumulatedLeft += inlineStyleValue(tabBarItem.element, "width");
|
|
}
|
|
}
|
|
|
|
_handleMouseUp(event)
|
|
{
|
|
console.assert(event.button === 0);
|
|
console.assert(typeof this._mouseDownPageX === "number" && !isNaN(this._mouseDownPageX));
|
|
if (isNaN(this._mouseDownPageX))
|
|
return;
|
|
|
|
this._tabContainer.classList.remove("dragging-tab");
|
|
|
|
if (!this._tabAnimatedClosedSinceMouseEnter) {
|
|
this._tabContainer.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._isDragging = false;
|
|
this._mouseDownPageX = NaN;
|
|
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);
|
|
}
|
|
|
|
_handleTabContainerMouseLeave(event)
|
|
{
|
|
if (!isNaN(this._mouseDownPageX) || !this._tabAnimatedClosedSinceMouseEnter || !this._tabContainer.classList.contains("static-layout") || this._tabContainer.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?
|
|
let barRect = this._tabContainer.getBoundingClientRect();
|
|
if (event.pageY > barRect.top && event.pageY < barRect.bottom && event.pageX > barRect.left && event.pageX < barRect.right)
|
|
return;
|
|
|
|
this._finishExpandingTabsAfterClose();
|
|
}
|
|
|
|
_handleTabContainerContextMenu(event)
|
|
{
|
|
let contextMenu = WI.ContextMenu.createFromEvent(event);
|
|
|
|
for (let tabClass of WI.knownTabClasses()) {
|
|
if (!tabClass.isTabAllowed() || !tabClass.shouldSaveTab())
|
|
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;
|
|
}
|
|
}
|
|
|
|
let checked = !!openTabBarItem;
|
|
let disabled = checked && this.normalTabCount === 1;
|
|
contextMenu.appendCheckboxItem(tabClass.tabInfo().displayName, () => {
|
|
if (openTabBarItem)
|
|
this.removeTabBarItem(openTabBarItem);
|
|
else
|
|
WI.createNewTabWithType(tabClass.Type, {shouldShowNewTab: true});
|
|
}, checked, disabled);
|
|
}
|
|
|
|
WI.sharedApp.extensionController.addContextMenuItemsForAllExtensionTabs(contextMenu);
|
|
}
|
|
};
|
|
|
|
WI.TabBar.CachedWidthSymbol = Symbol("cached-width");
|
|
|
|
WI.TabBar.Event = {
|
|
TabBarItemSelected: "tab-bar-tab-bar-item-selected",
|
|
TabBarItemAdded: "tab-bar-tab-bar-item-added",
|
|
TabBarItemRemoved: "tab-bar-tab-bar-item-removed",
|
|
TabBarItemsReordered: "tab-bar-tab-bar-items-reordered",
|
|
};
|