Merge mozilla-central to autoland. a=merge CLOSED TREE
[gecko.git] / browser / base / content / tabbrowser-tabs.js
blobed6b748e45e1066125aea832eeec863b8fe9d0a8
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   const TAB_PREVIEW_PREF = "browser.tabs.cardPreview.enabled";
14   class MozTabbrowserTabs extends MozElements.TabsBase {
15     constructor() {
16       super();
18       this.addEventListener("TabSelect", this);
19       this.addEventListener("TabClose", this);
20       this.addEventListener("TabAttrModified", this);
21       this.addEventListener("TabHide", this);
22       this.addEventListener("TabShow", this);
23       this.addEventListener("TabPinned", this);
24       this.addEventListener("TabUnpinned", this);
25       this.addEventListener("TabHoverStart", this);
26       this.addEventListener("TabHoverEnd", this);
27       this.addEventListener("transitionend", this);
28       this.addEventListener("dblclick", this);
29       this.addEventListener("click", this);
30       this.addEventListener("click", this, true);
31       this.addEventListener("keydown", this, { mozSystemGroup: true });
32       this.addEventListener("dragstart", this);
33       this.addEventListener("dragover", this);
34       this.addEventListener("drop", this);
35       this.addEventListener("dragend", this);
36       this.addEventListener("dragleave", this);
37       this.addEventListener("mouseleave", this);
38     }
40     init() {
41       this.arrowScrollbox = this.querySelector("arrowscrollbox");
42       this.arrowScrollbox.addEventListener("wheel", this, true);
44       this.baseConnect();
46       this._blockDblClick = false;
47       this._tabDropIndicator = this.querySelector(".tab-drop-indicator");
48       this._dragOverDelay = 350;
49       this._dragTime = 0;
50       this._closeButtonsUpdatePending = false;
51       this._closingTabsSpacer = this.querySelector(".closing-tabs-spacer");
52       this._tabDefaultMaxWidth = NaN;
53       this._lastTabClosedByMouse = false;
54       this._hasTabTempMaxWidth = false;
55       this._scrollButtonWidth = 0;
56       this._lastNumPinned = 0;
57       this._pinnedTabsLayoutCache = null;
58       this._animateElement = this.arrowScrollbox;
59       this._tabClipWidth = Services.prefs.getIntPref(
60         "browser.tabs.tabClipWidth"
61       );
62       this._hiddenSoundPlayingTabs = new Set();
63       this._allTabs = null;
64       this._visibleTabs = null;
66       var tab = this.allTabs[0];
67       tab.label = this.emptyTabTitle;
69       // Hide the secondary text for locales where it is unsupported due to size constraints.
70       const language = Services.locale.appLocaleAsBCP47;
71       const unsupportedLocales = Services.prefs.getCharPref(
72         "browser.tabs.secondaryTextUnsupportedLocales"
73       );
74       this.toggleAttribute(
75         "secondarytext-unsupported",
76         unsupportedLocales.split(",").includes(language.split("-")[0])
77       );
79       this.newTabButton.setAttribute(
80         "aria-label",
81         GetDynamicShortcutTooltipText("tabs-newtab-button")
82       );
84       let handleResize = () => {
85         this._updateCloseButtons();
86         this._handleTabSelect(true);
87       };
88       window.addEventListener("resize", handleResize);
89       this._fullscreenMutationObserver = new MutationObserver(handleResize);
90       this._fullscreenMutationObserver.observe(document.documentElement, {
91         attributeFilter: ["inFullscreen", "inDOMFullscreen"],
92       });
94       this.boundObserve = (...args) => this.observe(...args);
95       Services.prefs.addObserver("privacy.userContext", this.boundObserve);
96       this.observe(null, "nsPref:changed", "privacy.userContext.enabled");
98       XPCOMUtils.defineLazyPreferenceGetter(
99         this,
100         "_tabMinWidthPref",
101         "browser.tabs.tabMinWidth",
102         null,
103         (pref, prevValue, newValue) => (this._tabMinWidth = newValue),
104         newValue => {
105           const LIMIT = 50;
106           return Math.max(newValue, LIMIT);
107         }
108       );
110       this._tabMinWidth = this._tabMinWidthPref;
112       CustomizableUI.addListener(this);
113       this._updateNewTabVisibility();
114       this._initializeArrowScrollbox();
116       XPCOMUtils.defineLazyPreferenceGetter(
117         this,
118         "_closeTabByDblclick",
119         "browser.tabs.closeTabByDblclick",
120         false
121       );
123       if (gMultiProcessBrowser) {
124         this.tabbox.tabpanels.setAttribute("async", "true");
125       }
127       this.configureTooltip = () => {
128         // fall back to original tooltip behavior if pref is not set
129         this.tooltip = this._showCardPreviews ? null : "tabbrowser-tab-tooltip";
131         // activate new tooltip behavior if pref is set
132         document
133           .getElementById("tabbrowser-tab-preview")
134           .toggleAttribute("hidden", !this._showCardPreviews);
135       };
136       XPCOMUtils.defineLazyPreferenceGetter(
137         this,
138         "_showCardPreviews",
139         TAB_PREVIEW_PREF,
140         false,
141         () => this.configureTooltip()
142       );
143       this.configureTooltip();
144     }
146     on_TabSelect() {
147       this._handleTabSelect();
148     }
150     on_TabClose(event) {
151       this._hiddenSoundPlayingStatusChanged(event.target, { closed: true });
152     }
154     on_TabAttrModified(event) {
155       if (
156         ["soundplaying", "muted", "activemedia-blocked", "sharing"].some(attr =>
157           event.detail.changed.includes(attr)
158         )
159       ) {
160         this.updateTabIndicatorAttr(event.target);
161       }
163       if (
164         event.detail.changed.includes("soundplaying") &&
165         event.target.hidden
166       ) {
167         this._hiddenSoundPlayingStatusChanged(event.target);
168       }
169     }
171     on_TabHide(event) {
172       if (event.target.soundPlaying) {
173         this._hiddenSoundPlayingStatusChanged(event.target);
174       }
175     }
177     on_TabShow(event) {
178       if (event.target.soundPlaying) {
179         this._hiddenSoundPlayingStatusChanged(event.target);
180       }
181     }
183     on_TabPinned(event) {
184       this.updateTabIndicatorAttr(event.target);
185     }
187     on_TabUnpinned(event) {
188       this.updateTabIndicatorAttr(event.target);
189     }
191     on_TabHoverStart(event) {
192       if (this._showCardPreviews) {
193         const previewContainer = document.getElementById(
194           "tabbrowser-tab-preview"
195         );
196         previewContainer.tab = event.target;
197       }
198     }
200     on_TabHoverEnd(event) {
201       if (this._showCardPreviews) {
202         const previewContainer = document.getElementById(
203           "tabbrowser-tab-preview"
204         );
205         if (previewContainer.tab === event.target) {
206           previewContainer.tab = null;
207         }
208       }
209     }
211     on_transitionend(event) {
212       if (event.propertyName != "max-width") {
213         return;
214       }
216       let tab = event.target ? event.target.closest("tab") : null;
218       if (tab.hasAttribute("fadein")) {
219         if (tab._fullyOpen) {
220           this._updateCloseButtons();
221         } else {
222           this._handleNewTab(tab);
223         }
224       } else if (tab.closing) {
225         gBrowser._endRemoveTab(tab);
226       }
228       let evt = new CustomEvent("TabAnimationEnd", { bubbles: true });
229       tab.dispatchEvent(evt);
230     }
232     on_dblclick(event) {
233       // When the tabbar has an unified appearance with the titlebar
234       // and menubar, a double-click in it should have the same behavior
235       // as double-clicking the titlebar
236       if (TabsInTitlebar.enabled) {
237         return;
238       }
240       if (event.button != 0 || event.originalTarget.localName != "scrollbox") {
241         return;
242       }
244       if (!this._blockDblClick) {
245         BrowserOpenTab();
246       }
248       event.preventDefault();
249     }
251     on_click(event) {
252       if (event.eventPhase == Event.CAPTURING_PHASE && event.button == 0) {
253         /* Catches extra clicks meant for the in-tab close button.
254          * Placed here to avoid leaking (a temporary handler added from the
255          * in-tab close button binding would close over the tab and leak it
256          * until the handler itself was removed). (bug 897751)
257          *
258          * The only sequence in which a second click event (i.e. dblclik)
259          * can be dispatched on an in-tab close button is when it is shown
260          * after the first click (i.e. the first click event was dispatched
261          * on the tab). This happens when we show the close button only on
262          * the active tab. (bug 352021)
263          * The only sequence in which a third click event can be dispatched
264          * on an in-tab close button is when the tab was opened with a
265          * double click on the tabbar. (bug 378344)
266          * In both cases, it is most likely that the close button area has
267          * been accidentally clicked, therefore we do not close the tab.
268          *
269          * We don't want to ignore processing of more than one click event,
270          * though, since the user might actually be repeatedly clicking to
271          * close many tabs at once.
272          */
273         let target = event.originalTarget;
274         if (target.classList.contains("tab-close-button")) {
275           // We preemptively set this to allow the closing-multiple-tabs-
276           // in-a-row case.
277           if (this._blockDblClick) {
278             target._ignoredCloseButtonClicks = true;
279           } else if (event.detail > 1 && !target._ignoredCloseButtonClicks) {
280             target._ignoredCloseButtonClicks = true;
281             event.stopPropagation();
282             return;
283           } else {
284             // Reset the "ignored click" flag
285             target._ignoredCloseButtonClicks = false;
286           }
287         }
289         /* Protects from close-tab-button errant doubleclick:
290          * Since we're removing the event target, if the user
291          * double-clicks the button, the dblclick event will be dispatched
292          * with the tabbar as its event target (and explicit/originalTarget),
293          * which treats that as a mouse gesture for opening a new tab.
294          * In this context, we're manually blocking the dblclick event.
295          */
296         if (this._blockDblClick) {
297           if (!("_clickedTabBarOnce" in this)) {
298             this._clickedTabBarOnce = true;
299             return;
300           }
301           delete this._clickedTabBarOnce;
302           this._blockDblClick = false;
303         }
304       } else if (
305         event.eventPhase == Event.BUBBLING_PHASE &&
306         event.button == 1
307       ) {
308         let tab = event.target ? event.target.closest("tab") : null;
309         if (tab) {
310           if (tab.multiselected) {
311             gBrowser.removeMultiSelectedTabs();
312           } else {
313             gBrowser.removeTab(tab, {
314               animate: true,
315               triggeringEvent: event,
316             });
317           }
318         } else if (
319           event.originalTarget.closest("scrollbox") &&
320           !Services.prefs.getBoolPref(
321             "widget.gtk.titlebar-action-middle-click-enabled"
322           )
323         ) {
324           // Check whether the click
325           // was dispatched on the open space of it.
326           let visibleTabs = this._getVisibleTabs();
327           let lastTab = visibleTabs[visibleTabs.length - 1];
328           let winUtils = window.windowUtils;
329           let endOfTab =
330             winUtils.getBoundsWithoutFlushing(lastTab)[
331               RTL_UI ? "left" : "right"
332             ];
333           if (
334             (!RTL_UI && event.clientX > endOfTab) ||
335             (RTL_UI && event.clientX < endOfTab)
336           ) {
337             BrowserOpenTab();
338           }
339         } else {
340           return;
341         }
343         event.preventDefault();
344         event.stopPropagation();
345       }
346     }
348     on_keydown(event) {
349       let { altKey, shiftKey } = event;
350       let [accel, nonAccel] =
351         AppConstants.platform == "macosx"
352           ? [event.metaKey, event.ctrlKey]
353           : [event.ctrlKey, event.metaKey];
355       let keyComboForMove = accel && shiftKey && !altKey && !nonAccel;
356       let keyComboForFocus = accel && !shiftKey && !altKey && !nonAccel;
358       if (!keyComboForMove && !keyComboForFocus) {
359         return;
360       }
362       // Don't check if the event was already consumed because tab navigation
363       // should work always for better user experience.
364       let { visibleTabs, selectedTab } = gBrowser;
365       let { arrowKeysShouldWrap } = this;
366       let focusedTabIndex = this.ariaFocusedIndex;
367       if (focusedTabIndex == -1) {
368         focusedTabIndex = visibleTabs.indexOf(selectedTab);
369       }
370       let lastFocusedTabIndex = focusedTabIndex;
371       switch (event.keyCode) {
372         case KeyEvent.DOM_VK_UP:
373           if (keyComboForMove) {
374             gBrowser.moveTabBackward();
375           } else {
376             focusedTabIndex--;
377           }
378           break;
379         case KeyEvent.DOM_VK_DOWN:
380           if (keyComboForMove) {
381             gBrowser.moveTabForward();
382           } else {
383             focusedTabIndex++;
384           }
385           break;
386         case KeyEvent.DOM_VK_RIGHT:
387         case KeyEvent.DOM_VK_LEFT:
388           if (keyComboForMove) {
389             gBrowser.moveTabOver(event);
390           } else if (
391             (!RTL_UI && event.keyCode == KeyEvent.DOM_VK_RIGHT) ||
392             (RTL_UI && event.keyCode == KeyEvent.DOM_VK_LEFT)
393           ) {
394             focusedTabIndex++;
395           } else {
396             focusedTabIndex--;
397           }
398           break;
399         case KeyEvent.DOM_VK_HOME:
400           if (keyComboForMove) {
401             gBrowser.moveTabToStart();
402           } else {
403             focusedTabIndex = 0;
404           }
405           break;
406         case KeyEvent.DOM_VK_END:
407           if (keyComboForMove) {
408             gBrowser.moveTabToEnd();
409           } else {
410             focusedTabIndex = visibleTabs.length - 1;
411           }
412           break;
413         case KeyEvent.DOM_VK_SPACE:
414           if (visibleTabs[lastFocusedTabIndex].multiselected) {
415             gBrowser.removeFromMultiSelectedTabs(
416               visibleTabs[lastFocusedTabIndex]
417             );
418           } else {
419             gBrowser.addToMultiSelectedTabs(visibleTabs[lastFocusedTabIndex]);
420           }
421           break;
422         default:
423           // Consume the keydown event for the above keyboard
424           // shortcuts only.
425           return;
426       }
428       if (arrowKeysShouldWrap) {
429         if (focusedTabIndex >= visibleTabs.length) {
430           focusedTabIndex = 0;
431         } else if (focusedTabIndex < 0) {
432           focusedTabIndex = visibleTabs.length - 1;
433         }
434       } else {
435         focusedTabIndex = Math.min(
436           visibleTabs.length - 1,
437           Math.max(0, focusedTabIndex)
438         );
439       }
441       if (keyComboForFocus && focusedTabIndex != lastFocusedTabIndex) {
442         this.ariaFocusedItem = visibleTabs[focusedTabIndex];
443       }
445       event.preventDefault();
446     }
448     on_dragstart(event) {
449       var tab = this._getDragTargetTab(event);
450       if (!tab || this._isCustomizing) {
451         return;
452       }
454       this.startTabDrag(event, tab);
455     }
457     startTabDrag(event, tab, { fromTabList = false } = {}) {
458       let selectedTabs = gBrowser.selectedTabs;
459       let otherSelectedTabs = selectedTabs.filter(
460         selectedTab => selectedTab != tab
461       );
462       let dataTransferOrderedTabs = [tab].concat(otherSelectedTabs);
464       let dt = event.dataTransfer;
465       for (let i = 0; i < dataTransferOrderedTabs.length; i++) {
466         let dtTab = dataTransferOrderedTabs[i];
468         dt.mozSetDataAt(TAB_DROP_TYPE, dtTab, i);
469         let dtBrowser = dtTab.linkedBrowser;
471         // We must not set text/x-moz-url or text/plain data here,
472         // otherwise trying to detach the tab by dropping it on the desktop
473         // may result in an "internet shortcut"
474         dt.mozSetDataAt(
475           "text/x-moz-text-internal",
476           dtBrowser.currentURI.spec,
477           i
478         );
479       }
481       // Set the cursor to an arrow during tab drags.
482       dt.mozCursor = "default";
484       // Set the tab as the source of the drag, which ensures we have a stable
485       // node to deliver the `dragend` event.  See bug 1345473.
486       dt.addElement(tab);
488       if (tab.multiselected) {
489         this._groupSelectedTabs(tab);
490       }
492       // Create a canvas to which we capture the current tab.
493       // Until canvas is HiDPI-aware (bug 780362), we need to scale the desired
494       // canvas size (in CSS pixels) to the window's backing resolution in order
495       // to get a full-resolution drag image for use on HiDPI displays.
496       let scale = window.devicePixelRatio;
497       let canvas = this._dndCanvas;
498       if (!canvas) {
499         this._dndCanvas = canvas = document.createElementNS(
500           "http://www.w3.org/1999/xhtml",
501           "canvas"
502         );
503         canvas.style.width = "100%";
504         canvas.style.height = "100%";
505         canvas.mozOpaque = true;
506       }
508       canvas.width = 160 * scale;
509       canvas.height = 90 * scale;
510       let toDrag = canvas;
511       let dragImageOffset = -16;
512       let browser = tab.linkedBrowser;
513       if (gMultiProcessBrowser) {
514         var context = canvas.getContext("2d");
515         context.fillStyle = "white";
516         context.fillRect(0, 0, canvas.width, canvas.height);
518         let captureListener;
519         let platform = AppConstants.platform;
520         // On Windows and Mac we can update the drag image during a drag
521         // using updateDragImage. On Linux, we can use a panel.
522         if (platform == "win" || platform == "macosx") {
523           captureListener = function () {
524             dt.updateDragImage(canvas, dragImageOffset, dragImageOffset);
525           };
526         } else {
527           // Create a panel to use it in setDragImage
528           // which will tell xul to render a panel that follows
529           // the pointer while a dnd session is on.
530           if (!this._dndPanel) {
531             this._dndCanvas = canvas;
532             this._dndPanel = document.createXULElement("panel");
533             this._dndPanel.className = "dragfeedback-tab";
534             this._dndPanel.setAttribute("type", "drag");
535             let wrapper = document.createElementNS(
536               "http://www.w3.org/1999/xhtml",
537               "div"
538             );
539             wrapper.style.width = "160px";
540             wrapper.style.height = "90px";
541             wrapper.appendChild(canvas);
542             this._dndPanel.appendChild(wrapper);
543             document.documentElement.appendChild(this._dndPanel);
544           }
545           toDrag = this._dndPanel;
546         }
547         // PageThumb is async with e10s but that's fine
548         // since we can update the image during the dnd.
549         PageThumbs.captureToCanvas(browser, canvas)
550           .then(captureListener)
551           .catch(e => console.error(e));
552       } else {
553         // For the non e10s case we can just use PageThumbs
554         // sync, so let's use the canvas for setDragImage.
555         PageThumbs.captureToCanvas(browser, canvas).catch(e =>
556           console.error(e)
557         );
558         dragImageOffset = dragImageOffset * scale;
559       }
560       dt.setDragImage(toDrag, dragImageOffset, dragImageOffset);
562       // _dragData.offsetX/Y give the coordinates that the mouse should be
563       // positioned relative to the corner of the new window created upon
564       // dragend such that the mouse appears to have the same position
565       // relative to the corner of the dragged tab.
566       function clientX(ele) {
567         return ele.getBoundingClientRect().left;
568       }
569       let tabOffsetX = clientX(tab) - clientX(this);
570       tab._dragData = {
571         offsetX: event.screenX - window.screenX - tabOffsetX,
572         offsetY: event.screenY - window.screenY,
573         scrollX: this.arrowScrollbox.scrollbox.scrollLeft,
574         screenX: event.screenX,
575         movingTabs: (tab.multiselected ? gBrowser.selectedTabs : [tab]).filter(
576           t => t.pinned == tab.pinned
577         ),
578         fromTabList,
579       };
581       event.stopPropagation();
583       if (fromTabList) {
584         Services.telemetry.scalarAdd(
585           "browser.ui.interaction.all_tabs_panel_dragstart_tab_event_count",
586           1
587         );
588       }
589     }
591     on_dragover(event) {
592       var effects = this.getDropEffectForTabDrag(event);
594       var ind = this._tabDropIndicator;
595       if (effects == "" || effects == "none") {
596         ind.hidden = true;
597         return;
598       }
599       event.preventDefault();
600       event.stopPropagation();
602       var arrowScrollbox = this.arrowScrollbox;
604       // autoscroll the tab strip if we drag over the scroll
605       // buttons, even if we aren't dragging a tab, but then
606       // return to avoid drawing the drop indicator
607       var pixelsToScroll = 0;
608       if (this.hasAttribute("overflow")) {
609         switch (event.originalTarget) {
610           case arrowScrollbox._scrollButtonUp:
611             pixelsToScroll = arrowScrollbox.scrollIncrement * -1;
612             break;
613           case arrowScrollbox._scrollButtonDown:
614             pixelsToScroll = arrowScrollbox.scrollIncrement;
615             break;
616         }
617         if (pixelsToScroll) {
618           arrowScrollbox.scrollByPixels(
619             (RTL_UI ? -1 : 1) * pixelsToScroll,
620             true
621           );
622         }
623       }
625       let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
626       if (
627         (effects == "move" || effects == "copy") &&
628         this == draggedTab.container &&
629         !draggedTab._dragData.fromTabList
630       ) {
631         ind.hidden = true;
633         if (!this._isGroupTabsAnimationOver()) {
634           // Wait for grouping tabs animation to finish
635           return;
636         }
637         this._finishGroupSelectedTabs(draggedTab);
639         if (effects == "move") {
640           this._animateTabMove(event);
641           return;
642         }
643       }
645       this._finishAnimateTabMove();
647       if (effects == "link") {
648         let tab = this._getDragTargetTab(event, { ignoreTabSides: true });
649         if (tab) {
650           if (!this._dragTime) {
651             this._dragTime = Date.now();
652           }
653           if (Date.now() >= this._dragTime + this._dragOverDelay) {
654             this.selectedItem = tab;
655           }
656           ind.hidden = true;
657           return;
658         }
659       }
661       var rect = arrowScrollbox.getBoundingClientRect();
662       var newMargin;
663       if (pixelsToScroll) {
664         // if we are scrolling, put the drop indicator at the edge
665         // so that it doesn't jump while scrolling
666         let scrollRect = arrowScrollbox.scrollClientRect;
667         let minMargin = scrollRect.left - rect.left;
668         let maxMargin = Math.min(
669           minMargin + scrollRect.width,
670           scrollRect.right
671         );
672         if (RTL_UI) {
673           [minMargin, maxMargin] = [
674             this.clientWidth - maxMargin,
675             this.clientWidth - minMargin,
676           ];
677         }
678         newMargin = pixelsToScroll > 0 ? maxMargin : minMargin;
679       } else {
680         let newIndex = this._getDropIndex(event);
681         let children = this.allTabs;
682         if (newIndex == children.length) {
683           let tabRect = this._getVisibleTabs().at(-1).getBoundingClientRect();
684           if (RTL_UI) {
685             newMargin = rect.right - tabRect.left;
686           } else {
687             newMargin = tabRect.right - rect.left;
688           }
689         } else {
690           let tabRect = children[newIndex].getBoundingClientRect();
691           if (RTL_UI) {
692             newMargin = rect.right - tabRect.right;
693           } else {
694             newMargin = tabRect.left - rect.left;
695           }
696         }
697       }
699       ind.hidden = false;
700       newMargin += ind.clientWidth / 2;
701       if (RTL_UI) {
702         newMargin *= -1;
703       }
704       ind.style.transform = "translate(" + Math.round(newMargin) + "px)";
705     }
707     on_drop(event) {
708       var dt = event.dataTransfer;
709       var dropEffect = dt.dropEffect;
710       var draggedTab;
711       let movingTabs;
712       if (dt.mozTypesAt(0)[0] == TAB_DROP_TYPE) {
713         // tab copy or move
714         draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
715         // not our drop then
716         if (!draggedTab) {
717           return;
718         }
719         movingTabs = draggedTab._dragData.movingTabs;
720         draggedTab.container._finishGroupSelectedTabs(draggedTab);
721       }
723       this._tabDropIndicator.hidden = true;
724       event.stopPropagation();
725       if (draggedTab && dropEffect == "copy") {
726         // copy the dropped tab (wherever it's from)
727         let newIndex = this._getDropIndex(event);
728         let draggedTabCopy;
729         for (let tab of movingTabs) {
730           let newTab = gBrowser.duplicateTab(tab);
731           gBrowser.moveTabTo(newTab, newIndex++);
732           if (tab == draggedTab) {
733             draggedTabCopy = newTab;
734           }
735         }
736         if (draggedTab.container != this || event.shiftKey) {
737           this.selectedItem = draggedTabCopy;
738         }
739       } else if (draggedTab && draggedTab.container == this) {
740         let oldTranslateX = Math.round(draggedTab._dragData.translateX);
741         let tabWidth = Math.round(draggedTab._dragData.tabWidth);
742         let translateOffset = oldTranslateX % tabWidth;
743         let newTranslateX = oldTranslateX - translateOffset;
744         if (oldTranslateX > 0 && translateOffset > tabWidth / 2) {
745           newTranslateX += tabWidth;
746         } else if (oldTranslateX < 0 && -translateOffset > tabWidth / 2) {
747           newTranslateX -= tabWidth;
748         }
750         let dropIndex;
751         if (draggedTab._dragData.fromTabList) {
752           dropIndex = this._getDropIndex(event);
753         } else {
754           dropIndex =
755             "animDropIndex" in draggedTab._dragData &&
756             draggedTab._dragData.animDropIndex;
757         }
758         let incrementDropIndex = true;
759         if (dropIndex && dropIndex > movingTabs[0]._tPos) {
760           dropIndex--;
761           incrementDropIndex = false;
762         }
764         if (oldTranslateX && oldTranslateX != newTranslateX && !gReduceMotion) {
765           for (let tab of movingTabs) {
766             tab.toggleAttribute("tabdrop-samewindow", true);
767             tab.style.transform = "translateX(" + newTranslateX + "px)";
768             let postTransitionCleanup = () => {
769               tab.removeAttribute("tabdrop-samewindow");
771               this._finishAnimateTabMove();
772               if (dropIndex !== false) {
773                 gBrowser.moveTabTo(tab, dropIndex);
774                 if (incrementDropIndex) {
775                   dropIndex++;
776                 }
777               }
779               gBrowser.syncThrobberAnimations(tab);
780             };
781             if (gReduceMotion) {
782               postTransitionCleanup();
783             } else {
784               let onTransitionEnd = transitionendEvent => {
785                 if (
786                   transitionendEvent.propertyName != "transform" ||
787                   transitionendEvent.originalTarget != tab
788                 ) {
789                   return;
790                 }
791                 tab.removeEventListener("transitionend", onTransitionEnd);
793                 postTransitionCleanup();
794               };
795               tab.addEventListener("transitionend", onTransitionEnd);
796             }
797           }
798         } else {
799           this._finishAnimateTabMove();
800           if (dropIndex !== false) {
801             for (let tab of movingTabs) {
802               gBrowser.moveTabTo(tab, dropIndex);
803               if (incrementDropIndex) {
804                 dropIndex++;
805               }
806             }
807           }
808         }
809       } else if (draggedTab) {
810         // Move the tabs. To avoid multiple tab-switches in the original window,
811         // the selected tab should be adopted last.
812         const dropIndex = this._getDropIndex(event);
813         let newIndex = dropIndex;
814         let selectedTab;
815         let indexForSelectedTab;
816         for (let i = 0; i < movingTabs.length; ++i) {
817           const tab = movingTabs[i];
818           if (tab.selected) {
819             selectedTab = tab;
820             indexForSelectedTab = newIndex;
821           } else {
822             const newTab = gBrowser.adoptTab(tab, newIndex, tab == draggedTab);
823             if (newTab) {
824               ++newIndex;
825             }
826           }
827         }
828         if (selectedTab) {
829           const newTab = gBrowser.adoptTab(
830             selectedTab,
831             indexForSelectedTab,
832             selectedTab == draggedTab
833           );
834           if (newTab) {
835             ++newIndex;
836           }
837         }
839         // Restore tab selection
840         gBrowser.addRangeToMultiSelectedTabs(
841           gBrowser.tabs[dropIndex],
842           gBrowser.tabs[newIndex - 1]
843         );
844       } else {
845         // Pass true to disallow dropping javascript: or data: urls
846         let links;
847         try {
848           links = browserDragAndDrop.dropLinks(event, true);
849         } catch (ex) {}
851         if (!links || links.length === 0) {
852           return;
853         }
855         let inBackground = Services.prefs.getBoolPref(
856           "browser.tabs.loadInBackground"
857         );
858         if (event.shiftKey) {
859           inBackground = !inBackground;
860         }
862         let targetTab = this._getDragTargetTab(event, { ignoreTabSides: true });
863         let userContextId = this.selectedItem.getAttribute("usercontextid");
864         let replace = !!targetTab;
865         let newIndex = this._getDropIndex(event);
866         let urls = links.map(link => link.url);
867         let csp = browserDragAndDrop.getCsp(event);
868         let triggeringPrincipal =
869           browserDragAndDrop.getTriggeringPrincipal(event);
871         (async () => {
872           if (
873             urls.length >=
874             Services.prefs.getIntPref("browser.tabs.maxOpenBeforeWarn")
875           ) {
876             // Sync dialog cannot be used inside drop event handler.
877             let answer = await OpenInTabsUtils.promiseConfirmOpenInTabs(
878               urls.length,
879               window
880             );
881             if (!answer) {
882               return;
883             }
884           }
886           gBrowser.loadTabs(urls, {
887             inBackground,
888             replace,
889             allowThirdPartyFixup: true,
890             targetTab,
891             newIndex,
892             userContextId,
893             triggeringPrincipal,
894             csp,
895           });
896         })();
897       }
899       if (draggedTab) {
900         delete draggedTab._dragData;
901       }
902     }
904     on_dragend(event) {
905       var dt = event.dataTransfer;
906       var draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
908       // Prevent this code from running if a tabdrop animation is
909       // running since calling _finishAnimateTabMove would clear
910       // any CSS transition that is running.
911       if (draggedTab.hasAttribute("tabdrop-samewindow")) {
912         return;
913       }
915       this._finishGroupSelectedTabs(draggedTab);
916       this._finishAnimateTabMove();
918       if (
919         dt.mozUserCancelled ||
920         dt.dropEffect != "none" ||
921         this._isCustomizing
922       ) {
923         delete draggedTab._dragData;
924         return;
925       }
927       // Check if tab detaching is enabled
928       if (!Services.prefs.getBoolPref("browser.tabs.allowTabDetach")) {
929         return;
930       }
932       // Disable detach within the browser toolbox
933       var eX = event.screenX;
934       var eY = event.screenY;
935       var wX = window.screenX;
936       // check if the drop point is horizontally within the window
937       if (eX > wX && eX < wX + window.outerWidth) {
938         // also avoid detaching if the the tab was dropped too close to
939         // the tabbar (half a tab)
940         let rect = window.windowUtils.getBoundsWithoutFlushing(
941           this.arrowScrollbox
942         );
943         let detachTabThresholdY = window.screenY + rect.top + 1.5 * rect.height;
944         if (eY < detachTabThresholdY && eY > window.screenY) {
945           return;
946         }
947       }
949       // screen.availLeft et. al. only check the screen that this window is on,
950       // but we want to look at the screen the tab is being dropped onto.
951       var screen = event.screen;
952       var availX = {},
953         availY = {},
954         availWidth = {},
955         availHeight = {};
956       // Get available rect in desktop pixels.
957       screen.GetAvailRectDisplayPix(availX, availY, availWidth, availHeight);
958       availX = availX.value;
959       availY = availY.value;
960       availWidth = availWidth.value;
961       availHeight = availHeight.value;
963       // Compute the final window size in desktop pixels ensuring that the new
964       // window entirely fits within `screen`.
965       let ourCssToDesktopScale =
966         window.devicePixelRatio / window.desktopToDeviceScale;
967       let screenCssToDesktopScale =
968         screen.defaultCSSScaleFactor / screen.contentsScaleFactor;
970       // NOTE(emilio): Multiplying the sizes here for screenCssToDesktopScale
971       // means that we'll try to create a window that has the same amount of CSS
972       // pixels than our current window, not the same amount of device pixels.
973       // There are pros and cons of both conversions, though this matches the
974       // pre-existing intended behavior.
975       var winWidth = Math.min(
976         window.outerWidth * screenCssToDesktopScale,
977         availWidth
978       );
979       var winHeight = Math.min(
980         window.outerHeight * screenCssToDesktopScale,
981         availHeight
982       );
984       // This is slightly tricky: _dragData.offsetX/Y is an offset in CSS
985       // pixels. Since we're doing the sizing above based on those, we also need
986       // to apply the offset with pixels relative to the screen's scale rather
987       // than our scale.
988       var left = Math.min(
989         Math.max(
990           eX * ourCssToDesktopScale -
991             draggedTab._dragData.offsetX * screenCssToDesktopScale,
992           availX
993         ),
994         availX + availWidth - winWidth
995       );
996       var top = Math.min(
997         Math.max(
998           eY * ourCssToDesktopScale -
999             draggedTab._dragData.offsetY * screenCssToDesktopScale,
1000           availY
1001         ),
1002         availY + availHeight - winHeight
1003       );
1005       // Convert back left and top to our CSS pixel space.
1006       left /= ourCssToDesktopScale;
1007       top /= ourCssToDesktopScale;
1009       delete draggedTab._dragData;
1011       if (gBrowser.tabs.length == 1) {
1012         // resize _before_ move to ensure the window fits the new screen.  if
1013         // the window is too large for its screen, the window manager may do
1014         // automatic repositioning.
1015         //
1016         // Since we're resizing before moving to our new screen, we need to use
1017         // sizes relative to the current screen. If we moved, then resized, then
1018         // we could avoid this special-case and share this with the else branch
1019         // below...
1020         winWidth /= ourCssToDesktopScale;
1021         winHeight /= ourCssToDesktopScale;
1023         window.resizeTo(winWidth, winHeight);
1024         window.moveTo(left, top);
1025         window.focus();
1026       } else {
1027         // We're opening a new window in a new screen, so make sure to use sizes
1028         // relative to the new screen.
1029         winWidth /= screenCssToDesktopScale;
1030         winHeight /= screenCssToDesktopScale;
1032         let props = { screenX: left, screenY: top, suppressanimation: 1 };
1033         if (AppConstants.platform != "win") {
1034           props.outerWidth = winWidth;
1035           props.outerHeight = winHeight;
1036         }
1037         gBrowser.replaceTabsWithWindow(draggedTab, props);
1038       }
1039       event.stopPropagation();
1040     }
1042     on_dragleave(event) {
1043       this._dragTime = 0;
1045       // This does not work at all (see bug 458613)
1046       var target = event.relatedTarget;
1047       while (target && target != this) {
1048         target = target.parentNode;
1049       }
1050       if (target) {
1051         return;
1052       }
1054       this._tabDropIndicator.hidden = true;
1055       event.stopPropagation();
1056     }
1058     on_wheel(event) {
1059       if (
1060         Services.prefs.getBoolPref("toolkit.tabbox.switchByScrolling", false)
1061       ) {
1062         event.stopImmediatePropagation();
1063       }
1064     }
1066     get emptyTabTitle() {
1067       // Normal tab title is used also in the permanent private browsing mode.
1068       const l10nId =
1069         PrivateBrowsingUtils.isWindowPrivate(window) &&
1070         !Services.prefs.getBoolPref("browser.privatebrowsing.autostart")
1071           ? "tabbrowser-empty-private-tab-title"
1072           : "tabbrowser-empty-tab-title";
1073       return gBrowser.tabLocalization.formatValueSync(l10nId);
1074     }
1076     get tabbox() {
1077       return document.getElementById("tabbrowser-tabbox");
1078     }
1080     get newTabButton() {
1081       return this.querySelector("#tabs-newtab-button");
1082     }
1084     // Accessor for tabs.  arrowScrollbox has a container for non-tab elements
1085     // at the end, everything else is <tab>s.
1086     get allTabs() {
1087       if (this._allTabs) {
1088         return this._allTabs;
1089       }
1090       let children = Array.from(this.arrowScrollbox.children);
1091       children.pop();
1092       this._allTabs = children;
1093       return children;
1094     }
1096     _getVisibleTabs() {
1097       if (!this._visibleTabs) {
1098         this._visibleTabs = Array.prototype.filter.call(
1099           this.allTabs,
1100           tab => !tab.hidden && !tab.closing
1101         );
1102       }
1103       return this._visibleTabs;
1104     }
1106     _invalidateCachedTabs() {
1107       this._allTabs = null;
1108       this._visibleTabs = null;
1109     }
1111     _invalidateCachedVisibleTabs() {
1112       this._visibleTabs = null;
1113     }
1115     appendChild(tab) {
1116       return this.insertBefore(tab, null);
1117     }
1119     insertBefore(tab, node) {
1120       if (!this.arrowScrollbox) {
1121         throw new Error("Shouldn't call this without arrowscrollbox");
1122       }
1124       let { arrowScrollbox } = this;
1125       if (node == null) {
1126         // We have a container for non-tab elements at the end of the scrollbox.
1127         node = arrowScrollbox.lastChild;
1128       }
1129       return arrowScrollbox.insertBefore(tab, node);
1130     }
1132     set _tabMinWidth(val) {
1133       this.style.setProperty("--tab-min-width", val + "px");
1134     }
1136     get _isCustomizing() {
1137       return document.documentElement.getAttribute("customizing") == "true";
1138     }
1140     // This overrides the TabsBase _selectNewTab method so that we can
1141     // potentially interrupt keyboard tab switching when sharing the
1142     // window or screen.
1143     _selectNewTab(aNewTab, aFallbackDir, aWrap) {
1144       if (!gSharedTabWarning.willShowSharedTabWarning(aNewTab)) {
1145         super._selectNewTab(aNewTab, aFallbackDir, aWrap);
1146       }
1147     }
1149     _initializeArrowScrollbox() {
1150       let arrowScrollbox = this.arrowScrollbox;
1151       arrowScrollbox.shadowRoot.addEventListener(
1152         "underflow",
1153         event => {
1154           // Ignore underflow events:
1155           // - from nested scrollable elements
1156           // - for vertical orientation
1157           // - corresponding to an overflow event that we ignored
1158           if (
1159             event.originalTarget != arrowScrollbox.scrollbox ||
1160             event.detail == 0 ||
1161             !this.hasAttribute("overflow")
1162           ) {
1163             return;
1164           }
1166           this.removeAttribute("overflow");
1168           if (this._lastTabClosedByMouse) {
1169             this._expandSpacerBy(this._scrollButtonWidth);
1170           }
1172           for (let tab of gBrowser._removingTabs) {
1173             gBrowser.removeTab(tab);
1174           }
1176           this._positionPinnedTabs();
1177           this._updateCloseButtons();
1178         },
1179         true
1180       );
1182       arrowScrollbox.shadowRoot.addEventListener("overflow", event => {
1183         // Ignore overflow events:
1184         // - from nested scrollable elements
1185         // - for vertical orientation
1186         if (
1187           event.originalTarget != arrowScrollbox.scrollbox ||
1188           event.detail == 0
1189         ) {
1190           return;
1191         }
1193         this.toggleAttribute("overflow", true);
1194         this._positionPinnedTabs();
1195         this._updateCloseButtons();
1196         this._handleTabSelect(true);
1197       });
1199       // Override arrowscrollbox.js method, since our scrollbox's children are
1200       // inherited from the scrollbox binding parent (this).
1201       arrowScrollbox._getScrollableElements = () => {
1202         return this.allTabs.filter(arrowScrollbox._canScrollToElement);
1203       };
1204       arrowScrollbox._canScrollToElement = tab => {
1205         return !tab._pinnedUnscrollable && !tab.hidden;
1206       };
1207     }
1209     observe(aSubject, aTopic) {
1210       switch (aTopic) {
1211         case "nsPref:changed":
1212           // This is has to deal with changes in
1213           // privacy.userContext.enabled and
1214           // privacy.userContext.newTabContainerOnLeftClick.enabled.
1215           let containersEnabled =
1216             Services.prefs.getBoolPref("privacy.userContext.enabled") &&
1217             !PrivateBrowsingUtils.isWindowPrivate(window);
1219           // This pref won't change so often, so just recreate the menu.
1220           const newTabLeftClickOpensContainersMenu = Services.prefs.getBoolPref(
1221             "privacy.userContext.newTabContainerOnLeftClick.enabled"
1222           );
1224           // There are separate "new tab" buttons for when the tab strip
1225           // is overflowed and when it is not.  Attach the long click
1226           // popup to both of them.
1227           const newTab = document.getElementById("new-tab-button");
1228           const newTab2 = this.newTabButton;
1230           for (let parent of [newTab, newTab2]) {
1231             if (!parent) {
1232               continue;
1233             }
1235             parent.removeAttribute("type");
1236             if (parent.menupopup) {
1237               parent.menupopup.remove();
1238             }
1240             if (containersEnabled) {
1241               parent.setAttribute("context", "new-tab-button-popup");
1243               let popup = document
1244                 .getElementById("new-tab-button-popup")
1245                 .cloneNode(true);
1246               popup.removeAttribute("id");
1247               popup.className = "new-tab-popup";
1248               popup.setAttribute("position", "after_end");
1249               parent.prepend(popup);
1250               parent.setAttribute("type", "menu");
1251               // Update tooltip text
1252               nodeToTooltipMap[parent.id] = newTabLeftClickOpensContainersMenu
1253                 ? "newTabAlwaysContainer.tooltip"
1254                 : "newTabContainer.tooltip";
1255             } else {
1256               nodeToTooltipMap[parent.id] = "newTabButton.tooltip";
1257               parent.removeAttribute("context", "new-tab-button-popup");
1258             }
1259             // evict from tooltip cache
1260             gDynamicTooltipCache.delete(parent.id);
1262             // If containers and press-hold container menu are both used,
1263             // add to gClickAndHoldListenersOnElement; otherwise, remove.
1264             if (containersEnabled && !newTabLeftClickOpensContainersMenu) {
1265               gClickAndHoldListenersOnElement.add(parent);
1266             } else {
1267               gClickAndHoldListenersOnElement.remove(parent);
1268             }
1269           }
1271           break;
1272       }
1273     }
1275     _updateCloseButtons() {
1276       // If we're overflowing, tabs are at their minimum widths.
1277       if (this.hasAttribute("overflow")) {
1278         this.setAttribute("closebuttons", "activetab");
1279         return;
1280       }
1282       if (this._closeButtonsUpdatePending) {
1283         return;
1284       }
1285       this._closeButtonsUpdatePending = true;
1287       // Wait until after the next paint to get current layout data from
1288       // getBoundsWithoutFlushing.
1289       window.requestAnimationFrame(() => {
1290         window.requestAnimationFrame(() => {
1291           this._closeButtonsUpdatePending = false;
1293           // The scrollbox may have started overflowing since we checked
1294           // overflow earlier, so check again.
1295           if (this.hasAttribute("overflow")) {
1296             this.setAttribute("closebuttons", "activetab");
1297             return;
1298           }
1300           // Check if tab widths are below the threshold where we want to
1301           // remove close buttons from background tabs so that people don't
1302           // accidentally close tabs by selecting them.
1303           let rect = ele => {
1304             return window.windowUtils.getBoundsWithoutFlushing(ele);
1305           };
1306           let tab = this._getVisibleTabs()[gBrowser._numPinnedTabs];
1307           if (tab && rect(tab).width <= this._tabClipWidth) {
1308             this.setAttribute("closebuttons", "activetab");
1309           } else {
1310             this.removeAttribute("closebuttons");
1311           }
1312         });
1313       });
1314     }
1316     _updateHiddenTabsStatus() {
1317       this.toggleAttribute(
1318         "hashiddentabs",
1319         gBrowser.visibleTabs.length < gBrowser.tabs.length
1320       );
1321     }
1323     _handleTabSelect(aInstant) {
1324       let selectedTab = this.selectedItem;
1325       if (this.hasAttribute("overflow")) {
1326         this.arrowScrollbox.ensureElementIsVisible(selectedTab, aInstant);
1327       }
1329       selectedTab._notselectedsinceload = false;
1330     }
1332     /**
1333      * Try to keep the active tab's close button under the mouse cursor
1334      */
1335     _lockTabSizing(aTab, aTabWidth) {
1336       let tabs = this._getVisibleTabs();
1337       if (!tabs.length) {
1338         return;
1339       }
1341       var isEndTab = aTab._tPos > tabs[tabs.length - 1]._tPos;
1343       if (!this._tabDefaultMaxWidth) {
1344         this._tabDefaultMaxWidth = parseFloat(
1345           window.getComputedStyle(aTab).maxWidth
1346         );
1347       }
1348       this._lastTabClosedByMouse = true;
1349       this._scrollButtonWidth = window.windowUtils.getBoundsWithoutFlushing(
1350         this.arrowScrollbox._scrollButtonDown
1351       ).width;
1353       if (this.hasAttribute("overflow")) {
1354         // Don't need to do anything if we're in overflow mode and aren't scrolled
1355         // all the way to the right, or if we're closing the last tab.
1356         if (isEndTab || !this.arrowScrollbox._scrollButtonDown.disabled) {
1357           return;
1358         }
1359         // If the tab has an owner that will become the active tab, the owner will
1360         // be to the left of it, so we actually want the left tab to slide over.
1361         // This can't be done as easily in non-overflow mode, so we don't bother.
1362         if (aTab.owner) {
1363           return;
1364         }
1365         this._expandSpacerBy(aTabWidth);
1366       } else {
1367         // non-overflow mode
1368         // Locking is neither in effect nor needed, so let tabs expand normally.
1369         if (isEndTab && !this._hasTabTempMaxWidth) {
1370           return;
1371         }
1372         let numPinned = gBrowser._numPinnedTabs;
1373         // Force tabs to stay the same width, unless we're closing the last tab,
1374         // which case we need to let them expand just enough so that the overall
1375         // tabbar width is the same.
1376         if (isEndTab) {
1377           let numNormalTabs = tabs.length - numPinned;
1378           aTabWidth = (aTabWidth * (numNormalTabs + 1)) / numNormalTabs;
1379           if (aTabWidth > this._tabDefaultMaxWidth) {
1380             aTabWidth = this._tabDefaultMaxWidth;
1381           }
1382         }
1383         aTabWidth += "px";
1384         let tabsToReset = [];
1385         for (let i = numPinned; i < tabs.length; i++) {
1386           let tab = tabs[i];
1387           tab.style.setProperty("max-width", aTabWidth, "important");
1388           if (!isEndTab) {
1389             // keep tabs the same width
1390             tab.style.transition = "none";
1391             tabsToReset.push(tab);
1392           }
1393         }
1395         if (tabsToReset.length) {
1396           window
1397             .promiseDocumentFlushed(() => {})
1398             .then(() => {
1399               window.requestAnimationFrame(() => {
1400                 for (let tab of tabsToReset) {
1401                   tab.style.transition = "";
1402                 }
1403               });
1404             });
1405         }
1407         this._hasTabTempMaxWidth = true;
1408         gBrowser.addEventListener("mousemove", this);
1409         window.addEventListener("mouseout", this);
1410       }
1411     }
1413     _expandSpacerBy(pixels) {
1414       let spacer = this._closingTabsSpacer;
1415       spacer.style.width = parseFloat(spacer.style.width) + pixels + "px";
1416       this.toggleAttribute("using-closing-tabs-spacer", true);
1417       gBrowser.addEventListener("mousemove", this);
1418       window.addEventListener("mouseout", this);
1419     }
1421     _unlockTabSizing() {
1422       gBrowser.removeEventListener("mousemove", this);
1423       window.removeEventListener("mouseout", this);
1425       if (this._hasTabTempMaxWidth) {
1426         this._hasTabTempMaxWidth = false;
1427         let tabs = this._getVisibleTabs();
1428         for (let i = 0; i < tabs.length; i++) {
1429           tabs[i].style.maxWidth = "";
1430         }
1431       }
1433       if (this.hasAttribute("using-closing-tabs-spacer")) {
1434         this.removeAttribute("using-closing-tabs-spacer");
1435         this._closingTabsSpacer.style.width = 0;
1436       }
1437     }
1439     uiDensityChanged() {
1440       this._positionPinnedTabs();
1441       this._updateCloseButtons();
1442       this._handleTabSelect(true);
1443     }
1445     _positionPinnedTabs() {
1446       let tabs = this._getVisibleTabs();
1447       let numPinned = gBrowser._numPinnedTabs;
1448       let doPosition =
1449         this.hasAttribute("overflow") &&
1450         tabs.length > numPinned &&
1451         numPinned > 0;
1453       this.toggleAttribute("haspinnedtabs", !!numPinned);
1454       this.toggleAttribute("positionpinnedtabs", doPosition);
1456       if (doPosition) {
1457         let layoutData = this._pinnedTabsLayoutCache;
1458         let uiDensity = document.documentElement.getAttribute("uidensity");
1459         if (!layoutData || layoutData.uiDensity != uiDensity) {
1460           let arrowScrollbox = this.arrowScrollbox;
1461           layoutData = this._pinnedTabsLayoutCache = {
1462             uiDensity,
1463             pinnedTabWidth: tabs[0].getBoundingClientRect().width,
1464             scrollStartOffset:
1465               arrowScrollbox.scrollbox.getBoundingClientRect().left -
1466               arrowScrollbox.getBoundingClientRect().left +
1467               parseFloat(
1468                 getComputedStyle(arrowScrollbox.scrollbox).paddingInlineStart
1469               ),
1470           };
1471         }
1473         let width = 0;
1474         for (let i = numPinned - 1; i >= 0; i--) {
1475           let tab = tabs[i];
1476           width += layoutData.pinnedTabWidth;
1477           tab.style.setProperty(
1478             "margin-inline-start",
1479             -(width + layoutData.scrollStartOffset) + "px",
1480             "important"
1481           );
1482           tab._pinnedUnscrollable = true;
1483         }
1484         this.style.setProperty(
1485           "--tab-overflow-pinned-tabs-width",
1486           width + "px"
1487         );
1488       } else {
1489         for (let i = 0; i < numPinned; i++) {
1490           let tab = tabs[i];
1491           tab.style.marginInlineStart = "";
1492           tab._pinnedUnscrollable = false;
1493         }
1495         this.style.removeProperty("--tab-overflow-pinned-tabs-width");
1496       }
1498       if (this._lastNumPinned != numPinned) {
1499         this._lastNumPinned = numPinned;
1500         this._handleTabSelect(true);
1501       }
1502     }
1504     _animateTabMove(event) {
1505       let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
1506       let movingTabs = draggedTab._dragData.movingTabs;
1508       if (!this.hasAttribute("movingtab")) {
1509         this.toggleAttribute("movingtab", true);
1510         gNavToolbox.toggleAttribute("movingtab", true);
1511         if (!draggedTab.multiselected) {
1512           this.selectedItem = draggedTab;
1513         }
1514       }
1516       if (!("animLastScreenX" in draggedTab._dragData)) {
1517         draggedTab._dragData.animLastScreenX = draggedTab._dragData.screenX;
1518       }
1520       let screenX = event.screenX;
1521       if (screenX == draggedTab._dragData.animLastScreenX) {
1522         return;
1523       }
1525       // Direction of the mouse movement.
1526       let ltrMove = screenX > draggedTab._dragData.animLastScreenX;
1528       draggedTab._dragData.animLastScreenX = screenX;
1530       let pinned = draggedTab.pinned;
1531       let numPinned = gBrowser._numPinnedTabs;
1532       let tabs = this._getVisibleTabs().slice(
1533         pinned ? 0 : numPinned,
1534         pinned ? numPinned : undefined
1535       );
1537       if (RTL_UI) {
1538         tabs.reverse();
1539         // Copy moving tabs array to avoid infinite reversing.
1540         movingTabs = [...movingTabs].reverse();
1541       }
1542       let tabWidth = draggedTab.getBoundingClientRect().width;
1543       let shiftWidth = tabWidth * movingTabs.length;
1544       draggedTab._dragData.tabWidth = tabWidth;
1546       // Move the dragged tab based on the mouse position.
1548       let leftTab = tabs[0];
1549       let rightTab = tabs[tabs.length - 1];
1550       let rightMovingTabScreenX = movingTabs[movingTabs.length - 1].screenX;
1551       let leftMovingTabScreenX = movingTabs[0].screenX;
1552       let translateX = screenX - draggedTab._dragData.screenX;
1553       if (!pinned) {
1554         translateX +=
1555           this.arrowScrollbox.scrollbox.scrollLeft -
1556           draggedTab._dragData.scrollX;
1557       }
1558       let leftBound = leftTab.screenX - leftMovingTabScreenX;
1559       let rightBound =
1560         rightTab.screenX +
1561         rightTab.getBoundingClientRect().width -
1562         (rightMovingTabScreenX + tabWidth);
1563       translateX = Math.min(Math.max(translateX, leftBound), rightBound);
1565       for (let tab of movingTabs) {
1566         tab.style.transform = "translateX(" + translateX + "px)";
1567       }
1569       draggedTab._dragData.translateX = translateX;
1571       // Determine what tab we're dragging over.
1572       // * Single tab dragging: Point of reference is the center of the dragged tab. If that
1573       //   point touches a background tab, the dragged tab would take that
1574       //   tab's position when dropped.
1575       // * Multiple tabs dragging: All dragged tabs are one "giant" tab with two
1576       //   points of reference (center of tabs on the extremities). When
1577       //   mouse is moving from left to right, the right reference gets activated,
1578       //   otherwise the left reference will be used. Everything else works the same
1579       //   as single tab dragging.
1580       // * We're doing a binary search in order to reduce the amount of
1581       //   tabs we need to check.
1583       tabs = tabs.filter(t => !movingTabs.includes(t) || t == draggedTab);
1584       let leftTabCenter = leftMovingTabScreenX + translateX + tabWidth / 2;
1585       let rightTabCenter = rightMovingTabScreenX + translateX + tabWidth / 2;
1586       let tabCenter = ltrMove ? rightTabCenter : leftTabCenter;
1587       let newIndex = -1;
1588       let oldIndex =
1589         "animDropIndex" in draggedTab._dragData
1590           ? draggedTab._dragData.animDropIndex
1591           : movingTabs[0]._tPos;
1592       let low = 0;
1593       let high = tabs.length - 1;
1594       while (low <= high) {
1595         let mid = Math.floor((low + high) / 2);
1596         if (tabs[mid] == draggedTab && ++mid > high) {
1597           break;
1598         }
1599         screenX = tabs[mid].screenX + getTabShift(tabs[mid], oldIndex);
1600         if (screenX > tabCenter) {
1601           high = mid - 1;
1602         } else if (
1603           screenX + tabs[mid].getBoundingClientRect().width <
1604           tabCenter
1605         ) {
1606           low = mid + 1;
1607         } else {
1608           newIndex = tabs[mid]._tPos;
1609           break;
1610         }
1611       }
1612       if (newIndex >= oldIndex) {
1613         newIndex++;
1614       }
1615       if (newIndex < 0 || newIndex == oldIndex) {
1616         return;
1617       }
1618       draggedTab._dragData.animDropIndex = newIndex;
1620       // Shift background tabs to leave a gap where the dragged tab
1621       // would currently be dropped.
1623       for (let tab of tabs) {
1624         if (tab != draggedTab) {
1625           let shift = getTabShift(tab, newIndex);
1626           tab.style.transform = shift ? "translateX(" + shift + "px)" : "";
1627         }
1628       }
1630       function getTabShift(tab, dropIndex) {
1631         if (tab._tPos < draggedTab._tPos && tab._tPos >= dropIndex) {
1632           return RTL_UI ? -shiftWidth : shiftWidth;
1633         }
1634         if (tab._tPos > draggedTab._tPos && tab._tPos < dropIndex) {
1635           return RTL_UI ? shiftWidth : -shiftWidth;
1636         }
1637         return 0;
1638       }
1639     }
1641     _finishAnimateTabMove() {
1642       if (!this.hasAttribute("movingtab")) {
1643         return;
1644       }
1646       for (let tab of this._getVisibleTabs()) {
1647         tab.style.transform = "";
1648       }
1650       this.removeAttribute("movingtab");
1651       gNavToolbox.removeAttribute("movingtab");
1653       this._handleTabSelect();
1654     }
1656     /**
1657      * Regroup all selected tabs around the
1658      * tab in param
1659      */
1660     _groupSelectedTabs(tab) {
1661       let draggedTabPos = tab._tPos;
1662       let selectedTabs = gBrowser.selectedTabs;
1663       let animate = !gReduceMotion;
1665       tab.groupingTabsData = {
1666         finished: !animate,
1667       };
1669       // Animate left selected tabs
1671       let insertAtPos = draggedTabPos - 1;
1672       for (let i = selectedTabs.indexOf(tab) - 1; i > -1; i--) {
1673         let movingTab = selectedTabs[i];
1674         insertAtPos = newIndex(movingTab, insertAtPos);
1676         if (animate) {
1677           movingTab.groupingTabsData = {};
1678           addAnimationData(movingTab, insertAtPos, "left");
1679         } else {
1680           gBrowser.moveTabTo(movingTab, insertAtPos);
1681         }
1682         insertAtPos--;
1683       }
1685       // Animate right selected tabs
1687       insertAtPos = draggedTabPos + 1;
1688       for (
1689         let i = selectedTabs.indexOf(tab) + 1;
1690         i < selectedTabs.length;
1691         i++
1692       ) {
1693         let movingTab = selectedTabs[i];
1694         insertAtPos = newIndex(movingTab, insertAtPos);
1696         if (animate) {
1697           movingTab.groupingTabsData = {};
1698           addAnimationData(movingTab, insertAtPos, "right");
1699         } else {
1700           gBrowser.moveTabTo(movingTab, insertAtPos);
1701         }
1702         insertAtPos++;
1703       }
1705       // Slide the relevant tabs to their new position.
1706       for (let t of this._getVisibleTabs()) {
1707         if (t.groupingTabsData && t.groupingTabsData.translateX) {
1708           let translateX = (RTL_UI ? -1 : 1) * t.groupingTabsData.translateX;
1709           t.style.transform = "translateX(" + translateX + "px)";
1710         }
1711       }
1713       function newIndex(aTab, index) {
1714         // Don't allow mixing pinned and unpinned tabs.
1715         if (aTab.pinned) {
1716           return Math.min(index, gBrowser._numPinnedTabs - 1);
1717         }
1718         return Math.max(index, gBrowser._numPinnedTabs);
1719       }
1721       function addAnimationData(movingTab, movingTabNewIndex, side) {
1722         let movingTabOldIndex = movingTab._tPos;
1724         if (movingTabOldIndex == movingTabNewIndex) {
1725           // movingTab is already at the right position
1726           // and thus don't need to be animated.
1727           return;
1728         }
1730         let movingTabWidth = movingTab.getBoundingClientRect().width;
1731         let shift = (movingTabNewIndex - movingTabOldIndex) * movingTabWidth;
1733         movingTab.groupingTabsData.animate = true;
1734         movingTab.toggleAttribute("tab-grouping", true);
1736         movingTab.groupingTabsData.translateX = shift;
1738         let postTransitionCleanup = () => {
1739           movingTab.groupingTabsData.newIndex = movingTabNewIndex;
1740           movingTab.groupingTabsData.animate = false;
1741         };
1742         if (gReduceMotion) {
1743           postTransitionCleanup();
1744         } else {
1745           let onTransitionEnd = transitionendEvent => {
1746             if (
1747               transitionendEvent.propertyName != "transform" ||
1748               transitionendEvent.originalTarget != movingTab
1749             ) {
1750               return;
1751             }
1752             movingTab.removeEventListener("transitionend", onTransitionEnd);
1753             postTransitionCleanup();
1754           };
1756           movingTab.addEventListener("transitionend", onTransitionEnd);
1757         }
1759         // Add animation data for tabs between movingTab (selected
1760         // tab moving towards the dragged tab) and draggedTab.
1761         // Those tabs in the middle should move in
1762         // the opposite direction of movingTab.
1764         let lowerIndex = Math.min(movingTabOldIndex, draggedTabPos);
1765         let higherIndex = Math.max(movingTabOldIndex, draggedTabPos);
1767         for (let i = lowerIndex + 1; i < higherIndex; i++) {
1768           let middleTab = gBrowser.visibleTabs[i];
1770           if (middleTab.pinned != movingTab.pinned) {
1771             // Don't mix pinned and unpinned tabs
1772             break;
1773           }
1775           if (middleTab.multiselected) {
1776             // Skip because this selected tab should
1777             // be shifted towards the dragged Tab.
1778             continue;
1779           }
1781           if (
1782             !middleTab.groupingTabsData ||
1783             !middleTab.groupingTabsData.translateX
1784           ) {
1785             middleTab.groupingTabsData = { translateX: 0 };
1786           }
1787           if (side == "left") {
1788             middleTab.groupingTabsData.translateX -= movingTabWidth;
1789           } else {
1790             middleTab.groupingTabsData.translateX += movingTabWidth;
1791           }
1793           middleTab.toggleAttribute("tab-grouping", true);
1794         }
1795       }
1796     }
1798     _finishGroupSelectedTabs(tab) {
1799       if (!tab.groupingTabsData || tab.groupingTabsData.finished) {
1800         return;
1801       }
1803       tab.groupingTabsData.finished = true;
1805       let selectedTabs = gBrowser.selectedTabs;
1806       let tabIndex = selectedTabs.indexOf(tab);
1808       // Moving left tabs
1809       for (let i = tabIndex - 1; i > -1; i--) {
1810         let movingTab = selectedTabs[i];
1811         if (movingTab.groupingTabsData.newIndex) {
1812           gBrowser.moveTabTo(movingTab, movingTab.groupingTabsData.newIndex);
1813         }
1814       }
1816       // Moving right tabs
1817       for (let i = tabIndex + 1; i < selectedTabs.length; i++) {
1818         let movingTab = selectedTabs[i];
1819         if (movingTab.groupingTabsData.newIndex) {
1820           gBrowser.moveTabTo(movingTab, movingTab.groupingTabsData.newIndex);
1821         }
1822       }
1824       for (let t of this._getVisibleTabs()) {
1825         t.style.transform = "";
1826         t.removeAttribute("tab-grouping");
1827         delete t.groupingTabsData;
1828       }
1829     }
1831     _isGroupTabsAnimationOver() {
1832       for (let tab of gBrowser.selectedTabs) {
1833         if (tab.groupingTabsData && tab.groupingTabsData.animate) {
1834           return false;
1835         }
1836       }
1837       return true;
1838     }
1840     handleEvent(aEvent) {
1841       switch (aEvent.type) {
1842         case "mouseout":
1843           // If the "related target" (the node to which the pointer went) is not
1844           // a child of the current document, the mouse just left the window.
1845           let relatedTarget = aEvent.relatedTarget;
1846           if (relatedTarget && relatedTarget.ownerDocument == document) {
1847             break;
1848           }
1849         // fall through
1850         case "mousemove":
1851           if (document.getElementById("tabContextMenu").state != "open") {
1852             this._unlockTabSizing();
1853           }
1854           break;
1855         case "mouseleave":
1856           if (this._showCardPreviews) {
1857             let preview = document.getElementById("tabbrowser-tab-preview");
1858             preview.resetDelay();
1859           }
1860           break;
1861         default:
1862           let methodName = `on_${aEvent.type}`;
1863           if (methodName in this) {
1864             this[methodName](aEvent);
1865           } else {
1866             throw new Error(`Unexpected event ${aEvent.type}`);
1867           }
1868       }
1869     }
1871     _notifyBackgroundTab(aTab) {
1872       if (aTab.pinned || aTab.hidden || !this.hasAttribute("overflow")) {
1873         return;
1874       }
1876       this._lastTabToScrollIntoView = aTab;
1877       if (!this._backgroundTabScrollPromise) {
1878         this._backgroundTabScrollPromise = window
1879           .promiseDocumentFlushed(() => {
1880             let lastTabRect =
1881               this._lastTabToScrollIntoView.getBoundingClientRect();
1882             let selectedTab = this.selectedItem;
1883             if (selectedTab.pinned) {
1884               selectedTab = null;
1885             } else {
1886               selectedTab = selectedTab.getBoundingClientRect();
1887               selectedTab = {
1888                 left: selectedTab.left,
1889                 right: selectedTab.right,
1890               };
1891             }
1892             return [
1893               this._lastTabToScrollIntoView,
1894               this.arrowScrollbox.scrollClientRect,
1895               { left: lastTabRect.left, right: lastTabRect.right },
1896               selectedTab,
1897             ];
1898           })
1899           .then(([tabToScrollIntoView, scrollRect, tabRect, selectedRect]) => {
1900             // First off, remove the promise so we can re-enter if necessary.
1901             delete this._backgroundTabScrollPromise;
1902             // Then, if the layout info isn't for the last-scrolled-to-tab, re-run
1903             // the code above to get layout info for *that* tab, and don't do
1904             // anything here, as we really just want to run this for the last-opened tab.
1905             if (this._lastTabToScrollIntoView != tabToScrollIntoView) {
1906               this._notifyBackgroundTab(this._lastTabToScrollIntoView);
1907               return;
1908             }
1909             delete this._lastTabToScrollIntoView;
1910             // Is the new tab already completely visible?
1911             if (
1912               scrollRect.left <= tabRect.left &&
1913               tabRect.right <= scrollRect.right
1914             ) {
1915               return;
1916             }
1918             if (this.arrowScrollbox.smoothScroll) {
1919               // Can we make both the new tab and the selected tab completely visible?
1920               if (
1921                 !selectedRect ||
1922                 Math.max(
1923                   tabRect.right - selectedRect.left,
1924                   selectedRect.right - tabRect.left
1925                 ) <= scrollRect.width
1926               ) {
1927                 this.arrowScrollbox.ensureElementIsVisible(tabToScrollIntoView);
1928                 return;
1929               }
1931               this.arrowScrollbox.scrollByPixels(
1932                 RTL_UI
1933                   ? selectedRect.right - scrollRect.right
1934                   : selectedRect.left - scrollRect.left
1935               );
1936             }
1938             if (!this._animateElement.hasAttribute("highlight")) {
1939               this._animateElement.toggleAttribute("highlight", true);
1940               setTimeout(
1941                 function (ele) {
1942                   ele.removeAttribute("highlight");
1943                 },
1944                 150,
1945                 this._animateElement
1946               );
1947             }
1948           });
1949       }
1950     }
1952     /**
1953      * Returns the tab where an event happened, or null if it didn't occur on a tab.
1954      *
1955      * @param {Event} event
1956      *   The event for which we want to know on which tab it happened.
1957      * @param {object} options
1958      * @param {boolean} options.ignoreTabSides
1959      *   If set to true: events will only be associated with a tab if they happened
1960      *   on its central part (from 25% to 75%); if they happened on the left or right
1961      *   sides of the tab, the method will return null.
1962      */
1963     _getDragTargetTab(event, { ignoreTabSides = false } = {}) {
1964       let { target } = event;
1965       if (target.nodeType != Node.ELEMENT_NODE) {
1966         target = target.parentElement;
1967       }
1968       let tab = target?.closest("tab");
1969       if (tab && ignoreTabSides) {
1970         let { width } = tab.getBoundingClientRect();
1971         if (
1972           event.screenX < tab.screenX + width * 0.25 ||
1973           event.screenX > tab.screenX + width * 0.75
1974         ) {
1975           return null;
1976         }
1977       }
1978       return tab;
1979     }
1981     _getDropIndex(event) {
1982       let tab = this._getDragTargetTab(event);
1983       if (!tab) {
1984         return this.allTabs.length;
1985       }
1986       let middle = tab.screenX + tab.getBoundingClientRect().width / 2;
1987       let isBeforeMiddle = RTL_UI
1988         ? event.screenX > middle
1989         : event.screenX < middle;
1990       return tab._tPos + (isBeforeMiddle ? 0 : 1);
1991     }
1993     getDropEffectForTabDrag(event) {
1994       var dt = event.dataTransfer;
1996       let isMovingTabs = dt.mozItemCount > 0;
1997       for (let i = 0; i < dt.mozItemCount; i++) {
1998         // tabs are always added as the first type
1999         let types = dt.mozTypesAt(0);
2000         if (types[0] != TAB_DROP_TYPE) {
2001           isMovingTabs = false;
2002           break;
2003         }
2004       }
2006       if (isMovingTabs) {
2007         let sourceNode = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
2008         if (
2009           XULElement.isInstance(sourceNode) &&
2010           sourceNode.localName == "tab" &&
2011           sourceNode.ownerGlobal.isChromeWindow &&
2012           sourceNode.ownerDocument.documentElement.getAttribute("windowtype") ==
2013             "navigator:browser" &&
2014           sourceNode.ownerGlobal.gBrowser.tabContainer == sourceNode.container
2015         ) {
2016           // Do not allow transfering a private tab to a non-private window
2017           // and vice versa.
2018           if (
2019             PrivateBrowsingUtils.isWindowPrivate(window) !=
2020             PrivateBrowsingUtils.isWindowPrivate(sourceNode.ownerGlobal)
2021           ) {
2022             return "none";
2023           }
2025           if (
2026             window.gMultiProcessBrowser !=
2027             sourceNode.ownerGlobal.gMultiProcessBrowser
2028           ) {
2029             return "none";
2030           }
2032           if (
2033             window.gFissionBrowser != sourceNode.ownerGlobal.gFissionBrowser
2034           ) {
2035             return "none";
2036           }
2038           return dt.dropEffect == "copy" ? "copy" : "move";
2039         }
2040       }
2042       if (browserDragAndDrop.canDropLink(event)) {
2043         return "link";
2044       }
2045       return "none";
2046     }
2048     _handleNewTab(tab) {
2049       if (tab.container != this) {
2050         return;
2051       }
2052       tab._fullyOpen = true;
2053       gBrowser.tabAnimationsInProgress--;
2055       this._updateCloseButtons();
2057       if (tab.hasAttribute("selected")) {
2058         this._handleTabSelect();
2059       } else if (!tab.hasAttribute("skipbackgroundnotify")) {
2060         this._notifyBackgroundTab(tab);
2061       }
2063       // XXXmano: this is a temporary workaround for bug 345399
2064       // We need to manually update the scroll buttons disabled state
2065       // if a tab was inserted to the overflow area or removed from it
2066       // without any scrolling and when the tabbar has already
2067       // overflowed.
2068       this.arrowScrollbox._updateScrollButtonsDisabledState();
2070       // If this browser isn't lazy (indicating it's probably created by
2071       // session restore), preload the next about:newtab if we don't
2072       // already have a preloaded browser.
2073       if (tab.linkedPanel) {
2074         NewTabPagePreloading.maybeCreatePreloadedBrowser(window);
2075       }
2077       if (UserInteraction.running("browser.tabs.opening", window)) {
2078         UserInteraction.finish("browser.tabs.opening", window);
2079       }
2080     }
2082     _canAdvanceToTab(aTab) {
2083       return !aTab.closing;
2084     }
2086     /**
2087      * Returns the panel associated with a tab if it has a connected browser
2088      * and/or it is the selected tab.
2089      * For background lazy browsers, this will return null.
2090      */
2091     getRelatedElement(aTab) {
2092       if (!aTab) {
2093         return null;
2094       }
2096       // Cannot access gBrowser before it's initialized.
2097       if (!gBrowser._initialized) {
2098         return this.tabbox.tabpanels.firstElementChild;
2099       }
2101       // If the tab's browser is lazy, we need to `_insertBrowser` in order
2102       // to have a linkedPanel.  This will also serve to bind the browser
2103       // and make it ready to use. We only do this if the tab is selected
2104       // because otherwise, callers might end up unintentionally binding the
2105       // browser for lazy background tabs.
2106       if (!aTab.linkedPanel) {
2107         if (!aTab.selected) {
2108           return null;
2109         }
2110         gBrowser._insertBrowser(aTab);
2111       }
2112       return document.getElementById(aTab.linkedPanel);
2113     }
2115     _updateNewTabVisibility() {
2116       // Helper functions to help deal with customize mode wrapping some items
2117       let wrap = n =>
2118         n.parentNode.localName == "toolbarpaletteitem" ? n.parentNode : n;
2119       let unwrap = n =>
2120         n && n.localName == "toolbarpaletteitem" ? n.firstElementChild : n;
2122       // Starting from the tabs element, find the next sibling that:
2123       // - isn't hidden; and
2124       // - isn't the all-tabs button.
2125       // If it's the new tab button, consider the new tab button adjacent to the tabs.
2126       // If the new tab button is marked as adjacent and the tabstrip doesn't
2127       // overflow, we'll display the 'new tab' button inline in the tabstrip.
2128       // In all other cases, the separate new tab button is displayed in its
2129       // customized location.
2130       let sib = this;
2131       do {
2132         sib = unwrap(wrap(sib).nextElementSibling);
2133       } while (sib && (sib.hidden || sib.id == "alltabs-button"));
2135       this.toggleAttribute(
2136         "hasadjacentnewtabbutton",
2137         sib && sib.id == "new-tab-button"
2138       );
2139     }
2141     onWidgetAfterDOMChange(aNode, aNextNode, aContainer) {
2142       if (
2143         aContainer.ownerDocument == document &&
2144         aContainer.id == "TabsToolbar-customization-target"
2145       ) {
2146         this._updateNewTabVisibility();
2147       }
2148     }
2150     onAreaNodeRegistered(aArea, aContainer) {
2151       if (aContainer.ownerDocument == document && aArea == "TabsToolbar") {
2152         this._updateNewTabVisibility();
2153       }
2154     }
2156     onAreaReset(aArea, aContainer) {
2157       this.onAreaNodeRegistered(aArea, aContainer);
2158     }
2160     _hiddenSoundPlayingStatusChanged(tab, opts) {
2161       let closed = opts && opts.closed;
2162       if (!closed && tab.soundPlaying && tab.hidden) {
2163         this._hiddenSoundPlayingTabs.add(tab);
2164         this.toggleAttribute("hiddensoundplaying", true);
2165       } else {
2166         this._hiddenSoundPlayingTabs.delete(tab);
2167         if (this._hiddenSoundPlayingTabs.size == 0) {
2168           this.removeAttribute("hiddensoundplaying");
2169         }
2170       }
2171     }
2173     destroy() {
2174       if (this.boundObserve) {
2175         Services.prefs.removeObserver("privacy.userContext", this.boundObserve);
2176       }
2177       CustomizableUI.removeListener(this);
2178     }
2180     updateTabIndicatorAttr(tab) {
2181       const theseAttributes = ["soundplaying", "muted", "activemedia-blocked"];
2182       const notTheseAttributes = ["pinned", "sharing", "crashed"];
2184       if (notTheseAttributes.some(attr => tab.hasAttribute(attr))) {
2185         tab.removeAttribute("indicator-replaces-favicon");
2186         return;
2187       }
2189       tab.toggleAttribute(
2190         "indicator-replaces-favicon",
2191         theseAttributes.some(attr => tab.hasAttribute(attr))
2192       );
2193     }
2194   }
2196   customElements.define("tabbrowser-tabs", MozTabbrowserTabs, {
2197     extends: "tabs",
2198   });