зеркало из https://github.com/mozilla/gecko-dev.git
2447 строки
91 KiB
XML
2447 строки
91 KiB
XML
<?xml version="1.0"?>
|
|
|
|
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
|
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
|
|
|
<bindings id="tabBrowserBindings"
|
|
xmlns="http://www.mozilla.org/xbl"
|
|
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
|
xmlns:xbl="http://www.mozilla.org/xbl">
|
|
|
|
<binding id="tabbrowser-arrowscrollbox" extends="chrome://global/content/bindings/scrollbox.xml#arrowscrollbox-clicktoscroll">
|
|
<implementation>
|
|
<!-- Override scrollbox.xml method, since our scrollbox's children are
|
|
inherited from the binding parent -->
|
|
<method name="_getScrollableElements">
|
|
<body><![CDATA[
|
|
return Array.filter(document.getBindingParent(this).children,
|
|
this._canScrollToElement, this);
|
|
]]></body>
|
|
</method>
|
|
<method name="_canScrollToElement">
|
|
<parameter name="tab"/>
|
|
<body><![CDATA[
|
|
return !tab._pinnedUnscrollable && !tab.hidden;
|
|
]]></body>
|
|
</method>
|
|
</implementation>
|
|
|
|
<handlers>
|
|
<handler event="underflow" phase="capturing"><![CDATA[
|
|
// Ignore underflow events:
|
|
// - from nested scrollable elements
|
|
// - for vertical orientation
|
|
// - corresponding to an overflow event that we ignored
|
|
let tabs = document.getBindingParent(this);
|
|
if (event.originalTarget != this._scrollbox ||
|
|
event.detail == 0 ||
|
|
!tabs.hasAttribute("overflow")) {
|
|
return;
|
|
}
|
|
|
|
tabs.removeAttribute("overflow");
|
|
|
|
if (tabs._lastTabClosedByMouse) {
|
|
tabs._expandSpacerBy(this._scrollButtonDown.clientWidth);
|
|
}
|
|
|
|
for (let tab of Array.from(gBrowser._removingTabs)) {
|
|
gBrowser.removeTab(tab);
|
|
}
|
|
|
|
tabs._positionPinnedTabs();
|
|
]]></handler>
|
|
<handler event="overflow"><![CDATA[
|
|
// Ignore overflow events:
|
|
// - from nested scrollable elements
|
|
// - for vertical orientation
|
|
if (event.originalTarget != this._scrollbox ||
|
|
event.detail == 0) {
|
|
return;
|
|
}
|
|
|
|
var tabs = document.getBindingParent(this);
|
|
tabs.setAttribute("overflow", "true");
|
|
tabs._positionPinnedTabs();
|
|
tabs._handleTabSelect(true);
|
|
]]></handler>
|
|
</handlers>
|
|
</binding>
|
|
|
|
<binding id="tabbrowser-tabs"
|
|
extends="chrome://global/content/bindings/tabbox.xml#tabs">
|
|
<content>
|
|
<xul:hbox class="tab-drop-indicator-box">
|
|
<xul:image class="tab-drop-indicator" anonid="tab-drop-indicator" collapsed="true"/>
|
|
</xul:hbox>
|
|
<xul:arrowscrollbox anonid="arrowscrollbox" orient="horizontal" flex="1"
|
|
style="min-width: 1px;"
|
|
clicktoscroll="true"
|
|
class="tabbrowser-arrowscrollbox">
|
|
<!--
|
|
This is a hack to circumvent bug 472020, otherwise the tabs show up on the
|
|
right of the newtab button.
|
|
-->
|
|
<children includes="tab"/>
|
|
<!--
|
|
This is to ensure anything extensions put here will go before the newtab
|
|
button, necessary due to the previous hack.
|
|
-->
|
|
<children/>
|
|
<xul:toolbarbutton class="tabs-newtab-button toolbarbutton-1"
|
|
anonid="tabs-newtab-button"
|
|
command="cmd_newNavigatorTab"
|
|
onclick="checkForMiddleClick(this, event);"
|
|
tooltip="dynamic-shortcut-tooltip"/>
|
|
<xul:spacer class="closing-tabs-spacer" anonid="closing-tabs-spacer"
|
|
style="width: 0;"/>
|
|
</xul:arrowscrollbox>
|
|
</content>
|
|
|
|
<implementation implements="nsIObserver">
|
|
<constructor>
|
|
<![CDATA[
|
|
this._tabClipWidth = Services.prefs.getIntPref("browser.tabs.tabClipWidth");
|
|
this._hiddenSoundPlayingTabs = new Set();
|
|
|
|
let strId = PrivateBrowsingUtils.isWindowPrivate(window) ?
|
|
"emptyPrivateTabTitle" : "emptyTabTitle";
|
|
this.emptyTabTitle = gTabBrowserBundle.GetStringFromName("tabs." + strId);
|
|
|
|
var tab = this.firstElementChild;
|
|
tab.label = this.emptyTabTitle;
|
|
|
|
window.addEventListener("resize", this);
|
|
|
|
Services.prefs.addObserver("privacy.userContext", this);
|
|
this.observe(null, "nsPref:changed", "privacy.userContext.enabled");
|
|
|
|
XPCOMUtils.defineLazyPreferenceGetter(this, "_tabMinWidthPref",
|
|
"browser.tabs.tabMinWidth", null,
|
|
(pref, prevValue, newValue) => this._tabMinWidth = newValue,
|
|
newValue => {
|
|
const LIMIT = 50;
|
|
return Math.max(newValue, LIMIT);
|
|
},
|
|
);
|
|
|
|
this._tabMinWidth = this._tabMinWidthPref;
|
|
|
|
XPCOMUtils.defineLazyPreferenceGetter(this, "_multiselectEnabledPref",
|
|
"browser.tabs.multiselect", null,
|
|
(pref, prevValue, newValue) => this._multiselectEnabled = newValue);
|
|
this._multiselectEnabled = this._multiselectEnabledPref;
|
|
|
|
this._setPositionalAttributes();
|
|
|
|
CustomizableUI.addListener(this);
|
|
this._updateNewTabVisibility();
|
|
|
|
XPCOMUtils.defineLazyPreferenceGetter(this, "_closeTabByDblclick",
|
|
"browser.tabs.closeTabByDblclick", false);
|
|
|
|
if (gMultiProcessBrowser) {
|
|
this.tabbox.tabpanels.setAttribute("async", "true");
|
|
}
|
|
]]>
|
|
</constructor>
|
|
|
|
<destructor>
|
|
<![CDATA[
|
|
Services.prefs.removeObserver("privacy.userContext", this);
|
|
|
|
CustomizableUI.removeListener(this);
|
|
]]>
|
|
</destructor>
|
|
|
|
<field name="tabbox" readonly="true">
|
|
document.getElementById("tabbrowser-tabbox");
|
|
</field>
|
|
|
|
<field name="contextMenu" readonly="true">
|
|
document.getElementById("tabContextMenu");
|
|
</field>
|
|
|
|
<field name="arrowScrollbox">
|
|
document.getAnonymousElementByAttribute(this, "anonid", "arrowscrollbox");
|
|
</field>
|
|
|
|
<field name="_firstTab">null</field>
|
|
<field name="_lastTab">null</field>
|
|
<field name="_beforeSelectedTab">null</field>
|
|
<field name="_beforeHoveredTab">null</field>
|
|
<field name="_afterHoveredTab">null</field>
|
|
<field name="_hoveredTab">null</field>
|
|
|
|
<property name="_tabMinWidth">
|
|
<setter>
|
|
this.style.setProperty("--tab-min-width", val + "px");
|
|
return val;
|
|
</setter>
|
|
</property>
|
|
|
|
<property name="_multiselectEnabled">
|
|
<setter>
|
|
// Unlike boolean HTML attributes, the value of boolean ARIA attributes actually matters.
|
|
this.setAttribute("aria-multiselectable", !!val);
|
|
return val;
|
|
</setter>
|
|
<getter>
|
|
return this.getAttribute("aria-multiselectable") == "true";
|
|
</getter>
|
|
</property>
|
|
|
|
<method name="observe">
|
|
<parameter name="aSubject"/>
|
|
<parameter name="aTopic"/>
|
|
<parameter name="aData"/>
|
|
<body><![CDATA[
|
|
switch (aTopic) {
|
|
case "nsPref:changed":
|
|
// This is has to deal with changes in
|
|
// privacy.userContext.enabled and
|
|
// privacy.userContext.longPressBehavior.
|
|
let containersEnabled = Services.prefs.getBoolPref("privacy.userContext.enabled")
|
|
&& !PrivateBrowsingUtils.isWindowPrivate(window);
|
|
|
|
// This pref won't change so often, so just recreate the menu.
|
|
let longPressBehavior = Services.prefs.getIntPref("privacy.userContext.longPressBehavior");
|
|
|
|
// If longPressBehavior pref is set to 0 (or any invalid value)
|
|
// long press menu is disabled.
|
|
if (containersEnabled && (longPressBehavior <= 0 || longPressBehavior > 2)) {
|
|
containersEnabled = false;
|
|
}
|
|
|
|
const newTab = document.getElementById("new-tab-button");
|
|
const newTab2 = document.getAnonymousElementByAttribute(this, "anonid", "tabs-newtab-button");
|
|
|
|
for (let parent of [newTab, newTab2]) {
|
|
if (!parent)
|
|
continue;
|
|
|
|
gClickAndHoldListenersOnElement.remove(parent);
|
|
parent.removeAttribute("type");
|
|
if (parent.firstElementChild) {
|
|
parent.firstElementChild.remove();
|
|
}
|
|
|
|
if (containersEnabled) {
|
|
let popup = document.createElementNS(
|
|
"http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
|
|
"menupopup");
|
|
if (parent.id) {
|
|
popup.id = "newtab-popup";
|
|
} else {
|
|
popup.setAttribute("anonid", "newtab-popup");
|
|
}
|
|
popup.className = "new-tab-popup";
|
|
popup.setAttribute("position", "after_end");
|
|
popup.addEventListener("popupshowing", event => {
|
|
createUserContextMenu(event, {
|
|
useAccessKeys: false,
|
|
showDefaultTab: Services.prefs.getIntPref("privacy.userContext.longPressBehavior") == 1,
|
|
});
|
|
});
|
|
parent.appendChild(popup);
|
|
|
|
// longPressBehavior == 2 means that the menu is shown after X
|
|
// millisecs. Otherwise, with 1, the menu is open immediatelly.
|
|
if (longPressBehavior == 2) {
|
|
gClickAndHoldListenersOnElement.add(parent);
|
|
}
|
|
|
|
parent.setAttribute("type", "menu");
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
]]></body>
|
|
</method>
|
|
|
|
<property name="_isCustomizing" readonly="true">
|
|
<getter><![CDATA[
|
|
return document.documentElement.getAttribute("customizing") == "true";
|
|
]]></getter>
|
|
</property>
|
|
|
|
<method name="_getVisibleTabs">
|
|
<body><![CDATA[
|
|
// Cannot access gBrowser before it's initialized.
|
|
if (!gBrowser) {
|
|
return [ this.firstElementChild ];
|
|
}
|
|
|
|
return gBrowser.visibleTabs;
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="_setPositionalAttributes">
|
|
<body><![CDATA[
|
|
let visibleTabs = this._getVisibleTabs();
|
|
if (!visibleTabs.length) {
|
|
return;
|
|
}
|
|
let selectedTab = this.selectedItem;
|
|
let selectedIndex = visibleTabs.indexOf(selectedTab);
|
|
if (this._beforeSelectedTab) {
|
|
this._beforeSelectedTab.removeAttribute("beforeselected-visible");
|
|
}
|
|
|
|
if (selectedTab.closing || selectedIndex <= 0) {
|
|
this._beforeSelectedTab = null;
|
|
} else {
|
|
let beforeSelectedTab = visibleTabs[selectedIndex - 1];
|
|
let separatedByScrollButton = this.getAttribute("overflow") == "true" &&
|
|
beforeSelectedTab.pinned && !selectedTab.pinned;
|
|
if (!separatedByScrollButton) {
|
|
this._beforeSelectedTab = beforeSelectedTab;
|
|
this._beforeSelectedTab.setAttribute("beforeselected-visible",
|
|
"true");
|
|
}
|
|
}
|
|
|
|
if (this._firstTab)
|
|
this._firstTab.removeAttribute("first-visible-tab");
|
|
this._firstTab = visibleTabs[0];
|
|
this._firstTab.setAttribute("first-visible-tab", "true");
|
|
if (this._lastTab)
|
|
this._lastTab.removeAttribute("last-visible-tab");
|
|
this._lastTab = visibleTabs[visibleTabs.length - 1];
|
|
this._lastTab.setAttribute("last-visible-tab", "true");
|
|
|
|
let hoveredTab = this._hoveredTab;
|
|
if (hoveredTab) {
|
|
hoveredTab._mouseleave();
|
|
}
|
|
hoveredTab = this.querySelector("tab:hover");
|
|
if (hoveredTab) {
|
|
hoveredTab._mouseenter();
|
|
}
|
|
|
|
// Update before-multiselected attributes.
|
|
// gBrowser may not be initialized yet, so avoid using it
|
|
for (let i = 0; i < visibleTabs.length - 1; i++) {
|
|
let tab = visibleTabs[i];
|
|
let nextTab = visibleTabs[i + 1];
|
|
tab.removeAttribute("before-multiselected");
|
|
if (nextTab.multiselected) {
|
|
tab.setAttribute("before-multiselected", "true");
|
|
}
|
|
}
|
|
]]></body>
|
|
</method>
|
|
|
|
<field name="_blockDblClick">false</field>
|
|
|
|
<field name="_tabDropIndicator">
|
|
document.getAnonymousElementByAttribute(this, "anonid", "tab-drop-indicator");
|
|
</field>
|
|
|
|
<field name="_dragOverDelay">350</field>
|
|
<field name="_dragTime">0</field>
|
|
|
|
<field name="_closeButtonsUpdatePending">false</field>
|
|
<method name="_updateCloseButtons">
|
|
<body><![CDATA[
|
|
// If we're overflowing, tabs are at their minimum widths.
|
|
if (this.getAttribute("overflow") == "true") {
|
|
this.setAttribute("closebuttons", "activetab");
|
|
return;
|
|
}
|
|
|
|
if (this._closeButtonsUpdatePending) {
|
|
return;
|
|
}
|
|
this._closeButtonsUpdatePending = true;
|
|
|
|
// Wait until after the next paint to get current layout data from
|
|
// getBoundsWithoutFlushing.
|
|
window.requestAnimationFrame(() => {
|
|
window.requestAnimationFrame(() => {
|
|
this._closeButtonsUpdatePending = false;
|
|
|
|
// The scrollbox may have started overflowing since we checked
|
|
// overflow earlier, so check again.
|
|
if (this.getAttribute("overflow") == "true") {
|
|
this.setAttribute("closebuttons", "activetab");
|
|
return;
|
|
}
|
|
|
|
// Check if tab widths are below the threshold where we want to
|
|
// remove close buttons from background tabs so that people don't
|
|
// accidentally close tabs by selecting them.
|
|
let rect = ele => {
|
|
return window.windowUtils.getBoundsWithoutFlushing(ele);
|
|
};
|
|
let tab = this._getVisibleTabs()[gBrowser._numPinnedTabs];
|
|
if (tab && rect(tab).width <= this._tabClipWidth) {
|
|
this.setAttribute("closebuttons", "activetab");
|
|
} else {
|
|
this.removeAttribute("closebuttons");
|
|
}
|
|
});
|
|
});
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="_updateHiddenTabsStatus">
|
|
<body><![CDATA[
|
|
if (gBrowser.visibleTabs.length < gBrowser.tabs.length) {
|
|
this.setAttribute("hashiddentabs", "true");
|
|
} else {
|
|
this.removeAttribute("hashiddentabs");
|
|
}
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="_handleTabSelect">
|
|
<parameter name="aInstant"/>
|
|
<body><![CDATA[
|
|
let selectedTab = this.selectedItem;
|
|
if (this.getAttribute("overflow") == "true")
|
|
this.arrowScrollbox.ensureElementIsVisible(selectedTab, aInstant);
|
|
|
|
selectedTab._notselectedsinceload = false;
|
|
]]></body>
|
|
</method>
|
|
|
|
<field name="_closingTabsSpacer">
|
|
document.getAnonymousElementByAttribute(this, "anonid", "closing-tabs-spacer");
|
|
</field>
|
|
|
|
<field name="_tabDefaultMaxWidth">NaN</field>
|
|
<field name="_lastTabClosedByMouse">false</field>
|
|
<field name="_hasTabTempMaxWidth">false</field>
|
|
|
|
<!-- Try to keep the active tab's close button under the mouse cursor -->
|
|
<method name="_lockTabSizing">
|
|
<parameter name="aTab"/>
|
|
<parameter name="aTabWidth"/>
|
|
<body><![CDATA[
|
|
let tabs = this._getVisibleTabs();
|
|
if (!tabs.length) {
|
|
return;
|
|
}
|
|
|
|
var isEndTab = (aTab._tPos > tabs[tabs.length - 1]._tPos);
|
|
|
|
if (!this._tabDefaultMaxWidth) {
|
|
this._tabDefaultMaxWidth =
|
|
parseFloat(window.getComputedStyle(aTab).maxWidth);
|
|
}
|
|
this._lastTabClosedByMouse = true;
|
|
|
|
if (this.getAttribute("overflow") == "true") {
|
|
// Don't need to do anything if we're in overflow mode and aren't scrolled
|
|
// all the way to the right, or if we're closing the last tab.
|
|
if (isEndTab || !this.arrowScrollbox._scrollButtonDown.disabled) {
|
|
return;
|
|
}
|
|
// If the tab has an owner that will become the active tab, the owner will
|
|
// be to the left of it, so we actually want the left tab to slide over.
|
|
// This can't be done as easily in non-overflow mode, so we don't bother.
|
|
if (aTab.owner) {
|
|
return;
|
|
}
|
|
this._expandSpacerBy(aTabWidth);
|
|
} else { // non-overflow mode
|
|
// Locking is neither in effect nor needed, so let tabs expand normally.
|
|
if (isEndTab && !this._hasTabTempMaxWidth) {
|
|
return;
|
|
}
|
|
let numPinned = gBrowser._numPinnedTabs;
|
|
// Force tabs to stay the same width, unless we're closing the last tab,
|
|
// which case we need to let them expand just enough so that the overall
|
|
// tabbar width is the same.
|
|
if (isEndTab) {
|
|
let numNormalTabs = tabs.length - numPinned;
|
|
aTabWidth = aTabWidth * (numNormalTabs + 1) / numNormalTabs;
|
|
if (aTabWidth > this._tabDefaultMaxWidth) {
|
|
aTabWidth = this._tabDefaultMaxWidth;
|
|
}
|
|
}
|
|
aTabWidth += "px";
|
|
let tabsToReset = [];
|
|
for (let i = numPinned; i < tabs.length; i++) {
|
|
let tab = tabs[i];
|
|
tab.style.setProperty("max-width", aTabWidth, "important");
|
|
if (!isEndTab) { // keep tabs the same width
|
|
tab.style.transition = "none";
|
|
tabsToReset.push(tab);
|
|
}
|
|
}
|
|
|
|
if (tabsToReset.length) {
|
|
window.promiseDocumentFlushed(() => {}).then(() => {
|
|
window.requestAnimationFrame(() => {
|
|
for (let tab of tabsToReset) {
|
|
tab.style.transition = "";
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
this._hasTabTempMaxWidth = true;
|
|
gBrowser.addEventListener("mousemove", this);
|
|
window.addEventListener("mouseout", this);
|
|
}
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="_expandSpacerBy">
|
|
<parameter name="pixels"/>
|
|
<body><![CDATA[
|
|
let spacer = this._closingTabsSpacer;
|
|
spacer.style.width = parseFloat(spacer.style.width) + pixels + "px";
|
|
this.setAttribute("using-closing-tabs-spacer", "true");
|
|
gBrowser.addEventListener("mousemove", this);
|
|
window.addEventListener("mouseout", this);
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="_unlockTabSizing">
|
|
<body><![CDATA[
|
|
gBrowser.removeEventListener("mousemove", this);
|
|
window.removeEventListener("mouseout", this);
|
|
|
|
if (this._hasTabTempMaxWidth) {
|
|
this._hasTabTempMaxWidth = false;
|
|
let tabs = this._getVisibleTabs();
|
|
for (let i = 0; i < tabs.length; i++) {
|
|
tabs[i].style.maxWidth = "";
|
|
}
|
|
}
|
|
|
|
if (this.hasAttribute("using-closing-tabs-spacer")) {
|
|
this.removeAttribute("using-closing-tabs-spacer");
|
|
this._closingTabsSpacer.style.width = 0;
|
|
}
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="uiDensityChanged">
|
|
<body><![CDATA[
|
|
this._positionPinnedTabs();
|
|
this._updateCloseButtons();
|
|
this._handleTabSelect(true);
|
|
]]></body>
|
|
</method>
|
|
|
|
<field name="_lastNumPinned">0</field>
|
|
<field name="_pinnedTabsLayoutCache">null</field>
|
|
<method name="_positionPinnedTabs">
|
|
<body><![CDATA[
|
|
let numPinned = gBrowser._numPinnedTabs;
|
|
let doPosition = this.getAttribute("overflow") == "true" &&
|
|
this._getVisibleTabs().length > numPinned &&
|
|
numPinned > 0;
|
|
|
|
if (doPosition) {
|
|
this.setAttribute("positionpinnedtabs", "true");
|
|
|
|
let layoutData = this._pinnedTabsLayoutCache;
|
|
let uiDensity = document.documentElement.getAttribute("uidensity");
|
|
if (!layoutData ||
|
|
layoutData.uiDensity != uiDensity) {
|
|
let arrowScrollbox = this.arrowScrollbox;
|
|
layoutData = this._pinnedTabsLayoutCache = {
|
|
uiDensity,
|
|
pinnedTabWidth: this.children[0].getBoundingClientRect().width,
|
|
scrollButtonWidth: arrowScrollbox._scrollButtonDown.getBoundingClientRect().width,
|
|
};
|
|
}
|
|
|
|
let width = 0;
|
|
for (let i = numPinned - 1; i >= 0; i--) {
|
|
let tab = this.children[i];
|
|
width += layoutData.pinnedTabWidth;
|
|
tab.style.setProperty("margin-inline-start",
|
|
-(width + layoutData.scrollButtonWidth) + "px", "important");
|
|
tab._pinnedUnscrollable = true;
|
|
}
|
|
this.style.paddingInlineStart = width + "px";
|
|
} else {
|
|
this.removeAttribute("positionpinnedtabs");
|
|
|
|
for (let i = 0; i < numPinned; i++) {
|
|
let tab = this.children[i];
|
|
tab.style.marginInlineStart = "";
|
|
tab._pinnedUnscrollable = false;
|
|
}
|
|
|
|
this.style.paddingInlineStart = "";
|
|
}
|
|
|
|
if (this._lastNumPinned != numPinned) {
|
|
this._lastNumPinned = numPinned;
|
|
this._handleTabSelect(true);
|
|
}
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="_animateTabMove">
|
|
<parameter name="event"/>
|
|
<body><![CDATA[
|
|
let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
|
|
let movingTabs = draggedTab._dragData.movingTabs;
|
|
|
|
if (this.getAttribute("movingtab") != "true") {
|
|
this.setAttribute("movingtab", "true");
|
|
this.parentNode.setAttribute("movingtab", "true");
|
|
if (!draggedTab.multiselected)
|
|
this.selectedItem = draggedTab;
|
|
}
|
|
|
|
if (!("animLastScreenX" in draggedTab._dragData))
|
|
draggedTab._dragData.animLastScreenX = draggedTab._dragData.screenX;
|
|
|
|
let screenX = event.screenX;
|
|
if (screenX == draggedTab._dragData.animLastScreenX)
|
|
return;
|
|
|
|
// Direction of the mouse movement.
|
|
let ltrMove = screenX > draggedTab._dragData.animLastScreenX;
|
|
|
|
draggedTab._dragData.animLastScreenX = screenX;
|
|
|
|
let rtl = (window.getComputedStyle(this).direction == "rtl");
|
|
let pinned = draggedTab.pinned;
|
|
let numPinned = gBrowser._numPinnedTabs;
|
|
let tabs = this._getVisibleTabs()
|
|
.slice(pinned ? 0 : numPinned,
|
|
pinned ? numPinned : undefined);
|
|
|
|
if (rtl) {
|
|
tabs.reverse();
|
|
// Copy moving tabs array to avoid infinite reversing.
|
|
movingTabs = [...movingTabs].reverse();
|
|
}
|
|
let tabWidth = draggedTab.getBoundingClientRect().width;
|
|
let shiftWidth = tabWidth * movingTabs.length;
|
|
draggedTab._dragData.tabWidth = tabWidth;
|
|
|
|
// Move the dragged tab based on the mouse position.
|
|
|
|
let leftTab = tabs[0];
|
|
let rightTab = tabs[tabs.length - 1];
|
|
let rightMovingTabScreenX = movingTabs[movingTabs.length - 1].boxObject.screenX;
|
|
let leftMovingTabScreenX = movingTabs[0].boxObject.screenX;
|
|
let translateX = screenX - draggedTab._dragData.screenX;
|
|
if (!pinned) {
|
|
translateX += this.arrowScrollbox._scrollbox.scrollLeft - draggedTab._dragData.scrollX;
|
|
}
|
|
let leftBound = leftTab.boxObject.screenX - leftMovingTabScreenX;
|
|
let rightBound = (rightTab.boxObject.screenX + rightTab.boxObject.width) -
|
|
(rightMovingTabScreenX + tabWidth);
|
|
translateX = Math.min(Math.max(translateX, leftBound), rightBound);
|
|
|
|
for (let tab of movingTabs) {
|
|
tab.style.transform = "translateX(" + translateX + "px)";
|
|
}
|
|
|
|
draggedTab._dragData.translateX = translateX;
|
|
|
|
// Determine what tab we're dragging over.
|
|
// * Single tab dragging: Point of reference is the center of the dragged tab. If that
|
|
// point touches a background tab, the dragged tab would take that
|
|
// tab's position when dropped.
|
|
// * Multiple tabs dragging: All dragged tabs are one "giant" tab with two
|
|
// points of reference (center of tabs on the extremities). When
|
|
// mouse is moving from left to right, the right reference gets activated,
|
|
// otherwise the left reference will be used. Everything else works the same
|
|
// as single tab dragging.
|
|
// * We're doing a binary search in order to reduce the amount of
|
|
// tabs we need to check.
|
|
|
|
tabs = tabs.filter(t => !movingTabs.includes(t) || t == draggedTab);
|
|
let leftTabCenter = leftMovingTabScreenX + translateX + tabWidth / 2;
|
|
let rightTabCenter = rightMovingTabScreenX + translateX + tabWidth / 2;
|
|
let tabCenter = ltrMove ? rightTabCenter : leftTabCenter;
|
|
let newIndex = -1;
|
|
let oldIndex = "animDropIndex" in draggedTab._dragData ?
|
|
draggedTab._dragData.animDropIndex : movingTabs[0]._tPos;
|
|
let low = 0;
|
|
let high = tabs.length - 1;
|
|
while (low <= high) {
|
|
let mid = Math.floor((low + high) / 2);
|
|
if (tabs[mid] == draggedTab && ++mid > high)
|
|
break;
|
|
let boxObject = tabs[mid].boxObject;
|
|
screenX = boxObject.screenX + getTabShift(tabs[mid], oldIndex);
|
|
if (screenX > tabCenter) {
|
|
high = mid - 1;
|
|
} else if (screenX + boxObject.width < tabCenter) {
|
|
low = mid + 1;
|
|
} else {
|
|
newIndex = tabs[mid]._tPos;
|
|
break;
|
|
}
|
|
}
|
|
if (newIndex >= oldIndex)
|
|
newIndex++;
|
|
if (newIndex < 0 || newIndex == oldIndex)
|
|
return;
|
|
draggedTab._dragData.animDropIndex = newIndex;
|
|
|
|
// Shift background tabs to leave a gap where the dragged tab
|
|
// would currently be dropped.
|
|
|
|
for (let tab of tabs) {
|
|
if (tab != draggedTab) {
|
|
let shift = getTabShift(tab, newIndex);
|
|
tab.style.transform = shift ? "translateX(" + shift + "px)" : "";
|
|
}
|
|
}
|
|
|
|
function getTabShift(tab, dropIndex) {
|
|
if (tab._tPos < draggedTab._tPos && tab._tPos >= dropIndex)
|
|
return (rtl ? -shiftWidth : shiftWidth);
|
|
if (tab._tPos > draggedTab._tPos && tab._tPos < dropIndex)
|
|
return (rtl ? shiftWidth : -shiftWidth);
|
|
return 0;
|
|
}
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="_finishAnimateTabMove">
|
|
<body><![CDATA[
|
|
if (this.getAttribute("movingtab") != "true") {
|
|
return;
|
|
}
|
|
|
|
for (let tab of this._getVisibleTabs()) {
|
|
tab.style.transform = "";
|
|
}
|
|
|
|
this.removeAttribute("movingtab");
|
|
this.parentNode.removeAttribute("movingtab");
|
|
|
|
this._handleTabSelect();
|
|
]]></body>
|
|
</method>
|
|
|
|
<!-- Regroup all selected tabs around the
|
|
tab in param -->
|
|
<method name="_groupSelectedTabs">
|
|
<parameter name="tab"/>
|
|
<body><![CDATA[
|
|
let draggedTabPos = tab._tPos;
|
|
let selectedTabs = gBrowser.selectedTabs;
|
|
let animate = gBrowser.animationsEnabled;
|
|
|
|
tab.groupingTabsData = {
|
|
finished: !animate,
|
|
};
|
|
|
|
|
|
// Animate left selected tabs
|
|
|
|
let insertAtPos = draggedTabPos - 1;
|
|
for (let i = selectedTabs.indexOf(tab) - 1; i > -1; i--) {
|
|
let movingTab = selectedTabs[i];
|
|
insertAtPos = newIndex(movingTab, insertAtPos);
|
|
|
|
if (animate) {
|
|
movingTab.groupingTabsData = {};
|
|
addAnimationData(movingTab, insertAtPos, "left");
|
|
} else {
|
|
gBrowser.moveTabTo(movingTab, insertAtPos);
|
|
}
|
|
insertAtPos--;
|
|
}
|
|
|
|
// Animate right selected tabs
|
|
|
|
insertAtPos = draggedTabPos + 1;
|
|
for (let i = selectedTabs.indexOf(tab) + 1; i < selectedTabs.length; i++) {
|
|
let movingTab = selectedTabs[i];
|
|
insertAtPos = newIndex(movingTab, insertAtPos);
|
|
|
|
if (animate) {
|
|
movingTab.groupingTabsData = {};
|
|
addAnimationData(movingTab, insertAtPos, "right");
|
|
} else {
|
|
gBrowser.moveTabTo(movingTab, insertAtPos);
|
|
}
|
|
insertAtPos++;
|
|
}
|
|
|
|
// Slide the relevant tabs to their new position.
|
|
let rtl = Services.locale.isAppLocaleRTL ? -1 : 1;
|
|
for (let t of this._getVisibleTabs()) {
|
|
if (t.groupingTabsData && t.groupingTabsData.translateX) {
|
|
let translateX = rtl * t.groupingTabsData.translateX;
|
|
t.style.transform = "translateX(" + translateX + "px)";
|
|
}
|
|
}
|
|
|
|
function newIndex(aTab, index) {
|
|
// Don't allow mixing pinned and unpinned tabs.
|
|
if (aTab.pinned) {
|
|
return Math.min(index, gBrowser._numPinnedTabs - 1);
|
|
}
|
|
return Math.max(index, gBrowser._numPinnedTabs);
|
|
}
|
|
|
|
function addAnimationData(movingTab, movingTabNewIndex, side) {
|
|
let movingTabOldIndex = movingTab._tPos;
|
|
|
|
if (movingTabOldIndex == movingTabNewIndex) {
|
|
// movingTab is already at the right position
|
|
// and thus don't need to be animated.
|
|
return;
|
|
}
|
|
|
|
let movingTabWidth = movingTab.boxObject.width;
|
|
let shift = (movingTabNewIndex - movingTabOldIndex) * movingTabWidth;
|
|
|
|
movingTab.groupingTabsData.animate = true;
|
|
movingTab.setAttribute("tab-grouping", "true");
|
|
|
|
movingTab.groupingTabsData.translateX = shift;
|
|
|
|
let onTransitionEnd = transitionendEvent => {
|
|
if (transitionendEvent.propertyName != "transform" ||
|
|
transitionendEvent.originalTarget != movingTab) {
|
|
return;
|
|
}
|
|
movingTab.removeEventListener("transitionend", onTransitionEnd);
|
|
movingTab.groupingTabsData.newIndex = movingTabNewIndex;
|
|
movingTab.groupingTabsData.animate = false;
|
|
};
|
|
|
|
movingTab.addEventListener("transitionend", onTransitionEnd);
|
|
|
|
// Add animation data for tabs between movingTab (selected
|
|
// tab moving towards the dragged tab) and draggedTab.
|
|
// Those tabs in the middle should move in
|
|
// the opposite direction of movingTab.
|
|
|
|
let lowerIndex = Math.min(movingTabOldIndex, draggedTabPos);
|
|
let higherIndex = Math.max(movingTabOldIndex, draggedTabPos);
|
|
|
|
for (let i = lowerIndex + 1; i < higherIndex; i++) {
|
|
let middleTab = gBrowser.visibleTabs[i];
|
|
|
|
if (middleTab.pinned != movingTab.pinned) {
|
|
// Don't mix pinned and unpinned tabs
|
|
break;
|
|
}
|
|
|
|
if (middleTab.multiselected) {
|
|
// Skip because this selected tab should
|
|
// be shifted towards the dragged Tab.
|
|
continue;
|
|
}
|
|
|
|
if (!middleTab.groupingTabsData || !middleTab.groupingTabsData.translateX) {
|
|
middleTab.groupingTabsData = { translateX: 0};
|
|
}
|
|
if (side == "left") {
|
|
middleTab.groupingTabsData.translateX -= movingTabWidth;
|
|
} else {
|
|
middleTab.groupingTabsData.translateX += movingTabWidth;
|
|
}
|
|
|
|
middleTab.setAttribute("tab-grouping", "true");
|
|
}
|
|
}
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="_finishGroupSelectedTabs">
|
|
<parameter name="tab"/>
|
|
<body><![CDATA[
|
|
if (!tab.groupingTabsData || tab.groupingTabsData.finished)
|
|
return;
|
|
|
|
tab.groupingTabsData.finished = true;
|
|
|
|
let selectedTabs = gBrowser.selectedTabs;
|
|
let tabIndex = selectedTabs.indexOf(tab);
|
|
|
|
// Moving left tabs
|
|
for (let i = tabIndex - 1; i > -1; i--) {
|
|
let movingTab = selectedTabs[i];
|
|
if (movingTab.groupingTabsData.newIndex) {
|
|
gBrowser.moveTabTo(movingTab, movingTab.groupingTabsData.newIndex);
|
|
}
|
|
}
|
|
|
|
// Moving right tabs
|
|
for (let i = tabIndex + 1; i < selectedTabs.length; i++) {
|
|
let movingTab = selectedTabs[i];
|
|
if (movingTab.groupingTabsData.newIndex) {
|
|
gBrowser.moveTabTo(movingTab, movingTab.groupingTabsData.newIndex);
|
|
}
|
|
}
|
|
|
|
for (let t of this._getVisibleTabs()) {
|
|
t.style.transform = "";
|
|
t.removeAttribute("tab-grouping");
|
|
delete t.groupingTabsData;
|
|
}
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="_isGroupTabsAnimationOver">
|
|
<body><![CDATA[
|
|
for (let tab of gBrowser.selectedTabs) {
|
|
if (tab.groupingTabsData && tab.groupingTabsData.animate)
|
|
return false;
|
|
}
|
|
return true;
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="handleEvent">
|
|
<parameter name="aEvent"/>
|
|
<body><![CDATA[
|
|
switch (aEvent.type) {
|
|
case "resize":
|
|
if (aEvent.target != window)
|
|
break;
|
|
|
|
this._updateCloseButtons();
|
|
this._handleTabSelect(true);
|
|
break;
|
|
case "mouseout":
|
|
// If the "related target" (the node to which the pointer went) is not
|
|
// a child of the current document, the mouse just left the window.
|
|
let relatedTarget = aEvent.relatedTarget;
|
|
if (relatedTarget && relatedTarget.ownerDocument == document)
|
|
break;
|
|
case "mousemove":
|
|
if (document.getElementById("tabContextMenu").state != "open")
|
|
this._unlockTabSizing();
|
|
break;
|
|
}
|
|
]]></body>
|
|
</method>
|
|
|
|
<field name="_animateElement">
|
|
this.arrowScrollbox._scrollButtonDown;
|
|
</field>
|
|
|
|
<method name="_notifyBackgroundTab">
|
|
<parameter name="aTab"/>
|
|
<body><![CDATA[
|
|
if (aTab.pinned || aTab.hidden)
|
|
return;
|
|
|
|
var scrollRect = this.arrowScrollbox.scrollClientRect;
|
|
var tab = aTab.getBoundingClientRect();
|
|
|
|
// DOMRect left/right properties are immutable.
|
|
tab = {left: tab.left, right: tab.right};
|
|
|
|
// Is the new tab already completely visible?
|
|
if (scrollRect.left <= tab.left && tab.right <= scrollRect.right)
|
|
return;
|
|
|
|
if (this.arrowScrollbox.smoothScroll) {
|
|
let selectedTab = this.selectedItem;
|
|
let selected = !selectedTab.pinned &&
|
|
selectedTab.getBoundingClientRect();
|
|
|
|
// Can we make both the new tab and the selected tab completely visible?
|
|
if (!selected ||
|
|
Math.max(tab.right - selected.left, selected.right - tab.left) <=
|
|
scrollRect.width) {
|
|
this.arrowScrollbox.ensureElementIsVisible(aTab);
|
|
return;
|
|
}
|
|
|
|
this.arrowScrollbox.scrollByPixels(this.arrowScrollbox._isRTLScrollbox ?
|
|
selected.right - scrollRect.right :
|
|
selected.left - scrollRect.left);
|
|
}
|
|
|
|
if (!this._animateElement.hasAttribute("highlight")) {
|
|
this._animateElement.setAttribute("highlight", "true");
|
|
setTimeout(function(ele) {
|
|
ele.removeAttribute("highlight");
|
|
}, 150, this._animateElement);
|
|
}
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="_getDragTargetTab">
|
|
<parameter name="event"/>
|
|
<parameter name="isLink"/>
|
|
<body><![CDATA[
|
|
let tab = event.target.localName == "tab" ? event.target : null;
|
|
if (tab && isLink) {
|
|
let boxObject = tab.boxObject;
|
|
if (event.screenX < boxObject.screenX + boxObject.width * .25 ||
|
|
event.screenX > boxObject.screenX + boxObject.width * .75)
|
|
return null;
|
|
}
|
|
return tab;
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="_getDropIndex">
|
|
<parameter name="event"/>
|
|
<parameter name="isLink"/>
|
|
<body><![CDATA[
|
|
var tabs = this.children;
|
|
var tab = this._getDragTargetTab(event, isLink);
|
|
if (window.getComputedStyle(this).direction == "ltr") {
|
|
for (let i = tab ? tab._tPos : 0; i < tabs.length; i++)
|
|
if (event.screenX < tabs[i].boxObject.screenX + tabs[i].boxObject.width / 2)
|
|
return i;
|
|
} else {
|
|
for (let i = tab ? tab._tPos : 0; i < tabs.length; i++)
|
|
if (event.screenX > tabs[i].boxObject.screenX + tabs[i].boxObject.width / 2)
|
|
return i;
|
|
}
|
|
return tabs.length;
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="_getDropEffectForTabDrag">
|
|
<parameter name="event"/>
|
|
<body><![CDATA[
|
|
var dt = event.dataTransfer;
|
|
|
|
let isMovingTabs = dt.mozItemCount > 0;
|
|
for (let i = 0; i < dt.mozItemCount; i++) {
|
|
// tabs are always added as the first type
|
|
let types = dt.mozTypesAt(0);
|
|
if (types[0] != TAB_DROP_TYPE) {
|
|
isMovingTabs = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (isMovingTabs) {
|
|
let sourceNode = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
|
|
if (sourceNode instanceof XULElement &&
|
|
sourceNode.localName == "tab" &&
|
|
sourceNode.ownerGlobal.isChromeWindow &&
|
|
sourceNode.ownerDocument.documentElement.getAttribute("windowtype") == "navigator:browser" &&
|
|
sourceNode.ownerGlobal.gBrowser.tabContainer == sourceNode.parentNode) {
|
|
// Do not allow transfering a private tab to a non-private window
|
|
// and vice versa.
|
|
if (PrivateBrowsingUtils.isWindowPrivate(window) !=
|
|
PrivateBrowsingUtils.isWindowPrivate(sourceNode.ownerGlobal))
|
|
return "none";
|
|
|
|
if (window.gMultiProcessBrowser !=
|
|
sourceNode.ownerGlobal.gMultiProcessBrowser)
|
|
return "none";
|
|
|
|
return dt.dropEffect == "copy" ? "copy" : "move";
|
|
}
|
|
}
|
|
|
|
if (browserDragAndDrop.canDropLink(event)) {
|
|
return "link";
|
|
}
|
|
return "none";
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="_handleNewTab">
|
|
<parameter name="tab"/>
|
|
<body><![CDATA[
|
|
if (tab.parentNode != this) {
|
|
return;
|
|
}
|
|
tab._fullyOpen = true;
|
|
gBrowser.tabAnimationsInProgress--;
|
|
|
|
this._updateCloseButtons();
|
|
|
|
if (tab.getAttribute("selected") == "true") {
|
|
this._handleTabSelect();
|
|
} else if (!tab.hasAttribute("skipbackgroundnotify")) {
|
|
this._notifyBackgroundTab(tab);
|
|
}
|
|
|
|
// XXXmano: this is a temporary workaround for bug 345399
|
|
// We need to manually update the scroll buttons disabled state
|
|
// if a tab was inserted to the overflow area or removed from it
|
|
// without any scrolling and when the tabbar has already
|
|
// overflowed.
|
|
this.arrowScrollbox._updateScrollButtonsDisabledState();
|
|
|
|
// Preload the next about:newtab if there isn't one already.
|
|
gBrowser._createPreloadBrowser();
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="_canAdvanceToTab">
|
|
<parameter name="aTab"/>
|
|
<body>
|
|
<![CDATA[
|
|
return !aTab.closing;
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="getRelatedElement">
|
|
<parameter name="aTab"/>
|
|
<body>
|
|
<![CDATA[
|
|
if (!aTab) {
|
|
return null;
|
|
}
|
|
|
|
// Cannot access gBrowser before it's initialized.
|
|
if (!gBrowser) {
|
|
return this.tabbox.tabpanels.firstElementChild;
|
|
}
|
|
|
|
// If the tab's browser is lazy, we need to `_insertBrowser` in order
|
|
// to have a linkedPanel. This will also serve to bind the browser
|
|
// and make it ready to use when the tab is selected.
|
|
gBrowser._insertBrowser(aTab);
|
|
return document.getElementById(aTab.linkedPanel);
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="_updateNewTabVisibility">
|
|
<body><![CDATA[
|
|
// Helper functions to help deal with customize mode wrapping some items
|
|
let wrap = n => n.parentNode.localName == "toolbarpaletteitem" ? n.parentNode : n;
|
|
let unwrap = n => n && n.localName == "toolbarpaletteitem" ? n.firstElementChild : n;
|
|
|
|
// Starting from the tabs element, find the next sibling that:
|
|
// - isn't hidden; and
|
|
// - isn't one of the titlebar placeholder elements; and
|
|
// - isn't the all-tabs button.
|
|
// If it's the new tab button, consider the new tab button adjacent to the tabs.
|
|
// If the new tab button is marked as adjacent and the tabstrip doesn't
|
|
// overflow, we'll display the 'new tab' button inline in the tabstrip.
|
|
// In all other cases, the separate new tab button is displayed in its
|
|
// customized location.
|
|
let sib = this;
|
|
do {
|
|
sib = unwrap(wrap(sib).nextElementSibling);
|
|
} while (sib && (sib.hidden ||
|
|
sib.getAttribute("skipintoolbarset") == "true" ||
|
|
sib.id == "alltabs-button"));
|
|
|
|
const kAttr = "hasadjacentnewtabbutton";
|
|
if (sib && sib.id == "new-tab-button") {
|
|
this.setAttribute(kAttr, "true");
|
|
} else {
|
|
this.removeAttribute(kAttr);
|
|
}
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="onWidgetAfterDOMChange">
|
|
<parameter name="aNode"/>
|
|
<parameter name="aNextNode"/>
|
|
<parameter name="aContainer"/>
|
|
<body><![CDATA[
|
|
if (aContainer.ownerDocument == document &&
|
|
aContainer.id == "TabsToolbar") {
|
|
this._updateNewTabVisibility();
|
|
}
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="onAreaNodeRegistered">
|
|
<parameter name="aArea"/>
|
|
<parameter name="aContainer"/>
|
|
<body><![CDATA[
|
|
if (aContainer.ownerDocument == document &&
|
|
aArea == "TabsToolbar") {
|
|
this._updateNewTabVisibility();
|
|
}
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="onAreaReset">
|
|
<parameter name="aArea"/>
|
|
<parameter name="aContainer"/>
|
|
<body><![CDATA[
|
|
this.onAreaNodeRegistered(aArea, aContainer);
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="_hiddenSoundPlayingStatusChanged">
|
|
<parameter name="tab"/>
|
|
<parameter name="opts"/>
|
|
<body><![CDATA[
|
|
let closed = opts && opts.closed;
|
|
if (!closed && tab.soundPlaying && tab.hidden) {
|
|
this._hiddenSoundPlayingTabs.add(tab);
|
|
this.setAttribute("hiddensoundplaying", "true");
|
|
} else {
|
|
this._hiddenSoundPlayingTabs.delete(tab);
|
|
if (this._hiddenSoundPlayingTabs.size == 0) {
|
|
this.removeAttribute("hiddensoundplaying");
|
|
}
|
|
}
|
|
]]></body>
|
|
</method>
|
|
</implementation>
|
|
|
|
<handlers>
|
|
<handler event="TabSelect"><![CDATA[
|
|
this._handleTabSelect();
|
|
]]></handler>
|
|
|
|
<handler event="TabClose"><![CDATA[
|
|
this._hiddenSoundPlayingStatusChanged(event.target, {closed: true});
|
|
]]></handler>
|
|
|
|
<handler event="TabAttrModified"><![CDATA[
|
|
if (event.detail.changed.includes("soundplaying") && event.target.hidden) {
|
|
this._hiddenSoundPlayingStatusChanged(event.target);
|
|
}
|
|
]]></handler>
|
|
|
|
<handler event="TabHide"><![CDATA[
|
|
if (event.target.soundPlaying) {
|
|
this._hiddenSoundPlayingStatusChanged(event.target);
|
|
}
|
|
]]></handler>
|
|
|
|
<handler event="TabShow"><![CDATA[
|
|
if (event.target.soundPlaying) {
|
|
this._hiddenSoundPlayingStatusChanged(event.target);
|
|
}
|
|
]]></handler>
|
|
|
|
<handler event="transitionend"><![CDATA[
|
|
if (event.propertyName != "max-width") {
|
|
return;
|
|
}
|
|
|
|
var tab = event.target;
|
|
|
|
if (tab.getAttribute("fadein") == "true") {
|
|
if (tab._fullyOpen) {
|
|
this._updateCloseButtons();
|
|
} else {
|
|
this._handleNewTab(tab);
|
|
}
|
|
} else if (tab.closing) {
|
|
gBrowser._endRemoveTab(tab);
|
|
}
|
|
]]></handler>
|
|
|
|
<handler event="dblclick"><![CDATA[
|
|
// When the tabbar has an unified appearance with the titlebar
|
|
// and menubar, a double-click in it should have the same behavior
|
|
// as double-clicking the titlebar
|
|
if (TabsInTitlebar.enabled || this.parentNode._dragBindingAlive)
|
|
return;
|
|
|
|
if (event.button != 0 ||
|
|
event.originalTarget.localName != "box")
|
|
return;
|
|
|
|
if (!this._blockDblClick)
|
|
BrowserOpenTab();
|
|
|
|
event.preventDefault();
|
|
]]></handler>
|
|
|
|
<handler event="click" button="0" phase="capturing"><![CDATA[
|
|
/* Catches extra clicks meant for the in-tab close button.
|
|
* Placed here to avoid leaking (a temporary handler added from the
|
|
* in-tab close button binding would close over the tab and leak it
|
|
* until the handler itself was removed). (bug 897751)
|
|
*
|
|
* The only sequence in which a second click event (i.e. dblclik)
|
|
* can be dispatched on an in-tab close button is when it is shown
|
|
* after the first click (i.e. the first click event was dispatched
|
|
* on the tab). This happens when we show the close button only on
|
|
* the active tab. (bug 352021)
|
|
* The only sequence in which a third click event can be dispatched
|
|
* on an in-tab close button is when the tab was opened with a
|
|
* double click on the tabbar. (bug 378344)
|
|
* In both cases, it is most likely that the close button area has
|
|
* been accidentally clicked, therefore we do not close the tab.
|
|
*
|
|
* We don't want to ignore processing of more than one click event,
|
|
* though, since the user might actually be repeatedly clicking to
|
|
* close many tabs at once.
|
|
*/
|
|
let target = event.originalTarget;
|
|
if (target.classList.contains("tab-close-button")) {
|
|
// We preemptively set this to allow the closing-multiple-tabs-
|
|
// in-a-row case.
|
|
if (this._blockDblClick) {
|
|
target._ignoredCloseButtonClicks = true;
|
|
} else if (event.detail > 1 && !target._ignoredCloseButtonClicks) {
|
|
target._ignoredCloseButtonClicks = true;
|
|
event.stopPropagation();
|
|
return;
|
|
} else {
|
|
// Reset the "ignored click" flag
|
|
target._ignoredCloseButtonClicks = false;
|
|
}
|
|
}
|
|
|
|
/* Protects from close-tab-button errant doubleclick:
|
|
* Since we're removing the event target, if the user
|
|
* double-clicks the button, the dblclick event will be dispatched
|
|
* with the tabbar as its event target (and explicit/originalTarget),
|
|
* which treats that as a mouse gesture for opening a new tab.
|
|
* In this context, we're manually blocking the dblclick event.
|
|
*/
|
|
if (this._blockDblClick) {
|
|
if (!("_clickedTabBarOnce" in this)) {
|
|
this._clickedTabBarOnce = true;
|
|
return;
|
|
}
|
|
delete this._clickedTabBarOnce;
|
|
this._blockDblClick = false;
|
|
}
|
|
]]></handler>
|
|
|
|
<handler event="click"><![CDATA[
|
|
if (event.button != 1) {
|
|
return;
|
|
}
|
|
|
|
if (event.target.localName == "tab") {
|
|
gBrowser.removeTab(event.target, {
|
|
animate: true,
|
|
byMouse: event.mozInputSource == MouseEvent.MOZ_SOURCE_MOUSE,
|
|
});
|
|
} else if (event.originalTarget.localName == "box") {
|
|
// The user middleclicked an open space on the tabstrip. This could
|
|
// be because they intend to open a new tab, but it could also be
|
|
// because they just removed a tab and they now middleclicked on the
|
|
// resulting space while that tab is closing. In that case, we don't
|
|
// want to open a tab. So if we're removing one or more tabs, and
|
|
// the tab click is before the end of the last visible tab, we do
|
|
// nothing.
|
|
if (gBrowser._removingTabs.length) {
|
|
let visibleTabs = this._getVisibleTabs();
|
|
let ltr = (window.getComputedStyle(this).direction == "ltr");
|
|
let lastTab = visibleTabs[visibleTabs.length - 1];
|
|
let endOfTab = lastTab.getBoundingClientRect()[ltr ? "right" : "left"];
|
|
if ((ltr && event.clientX > endOfTab) ||
|
|
(!ltr && event.clientX < endOfTab)) {
|
|
BrowserOpenTab();
|
|
}
|
|
} else {
|
|
BrowserOpenTab();
|
|
}
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
event.stopPropagation();
|
|
]]></handler>
|
|
|
|
<handler event="keydown" group="system"><![CDATA[
|
|
let {altKey, shiftKey} = event;
|
|
let [accel, nonAccel] = AppConstants.platform == "macosx" ? [event.metaKey, event.ctrlKey] : [event.ctrlKey, event.metaKey];
|
|
|
|
let keyComboForMove = accel && shiftKey && !altKey && !nonAccel;
|
|
let keyComboForFocus = accel && !shiftKey && !altKey && !nonAccel;
|
|
|
|
if (!keyComboForMove && !keyComboForFocus) {
|
|
return;
|
|
}
|
|
|
|
// Don't check if the event was already consumed because tab navigation
|
|
// should work always for better user experience.
|
|
let {visibleTabs, selectedTab} = gBrowser;
|
|
let {arrowKeysShouldWrap} = this;
|
|
let focusedTabIndex = this.ariaFocusedIndex;
|
|
if (focusedTabIndex == -1) {
|
|
focusedTabIndex = visibleTabs.indexOf(selectedTab);
|
|
}
|
|
let lastFocusedTabIndex = focusedTabIndex;
|
|
switch (event.keyCode) {
|
|
case KeyEvent.DOM_VK_UP:
|
|
if (keyComboForMove) {
|
|
gBrowser.moveTabBackward();
|
|
} else {
|
|
focusedTabIndex--;
|
|
}
|
|
break;
|
|
case KeyEvent.DOM_VK_DOWN:
|
|
if (keyComboForMove) {
|
|
gBrowser.moveTabForward();
|
|
} else {
|
|
focusedTabIndex++;
|
|
}
|
|
break;
|
|
case KeyEvent.DOM_VK_RIGHT:
|
|
case KeyEvent.DOM_VK_LEFT:
|
|
if (keyComboForMove) {
|
|
gBrowser.moveTabOver(event);
|
|
} else {
|
|
let isRTL = Services.locale.isAppLocaleRTL;
|
|
if ((!isRTL && event.keyCode == KeyEvent.DOM_VK_RIGHT) ||
|
|
(isRTL && event.keyCode == KeyEvent.DOM_VK_LEFT)) {
|
|
focusedTabIndex++;
|
|
} else {
|
|
focusedTabIndex--;
|
|
}
|
|
}
|
|
break;
|
|
case KeyEvent.DOM_VK_HOME:
|
|
if (keyComboForMove) {
|
|
gBrowser.moveTabToStart();
|
|
} else {
|
|
focusedTabIndex = 0;
|
|
}
|
|
break;
|
|
case KeyEvent.DOM_VK_END:
|
|
if (keyComboForMove) {
|
|
gBrowser.moveTabToEnd();
|
|
} else {
|
|
focusedTabIndex = visibleTabs.length - 1;
|
|
}
|
|
break;
|
|
case KeyEvent.DOM_VK_SPACE:
|
|
if (visibleTabs[lastFocusedTabIndex].multiselected) {
|
|
gBrowser.removeFromMultiSelectedTabs(visibleTabs[lastFocusedTabIndex]);
|
|
} else {
|
|
gBrowser.addToMultiSelectedTabs(visibleTabs[lastFocusedTabIndex], false);
|
|
}
|
|
break;
|
|
default:
|
|
// Consume the keydown event for the above keyboard
|
|
// shortcuts only.
|
|
return;
|
|
}
|
|
|
|
if (arrowKeysShouldWrap) {
|
|
if (focusedTabIndex >= visibleTabs.length) {
|
|
focusedTabIndex = 0;
|
|
} else if (focusedTabIndex < 0) {
|
|
focusedTabIndex = visibleTabs.length - 1;
|
|
}
|
|
} else {
|
|
focusedTabIndex = Math.min(visibleTabs.length - 1, Math.max(0, focusedTabIndex));
|
|
}
|
|
|
|
if (keyComboForFocus &&
|
|
focusedTabIndex != lastFocusedTabIndex) {
|
|
this.ariaFocusedItem = visibleTabs[focusedTabIndex];
|
|
}
|
|
|
|
event.preventDefault();
|
|
]]></handler>
|
|
|
|
<handler event="dragstart"><![CDATA[
|
|
var tab = this._getDragTargetTab(event, false);
|
|
if (!tab || this._isCustomizing)
|
|
return;
|
|
|
|
let selectedTabs = gBrowser.selectedTabs;
|
|
let otherSelectedTabs = selectedTabs.filter(selectedTab => selectedTab != tab);
|
|
let dataTransferOrderedTabs = [tab].concat(otherSelectedTabs);
|
|
|
|
let dt = event.dataTransfer;
|
|
for (let i = 0; i < dataTransferOrderedTabs.length; i++) {
|
|
let dtTab = dataTransferOrderedTabs[i];
|
|
|
|
dt.mozSetDataAt(TAB_DROP_TYPE, dtTab, i);
|
|
let dtBrowser = dtTab.linkedBrowser;
|
|
|
|
// We must not set text/x-moz-url or text/plain data here,
|
|
// otherwise trying to detach the tab by dropping it on the desktop
|
|
// may result in an "internet shortcut"
|
|
dt.mozSetDataAt("text/x-moz-text-internal", dtBrowser.currentURI.spec, i);
|
|
}
|
|
|
|
// Set the cursor to an arrow during tab drags.
|
|
dt.mozCursor = "default";
|
|
|
|
// Set the tab as the source of the drag, which ensures we have a stable
|
|
// node to deliver the `dragend` event. See bug 1345473.
|
|
dt.addElement(tab);
|
|
|
|
if (tab.multiselected) {
|
|
this._groupSelectedTabs(tab);
|
|
}
|
|
|
|
// Create a canvas to which we capture the current tab.
|
|
// Until canvas is HiDPI-aware (bug 780362), we need to scale the desired
|
|
// canvas size (in CSS pixels) to the window's backing resolution in order
|
|
// to get a full-resolution drag image for use on HiDPI displays.
|
|
let windowUtils = window.windowUtils;
|
|
let scale = windowUtils.screenPixelsPerCSSPixel / windowUtils.fullZoom;
|
|
let canvas = this._dndCanvas;
|
|
if (!canvas) {
|
|
this._dndCanvas = canvas =
|
|
document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
|
|
canvas.style.width = "100%";
|
|
canvas.style.height = "100%";
|
|
canvas.mozOpaque = true;
|
|
}
|
|
|
|
canvas.width = 160 * scale;
|
|
canvas.height = 90 * scale;
|
|
let toDrag = canvas;
|
|
let dragImageOffset = -16;
|
|
let browser = tab.linkedBrowser;
|
|
if (gMultiProcessBrowser) {
|
|
var context = canvas.getContext("2d");
|
|
context.fillStyle = "white";
|
|
context.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
|
let captureListener;
|
|
let platform = AppConstants.platform;
|
|
// On Windows and Mac we can update the drag image during a drag
|
|
// using updateDragImage. On Linux, we can use a panel.
|
|
if (platform == "win" || platform == "macosx") {
|
|
captureListener = function() {
|
|
dt.updateDragImage(canvas, dragImageOffset, dragImageOffset);
|
|
};
|
|
} else {
|
|
// Create a panel to use it in setDragImage
|
|
// which will tell xul to render a panel that follows
|
|
// the pointer while a dnd session is on.
|
|
if (!this._dndPanel) {
|
|
this._dndCanvas = canvas;
|
|
this._dndPanel = document.createXULElement("panel");
|
|
this._dndPanel.className = "dragfeedback-tab";
|
|
this._dndPanel.setAttribute("type", "drag");
|
|
let wrapper = document.createElementNS("http://www.w3.org/1999/xhtml", "div");
|
|
wrapper.style.width = "160px";
|
|
wrapper.style.height = "90px";
|
|
wrapper.appendChild(canvas);
|
|
this._dndPanel.appendChild(wrapper);
|
|
document.documentElement.appendChild(this._dndPanel);
|
|
}
|
|
toDrag = this._dndPanel;
|
|
}
|
|
// PageThumb is async with e10s but that's fine
|
|
// since we can update the image during the dnd.
|
|
PageThumbs.captureToCanvas(browser, canvas, captureListener);
|
|
} else {
|
|
// For the non e10s case we can just use PageThumbs
|
|
// sync, so let's use the canvas for setDragImage.
|
|
PageThumbs.captureToCanvas(browser, canvas);
|
|
dragImageOffset = dragImageOffset * scale;
|
|
}
|
|
dt.setDragImage(toDrag, dragImageOffset, dragImageOffset);
|
|
|
|
// _dragData.offsetX/Y give the coordinates that the mouse should be
|
|
// positioned relative to the corner of the new window created upon
|
|
// dragend such that the mouse appears to have the same position
|
|
// relative to the corner of the dragged tab.
|
|
function clientX(ele) {
|
|
return ele.getBoundingClientRect().left;
|
|
}
|
|
let tabOffsetX = clientX(tab) - clientX(this);
|
|
tab._dragData = {
|
|
offsetX: event.screenX - window.screenX - tabOffsetX,
|
|
offsetY: event.screenY - window.screenY,
|
|
scrollX: this.arrowScrollbox._scrollbox.scrollLeft,
|
|
screenX: event.screenX,
|
|
movingTabs: (tab.multiselected ? gBrowser.selectedTabs : [tab])
|
|
.filter(t => t.pinned == tab.pinned),
|
|
};
|
|
|
|
event.stopPropagation();
|
|
]]></handler>
|
|
|
|
<handler event="dragover"><![CDATA[
|
|
var effects = this._getDropEffectForTabDrag(event);
|
|
|
|
var ind = this._tabDropIndicator;
|
|
if (effects == "" || effects == "none") {
|
|
ind.collapsed = true;
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
var arrowScrollbox = this.arrowScrollbox;
|
|
var ltr = (window.getComputedStyle(this).direction == "ltr");
|
|
|
|
// autoscroll the tab strip if we drag over the scroll
|
|
// buttons, even if we aren't dragging a tab, but then
|
|
// return to avoid drawing the drop indicator
|
|
var pixelsToScroll = 0;
|
|
if (this.getAttribute("overflow") == "true") {
|
|
var targetAnonid = event.originalTarget.getAttribute("anonid");
|
|
switch (targetAnonid) {
|
|
case "scrollbutton-up":
|
|
pixelsToScroll = arrowScrollbox.scrollIncrement * -1;
|
|
break;
|
|
case "scrollbutton-down":
|
|
pixelsToScroll = arrowScrollbox.scrollIncrement;
|
|
break;
|
|
}
|
|
if (pixelsToScroll)
|
|
arrowScrollbox.scrollByPixels((ltr ? 1 : -1) * pixelsToScroll, true);
|
|
}
|
|
|
|
let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
|
|
if ((effects == "move" || effects == "copy") &&
|
|
this == draggedTab.parentNode) {
|
|
ind.collapsed = true;
|
|
|
|
if (!this._isGroupTabsAnimationOver()) {
|
|
// Wait for grouping tabs animation to finish
|
|
return;
|
|
}
|
|
this._finishGroupSelectedTabs(draggedTab);
|
|
|
|
if (effects == "move") {
|
|
this._animateTabMove(event);
|
|
return;
|
|
}
|
|
}
|
|
|
|
this._finishAnimateTabMove();
|
|
|
|
if (effects == "link") {
|
|
let tab = this._getDragTargetTab(event, true);
|
|
if (tab) {
|
|
if (!this._dragTime)
|
|
this._dragTime = Date.now();
|
|
if (Date.now() >= this._dragTime + this._dragOverDelay)
|
|
this.selectedItem = tab;
|
|
ind.collapsed = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
var rect = arrowScrollbox.getBoundingClientRect();
|
|
var newMargin;
|
|
if (pixelsToScroll) {
|
|
// if we are scrolling, put the drop indicator at the edge
|
|
// so that it doesn't jump while scrolling
|
|
let scrollRect = arrowScrollbox.scrollClientRect;
|
|
let minMargin = scrollRect.left - rect.left;
|
|
let maxMargin = Math.min(minMargin + scrollRect.width,
|
|
scrollRect.right);
|
|
if (!ltr)
|
|
[minMargin, maxMargin] = [this.clientWidth - maxMargin,
|
|
this.clientWidth - minMargin];
|
|
newMargin = (pixelsToScroll > 0) ? maxMargin : minMargin;
|
|
} else {
|
|
let newIndex = this._getDropIndex(event, effects == "link");
|
|
if (newIndex == this.children.length) {
|
|
let tabRect = this.children[newIndex - 1].getBoundingClientRect();
|
|
if (ltr)
|
|
newMargin = tabRect.right - rect.left;
|
|
else
|
|
newMargin = rect.right - tabRect.left;
|
|
} else {
|
|
let tabRect = this.children[newIndex].getBoundingClientRect();
|
|
if (ltr)
|
|
newMargin = tabRect.left - rect.left;
|
|
else
|
|
newMargin = rect.right - tabRect.right;
|
|
}
|
|
}
|
|
|
|
ind.collapsed = false;
|
|
|
|
newMargin += ind.clientWidth / 2;
|
|
if (!ltr)
|
|
newMargin *= -1;
|
|
|
|
ind.style.transform = "translate(" + Math.round(newMargin) + "px)";
|
|
ind.style.marginInlineStart = (-ind.clientWidth) + "px";
|
|
]]></handler>
|
|
|
|
<handler event="drop"><![CDATA[
|
|
var dt = event.dataTransfer;
|
|
var dropEffect = dt.dropEffect;
|
|
var draggedTab;
|
|
let movingTabs;
|
|
if (dt.mozTypesAt(0)[0] == TAB_DROP_TYPE) { // tab copy or move
|
|
draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
|
|
// not our drop then
|
|
if (!draggedTab)
|
|
return;
|
|
movingTabs = draggedTab._dragData.movingTabs;
|
|
draggedTab.parentNode._finishGroupSelectedTabs(draggedTab);
|
|
}
|
|
|
|
this._tabDropIndicator.collapsed = true;
|
|
event.stopPropagation();
|
|
if (draggedTab && dropEffect == "copy") {
|
|
// copy the dropped tab (wherever it's from)
|
|
let newIndex = this._getDropIndex(event, false);
|
|
let draggedTabCopy;
|
|
for (let tab of movingTabs) {
|
|
let newTab = gBrowser.duplicateTab(tab);
|
|
gBrowser.moveTabTo(newTab, newIndex++);
|
|
if (tab == draggedTab)
|
|
draggedTabCopy = newTab;
|
|
}
|
|
if (draggedTab.parentNode != this || event.shiftKey) {
|
|
this.selectedItem = draggedTabCopy;
|
|
}
|
|
} else if (draggedTab && draggedTab.parentNode == this) {
|
|
let oldTranslateX = Math.round(draggedTab._dragData.translateX);
|
|
let tabWidth = Math.round(draggedTab._dragData.tabWidth);
|
|
let translateOffset = oldTranslateX % tabWidth;
|
|
let newTranslateX = oldTranslateX - translateOffset;
|
|
if (oldTranslateX > 0 && translateOffset > tabWidth / 2) {
|
|
newTranslateX += tabWidth;
|
|
} else if (oldTranslateX < 0 && -translateOffset > tabWidth / 2) {
|
|
newTranslateX -= tabWidth;
|
|
}
|
|
|
|
let dropIndex = "animDropIndex" in draggedTab._dragData &&
|
|
draggedTab._dragData.animDropIndex;
|
|
let incrementDropIndex = true;
|
|
if (dropIndex && dropIndex > movingTabs[0]._tPos) {
|
|
dropIndex--;
|
|
incrementDropIndex = false;
|
|
}
|
|
|
|
let animate = gBrowser.animationsEnabled;
|
|
if (oldTranslateX && oldTranslateX != newTranslateX && animate) {
|
|
for (let tab of movingTabs) {
|
|
tab.setAttribute("tabdrop-samewindow", "true");
|
|
tab.style.transform = "translateX(" + newTranslateX + "px)";
|
|
let onTransitionEnd = transitionendEvent => {
|
|
if (transitionendEvent.propertyName != "transform" ||
|
|
transitionendEvent.originalTarget != tab) {
|
|
return;
|
|
}
|
|
tab.removeEventListener("transitionend", onTransitionEnd);
|
|
|
|
tab.removeAttribute("tabdrop-samewindow");
|
|
|
|
this._finishAnimateTabMove();
|
|
if (dropIndex !== false) {
|
|
gBrowser.moveTabTo(tab, dropIndex);
|
|
if (incrementDropIndex)
|
|
dropIndex++;
|
|
}
|
|
|
|
gBrowser.syncThrobberAnimations(tab);
|
|
};
|
|
tab.addEventListener("transitionend", onTransitionEnd);
|
|
}
|
|
} else {
|
|
this._finishAnimateTabMove();
|
|
if (dropIndex !== false) {
|
|
for (let tab of movingTabs) {
|
|
gBrowser.moveTabTo(tab, dropIndex);
|
|
if (incrementDropIndex)
|
|
dropIndex++;
|
|
}
|
|
}
|
|
}
|
|
} else if (draggedTab) {
|
|
let newIndex = this._getDropIndex(event, false);
|
|
let newTabs = [];
|
|
for (let tab of movingTabs) {
|
|
let newTab = gBrowser.adoptTab(tab, newIndex++, tab == draggedTab);
|
|
newTabs.push(newTab);
|
|
}
|
|
|
|
// Restore tab selection
|
|
gBrowser.addRangeToMultiSelectedTabs(newTabs[0], newTabs[newTabs.length - 1]);
|
|
} else {
|
|
// Pass true to disallow dropping javascript: or data: urls
|
|
let links;
|
|
try {
|
|
links = browserDragAndDrop.dropLinks(event, true);
|
|
} catch (ex) {}
|
|
|
|
if (!links || links.length === 0)
|
|
return;
|
|
|
|
let inBackground = Services.prefs.getBoolPref("browser.tabs.loadInBackground");
|
|
if (event.shiftKey)
|
|
inBackground = !inBackground;
|
|
|
|
let targetTab = this._getDragTargetTab(event, true);
|
|
let userContextId = this.selectedItem.getAttribute("usercontextid");
|
|
let replace = !!targetTab;
|
|
let newIndex = this._getDropIndex(event, true);
|
|
let urls = links.map(link => link.url);
|
|
|
|
let triggeringPrincipal = browserDragAndDrop.getTriggeringPrincipal(event);
|
|
|
|
(async () => {
|
|
if (urls.length >= Services.prefs.getIntPref("browser.tabs.maxOpenBeforeWarn")) {
|
|
// Sync dialog cannot be used inside drop event handler.
|
|
let answer = await OpenInTabsUtils.promiseConfirmOpenInTabs(urls.length,
|
|
window);
|
|
if (!answer) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
gBrowser.loadTabs(urls, {
|
|
inBackground,
|
|
replace,
|
|
allowThirdPartyFixup: true,
|
|
targetTab,
|
|
newIndex,
|
|
userContextId,
|
|
triggeringPrincipal,
|
|
});
|
|
})();
|
|
}
|
|
|
|
if (draggedTab) {
|
|
delete draggedTab._dragData;
|
|
}
|
|
]]></handler>
|
|
|
|
<handler event="dragend"><![CDATA[
|
|
var dt = event.dataTransfer;
|
|
var draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
|
|
|
|
// Prevent this code from running if a tabdrop animation is
|
|
// running since calling _finishAnimateTabMove would clear
|
|
// any CSS transition that is running.
|
|
if (draggedTab.hasAttribute("tabdrop-samewindow"))
|
|
return;
|
|
|
|
this._finishGroupSelectedTabs(draggedTab);
|
|
this._finishAnimateTabMove();
|
|
|
|
if (dt.mozUserCancelled || dt.dropEffect != "none" || this._isCustomizing) {
|
|
delete draggedTab._dragData;
|
|
return;
|
|
}
|
|
|
|
// Disable detach within the browser toolbox
|
|
var eX = event.screenX;
|
|
var eY = event.screenY;
|
|
var wX = window.screenX;
|
|
// check if the drop point is horizontally within the window
|
|
if (eX > wX && eX < (wX + window.outerWidth)) {
|
|
let bo = this.arrowScrollbox.boxObject;
|
|
// also avoid detaching if the the tab was dropped too close to
|
|
// the tabbar (half a tab)
|
|
let endScreenY = bo.screenY + 1.5 * bo.height;
|
|
if (eY < endScreenY && eY > window.screenY)
|
|
return;
|
|
}
|
|
|
|
// screen.availLeft et. al. only check the screen that this window is on,
|
|
// but we want to look at the screen the tab is being dropped onto.
|
|
var screen = Cc["@mozilla.org/gfx/screenmanager;1"]
|
|
.getService(Ci.nsIScreenManager)
|
|
.screenForRect(eX, eY, 1, 1);
|
|
var fullX = {}, fullY = {}, fullWidth = {}, fullHeight = {};
|
|
var availX = {}, availY = {}, availWidth = {}, availHeight = {};
|
|
// get full screen rect and available rect, both in desktop pix
|
|
screen.GetRectDisplayPix(fullX, fullY, fullWidth, fullHeight);
|
|
screen.GetAvailRectDisplayPix(availX, availY, availWidth, availHeight);
|
|
|
|
// scale factor to convert desktop pixels to CSS px
|
|
var scaleFactor =
|
|
screen.contentsScaleFactor / screen.defaultCSSScaleFactor;
|
|
// synchronize CSS-px top-left coordinates with the screen's desktop-px
|
|
// coordinates, to ensure uniqueness across multiple screens
|
|
// (compare the equivalent adjustments in nsGlobalWindow::GetScreenXY()
|
|
// and related methods)
|
|
availX.value = (availX.value - fullX.value) * scaleFactor + fullX.value;
|
|
availY.value = (availY.value - fullY.value) * scaleFactor + fullY.value;
|
|
availWidth.value *= scaleFactor;
|
|
availHeight.value *= scaleFactor;
|
|
|
|
// ensure new window entirely within screen
|
|
var winWidth = Math.min(window.outerWidth, availWidth.value);
|
|
var winHeight = Math.min(window.outerHeight, availHeight.value);
|
|
var left = Math.min(Math.max(eX - draggedTab._dragData.offsetX, availX.value),
|
|
availX.value + availWidth.value - winWidth);
|
|
var top = Math.min(Math.max(eY - draggedTab._dragData.offsetY, availY.value),
|
|
availY.value + availHeight.value - winHeight);
|
|
|
|
delete draggedTab._dragData;
|
|
|
|
if (gBrowser.tabs.length == 1) {
|
|
// resize _before_ move to ensure the window fits the new screen. if
|
|
// the window is too large for its screen, the window manager may do
|
|
// automatic repositioning.
|
|
window.resizeTo(winWidth, winHeight);
|
|
window.moveTo(left, top);
|
|
window.focus();
|
|
} else {
|
|
let props = { screenX: left, screenY: top, suppressanimation: 1 };
|
|
if (AppConstants.platform != "win") {
|
|
props.outerWidth = winWidth;
|
|
props.outerHeight = winHeight;
|
|
}
|
|
gBrowser.replaceTabsWithWindow(draggedTab, props);
|
|
}
|
|
event.stopPropagation();
|
|
]]></handler>
|
|
|
|
<handler event="dragexit"><![CDATA[
|
|
this._dragTime = 0;
|
|
|
|
// This does not work at all (see bug 458613)
|
|
var target = event.relatedTarget;
|
|
while (target && target != this)
|
|
target = target.parentNode;
|
|
if (target)
|
|
return;
|
|
|
|
this._tabDropIndicator.collapsed = true;
|
|
event.stopPropagation();
|
|
]]></handler>
|
|
</handlers>
|
|
</binding>
|
|
|
|
<binding id="tabbrowser-tab" display="xul:hbox"
|
|
extends="chrome://global/content/bindings/tabbox.xml#tab">
|
|
<content context="tabContextMenu">
|
|
<xul:stack class="tab-stack" flex="1">
|
|
<xul:vbox xbl:inherits="selected=visuallyselected,fadein"
|
|
class="tab-background">
|
|
<xul:hbox xbl:inherits="selected=visuallyselected,multiselected,before-multiselected"
|
|
class="tab-line"/>
|
|
<xul:spacer flex="1"/>
|
|
<xul:hbox class="tab-bottom-line"/>
|
|
</xul:vbox>
|
|
<xul:hbox xbl:inherits="pinned,bursting,notselectedsinceload"
|
|
anonid="tab-loading-burst"
|
|
class="tab-loading-burst"/>
|
|
<xul:hbox xbl:inherits="pinned,selected=visuallyselected,titlechanged,attention"
|
|
class="tab-content" align="center">
|
|
<xul:hbox xbl:inherits="fadein,pinned,busy,progress,selected=visuallyselected"
|
|
anonid="tab-throbber"
|
|
class="tab-throbber"
|
|
layer="true"/>
|
|
<xul:image xbl:inherits="fadein,pinned,busy,progress,selected=visuallyselected"
|
|
class="tab-throbber-fallback"
|
|
role="presentation"
|
|
layer="true"/>
|
|
<xul:hbox xbl:inherits="fadein,pinned,busy,progress,selected=visuallyselected,pendingicon"
|
|
anonid="tab-icon-pending"
|
|
class="tab-icon-pending"/>
|
|
<xul:image xbl:inherits="src=image,triggeringprincipal=iconloadingprincipal,requestcontextid,fadein,pinned,selected=visuallyselected,busy,crashed,sharing"
|
|
anonid="tab-icon-image"
|
|
class="tab-icon-image"
|
|
validate="never"
|
|
role="presentation"/>
|
|
<xul:image xbl:inherits="sharing,selected=visuallyselected,pinned"
|
|
anonid="sharing-icon"
|
|
class="tab-sharing-icon-overlay"
|
|
role="presentation"/>
|
|
<xul:image xbl:inherits="crashed,busy,soundplaying,soundplaying-scheduledremoval,pinned,muted,blocked,selected=visuallyselected,activemedia-blocked"
|
|
anonid="overlay-icon"
|
|
class="tab-icon-overlay"
|
|
role="presentation"/>
|
|
<xul:hbox class="tab-label-container"
|
|
xbl:inherits="pinned,selected=visuallyselected,labeldirection"
|
|
onoverflow="this.setAttribute('textoverflow', 'true');"
|
|
onunderflow="this.removeAttribute('textoverflow');"
|
|
flex="1">
|
|
<xul:label class="tab-text tab-label" anonid="tab-label"
|
|
xbl:inherits="xbl:text=label,accesskey,fadein,pinned,selected=visuallyselected,attention"
|
|
role="presentation"/>
|
|
</xul:hbox>
|
|
<xul:image xbl:inherits="soundplaying,soundplaying-scheduledremoval,pinned,muted,blocked,selected=visuallyselected,activemedia-blocked"
|
|
anonid="soundplaying-icon"
|
|
class="tab-icon-sound"
|
|
role="presentation"/>
|
|
<xul:image anonid="close-button"
|
|
xbl:inherits="fadein,pinned,selected=visuallyselected"
|
|
class="tab-close-button close-icon"
|
|
role="presentation"/>
|
|
</xul:hbox>
|
|
</xul:stack>
|
|
</content>
|
|
|
|
<implementation>
|
|
<constructor><![CDATA[
|
|
if (!("_lastAccessed" in this)) {
|
|
this.updateLastAccessed();
|
|
}
|
|
]]></constructor>
|
|
|
|
<property name="_visuallySelected">
|
|
<setter>
|
|
<![CDATA[
|
|
if (val == (this.getAttribute("visuallyselected") == "true")) {
|
|
return val;
|
|
}
|
|
|
|
if (val) {
|
|
this.setAttribute("visuallyselected", "true");
|
|
} else {
|
|
this.removeAttribute("visuallyselected");
|
|
}
|
|
gBrowser._tabAttrModified(this, ["visuallyselected"]);
|
|
|
|
return val;
|
|
]]>
|
|
</setter>
|
|
</property>
|
|
|
|
<property name="_selected">
|
|
<setter>
|
|
<![CDATA[
|
|
// in e10s we want to only pseudo-select a tab before its rendering is done, so that
|
|
// the rest of the system knows that the tab is selected, but we don't want to update its
|
|
// visual status to selected until after we receive confirmation that its content has painted.
|
|
if (val)
|
|
this.setAttribute("selected", "true");
|
|
else
|
|
this.removeAttribute("selected");
|
|
|
|
// If we're non-e10s we should update the visual selection as well at the same time,
|
|
// *or* if we're e10s and the visually selected tab isn't changing, in which case the
|
|
// tab switcher code won't run and update anything else (like the before- and after-
|
|
// selected attributes).
|
|
if (!gMultiProcessBrowser || (val && this.hasAttribute("visuallyselected"))) {
|
|
this._visuallySelected = val;
|
|
}
|
|
|
|
return val;
|
|
]]>
|
|
</setter>
|
|
</property>
|
|
<field name="_selectedOnFirstMouseDown">false</field>
|
|
|
|
<property name="pinned" readonly="true">
|
|
<getter>
|
|
return this.getAttribute("pinned") == "true";
|
|
</getter>
|
|
</property>
|
|
<property name="hidden" readonly="true">
|
|
<getter>
|
|
return this.getAttribute("hidden") == "true";
|
|
</getter>
|
|
</property>
|
|
<property name="muted" readonly="true">
|
|
<getter>
|
|
return this.getAttribute("muted") == "true";
|
|
</getter>
|
|
</property>
|
|
<property name="multiselected" readonly="true">
|
|
<getter>
|
|
return this.getAttribute("multiselected") == "true";
|
|
</getter>
|
|
</property>
|
|
<property name="beforeMultiselected" readonly="true">
|
|
<getter>
|
|
return this.getAttribute("before-multiselected") == "true";
|
|
</getter>
|
|
</property>
|
|
<!--
|
|
Describes how the tab ended up in this mute state. May be any of:
|
|
|
|
- undefined: The tabs mute state has never changed.
|
|
- null: The mute state was last changed through the UI.
|
|
- Any string: The ID was changed through an extension API. The string
|
|
must be the ID of the extension which changed it.
|
|
-->
|
|
<field name="muteReason">undefined</field>
|
|
|
|
<property name="userContextId" readonly="true">
|
|
<getter>
|
|
return this.hasAttribute("usercontextid")
|
|
? parseInt(this.getAttribute("usercontextid"))
|
|
: 0;
|
|
</getter>
|
|
</property>
|
|
|
|
<property name="soundPlaying" readonly="true">
|
|
<getter>
|
|
return this.getAttribute("soundplaying") == "true";
|
|
</getter>
|
|
</property>
|
|
|
|
<property name="activeMediaBlocked" readonly="true">
|
|
<getter>
|
|
return this.getAttribute("activemedia-blocked") == "true";
|
|
</getter>
|
|
</property>
|
|
|
|
<property name="lastAccessed">
|
|
<getter>
|
|
return this._lastAccessed == Infinity ? Date.now() : this._lastAccessed;
|
|
</getter>
|
|
</property>
|
|
<method name="updateLastAccessed">
|
|
<parameter name="aDate"/>
|
|
<body><![CDATA[
|
|
this._lastAccessed = this.selected ? Infinity : (aDate || Date.now());
|
|
]]></body>
|
|
</method>
|
|
|
|
<field name="mOverCloseButton">false</field>
|
|
<property name="_overPlayingIcon" readonly="true">
|
|
<getter><![CDATA[
|
|
let iconVisible = this.hasAttribute("soundplaying") ||
|
|
this.hasAttribute("muted") ||
|
|
this.hasAttribute("activemedia-blocked");
|
|
let soundPlayingIcon =
|
|
document.getAnonymousElementByAttribute(this, "anonid", "soundplaying-icon");
|
|
let overlayIcon =
|
|
document.getAnonymousElementByAttribute(this, "anonid", "overlay-icon");
|
|
|
|
return soundPlayingIcon && soundPlayingIcon.matches(":hover") ||
|
|
(overlayIcon && overlayIcon.matches(":hover") && iconVisible);
|
|
]]></getter>
|
|
</property>
|
|
<field name="mCorrespondingMenuitem">null</field>
|
|
|
|
<!--
|
|
While it would make sense to track this in a field, the field will get nuked
|
|
once the node is gone from the DOM, which causes us to think the tab is not
|
|
closed, which causes us to make wrong decisions. So we use an expando instead.
|
|
<field name="closing">false</field>
|
|
-->
|
|
|
|
<method name="_mouseenter">
|
|
<body><![CDATA[
|
|
if (this.hidden || this.closing) {
|
|
return;
|
|
}
|
|
|
|
let tabContainer = this.parentNode;
|
|
let visibleTabs = tabContainer._getVisibleTabs();
|
|
let tabIndex = visibleTabs.indexOf(this);
|
|
|
|
if (this.selected)
|
|
tabContainer._handleTabSelect();
|
|
|
|
if (tabIndex == 0) {
|
|
tabContainer._beforeHoveredTab = null;
|
|
} else {
|
|
let candidate = visibleTabs[tabIndex - 1];
|
|
let separatedByScrollButton =
|
|
tabContainer.getAttribute("overflow") == "true" &&
|
|
candidate.pinned && !this.pinned;
|
|
if (!candidate.selected && !separatedByScrollButton) {
|
|
tabContainer._beforeHoveredTab = candidate;
|
|
candidate.setAttribute("beforehovered", "true");
|
|
}
|
|
}
|
|
|
|
if (tabIndex == visibleTabs.length - 1) {
|
|
tabContainer._afterHoveredTab = null;
|
|
} else {
|
|
let candidate = visibleTabs[tabIndex + 1];
|
|
if (!candidate.selected) {
|
|
tabContainer._afterHoveredTab = candidate;
|
|
candidate.setAttribute("afterhovered", "true");
|
|
}
|
|
}
|
|
|
|
tabContainer._hoveredTab = this;
|
|
if (this.linkedPanel && !this.selected) {
|
|
this.linkedBrowser.unselectedTabHover(true);
|
|
this.startUnselectedTabHoverTimer();
|
|
}
|
|
|
|
// Prepare connection to host beforehand.
|
|
SessionStore.speculativeConnectOnTabHover(this);
|
|
|
|
let tabToWarm = this;
|
|
if (this.mOverCloseButton) {
|
|
tabToWarm = gBrowser._findTabToBlurTo(this);
|
|
}
|
|
gBrowser.warmupTab(tabToWarm);
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="_mouseleave">
|
|
<body><![CDATA[
|
|
let tabContainer = this.parentNode;
|
|
if (tabContainer._beforeHoveredTab) {
|
|
tabContainer._beforeHoveredTab.removeAttribute("beforehovered");
|
|
tabContainer._beforeHoveredTab = null;
|
|
}
|
|
if (tabContainer._afterHoveredTab) {
|
|
tabContainer._afterHoveredTab.removeAttribute("afterhovered");
|
|
tabContainer._afterHoveredTab = null;
|
|
}
|
|
|
|
tabContainer._hoveredTab = null;
|
|
if (this.linkedPanel && !this.selected) {
|
|
this.linkedBrowser.unselectedTabHover(false);
|
|
this.cancelUnselectedTabHoverTimer();
|
|
}
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="startUnselectedTabHoverTimer">
|
|
<body><![CDATA[
|
|
// Only record data when we need to.
|
|
if (!this.linkedBrowser.shouldHandleUnselectedTabHover) {
|
|
return;
|
|
}
|
|
|
|
if (!TelemetryStopwatch.running("HOVER_UNTIL_UNSELECTED_TAB_OPENED", this)) {
|
|
TelemetryStopwatch.start("HOVER_UNTIL_UNSELECTED_TAB_OPENED", this);
|
|
}
|
|
|
|
if (this._hoverTabTimer) {
|
|
clearTimeout(this._hoverTabTimer);
|
|
this._hoverTabTimer = null;
|
|
}
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="cancelUnselectedTabHoverTimer">
|
|
<body><![CDATA[
|
|
// Since we're listening "mouseout" event, instead of "mouseleave".
|
|
// Every time the cursor is moving from the tab to its child node (icon),
|
|
// it would dispatch "mouseout"(for tab) first and then dispatch
|
|
// "mouseover" (for icon, eg: close button, speaker icon) soon.
|
|
// It causes we would cancel present TelemetryStopwatch immediately
|
|
// when cursor is moving on the icon, and then start a new one.
|
|
// In order to avoid this situation, we could delay cancellation and
|
|
// remove it if we get "mouseover" within very short period.
|
|
this._hoverTabTimer = setTimeout(() => {
|
|
if (TelemetryStopwatch.running("HOVER_UNTIL_UNSELECTED_TAB_OPENED", this)) {
|
|
TelemetryStopwatch.cancel("HOVER_UNTIL_UNSELECTED_TAB_OPENED", this);
|
|
}
|
|
}, 100);
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="finishUnselectedTabHoverTimer">
|
|
<body><![CDATA[
|
|
// Stop timer when the tab is opened.
|
|
if (TelemetryStopwatch.running("HOVER_UNTIL_UNSELECTED_TAB_OPENED", this)) {
|
|
TelemetryStopwatch.finish("HOVER_UNTIL_UNSELECTED_TAB_OPENED", this);
|
|
}
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="startMediaBlockTimer">
|
|
<body><![CDATA[
|
|
TelemetryStopwatch.start("TAB_MEDIA_BLOCKING_TIME_MS", this);
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="finishMediaBlockTimer">
|
|
<body><![CDATA[
|
|
TelemetryStopwatch.finish("TAB_MEDIA_BLOCKING_TIME_MS", this);
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="toggleMuteAudio">
|
|
<parameter name="aMuteReason"/>
|
|
<body>
|
|
<![CDATA[
|
|
let browser = this.linkedBrowser;
|
|
let modifiedAttrs = [];
|
|
let hist = Services.telemetry.getHistogramById("TAB_AUDIO_INDICATOR_USED");
|
|
|
|
if (this.hasAttribute("activemedia-blocked")) {
|
|
this.removeAttribute("activemedia-blocked");
|
|
modifiedAttrs.push("activemedia-blocked");
|
|
|
|
browser.resumeMedia();
|
|
hist.add(3 /* unblockByClickingIcon */);
|
|
this.finishMediaBlockTimer();
|
|
} else {
|
|
if (browser.audioMuted) {
|
|
if (this.linkedPanel) {
|
|
// "Lazy Browser" should not invoke its unmute method
|
|
browser.unmute();
|
|
}
|
|
this.removeAttribute("muted");
|
|
hist.add(1 /* unmute */);
|
|
} else {
|
|
if (this.linkedPanel) {
|
|
// "Lazy Browser" should not invoke its mute method
|
|
browser.mute();
|
|
}
|
|
this.setAttribute("muted", "true");
|
|
hist.add(0 /* mute */);
|
|
}
|
|
this.muteReason = aMuteReason || null;
|
|
modifiedAttrs.push("muted");
|
|
}
|
|
gBrowser._tabAttrModified(this, modifiedAttrs);
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="setUserContextId">
|
|
<parameter name="aUserContextId"/>
|
|
<body>
|
|
<![CDATA[
|
|
if (aUserContextId) {
|
|
if (this.linkedBrowser) {
|
|
this.linkedBrowser.setAttribute("usercontextid", aUserContextId);
|
|
}
|
|
this.setAttribute("usercontextid", aUserContextId);
|
|
} else {
|
|
if (this.linkedBrowser) {
|
|
this.linkedBrowser.removeAttribute("usercontextid");
|
|
}
|
|
this.removeAttribute("usercontextid");
|
|
}
|
|
|
|
ContextualIdentityService.setTabStyle(this);
|
|
]]>
|
|
</body>
|
|
</method>
|
|
</implementation>
|
|
|
|
<handlers>
|
|
<handler event="mouseover"><![CDATA[
|
|
if (event.originalTarget.getAttribute("anonid") == "close-button") {
|
|
this.mOverCloseButton = true;
|
|
}
|
|
|
|
this._mouseenter();
|
|
]]></handler>
|
|
<handler event="mouseout"><![CDATA[
|
|
if (event.originalTarget.getAttribute("anonid") == "close-button") {
|
|
this.mOverCloseButton = false;
|
|
}
|
|
|
|
this._mouseleave();
|
|
]]></handler>
|
|
|
|
<handler event="dragstart" phase="capturing">
|
|
this.style.MozUserFocus = "";
|
|
</handler>
|
|
|
|
<handler event="dragstart"><![CDATA[
|
|
if (this.mOverCloseButton) {
|
|
event.stopPropagation();
|
|
}
|
|
]]></handler>
|
|
|
|
<handler event="mousedown" phase="capturing">
|
|
<![CDATA[
|
|
if (event.button == 0 && !this.selected && this.multiselected) {
|
|
gBrowser.lockClearMultiSelectionOnce();
|
|
}
|
|
|
|
let tabContainer = this.parentNode;
|
|
if (tabContainer._closeTabByDblclick &&
|
|
event.button == 0 &&
|
|
event.detail == 1) {
|
|
this._selectedOnFirstMouseDown = this.selected;
|
|
}
|
|
|
|
if (this.selected) {
|
|
this.style.MozUserFocus = "ignore";
|
|
} else {
|
|
// When browser.tabs.multiselect config is set to false,
|
|
// then we ignore the state of multi-selection keys (Ctrl/Cmd).
|
|
const tabSelectionToggled = tabContainer._multiselectEnabled &&
|
|
(event.getModifierState("Accel") || event.shiftKey);
|
|
|
|
if (this.mOverCloseButton || this._overPlayingIcon || tabSelectionToggled) {
|
|
// Prevent tabbox.xml from selecting the tab.
|
|
event.stopPropagation();
|
|
}
|
|
}
|
|
|
|
if (event.button == 1) {
|
|
gBrowser.warmupTab(gBrowser._findTabToBlurTo(this));
|
|
}
|
|
]]>
|
|
</handler>
|
|
<handler event="mouseup">
|
|
// Make sure that clear-selection is released.
|
|
// Otherwise selection using Shift key may be broken.
|
|
gBrowser.unlockClearMultiSelection();
|
|
|
|
this.style.MozUserFocus = "";
|
|
</handler>
|
|
|
|
<handler event="click" button="0"><![CDATA[
|
|
let tabContainer = this.parentNode;
|
|
if (tabContainer._multiselectEnabled) {
|
|
let shiftKey = event.shiftKey;
|
|
let accelKey = event.getModifierState("Accel");
|
|
if (shiftKey) {
|
|
const lastSelectedTab = gBrowser.lastMultiSelectedTab;
|
|
if (!accelKey) {
|
|
gBrowser.selectedTab = lastSelectedTab;
|
|
|
|
// Make sure selection is cleared when tab-switch doesn't happen.
|
|
gBrowser.clearMultiSelectedTabs(false);
|
|
}
|
|
gBrowser.addRangeToMultiSelectedTabs(lastSelectedTab, this);
|
|
return;
|
|
}
|
|
if (accelKey) {
|
|
// Ctrl (Cmd for mac) key is pressed
|
|
if (this.multiselected) {
|
|
gBrowser.removeFromMultiSelectedTabs(this, true);
|
|
} else if (this != gBrowser.selectedTab) {
|
|
gBrowser.addToMultiSelectedTabs(this, false);
|
|
gBrowser.lastMultiSelectedTab = this;
|
|
}
|
|
return;
|
|
}
|
|
|
|
const overCloseButton = event.originalTarget.getAttribute("anonid") == "close-button";
|
|
if (gBrowser.multiSelectedTabsCount > 0 && !overCloseButton && !this._overPlayingIcon) {
|
|
// Tabs were previously multi-selected and user clicks on a tab
|
|
// without holding Ctrl/Cmd Key
|
|
|
|
// Force positional attributes to update when the
|
|
// target (of the click) is the "active" tab.
|
|
let updatePositionalAttr = gBrowser.selectedTab == this;
|
|
|
|
gBrowser.clearMultiSelectedTabs(updatePositionalAttr);
|
|
}
|
|
}
|
|
|
|
if (this._overPlayingIcon) {
|
|
if (this.multiselected) {
|
|
gBrowser.toggleMuteAudioOnMultiSelectedTabs(this);
|
|
} else {
|
|
this.toggleMuteAudio();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (event.originalTarget.getAttribute("anonid") == "close-button") {
|
|
if (this.multiselected) {
|
|
gBrowser.removeMultiSelectedTabs();
|
|
} else {
|
|
gBrowser.removeTab(this, {
|
|
animate: true,
|
|
byMouse: event.mozInputSource == MouseEvent.MOZ_SOURCE_MOUSE,
|
|
});
|
|
}
|
|
// This enables double-click protection for the tab container
|
|
// (see tabbrowser-tabs 'click' handler).
|
|
gBrowser.tabContainer._blockDblClick = true;
|
|
}
|
|
]]></handler>
|
|
|
|
<handler event="dblclick" button="0" phase="capturing"><![CDATA[
|
|
// for the one-close-button case
|
|
if (event.originalTarget.getAttribute("anonid") == "close-button") {
|
|
event.stopPropagation();
|
|
}
|
|
|
|
let tabContainer = this.parentNode;
|
|
if (tabContainer._closeTabByDblclick &&
|
|
this._selectedOnFirstMouseDown &&
|
|
this.selected &&
|
|
!this._overPlayingIcon) {
|
|
gBrowser.removeTab(this, {
|
|
animate: true,
|
|
byMouse: event.mozInputSource == MouseEvent.MOZ_SOURCE_MOUSE,
|
|
});
|
|
}
|
|
]]></handler>
|
|
|
|
<handler event="animationend">
|
|
<![CDATA[
|
|
if (event.originalTarget.getAttribute("anonid") == "tab-loading-burst") {
|
|
this.removeAttribute("bursting");
|
|
}
|
|
]]>
|
|
</handler>
|
|
</handlers>
|
|
</binding>
|
|
</bindings>
|