diff --git a/accessible/xul/XULTabAccessible.cpp b/accessible/xul/XULTabAccessible.cpp index 55a0b371cce9..13793cbf7730 100644 --- a/accessible/xul/XULTabAccessible.cpp +++ b/accessible/xul/XULTabAccessible.cpp @@ -87,7 +87,9 @@ Relation XULTabAccessible::RelationByType(RelationType aType) const { if (aType != RelationType::LABEL_FOR) return rel; // Expose 'LABEL_FOR' relation on tab accessible for tabpanel accessible. - nsIContent* parent = mContent->GetParent(); + ErrorResult rv; + nsIContent* parent = + mContent->AsElement()->Closest(NS_LITERAL_STRING("tabs"), rv); if (!parent) return rel; nsCOMPtr tabsElm = diff --git a/browser/base/content/browser.css b/browser/base/content/browser.css index a7f6dea87ac8..3590962fd196 100644 --- a/browser/base/content/browser.css +++ b/browser/base/content/browser.css @@ -126,10 +126,6 @@ panelview[mainview] > .panel-header { transition: height var(--panelui-subview-transition-duration); } -#tabbrowser-tabs { - -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser-tabs"); -} - @supports -moz-bool-pref("layout.css.emulate-moz-box-with-flex") { #tabbrowser-tabs { /* Without this, the tabs container width extends beyond the window width */ @@ -203,13 +199,13 @@ panelview[mainview] > .panel-header { } %endif -#tabbrowser-tabs[positionpinnedtabs] > .tabbrowser-tab[pinned] { +#tabbrowser-tabs[positionpinnedtabs] > .tabbrowser-arrowscrollbox > .tabbrowser-tab[pinned] { position: fixed !important; display: block; /* position:fixed already does this (bug 579776), but let's be explicit */ } -#tabbrowser-tabs[movingtab] > .tabbrowser-tab[selected], -#tabbrowser-tabs[movingtab] > .tabbrowser-tab[multiselected] { +#tabbrowser-tabs[movingtab] > .tabbrowser-arrowscrollbox > .tabbrowser-tab[selected], +#tabbrowser-tabs[movingtab] > .tabbrowser-arrowscrollbox > .tabbrowser-tab[multiselected] { position: relative; z-index: 2; pointer-events: none; /* avoid blocking dragover events on scroll buttons */ diff --git a/browser/base/content/browser.xhtml b/browser/base/content/browser.xhtml index 252c5b2af3ad..49fefce2e20b 100644 --- a/browser/base/content/browser.xhtml +++ b/browser/base/content/browser.xhtml @@ -95,6 +95,7 @@ Services.scriptloader.loadSubScript("chrome://browser/content/browser-tabsintitlebar.js", this); Services.scriptloader.loadSubScript("chrome://browser/content/tabbrowser.js", this); Services.scriptloader.loadSubScript("chrome://browser/content/tabbrowser-tab.js", this); + Services.scriptloader.loadSubScript("chrome://browser/content/tabbrowser-tabs.js", this); Services.scriptloader.loadSubScript("chrome://browser/content/search/autocomplete-popup.js", this); Services.scriptloader.loadSubScript("chrome://browser/content/search/searchbar.js", this); @@ -723,11 +724,19 @@ - + + + + + + + + this.observe(...args); + Services.prefs.addObserver("privacy.userContext", this.boundObserve); + 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(); + this._initializeArrowScrollbox(); + + XPCOMUtils.defineLazyPreferenceGetter(this, "_closeTabByDblclick", + "browser.tabs.closeTabByDblclick", false); + + if (gMultiProcessBrowser) { + this.tabbox.tabpanels.setAttribute("async", "true"); + } + } + + on_TabSelect(event) { + this._handleTabSelect(); + } + + on_TabClose(event) { + this._hiddenSoundPlayingStatusChanged(event.target, { closed: true }); + } + + on_TabAttrModified(event) { + if (event.detail.changed.includes("soundplaying") && event.target.hidden) { + this._hiddenSoundPlayingStatusChanged(event.target); + } + } + + on_TabHide(event) { + if (event.target.soundPlaying) { + this._hiddenSoundPlayingStatusChanged(event.target); + } + } + + on_TabShow(event) { + if (event.target.soundPlaying) { + this._hiddenSoundPlayingStatusChanged(event.target); + } + } + + on_transitionend(event) { + if (event.propertyName != "max-width") { + return; + } + + let tab = event.target ? event.target.closest("tab") : null; + + if (tab.getAttribute("fadein") == "true") { + if (tab._fullyOpen) { + this._updateCloseButtons(); + } else { + this._handleNewTab(tab); + } + } else if (tab.closing) { + gBrowser._endRemoveTab(tab); + } + + let evt = new CustomEvent("TabAnimationEnd", { bubbles: true }); + tab.dispatchEvent(evt); + } + + on_dblclick(event) { + // 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) + return; + + if (event.button != 0 || + event.originalTarget.localName != "scrollbox") + return; + + if (!this._blockDblClick) + BrowserOpenTab(); + + event.preventDefault(); + } + + on_click(event) { + if (event.eventPhase == Event.CAPTURING_PHASE && event.button == 0) { + /* 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; + } + } else if (event.eventPhase == Event.BUBBLING_PHASE && event.button == 1) { + let tab = event.target ? event.target.closest("tab") : null; + if (tab) { + gBrowser.removeTab(tab, { + animate: true, + byMouse: event.mozInputSource == MouseEvent.MOZ_SOURCE_MOUSE, + }); + } else if (event.originalTarget.localName == "scrollbox") { + // The user middleclicked on the tabstrip. Check whether the click + // was dispatched on the open space of it. + let visibleTabs = this._getVisibleTabs(); + let lastTab = visibleTabs[visibleTabs.length - 1]; + let winUtils = window.windowUtils; + let endOfTab = winUtils.getBoundsWithoutFlushing(lastTab)[RTL_UI ? "left" : "right"]; + if ((!RTL_UI && event.clientX > endOfTab) || + (RTL_UI && event.clientX < endOfTab)) { + BrowserOpenTab(); + } + } else { + return; + } + + event.stopPropagation(); + } + } + + on_keydown(event) { + 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 if ((!RTL_UI && event.keyCode == KeyEvent.DOM_VK_RIGHT) || + (RTL_UI && 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(); + } + + on_dragstart(event) { + 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(); + } + + on_dragover(event) { + var effects = this._getDropEffectForTabDrag(event); + + var ind = this._tabDropIndicator; + if (effects == "" || effects == "none") { + ind.hidden = true; + return; + } + event.preventDefault(); + event.stopPropagation(); + + var arrowScrollbox = this.arrowScrollbox; + + // 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((RTL_UI ? -1 : 1) * pixelsToScroll, true); + } + + let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0); + if ((effects == "move" || effects == "copy") && + this == draggedTab.container) { + ind.hidden = 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.hidden = 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"); + let children = this.allTabs; + if (newIndex == children.length) { + let tabRect = children[newIndex - 1].getBoundingClientRect(); + if (RTL_UI) { + newMargin = rect.right - tabRect.left; + } else { + newMargin = tabRect.right - rect.left; + } + } else { + let tabRect = children[newIndex].getBoundingClientRect(); + if (RTL_UI) { + newMargin = rect.right - tabRect.right; + } else { + newMargin = tabRect.left - rect.left; + } + } + } + + ind.hidden = false; + + newMargin += ind.clientWidth / 2; + if (RTL_UI) { + newMargin *= -1; + } + + ind.style.transform = "translate(" + Math.round(newMargin) + "px)"; + ind.style.marginInlineStart = (-ind.clientWidth) + "px"; + } + + on_drop(event) { + 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.container._finishGroupSelectedTabs(draggedTab); + } + + this._tabDropIndicator.hidden = 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.container != this || event.shiftKey) { + this.selectedItem = draggedTabCopy; + } + } else if (draggedTab && draggedTab.container == 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; + } + } + + on_dragend(event) { + 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)) { + // 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(); + } + + on_dragexit(event) { + 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.hidden = true; + event.stopPropagation(); + } + + get tabbox() { + return document.getElementById("tabbrowser-tabbox"); + } + + get newTabButton() { + return this.querySelector(".tabs-newtab-button"); + } + + // Accessor for tabs. arrowScrollbox has two non-tab elements at the + // end, everything else is s + get allTabs() { + let children = Array.from(this.arrowScrollbox.children); + children.pop(); + children.pop(); + return children; + } + + appendChild(tab) { + return this.insertBefore(tab, null); + } + + insertBefore(tab, node) { + if (!this.arrowScrollbox) { + throw new Error("Shouldn't call this without arrowscrollbox"); + } + + let {arrowScrollbox} = this; + if (node == null) { + // we have a toolbarbutton and a space at the end of the scrollbox + node = arrowScrollbox.lastChild.previousSibling; + } + return arrowScrollbox.insertBefore(tab, node); + } + + set _tabMinWidth(val) { + this.style.setProperty("--tab-min-width", val + "px"); + return val; + } + + set _multiselectEnabled(val) { + // Unlike boolean HTML attributes, the value of boolean ARIA attributes actually matters. + this.setAttribute("aria-multiselectable", !!val); + return val; + } + + get _multiselectEnabled() { + return this.getAttribute("aria-multiselectable") == "true"; + } + + get _isCustomizing() { + return document.documentElement.getAttribute("customizing") == "true"; + } + + _initializeArrowScrollbox() { + let arrowScrollbox = this.arrowScrollbox; + arrowScrollbox.addEventListener("underflow", event => { + // Ignore underflow events: + // - from nested scrollable elements + // - for vertical orientation + // - corresponding to an overflow event that we ignored + if (event.originalTarget != arrowScrollbox.scrollbox || + event.detail == 0 || + !this.hasAttribute("overflow")) { + return; + } + + this.removeAttribute("overflow"); + + if (this._lastTabClosedByMouse) { + this._expandSpacerBy(this._scrollButtonWidth); + } + + for (let tab of Array.from(gBrowser._removingTabs)) { + gBrowser.removeTab(tab); + } + + this._positionPinnedTabs(); + }, true); + + arrowScrollbox.addEventListener("overflow", event => { + // Ignore overflow events: + // - from nested scrollable elements + // - for vertical orientation + if (event.originalTarget != arrowScrollbox.scrollbox || + event.detail == 0) { + return; + } + + this.setAttribute("overflow", "true"); + this._positionPinnedTabs(); + this._handleTabSelect(true); + }); + + // Override scrollbox.xml method, since our scrollbox's children are + // inherited from the scrollbox binding parent (this). + arrowScrollbox._getScrollableElements = () => { + return this.allTabs.filter(arrowScrollbox._canScrollToElement); + }; + arrowScrollbox._canScrollToElement = tab => { + return !tab._pinnedUnscrollable && !tab.hidden; + }; + } + + observe(aSubject, aTopic, aData) { + 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; + } + + // There are separate "new tab" buttons for when the tab strip + // is overflowed and when it is not. Attach the long click + // popup to both of them. + const newTab = document.getElementById("new-tab-button"); + const newTab2 = this.newTabButton; + + for (let parent of [newTab, newTab2]) { + if (!parent) + continue; + + gClickAndHoldListenersOnElement.remove(parent); + parent.removeAttribute("type"); + if (parent.menupopup) { + parent.menupopup.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.prepend(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; + } + } + + _getVisibleTabs() { + // Cannot access gBrowser before it's initialized. + if (!gBrowser) { + return this.allTabs[0]; + } + + return gBrowser.visibleTabs; + } + + _setPositionalAttributes() { + 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"); + } + } + } + + _updateCloseButtons() { + // 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"); + } + }); + }); + } + + _updateHiddenTabsStatus() { + if (gBrowser.visibleTabs.length < gBrowser.tabs.length) { + this.setAttribute("hashiddentabs", "true"); + } else { + this.removeAttribute("hashiddentabs"); + } + } + + _handleTabSelect(aInstant) { + let selectedTab = this.selectedItem; + if (this.getAttribute("overflow") == "true") + this.arrowScrollbox.ensureElementIsVisible(selectedTab, aInstant); + + selectedTab._notselectedsinceload = false; + } + + /** + * Try to keep the active tab's close button under the mouse cursor + */ + _lockTabSizing(aTab, aTabWidth) { + 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; + this._scrollButtonWidth = window.windowUtils.getBoundsWithoutFlushing(this.arrowScrollbox._scrollButtonDown).width; + + 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); + } + } + + _expandSpacerBy(pixels) { + 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); + } + + _unlockTabSizing() { + 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; + } + } + + uiDensityChanged() { + this._positionPinnedTabs(); + this._updateCloseButtons(); + this._handleTabSelect(true); + } + + _positionPinnedTabs() { + let numPinned = gBrowser._numPinnedTabs; + let doPosition = this.getAttribute("overflow") == "true" && + this._getVisibleTabs().length > numPinned && + numPinned > 0; + let tabs = this.allTabs; + + 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.allTabs[0].getBoundingClientRect().width, + scrollButtonWidth: arrowScrollbox._scrollButtonDown.getBoundingClientRect().width, + }; + } + + let width = 0; + for (let i = numPinned - 1; i >= 0; i--) { + let tab = tabs[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 = tabs[i]; + tab.style.marginInlineStart = ""; + tab._pinnedUnscrollable = false; + } + + this.style.paddingInlineStart = ""; + } + + if (this._lastNumPinned != numPinned) { + this._lastNumPinned = numPinned; + this._handleTabSelect(true); + } + } + + _animateTabMove(event) { + let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0); + let movingTabs = draggedTab._dragData.movingTabs; + + if (this.getAttribute("movingtab") != "true") { + this.setAttribute("movingtab", "true"); + gNavToolbox.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 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.getBoundingClientRect().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; + } + } + + _finishAnimateTabMove() { + if (this.getAttribute("movingtab") != "true") { + return; + } + + for (let tab of this._getVisibleTabs()) { + tab.style.transform = ""; + } + + this.removeAttribute("movingtab"); + gNavToolbox.removeAttribute("movingtab"); + + this._handleTabSelect(); + } + + /** + * Regroup all selected tabs around the + * tab in param + */ + _groupSelectedTabs(tab) { + 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. + 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.getBoundingClientRect().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"); + } + } + } + + _finishGroupSelectedTabs(tab) { + 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; + } + } + + _isGroupTabsAnimationOver() { + for (let tab of gBrowser.selectedTabs) { + if (tab.groupingTabsData && tab.groupingTabsData.animate) + return false; + } + return true; + } + + handleEvent(aEvent) { + 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; + default: + let methodName = `on_${aEvent.type}`; + if (methodName in this) { + this[methodName](aEvent); + } else { + throw new Error(`Unexpected event ${aEvent.type}`); + } + } + } + + _notifyBackgroundTab(aTab) { + if (aTab.pinned || aTab.hidden || this.getAttribute("overflow") != "true") + return; + + this._lastTabToScrollIntoView = aTab; + if (!this._backgroundTabScrollPromise) { + this._backgroundTabScrollPromise = window.promiseDocumentFlushed(() => { + 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(([tabToScrollIntoView, 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 != tabToScrollIntoView) { + 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(tabToScrollIntoView); + 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); + } + }); + } + } + + _getDragTargetTab(event, isLink) { + let tab = event.target; + while (tab && tab.localName != "tab") { + tab = tab.parentNode; + } + if (tab && isLink) { + let { width } = tab.getBoundingClientRect(); + if (event.screenX < tab.screenX + width * .25 || + event.screenX > tab.screenX + width * .75) + return null; + } + return tab; + } + + _getDropIndex(event, isLink) { + var tabs = this.allTabs; + var tab = this._getDragTargetTab(event, isLink); + if (!RTL_UI) { + for (let i = tab ? tab._tPos : 0; i < tabs.length; i++) + if (event.screenX < tabs[i].screenX + tabs[i].getBoundingClientRect().width / 2) + return i; + } else { + for (let i = tab ? tab._tPos : 0; i < tabs.length; i++) + if (event.screenX > tabs[i].screenX + tabs[i].getBoundingClientRect().width / 2) + return i; + } + return tabs.length; + } + + _getDropEffectForTabDrag(event) { + 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.container) { + // 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"; + } + + _handleNewTab(tab) { + if (tab.container != 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(); + + // If this browser isn't lazy (indicating it's probably created by + // session restore), preload the next about:newtab if we don't + // already have a preloaded browser. + if (tab.linkedPanel) { + NewTabPagePreloading.maybeCreatePreloadedBrowser(window); + } + } + + _canAdvanceToTab(aTab) { + return !aTab.closing; + } + + getRelatedElement(aTab) { + if (!aTab) { + return null; + } + + // Cannot access gBrowser before it's initialized. + if (!gBrowser._initialized) { + 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); + } + + _updateNewTabVisibility() { + // 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 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); + } + } + + onWidgetAfterDOMChange(aNode, aNextNode, aContainer) { + if (aContainer.ownerDocument == document && + aContainer.id == "TabsToolbar-customization-target") { + this._updateNewTabVisibility(); + } + } + + onAreaNodeRegistered(aArea, aContainer) { + if (aContainer.ownerDocument == document && + aArea == "TabsToolbar") { + this._updateNewTabVisibility(); + } + } + + onAreaReset(aArea, aContainer) { + this.onAreaNodeRegistered(aArea, aContainer); + } + + _hiddenSoundPlayingStatusChanged(tab, opts) { + 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"); + } + } + } + + destroy() { + if (this.boundObserve) { + Services.prefs.removeObserver("privacy.userContext", this.boundObserve); + } + + CustomizableUI.removeListener(this); + } +} + +customElements.define("tabbrowser-tabs", MozTabbrowserTabs, {extends: "tabs"}); +} diff --git a/browser/base/content/tabbrowser.css b/browser/base/content/tabbrowser.css index 1d2518087020..536689945111 100644 --- a/browser/base/content/tabbrowser.css +++ b/browser/base/content/tabbrowser.css @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ .tab-close-button[pinned], -#tabbrowser-tabs[closebuttons="activetab"] > .tabbrowser-tab > .tab-stack > .tab-content > .tab-close-button:not([selected="true"]), +#tabbrowser-tabs[closebuttons="activetab"] > .tabbrowser-arrowscrollbox > .tabbrowser-tab > .tab-stack > .tab-content > .tab-close-button:not([selected="true"]), .tab-icon-pending:not([pendingicon]), .tab-icon-pending[busy], .tab-icon-pending[pinned], diff --git a/browser/base/content/tabbrowser.js b/browser/base/content/tabbrowser.js index 022e65b8ffe8..67a95ce643ed 100644 --- a/browser/base/content/tabbrowser.js +++ b/browser/base/content/tabbrowser.js @@ -32,6 +32,7 @@ window._gBrowser = { window.addEventListener("occlusionstatechange", this); window.addEventListener("framefocusrequested", this); + this.tabContainer.init(); this._setupInitialBrowserAndTab(); if (Services.prefs.getBoolPref("browser.display.use_system_colors")) { @@ -67,6 +68,7 @@ window._gBrowser = { "toolkit.cosmeticAnimations.enabled"); this._setupEventListeners(); + this._initialized = true; }, ownerGlobal: window, @@ -77,6 +79,8 @@ window._gBrowser = { _visibleTabs: null, + _tabs: null, + _lastRelatedTabMap: new WeakMap(), mProgressListeners: [], @@ -205,7 +209,10 @@ window._gBrowser = { }, get tabs() { - return this.tabContainer.allTabs; + if (!this._tabs) { + this._tabs = this.tabContainer.allTabs; + } + return this._tabs; }, get tabbox() { @@ -218,19 +225,16 @@ window._gBrowser = { return this.tabpanels = document.getElementById("tabbrowser-tabpanels"); }, - get addEventListener() { - delete this.addEventListener; - return this.addEventListener = this.tabpanels.addEventListener.bind(this.tabpanels); + addEventListener(...args) { + this.tabpanels.addEventListener(...args); }, - get removeEventListener() { - delete this.removeEventListener; - return this.removeEventListener = this.tabpanels.removeEventListener.bind(this.tabpanels); + removeEventListener(...args) { + this.tabpanels.removeEventListener(...args); }, - get dispatchEvent() { - delete this.dispatchEvent; - return this.dispatchEvent = this.tabpanels.dispatchEvent.bind(this.tabpanels); + dispatchEvent(...args) { + return this.tabpanels.dispatchEvent(...args); }, get visibleTabs() { @@ -494,6 +498,11 @@ window._gBrowser = { return this.selectedBrowser.userTypedValue; }, + _invalidateCachedTabs() { + this._tabs = null; + this._visibleTabs = null; + }, + _setFindbarData() { // Ensure we know what the find bar key is in the content process: let {sharedData} = Services.ppmm; @@ -2366,9 +2375,6 @@ window._gBrowser = { }, 0, this.tabContainer); } - // invalidate cache - this._visibleTabs = null; - let usingPreloadedContent = false; let b; @@ -2416,6 +2422,7 @@ window._gBrowser = { } let tabAfter = this.tabs[index] || null; + this._invalidateCachedTabs(); this.tabContainer.insertBefore(t, tabAfter); if (tabAfter) { this._updateTabsAfterInsert(); @@ -2959,7 +2966,7 @@ window._gBrowser = { aTab.closing = true; this._removingTabs.push(aTab); - this._visibleTabs = null; // invalidate cache + this._invalidateCachedTabs(); // Invalidate hovered tab state tracking for this closing tab. if (this.tabContainer._hoveredTab == aTab) @@ -3085,6 +3092,7 @@ window._gBrowser = { // Remove the tab ... aTab.remove(); + this._invalidateCachedTabs(); // Update hashiddentabs if this tab was hidden. if (aTab.hidden) @@ -3538,7 +3546,7 @@ window._gBrowser = { showTab(aTab) { if (aTab.hidden) { aTab.removeAttribute("hidden"); - this._visibleTabs = null; // invalidate cache + this._invalidateCachedTabs(); this.tabContainer._updateCloseButtons(); this.tabContainer._updateHiddenTabsStatus(); @@ -3556,7 +3564,7 @@ window._gBrowser = { if (!aTab.hidden && !aTab.pinned && !aTab.selected && !aTab.closing && !aTab._sharingState) { aTab.setAttribute("hidden", "true"); - this._visibleTabs = null; // invalidate cache + this._invalidateCachedTabs(); this.tabContainer._updateCloseButtons(); this.tabContainer._updateHiddenTabsStatus(); @@ -3723,10 +3731,9 @@ window._gBrowser = { aIndex = aIndex < aTab._tPos ? aIndex : aIndex + 1; - // invalidate cache - this._visibleTabs = null; - - this.tabContainer.insertBefore(aTab, this.tabs[aIndex] || null); + let neighbor = this.tabs[aIndex] || null; + this._invalidateCachedTabs(); + this.tabContainer.insertBefore(aTab, neighbor); this._updateTabsAfterInsert(); if (wasFocused) @@ -3889,7 +3896,7 @@ window._gBrowser = { return; } - const tabs = this._visibleTabs; + const tabs = this.visibleTabs; const indexOfTab1 = tabs.indexOf(aTab1); const indexOfTab2 = tabs.indexOf(aTab2); @@ -4450,6 +4457,7 @@ window._gBrowser = { }, destroy() { + this.tabContainer.destroy(); Services.obs.removeObserver(this, "contextual-identity-updated"); for (let tab of this.tabs) { diff --git a/browser/base/content/tabbrowser.xml b/browser/base/content/tabbrowser.xml deleted file mode 100644 index 3378f28cbe65..000000000000 --- a/browser/base/content/tabbrowser.xml +++ /dev/null @@ -1,1915 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - 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(); - this._initializeArrowScrollbox(); - - 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"; - - - - - { - // Ignore underflow events: - // - from nested scrollable elements - // - for vertical orientation - // - corresponding to an overflow event that we ignored - if (event.originalTarget != arrowScrollbox.scrollbox || - event.detail == 0 || - !this.hasAttribute("overflow")) { - return; - } - - this.removeAttribute("overflow"); - - if (this._lastTabClosedByMouse) { - this._expandSpacerBy(this._scrollButtonWidth); - } - - for (let tab of Array.from(gBrowser._removingTabs)) { - gBrowser.removeTab(tab); - } - - this._positionPinnedTabs(); - }, true); - - arrowScrollbox.addEventListener("overflow", event => { - // Ignore overflow events: - // - from nested scrollable elements - // - for vertical orientation - if (event.originalTarget != arrowScrollbox.scrollbox || - event.detail == 0) { - return; - } - - this.setAttribute("overflow", "true"); - this._positionPinnedTabs(); - this._handleTabSelect(true); - }); - - // Override scrollbox.xml method, since our scrollbox's children are - // inherited from the scrollbox binding parent (this). - arrowScrollbox._getScrollableElements = () => { - return Array.prototype.filter.call(this.children, arrowScrollbox._canScrollToElement); - }; - arrowScrollbox._canScrollToElement = tab => { - return !tab._pinnedUnscrollable && !tab.hidden; - }; - ]]> - - - - - - - 2)) { - containersEnabled = false; - } - - // There are separate "new tab" buttons for when the tab strip - // is overflowed and when it is not. Attach the long click - // popup to both of them. - 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.menupopup) { - parent.menupopup.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.prepend(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 - 0 - - - - - - tabs[tabs.length - 1]._tPos); - - if (!this._tabDefaultMaxWidth) { - this._tabDefaultMaxWidth = - parseFloat(window.getComputedStyle(aTab).maxWidth); - } - this._lastTabClosedByMouse = true; - this._scrollButtonWidth = window.windowUtils.getBoundsWithoutFlushing(this.arrowScrollbox._scrollButtonDown).width; - - 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.getBoundingClientRect().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.getBoundingClientRect().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(([tabToScrollIntoView, 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 != tabToScrollIntoView) { - 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(tabToScrollIntoView); - 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 + width * .75) - return null; - } - return tab; - ]]> - - - - - - tabs[i].screenX + tabs[i].getBoundingClientRect().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(); - ]]> - - - - - diff --git a/browser/base/content/test/performance/browser_startup_images.js b/browser/base/content/test/performance/browser_startup_images.js index e1e3a2193998..c52f509bdbad 100644 --- a/browser/base/content/test/performance/browser_startup_images.js +++ b/browser/base/content/test/performance/browser_startup_images.js @@ -31,18 +31,6 @@ const whitelist = [ platforms: ["linux", "win", "macosx"], }, - { - file: "chrome://browser/skin/tabbrowser/tabDragIndicator.png", - hidpi: "chrome://browser/skin/tabbrowser/tabDragIndicator@2x.png", - platforms: ["macosx"], - }, - - { - file: "chrome://browser/skin/tabbrowser/tabDragIndicator.png", - hidpi: "", - platforms: ["linux", "win"], - }, - { file: "resource://gre-resources/loading-image.png", platforms: ["win", "macosx"], diff --git a/browser/base/content/test/performance/browser_tabclose.js b/browser/base/content/test/performance/browser_tabclose.js index 7eb8f4940424..df1884e6c2f4 100644 --- a/browser/base/content/test/performance/browser_tabclose.js +++ b/browser/base/content/test/performance/browser_tabclose.js @@ -26,10 +26,8 @@ add_task(async function() { await BrowserTestUtils.waitForCondition(() => tab._fullyOpen); let tabStripRect = gBrowser.tabContainer.arrowScrollbox.getBoundingClientRect(); - let newTabButtonRect = - document.getAnonymousElementByAttribute(gBrowser.tabContainer, - "anonid", "tabs-newtab-button") - .getBoundingClientRect(); + let newTabButtonRect = gBrowser.tabContainer.newTabButton + .getBoundingClientRect(); let inRange = (val, min, max) => min <= val && val <= max; // Add a reflow observer and open a new tab. diff --git a/browser/base/content/test/performance/browser_tabdetach.js b/browser/base/content/test/performance/browser_tabdetach.js index a9243d86fd33..d6f368deef10 100644 --- a/browser/base/content/test/performance/browser_tabdetach.js +++ b/browser/base/content/test/performance/browser_tabdetach.js @@ -12,8 +12,9 @@ const EXPECTED_REFLOWS = [ { stack: [ - "clientX@chrome://browser/content/tabbrowser.xml", - "onxbldragstart@chrome://browser/content/tabbrowser.xml", + "clientX@chrome://browser/content/tabbrowser-tabs.js", + "on_dragstart@chrome://browser/content/tabbrowser-tabs.js", + "handleEvent@chrome://browser/content/tabbrowser-tabs.js", "synthesizeMouseAtPoint@chrome://mochikit/content/tests/SimpleTest/EventUtils.js", "synthesizeMouse@chrome://mochikit/content/tests/SimpleTest/EventUtils.js", "synthesizePlainDragAndDrop@chrome://mochikit/content/tests/SimpleTest/EventUtils.js", @@ -23,7 +24,8 @@ const EXPECTED_REFLOWS = [ { stack: [ - "onxbldragstart@chrome://browser/content/tabbrowser.xml", + "on_dragstart@chrome://browser/content/tabbrowser-tabs.js", + "handleEvent@chrome://browser/content/tabbrowser-tabs.js", "synthesizeMouseAtPoint@chrome://mochikit/content/tests/SimpleTest/EventUtils.js", "synthesizeMouse@chrome://mochikit/content/tests/SimpleTest/EventUtils.js", "synthesizePlainDragAndDrop@chrome://mochikit/content/tests/SimpleTest/EventUtils.js", diff --git a/browser/base/content/test/performance/head.js b/browser/base/content/test/performance/head.js index e06a863200f2..514575dbd605 100644 --- a/browser/base/content/test/performance/head.js +++ b/browser/base/content/test/performance/head.js @@ -278,9 +278,7 @@ async function ensureFocusedUrlbar() { */ function computeMaxTabCount() { let currentTabCount = gBrowser.tabs.length; - let newTabButton = - document.getAnonymousElementByAttribute(gBrowser.tabContainer, - "anonid", "tabs-newtab-button"); + let newTabButton = gBrowser.tabContainer.newTabButton; let newTabRect = newTabButton.getBoundingClientRect(); let tabStripRect = gBrowser.tabContainer.arrowScrollbox.getBoundingClientRect(); let availableTabStripWidth = tabStripRect.width - newTabRect.width; diff --git a/browser/base/content/test/tabs/browser_multiselect_tabs_open_related.js b/browser/base/content/test/tabs/browser_multiselect_tabs_open_related.js index 1b18dd0e0526..ab9eeaed43e8 100644 --- a/browser/base/content/test/tabs/browser_multiselect_tabs_open_related.js +++ b/browser/base/content/test/tabs/browser_multiselect_tabs_open_related.js @@ -21,8 +21,7 @@ add_task(async function test() { let metaKeyEvent = AppConstants.platform == "macosx" ? {metaKey: true} : {ctrlKey: true}; - let tabs = document.getElementById("tabbrowser-tabs"); - let newTabButton = document.getAnonymousElementByAttribute(tabs, "anonid", "tabs-newtab-button"); + let newTabButton = gBrowser.tabContainer.newTabButton; let promiseTabOpened = BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "TabOpen"); EventUtils.synthesizeMouseAtCenter(newTabButton, metaKeyEvent); let openEvent = await promiseTabOpened; diff --git a/browser/base/jar.mn b/browser/base/jar.mn index f6377d947605..540f045777dd 100644 --- a/browser/base/jar.mn +++ b/browser/base/jar.mn @@ -92,7 +92,7 @@ browser.jar: content/browser/tabbrowser.css (content/tabbrowser.css) content/browser/tabbrowser.js (content/tabbrowser.js) content/browser/tabbrowser-tab.js (content/tabbrowser-tab.js) - content/browser/tabbrowser.xml (content/tabbrowser.xml) + content/browser/tabbrowser-tabs.js (content/tabbrowser-tabs.js) * content/browser/urlbarBindings.xml (content/urlbarBindings.xml) content/browser/utilityOverlay.js (content/utilityOverlay.js) content/browser/webext-panels.js (content/webext-panels.js) diff --git a/browser/components/contextualidentity/test/browser/browser_newtabButton.js b/browser/components/contextualidentity/test/browser/browser_newtabButton.js index 46e0b0aa8bc6..852848db14c4 100644 --- a/browser/components/contextualidentity/test/browser/browser_newtabButton.js +++ b/browser/components/contextualidentity/test/browser/browser_newtabButton.js @@ -3,18 +3,23 @@ // Testing that when the user opens the add tab menu and clicks menu items // the correct context id is opened +function findPopup(browser = gBrowser) { + return browser.tabContainer.querySelector(".new-tab-popup"); +} + add_task(async function test_menu_with_timeout() { await SpecialPowers.pushPrefEnv({"set": [ ["privacy.userContext.enabled", true], ["privacy.userContext.longPressBehavior", 2], ]}); - let newTab = document.getElementById("tabbrowser-tabs"); - let newTabButton = document.getAnonymousElementByAttribute(newTab, "anonid", "tabs-newtab-button"); + let newTabButton = gBrowser.tabContainer.newTabButton; ok(newTabButton, "New tab button exists"); ok(!newTabButton.hidden, "New tab button is visible"); - await BrowserTestUtils.waitForCondition(() => !!document.getAnonymousElementByAttribute(newTab, "anonid", "newtab-popup"), "Wait for popup to exist"); - let popup = document.getAnonymousElementByAttribute(newTab, "anonid", "newtab-popup"); + + await BrowserTestUtils.waitForCondition(() => !!findPopup(), "Wait for popup to exist"); + + let popup = findPopup(); for (let i = 1; i <= 4; i++) { let popupShownPromise = BrowserTestUtils.waitForEvent(popup, "popupshown"); @@ -41,12 +46,12 @@ add_task(async function test_menu_without_timeout() { ["privacy.userContext.longPressBehavior", 1], ]}); - let newTab = document.getElementById("tabbrowser-tabs"); - let newTabButton = document.getAnonymousElementByAttribute(newTab, "anonid", "tabs-newtab-button"); + let newTabButton = gBrowser.tabContainer.newTabButton; ok(newTabButton, "New tab button exists"); ok(!newTabButton.hidden, "New tab button is visible"); - await BrowserTestUtils.waitForCondition(() => !!document.getAnonymousElementByAttribute(newTab, "anonid", "newtab-popup"), "Wait for popup to exist"); - let popup = document.getAnonymousElementByAttribute(newTab, "anonid", "newtab-popup"); + + await BrowserTestUtils.waitForCondition(() => !!findPopup(), "Wait for popup to exist"); + let popup = findPopup(); let popupShownPromise = BrowserTestUtils.waitForEvent(popup, "popupshown"); let popupHiddenPromise = BrowserTestUtils.waitForEvent(popup, "popuphidden"); @@ -88,11 +93,10 @@ add_task(async function test_no_menu() { ["privacy.userContext.longPressBehavior", 0], ]}); - let newTab = document.getElementById("tabbrowser-tabs"); - let newTabButton = document.getAnonymousElementByAttribute(newTab, "anonid", "tabs-newtab-button"); + let newTabButton = gBrowser.tabContainer.newTabButton; ok(newTabButton, "New tab button exists"); ok(!newTabButton.hidden, "New tab button is visible"); - let popup = document.getAnonymousElementByAttribute(newTab, "anonid", "newtab-popup"); + let popup = findPopup(); ok(!popup, "new tab should not have a popup"); }); @@ -100,12 +104,13 @@ add_task(async function test_private_mode() { let privateWindow = await BrowserTestUtils.openNewBrowserWindow({private: true}); let privateDocument = privateWindow.document; let {tabContainer} = privateWindow.gBrowser; - let newTab = privateDocument.getAnonymousElementByAttribute(tabContainer, "anonid", "tabs-newtab-button"); + let newTab = tabContainer.newTabButton; let newTab2 = privateDocument.getElementById("new-tab-button"); // Check to ensure we are talking about the right button ok(!!newTab.clientWidth, "new tab button should not be hidden"); ok(!newTab2.clientWidth, "overflow new tab button should be hidden"); - let popup = privateDocument.getAnonymousElementByAttribute(newTab, "anonid", "newtab-popup"); + let popup = findPopup(privateWindow.gBrowser); ok(!popup, "new tab should not have a popup"); await BrowserTestUtils.closeWindow(privateWindow); }); + diff --git a/browser/components/customizableui/test/browser_newtab_button_customizemode.js b/browser/components/customizableui/test/browser_newtab_button_customizemode.js index 7e8228216ab5..8d7d2d0f9ae2 100644 --- a/browser/components/customizableui/test/browser_newtab_button_customizemode.js +++ b/browser/components/customizableui/test/browser_newtab_button_customizemode.js @@ -9,7 +9,7 @@ */ const kGlobalNewTabButton = document.getElementById("new-tab-button"); -const kInnerNewTabButton = document.getAnonymousElementByAttribute(gBrowser.tabContainer, "anonid", "tabs-newtab-button"); +const kInnerNewTabButton = gBrowser.tabContainer.newTabButton; function assertNewTabButton(which) { if (which == "global") { diff --git a/browser/themes/shared/tabs.inc.css b/browser/themes/shared/tabs.inc.css index be63bba202ba..8ef44a454f07 100644 --- a/browser/themes/shared/tabs.inc.css +++ b/browser/themes/shared/tabs.inc.css @@ -49,7 +49,7 @@ #tabbrowser-tabs, #tabbrowser-tabs > .tabbrowser-arrowscrollbox, -#tabbrowser-tabs[positionpinnedtabs] > .tabbrowser-tab[pinned] { +#tabbrowser-tabs[positionpinnedtabs] > .tabbrowser-arrowscrollbox > .tabbrowser-tab[pinned] { min-height: var(--tab-min-height); } @@ -537,8 +537,8 @@ */ /* Lightweight theme on tabs */ -#tabbrowser-tabs:not([movingtab]) > .tabbrowser-tab > .tab-stack > .tab-background[multiselected=true]:-moz-lwtheme, -#tabbrowser-tabs:not([movingtab]) > .tabbrowser-tab > .tab-stack > .tab-background[selected=true]:-moz-lwtheme { +#tabbrowser-tabs:not([movingtab]) > .tabbrowser-arrowscrollbox > .tabbrowser-tab > .tab-stack > .tab-background[multiselected=true]:-moz-lwtheme, +#tabbrowser-tabs:not([movingtab]) > .tabbrowser-arrowscrollbox > .tabbrowser-tab > .tab-stack > .tab-background[selected=true]:-moz-lwtheme { background-attachment: scroll, scroll, fixed; background-color: transparent; background-image: linear-gradient(var(--lwt-selected-tab-background-color, transparent), var(--lwt-selected-tab-background-color, transparent)), @@ -582,19 +582,19 @@ /* Pinned tabs */ /* Pinned tab separators need position: absolute when positioned (during overflow). */ -#tabbrowser-tabs[positionpinnedtabs] > .tabbrowser-tab[pinned]::after { +#tabbrowser-tabs[positionpinnedtabs] > .tabbrowser-arrowscrollbox > .tabbrowser-tab[pinned]::after { position: absolute; top: 0; bottom: 0; right: 0; } -#tabbrowser-tabs[positionpinnedtabs] > .tabbrowser-tab[pinned]:-moz-locale-dir(rtl)::after { +#tabbrowser-tabs[positionpinnedtabs] > .tabbrowser-arrowscrollbox > .tabbrowser-tab[pinned]:-moz-locale-dir(rtl)::after { right: unset; left: 0; } -#tabbrowser-tabs[positionpinnedtabs] > .tabbrowser-tab[pinned] > .tab-stack { +#tabbrowser-tabs[positionpinnedtabs] > .tabbrowser-arrowscrollbox > .tabbrowser-tab[pinned] > .tab-stack { border-inline-end: 1px solid transparent; } @@ -664,16 +664,16 @@ %endif /* Show full height tab separators on hover and multiselection. */ .tabbrowser-tab:hover::after, -#tabbrowser-tabs:not([movingtab]) > .tabbrowser-tab[beforehovered]::after, +#tabbrowser-tabs:not([movingtab]) > .tabbrowser-arrowscrollbox > .tabbrowser-tab[beforehovered]::after, .tabbrowser-tab[multiselected]::after, -#tabbrowser-tabs:not([movingtab]) > .tabbrowser-tab[before-multiselected]::after { +#tabbrowser-tabs:not([movingtab]) > .tabbrowser-arrowscrollbox > .tabbrowser-tab[before-multiselected]::after { margin-top: var(--tabs-top-border-width); margin-bottom: 0; } /* Show full height tab separators on selected tabs. */ -#tabbrowser-tabs:not([movingtab]) > .tabbrowser-tab[beforeselected-visible]::after, -#tabbrowser-tabs[movingtab] > .tabbrowser-tab[visuallyselected]::before, +#tabbrowser-tabs:not([movingtab]) > .tabbrowser-arrowscrollbox > .tabbrowser-tab[beforeselected-visible]::after, +#tabbrowser-tabs[movingtab] > .tabbrowser-arrowscrollbox > .tabbrowser-tab[visuallyselected]::before, .tabbrowser-tab[visuallyselected]::after { border-color: var(--tabs-border-color); margin-top: 0; @@ -683,7 +683,7 @@ .tabbrowser-tab::after, /* Also show separators beside the selected tab when dragging it. */ -#tabbrowser-tabs[movingtab] > .tabbrowser-tab[visuallyselected]::before { +#tabbrowser-tabs[movingtab] > .tabbrowser-arrowscrollbox > .tabbrowser-tab[visuallyselected]::before { content: ""; display: -moz-box; } diff --git a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Tabs.jsm b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Tabs.jsm index 1f90daeca329..6437a38ff05f 100644 --- a/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Tabs.jsm +++ b/browser/tools/mozscreenshots/mozscreenshots/extension/configurations/Tabs.jsm @@ -59,8 +59,7 @@ var Tabs = { browserWindow.gBrowser.selectTabAtIndex(5); hoverTab(browserWindow.gBrowser.tabs[2]); // also hover the new tab button - let newTabButton = browserWindow.document.getAnonymousElementByAttribute(browserWindow. - gBrowser.tabContainer, "anonid", "tabs-newtab-button"); + let newTabButton = browserWindow.gBrowser.tabContainer.newTabButton; hoverTab(newTabButton); browserWindow.gBrowser.tabs[browserWindow.gBrowser.tabs.length - 1]. setAttribute("beforehovered", true); @@ -197,7 +196,7 @@ function closeAllButOneTab(url = "about:blank") { }); if (gBrowser.selectedTab.pinned) gBrowser.unpinTab(gBrowser.selectedTab); - let newTabButton = browserWindow.document.getAnonymousElementByAttribute(browserWindow.gBrowser.tabContainer, "class", "tabs-newtab-button toolbarbutton-1"); + let newTabButton = gBrowser.tabContainer.newTabButton; hoverTab(newTabButton, false); } diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/tabbar.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/tabbar.py index 2cf77ad52900..08db7bbb5c3e 100644 --- a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/tabbar.py +++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/tabbar.py @@ -36,7 +36,7 @@ class TabBar(UIBaseLib): :returns: Reference to the new tab button. """ - return self.toolbar.find_element(By.ANON_ATTRIBUTE, {'anonid': 'tabs-newtab-button'}) + return self.toolbar.find_element(By.CLASS_NAME, 'tabs-newtab-button') @property def tabs(self): diff --git a/toolkit/content/jar.mn b/toolkit/content/jar.mn index 565572a85102..d4e5313da869 100644 --- a/toolkit/content/jar.mn +++ b/toolkit/content/jar.mn @@ -71,7 +71,6 @@ toolkit.jar: content/global/bindings/richlistbox.xml (widgets/richlistbox.xml) content/global/bindings/scrollbox.xml (widgets/scrollbox.xml) content/global/bindings/spinner.js (widgets/spinner.js) - content/global/bindings/tabbox.xml (widgets/tabbox.xml) * content/global/bindings/textbox.xml (widgets/textbox.xml) content/global/bindings/timekeeper.js (widgets/timekeeper.js) content/global/bindings/timepicker.js (widgets/timepicker.js) diff --git a/toolkit/content/widgets/tabbox.js b/toolkit/content/widgets/tabbox.js index 8a74009927ea..50b0df65e15a 100644 --- a/toolkit/content/widgets/tabbox.js +++ b/toolkit/content/widgets/tabbox.js @@ -431,4 +431,354 @@ MozElements.MozTab = class MozTab extends MozElements.BaseText { MozXULElement.implementCustomInterface(MozElements.MozTab, [Ci.nsIDOMXULSelectControlItemElement]); customElements.define("tab", MozElements.MozTab); + +class TabsBase extends MozElements.BaseControl { + constructor() { + super(); + + this.addEventListener("DOMMouseScroll", (event) => { + if (Services.prefs.getBoolPref("toolkit.tabbox.switchByScrolling")) { + if (event.detail > 0) { + this.advanceSelectedTab(1, false); + } else { + this.advanceSelectedTab(-1, false); + } + event.stopPropagation(); + } + }); + } + + // to be called from derived class connectedCallback + baseConnect() { + this._tabbox = null; + this.ACTIVE_DESCENDANT_ID = "keyboard-focused-tab-" + Math.trunc(Math.random() * 1000000); + + if (!this.hasAttribute("orient")) + this.setAttribute("orient", "horizontal"); + + if (this.tabbox && this.tabbox.hasAttribute("selectedIndex")) { + let selectedIndex = parseInt(this.tabbox.getAttribute("selectedIndex")); + this.selectedIndex = selectedIndex > 0 ? selectedIndex : 0; + return; + } + + let children = this.allTabs; + let length = children.length; + for (var i = 0; i < length; i++) { + if (children[i].getAttribute("selected") == "true") { + this.selectedIndex = i; + return; + } + } + + var value = this.value; + if (value) + this.value = value; + else + this.selectedIndex = 0; + } + + /** + * nsIDOMXULSelectControlElement + */ + get itemCount() { + return this.allTabs.length; + } + + set value(val) { + this.setAttribute("value", val); + var children = this.allTabs; + for (var c = children.length - 1; c >= 0; c--) { + if (children[c].value == val) { + this.selectedIndex = c; + break; + } + } + return val; + } + + get value() { + return this.getAttribute("value"); + } + + get tabbox() { + if (!this._tabbox) { + // Memoize the result in a field rather than replacing this property, + // so that it can be reset along with the binding. + this._tabbox = this.closest("tabbox"); + } + + return this._tabbox; + } + + set selectedIndex(val) { + var tab = this.getItemAtIndex(val); + if (tab) { + for (let otherTab of this.allTabs) { + if (otherTab != tab && otherTab.selected) { + otherTab._selected = false; + } + } + tab._selected = true; + + this.setAttribute("value", tab.value); + + let linkedPanel = this.getRelatedElement(tab); + if (linkedPanel) { + this.tabbox.setAttribute("selectedIndex", val); + + // This will cause an onselect event to fire for the tabpanel + // element. + this.tabbox.tabpanels.selectedPanel = linkedPanel; + } + } + return val; + } + + get selectedIndex() { + const tabs = this.allTabs; + for (var i = 0; i < tabs.length; i++) { + if (tabs[i].selected) + return i; + } + return -1; + } + + set selectedItem(val) { + if (val && !val.selected) + // The selectedIndex setter ignores invalid values + // such as -1 if |val| isn't one of our child nodes. + this.selectedIndex = this.getIndexOfItem(val); + return val; + } + + get selectedItem() { + const tabs = this.allTabs; + for (var i = 0; i < tabs.length; i++) { + if (tabs[i].selected) + return tabs[i]; + } + return null; + } + + get ariaFocusedIndex() { + const tabs = this.allTabs; + for (var i = 0; i < tabs.length; i++) { + if (tabs[i].id == this.ACTIVE_DESCENDANT_ID) + return i; + } + return -1; + } + + set ariaFocusedItem(val) { + let setNewItem = val && this.getIndexOfItem(val) != -1; + let clearExistingItem = this.ariaFocusedItem && (!val || setNewItem); + if (clearExistingItem) { + let ariaFocusedItem = this.ariaFocusedItem; + ariaFocusedItem.classList.remove("keyboard-focused-tab"); + ariaFocusedItem.id = ""; + this.selectedItem.removeAttribute("aria-activedescendant"); + } + + if (setNewItem) { + this.ariaFocusedItem = null; + val.id = this.ACTIVE_DESCENDANT_ID; + val.classList.add("keyboard-focused-tab"); + this.selectedItem.setAttribute("aria-activedescendant", this.ACTIVE_DESCENDANT_ID); + } + + return val; + } + + get ariaFocusedItem() { + return document.getElementById(this.ACTIVE_DESCENDANT_ID); + } + + /** + * nsIDOMXULRelatedElement + */ + getRelatedElement(aTabElm) { + if (!aTabElm) + return null; + + let tabboxElm = this.tabbox; + if (!tabboxElm) + return null; + + let tabpanelsElm = tabboxElm.tabpanels; + if (!tabpanelsElm) + return null; + + // Get linked tab panel by 'linkedpanel' attribute on the given tab + // element. + let linkedPanelId = aTabElm.linkedPanel; + if (linkedPanelId) { + return this.ownerDocument.getElementById(linkedPanelId); + } + + // otherwise linked tabpanel element has the same index as the given + // tab element. + let tabElmIdx = this.getIndexOfItem(aTabElm); + return tabpanelsElm.children[tabElmIdx]; + } + + getIndexOfItem(item) { + return Array.prototype.indexOf.call(this.allTabs, item); + } + + getItemAtIndex(index) { + return this.allTabs[index] || null; + } + + /** + * Find an adjacent tab. + * + * @param {Node} startTab A element to start searching from. + * @param {Number} opts.direction 1 to search forward, -1 to search backward. + * @param {Boolean} opts.wrap If true, wrap around if the search reaches + * the end (or beginning) of the tab strip. + * @param {Boolean} opts.startWithAdjacent + * If true (which is the default), start + * searching from the next tab after (or + * before) startTab. If false, startTab may + * be returned if it passes the filter. + * @param {Boolean} opts.advance If false, start searching with startTab. If + * true, start searching with an adjacent tab. + * @param {Function} opts.filter A function to select which tabs to return. + * + * @return {Node | null} The next element or, if none exists, null. + */ + findNextTab(startTab, opts = {}) { + let { + direction = 1, + wrap = false, + startWithAdjacent = true, + filter = tab => true, + } = opts; + + let tab = startTab; + if (!startWithAdjacent && filter(tab)) { + return tab; + } + + let children = this.allTabs; + let i = children.indexOf(tab); + if (i < 0) { + return null; + } + + while (true) { + i += direction; + if (wrap) { + if (i < 0) { + i = children.length - 1; + } else if (i >= children.length) { + i = 0; + } + } else if (i < 0 || i >= children.length) { + return null; + } + + tab = children[i]; + if (tab == startTab) { + return null; + } + if (filter(tab)) { + return tab; + } + } + } + + _selectNewTab(aNewTab, aFallbackDir, aWrap) { + this.ariaFocusedItem = null; + + aNewTab = this.findNextTab(aNewTab, { + direction: aFallbackDir, + wrap: aWrap, + startWithAdjacent: false, + filter: tab => !tab.hidden && !tab.disabled && this._canAdvanceToTab(tab), + }); + + var isTabFocused = false; + try { + isTabFocused = + (document.commandDispatcher.focusedElement == this.selectedItem); + } catch (e) {} + this.selectedItem = aNewTab; + if (isTabFocused) { + aNewTab.focus(); + } else if (this.getAttribute("setfocus") != "false") { + let selectedPanel = this.tabbox.selectedPanel; + document.commandDispatcher.advanceFocusIntoSubtree(selectedPanel); + + // Make sure that the focus doesn't move outside the tabbox + if (this.tabbox) { + try { + let el = document.commandDispatcher.focusedElement; + while (el && el != this.tabbox.tabpanels) { + if (el == this.tabbox || el == selectedPanel) + return; + el = el.parentNode; + } + aNewTab.focus(); + } catch (e) {} + } + } + } + + _canAdvanceToTab(aTab) { + return true; + } + + advanceSelectedTab(aDir, aWrap) { + let startTab = this.ariaFocusedItem || this.selectedItem; + let newTab = this.findNextTab(startTab, { + direction: aDir, + wrap: aWrap, + }); + this._selectNewTab(newTab, aDir, aWrap); + } + + appendItem(label, value) { + var tab = document.createXULElement("tab"); + tab.setAttribute("label", label); + tab.setAttribute("value", value); + this.appendChild(tab); + return tab; + } +} + +MozXULElement.implementCustomInterface(TabsBase, [Ci.nsIDOMXULSelectControlElement, Ci.nsIDOMXULRelatedElement]); + +MozElements.TabsBase = TabsBase; + +class MozTabs extends TabsBase { + connectedCallback() { + if (this.delayConnectedCallback()) { + return; + } + + let start = MozXULElement.parseXULToFragment(``); + this.insertBefore(start, this.firstChild); + + let end = MozXULElement.parseXULToFragment(``); + this.insertBefore(end, null); + + this.baseConnect(); + } + + // Accessor for tabs. This element has spacers as the first and + // last elements and s are everything in between. + get allTabs() { + let children = Array.from(this.children); + return children.splice(1, children.length - 2); + } + + appendChild(tab) { + // insert before the end spacer. + this.insertBefore(tab, this.lastChild); + } +} + +customElements.define("tabs", MozTabs); } diff --git a/toolkit/content/widgets/tabbox.xml b/toolkit/content/widgets/tabbox.xml deleted file mode 100644 index eda95cb7c64a..000000000000 --- a/toolkit/content/widgets/tabbox.xml +++ /dev/null @@ -1,440 +0,0 @@ - - - - - - - - - - - - - - - 0 ? selectedIndex : 0; - return; - } - - var children = this.children; - var length = children.length; - for (var i = 0; i < length; i++) { - if (children[i].getAttribute("selected") == "true") { - this.selectedIndex = i; - return; - } - } - - var value = this.value; - if (value) - this.value = value; - else - this.selectedIndex = 0; - ]]> - - - - - return this.children; - - - - - - - - - - - - - - - - - = 0; c--) { - if (children[c].value == val) { - this.selectedIndex = c; - break; - } - } - return val; - ]]> - - - - null - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - element to start searching from. - * opts.direction 1 to search forward, -1 to search backward. - * opts.wrap If true, wrap around if the search reaches - * the end (or beginning) of the tab strip. - * opts.startWithAdjacent If true (which is the default), start - * searching from the next tab after - * (or before) startTab. If false, - * startTab may be returned if it passes - * the filter. - * opts.filter A function to select which tabs to return. - * - * returns the next element or, if none exists, null. - */ - let { - direction = 1, - wrap = false, - startWithAdjacent = true, - filter = tab => true, - } = opts || {}; - - let tab = startTab; - if (!startWithAdjacent && filter(tab)) { - return tab; - } - - let tabs = this.allTabs; - let i = tabs.indexOf(tab); - if (i < 0) { - return null; - } - - while (true) { - i += direction; - if (wrap) { - if (i < 0) { - i = tabs.length - 1; - } else if (i >= tabs.length) { - i = 0; - } - } else if (i < 0 || i >= tabs.length) { - return null; - } - - tab = tabs[i]; - if (tab == startTab) { - return null; - } - if (filter(tab)) { - return tab; - } - } - ]]> - - - - - - - - - !tab.hidden && !tab.disabled && this._canAdvanceToTab(tab), - }); - - var isTabFocused = false; - try { - isTabFocused = - (document.commandDispatcher.focusedElement == this.selectedItem); - } catch (e) {} - this.selectedItem = aNewTab; - if (isTabFocused) { - aNewTab.focus(); - } else if (this.getAttribute("setfocus") != "false") { - let selectedPanel = this.tabbox.selectedPanel; - document.commandDispatcher.advanceFocusIntoSubtree(selectedPanel); - - // Make sure that the focus doesn't move outside the tabbox - if (this.tabbox) { - try { - let el = document.commandDispatcher.focusedElement; - while (el && el != this.tabbox.tabpanels) { - if (el == this.tabbox || el == selectedPanel) - return; - el = el.parentNode; - } - aNewTab.focus(); - } catch (e) { - } - } - } - ]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 0) { - this.advanceSelectedTab(1, false); - } else { - this.advanceSelectedTab(-1, false); - } - event.stopPropagation(); - } - ]]> - - - - diff --git a/toolkit/content/xul.css b/toolkit/content/xul.css index 98cd83975d0a..4c4e73afc3ab 100644 --- a/toolkit/content/xul.css +++ b/toolkit/content/xul.css @@ -408,7 +408,6 @@ tabbox { } tabs { - -moz-binding: url("chrome://global/content/bindings/tabbox.xml#tabs"); -moz-box-orient: horizontal; }