1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 /* eslint-env mozilla/browser-window */
9 // This is loaded into all browser windows. Wrap in a block to prevent
10 // leaking to window scope.
12 const TAB_PREVIEW_PREF = "browser.tabs.cardPreview.enabled";
14 class MozTabbrowserTabs extends MozElements.TabsBase {
18 this.addEventListener("TabSelect", this);
19 this.addEventListener("TabClose", this);
20 this.addEventListener("TabAttrModified", this);
21 this.addEventListener("TabHide", this);
22 this.addEventListener("TabShow", this);
23 this.addEventListener("TabPinned", this);
24 this.addEventListener("TabUnpinned", this);
25 this.addEventListener("TabHoverStart", this);
26 this.addEventListener("TabHoverEnd", this);
27 this.addEventListener("transitionend", this);
28 this.addEventListener("dblclick", this);
29 this.addEventListener("click", this);
30 this.addEventListener("click", this, true);
31 this.addEventListener("keydown", this, { mozSystemGroup: true });
32 this.addEventListener("dragstart", this);
33 this.addEventListener("dragover", this);
34 this.addEventListener("drop", this);
35 this.addEventListener("dragend", this);
36 this.addEventListener("dragleave", this);
37 this.addEventListener("mouseleave", this);
41 this.arrowScrollbox = this.querySelector("arrowscrollbox");
42 this.arrowScrollbox.addEventListener("wheel", this, true);
46 this._blockDblClick = false;
47 this._tabDropIndicator = this.querySelector(".tab-drop-indicator");
48 this._dragOverDelay = 350;
50 this._closeButtonsUpdatePending = false;
51 this._closingTabsSpacer = this.querySelector(".closing-tabs-spacer");
52 this._tabDefaultMaxWidth = NaN;
53 this._lastTabClosedByMouse = false;
54 this._hasTabTempMaxWidth = false;
55 this._scrollButtonWidth = 0;
56 this._lastNumPinned = 0;
57 this._pinnedTabsLayoutCache = null;
58 this._animateElement = this.arrowScrollbox;
59 this._tabClipWidth = Services.prefs.getIntPref(
60 "browser.tabs.tabClipWidth"
62 this._hiddenSoundPlayingTabs = new Set();
64 this._visibleTabs = null;
66 var tab = this.allTabs[0];
67 tab.label = this.emptyTabTitle;
69 // Hide the secondary text for locales where it is unsupported due to size constraints.
70 const language = Services.locale.appLocaleAsBCP47;
71 const unsupportedLocales = Services.prefs.getCharPref(
72 "browser.tabs.secondaryTextUnsupportedLocales"
75 "secondarytext-unsupported",
76 unsupportedLocales.split(",").includes(language.split("-")[0])
79 this.newTabButton.setAttribute(
81 GetDynamicShortcutTooltipText("tabs-newtab-button")
84 let handleResize = () => {
85 this._updateCloseButtons();
86 this._handleTabSelect(true);
88 window.addEventListener("resize", handleResize);
89 this._fullscreenMutationObserver = new MutationObserver(handleResize);
90 this._fullscreenMutationObserver.observe(document.documentElement, {
91 attributeFilter: ["inFullscreen", "inDOMFullscreen"],
94 this.boundObserve = (...args) => this.observe(...args);
95 Services.prefs.addObserver("privacy.userContext", this.boundObserve);
96 this.observe(null, "nsPref:changed", "privacy.userContext.enabled");
98 XPCOMUtils.defineLazyPreferenceGetter(
101 "browser.tabs.tabMinWidth",
103 (pref, prevValue, newValue) => (this._tabMinWidth = newValue),
106 return Math.max(newValue, LIMIT);
110 this._tabMinWidth = this._tabMinWidthPref;
112 CustomizableUI.addListener(this);
113 this._updateNewTabVisibility();
114 this._initializeArrowScrollbox();
116 XPCOMUtils.defineLazyPreferenceGetter(
118 "_closeTabByDblclick",
119 "browser.tabs.closeTabByDblclick",
123 if (gMultiProcessBrowser) {
124 this.tabbox.tabpanels.setAttribute("async", "true");
127 this.configureTooltip = () => {
128 // fall back to original tooltip behavior if pref is not set
129 this.tooltip = this._showCardPreviews ? null : "tabbrowser-tab-tooltip";
131 // activate new tooltip behavior if pref is set
133 .getElementById("tabbrowser-tab-preview")
134 .toggleAttribute("hidden", !this._showCardPreviews);
136 XPCOMUtils.defineLazyPreferenceGetter(
141 () => this.configureTooltip()
143 this.configureTooltip();
147 this._handleTabSelect();
151 this._hiddenSoundPlayingStatusChanged(event.target, { closed: true });
154 on_TabAttrModified(event) {
156 ["soundplaying", "muted", "activemedia-blocked", "sharing"].some(attr =>
157 event.detail.changed.includes(attr)
160 this.updateTabIndicatorAttr(event.target);
164 event.detail.changed.includes("soundplaying") &&
167 this._hiddenSoundPlayingStatusChanged(event.target);
172 if (event.target.soundPlaying) {
173 this._hiddenSoundPlayingStatusChanged(event.target);
178 if (event.target.soundPlaying) {
179 this._hiddenSoundPlayingStatusChanged(event.target);
183 on_TabPinned(event) {
184 this.updateTabIndicatorAttr(event.target);
187 on_TabUnpinned(event) {
188 this.updateTabIndicatorAttr(event.target);
191 on_TabHoverStart(event) {
192 if (this._showCardPreviews) {
193 const previewContainer = document.getElementById(
194 "tabbrowser-tab-preview"
196 previewContainer.tab = event.target;
200 on_TabHoverEnd(event) {
201 if (this._showCardPreviews) {
202 const previewContainer = document.getElementById(
203 "tabbrowser-tab-preview"
205 if (previewContainer.tab === event.target) {
206 previewContainer.tab = null;
211 on_transitionend(event) {
212 if (event.propertyName != "max-width") {
216 let tab = event.target ? event.target.closest("tab") : null;
218 if (tab.hasAttribute("fadein")) {
219 if (tab._fullyOpen) {
220 this._updateCloseButtons();
222 this._handleNewTab(tab);
224 } else if (tab.closing) {
225 gBrowser._endRemoveTab(tab);
228 let evt = new CustomEvent("TabAnimationEnd", { bubbles: true });
229 tab.dispatchEvent(evt);
233 // When the tabbar has an unified appearance with the titlebar
234 // and menubar, a double-click in it should have the same behavior
235 // as double-clicking the titlebar
236 if (TabsInTitlebar.enabled) {
240 if (event.button != 0 || event.originalTarget.localName != "scrollbox") {
244 if (!this._blockDblClick) {
248 event.preventDefault();
252 if (event.eventPhase == Event.CAPTURING_PHASE && event.button == 0) {
253 /* Catches extra clicks meant for the in-tab close button.
254 * Placed here to avoid leaking (a temporary handler added from the
255 * in-tab close button binding would close over the tab and leak it
256 * until the handler itself was removed). (bug 897751)
258 * The only sequence in which a second click event (i.e. dblclik)
259 * can be dispatched on an in-tab close button is when it is shown
260 * after the first click (i.e. the first click event was dispatched
261 * on the tab). This happens when we show the close button only on
262 * the active tab. (bug 352021)
263 * The only sequence in which a third click event can be dispatched
264 * on an in-tab close button is when the tab was opened with a
265 * double click on the tabbar. (bug 378344)
266 * In both cases, it is most likely that the close button area has
267 * been accidentally clicked, therefore we do not close the tab.
269 * We don't want to ignore processing of more than one click event,
270 * though, since the user might actually be repeatedly clicking to
271 * close many tabs at once.
273 let target = event.originalTarget;
274 if (target.classList.contains("tab-close-button")) {
275 // We preemptively set this to allow the closing-multiple-tabs-
277 if (this._blockDblClick) {
278 target._ignoredCloseButtonClicks = true;
279 } else if (event.detail > 1 && !target._ignoredCloseButtonClicks) {
280 target._ignoredCloseButtonClicks = true;
281 event.stopPropagation();
284 // Reset the "ignored click" flag
285 target._ignoredCloseButtonClicks = false;
289 /* Protects from close-tab-button errant doubleclick:
290 * Since we're removing the event target, if the user
291 * double-clicks the button, the dblclick event will be dispatched
292 * with the tabbar as its event target (and explicit/originalTarget),
293 * which treats that as a mouse gesture for opening a new tab.
294 * In this context, we're manually blocking the dblclick event.
296 if (this._blockDblClick) {
297 if (!("_clickedTabBarOnce" in this)) {
298 this._clickedTabBarOnce = true;
301 delete this._clickedTabBarOnce;
302 this._blockDblClick = false;
305 event.eventPhase == Event.BUBBLING_PHASE &&
308 let tab = event.target ? event.target.closest("tab") : null;
310 if (tab.multiselected) {
311 gBrowser.removeMultiSelectedTabs();
313 gBrowser.removeTab(tab, {
315 triggeringEvent: event,
319 event.originalTarget.closest("scrollbox") &&
320 !Services.prefs.getBoolPref(
321 "widget.gtk.titlebar-action-middle-click-enabled"
324 // Check whether the click
325 // was dispatched on the open space of it.
326 let visibleTabs = this._getVisibleTabs();
327 let lastTab = visibleTabs[visibleTabs.length - 1];
328 let winUtils = window.windowUtils;
330 winUtils.getBoundsWithoutFlushing(lastTab)[
331 RTL_UI ? "left" : "right"
334 (!RTL_UI && event.clientX > endOfTab) ||
335 (RTL_UI && event.clientX < endOfTab)
343 event.preventDefault();
344 event.stopPropagation();
349 let { altKey, shiftKey } = event;
350 let [accel, nonAccel] =
351 AppConstants.platform == "macosx"
352 ? [event.metaKey, event.ctrlKey]
353 : [event.ctrlKey, event.metaKey];
355 let keyComboForMove = accel && shiftKey && !altKey && !nonAccel;
356 let keyComboForFocus = accel && !shiftKey && !altKey && !nonAccel;
358 if (!keyComboForMove && !keyComboForFocus) {
362 // Don't check if the event was already consumed because tab navigation
363 // should work always for better user experience.
364 let { visibleTabs, selectedTab } = gBrowser;
365 let { arrowKeysShouldWrap } = this;
366 let focusedTabIndex = this.ariaFocusedIndex;
367 if (focusedTabIndex == -1) {
368 focusedTabIndex = visibleTabs.indexOf(selectedTab);
370 let lastFocusedTabIndex = focusedTabIndex;
371 switch (event.keyCode) {
372 case KeyEvent.DOM_VK_UP:
373 if (keyComboForMove) {
374 gBrowser.moveTabBackward();
379 case KeyEvent.DOM_VK_DOWN:
380 if (keyComboForMove) {
381 gBrowser.moveTabForward();
386 case KeyEvent.DOM_VK_RIGHT:
387 case KeyEvent.DOM_VK_LEFT:
388 if (keyComboForMove) {
389 gBrowser.moveTabOver(event);
391 (!RTL_UI && event.keyCode == KeyEvent.DOM_VK_RIGHT) ||
392 (RTL_UI && event.keyCode == KeyEvent.DOM_VK_LEFT)
399 case KeyEvent.DOM_VK_HOME:
400 if (keyComboForMove) {
401 gBrowser.moveTabToStart();
406 case KeyEvent.DOM_VK_END:
407 if (keyComboForMove) {
408 gBrowser.moveTabToEnd();
410 focusedTabIndex = visibleTabs.length - 1;
413 case KeyEvent.DOM_VK_SPACE:
414 if (visibleTabs[lastFocusedTabIndex].multiselected) {
415 gBrowser.removeFromMultiSelectedTabs(
416 visibleTabs[lastFocusedTabIndex]
419 gBrowser.addToMultiSelectedTabs(visibleTabs[lastFocusedTabIndex]);
423 // Consume the keydown event for the above keyboard
428 if (arrowKeysShouldWrap) {
429 if (focusedTabIndex >= visibleTabs.length) {
431 } else if (focusedTabIndex < 0) {
432 focusedTabIndex = visibleTabs.length - 1;
435 focusedTabIndex = Math.min(
436 visibleTabs.length - 1,
437 Math.max(0, focusedTabIndex)
441 if (keyComboForFocus && focusedTabIndex != lastFocusedTabIndex) {
442 this.ariaFocusedItem = visibleTabs[focusedTabIndex];
445 event.preventDefault();
448 on_dragstart(event) {
449 var tab = this._getDragTargetTab(event);
450 if (!tab || this._isCustomizing) {
454 this.startTabDrag(event, tab);
457 startTabDrag(event, tab, { fromTabList = false } = {}) {
458 let selectedTabs = gBrowser.selectedTabs;
459 let otherSelectedTabs = selectedTabs.filter(
460 selectedTab => selectedTab != tab
462 let dataTransferOrderedTabs = [tab].concat(otherSelectedTabs);
464 let dt = event.dataTransfer;
465 for (let i = 0; i < dataTransferOrderedTabs.length; i++) {
466 let dtTab = dataTransferOrderedTabs[i];
468 dt.mozSetDataAt(TAB_DROP_TYPE, dtTab, i);
469 let dtBrowser = dtTab.linkedBrowser;
471 // We must not set text/x-moz-url or text/plain data here,
472 // otherwise trying to detach the tab by dropping it on the desktop
473 // may result in an "internet shortcut"
475 "text/x-moz-text-internal",
476 dtBrowser.currentURI.spec,
481 // Set the cursor to an arrow during tab drags.
482 dt.mozCursor = "default";
484 // Set the tab as the source of the drag, which ensures we have a stable
485 // node to deliver the `dragend` event. See bug 1345473.
488 if (tab.multiselected) {
489 this._groupSelectedTabs(tab);
492 // Create a canvas to which we capture the current tab.
493 // Until canvas is HiDPI-aware (bug 780362), we need to scale the desired
494 // canvas size (in CSS pixels) to the window's backing resolution in order
495 // to get a full-resolution drag image for use on HiDPI displays.
496 let scale = window.devicePixelRatio;
497 let canvas = this._dndCanvas;
499 this._dndCanvas = canvas = document.createElementNS(
500 "http://www.w3.org/1999/xhtml",
503 canvas.style.width = "100%";
504 canvas.style.height = "100%";
505 canvas.mozOpaque = true;
508 canvas.width = 160 * scale;
509 canvas.height = 90 * scale;
511 let dragImageOffset = -16;
512 let browser = tab.linkedBrowser;
513 if (gMultiProcessBrowser) {
514 var context = canvas.getContext("2d");
515 context.fillStyle = "white";
516 context.fillRect(0, 0, canvas.width, canvas.height);
519 let platform = AppConstants.platform;
520 // On Windows and Mac we can update the drag image during a drag
521 // using updateDragImage. On Linux, we can use a panel.
522 if (platform == "win" || platform == "macosx") {
523 captureListener = function () {
524 dt.updateDragImage(canvas, dragImageOffset, dragImageOffset);
527 // Create a panel to use it in setDragImage
528 // which will tell xul to render a panel that follows
529 // the pointer while a dnd session is on.
530 if (!this._dndPanel) {
531 this._dndCanvas = canvas;
532 this._dndPanel = document.createXULElement("panel");
533 this._dndPanel.className = "dragfeedback-tab";
534 this._dndPanel.setAttribute("type", "drag");
535 let wrapper = document.createElementNS(
536 "http://www.w3.org/1999/xhtml",
539 wrapper.style.width = "160px";
540 wrapper.style.height = "90px";
541 wrapper.appendChild(canvas);
542 this._dndPanel.appendChild(wrapper);
543 document.documentElement.appendChild(this._dndPanel);
545 toDrag = this._dndPanel;
547 // PageThumb is async with e10s but that's fine
548 // since we can update the image during the dnd.
549 PageThumbs.captureToCanvas(browser, canvas)
550 .then(captureListener)
551 .catch(e => console.error(e));
553 // For the non e10s case we can just use PageThumbs
554 // sync, so let's use the canvas for setDragImage.
555 PageThumbs.captureToCanvas(browser, canvas).catch(e =>
558 dragImageOffset = dragImageOffset * scale;
560 dt.setDragImage(toDrag, dragImageOffset, dragImageOffset);
562 // _dragData.offsetX/Y give the coordinates that the mouse should be
563 // positioned relative to the corner of the new window created upon
564 // dragend such that the mouse appears to have the same position
565 // relative to the corner of the dragged tab.
566 function clientX(ele) {
567 return ele.getBoundingClientRect().left;
569 let tabOffsetX = clientX(tab) - clientX(this);
571 offsetX: event.screenX - window.screenX - tabOffsetX,
572 offsetY: event.screenY - window.screenY,
573 scrollX: this.arrowScrollbox.scrollbox.scrollLeft,
574 screenX: event.screenX,
575 movingTabs: (tab.multiselected ? gBrowser.selectedTabs : [tab]).filter(
576 t => t.pinned == tab.pinned
581 event.stopPropagation();
584 Services.telemetry.scalarAdd(
585 "browser.ui.interaction.all_tabs_panel_dragstart_tab_event_count",
592 var effects = this.getDropEffectForTabDrag(event);
594 var ind = this._tabDropIndicator;
595 if (effects == "" || effects == "none") {
599 event.preventDefault();
600 event.stopPropagation();
602 var arrowScrollbox = this.arrowScrollbox;
604 // autoscroll the tab strip if we drag over the scroll
605 // buttons, even if we aren't dragging a tab, but then
606 // return to avoid drawing the drop indicator
607 var pixelsToScroll = 0;
608 if (this.hasAttribute("overflow")) {
609 switch (event.originalTarget) {
610 case arrowScrollbox._scrollButtonUp:
611 pixelsToScroll = arrowScrollbox.scrollIncrement * -1;
613 case arrowScrollbox._scrollButtonDown:
614 pixelsToScroll = arrowScrollbox.scrollIncrement;
617 if (pixelsToScroll) {
618 arrowScrollbox.scrollByPixels(
619 (RTL_UI ? -1 : 1) * pixelsToScroll,
625 let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
627 (effects == "move" || effects == "copy") &&
628 this == draggedTab.container &&
629 !draggedTab._dragData.fromTabList
633 if (!this._isGroupTabsAnimationOver()) {
634 // Wait for grouping tabs animation to finish
637 this._finishGroupSelectedTabs(draggedTab);
639 if (effects == "move") {
640 this._animateTabMove(event);
645 this._finishAnimateTabMove();
647 if (effects == "link") {
648 let tab = this._getDragTargetTab(event, { ignoreTabSides: true });
650 if (!this._dragTime) {
651 this._dragTime = Date.now();
653 if (Date.now() >= this._dragTime + this._dragOverDelay) {
654 this.selectedItem = tab;
661 var rect = arrowScrollbox.getBoundingClientRect();
663 if (pixelsToScroll) {
664 // if we are scrolling, put the drop indicator at the edge
665 // so that it doesn't jump while scrolling
666 let scrollRect = arrowScrollbox.scrollClientRect;
667 let minMargin = scrollRect.left - rect.left;
668 let maxMargin = Math.min(
669 minMargin + scrollRect.width,
673 [minMargin, maxMargin] = [
674 this.clientWidth - maxMargin,
675 this.clientWidth - minMargin,
678 newMargin = pixelsToScroll > 0 ? maxMargin : minMargin;
680 let newIndex = this._getDropIndex(event);
681 let children = this.allTabs;
682 if (newIndex == children.length) {
683 let tabRect = this._getVisibleTabs().at(-1).getBoundingClientRect();
685 newMargin = rect.right - tabRect.left;
687 newMargin = tabRect.right - rect.left;
690 let tabRect = children[newIndex].getBoundingClientRect();
692 newMargin = rect.right - tabRect.right;
694 newMargin = tabRect.left - rect.left;
700 newMargin += ind.clientWidth / 2;
704 ind.style.transform = "translate(" + Math.round(newMargin) + "px)";
708 var dt = event.dataTransfer;
709 var dropEffect = dt.dropEffect;
712 if (dt.mozTypesAt(0)[0] == TAB_DROP_TYPE) {
714 draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
719 movingTabs = draggedTab._dragData.movingTabs;
720 draggedTab.container._finishGroupSelectedTabs(draggedTab);
723 this._tabDropIndicator.hidden = true;
724 event.stopPropagation();
725 if (draggedTab && dropEffect == "copy") {
726 // copy the dropped tab (wherever it's from)
727 let newIndex = this._getDropIndex(event);
729 for (let tab of movingTabs) {
730 let newTab = gBrowser.duplicateTab(tab);
731 gBrowser.moveTabTo(newTab, newIndex++);
732 if (tab == draggedTab) {
733 draggedTabCopy = newTab;
736 if (draggedTab.container != this || event.shiftKey) {
737 this.selectedItem = draggedTabCopy;
739 } else if (draggedTab && draggedTab.container == this) {
740 let oldTranslateX = Math.round(draggedTab._dragData.translateX);
741 let tabWidth = Math.round(draggedTab._dragData.tabWidth);
742 let translateOffset = oldTranslateX % tabWidth;
743 let newTranslateX = oldTranslateX - translateOffset;
744 if (oldTranslateX > 0 && translateOffset > tabWidth / 2) {
745 newTranslateX += tabWidth;
746 } else if (oldTranslateX < 0 && -translateOffset > tabWidth / 2) {
747 newTranslateX -= tabWidth;
751 if (draggedTab._dragData.fromTabList) {
752 dropIndex = this._getDropIndex(event);
755 "animDropIndex" in draggedTab._dragData &&
756 draggedTab._dragData.animDropIndex;
758 let incrementDropIndex = true;
759 if (dropIndex && dropIndex > movingTabs[0]._tPos) {
761 incrementDropIndex = false;
764 if (oldTranslateX && oldTranslateX != newTranslateX && !gReduceMotion) {
765 for (let tab of movingTabs) {
766 tab.toggleAttribute("tabdrop-samewindow", true);
767 tab.style.transform = "translateX(" + newTranslateX + "px)";
768 let postTransitionCleanup = () => {
769 tab.removeAttribute("tabdrop-samewindow");
771 this._finishAnimateTabMove();
772 if (dropIndex !== false) {
773 gBrowser.moveTabTo(tab, dropIndex);
774 if (incrementDropIndex) {
779 gBrowser.syncThrobberAnimations(tab);
782 postTransitionCleanup();
784 let onTransitionEnd = transitionendEvent => {
786 transitionendEvent.propertyName != "transform" ||
787 transitionendEvent.originalTarget != tab
791 tab.removeEventListener("transitionend", onTransitionEnd);
793 postTransitionCleanup();
795 tab.addEventListener("transitionend", onTransitionEnd);
799 this._finishAnimateTabMove();
800 if (dropIndex !== false) {
801 for (let tab of movingTabs) {
802 gBrowser.moveTabTo(tab, dropIndex);
803 if (incrementDropIndex) {
809 } else if (draggedTab) {
810 // Move the tabs. To avoid multiple tab-switches in the original window,
811 // the selected tab should be adopted last.
812 const dropIndex = this._getDropIndex(event);
813 let newIndex = dropIndex;
815 let indexForSelectedTab;
816 for (let i = 0; i < movingTabs.length; ++i) {
817 const tab = movingTabs[i];
820 indexForSelectedTab = newIndex;
822 const newTab = gBrowser.adoptTab(tab, newIndex, tab == draggedTab);
829 const newTab = gBrowser.adoptTab(
832 selectedTab == draggedTab
839 // Restore tab selection
840 gBrowser.addRangeToMultiSelectedTabs(
841 gBrowser.tabs[dropIndex],
842 gBrowser.tabs[newIndex - 1]
845 // Pass true to disallow dropping javascript: or data: urls
848 links = browserDragAndDrop.dropLinks(event, true);
851 if (!links || links.length === 0) {
855 let inBackground = Services.prefs.getBoolPref(
856 "browser.tabs.loadInBackground"
858 if (event.shiftKey) {
859 inBackground = !inBackground;
862 let targetTab = this._getDragTargetTab(event, { ignoreTabSides: true });
863 let userContextId = this.selectedItem.getAttribute("usercontextid");
864 let replace = !!targetTab;
865 let newIndex = this._getDropIndex(event);
866 let urls = links.map(link => link.url);
867 let csp = browserDragAndDrop.getCsp(event);
868 let triggeringPrincipal =
869 browserDragAndDrop.getTriggeringPrincipal(event);
874 Services.prefs.getIntPref("browser.tabs.maxOpenBeforeWarn")
876 // Sync dialog cannot be used inside drop event handler.
877 let answer = await OpenInTabsUtils.promiseConfirmOpenInTabs(
886 gBrowser.loadTabs(urls, {
889 allowThirdPartyFixup: true,
900 delete draggedTab._dragData;
905 var dt = event.dataTransfer;
906 var draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
908 // Prevent this code from running if a tabdrop animation is
909 // running since calling _finishAnimateTabMove would clear
910 // any CSS transition that is running.
911 if (draggedTab.hasAttribute("tabdrop-samewindow")) {
915 this._finishGroupSelectedTabs(draggedTab);
916 this._finishAnimateTabMove();
919 dt.mozUserCancelled ||
920 dt.dropEffect != "none" ||
923 delete draggedTab._dragData;
927 // Check if tab detaching is enabled
928 if (!Services.prefs.getBoolPref("browser.tabs.allowTabDetach")) {
932 // Disable detach within the browser toolbox
933 var eX = event.screenX;
934 var eY = event.screenY;
935 var wX = window.screenX;
936 // check if the drop point is horizontally within the window
937 if (eX > wX && eX < wX + window.outerWidth) {
938 // also avoid detaching if the the tab was dropped too close to
939 // the tabbar (half a tab)
940 let rect = window.windowUtils.getBoundsWithoutFlushing(
943 let detachTabThresholdY = window.screenY + rect.top + 1.5 * rect.height;
944 if (eY < detachTabThresholdY && eY > window.screenY) {
949 // screen.availLeft et. al. only check the screen that this window is on,
950 // but we want to look at the screen the tab is being dropped onto.
951 var screen = event.screen;
956 // Get available rect in desktop pixels.
957 screen.GetAvailRectDisplayPix(availX, availY, availWidth, availHeight);
958 availX = availX.value;
959 availY = availY.value;
960 availWidth = availWidth.value;
961 availHeight = availHeight.value;
963 // Compute the final window size in desktop pixels ensuring that the new
964 // window entirely fits within `screen`.
965 let ourCssToDesktopScale =
966 window.devicePixelRatio / window.desktopToDeviceScale;
967 let screenCssToDesktopScale =
968 screen.defaultCSSScaleFactor / screen.contentsScaleFactor;
970 // NOTE(emilio): Multiplying the sizes here for screenCssToDesktopScale
971 // means that we'll try to create a window that has the same amount of CSS
972 // pixels than our current window, not the same amount of device pixels.
973 // There are pros and cons of both conversions, though this matches the
974 // pre-existing intended behavior.
975 var winWidth = Math.min(
976 window.outerWidth * screenCssToDesktopScale,
979 var winHeight = Math.min(
980 window.outerHeight * screenCssToDesktopScale,
984 // This is slightly tricky: _dragData.offsetX/Y is an offset in CSS
985 // pixels. Since we're doing the sizing above based on those, we also need
986 // to apply the offset with pixels relative to the screen's scale rather
990 eX * ourCssToDesktopScale -
991 draggedTab._dragData.offsetX * screenCssToDesktopScale,
994 availX + availWidth - winWidth
998 eY * ourCssToDesktopScale -
999 draggedTab._dragData.offsetY * screenCssToDesktopScale,
1002 availY + availHeight - winHeight
1005 // Convert back left and top to our CSS pixel space.
1006 left /= ourCssToDesktopScale;
1007 top /= ourCssToDesktopScale;
1009 delete draggedTab._dragData;
1011 if (gBrowser.tabs.length == 1) {
1012 // resize _before_ move to ensure the window fits the new screen. if
1013 // the window is too large for its screen, the window manager may do
1014 // automatic repositioning.
1016 // Since we're resizing before moving to our new screen, we need to use
1017 // sizes relative to the current screen. If we moved, then resized, then
1018 // we could avoid this special-case and share this with the else branch
1020 winWidth /= ourCssToDesktopScale;
1021 winHeight /= ourCssToDesktopScale;
1023 window.resizeTo(winWidth, winHeight);
1024 window.moveTo(left, top);
1027 // We're opening a new window in a new screen, so make sure to use sizes
1028 // relative to the new screen.
1029 winWidth /= screenCssToDesktopScale;
1030 winHeight /= screenCssToDesktopScale;
1032 let props = { screenX: left, screenY: top, suppressanimation: 1 };
1033 if (AppConstants.platform != "win") {
1034 props.outerWidth = winWidth;
1035 props.outerHeight = winHeight;
1037 gBrowser.replaceTabsWithWindow(draggedTab, props);
1039 event.stopPropagation();
1042 on_dragleave(event) {
1045 // This does not work at all (see bug 458613)
1046 var target = event.relatedTarget;
1047 while (target && target != this) {
1048 target = target.parentNode;
1054 this._tabDropIndicator.hidden = true;
1055 event.stopPropagation();
1060 Services.prefs.getBoolPref("toolkit.tabbox.switchByScrolling", false)
1062 event.stopImmediatePropagation();
1066 get emptyTabTitle() {
1067 // Normal tab title is used also in the permanent private browsing mode.
1069 PrivateBrowsingUtils.isWindowPrivate(window) &&
1070 !Services.prefs.getBoolPref("browser.privatebrowsing.autostart")
1071 ? "tabbrowser-empty-private-tab-title"
1072 : "tabbrowser-empty-tab-title";
1073 return gBrowser.tabLocalization.formatValueSync(l10nId);
1077 return document.getElementById("tabbrowser-tabbox");
1080 get newTabButton() {
1081 return this.querySelector("#tabs-newtab-button");
1084 // Accessor for tabs. arrowScrollbox has a container for non-tab elements
1085 // at the end, everything else is <tab>s.
1087 if (this._allTabs) {
1088 return this._allTabs;
1090 let children = Array.from(this.arrowScrollbox.children);
1092 this._allTabs = children;
1097 if (!this._visibleTabs) {
1098 this._visibleTabs = Array.prototype.filter.call(
1100 tab => !tab.hidden && !tab.closing
1103 return this._visibleTabs;
1106 _invalidateCachedTabs() {
1107 this._allTabs = null;
1108 this._visibleTabs = null;
1111 _invalidateCachedVisibleTabs() {
1112 this._visibleTabs = null;
1116 return this.insertBefore(tab, null);
1119 insertBefore(tab, node) {
1120 if (!this.arrowScrollbox) {
1121 throw new Error("Shouldn't call this without arrowscrollbox");
1124 let { arrowScrollbox } = this;
1126 // We have a container for non-tab elements at the end of the scrollbox.
1127 node = arrowScrollbox.lastChild;
1129 return arrowScrollbox.insertBefore(tab, node);
1132 set _tabMinWidth(val) {
1133 this.style.setProperty("--tab-min-width", val + "px");
1136 get _isCustomizing() {
1137 return document.documentElement.getAttribute("customizing") == "true";
1140 // This overrides the TabsBase _selectNewTab method so that we can
1141 // potentially interrupt keyboard tab switching when sharing the
1142 // window or screen.
1143 _selectNewTab(aNewTab, aFallbackDir, aWrap) {
1144 if (!gSharedTabWarning.willShowSharedTabWarning(aNewTab)) {
1145 super._selectNewTab(aNewTab, aFallbackDir, aWrap);
1149 _initializeArrowScrollbox() {
1150 let arrowScrollbox = this.arrowScrollbox;
1151 arrowScrollbox.shadowRoot.addEventListener(
1154 // Ignore underflow events:
1155 // - from nested scrollable elements
1156 // - for vertical orientation
1157 // - corresponding to an overflow event that we ignored
1159 event.originalTarget != arrowScrollbox.scrollbox ||
1160 event.detail == 0 ||
1161 !this.hasAttribute("overflow")
1166 this.removeAttribute("overflow");
1168 if (this._lastTabClosedByMouse) {
1169 this._expandSpacerBy(this._scrollButtonWidth);
1172 for (let tab of gBrowser._removingTabs) {
1173 gBrowser.removeTab(tab);
1176 this._positionPinnedTabs();
1177 this._updateCloseButtons();
1182 arrowScrollbox.shadowRoot.addEventListener("overflow", event => {
1183 // Ignore overflow events:
1184 // - from nested scrollable elements
1185 // - for vertical orientation
1187 event.originalTarget != arrowScrollbox.scrollbox ||
1193 this.toggleAttribute("overflow", true);
1194 this._positionPinnedTabs();
1195 this._updateCloseButtons();
1196 this._handleTabSelect(true);
1199 // Override arrowscrollbox.js method, since our scrollbox's children are
1200 // inherited from the scrollbox binding parent (this).
1201 arrowScrollbox._getScrollableElements = () => {
1202 return this.allTabs.filter(arrowScrollbox._canScrollToElement);
1204 arrowScrollbox._canScrollToElement = tab => {
1205 return !tab._pinnedUnscrollable && !tab.hidden;
1209 observe(aSubject, aTopic) {
1211 case "nsPref:changed":
1212 // This is has to deal with changes in
1213 // privacy.userContext.enabled and
1214 // privacy.userContext.newTabContainerOnLeftClick.enabled.
1215 let containersEnabled =
1216 Services.prefs.getBoolPref("privacy.userContext.enabled") &&
1217 !PrivateBrowsingUtils.isWindowPrivate(window);
1219 // This pref won't change so often, so just recreate the menu.
1220 const newTabLeftClickOpensContainersMenu = Services.prefs.getBoolPref(
1221 "privacy.userContext.newTabContainerOnLeftClick.enabled"
1224 // There are separate "new tab" buttons for when the tab strip
1225 // is overflowed and when it is not. Attach the long click
1226 // popup to both of them.
1227 const newTab = document.getElementById("new-tab-button");
1228 const newTab2 = this.newTabButton;
1230 for (let parent of [newTab, newTab2]) {
1235 parent.removeAttribute("type");
1236 if (parent.menupopup) {
1237 parent.menupopup.remove();
1240 if (containersEnabled) {
1241 parent.setAttribute("context", "new-tab-button-popup");
1243 let popup = document
1244 .getElementById("new-tab-button-popup")
1246 popup.removeAttribute("id");
1247 popup.className = "new-tab-popup";
1248 popup.setAttribute("position", "after_end");
1249 parent.prepend(popup);
1250 parent.setAttribute("type", "menu");
1251 // Update tooltip text
1252 nodeToTooltipMap[parent.id] = newTabLeftClickOpensContainersMenu
1253 ? "newTabAlwaysContainer.tooltip"
1254 : "newTabContainer.tooltip";
1256 nodeToTooltipMap[parent.id] = "newTabButton.tooltip";
1257 parent.removeAttribute("context", "new-tab-button-popup");
1259 // evict from tooltip cache
1260 gDynamicTooltipCache.delete(parent.id);
1262 // If containers and press-hold container menu are both used,
1263 // add to gClickAndHoldListenersOnElement; otherwise, remove.
1264 if (containersEnabled && !newTabLeftClickOpensContainersMenu) {
1265 gClickAndHoldListenersOnElement.add(parent);
1267 gClickAndHoldListenersOnElement.remove(parent);
1275 _updateCloseButtons() {
1276 // If we're overflowing, tabs are at their minimum widths.
1277 if (this.hasAttribute("overflow")) {
1278 this.setAttribute("closebuttons", "activetab");
1282 if (this._closeButtonsUpdatePending) {
1285 this._closeButtonsUpdatePending = true;
1287 // Wait until after the next paint to get current layout data from
1288 // getBoundsWithoutFlushing.
1289 window.requestAnimationFrame(() => {
1290 window.requestAnimationFrame(() => {
1291 this._closeButtonsUpdatePending = false;
1293 // The scrollbox may have started overflowing since we checked
1294 // overflow earlier, so check again.
1295 if (this.hasAttribute("overflow")) {
1296 this.setAttribute("closebuttons", "activetab");
1300 // Check if tab widths are below the threshold where we want to
1301 // remove close buttons from background tabs so that people don't
1302 // accidentally close tabs by selecting them.
1304 return window.windowUtils.getBoundsWithoutFlushing(ele);
1306 let tab = this._getVisibleTabs()[gBrowser._numPinnedTabs];
1307 if (tab && rect(tab).width <= this._tabClipWidth) {
1308 this.setAttribute("closebuttons", "activetab");
1310 this.removeAttribute("closebuttons");
1316 _updateHiddenTabsStatus() {
1317 this.toggleAttribute(
1319 gBrowser.visibleTabs.length < gBrowser.tabs.length
1323 _handleTabSelect(aInstant) {
1324 let selectedTab = this.selectedItem;
1325 if (this.hasAttribute("overflow")) {
1326 this.arrowScrollbox.ensureElementIsVisible(selectedTab, aInstant);
1329 selectedTab._notselectedsinceload = false;
1333 * Try to keep the active tab's close button under the mouse cursor
1335 _lockTabSizing(aTab, aTabWidth) {
1336 let tabs = this._getVisibleTabs();
1341 var isEndTab = aTab._tPos > tabs[tabs.length - 1]._tPos;
1343 if (!this._tabDefaultMaxWidth) {
1344 this._tabDefaultMaxWidth = parseFloat(
1345 window.getComputedStyle(aTab).maxWidth
1348 this._lastTabClosedByMouse = true;
1349 this._scrollButtonWidth = window.windowUtils.getBoundsWithoutFlushing(
1350 this.arrowScrollbox._scrollButtonDown
1353 if (this.hasAttribute("overflow")) {
1354 // Don't need to do anything if we're in overflow mode and aren't scrolled
1355 // all the way to the right, or if we're closing the last tab.
1356 if (isEndTab || !this.arrowScrollbox._scrollButtonDown.disabled) {
1359 // If the tab has an owner that will become the active tab, the owner will
1360 // be to the left of it, so we actually want the left tab to slide over.
1361 // This can't be done as easily in non-overflow mode, so we don't bother.
1365 this._expandSpacerBy(aTabWidth);
1367 // non-overflow mode
1368 // Locking is neither in effect nor needed, so let tabs expand normally.
1369 if (isEndTab && !this._hasTabTempMaxWidth) {
1372 let numPinned = gBrowser._numPinnedTabs;
1373 // Force tabs to stay the same width, unless we're closing the last tab,
1374 // which case we need to let them expand just enough so that the overall
1375 // tabbar width is the same.
1377 let numNormalTabs = tabs.length - numPinned;
1378 aTabWidth = (aTabWidth * (numNormalTabs + 1)) / numNormalTabs;
1379 if (aTabWidth > this._tabDefaultMaxWidth) {
1380 aTabWidth = this._tabDefaultMaxWidth;
1384 let tabsToReset = [];
1385 for (let i = numPinned; i < tabs.length; i++) {
1387 tab.style.setProperty("max-width", aTabWidth, "important");
1389 // keep tabs the same width
1390 tab.style.transition = "none";
1391 tabsToReset.push(tab);
1395 if (tabsToReset.length) {
1397 .promiseDocumentFlushed(() => {})
1399 window.requestAnimationFrame(() => {
1400 for (let tab of tabsToReset) {
1401 tab.style.transition = "";
1407 this._hasTabTempMaxWidth = true;
1408 gBrowser.addEventListener("mousemove", this);
1409 window.addEventListener("mouseout", this);
1413 _expandSpacerBy(pixels) {
1414 let spacer = this._closingTabsSpacer;
1415 spacer.style.width = parseFloat(spacer.style.width) + pixels + "px";
1416 this.toggleAttribute("using-closing-tabs-spacer", true);
1417 gBrowser.addEventListener("mousemove", this);
1418 window.addEventListener("mouseout", this);
1421 _unlockTabSizing() {
1422 gBrowser.removeEventListener("mousemove", this);
1423 window.removeEventListener("mouseout", this);
1425 if (this._hasTabTempMaxWidth) {
1426 this._hasTabTempMaxWidth = false;
1427 let tabs = this._getVisibleTabs();
1428 for (let i = 0; i < tabs.length; i++) {
1429 tabs[i].style.maxWidth = "";
1433 if (this.hasAttribute("using-closing-tabs-spacer")) {
1434 this.removeAttribute("using-closing-tabs-spacer");
1435 this._closingTabsSpacer.style.width = 0;
1439 uiDensityChanged() {
1440 this._positionPinnedTabs();
1441 this._updateCloseButtons();
1442 this._handleTabSelect(true);
1445 _positionPinnedTabs() {
1446 let tabs = this._getVisibleTabs();
1447 let numPinned = gBrowser._numPinnedTabs;
1449 this.hasAttribute("overflow") &&
1450 tabs.length > numPinned &&
1453 this.toggleAttribute("haspinnedtabs", !!numPinned);
1454 this.toggleAttribute("positionpinnedtabs", doPosition);
1457 let layoutData = this._pinnedTabsLayoutCache;
1458 let uiDensity = document.documentElement.getAttribute("uidensity");
1459 if (!layoutData || layoutData.uiDensity != uiDensity) {
1460 let arrowScrollbox = this.arrowScrollbox;
1461 layoutData = this._pinnedTabsLayoutCache = {
1463 pinnedTabWidth: tabs[0].getBoundingClientRect().width,
1465 arrowScrollbox.scrollbox.getBoundingClientRect().left -
1466 arrowScrollbox.getBoundingClientRect().left +
1468 getComputedStyle(arrowScrollbox.scrollbox).paddingInlineStart
1474 for (let i = numPinned - 1; i >= 0; i--) {
1476 width += layoutData.pinnedTabWidth;
1477 tab.style.setProperty(
1478 "margin-inline-start",
1479 -(width + layoutData.scrollStartOffset) + "px",
1482 tab._pinnedUnscrollable = true;
1484 this.style.setProperty(
1485 "--tab-overflow-pinned-tabs-width",
1489 for (let i = 0; i < numPinned; i++) {
1491 tab.style.marginInlineStart = "";
1492 tab._pinnedUnscrollable = false;
1495 this.style.removeProperty("--tab-overflow-pinned-tabs-width");
1498 if (this._lastNumPinned != numPinned) {
1499 this._lastNumPinned = numPinned;
1500 this._handleTabSelect(true);
1504 _animateTabMove(event) {
1505 let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
1506 let movingTabs = draggedTab._dragData.movingTabs;
1508 if (!this.hasAttribute("movingtab")) {
1509 this.toggleAttribute("movingtab", true);
1510 gNavToolbox.toggleAttribute("movingtab", true);
1511 if (!draggedTab.multiselected) {
1512 this.selectedItem = draggedTab;
1516 if (!("animLastScreenX" in draggedTab._dragData)) {
1517 draggedTab._dragData.animLastScreenX = draggedTab._dragData.screenX;
1520 let screenX = event.screenX;
1521 if (screenX == draggedTab._dragData.animLastScreenX) {
1525 // Direction of the mouse movement.
1526 let ltrMove = screenX > draggedTab._dragData.animLastScreenX;
1528 draggedTab._dragData.animLastScreenX = screenX;
1530 let pinned = draggedTab.pinned;
1531 let numPinned = gBrowser._numPinnedTabs;
1532 let tabs = this._getVisibleTabs().slice(
1533 pinned ? 0 : numPinned,
1534 pinned ? numPinned : undefined
1539 // Copy moving tabs array to avoid infinite reversing.
1540 movingTabs = [...movingTabs].reverse();
1542 let tabWidth = draggedTab.getBoundingClientRect().width;
1543 let shiftWidth = tabWidth * movingTabs.length;
1544 draggedTab._dragData.tabWidth = tabWidth;
1546 // Move the dragged tab based on the mouse position.
1548 let leftTab = tabs[0];
1549 let rightTab = tabs[tabs.length - 1];
1550 let rightMovingTabScreenX = movingTabs[movingTabs.length - 1].screenX;
1551 let leftMovingTabScreenX = movingTabs[0].screenX;
1552 let translateX = screenX - draggedTab._dragData.screenX;
1555 this.arrowScrollbox.scrollbox.scrollLeft -
1556 draggedTab._dragData.scrollX;
1558 let leftBound = leftTab.screenX - leftMovingTabScreenX;
1561 rightTab.getBoundingClientRect().width -
1562 (rightMovingTabScreenX + tabWidth);
1563 translateX = Math.min(Math.max(translateX, leftBound), rightBound);
1565 for (let tab of movingTabs) {
1566 tab.style.transform = "translateX(" + translateX + "px)";
1569 draggedTab._dragData.translateX = translateX;
1571 // Determine what tab we're dragging over.
1572 // * Single tab dragging: Point of reference is the center of the dragged tab. If that
1573 // point touches a background tab, the dragged tab would take that
1574 // tab's position when dropped.
1575 // * Multiple tabs dragging: All dragged tabs are one "giant" tab with two
1576 // points of reference (center of tabs on the extremities). When
1577 // mouse is moving from left to right, the right reference gets activated,
1578 // otherwise the left reference will be used. Everything else works the same
1579 // as single tab dragging.
1580 // * We're doing a binary search in order to reduce the amount of
1581 // tabs we need to check.
1583 tabs = tabs.filter(t => !movingTabs.includes(t) || t == draggedTab);
1584 let leftTabCenter = leftMovingTabScreenX + translateX + tabWidth / 2;
1585 let rightTabCenter = rightMovingTabScreenX + translateX + tabWidth / 2;
1586 let tabCenter = ltrMove ? rightTabCenter : leftTabCenter;
1589 "animDropIndex" in draggedTab._dragData
1590 ? draggedTab._dragData.animDropIndex
1591 : movingTabs[0]._tPos;
1593 let high = tabs.length - 1;
1594 while (low <= high) {
1595 let mid = Math.floor((low + high) / 2);
1596 if (tabs[mid] == draggedTab && ++mid > high) {
1599 screenX = tabs[mid].screenX + getTabShift(tabs[mid], oldIndex);
1600 if (screenX > tabCenter) {
1603 screenX + tabs[mid].getBoundingClientRect().width <
1608 newIndex = tabs[mid]._tPos;
1612 if (newIndex >= oldIndex) {
1615 if (newIndex < 0 || newIndex == oldIndex) {
1618 draggedTab._dragData.animDropIndex = newIndex;
1620 // Shift background tabs to leave a gap where the dragged tab
1621 // would currently be dropped.
1623 for (let tab of tabs) {
1624 if (tab != draggedTab) {
1625 let shift = getTabShift(tab, newIndex);
1626 tab.style.transform = shift ? "translateX(" + shift + "px)" : "";
1630 function getTabShift(tab, dropIndex) {
1631 if (tab._tPos < draggedTab._tPos && tab._tPos >= dropIndex) {
1632 return RTL_UI ? -shiftWidth : shiftWidth;
1634 if (tab._tPos > draggedTab._tPos && tab._tPos < dropIndex) {
1635 return RTL_UI ? shiftWidth : -shiftWidth;
1641 _finishAnimateTabMove() {
1642 if (!this.hasAttribute("movingtab")) {
1646 for (let tab of this._getVisibleTabs()) {
1647 tab.style.transform = "";
1650 this.removeAttribute("movingtab");
1651 gNavToolbox.removeAttribute("movingtab");
1653 this._handleTabSelect();
1657 * Regroup all selected tabs around the
1660 _groupSelectedTabs(tab) {
1661 let draggedTabPos = tab._tPos;
1662 let selectedTabs = gBrowser.selectedTabs;
1663 let animate = !gReduceMotion;
1665 tab.groupingTabsData = {
1669 // Animate left selected tabs
1671 let insertAtPos = draggedTabPos - 1;
1672 for (let i = selectedTabs.indexOf(tab) - 1; i > -1; i--) {
1673 let movingTab = selectedTabs[i];
1674 insertAtPos = newIndex(movingTab, insertAtPos);
1677 movingTab.groupingTabsData = {};
1678 addAnimationData(movingTab, insertAtPos, "left");
1680 gBrowser.moveTabTo(movingTab, insertAtPos);
1685 // Animate right selected tabs
1687 insertAtPos = draggedTabPos + 1;
1689 let i = selectedTabs.indexOf(tab) + 1;
1690 i < selectedTabs.length;
1693 let movingTab = selectedTabs[i];
1694 insertAtPos = newIndex(movingTab, insertAtPos);
1697 movingTab.groupingTabsData = {};
1698 addAnimationData(movingTab, insertAtPos, "right");
1700 gBrowser.moveTabTo(movingTab, insertAtPos);
1705 // Slide the relevant tabs to their new position.
1706 for (let t of this._getVisibleTabs()) {
1707 if (t.groupingTabsData && t.groupingTabsData.translateX) {
1708 let translateX = (RTL_UI ? -1 : 1) * t.groupingTabsData.translateX;
1709 t.style.transform = "translateX(" + translateX + "px)";
1713 function newIndex(aTab, index) {
1714 // Don't allow mixing pinned and unpinned tabs.
1716 return Math.min(index, gBrowser._numPinnedTabs - 1);
1718 return Math.max(index, gBrowser._numPinnedTabs);
1721 function addAnimationData(movingTab, movingTabNewIndex, side) {
1722 let movingTabOldIndex = movingTab._tPos;
1724 if (movingTabOldIndex == movingTabNewIndex) {
1725 // movingTab is already at the right position
1726 // and thus don't need to be animated.
1730 let movingTabWidth = movingTab.getBoundingClientRect().width;
1731 let shift = (movingTabNewIndex - movingTabOldIndex) * movingTabWidth;
1733 movingTab.groupingTabsData.animate = true;
1734 movingTab.toggleAttribute("tab-grouping", true);
1736 movingTab.groupingTabsData.translateX = shift;
1738 let postTransitionCleanup = () => {
1739 movingTab.groupingTabsData.newIndex = movingTabNewIndex;
1740 movingTab.groupingTabsData.animate = false;
1742 if (gReduceMotion) {
1743 postTransitionCleanup();
1745 let onTransitionEnd = transitionendEvent => {
1747 transitionendEvent.propertyName != "transform" ||
1748 transitionendEvent.originalTarget != movingTab
1752 movingTab.removeEventListener("transitionend", onTransitionEnd);
1753 postTransitionCleanup();
1756 movingTab.addEventListener("transitionend", onTransitionEnd);
1759 // Add animation data for tabs between movingTab (selected
1760 // tab moving towards the dragged tab) and draggedTab.
1761 // Those tabs in the middle should move in
1762 // the opposite direction of movingTab.
1764 let lowerIndex = Math.min(movingTabOldIndex, draggedTabPos);
1765 let higherIndex = Math.max(movingTabOldIndex, draggedTabPos);
1767 for (let i = lowerIndex + 1; i < higherIndex; i++) {
1768 let middleTab = gBrowser.visibleTabs[i];
1770 if (middleTab.pinned != movingTab.pinned) {
1771 // Don't mix pinned and unpinned tabs
1775 if (middleTab.multiselected) {
1776 // Skip because this selected tab should
1777 // be shifted towards the dragged Tab.
1782 !middleTab.groupingTabsData ||
1783 !middleTab.groupingTabsData.translateX
1785 middleTab.groupingTabsData = { translateX: 0 };
1787 if (side == "left") {
1788 middleTab.groupingTabsData.translateX -= movingTabWidth;
1790 middleTab.groupingTabsData.translateX += movingTabWidth;
1793 middleTab.toggleAttribute("tab-grouping", true);
1798 _finishGroupSelectedTabs(tab) {
1799 if (!tab.groupingTabsData || tab.groupingTabsData.finished) {
1803 tab.groupingTabsData.finished = true;
1805 let selectedTabs = gBrowser.selectedTabs;
1806 let tabIndex = selectedTabs.indexOf(tab);
1809 for (let i = tabIndex - 1; i > -1; i--) {
1810 let movingTab = selectedTabs[i];
1811 if (movingTab.groupingTabsData.newIndex) {
1812 gBrowser.moveTabTo(movingTab, movingTab.groupingTabsData.newIndex);
1816 // Moving right tabs
1817 for (let i = tabIndex + 1; i < selectedTabs.length; i++) {
1818 let movingTab = selectedTabs[i];
1819 if (movingTab.groupingTabsData.newIndex) {
1820 gBrowser.moveTabTo(movingTab, movingTab.groupingTabsData.newIndex);
1824 for (let t of this._getVisibleTabs()) {
1825 t.style.transform = "";
1826 t.removeAttribute("tab-grouping");
1827 delete t.groupingTabsData;
1831 _isGroupTabsAnimationOver() {
1832 for (let tab of gBrowser.selectedTabs) {
1833 if (tab.groupingTabsData && tab.groupingTabsData.animate) {
1840 handleEvent(aEvent) {
1841 switch (aEvent.type) {
1843 // If the "related target" (the node to which the pointer went) is not
1844 // a child of the current document, the mouse just left the window.
1845 let relatedTarget = aEvent.relatedTarget;
1846 if (relatedTarget && relatedTarget.ownerDocument == document) {
1851 if (document.getElementById("tabContextMenu").state != "open") {
1852 this._unlockTabSizing();
1856 if (this._showCardPreviews) {
1857 let preview = document.getElementById("tabbrowser-tab-preview");
1858 preview.resetDelay();
1862 let methodName = `on_${aEvent.type}`;
1863 if (methodName in this) {
1864 this[methodName](aEvent);
1866 throw new Error(`Unexpected event ${aEvent.type}`);
1871 _notifyBackgroundTab(aTab) {
1872 if (aTab.pinned || aTab.hidden || !this.hasAttribute("overflow")) {
1876 this._lastTabToScrollIntoView = aTab;
1877 if (!this._backgroundTabScrollPromise) {
1878 this._backgroundTabScrollPromise = window
1879 .promiseDocumentFlushed(() => {
1881 this._lastTabToScrollIntoView.getBoundingClientRect();
1882 let selectedTab = this.selectedItem;
1883 if (selectedTab.pinned) {
1886 selectedTab = selectedTab.getBoundingClientRect();
1888 left: selectedTab.left,
1889 right: selectedTab.right,
1893 this._lastTabToScrollIntoView,
1894 this.arrowScrollbox.scrollClientRect,
1895 { left: lastTabRect.left, right: lastTabRect.right },
1899 .then(([tabToScrollIntoView, scrollRect, tabRect, selectedRect]) => {
1900 // First off, remove the promise so we can re-enter if necessary.
1901 delete this._backgroundTabScrollPromise;
1902 // Then, if the layout info isn't for the last-scrolled-to-tab, re-run
1903 // the code above to get layout info for *that* tab, and don't do
1904 // anything here, as we really just want to run this for the last-opened tab.
1905 if (this._lastTabToScrollIntoView != tabToScrollIntoView) {
1906 this._notifyBackgroundTab(this._lastTabToScrollIntoView);
1909 delete this._lastTabToScrollIntoView;
1910 // Is the new tab already completely visible?
1912 scrollRect.left <= tabRect.left &&
1913 tabRect.right <= scrollRect.right
1918 if (this.arrowScrollbox.smoothScroll) {
1919 // Can we make both the new tab and the selected tab completely visible?
1923 tabRect.right - selectedRect.left,
1924 selectedRect.right - tabRect.left
1925 ) <= scrollRect.width
1927 this.arrowScrollbox.ensureElementIsVisible(tabToScrollIntoView);
1931 this.arrowScrollbox.scrollByPixels(
1933 ? selectedRect.right - scrollRect.right
1934 : selectedRect.left - scrollRect.left
1938 if (!this._animateElement.hasAttribute("highlight")) {
1939 this._animateElement.toggleAttribute("highlight", true);
1942 ele.removeAttribute("highlight");
1945 this._animateElement
1953 * Returns the tab where an event happened, or null if it didn't occur on a tab.
1955 * @param {Event} event
1956 * The event for which we want to know on which tab it happened.
1957 * @param {object} options
1958 * @param {boolean} options.ignoreTabSides
1959 * If set to true: events will only be associated with a tab if they happened
1960 * on its central part (from 25% to 75%); if they happened on the left or right
1961 * sides of the tab, the method will return null.
1963 _getDragTargetTab(event, { ignoreTabSides = false } = {}) {
1964 let { target } = event;
1965 if (target.nodeType != Node.ELEMENT_NODE) {
1966 target = target.parentElement;
1968 let tab = target?.closest("tab");
1969 if (tab && ignoreTabSides) {
1970 let { width } = tab.getBoundingClientRect();
1972 event.screenX < tab.screenX + width * 0.25 ||
1973 event.screenX > tab.screenX + width * 0.75
1981 _getDropIndex(event) {
1982 let tab = this._getDragTargetTab(event);
1984 return this.allTabs.length;
1986 let middle = tab.screenX + tab.getBoundingClientRect().width / 2;
1987 let isBeforeMiddle = RTL_UI
1988 ? event.screenX > middle
1989 : event.screenX < middle;
1990 return tab._tPos + (isBeforeMiddle ? 0 : 1);
1993 getDropEffectForTabDrag(event) {
1994 var dt = event.dataTransfer;
1996 let isMovingTabs = dt.mozItemCount > 0;
1997 for (let i = 0; i < dt.mozItemCount; i++) {
1998 // tabs are always added as the first type
1999 let types = dt.mozTypesAt(0);
2000 if (types[0] != TAB_DROP_TYPE) {
2001 isMovingTabs = false;
2007 let sourceNode = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
2009 XULElement.isInstance(sourceNode) &&
2010 sourceNode.localName == "tab" &&
2011 sourceNode.ownerGlobal.isChromeWindow &&
2012 sourceNode.ownerDocument.documentElement.getAttribute("windowtype") ==
2013 "navigator:browser" &&
2014 sourceNode.ownerGlobal.gBrowser.tabContainer == sourceNode.container
2016 // Do not allow transfering a private tab to a non-private window
2019 PrivateBrowsingUtils.isWindowPrivate(window) !=
2020 PrivateBrowsingUtils.isWindowPrivate(sourceNode.ownerGlobal)
2026 window.gMultiProcessBrowser !=
2027 sourceNode.ownerGlobal.gMultiProcessBrowser
2033 window.gFissionBrowser != sourceNode.ownerGlobal.gFissionBrowser
2038 return dt.dropEffect == "copy" ? "copy" : "move";
2042 if (browserDragAndDrop.canDropLink(event)) {
2048 _handleNewTab(tab) {
2049 if (tab.container != this) {
2052 tab._fullyOpen = true;
2053 gBrowser.tabAnimationsInProgress--;
2055 this._updateCloseButtons();
2057 if (tab.hasAttribute("selected")) {
2058 this._handleTabSelect();
2059 } else if (!tab.hasAttribute("skipbackgroundnotify")) {
2060 this._notifyBackgroundTab(tab);
2063 // XXXmano: this is a temporary workaround for bug 345399
2064 // We need to manually update the scroll buttons disabled state
2065 // if a tab was inserted to the overflow area or removed from it
2066 // without any scrolling and when the tabbar has already
2068 this.arrowScrollbox._updateScrollButtonsDisabledState();
2070 // If this browser isn't lazy (indicating it's probably created by
2071 // session restore), preload the next about:newtab if we don't
2072 // already have a preloaded browser.
2073 if (tab.linkedPanel) {
2074 NewTabPagePreloading.maybeCreatePreloadedBrowser(window);
2077 if (UserInteraction.running("browser.tabs.opening", window)) {
2078 UserInteraction.finish("browser.tabs.opening", window);
2082 _canAdvanceToTab(aTab) {
2083 return !aTab.closing;
2087 * Returns the panel associated with a tab if it has a connected browser
2088 * and/or it is the selected tab.
2089 * For background lazy browsers, this will return null.
2091 getRelatedElement(aTab) {
2096 // Cannot access gBrowser before it's initialized.
2097 if (!gBrowser._initialized) {
2098 return this.tabbox.tabpanels.firstElementChild;
2101 // If the tab's browser is lazy, we need to `_insertBrowser` in order
2102 // to have a linkedPanel. This will also serve to bind the browser
2103 // and make it ready to use. We only do this if the tab is selected
2104 // because otherwise, callers might end up unintentionally binding the
2105 // browser for lazy background tabs.
2106 if (!aTab.linkedPanel) {
2107 if (!aTab.selected) {
2110 gBrowser._insertBrowser(aTab);
2112 return document.getElementById(aTab.linkedPanel);
2115 _updateNewTabVisibility() {
2116 // Helper functions to help deal with customize mode wrapping some items
2118 n.parentNode.localName == "toolbarpaletteitem" ? n.parentNode : n;
2120 n && n.localName == "toolbarpaletteitem" ? n.firstElementChild : n;
2122 // Starting from the tabs element, find the next sibling that:
2123 // - isn't hidden; and
2124 // - isn't the all-tabs button.
2125 // If it's the new tab button, consider the new tab button adjacent to the tabs.
2126 // If the new tab button is marked as adjacent and the tabstrip doesn't
2127 // overflow, we'll display the 'new tab' button inline in the tabstrip.
2128 // In all other cases, the separate new tab button is displayed in its
2129 // customized location.
2132 sib = unwrap(wrap(sib).nextElementSibling);
2133 } while (sib && (sib.hidden || sib.id == "alltabs-button"));
2135 this.toggleAttribute(
2136 "hasadjacentnewtabbutton",
2137 sib && sib.id == "new-tab-button"
2141 onWidgetAfterDOMChange(aNode, aNextNode, aContainer) {
2143 aContainer.ownerDocument == document &&
2144 aContainer.id == "TabsToolbar-customization-target"
2146 this._updateNewTabVisibility();
2150 onAreaNodeRegistered(aArea, aContainer) {
2151 if (aContainer.ownerDocument == document && aArea == "TabsToolbar") {
2152 this._updateNewTabVisibility();
2156 onAreaReset(aArea, aContainer) {
2157 this.onAreaNodeRegistered(aArea, aContainer);
2160 _hiddenSoundPlayingStatusChanged(tab, opts) {
2161 let closed = opts && opts.closed;
2162 if (!closed && tab.soundPlaying && tab.hidden) {
2163 this._hiddenSoundPlayingTabs.add(tab);
2164 this.toggleAttribute("hiddensoundplaying", true);
2166 this._hiddenSoundPlayingTabs.delete(tab);
2167 if (this._hiddenSoundPlayingTabs.size == 0) {
2168 this.removeAttribute("hiddensoundplaying");
2174 if (this.boundObserve) {
2175 Services.prefs.removeObserver("privacy.userContext", this.boundObserve);
2177 CustomizableUI.removeListener(this);
2180 updateTabIndicatorAttr(tab) {
2181 const theseAttributes = ["soundplaying", "muted", "activemedia-blocked"];
2182 const notTheseAttributes = ["pinned", "sharing", "crashed"];
2184 if (notTheseAttributes.some(attr => tab.hasAttribute(attr))) {
2185 tab.removeAttribute("indicator-replaces-favicon");
2189 tab.toggleAttribute(
2190 "indicator-replaces-favicon",
2191 theseAttributes.some(attr => tab.hasAttribute(attr))
2196 customElements.define("tabbrowser-tabs", MozTabbrowserTabs, {