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"); } ]]> document.getElementById("tabbrowser-tabbox"); document.getElementById("tabContextMenu"); document.getAnonymousElementByAttribute(this, "anonid", "arrowscrollbox"); null null null null null null this.style.setProperty("--tab-min-width", val + "px"); return val; // Unlike boolean HTML attributes, the value of boolean ARIA attributes actually matters. this.setAttribute("aria-multiselectable", !!val); return val; return this.getAttribute("aria-multiselectable") == "true"; 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; } ]]> false document.getAnonymousElementByAttribute(this, "anonid", "tab-drop-indicator"); 350 0 false { 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"); } }); }); ]]> document.getAnonymousElementByAttribute(this, "anonid", "closing-tabs-spacer"); NaN false false 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); } ]]> 0 null 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); } ]]> draggedTab._dragData.animLastScreenX; draggedTab._dragData.animLastScreenX = screenX; let pinned = draggedTab.pinned; let numPinned = gBrowser._numPinnedTabs; let tabs = this._getVisibleTabs() .slice(pinned ? 0 : numPinned, pinned ? numPinned : undefined); if (RTL_UI) { 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].screenX; let leftMovingTabScreenX = movingTabs[0].screenX; let translateX = screenX - draggedTab._dragData.screenX; if (!pinned) { translateX += this.arrowScrollbox.scrollbox.scrollLeft - draggedTab._dragData.scrollX; } let leftBound = leftTab.screenX - leftMovingTabScreenX; let rightBound = (rightTab.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; screenX = tabs[mid].screenX + getTabShift(tabs[mid], oldIndex); if (screenX > tabCenter) { high = mid - 1; } else if (screenX + tabs[mid].getBoundingClientRect().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_UI ? -shiftWidth : shiftWidth); if (tab._tPos > draggedTab._tPos && tab._tPos < dropIndex) return (RTL_UI ? shiftWidth : -shiftWidth); return 0; } ]]> -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. for (let t of this._getVisibleTabs()) { if (t.groupingTabsData && t.groupingTabsData.translateX) { let translateX = (RTL_UI ? -1 : 1) * 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"); } } ]]> -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; } ]]> this.arrowScrollbox._scrollButtonDown; { let lastTabRect = this._lastTabToScrollIntoView.getBoundingClientRect(); let selectedTab = this.selectedItem; if (selectedTab.pinned) { selectedTab = null; } else { selectedTab = selectedTab.getBoundingClientRect(); selectedTab = {left: selectedTab.left, right: selectedTab.right}; } return [ this._lastTabToScrollIntoView, this.arrowScrollbox.scrollClientRect, {left: lastTabRect.left, right: lastTabRect.right}, selectedTab, ]; }).then(([tabUsed, scrollRect, tabRect, selectedRect]) => { // First off, remove the promise so we can re-enter if necessary. delete this._backgroundTabScrollPromise; // Then, if the layout info isn't for the last-scrolled-to-tab, re-run // the code above to get layout info for *that* tab, and don't do // anything here, as we really just want to run this for the last-opened tab. if (this._lastTabToScrollIntoView != tabUsed) { this._notifyBackgroundTab(this._lastTabToScrollIntoView); return; } delete this._lastTabToScrollIntoView; // Is the new tab already completely visible? if (scrollRect.left <= tabRect.left && tabRect.right <= scrollRect.right) return; if (this.arrowScrollbox.smoothScroll) { // Can we make both the new tab and the selected tab completely visible? if (!selectedRect || Math.max(tabRect.right - selectedRect.left, selectedRect.right - tabRect.left) <= scrollRect.width) { this.arrowScrollbox.ensureElementIsVisible(aTab); return; } this.arrowScrollbox.scrollByPixels(RTL_UI ? selectedRect.right - scrollRect.right : selectedRect.left - scrollRect.left); } if (!this._animateElement.hasAttribute("highlight")) { this._animateElement.setAttribute("highlight", "true"); setTimeout(function(ele) { ele.removeAttribute("highlight"); }, 150, this._animateElement); } }); } ]]> tab.screenX + boxObject.width * .75) return null; } return tab; ]]> tabs[i].screenX + tabs[i].boxObject.width / 2) return i; } return tabs.length; ]]> 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"; ]]> 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 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.id == "alltabs-button")); const kAttr = "hasadjacentnewtabbutton"; if (sib && sib.id == "new-tab-button") { this.setAttribute(kAttr, "true"); } else { this.removeAttribute(kAttr); } ]]> 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; } ]]> endOfTab) || (RTL_UI && event.clientX < endOfTab)) { BrowserOpenTab(); } } else { return; } event.stopPropagation(); ]]> = 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(); ]]> 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(); ]]> = 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 (RTL_UI) { [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 (RTL_UI) { newMargin = rect.right - tabRect.left; } else { newMargin = tabRect.right - rect.left; } } else { let tabRect = this.children[newIndex].getBoundingClientRect(); if (RTL_UI) { newMargin = rect.right - tabRect.right; } else { newMargin = tabRect.left - rect.left; } } } ind.collapsed = false; newMargin += ind.clientWidth / 2; if (RTL_UI) { newMargin *= -1; } ind.style.transform = "translate(" + Math.round(newMargin) + "px)"; ind.style.marginInlineStart = (-ind.clientWidth) + "px"; ]]> 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; } ]]> wX && eX < (wX + window.outerWidth)) { // also avoid detaching if the the tab was dropped too close to // the tabbar (half a tab) let rect = window.windowUtils.getBoundsWithoutFlushing(this.arrowScrollbox); let detachTabThresholdY = window.screenY + rect.top + 1.5 * rect.height; if (eY < detachTabThresholdY && 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(); ]]> false return this.getAttribute("pinned") == "true"; return this.getAttribute("hidden") == "true"; return this.getAttribute("muted") == "true"; return this.getAttribute("multiselected") == "true"; return this.getAttribute("before-multiselected") == "true"; undefined return this.hasAttribute("usercontextid") ? parseInt(this.getAttribute("usercontextid")) : 0; return this.getAttribute("soundplaying") == "true"; return this.getAttribute("activemedia-blocked") == "true"; // Determines if a tab is "empty", usually used in the context of determining // if it's ok to close the tab. if (this.hasAttribute("busy")) return false; if (this.hasAttribute("customizemode")) return false; let browser = this.linkedBrowser; if (!isBlankPageURL(browser.currentURI.spec)) return false; if (!checkEmptyPageOrigin(browser)) return false; if (browser.canGoForward || browser.canGoBack) return false; return true; return this._lastAccessed == Infinity ? Date.now() : this._lastAccessed; false null { if (TelemetryStopwatch.running("HOVER_UNTIL_UNSELECTED_TAB_OPENED", this)) { TelemetryStopwatch.cancel("HOVER_UNTIL_UNSELECTED_TAB_OPENED", this); } }, 100); ]]> this.style.MozUserFocus = ""; // Make sure that clear-selection is released. // Otherwise selection using Shift key may be broken. gBrowser.unlockClearMultiSelection(); this.style.MozUserFocus = ""; 0 && !event.originalTarget.classList.contains("tab-close-button") && !event.originalTarget.classList.contains("tab-icon-sound") && !event.originalTarget.classList.contains("tab-icon-overlay")) { // 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 (event.originalTarget.classList.contains("tab-icon-sound") || (event.originalTarget.classList.contains("tab-icon-overlay") && (event.originalTarget.hasAttribute("soundplaying") || event.originalTarget.hasAttribute("muted")))) { 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; } ]]>