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 class MozTabbrowserTabs extends MozElements.TabsBase {
16 this.addEventListener("TabSelect", this);
17 this.addEventListener("TabClose", this);
18 this.addEventListener("TabAttrModified", this);
19 this.addEventListener("TabHide", this);
20 this.addEventListener("TabShow", this);
21 this.addEventListener("TabPinned", this);
22 this.addEventListener("TabUnpinned", this);
23 this.addEventListener("transitionend", this);
24 this.addEventListener("dblclick", this);
25 this.addEventListener("click", this);
26 this.addEventListener("click", this, true);
27 this.addEventListener("keydown", this, { mozSystemGroup: true });
28 this.addEventListener("dragstart", this);
29 this.addEventListener("dragover", this);
30 this.addEventListener("drop", this);
31 this.addEventListener("dragend", this);
32 this.addEventListener("dragleave", this);
36 this.arrowScrollbox = this.querySelector("arrowscrollbox");
37 this.arrowScrollbox.addEventListener("wheel", this, true);
41 this._blockDblClick = false;
42 this._tabDropIndicator = this.querySelector(".tab-drop-indicator");
43 this._dragOverDelay = 350;
45 this._closeButtonsUpdatePending = false;
46 this._closingTabsSpacer = this.querySelector(".closing-tabs-spacer");
47 this._tabDefaultMaxWidth = NaN;
48 this._lastTabClosedByMouse = false;
49 this._hasTabTempMaxWidth = false;
50 this._scrollButtonWidth = 0;
51 this._lastNumPinned = 0;
52 this._pinnedTabsLayoutCache = null;
53 this._animateElement = this.arrowScrollbox;
54 this._tabClipWidth = Services.prefs.getIntPref(
55 "browser.tabs.tabClipWidth"
57 this._hiddenSoundPlayingTabs = new Set();
59 this._visibleTabs = null;
61 var tab = this.allTabs[0];
62 tab.label = this.emptyTabTitle;
64 // Hide the secondary text for locales where it is unsupported due to size constraints.
65 const language = Services.locale.appLocaleAsBCP47;
66 const unsupportedLocales = Services.prefs.getCharPref(
67 "browser.tabs.secondaryTextUnsupportedLocales"
70 "secondarytext-unsupported",
71 unsupportedLocales.split(",").includes(language.split("-")[0])
74 this.newTabButton.setAttribute(
76 GetDynamicShortcutTooltipText("tabs-newtab-button")
79 let handleResize = () => {
80 this._updateCloseButtons();
81 this._handleTabSelect(true);
83 window.addEventListener("resize", handleResize);
84 this._fullscreenMutationObserver = new MutationObserver(handleResize);
85 this._fullscreenMutationObserver.observe(document.documentElement, {
86 attributeFilter: ["inFullscreen", "inDOMFullscreen"],
89 this.boundObserve = (...args) => this.observe(...args);
90 Services.prefs.addObserver("privacy.userContext", this.boundObserve);
91 this.observe(null, "nsPref:changed", "privacy.userContext.enabled");
93 XPCOMUtils.defineLazyPreferenceGetter(
96 "browser.tabs.tabMinWidth",
98 (pref, prevValue, newValue) => (this._tabMinWidth = newValue),
101 return Math.max(newValue, LIMIT);
105 this._tabMinWidth = this._tabMinWidthPref;
107 CustomizableUI.addListener(this);
108 this._updateNewTabVisibility();
109 this._initializeArrowScrollbox();
111 XPCOMUtils.defineLazyPreferenceGetter(
113 "_closeTabByDblclick",
114 "browser.tabs.closeTabByDblclick",
118 if (gMultiProcessBrowser) {
119 this.tabbox.tabpanels.setAttribute("async", "true");
123 on_TabSelect(event) {
124 this._handleTabSelect();
128 this._hiddenSoundPlayingStatusChanged(event.target, { closed: true });
131 on_TabAttrModified(event) {
133 ["soundplaying", "muted", "activemedia-blocked", "sharing"].some(attr =>
134 event.detail.changed.includes(attr)
137 this.updateTabIndicatorAttr(event.target);
141 event.detail.changed.includes("soundplaying") &&
144 this._hiddenSoundPlayingStatusChanged(event.target);
149 if (event.target.soundPlaying) {
150 this._hiddenSoundPlayingStatusChanged(event.target);
155 if (event.target.soundPlaying) {
156 this._hiddenSoundPlayingStatusChanged(event.target);
160 on_TabPinned(event) {
161 this.updateTabIndicatorAttr(event.target);
164 on_TabUnpinned(event) {
165 this.updateTabIndicatorAttr(event.target);
168 on_transitionend(event) {
169 if (event.propertyName != "max-width") {
173 let tab = event.target ? event.target.closest("tab") : null;
175 if (tab.hasAttribute("fadein")) {
176 if (tab._fullyOpen) {
177 this._updateCloseButtons();
179 this._handleNewTab(tab);
181 } else if (tab.closing) {
182 gBrowser._endRemoveTab(tab);
185 let evt = new CustomEvent("TabAnimationEnd", { bubbles: true });
186 tab.dispatchEvent(evt);
190 // When the tabbar has an unified appearance with the titlebar
191 // and menubar, a double-click in it should have the same behavior
192 // as double-clicking the titlebar
193 if (TabsInTitlebar.enabled) {
197 if (event.button != 0 || event.originalTarget.localName != "scrollbox") {
201 if (!this._blockDblClick) {
205 event.preventDefault();
209 if (event.eventPhase == Event.CAPTURING_PHASE && event.button == 0) {
210 /* Catches extra clicks meant for the in-tab close button.
211 * Placed here to avoid leaking (a temporary handler added from the
212 * in-tab close button binding would close over the tab and leak it
213 * until the handler itself was removed). (bug 897751)
215 * The only sequence in which a second click event (i.e. dblclik)
216 * can be dispatched on an in-tab close button is when it is shown
217 * after the first click (i.e. the first click event was dispatched
218 * on the tab). This happens when we show the close button only on
219 * the active tab. (bug 352021)
220 * The only sequence in which a third click event can be dispatched
221 * on an in-tab close button is when the tab was opened with a
222 * double click on the tabbar. (bug 378344)
223 * In both cases, it is most likely that the close button area has
224 * been accidentally clicked, therefore we do not close the tab.
226 * We don't want to ignore processing of more than one click event,
227 * though, since the user might actually be repeatedly clicking to
228 * close many tabs at once.
230 let target = event.originalTarget;
231 if (target.classList.contains("tab-close-button")) {
232 // We preemptively set this to allow the closing-multiple-tabs-
234 if (this._blockDblClick) {
235 target._ignoredCloseButtonClicks = true;
236 } else if (event.detail > 1 && !target._ignoredCloseButtonClicks) {
237 target._ignoredCloseButtonClicks = true;
238 event.stopPropagation();
241 // Reset the "ignored click" flag
242 target._ignoredCloseButtonClicks = false;
246 /* Protects from close-tab-button errant doubleclick:
247 * Since we're removing the event target, if the user
248 * double-clicks the button, the dblclick event will be dispatched
249 * with the tabbar as its event target (and explicit/originalTarget),
250 * which treats that as a mouse gesture for opening a new tab.
251 * In this context, we're manually blocking the dblclick event.
253 if (this._blockDblClick) {
254 if (!("_clickedTabBarOnce" in this)) {
255 this._clickedTabBarOnce = true;
258 delete this._clickedTabBarOnce;
259 this._blockDblClick = false;
262 event.eventPhase == Event.BUBBLING_PHASE &&
265 let tab = event.target ? event.target.closest("tab") : null;
267 if (tab.multiselected) {
268 gBrowser.removeMultiSelectedTabs();
270 gBrowser.removeTab(tab, {
272 triggeringEvent: event,
275 } else if (event.originalTarget.closest("scrollbox")) {
276 // The user middleclicked on the tabstrip. Check whether the click
277 // was dispatched on the open space of it.
278 let visibleTabs = this._getVisibleTabs();
279 let lastTab = visibleTabs[visibleTabs.length - 1];
280 let winUtils = window.windowUtils;
282 winUtils.getBoundsWithoutFlushing(lastTab)[
283 RTL_UI ? "left" : "right"
286 (!RTL_UI && event.clientX > endOfTab) ||
287 (RTL_UI && event.clientX < endOfTab)
295 event.preventDefault();
296 event.stopPropagation();
301 let { altKey, shiftKey } = event;
302 let [accel, nonAccel] =
303 AppConstants.platform == "macosx"
304 ? [event.metaKey, event.ctrlKey]
305 : [event.ctrlKey, event.metaKey];
307 let keyComboForMove = accel && shiftKey && !altKey && !nonAccel;
308 let keyComboForFocus = accel && !shiftKey && !altKey && !nonAccel;
310 if (!keyComboForMove && !keyComboForFocus) {
314 // Don't check if the event was already consumed because tab navigation
315 // should work always for better user experience.
316 let { visibleTabs, selectedTab } = gBrowser;
317 let { arrowKeysShouldWrap } = this;
318 let focusedTabIndex = this.ariaFocusedIndex;
319 if (focusedTabIndex == -1) {
320 focusedTabIndex = visibleTabs.indexOf(selectedTab);
322 let lastFocusedTabIndex = focusedTabIndex;
323 switch (event.keyCode) {
324 case KeyEvent.DOM_VK_UP:
325 if (keyComboForMove) {
326 gBrowser.moveTabBackward();
331 case KeyEvent.DOM_VK_DOWN:
332 if (keyComboForMove) {
333 gBrowser.moveTabForward();
338 case KeyEvent.DOM_VK_RIGHT:
339 case KeyEvent.DOM_VK_LEFT:
340 if (keyComboForMove) {
341 gBrowser.moveTabOver(event);
343 (!RTL_UI && event.keyCode == KeyEvent.DOM_VK_RIGHT) ||
344 (RTL_UI && event.keyCode == KeyEvent.DOM_VK_LEFT)
351 case KeyEvent.DOM_VK_HOME:
352 if (keyComboForMove) {
353 gBrowser.moveTabToStart();
358 case KeyEvent.DOM_VK_END:
359 if (keyComboForMove) {
360 gBrowser.moveTabToEnd();
362 focusedTabIndex = visibleTabs.length - 1;
365 case KeyEvent.DOM_VK_SPACE:
366 if (visibleTabs[lastFocusedTabIndex].multiselected) {
367 gBrowser.removeFromMultiSelectedTabs(
368 visibleTabs[lastFocusedTabIndex]
371 gBrowser.addToMultiSelectedTabs(visibleTabs[lastFocusedTabIndex]);
375 // Consume the keydown event for the above keyboard
380 if (arrowKeysShouldWrap) {
381 if (focusedTabIndex >= visibleTabs.length) {
383 } else if (focusedTabIndex < 0) {
384 focusedTabIndex = visibleTabs.length - 1;
387 focusedTabIndex = Math.min(
388 visibleTabs.length - 1,
389 Math.max(0, focusedTabIndex)
393 if (keyComboForFocus && focusedTabIndex != lastFocusedTabIndex) {
394 this.ariaFocusedItem = visibleTabs[focusedTabIndex];
397 event.preventDefault();
400 on_dragstart(event) {
401 var tab = this._getDragTargetTab(event);
402 if (!tab || this._isCustomizing) {
406 this.startTabDrag(event, tab);
409 startTabDrag(event, tab, { fromTabList = false } = {}) {
410 let selectedTabs = gBrowser.selectedTabs;
411 let otherSelectedTabs = selectedTabs.filter(
412 selectedTab => selectedTab != tab
414 let dataTransferOrderedTabs = [tab].concat(otherSelectedTabs);
416 let dt = event.dataTransfer;
417 for (let i = 0; i < dataTransferOrderedTabs.length; i++) {
418 let dtTab = dataTransferOrderedTabs[i];
420 dt.mozSetDataAt(TAB_DROP_TYPE, dtTab, i);
421 let dtBrowser = dtTab.linkedBrowser;
423 // We must not set text/x-moz-url or text/plain data here,
424 // otherwise trying to detach the tab by dropping it on the desktop
425 // may result in an "internet shortcut"
427 "text/x-moz-text-internal",
428 dtBrowser.currentURI.spec,
433 // Set the cursor to an arrow during tab drags.
434 dt.mozCursor = "default";
436 // Set the tab as the source of the drag, which ensures we have a stable
437 // node to deliver the `dragend` event. See bug 1345473.
440 if (tab.multiselected) {
441 this._groupSelectedTabs(tab);
444 // Create a canvas to which we capture the current tab.
445 // Until canvas is HiDPI-aware (bug 780362), we need to scale the desired
446 // canvas size (in CSS pixels) to the window's backing resolution in order
447 // to get a full-resolution drag image for use on HiDPI displays.
448 let scale = window.devicePixelRatio;
449 let canvas = this._dndCanvas;
451 this._dndCanvas = canvas = document.createElementNS(
452 "http://www.w3.org/1999/xhtml",
455 canvas.style.width = "100%";
456 canvas.style.height = "100%";
457 canvas.mozOpaque = true;
460 canvas.width = 160 * scale;
461 canvas.height = 90 * scale;
463 let dragImageOffset = -16;
464 let browser = tab.linkedBrowser;
465 if (gMultiProcessBrowser) {
466 var context = canvas.getContext("2d");
467 context.fillStyle = "white";
468 context.fillRect(0, 0, canvas.width, canvas.height);
471 let platform = AppConstants.platform;
472 // On Windows and Mac we can update the drag image during a drag
473 // using updateDragImage. On Linux, we can use a panel.
474 if (platform == "win" || platform == "macosx") {
475 captureListener = function () {
476 dt.updateDragImage(canvas, dragImageOffset, dragImageOffset);
479 // Create a panel to use it in setDragImage
480 // which will tell xul to render a panel that follows
481 // the pointer while a dnd session is on.
482 if (!this._dndPanel) {
483 this._dndCanvas = canvas;
484 this._dndPanel = document.createXULElement("panel");
485 this._dndPanel.className = "dragfeedback-tab";
486 this._dndPanel.setAttribute("type", "drag");
487 let wrapper = document.createElementNS(
488 "http://www.w3.org/1999/xhtml",
491 wrapper.style.width = "160px";
492 wrapper.style.height = "90px";
493 wrapper.appendChild(canvas);
494 this._dndPanel.appendChild(wrapper);
495 document.documentElement.appendChild(this._dndPanel);
497 toDrag = this._dndPanel;
499 // PageThumb is async with e10s but that's fine
500 // since we can update the image during the dnd.
501 PageThumbs.captureToCanvas(browser, canvas)
502 .then(captureListener)
503 .catch(e => console.error(e));
505 // For the non e10s case we can just use PageThumbs
506 // sync, so let's use the canvas for setDragImage.
507 PageThumbs.captureToCanvas(browser, canvas).catch(e =>
510 dragImageOffset = dragImageOffset * scale;
512 dt.setDragImage(toDrag, dragImageOffset, dragImageOffset);
514 // _dragData.offsetX/Y give the coordinates that the mouse should be
515 // positioned relative to the corner of the new window created upon
516 // dragend such that the mouse appears to have the same position
517 // relative to the corner of the dragged tab.
518 function clientX(ele) {
519 return ele.getBoundingClientRect().left;
521 let tabOffsetX = clientX(tab) - clientX(this);
523 offsetX: event.screenX - window.screenX - tabOffsetX,
524 offsetY: event.screenY - window.screenY,
525 scrollX: this.arrowScrollbox.scrollbox.scrollLeft,
526 screenX: event.screenX,
527 movingTabs: (tab.multiselected ? gBrowser.selectedTabs : [tab]).filter(
528 t => t.pinned == tab.pinned
533 event.stopPropagation();
536 Services.telemetry.scalarAdd(
537 "browser.ui.interaction.all_tabs_panel_dragstart_tab_event_count",
544 var effects = this.getDropEffectForTabDrag(event);
546 var ind = this._tabDropIndicator;
547 if (effects == "" || effects == "none") {
551 event.preventDefault();
552 event.stopPropagation();
554 var arrowScrollbox = this.arrowScrollbox;
556 // autoscroll the tab strip if we drag over the scroll
557 // buttons, even if we aren't dragging a tab, but then
558 // return to avoid drawing the drop indicator
559 var pixelsToScroll = 0;
560 if (this.hasAttribute("overflow")) {
561 switch (event.originalTarget) {
562 case arrowScrollbox._scrollButtonUp:
563 pixelsToScroll = arrowScrollbox.scrollIncrement * -1;
565 case arrowScrollbox._scrollButtonDown:
566 pixelsToScroll = arrowScrollbox.scrollIncrement;
569 if (pixelsToScroll) {
570 arrowScrollbox.scrollByPixels(
571 (RTL_UI ? -1 : 1) * pixelsToScroll,
577 let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
579 (effects == "move" || effects == "copy") &&
580 this == draggedTab.container &&
581 !draggedTab._dragData.fromTabList
585 if (!this._isGroupTabsAnimationOver()) {
586 // Wait for grouping tabs animation to finish
589 this._finishGroupSelectedTabs(draggedTab);
591 if (effects == "move") {
592 this._animateTabMove(event);
597 this._finishAnimateTabMove();
599 if (effects == "link") {
600 let tab = this._getDragTargetTab(event, { ignoreTabSides: true });
602 if (!this._dragTime) {
603 this._dragTime = Date.now();
605 if (Date.now() >= this._dragTime + this._dragOverDelay) {
606 this.selectedItem = tab;
613 var rect = arrowScrollbox.getBoundingClientRect();
615 if (pixelsToScroll) {
616 // if we are scrolling, put the drop indicator at the edge
617 // so that it doesn't jump while scrolling
618 let scrollRect = arrowScrollbox.scrollClientRect;
619 let minMargin = scrollRect.left - rect.left;
620 let maxMargin = Math.min(
621 minMargin + scrollRect.width,
625 [minMargin, maxMargin] = [
626 this.clientWidth - maxMargin,
627 this.clientWidth - minMargin,
630 newMargin = pixelsToScroll > 0 ? maxMargin : minMargin;
632 let newIndex = this._getDropIndex(event);
633 let children = this.allTabs;
634 if (newIndex == children.length) {
635 let tabRect = this._getVisibleTabs().at(-1).getBoundingClientRect();
637 newMargin = rect.right - tabRect.left;
639 newMargin = tabRect.right - rect.left;
642 let tabRect = children[newIndex].getBoundingClientRect();
644 newMargin = rect.right - tabRect.right;
646 newMargin = tabRect.left - rect.left;
652 newMargin += ind.clientWidth / 2;
656 ind.style.transform = "translate(" + Math.round(newMargin) + "px)";
660 var dt = event.dataTransfer;
661 var dropEffect = dt.dropEffect;
664 if (dt.mozTypesAt(0)[0] == TAB_DROP_TYPE) {
666 draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
671 movingTabs = draggedTab._dragData.movingTabs;
672 draggedTab.container._finishGroupSelectedTabs(draggedTab);
675 this._tabDropIndicator.hidden = true;
676 event.stopPropagation();
677 if (draggedTab && dropEffect == "copy") {
678 // copy the dropped tab (wherever it's from)
679 let newIndex = this._getDropIndex(event);
681 for (let tab of movingTabs) {
682 let newTab = gBrowser.duplicateTab(tab);
683 gBrowser.moveTabTo(newTab, newIndex++);
684 if (tab == draggedTab) {
685 draggedTabCopy = newTab;
688 if (draggedTab.container != this || event.shiftKey) {
689 this.selectedItem = draggedTabCopy;
691 } else if (draggedTab && draggedTab.container == this) {
692 let oldTranslateX = Math.round(draggedTab._dragData.translateX);
693 let tabWidth = Math.round(draggedTab._dragData.tabWidth);
694 let translateOffset = oldTranslateX % tabWidth;
695 let newTranslateX = oldTranslateX - translateOffset;
696 if (oldTranslateX > 0 && translateOffset > tabWidth / 2) {
697 newTranslateX += tabWidth;
698 } else if (oldTranslateX < 0 && -translateOffset > tabWidth / 2) {
699 newTranslateX -= tabWidth;
703 if (draggedTab._dragData.fromTabList) {
704 dropIndex = this._getDropIndex(event);
707 "animDropIndex" in draggedTab._dragData &&
708 draggedTab._dragData.animDropIndex;
710 let incrementDropIndex = true;
711 if (dropIndex && dropIndex > movingTabs[0]._tPos) {
713 incrementDropIndex = false;
716 if (oldTranslateX && oldTranslateX != newTranslateX && !gReduceMotion) {
717 for (let tab of movingTabs) {
718 tab.toggleAttribute("tabdrop-samewindow", true);
719 tab.style.transform = "translateX(" + newTranslateX + "px)";
720 let postTransitionCleanup = () => {
721 tab.removeAttribute("tabdrop-samewindow");
723 this._finishAnimateTabMove();
724 if (dropIndex !== false) {
725 gBrowser.moveTabTo(tab, dropIndex);
726 if (incrementDropIndex) {
731 gBrowser.syncThrobberAnimations(tab);
734 postTransitionCleanup();
736 let onTransitionEnd = transitionendEvent => {
738 transitionendEvent.propertyName != "transform" ||
739 transitionendEvent.originalTarget != tab
743 tab.removeEventListener("transitionend", onTransitionEnd);
745 postTransitionCleanup();
747 tab.addEventListener("transitionend", onTransitionEnd);
751 this._finishAnimateTabMove();
752 if (dropIndex !== false) {
753 for (let tab of movingTabs) {
754 gBrowser.moveTabTo(tab, dropIndex);
755 if (incrementDropIndex) {
761 } else if (draggedTab) {
762 // Move the tabs. To avoid multiple tab-switches in the original window,
763 // the selected tab should be adopted last.
764 const dropIndex = this._getDropIndex(event);
765 let newIndex = dropIndex;
767 let indexForSelectedTab;
768 for (let i = 0; i < movingTabs.length; ++i) {
769 const tab = movingTabs[i];
772 indexForSelectedTab = newIndex;
774 const newTab = gBrowser.adoptTab(tab, newIndex, tab == draggedTab);
781 const newTab = gBrowser.adoptTab(
784 selectedTab == draggedTab
791 // Restore tab selection
792 gBrowser.addRangeToMultiSelectedTabs(
793 gBrowser.tabs[dropIndex],
794 gBrowser.tabs[newIndex - 1]
797 // Pass true to disallow dropping javascript: or data: urls
800 links = browserDragAndDrop.dropLinks(event, true);
803 if (!links || links.length === 0) {
807 let inBackground = Services.prefs.getBoolPref(
808 "browser.tabs.loadInBackground"
810 if (event.shiftKey) {
811 inBackground = !inBackground;
814 let targetTab = this._getDragTargetTab(event, { ignoreTabSides: true });
815 let userContextId = this.selectedItem.getAttribute("usercontextid");
816 let replace = !!targetTab;
817 let newIndex = this._getDropIndex(event);
818 let urls = links.map(link => link.url);
819 let csp = browserDragAndDrop.getCsp(event);
820 let triggeringPrincipal =
821 browserDragAndDrop.getTriggeringPrincipal(event);
826 Services.prefs.getIntPref("browser.tabs.maxOpenBeforeWarn")
828 // Sync dialog cannot be used inside drop event handler.
829 let answer = await OpenInTabsUtils.promiseConfirmOpenInTabs(
838 gBrowser.loadTabs(urls, {
841 allowThirdPartyFixup: true,
852 delete draggedTab._dragData;
857 var dt = event.dataTransfer;
858 var draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
860 // Prevent this code from running if a tabdrop animation is
861 // running since calling _finishAnimateTabMove would clear
862 // any CSS transition that is running.
863 if (draggedTab.hasAttribute("tabdrop-samewindow")) {
867 this._finishGroupSelectedTabs(draggedTab);
868 this._finishAnimateTabMove();
871 dt.mozUserCancelled ||
872 dt.dropEffect != "none" ||
875 delete draggedTab._dragData;
879 // Check if tab detaching is enabled
880 if (!Services.prefs.getBoolPref("browser.tabs.allowTabDetach")) {
884 // Disable detach within the browser toolbox
885 var eX = event.screenX;
886 var eY = event.screenY;
887 var wX = window.screenX;
888 // check if the drop point is horizontally within the window
889 if (eX > wX && eX < wX + window.outerWidth) {
890 // also avoid detaching if the the tab was dropped too close to
891 // the tabbar (half a tab)
892 let rect = window.windowUtils.getBoundsWithoutFlushing(
895 let detachTabThresholdY = window.screenY + rect.top + 1.5 * rect.height;
896 if (eY < detachTabThresholdY && eY > window.screenY) {
901 // screen.availLeft et. al. only check the screen that this window is on,
902 // but we want to look at the screen the tab is being dropped onto.
903 var screen = event.screen;
908 // Get available rect in desktop pixels.
909 screen.GetAvailRectDisplayPix(availX, availY, availWidth, availHeight);
910 availX = availX.value;
911 availY = availY.value;
912 availWidth = availWidth.value;
913 availHeight = availHeight.value;
915 // Compute the final window size in desktop pixels ensuring that the new
916 // window entirely fits within `screen`.
917 let ourCssToDesktopScale =
918 window.devicePixelRatio / window.desktopToDeviceScale;
919 let screenCssToDesktopScale =
920 screen.defaultCSSScaleFactor / screen.contentsScaleFactor;
922 // NOTE(emilio): Multiplying the sizes here for screenCssToDesktopScale
923 // means that we'll try to create a window that has the same amount of CSS
924 // pixels than our current window, not the same amount of device pixels.
925 // There are pros and cons of both conversions, though this matches the
926 // pre-existing intended behavior.
927 var winWidth = Math.min(
928 window.outerWidth * screenCssToDesktopScale,
931 var winHeight = Math.min(
932 window.outerHeight * screenCssToDesktopScale,
936 // This is slightly tricky: _dragData.offsetX/Y is an offset in CSS
937 // pixels. Since we're doing the sizing above based on those, we also need
938 // to apply the offset with pixels relative to the screen's scale rather
942 eX * ourCssToDesktopScale -
943 draggedTab._dragData.offsetX * screenCssToDesktopScale,
946 availX + availWidth - winWidth
950 eY * ourCssToDesktopScale -
951 draggedTab._dragData.offsetY * screenCssToDesktopScale,
954 availY + availHeight - winHeight
957 // Convert back left and top to our CSS pixel space.
958 left /= ourCssToDesktopScale;
959 top /= ourCssToDesktopScale;
961 delete draggedTab._dragData;
963 if (gBrowser.tabs.length == 1) {
964 // resize _before_ move to ensure the window fits the new screen. if
965 // the window is too large for its screen, the window manager may do
966 // automatic repositioning.
968 // Since we're resizing before moving to our new screen, we need to use
969 // sizes relative to the current screen. If we moved, then resized, then
970 // we could avoid this special-case and share this with the else branch
972 winWidth /= ourCssToDesktopScale;
973 winHeight /= ourCssToDesktopScale;
975 window.resizeTo(winWidth, winHeight);
976 window.moveTo(left, top);
979 // We're opening a new window in a new screen, so make sure to use sizes
980 // relative to the new screen.
981 winWidth /= screenCssToDesktopScale;
982 winHeight /= screenCssToDesktopScale;
984 let props = { screenX: left, screenY: top, suppressanimation: 1 };
985 if (AppConstants.platform != "win") {
986 props.outerWidth = winWidth;
987 props.outerHeight = winHeight;
989 gBrowser.replaceTabsWithWindow(draggedTab, props);
991 event.stopPropagation();
994 on_dragleave(event) {
997 // This does not work at all (see bug 458613)
998 var target = event.relatedTarget;
999 while (target && target != this) {
1000 target = target.parentNode;
1006 this._tabDropIndicator.hidden = true;
1007 event.stopPropagation();
1012 Services.prefs.getBoolPref("toolkit.tabbox.switchByScrolling", false)
1014 event.stopImmediatePropagation();
1018 get emptyTabTitle() {
1019 // Normal tab title is used also in the permanent private browsing mode.
1021 PrivateBrowsingUtils.isWindowPrivate(window) &&
1022 !Services.prefs.getBoolPref("browser.privatebrowsing.autostart")
1023 ? "tabbrowser-empty-private-tab-title"
1024 : "tabbrowser-empty-tab-title";
1025 return gBrowser.tabLocalization.formatValueSync(l10nId);
1029 return document.getElementById("tabbrowser-tabbox");
1032 get newTabButton() {
1033 return this.querySelector("#tabs-newtab-button");
1036 // Accessor for tabs. arrowScrollbox has a container for non-tab elements
1037 // at the end, everything else is <tab>s.
1039 if (this._allTabs) {
1040 return this._allTabs;
1042 let children = Array.from(this.arrowScrollbox.children);
1044 this._allTabs = children;
1049 if (!this._visibleTabs) {
1050 this._visibleTabs = Array.prototype.filter.call(
1052 tab => !tab.hidden && !tab.closing
1055 return this._visibleTabs;
1058 _invalidateCachedTabs() {
1059 this._allTabs = null;
1060 this._visibleTabs = null;
1063 _invalidateCachedVisibleTabs() {
1064 this._visibleTabs = null;
1068 return this.insertBefore(tab, null);
1071 insertBefore(tab, node) {
1072 if (!this.arrowScrollbox) {
1073 throw new Error("Shouldn't call this without arrowscrollbox");
1076 let { arrowScrollbox } = this;
1078 // We have a container for non-tab elements at the end of the scrollbox.
1079 node = arrowScrollbox.lastChild;
1081 return arrowScrollbox.insertBefore(tab, node);
1084 set _tabMinWidth(val) {
1085 this.style.setProperty("--tab-min-width", val + "px");
1088 get _isCustomizing() {
1089 return document.documentElement.getAttribute("customizing") == "true";
1092 // This overrides the TabsBase _selectNewTab method so that we can
1093 // potentially interrupt keyboard tab switching when sharing the
1094 // window or screen.
1095 _selectNewTab(aNewTab, aFallbackDir, aWrap) {
1096 if (!gSharedTabWarning.willShowSharedTabWarning(aNewTab)) {
1097 super._selectNewTab(aNewTab, aFallbackDir, aWrap);
1101 _initializeArrowScrollbox() {
1102 let arrowScrollbox = this.arrowScrollbox;
1103 arrowScrollbox.shadowRoot.addEventListener(
1106 // Ignore underflow events:
1107 // - from nested scrollable elements
1108 // - for vertical orientation
1109 // - corresponding to an overflow event that we ignored
1111 event.originalTarget != arrowScrollbox.scrollbox ||
1112 event.detail == 0 ||
1113 !this.hasAttribute("overflow")
1118 this.removeAttribute("overflow");
1120 if (this._lastTabClosedByMouse) {
1121 this._expandSpacerBy(this._scrollButtonWidth);
1124 for (let tab of gBrowser._removingTabs) {
1125 gBrowser.removeTab(tab);
1128 this._positionPinnedTabs();
1129 this._updateCloseButtons();
1134 arrowScrollbox.shadowRoot.addEventListener("overflow", event => {
1135 // Ignore overflow events:
1136 // - from nested scrollable elements
1137 // - for vertical orientation
1139 event.originalTarget != arrowScrollbox.scrollbox ||
1145 this.toggleAttribute("overflow", true);
1146 this._positionPinnedTabs();
1147 this._updateCloseButtons();
1148 this._handleTabSelect(true);
1151 // Override arrowscrollbox.js method, since our scrollbox's children are
1152 // inherited from the scrollbox binding parent (this).
1153 arrowScrollbox._getScrollableElements = () => {
1154 return this.allTabs.filter(arrowScrollbox._canScrollToElement);
1156 arrowScrollbox._canScrollToElement = tab => {
1157 return !tab._pinnedUnscrollable && !tab.hidden;
1161 observe(aSubject, aTopic, aData) {
1163 case "nsPref:changed":
1164 // This is has to deal with changes in
1165 // privacy.userContext.enabled and
1166 // privacy.userContext.newTabContainerOnLeftClick.enabled.
1167 let containersEnabled =
1168 Services.prefs.getBoolPref("privacy.userContext.enabled") &&
1169 !PrivateBrowsingUtils.isWindowPrivate(window);
1171 // This pref won't change so often, so just recreate the menu.
1172 const newTabLeftClickOpensContainersMenu = Services.prefs.getBoolPref(
1173 "privacy.userContext.newTabContainerOnLeftClick.enabled"
1176 // There are separate "new tab" buttons for when the tab strip
1177 // is overflowed and when it is not. Attach the long click
1178 // popup to both of them.
1179 const newTab = document.getElementById("new-tab-button");
1180 const newTab2 = this.newTabButton;
1182 for (let parent of [newTab, newTab2]) {
1187 parent.removeAttribute("type");
1188 if (parent.menupopup) {
1189 parent.menupopup.remove();
1192 if (containersEnabled) {
1193 parent.setAttribute("context", "new-tab-button-popup");
1195 let popup = document
1196 .getElementById("new-tab-button-popup")
1198 popup.removeAttribute("id");
1199 popup.className = "new-tab-popup";
1200 popup.setAttribute("position", "after_end");
1201 parent.prepend(popup);
1202 parent.setAttribute("type", "menu");
1203 // Update tooltip text
1204 nodeToTooltipMap[parent.id] = newTabLeftClickOpensContainersMenu
1205 ? "newTabAlwaysContainer.tooltip"
1206 : "newTabContainer.tooltip";
1208 nodeToTooltipMap[parent.id] = "newTabButton.tooltip";
1209 parent.removeAttribute("context", "new-tab-button-popup");
1211 // evict from tooltip cache
1212 gDynamicTooltipCache.delete(parent.id);
1214 // If containers and press-hold container menu are both used,
1215 // add to gClickAndHoldListenersOnElement; otherwise, remove.
1216 if (containersEnabled && !newTabLeftClickOpensContainersMenu) {
1217 gClickAndHoldListenersOnElement.add(parent);
1219 gClickAndHoldListenersOnElement.remove(parent);
1227 _updateCloseButtons() {
1228 // If we're overflowing, tabs are at their minimum widths.
1229 if (this.hasAttribute("overflow")) {
1230 this.setAttribute("closebuttons", "activetab");
1234 if (this._closeButtonsUpdatePending) {
1237 this._closeButtonsUpdatePending = true;
1239 // Wait until after the next paint to get current layout data from
1240 // getBoundsWithoutFlushing.
1241 window.requestAnimationFrame(() => {
1242 window.requestAnimationFrame(() => {
1243 this._closeButtonsUpdatePending = false;
1245 // The scrollbox may have started overflowing since we checked
1246 // overflow earlier, so check again.
1247 if (this.hasAttribute("overflow")) {
1248 this.setAttribute("closebuttons", "activetab");
1252 // Check if tab widths are below the threshold where we want to
1253 // remove close buttons from background tabs so that people don't
1254 // accidentally close tabs by selecting them.
1256 return window.windowUtils.getBoundsWithoutFlushing(ele);
1258 let tab = this._getVisibleTabs()[gBrowser._numPinnedTabs];
1259 if (tab && rect(tab).width <= this._tabClipWidth) {
1260 this.setAttribute("closebuttons", "activetab");
1262 this.removeAttribute("closebuttons");
1268 _updateHiddenTabsStatus() {
1269 this.toggleAttribute(
1271 gBrowser.visibleTabs.length < gBrowser.tabs.length
1275 _handleTabSelect(aInstant) {
1276 let selectedTab = this.selectedItem;
1277 if (this.hasAttribute("overflow")) {
1278 this.arrowScrollbox.ensureElementIsVisible(selectedTab, aInstant);
1281 selectedTab._notselectedsinceload = false;
1285 * Try to keep the active tab's close button under the mouse cursor
1287 _lockTabSizing(aTab, aTabWidth) {
1288 let tabs = this._getVisibleTabs();
1293 var isEndTab = aTab._tPos > tabs[tabs.length - 1]._tPos;
1295 if (!this._tabDefaultMaxWidth) {
1296 this._tabDefaultMaxWidth = parseFloat(
1297 window.getComputedStyle(aTab).maxWidth
1300 this._lastTabClosedByMouse = true;
1301 this._scrollButtonWidth = window.windowUtils.getBoundsWithoutFlushing(
1302 this.arrowScrollbox._scrollButtonDown
1305 if (this.hasAttribute("overflow")) {
1306 // Don't need to do anything if we're in overflow mode and aren't scrolled
1307 // all the way to the right, or if we're closing the last tab.
1308 if (isEndTab || !this.arrowScrollbox._scrollButtonDown.disabled) {
1311 // If the tab has an owner that will become the active tab, the owner will
1312 // be to the left of it, so we actually want the left tab to slide over.
1313 // This can't be done as easily in non-overflow mode, so we don't bother.
1317 this._expandSpacerBy(aTabWidth);
1319 // non-overflow mode
1320 // Locking is neither in effect nor needed, so let tabs expand normally.
1321 if (isEndTab && !this._hasTabTempMaxWidth) {
1324 let numPinned = gBrowser._numPinnedTabs;
1325 // Force tabs to stay the same width, unless we're closing the last tab,
1326 // which case we need to let them expand just enough so that the overall
1327 // tabbar width is the same.
1329 let numNormalTabs = tabs.length - numPinned;
1330 aTabWidth = (aTabWidth * (numNormalTabs + 1)) / numNormalTabs;
1331 if (aTabWidth > this._tabDefaultMaxWidth) {
1332 aTabWidth = this._tabDefaultMaxWidth;
1336 let tabsToReset = [];
1337 for (let i = numPinned; i < tabs.length; i++) {
1339 tab.style.setProperty("max-width", aTabWidth, "important");
1341 // keep tabs the same width
1342 tab.style.transition = "none";
1343 tabsToReset.push(tab);
1347 if (tabsToReset.length) {
1349 .promiseDocumentFlushed(() => {})
1351 window.requestAnimationFrame(() => {
1352 for (let tab of tabsToReset) {
1353 tab.style.transition = "";
1359 this._hasTabTempMaxWidth = true;
1360 gBrowser.addEventListener("mousemove", this);
1361 window.addEventListener("mouseout", this);
1365 _expandSpacerBy(pixels) {
1366 let spacer = this._closingTabsSpacer;
1367 spacer.style.width = parseFloat(spacer.style.width) + pixels + "px";
1368 this.toggleAttribute("using-closing-tabs-spacer", true);
1369 gBrowser.addEventListener("mousemove", this);
1370 window.addEventListener("mouseout", this);
1373 _unlockTabSizing() {
1374 gBrowser.removeEventListener("mousemove", this);
1375 window.removeEventListener("mouseout", this);
1377 if (this._hasTabTempMaxWidth) {
1378 this._hasTabTempMaxWidth = false;
1379 let tabs = this._getVisibleTabs();
1380 for (let i = 0; i < tabs.length; i++) {
1381 tabs[i].style.maxWidth = "";
1385 if (this.hasAttribute("using-closing-tabs-spacer")) {
1386 this.removeAttribute("using-closing-tabs-spacer");
1387 this._closingTabsSpacer.style.width = 0;
1391 uiDensityChanged() {
1392 this._positionPinnedTabs();
1393 this._updateCloseButtons();
1394 this._handleTabSelect(true);
1397 _positionPinnedTabs() {
1398 let tabs = this._getVisibleTabs();
1399 let numPinned = gBrowser._numPinnedTabs;
1401 this.hasAttribute("overflow") &&
1402 tabs.length > numPinned &&
1405 this.toggleAttribute("haspinnedtabs", !!numPinned);
1406 this.toggleAttribute("positionpinnedtabs", doPosition);
1409 let layoutData = this._pinnedTabsLayoutCache;
1410 let uiDensity = document.documentElement.getAttribute("uidensity");
1411 if (!layoutData || layoutData.uiDensity != uiDensity) {
1412 let arrowScrollbox = this.arrowScrollbox;
1413 layoutData = this._pinnedTabsLayoutCache = {
1415 pinnedTabWidth: tabs[0].getBoundingClientRect().width,
1417 arrowScrollbox.scrollbox.getBoundingClientRect().left -
1418 arrowScrollbox.getBoundingClientRect().left +
1420 getComputedStyle(arrowScrollbox.scrollbox).paddingInlineStart
1426 for (let i = numPinned - 1; i >= 0; i--) {
1428 width += layoutData.pinnedTabWidth;
1429 tab.style.setProperty(
1430 "margin-inline-start",
1431 -(width + layoutData.scrollStartOffset) + "px",
1434 tab._pinnedUnscrollable = true;
1436 this.style.setProperty(
1437 "--tab-overflow-pinned-tabs-width",
1441 for (let i = 0; i < numPinned; i++) {
1443 tab.style.marginInlineStart = "";
1444 tab._pinnedUnscrollable = false;
1447 this.style.removeProperty("--tab-overflow-pinned-tabs-width");
1450 if (this._lastNumPinned != numPinned) {
1451 this._lastNumPinned = numPinned;
1452 this._handleTabSelect(true);
1456 _animateTabMove(event) {
1457 let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
1458 let movingTabs = draggedTab._dragData.movingTabs;
1460 if (!this.hasAttribute("movingtab")) {
1461 this.toggleAttribute("movingtab", true);
1462 gNavToolbox.toggleAttribute("movingtab", true);
1463 if (!draggedTab.multiselected) {
1464 this.selectedItem = draggedTab;
1468 if (!("animLastScreenX" in draggedTab._dragData)) {
1469 draggedTab._dragData.animLastScreenX = draggedTab._dragData.screenX;
1472 let screenX = event.screenX;
1473 if (screenX == draggedTab._dragData.animLastScreenX) {
1477 // Direction of the mouse movement.
1478 let ltrMove = screenX > draggedTab._dragData.animLastScreenX;
1480 draggedTab._dragData.animLastScreenX = screenX;
1482 let pinned = draggedTab.pinned;
1483 let numPinned = gBrowser._numPinnedTabs;
1484 let tabs = this._getVisibleTabs().slice(
1485 pinned ? 0 : numPinned,
1486 pinned ? numPinned : undefined
1491 // Copy moving tabs array to avoid infinite reversing.
1492 movingTabs = [...movingTabs].reverse();
1494 let tabWidth = draggedTab.getBoundingClientRect().width;
1495 let shiftWidth = tabWidth * movingTabs.length;
1496 draggedTab._dragData.tabWidth = tabWidth;
1498 // Move the dragged tab based on the mouse position.
1500 let leftTab = tabs[0];
1501 let rightTab = tabs[tabs.length - 1];
1502 let rightMovingTabScreenX = movingTabs[movingTabs.length - 1].screenX;
1503 let leftMovingTabScreenX = movingTabs[0].screenX;
1504 let translateX = screenX - draggedTab._dragData.screenX;
1507 this.arrowScrollbox.scrollbox.scrollLeft -
1508 draggedTab._dragData.scrollX;
1510 let leftBound = leftTab.screenX - leftMovingTabScreenX;
1513 rightTab.getBoundingClientRect().width -
1514 (rightMovingTabScreenX + tabWidth);
1515 translateX = Math.min(Math.max(translateX, leftBound), rightBound);
1517 for (let tab of movingTabs) {
1518 tab.style.transform = "translateX(" + translateX + "px)";
1521 draggedTab._dragData.translateX = translateX;
1523 // Determine what tab we're dragging over.
1524 // * Single tab dragging: Point of reference is the center of the dragged tab. If that
1525 // point touches a background tab, the dragged tab would take that
1526 // tab's position when dropped.
1527 // * Multiple tabs dragging: All dragged tabs are one "giant" tab with two
1528 // points of reference (center of tabs on the extremities). When
1529 // mouse is moving from left to right, the right reference gets activated,
1530 // otherwise the left reference will be used. Everything else works the same
1531 // as single tab dragging.
1532 // * We're doing a binary search in order to reduce the amount of
1533 // tabs we need to check.
1535 tabs = tabs.filter(t => !movingTabs.includes(t) || t == draggedTab);
1536 let leftTabCenter = leftMovingTabScreenX + translateX + tabWidth / 2;
1537 let rightTabCenter = rightMovingTabScreenX + translateX + tabWidth / 2;
1538 let tabCenter = ltrMove ? rightTabCenter : leftTabCenter;
1541 "animDropIndex" in draggedTab._dragData
1542 ? draggedTab._dragData.animDropIndex
1543 : movingTabs[0]._tPos;
1545 let high = tabs.length - 1;
1546 while (low <= high) {
1547 let mid = Math.floor((low + high) / 2);
1548 if (tabs[mid] == draggedTab && ++mid > high) {
1551 screenX = tabs[mid].screenX + getTabShift(tabs[mid], oldIndex);
1552 if (screenX > tabCenter) {
1555 screenX + tabs[mid].getBoundingClientRect().width <
1560 newIndex = tabs[mid]._tPos;
1564 if (newIndex >= oldIndex) {
1567 if (newIndex < 0 || newIndex == oldIndex) {
1570 draggedTab._dragData.animDropIndex = newIndex;
1572 // Shift background tabs to leave a gap where the dragged tab
1573 // would currently be dropped.
1575 for (let tab of tabs) {
1576 if (tab != draggedTab) {
1577 let shift = getTabShift(tab, newIndex);
1578 tab.style.transform = shift ? "translateX(" + shift + "px)" : "";
1582 function getTabShift(tab, dropIndex) {
1583 if (tab._tPos < draggedTab._tPos && tab._tPos >= dropIndex) {
1584 return RTL_UI ? -shiftWidth : shiftWidth;
1586 if (tab._tPos > draggedTab._tPos && tab._tPos < dropIndex) {
1587 return RTL_UI ? shiftWidth : -shiftWidth;
1593 _finishAnimateTabMove() {
1594 if (!this.hasAttribute("movingtab")) {
1598 for (let tab of this._getVisibleTabs()) {
1599 tab.style.transform = "";
1602 this.removeAttribute("movingtab");
1603 gNavToolbox.removeAttribute("movingtab");
1605 this._handleTabSelect();
1609 * Regroup all selected tabs around the
1612 _groupSelectedTabs(tab) {
1613 let draggedTabPos = tab._tPos;
1614 let selectedTabs = gBrowser.selectedTabs;
1615 let animate = !gReduceMotion;
1617 tab.groupingTabsData = {
1621 // Animate left selected tabs
1623 let insertAtPos = draggedTabPos - 1;
1624 for (let i = selectedTabs.indexOf(tab) - 1; i > -1; i--) {
1625 let movingTab = selectedTabs[i];
1626 insertAtPos = newIndex(movingTab, insertAtPos);
1629 movingTab.groupingTabsData = {};
1630 addAnimationData(movingTab, insertAtPos, "left");
1632 gBrowser.moveTabTo(movingTab, insertAtPos);
1637 // Animate right selected tabs
1639 insertAtPos = draggedTabPos + 1;
1641 let i = selectedTabs.indexOf(tab) + 1;
1642 i < selectedTabs.length;
1645 let movingTab = selectedTabs[i];
1646 insertAtPos = newIndex(movingTab, insertAtPos);
1649 movingTab.groupingTabsData = {};
1650 addAnimationData(movingTab, insertAtPos, "right");
1652 gBrowser.moveTabTo(movingTab, insertAtPos);
1657 // Slide the relevant tabs to their new position.
1658 for (let t of this._getVisibleTabs()) {
1659 if (t.groupingTabsData && t.groupingTabsData.translateX) {
1660 let translateX = (RTL_UI ? -1 : 1) * t.groupingTabsData.translateX;
1661 t.style.transform = "translateX(" + translateX + "px)";
1665 function newIndex(aTab, index) {
1666 // Don't allow mixing pinned and unpinned tabs.
1668 return Math.min(index, gBrowser._numPinnedTabs - 1);
1670 return Math.max(index, gBrowser._numPinnedTabs);
1673 function addAnimationData(movingTab, movingTabNewIndex, side) {
1674 let movingTabOldIndex = movingTab._tPos;
1676 if (movingTabOldIndex == movingTabNewIndex) {
1677 // movingTab is already at the right position
1678 // and thus don't need to be animated.
1682 let movingTabWidth = movingTab.getBoundingClientRect().width;
1683 let shift = (movingTabNewIndex - movingTabOldIndex) * movingTabWidth;
1685 movingTab.groupingTabsData.animate = true;
1686 movingTab.toggleAttribute("tab-grouping", true);
1688 movingTab.groupingTabsData.translateX = shift;
1690 let postTransitionCleanup = () => {
1691 movingTab.groupingTabsData.newIndex = movingTabNewIndex;
1692 movingTab.groupingTabsData.animate = false;
1694 if (gReduceMotion) {
1695 postTransitionCleanup();
1697 let onTransitionEnd = transitionendEvent => {
1699 transitionendEvent.propertyName != "transform" ||
1700 transitionendEvent.originalTarget != movingTab
1704 movingTab.removeEventListener("transitionend", onTransitionEnd);
1705 postTransitionCleanup();
1708 movingTab.addEventListener("transitionend", onTransitionEnd);
1711 // Add animation data for tabs between movingTab (selected
1712 // tab moving towards the dragged tab) and draggedTab.
1713 // Those tabs in the middle should move in
1714 // the opposite direction of movingTab.
1716 let lowerIndex = Math.min(movingTabOldIndex, draggedTabPos);
1717 let higherIndex = Math.max(movingTabOldIndex, draggedTabPos);
1719 for (let i = lowerIndex + 1; i < higherIndex; i++) {
1720 let middleTab = gBrowser.visibleTabs[i];
1722 if (middleTab.pinned != movingTab.pinned) {
1723 // Don't mix pinned and unpinned tabs
1727 if (middleTab.multiselected) {
1728 // Skip because this selected tab should
1729 // be shifted towards the dragged Tab.
1734 !middleTab.groupingTabsData ||
1735 !middleTab.groupingTabsData.translateX
1737 middleTab.groupingTabsData = { translateX: 0 };
1739 if (side == "left") {
1740 middleTab.groupingTabsData.translateX -= movingTabWidth;
1742 middleTab.groupingTabsData.translateX += movingTabWidth;
1745 middleTab.toggleAttribute("tab-grouping", true);
1750 _finishGroupSelectedTabs(tab) {
1751 if (!tab.groupingTabsData || tab.groupingTabsData.finished) {
1755 tab.groupingTabsData.finished = true;
1757 let selectedTabs = gBrowser.selectedTabs;
1758 let tabIndex = selectedTabs.indexOf(tab);
1761 for (let i = tabIndex - 1; i > -1; i--) {
1762 let movingTab = selectedTabs[i];
1763 if (movingTab.groupingTabsData.newIndex) {
1764 gBrowser.moveTabTo(movingTab, movingTab.groupingTabsData.newIndex);
1768 // Moving right tabs
1769 for (let i = tabIndex + 1; i < selectedTabs.length; i++) {
1770 let movingTab = selectedTabs[i];
1771 if (movingTab.groupingTabsData.newIndex) {
1772 gBrowser.moveTabTo(movingTab, movingTab.groupingTabsData.newIndex);
1776 for (let t of this._getVisibleTabs()) {
1777 t.style.transform = "";
1778 t.removeAttribute("tab-grouping");
1779 delete t.groupingTabsData;
1783 _isGroupTabsAnimationOver() {
1784 for (let tab of gBrowser.selectedTabs) {
1785 if (tab.groupingTabsData && tab.groupingTabsData.animate) {
1792 handleEvent(aEvent) {
1793 switch (aEvent.type) {
1795 // If the "related target" (the node to which the pointer went) is not
1796 // a child of the current document, the mouse just left the window.
1797 let relatedTarget = aEvent.relatedTarget;
1798 if (relatedTarget && relatedTarget.ownerDocument == document) {
1803 if (document.getElementById("tabContextMenu").state != "open") {
1804 this._unlockTabSizing();
1808 let methodName = `on_${aEvent.type}`;
1809 if (methodName in this) {
1810 this[methodName](aEvent);
1812 throw new Error(`Unexpected event ${aEvent.type}`);
1817 _notifyBackgroundTab(aTab) {
1818 if (aTab.pinned || aTab.hidden || !this.hasAttribute("overflow")) {
1822 this._lastTabToScrollIntoView = aTab;
1823 if (!this._backgroundTabScrollPromise) {
1824 this._backgroundTabScrollPromise = window
1825 .promiseDocumentFlushed(() => {
1827 this._lastTabToScrollIntoView.getBoundingClientRect();
1828 let selectedTab = this.selectedItem;
1829 if (selectedTab.pinned) {
1832 selectedTab = selectedTab.getBoundingClientRect();
1834 left: selectedTab.left,
1835 right: selectedTab.right,
1839 this._lastTabToScrollIntoView,
1840 this.arrowScrollbox.scrollClientRect,
1841 { left: lastTabRect.left, right: lastTabRect.right },
1845 .then(([tabToScrollIntoView, scrollRect, tabRect, selectedRect]) => {
1846 // First off, remove the promise so we can re-enter if necessary.
1847 delete this._backgroundTabScrollPromise;
1848 // Then, if the layout info isn't for the last-scrolled-to-tab, re-run
1849 // the code above to get layout info for *that* tab, and don't do
1850 // anything here, as we really just want to run this for the last-opened tab.
1851 if (this._lastTabToScrollIntoView != tabToScrollIntoView) {
1852 this._notifyBackgroundTab(this._lastTabToScrollIntoView);
1855 delete this._lastTabToScrollIntoView;
1856 // Is the new tab already completely visible?
1858 scrollRect.left <= tabRect.left &&
1859 tabRect.right <= scrollRect.right
1864 if (this.arrowScrollbox.smoothScroll) {
1865 // Can we make both the new tab and the selected tab completely visible?
1869 tabRect.right - selectedRect.left,
1870 selectedRect.right - tabRect.left
1871 ) <= scrollRect.width
1873 this.arrowScrollbox.ensureElementIsVisible(tabToScrollIntoView);
1877 this.arrowScrollbox.scrollByPixels(
1879 ? selectedRect.right - scrollRect.right
1880 : selectedRect.left - scrollRect.left
1884 if (!this._animateElement.hasAttribute("highlight")) {
1885 this._animateElement.toggleAttribute("highlight", true);
1888 ele.removeAttribute("highlight");
1891 this._animateElement
1899 * Returns the tab where an event happened, or null if it didn't occur on a tab.
1901 * @param {Event} event
1902 * The event for which we want to know on which tab it happened.
1903 * @param {object} options
1904 * @param {boolean} options.ignoreTabSides
1905 * If set to true: events will only be associated with a tab if they happened
1906 * on its central part (from 25% to 75%); if they happened on the left or right
1907 * sides of the tab, the method will return null.
1909 _getDragTargetTab(event, { ignoreTabSides = false } = {}) {
1910 let { target } = event;
1911 if (target.nodeType != Node.ELEMENT_NODE) {
1912 target = target.parentElement;
1914 let tab = target?.closest("tab");
1915 if (tab && ignoreTabSides) {
1916 let { width } = tab.getBoundingClientRect();
1918 event.screenX < tab.screenX + width * 0.25 ||
1919 event.screenX > tab.screenX + width * 0.75
1927 _getDropIndex(event) {
1928 let tab = this._getDragTargetTab(event);
1930 return this.allTabs.length;
1932 let middle = tab.screenX + tab.getBoundingClientRect().width / 2;
1933 let isBeforeMiddle = RTL_UI
1934 ? event.screenX > middle
1935 : event.screenX < middle;
1936 return tab._tPos + (isBeforeMiddle ? 0 : 1);
1939 getDropEffectForTabDrag(event) {
1940 var dt = event.dataTransfer;
1942 let isMovingTabs = dt.mozItemCount > 0;
1943 for (let i = 0; i < dt.mozItemCount; i++) {
1944 // tabs are always added as the first type
1945 let types = dt.mozTypesAt(0);
1946 if (types[0] != TAB_DROP_TYPE) {
1947 isMovingTabs = false;
1953 let sourceNode = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
1955 XULElement.isInstance(sourceNode) &&
1956 sourceNode.localName == "tab" &&
1957 sourceNode.ownerGlobal.isChromeWindow &&
1958 sourceNode.ownerDocument.documentElement.getAttribute("windowtype") ==
1959 "navigator:browser" &&
1960 sourceNode.ownerGlobal.gBrowser.tabContainer == sourceNode.container
1962 // Do not allow transfering a private tab to a non-private window
1965 PrivateBrowsingUtils.isWindowPrivate(window) !=
1966 PrivateBrowsingUtils.isWindowPrivate(sourceNode.ownerGlobal)
1972 window.gMultiProcessBrowser !=
1973 sourceNode.ownerGlobal.gMultiProcessBrowser
1979 window.gFissionBrowser != sourceNode.ownerGlobal.gFissionBrowser
1984 return dt.dropEffect == "copy" ? "copy" : "move";
1988 if (browserDragAndDrop.canDropLink(event)) {
1994 _handleNewTab(tab) {
1995 if (tab.container != this) {
1998 tab._fullyOpen = true;
1999 gBrowser.tabAnimationsInProgress--;
2001 this._updateCloseButtons();
2003 if (tab.hasAttribute("selected")) {
2004 this._handleTabSelect();
2005 } else if (!tab.hasAttribute("skipbackgroundnotify")) {
2006 this._notifyBackgroundTab(tab);
2009 // XXXmano: this is a temporary workaround for bug 345399
2010 // We need to manually update the scroll buttons disabled state
2011 // if a tab was inserted to the overflow area or removed from it
2012 // without any scrolling and when the tabbar has already
2014 this.arrowScrollbox._updateScrollButtonsDisabledState();
2016 // If this browser isn't lazy (indicating it's probably created by
2017 // session restore), preload the next about:newtab if we don't
2018 // already have a preloaded browser.
2019 if (tab.linkedPanel) {
2020 NewTabPagePreloading.maybeCreatePreloadedBrowser(window);
2023 if (UserInteraction.running("browser.tabs.opening", window)) {
2024 UserInteraction.finish("browser.tabs.opening", window);
2028 _canAdvanceToTab(aTab) {
2029 return !aTab.closing;
2033 * Returns the panel associated with a tab if it has a connected browser
2034 * and/or it is the selected tab.
2035 * For background lazy browsers, this will return null.
2037 getRelatedElement(aTab) {
2042 // Cannot access gBrowser before it's initialized.
2043 if (!gBrowser._initialized) {
2044 return this.tabbox.tabpanels.firstElementChild;
2047 // If the tab's browser is lazy, we need to `_insertBrowser` in order
2048 // to have a linkedPanel. This will also serve to bind the browser
2049 // and make it ready to use. We only do this if the tab is selected
2050 // because otherwise, callers might end up unintentionally binding the
2051 // browser for lazy background tabs.
2052 if (!aTab.linkedPanel) {
2053 if (!aTab.selected) {
2056 gBrowser._insertBrowser(aTab);
2058 return document.getElementById(aTab.linkedPanel);
2061 _updateNewTabVisibility() {
2062 // Helper functions to help deal with customize mode wrapping some items
2064 n.parentNode.localName == "toolbarpaletteitem" ? n.parentNode : n;
2066 n && n.localName == "toolbarpaletteitem" ? n.firstElementChild : n;
2068 // Starting from the tabs element, find the next sibling that:
2069 // - isn't hidden; and
2070 // - isn't the all-tabs button.
2071 // If it's the new tab button, consider the new tab button adjacent to the tabs.
2072 // If the new tab button is marked as adjacent and the tabstrip doesn't
2073 // overflow, we'll display the 'new tab' button inline in the tabstrip.
2074 // In all other cases, the separate new tab button is displayed in its
2075 // customized location.
2078 sib = unwrap(wrap(sib).nextElementSibling);
2079 } while (sib && (sib.hidden || sib.id == "alltabs-button"));
2081 this.toggleAttribute(
2082 "hasadjacentnewtabbutton",
2083 sib && sib.id == "new-tab-button"
2087 onWidgetAfterDOMChange(aNode, aNextNode, aContainer) {
2089 aContainer.ownerDocument == document &&
2090 aContainer.id == "TabsToolbar-customization-target"
2092 this._updateNewTabVisibility();
2096 onAreaNodeRegistered(aArea, aContainer) {
2097 if (aContainer.ownerDocument == document && aArea == "TabsToolbar") {
2098 this._updateNewTabVisibility();
2102 onAreaReset(aArea, aContainer) {
2103 this.onAreaNodeRegistered(aArea, aContainer);
2106 _hiddenSoundPlayingStatusChanged(tab, opts) {
2107 let closed = opts && opts.closed;
2108 if (!closed && tab.soundPlaying && tab.hidden) {
2109 this._hiddenSoundPlayingTabs.add(tab);
2110 this.toggleAttribute("hiddensoundplaying", true);
2112 this._hiddenSoundPlayingTabs.delete(tab);
2113 if (this._hiddenSoundPlayingTabs.size == 0) {
2114 this.removeAttribute("hiddensoundplaying");
2120 if (this.boundObserve) {
2121 Services.prefs.removeObserver("privacy.userContext", this.boundObserve);
2123 CustomizableUI.removeListener(this);
2126 updateTabIndicatorAttr(tab) {
2127 const theseAttributes = ["soundplaying", "muted", "activemedia-blocked"];
2128 const notTheseAttributes = ["pinned", "sharing", "crashed"];
2130 if (notTheseAttributes.some(attr => tab.hasAttribute(attr))) {
2131 tab.removeAttribute("indicator-replaces-favicon");
2135 tab.toggleAttribute(
2136 "indicator-replaces-favicon",
2137 theseAttributes.some(attr => tab.hasAttribute(attr))
2142 customElements.define("tabbrowser-tabs", MozTabbrowserTabs, {