Bug 1864861: part 2) Remove `aIsPreload` argument from `FontLoaderUtils::BuildChannel...
[gecko.git] / browser / base / content / tabbrowser-tabs.js
blob71846eca963d0ba652c60106a80850fd936db2d9
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 */
7 "use strict";
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 {
13     constructor() {
14       super();
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);
33     }
35     init() {
36       this.arrowScrollbox = this.querySelector("arrowscrollbox");
37       this.arrowScrollbox.addEventListener("wheel", this, true);
39       this.baseConnect();
41       this._blockDblClick = false;
42       this._tabDropIndicator = this.querySelector(".tab-drop-indicator");
43       this._dragOverDelay = 350;
44       this._dragTime = 0;
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"
56       );
57       this._hiddenSoundPlayingTabs = new Set();
58       this._allTabs = null;
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"
68       );
69       this.toggleAttribute(
70         "secondarytext-unsupported",
71         unsupportedLocales.split(",").includes(language.split("-")[0])
72       );
74       this.newTabButton.setAttribute(
75         "aria-label",
76         GetDynamicShortcutTooltipText("tabs-newtab-button")
77       );
79       let handleResize = () => {
80         this._updateCloseButtons();
81         this._handleTabSelect(true);
82       };
83       window.addEventListener("resize", handleResize);
84       this._fullscreenMutationObserver = new MutationObserver(handleResize);
85       this._fullscreenMutationObserver.observe(document.documentElement, {
86         attributeFilter: ["inFullscreen", "inDOMFullscreen"],
87       });
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(
94         this,
95         "_tabMinWidthPref",
96         "browser.tabs.tabMinWidth",
97         null,
98         (pref, prevValue, newValue) => (this._tabMinWidth = newValue),
99         newValue => {
100           const LIMIT = 50;
101           return Math.max(newValue, LIMIT);
102         }
103       );
105       this._tabMinWidth = this._tabMinWidthPref;
107       CustomizableUI.addListener(this);
108       this._updateNewTabVisibility();
109       this._initializeArrowScrollbox();
111       XPCOMUtils.defineLazyPreferenceGetter(
112         this,
113         "_closeTabByDblclick",
114         "browser.tabs.closeTabByDblclick",
115         false
116       );
118       if (gMultiProcessBrowser) {
119         this.tabbox.tabpanels.setAttribute("async", "true");
120       }
121     }
123     on_TabSelect(event) {
124       this._handleTabSelect();
125     }
127     on_TabClose(event) {
128       this._hiddenSoundPlayingStatusChanged(event.target, { closed: true });
129     }
131     on_TabAttrModified(event) {
132       if (
133         ["soundplaying", "muted", "activemedia-blocked", "sharing"].some(attr =>
134           event.detail.changed.includes(attr)
135         )
136       ) {
137         this.updateTabIndicatorAttr(event.target);
138       }
140       if (
141         event.detail.changed.includes("soundplaying") &&
142         event.target.hidden
143       ) {
144         this._hiddenSoundPlayingStatusChanged(event.target);
145       }
146     }
148     on_TabHide(event) {
149       if (event.target.soundPlaying) {
150         this._hiddenSoundPlayingStatusChanged(event.target);
151       }
152     }
154     on_TabShow(event) {
155       if (event.target.soundPlaying) {
156         this._hiddenSoundPlayingStatusChanged(event.target);
157       }
158     }
160     on_TabPinned(event) {
161       this.updateTabIndicatorAttr(event.target);
162     }
164     on_TabUnpinned(event) {
165       this.updateTabIndicatorAttr(event.target);
166     }
168     on_transitionend(event) {
169       if (event.propertyName != "max-width") {
170         return;
171       }
173       let tab = event.target ? event.target.closest("tab") : null;
175       if (tab.hasAttribute("fadein")) {
176         if (tab._fullyOpen) {
177           this._updateCloseButtons();
178         } else {
179           this._handleNewTab(tab);
180         }
181       } else if (tab.closing) {
182         gBrowser._endRemoveTab(tab);
183       }
185       let evt = new CustomEvent("TabAnimationEnd", { bubbles: true });
186       tab.dispatchEvent(evt);
187     }
189     on_dblclick(event) {
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) {
194         return;
195       }
197       if (event.button != 0 || event.originalTarget.localName != "scrollbox") {
198         return;
199       }
201       if (!this._blockDblClick) {
202         BrowserOpenTab();
203       }
205       event.preventDefault();
206     }
208     on_click(event) {
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)
214          *
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.
225          *
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.
229          */
230         let target = event.originalTarget;
231         if (target.classList.contains("tab-close-button")) {
232           // We preemptively set this to allow the closing-multiple-tabs-
233           // in-a-row case.
234           if (this._blockDblClick) {
235             target._ignoredCloseButtonClicks = true;
236           } else if (event.detail > 1 && !target._ignoredCloseButtonClicks) {
237             target._ignoredCloseButtonClicks = true;
238             event.stopPropagation();
239             return;
240           } else {
241             // Reset the "ignored click" flag
242             target._ignoredCloseButtonClicks = false;
243           }
244         }
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.
252          */
253         if (this._blockDblClick) {
254           if (!("_clickedTabBarOnce" in this)) {
255             this._clickedTabBarOnce = true;
256             return;
257           }
258           delete this._clickedTabBarOnce;
259           this._blockDblClick = false;
260         }
261       } else if (
262         event.eventPhase == Event.BUBBLING_PHASE &&
263         event.button == 1
264       ) {
265         let tab = event.target ? event.target.closest("tab") : null;
266         if (tab) {
267           if (tab.multiselected) {
268             gBrowser.removeMultiSelectedTabs();
269           } else {
270             gBrowser.removeTab(tab, {
271               animate: true,
272               triggeringEvent: event,
273             });
274           }
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;
281           let endOfTab =
282             winUtils.getBoundsWithoutFlushing(lastTab)[
283               RTL_UI ? "left" : "right"
284             ];
285           if (
286             (!RTL_UI && event.clientX > endOfTab) ||
287             (RTL_UI && event.clientX < endOfTab)
288           ) {
289             BrowserOpenTab();
290           }
291         } else {
292           return;
293         }
295         event.preventDefault();
296         event.stopPropagation();
297       }
298     }
300     on_keydown(event) {
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) {
311         return;
312       }
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);
321       }
322       let lastFocusedTabIndex = focusedTabIndex;
323       switch (event.keyCode) {
324         case KeyEvent.DOM_VK_UP:
325           if (keyComboForMove) {
326             gBrowser.moveTabBackward();
327           } else {
328             focusedTabIndex--;
329           }
330           break;
331         case KeyEvent.DOM_VK_DOWN:
332           if (keyComboForMove) {
333             gBrowser.moveTabForward();
334           } else {
335             focusedTabIndex++;
336           }
337           break;
338         case KeyEvent.DOM_VK_RIGHT:
339         case KeyEvent.DOM_VK_LEFT:
340           if (keyComboForMove) {
341             gBrowser.moveTabOver(event);
342           } else if (
343             (!RTL_UI && event.keyCode == KeyEvent.DOM_VK_RIGHT) ||
344             (RTL_UI && event.keyCode == KeyEvent.DOM_VK_LEFT)
345           ) {
346             focusedTabIndex++;
347           } else {
348             focusedTabIndex--;
349           }
350           break;
351         case KeyEvent.DOM_VK_HOME:
352           if (keyComboForMove) {
353             gBrowser.moveTabToStart();
354           } else {
355             focusedTabIndex = 0;
356           }
357           break;
358         case KeyEvent.DOM_VK_END:
359           if (keyComboForMove) {
360             gBrowser.moveTabToEnd();
361           } else {
362             focusedTabIndex = visibleTabs.length - 1;
363           }
364           break;
365         case KeyEvent.DOM_VK_SPACE:
366           if (visibleTabs[lastFocusedTabIndex].multiselected) {
367             gBrowser.removeFromMultiSelectedTabs(
368               visibleTabs[lastFocusedTabIndex]
369             );
370           } else {
371             gBrowser.addToMultiSelectedTabs(visibleTabs[lastFocusedTabIndex]);
372           }
373           break;
374         default:
375           // Consume the keydown event for the above keyboard
376           // shortcuts only.
377           return;
378       }
380       if (arrowKeysShouldWrap) {
381         if (focusedTabIndex >= visibleTabs.length) {
382           focusedTabIndex = 0;
383         } else if (focusedTabIndex < 0) {
384           focusedTabIndex = visibleTabs.length - 1;
385         }
386       } else {
387         focusedTabIndex = Math.min(
388           visibleTabs.length - 1,
389           Math.max(0, focusedTabIndex)
390         );
391       }
393       if (keyComboForFocus && focusedTabIndex != lastFocusedTabIndex) {
394         this.ariaFocusedItem = visibleTabs[focusedTabIndex];
395       }
397       event.preventDefault();
398     }
400     on_dragstart(event) {
401       var tab = this._getDragTargetTab(event);
402       if (!tab || this._isCustomizing) {
403         return;
404       }
406       this.startTabDrag(event, tab);
407     }
409     startTabDrag(event, tab, { fromTabList = false } = {}) {
410       let selectedTabs = gBrowser.selectedTabs;
411       let otherSelectedTabs = selectedTabs.filter(
412         selectedTab => selectedTab != tab
413       );
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"
426         dt.mozSetDataAt(
427           "text/x-moz-text-internal",
428           dtBrowser.currentURI.spec,
429           i
430         );
431       }
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.
438       dt.addElement(tab);
440       if (tab.multiselected) {
441         this._groupSelectedTabs(tab);
442       }
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;
450       if (!canvas) {
451         this._dndCanvas = canvas = document.createElementNS(
452           "http://www.w3.org/1999/xhtml",
453           "canvas"
454         );
455         canvas.style.width = "100%";
456         canvas.style.height = "100%";
457         canvas.mozOpaque = true;
458       }
460       canvas.width = 160 * scale;
461       canvas.height = 90 * scale;
462       let toDrag = canvas;
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);
470         let captureListener;
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);
477           };
478         } else {
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",
489               "div"
490             );
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);
496           }
497           toDrag = this._dndPanel;
498         }
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));
504       } else {
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 =>
508           console.error(e)
509         );
510         dragImageOffset = dragImageOffset * scale;
511       }
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;
520       }
521       let tabOffsetX = clientX(tab) - clientX(this);
522       tab._dragData = {
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
529         ),
530         fromTabList,
531       };
533       event.stopPropagation();
535       if (fromTabList) {
536         Services.telemetry.scalarAdd(
537           "browser.ui.interaction.all_tabs_panel_dragstart_tab_event_count",
538           1
539         );
540       }
541     }
543     on_dragover(event) {
544       var effects = this.getDropEffectForTabDrag(event);
546       var ind = this._tabDropIndicator;
547       if (effects == "" || effects == "none") {
548         ind.hidden = true;
549         return;
550       }
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;
564             break;
565           case arrowScrollbox._scrollButtonDown:
566             pixelsToScroll = arrowScrollbox.scrollIncrement;
567             break;
568         }
569         if (pixelsToScroll) {
570           arrowScrollbox.scrollByPixels(
571             (RTL_UI ? -1 : 1) * pixelsToScroll,
572             true
573           );
574         }
575       }
577       let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
578       if (
579         (effects == "move" || effects == "copy") &&
580         this == draggedTab.container &&
581         !draggedTab._dragData.fromTabList
582       ) {
583         ind.hidden = true;
585         if (!this._isGroupTabsAnimationOver()) {
586           // Wait for grouping tabs animation to finish
587           return;
588         }
589         this._finishGroupSelectedTabs(draggedTab);
591         if (effects == "move") {
592           this._animateTabMove(event);
593           return;
594         }
595       }
597       this._finishAnimateTabMove();
599       if (effects == "link") {
600         let tab = this._getDragTargetTab(event, { ignoreTabSides: true });
601         if (tab) {
602           if (!this._dragTime) {
603             this._dragTime = Date.now();
604           }
605           if (Date.now() >= this._dragTime + this._dragOverDelay) {
606             this.selectedItem = tab;
607           }
608           ind.hidden = true;
609           return;
610         }
611       }
613       var rect = arrowScrollbox.getBoundingClientRect();
614       var newMargin;
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,
622           scrollRect.right
623         );
624         if (RTL_UI) {
625           [minMargin, maxMargin] = [
626             this.clientWidth - maxMargin,
627             this.clientWidth - minMargin,
628           ];
629         }
630         newMargin = pixelsToScroll > 0 ? maxMargin : minMargin;
631       } else {
632         let newIndex = this._getDropIndex(event);
633         let children = this.allTabs;
634         if (newIndex == children.length) {
635           let tabRect = this._getVisibleTabs().at(-1).getBoundingClientRect();
636           if (RTL_UI) {
637             newMargin = rect.right - tabRect.left;
638           } else {
639             newMargin = tabRect.right - rect.left;
640           }
641         } else {
642           let tabRect = children[newIndex].getBoundingClientRect();
643           if (RTL_UI) {
644             newMargin = rect.right - tabRect.right;
645           } else {
646             newMargin = tabRect.left - rect.left;
647           }
648         }
649       }
651       ind.hidden = false;
652       newMargin += ind.clientWidth / 2;
653       if (RTL_UI) {
654         newMargin *= -1;
655       }
656       ind.style.transform = "translate(" + Math.round(newMargin) + "px)";
657     }
659     on_drop(event) {
660       var dt = event.dataTransfer;
661       var dropEffect = dt.dropEffect;
662       var draggedTab;
663       let movingTabs;
664       if (dt.mozTypesAt(0)[0] == TAB_DROP_TYPE) {
665         // tab copy or move
666         draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
667         // not our drop then
668         if (!draggedTab) {
669           return;
670         }
671         movingTabs = draggedTab._dragData.movingTabs;
672         draggedTab.container._finishGroupSelectedTabs(draggedTab);
673       }
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);
680         let draggedTabCopy;
681         for (let tab of movingTabs) {
682           let newTab = gBrowser.duplicateTab(tab);
683           gBrowser.moveTabTo(newTab, newIndex++);
684           if (tab == draggedTab) {
685             draggedTabCopy = newTab;
686           }
687         }
688         if (draggedTab.container != this || event.shiftKey) {
689           this.selectedItem = draggedTabCopy;
690         }
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;
700         }
702         let dropIndex;
703         if (draggedTab._dragData.fromTabList) {
704           dropIndex = this._getDropIndex(event);
705         } else {
706           dropIndex =
707             "animDropIndex" in draggedTab._dragData &&
708             draggedTab._dragData.animDropIndex;
709         }
710         let incrementDropIndex = true;
711         if (dropIndex && dropIndex > movingTabs[0]._tPos) {
712           dropIndex--;
713           incrementDropIndex = false;
714         }
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) {
727                   dropIndex++;
728                 }
729               }
731               gBrowser.syncThrobberAnimations(tab);
732             };
733             if (gReduceMotion) {
734               postTransitionCleanup();
735             } else {
736               let onTransitionEnd = transitionendEvent => {
737                 if (
738                   transitionendEvent.propertyName != "transform" ||
739                   transitionendEvent.originalTarget != tab
740                 ) {
741                   return;
742                 }
743                 tab.removeEventListener("transitionend", onTransitionEnd);
745                 postTransitionCleanup();
746               };
747               tab.addEventListener("transitionend", onTransitionEnd);
748             }
749           }
750         } else {
751           this._finishAnimateTabMove();
752           if (dropIndex !== false) {
753             for (let tab of movingTabs) {
754               gBrowser.moveTabTo(tab, dropIndex);
755               if (incrementDropIndex) {
756                 dropIndex++;
757               }
758             }
759           }
760         }
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;
766         let selectedTab;
767         let indexForSelectedTab;
768         for (let i = 0; i < movingTabs.length; ++i) {
769           const tab = movingTabs[i];
770           if (tab.selected) {
771             selectedTab = tab;
772             indexForSelectedTab = newIndex;
773           } else {
774             const newTab = gBrowser.adoptTab(tab, newIndex, tab == draggedTab);
775             if (newTab) {
776               ++newIndex;
777             }
778           }
779         }
780         if (selectedTab) {
781           const newTab = gBrowser.adoptTab(
782             selectedTab,
783             indexForSelectedTab,
784             selectedTab == draggedTab
785           );
786           if (newTab) {
787             ++newIndex;
788           }
789         }
791         // Restore tab selection
792         gBrowser.addRangeToMultiSelectedTabs(
793           gBrowser.tabs[dropIndex],
794           gBrowser.tabs[newIndex - 1]
795         );
796       } else {
797         // Pass true to disallow dropping javascript: or data: urls
798         let links;
799         try {
800           links = browserDragAndDrop.dropLinks(event, true);
801         } catch (ex) {}
803         if (!links || links.length === 0) {
804           return;
805         }
807         let inBackground = Services.prefs.getBoolPref(
808           "browser.tabs.loadInBackground"
809         );
810         if (event.shiftKey) {
811           inBackground = !inBackground;
812         }
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);
823         (async () => {
824           if (
825             urls.length >=
826             Services.prefs.getIntPref("browser.tabs.maxOpenBeforeWarn")
827           ) {
828             // Sync dialog cannot be used inside drop event handler.
829             let answer = await OpenInTabsUtils.promiseConfirmOpenInTabs(
830               urls.length,
831               window
832             );
833             if (!answer) {
834               return;
835             }
836           }
838           gBrowser.loadTabs(urls, {
839             inBackground,
840             replace,
841             allowThirdPartyFixup: true,
842             targetTab,
843             newIndex,
844             userContextId,
845             triggeringPrincipal,
846             csp,
847           });
848         })();
849       }
851       if (draggedTab) {
852         delete draggedTab._dragData;
853       }
854     }
856     on_dragend(event) {
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")) {
864         return;
865       }
867       this._finishGroupSelectedTabs(draggedTab);
868       this._finishAnimateTabMove();
870       if (
871         dt.mozUserCancelled ||
872         dt.dropEffect != "none" ||
873         this._isCustomizing
874       ) {
875         delete draggedTab._dragData;
876         return;
877       }
879       // Check if tab detaching is enabled
880       if (!Services.prefs.getBoolPref("browser.tabs.allowTabDetach")) {
881         return;
882       }
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(
893           this.arrowScrollbox
894         );
895         let detachTabThresholdY = window.screenY + rect.top + 1.5 * rect.height;
896         if (eY < detachTabThresholdY && eY > window.screenY) {
897           return;
898         }
899       }
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;
904       var availX = {},
905         availY = {},
906         availWidth = {},
907         availHeight = {};
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,
929         availWidth
930       );
931       var winHeight = Math.min(
932         window.outerHeight * screenCssToDesktopScale,
933         availHeight
934       );
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
939       // than our scale.
940       var left = Math.min(
941         Math.max(
942           eX * ourCssToDesktopScale -
943             draggedTab._dragData.offsetX * screenCssToDesktopScale,
944           availX
945         ),
946         availX + availWidth - winWidth
947       );
948       var top = Math.min(
949         Math.max(
950           eY * ourCssToDesktopScale -
951             draggedTab._dragData.offsetY * screenCssToDesktopScale,
952           availY
953         ),
954         availY + availHeight - winHeight
955       );
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.
967         //
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
971         // below...
972         winWidth /= ourCssToDesktopScale;
973         winHeight /= ourCssToDesktopScale;
975         window.resizeTo(winWidth, winHeight);
976         window.moveTo(left, top);
977         window.focus();
978       } else {
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;
988         }
989         gBrowser.replaceTabsWithWindow(draggedTab, props);
990       }
991       event.stopPropagation();
992     }
994     on_dragleave(event) {
995       this._dragTime = 0;
997       // This does not work at all (see bug 458613)
998       var target = event.relatedTarget;
999       while (target && target != this) {
1000         target = target.parentNode;
1001       }
1002       if (target) {
1003         return;
1004       }
1006       this._tabDropIndicator.hidden = true;
1007       event.stopPropagation();
1008     }
1010     on_wheel(event) {
1011       if (
1012         Services.prefs.getBoolPref("toolkit.tabbox.switchByScrolling", false)
1013       ) {
1014         event.stopImmediatePropagation();
1015       }
1016     }
1018     get emptyTabTitle() {
1019       // Normal tab title is used also in the permanent private browsing mode.
1020       const l10nId =
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);
1026     }
1028     get tabbox() {
1029       return document.getElementById("tabbrowser-tabbox");
1030     }
1032     get newTabButton() {
1033       return this.querySelector("#tabs-newtab-button");
1034     }
1036     // Accessor for tabs.  arrowScrollbox has a container for non-tab elements
1037     // at the end, everything else is <tab>s.
1038     get allTabs() {
1039       if (this._allTabs) {
1040         return this._allTabs;
1041       }
1042       let children = Array.from(this.arrowScrollbox.children);
1043       children.pop();
1044       this._allTabs = children;
1045       return children;
1046     }
1048     _getVisibleTabs() {
1049       if (!this._visibleTabs) {
1050         this._visibleTabs = Array.prototype.filter.call(
1051           this.allTabs,
1052           tab => !tab.hidden && !tab.closing
1053         );
1054       }
1055       return this._visibleTabs;
1056     }
1058     _invalidateCachedTabs() {
1059       this._allTabs = null;
1060       this._visibleTabs = null;
1061     }
1063     _invalidateCachedVisibleTabs() {
1064       this._visibleTabs = null;
1065     }
1067     appendChild(tab) {
1068       return this.insertBefore(tab, null);
1069     }
1071     insertBefore(tab, node) {
1072       if (!this.arrowScrollbox) {
1073         throw new Error("Shouldn't call this without arrowscrollbox");
1074       }
1076       let { arrowScrollbox } = this;
1077       if (node == null) {
1078         // We have a container for non-tab elements at the end of the scrollbox.
1079         node = arrowScrollbox.lastChild;
1080       }
1081       return arrowScrollbox.insertBefore(tab, node);
1082     }
1084     set _tabMinWidth(val) {
1085       this.style.setProperty("--tab-min-width", val + "px");
1086     }
1088     get _isCustomizing() {
1089       return document.documentElement.getAttribute("customizing") == "true";
1090     }
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);
1098       }
1099     }
1101     _initializeArrowScrollbox() {
1102       let arrowScrollbox = this.arrowScrollbox;
1103       arrowScrollbox.shadowRoot.addEventListener(
1104         "underflow",
1105         event => {
1106           // Ignore underflow events:
1107           // - from nested scrollable elements
1108           // - for vertical orientation
1109           // - corresponding to an overflow event that we ignored
1110           if (
1111             event.originalTarget != arrowScrollbox.scrollbox ||
1112             event.detail == 0 ||
1113             !this.hasAttribute("overflow")
1114           ) {
1115             return;
1116           }
1118           this.removeAttribute("overflow");
1120           if (this._lastTabClosedByMouse) {
1121             this._expandSpacerBy(this._scrollButtonWidth);
1122           }
1124           for (let tab of gBrowser._removingTabs) {
1125             gBrowser.removeTab(tab);
1126           }
1128           this._positionPinnedTabs();
1129           this._updateCloseButtons();
1130         },
1131         true
1132       );
1134       arrowScrollbox.shadowRoot.addEventListener("overflow", event => {
1135         // Ignore overflow events:
1136         // - from nested scrollable elements
1137         // - for vertical orientation
1138         if (
1139           event.originalTarget != arrowScrollbox.scrollbox ||
1140           event.detail == 0
1141         ) {
1142           return;
1143         }
1145         this.toggleAttribute("overflow", true);
1146         this._positionPinnedTabs();
1147         this._updateCloseButtons();
1148         this._handleTabSelect(true);
1149       });
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);
1155       };
1156       arrowScrollbox._canScrollToElement = tab => {
1157         return !tab._pinnedUnscrollable && !tab.hidden;
1158       };
1159     }
1161     observe(aSubject, aTopic, aData) {
1162       switch (aTopic) {
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"
1174           );
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]) {
1183             if (!parent) {
1184               continue;
1185             }
1187             parent.removeAttribute("type");
1188             if (parent.menupopup) {
1189               parent.menupopup.remove();
1190             }
1192             if (containersEnabled) {
1193               parent.setAttribute("context", "new-tab-button-popup");
1195               let popup = document
1196                 .getElementById("new-tab-button-popup")
1197                 .cloneNode(true);
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";
1207             } else {
1208               nodeToTooltipMap[parent.id] = "newTabButton.tooltip";
1209               parent.removeAttribute("context", "new-tab-button-popup");
1210             }
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);
1218             } else {
1219               gClickAndHoldListenersOnElement.remove(parent);
1220             }
1221           }
1223           break;
1224       }
1225     }
1227     _updateCloseButtons() {
1228       // If we're overflowing, tabs are at their minimum widths.
1229       if (this.hasAttribute("overflow")) {
1230         this.setAttribute("closebuttons", "activetab");
1231         return;
1232       }
1234       if (this._closeButtonsUpdatePending) {
1235         return;
1236       }
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");
1249             return;
1250           }
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.
1255           let rect = ele => {
1256             return window.windowUtils.getBoundsWithoutFlushing(ele);
1257           };
1258           let tab = this._getVisibleTabs()[gBrowser._numPinnedTabs];
1259           if (tab && rect(tab).width <= this._tabClipWidth) {
1260             this.setAttribute("closebuttons", "activetab");
1261           } else {
1262             this.removeAttribute("closebuttons");
1263           }
1264         });
1265       });
1266     }
1268     _updateHiddenTabsStatus() {
1269       this.toggleAttribute(
1270         "hashiddentabs",
1271         gBrowser.visibleTabs.length < gBrowser.tabs.length
1272       );
1273     }
1275     _handleTabSelect(aInstant) {
1276       let selectedTab = this.selectedItem;
1277       if (this.hasAttribute("overflow")) {
1278         this.arrowScrollbox.ensureElementIsVisible(selectedTab, aInstant);
1279       }
1281       selectedTab._notselectedsinceload = false;
1282     }
1284     /**
1285      * Try to keep the active tab's close button under the mouse cursor
1286      */
1287     _lockTabSizing(aTab, aTabWidth) {
1288       let tabs = this._getVisibleTabs();
1289       if (!tabs.length) {
1290         return;
1291       }
1293       var isEndTab = aTab._tPos > tabs[tabs.length - 1]._tPos;
1295       if (!this._tabDefaultMaxWidth) {
1296         this._tabDefaultMaxWidth = parseFloat(
1297           window.getComputedStyle(aTab).maxWidth
1298         );
1299       }
1300       this._lastTabClosedByMouse = true;
1301       this._scrollButtonWidth = window.windowUtils.getBoundsWithoutFlushing(
1302         this.arrowScrollbox._scrollButtonDown
1303       ).width;
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) {
1309           return;
1310         }
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.
1314         if (aTab.owner) {
1315           return;
1316         }
1317         this._expandSpacerBy(aTabWidth);
1318       } else {
1319         // non-overflow mode
1320         // Locking is neither in effect nor needed, so let tabs expand normally.
1321         if (isEndTab && !this._hasTabTempMaxWidth) {
1322           return;
1323         }
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.
1328         if (isEndTab) {
1329           let numNormalTabs = tabs.length - numPinned;
1330           aTabWidth = (aTabWidth * (numNormalTabs + 1)) / numNormalTabs;
1331           if (aTabWidth > this._tabDefaultMaxWidth) {
1332             aTabWidth = this._tabDefaultMaxWidth;
1333           }
1334         }
1335         aTabWidth += "px";
1336         let tabsToReset = [];
1337         for (let i = numPinned; i < tabs.length; i++) {
1338           let tab = tabs[i];
1339           tab.style.setProperty("max-width", aTabWidth, "important");
1340           if (!isEndTab) {
1341             // keep tabs the same width
1342             tab.style.transition = "none";
1343             tabsToReset.push(tab);
1344           }
1345         }
1347         if (tabsToReset.length) {
1348           window
1349             .promiseDocumentFlushed(() => {})
1350             .then(() => {
1351               window.requestAnimationFrame(() => {
1352                 for (let tab of tabsToReset) {
1353                   tab.style.transition = "";
1354                 }
1355               });
1356             });
1357         }
1359         this._hasTabTempMaxWidth = true;
1360         gBrowser.addEventListener("mousemove", this);
1361         window.addEventListener("mouseout", this);
1362       }
1363     }
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);
1371     }
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 = "";
1382         }
1383       }
1385       if (this.hasAttribute("using-closing-tabs-spacer")) {
1386         this.removeAttribute("using-closing-tabs-spacer");
1387         this._closingTabsSpacer.style.width = 0;
1388       }
1389     }
1391     uiDensityChanged() {
1392       this._positionPinnedTabs();
1393       this._updateCloseButtons();
1394       this._handleTabSelect(true);
1395     }
1397     _positionPinnedTabs() {
1398       let tabs = this._getVisibleTabs();
1399       let numPinned = gBrowser._numPinnedTabs;
1400       let doPosition =
1401         this.hasAttribute("overflow") &&
1402         tabs.length > numPinned &&
1403         numPinned > 0;
1405       this.toggleAttribute("haspinnedtabs", !!numPinned);
1406       this.toggleAttribute("positionpinnedtabs", doPosition);
1408       if (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 = {
1414             uiDensity,
1415             pinnedTabWidth: tabs[0].getBoundingClientRect().width,
1416             scrollStartOffset:
1417               arrowScrollbox.scrollbox.getBoundingClientRect().left -
1418               arrowScrollbox.getBoundingClientRect().left +
1419               parseFloat(
1420                 getComputedStyle(arrowScrollbox.scrollbox).paddingInlineStart
1421               ),
1422           };
1423         }
1425         let width = 0;
1426         for (let i = numPinned - 1; i >= 0; i--) {
1427           let tab = tabs[i];
1428           width += layoutData.pinnedTabWidth;
1429           tab.style.setProperty(
1430             "margin-inline-start",
1431             -(width + layoutData.scrollStartOffset) + "px",
1432             "important"
1433           );
1434           tab._pinnedUnscrollable = true;
1435         }
1436         this.style.setProperty(
1437           "--tab-overflow-pinned-tabs-width",
1438           width + "px"
1439         );
1440       } else {
1441         for (let i = 0; i < numPinned; i++) {
1442           let tab = tabs[i];
1443           tab.style.marginInlineStart = "";
1444           tab._pinnedUnscrollable = false;
1445         }
1447         this.style.removeProperty("--tab-overflow-pinned-tabs-width");
1448       }
1450       if (this._lastNumPinned != numPinned) {
1451         this._lastNumPinned = numPinned;
1452         this._handleTabSelect(true);
1453       }
1454     }
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;
1465         }
1466       }
1468       if (!("animLastScreenX" in draggedTab._dragData)) {
1469         draggedTab._dragData.animLastScreenX = draggedTab._dragData.screenX;
1470       }
1472       let screenX = event.screenX;
1473       if (screenX == draggedTab._dragData.animLastScreenX) {
1474         return;
1475       }
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
1487       );
1489       if (RTL_UI) {
1490         tabs.reverse();
1491         // Copy moving tabs array to avoid infinite reversing.
1492         movingTabs = [...movingTabs].reverse();
1493       }
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;
1505       if (!pinned) {
1506         translateX +=
1507           this.arrowScrollbox.scrollbox.scrollLeft -
1508           draggedTab._dragData.scrollX;
1509       }
1510       let leftBound = leftTab.screenX - leftMovingTabScreenX;
1511       let rightBound =
1512         rightTab.screenX +
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)";
1519       }
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;
1539       let newIndex = -1;
1540       let oldIndex =
1541         "animDropIndex" in draggedTab._dragData
1542           ? draggedTab._dragData.animDropIndex
1543           : movingTabs[0]._tPos;
1544       let low = 0;
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) {
1549           break;
1550         }
1551         screenX = tabs[mid].screenX + getTabShift(tabs[mid], oldIndex);
1552         if (screenX > tabCenter) {
1553           high = mid - 1;
1554         } else if (
1555           screenX + tabs[mid].getBoundingClientRect().width <
1556           tabCenter
1557         ) {
1558           low = mid + 1;
1559         } else {
1560           newIndex = tabs[mid]._tPos;
1561           break;
1562         }
1563       }
1564       if (newIndex >= oldIndex) {
1565         newIndex++;
1566       }
1567       if (newIndex < 0 || newIndex == oldIndex) {
1568         return;
1569       }
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)" : "";
1579         }
1580       }
1582       function getTabShift(tab, dropIndex) {
1583         if (tab._tPos < draggedTab._tPos && tab._tPos >= dropIndex) {
1584           return RTL_UI ? -shiftWidth : shiftWidth;
1585         }
1586         if (tab._tPos > draggedTab._tPos && tab._tPos < dropIndex) {
1587           return RTL_UI ? shiftWidth : -shiftWidth;
1588         }
1589         return 0;
1590       }
1591     }
1593     _finishAnimateTabMove() {
1594       if (!this.hasAttribute("movingtab")) {
1595         return;
1596       }
1598       for (let tab of this._getVisibleTabs()) {
1599         tab.style.transform = "";
1600       }
1602       this.removeAttribute("movingtab");
1603       gNavToolbox.removeAttribute("movingtab");
1605       this._handleTabSelect();
1606     }
1608     /**
1609      * Regroup all selected tabs around the
1610      * tab in param
1611      */
1612     _groupSelectedTabs(tab) {
1613       let draggedTabPos = tab._tPos;
1614       let selectedTabs = gBrowser.selectedTabs;
1615       let animate = !gReduceMotion;
1617       tab.groupingTabsData = {
1618         finished: !animate,
1619       };
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);
1628         if (animate) {
1629           movingTab.groupingTabsData = {};
1630           addAnimationData(movingTab, insertAtPos, "left");
1631         } else {
1632           gBrowser.moveTabTo(movingTab, insertAtPos);
1633         }
1634         insertAtPos--;
1635       }
1637       // Animate right selected tabs
1639       insertAtPos = draggedTabPos + 1;
1640       for (
1641         let i = selectedTabs.indexOf(tab) + 1;
1642         i < selectedTabs.length;
1643         i++
1644       ) {
1645         let movingTab = selectedTabs[i];
1646         insertAtPos = newIndex(movingTab, insertAtPos);
1648         if (animate) {
1649           movingTab.groupingTabsData = {};
1650           addAnimationData(movingTab, insertAtPos, "right");
1651         } else {
1652           gBrowser.moveTabTo(movingTab, insertAtPos);
1653         }
1654         insertAtPos++;
1655       }
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)";
1662         }
1663       }
1665       function newIndex(aTab, index) {
1666         // Don't allow mixing pinned and unpinned tabs.
1667         if (aTab.pinned) {
1668           return Math.min(index, gBrowser._numPinnedTabs - 1);
1669         }
1670         return Math.max(index, gBrowser._numPinnedTabs);
1671       }
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.
1679           return;
1680         }
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;
1693         };
1694         if (gReduceMotion) {
1695           postTransitionCleanup();
1696         } else {
1697           let onTransitionEnd = transitionendEvent => {
1698             if (
1699               transitionendEvent.propertyName != "transform" ||
1700               transitionendEvent.originalTarget != movingTab
1701             ) {
1702               return;
1703             }
1704             movingTab.removeEventListener("transitionend", onTransitionEnd);
1705             postTransitionCleanup();
1706           };
1708           movingTab.addEventListener("transitionend", onTransitionEnd);
1709         }
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
1724             break;
1725           }
1727           if (middleTab.multiselected) {
1728             // Skip because this selected tab should
1729             // be shifted towards the dragged Tab.
1730             continue;
1731           }
1733           if (
1734             !middleTab.groupingTabsData ||
1735             !middleTab.groupingTabsData.translateX
1736           ) {
1737             middleTab.groupingTabsData = { translateX: 0 };
1738           }
1739           if (side == "left") {
1740             middleTab.groupingTabsData.translateX -= movingTabWidth;
1741           } else {
1742             middleTab.groupingTabsData.translateX += movingTabWidth;
1743           }
1745           middleTab.toggleAttribute("tab-grouping", true);
1746         }
1747       }
1748     }
1750     _finishGroupSelectedTabs(tab) {
1751       if (!tab.groupingTabsData || tab.groupingTabsData.finished) {
1752         return;
1753       }
1755       tab.groupingTabsData.finished = true;
1757       let selectedTabs = gBrowser.selectedTabs;
1758       let tabIndex = selectedTabs.indexOf(tab);
1760       // Moving left tabs
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);
1765         }
1766       }
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);
1773         }
1774       }
1776       for (let t of this._getVisibleTabs()) {
1777         t.style.transform = "";
1778         t.removeAttribute("tab-grouping");
1779         delete t.groupingTabsData;
1780       }
1781     }
1783     _isGroupTabsAnimationOver() {
1784       for (let tab of gBrowser.selectedTabs) {
1785         if (tab.groupingTabsData && tab.groupingTabsData.animate) {
1786           return false;
1787         }
1788       }
1789       return true;
1790     }
1792     handleEvent(aEvent) {
1793       switch (aEvent.type) {
1794         case "mouseout":
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) {
1799             break;
1800           }
1801         // fall through
1802         case "mousemove":
1803           if (document.getElementById("tabContextMenu").state != "open") {
1804             this._unlockTabSizing();
1805           }
1806           break;
1807         default:
1808           let methodName = `on_${aEvent.type}`;
1809           if (methodName in this) {
1810             this[methodName](aEvent);
1811           } else {
1812             throw new Error(`Unexpected event ${aEvent.type}`);
1813           }
1814       }
1815     }
1817     _notifyBackgroundTab(aTab) {
1818       if (aTab.pinned || aTab.hidden || !this.hasAttribute("overflow")) {
1819         return;
1820       }
1822       this._lastTabToScrollIntoView = aTab;
1823       if (!this._backgroundTabScrollPromise) {
1824         this._backgroundTabScrollPromise = window
1825           .promiseDocumentFlushed(() => {
1826             let lastTabRect =
1827               this._lastTabToScrollIntoView.getBoundingClientRect();
1828             let selectedTab = this.selectedItem;
1829             if (selectedTab.pinned) {
1830               selectedTab = null;
1831             } else {
1832               selectedTab = selectedTab.getBoundingClientRect();
1833               selectedTab = {
1834                 left: selectedTab.left,
1835                 right: selectedTab.right,
1836               };
1837             }
1838             return [
1839               this._lastTabToScrollIntoView,
1840               this.arrowScrollbox.scrollClientRect,
1841               { left: lastTabRect.left, right: lastTabRect.right },
1842               selectedTab,
1843             ];
1844           })
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);
1853               return;
1854             }
1855             delete this._lastTabToScrollIntoView;
1856             // Is the new tab already completely visible?
1857             if (
1858               scrollRect.left <= tabRect.left &&
1859               tabRect.right <= scrollRect.right
1860             ) {
1861               return;
1862             }
1864             if (this.arrowScrollbox.smoothScroll) {
1865               // Can we make both the new tab and the selected tab completely visible?
1866               if (
1867                 !selectedRect ||
1868                 Math.max(
1869                   tabRect.right - selectedRect.left,
1870                   selectedRect.right - tabRect.left
1871                 ) <= scrollRect.width
1872               ) {
1873                 this.arrowScrollbox.ensureElementIsVisible(tabToScrollIntoView);
1874                 return;
1875               }
1877               this.arrowScrollbox.scrollByPixels(
1878                 RTL_UI
1879                   ? selectedRect.right - scrollRect.right
1880                   : selectedRect.left - scrollRect.left
1881               );
1882             }
1884             if (!this._animateElement.hasAttribute("highlight")) {
1885               this._animateElement.toggleAttribute("highlight", true);
1886               setTimeout(
1887                 function (ele) {
1888                   ele.removeAttribute("highlight");
1889                 },
1890                 150,
1891                 this._animateElement
1892               );
1893             }
1894           });
1895       }
1896     }
1898     /**
1899      * Returns the tab where an event happened, or null if it didn't occur on a tab.
1900      *
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.
1908      */
1909     _getDragTargetTab(event, { ignoreTabSides = false } = {}) {
1910       let { target } = event;
1911       if (target.nodeType != Node.ELEMENT_NODE) {
1912         target = target.parentElement;
1913       }
1914       let tab = target?.closest("tab");
1915       if (tab && ignoreTabSides) {
1916         let { width } = tab.getBoundingClientRect();
1917         if (
1918           event.screenX < tab.screenX + width * 0.25 ||
1919           event.screenX > tab.screenX + width * 0.75
1920         ) {
1921           return null;
1922         }
1923       }
1924       return tab;
1925     }
1927     _getDropIndex(event) {
1928       let tab = this._getDragTargetTab(event);
1929       if (!tab) {
1930         return this.allTabs.length;
1931       }
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);
1937     }
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;
1948           break;
1949         }
1950       }
1952       if (isMovingTabs) {
1953         let sourceNode = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
1954         if (
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
1961         ) {
1962           // Do not allow transfering a private tab to a non-private window
1963           // and vice versa.
1964           if (
1965             PrivateBrowsingUtils.isWindowPrivate(window) !=
1966             PrivateBrowsingUtils.isWindowPrivate(sourceNode.ownerGlobal)
1967           ) {
1968             return "none";
1969           }
1971           if (
1972             window.gMultiProcessBrowser !=
1973             sourceNode.ownerGlobal.gMultiProcessBrowser
1974           ) {
1975             return "none";
1976           }
1978           if (
1979             window.gFissionBrowser != sourceNode.ownerGlobal.gFissionBrowser
1980           ) {
1981             return "none";
1982           }
1984           return dt.dropEffect == "copy" ? "copy" : "move";
1985         }
1986       }
1988       if (browserDragAndDrop.canDropLink(event)) {
1989         return "link";
1990       }
1991       return "none";
1992     }
1994     _handleNewTab(tab) {
1995       if (tab.container != this) {
1996         return;
1997       }
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);
2007       }
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
2013       // overflowed.
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);
2021       }
2023       if (UserInteraction.running("browser.tabs.opening", window)) {
2024         UserInteraction.finish("browser.tabs.opening", window);
2025       }
2026     }
2028     _canAdvanceToTab(aTab) {
2029       return !aTab.closing;
2030     }
2032     /**
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.
2036      */
2037     getRelatedElement(aTab) {
2038       if (!aTab) {
2039         return null;
2040       }
2042       // Cannot access gBrowser before it's initialized.
2043       if (!gBrowser._initialized) {
2044         return this.tabbox.tabpanels.firstElementChild;
2045       }
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) {
2054           return null;
2055         }
2056         gBrowser._insertBrowser(aTab);
2057       }
2058       return document.getElementById(aTab.linkedPanel);
2059     }
2061     _updateNewTabVisibility() {
2062       // Helper functions to help deal with customize mode wrapping some items
2063       let wrap = n =>
2064         n.parentNode.localName == "toolbarpaletteitem" ? n.parentNode : n;
2065       let unwrap = 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.
2076       let sib = this;
2077       do {
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"
2084       );
2085     }
2087     onWidgetAfterDOMChange(aNode, aNextNode, aContainer) {
2088       if (
2089         aContainer.ownerDocument == document &&
2090         aContainer.id == "TabsToolbar-customization-target"
2091       ) {
2092         this._updateNewTabVisibility();
2093       }
2094     }
2096     onAreaNodeRegistered(aArea, aContainer) {
2097       if (aContainer.ownerDocument == document && aArea == "TabsToolbar") {
2098         this._updateNewTabVisibility();
2099       }
2100     }
2102     onAreaReset(aArea, aContainer) {
2103       this.onAreaNodeRegistered(aArea, aContainer);
2104     }
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);
2111       } else {
2112         this._hiddenSoundPlayingTabs.delete(tab);
2113         if (this._hiddenSoundPlayingTabs.size == 0) {
2114           this.removeAttribute("hiddensoundplaying");
2115         }
2116       }
2117     }
2119     destroy() {
2120       if (this.boundObserve) {
2121         Services.prefs.removeObserver("privacy.userContext", this.boundObserve);
2122       }
2123       CustomizableUI.removeListener(this);
2124     }
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");
2132         return;
2133       }
2135       tab.toggleAttribute(
2136         "indicator-replaces-favicon",
2137         theseAttributes.some(attr => tab.hasAttribute(attr))
2138       );
2139     }
2140   }
2142   customElements.define("tabbrowser-tabs", MozTabbrowserTabs, {
2143     extends: "tabs",
2144   });