forked from mirrors/gecko-dev
		
	 a1eb3c4eb8
			
		
	
	
		a1eb3c4eb8
		
	
	
	
	
		
			
			Depends on D166026 Differential Revision: https://phabricator.services.mozilla.com/D166027
		
			
				
	
	
		
			2161 lines
		
	
	
	
		
			70 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			2161 lines
		
	
	
	
		
			70 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* This Source Code Form is subject to the terms of the Mozilla Public
 | |
|  * License, v. 2.0. If a copy of the MPL was not distributed with this
 | |
|  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 | |
| 
 | |
| /* eslint-env mozilla/browser-window */
 | |
| 
 | |
| "use strict";
 | |
| 
 | |
| // This is loaded into all browser windows. Wrap in a block to prevent
 | |
| // leaking to window scope.
 | |
| {
 | |
|   class MozTabbrowserTabs extends MozElements.TabsBase {
 | |
|     constructor() {
 | |
|       super();
 | |
| 
 | |
|       this.addEventListener("TabSelect", this);
 | |
|       this.addEventListener("TabClose", this);
 | |
|       this.addEventListener("TabAttrModified", this);
 | |
|       this.addEventListener("TabHide", this);
 | |
|       this.addEventListener("TabShow", this);
 | |
|       this.addEventListener("TabPinned", this);
 | |
|       this.addEventListener("TabUnpinned", this);
 | |
|       this.addEventListener("transitionend", this);
 | |
|       this.addEventListener("dblclick", this);
 | |
|       this.addEventListener("click", this);
 | |
|       this.addEventListener("click", this, true);
 | |
|       this.addEventListener("keydown", this, { mozSystemGroup: true });
 | |
|       this.addEventListener("dragstart", this);
 | |
|       this.addEventListener("dragover", this);
 | |
|       this.addEventListener("drop", this);
 | |
|       this.addEventListener("dragend", this);
 | |
|       this.addEventListener("dragleave", this);
 | |
|     }
 | |
| 
 | |
|     init() {
 | |
|       this.arrowScrollbox = this.querySelector("arrowscrollbox");
 | |
|       this.arrowScrollbox.addEventListener("wheel", this, true);
 | |
| 
 | |
|       this.baseConnect();
 | |
| 
 | |
|       this._beforeHoveredTab = null;
 | |
|       this._afterHoveredTab = null;
 | |
|       this._hoveredTab = null;
 | |
|       this._blockDblClick = false;
 | |
|       this._tabDropIndicator = this.querySelector(".tab-drop-indicator");
 | |
|       this._dragOverDelay = 350;
 | |
|       this._dragTime = 0;
 | |
|       this._closeButtonsUpdatePending = false;
 | |
|       this._closingTabsSpacer = this.querySelector(".closing-tabs-spacer");
 | |
|       this._tabDefaultMaxWidth = NaN;
 | |
|       this._lastTabClosedByMouse = false;
 | |
|       this._hasTabTempMaxWidth = false;
 | |
|       this._scrollButtonWidth = 0;
 | |
|       this._lastNumPinned = 0;
 | |
|       this._pinnedTabsLayoutCache = null;
 | |
|       this._animateElement = this.arrowScrollbox;
 | |
|       this._tabClipWidth = Services.prefs.getIntPref(
 | |
|         "browser.tabs.tabClipWidth"
 | |
|       );
 | |
|       this._hiddenSoundPlayingTabs = new Set();
 | |
| 
 | |
|       var tab = this.allTabs[0];
 | |
|       tab.label = this.emptyTabTitle;
 | |
| 
 | |
|       // Hide the secondary text for locales where it is unsupported due to size constraints.
 | |
|       const language = Services.locale.appLocaleAsBCP47;
 | |
|       const unsupportedLocales = Services.prefs.getCharPref(
 | |
|         "browser.tabs.secondaryTextUnsupportedLocales"
 | |
|       );
 | |
|       this.toggleAttribute(
 | |
|         "secondarytext-unsupported",
 | |
|         unsupportedLocales.split(",").includes(language.split("-")[0])
 | |
|       );
 | |
| 
 | |
|       this.newTabButton.setAttribute(
 | |
|         "aria-label",
 | |
|         GetDynamicShortcutTooltipText("tabs-newtab-button")
 | |
|       );
 | |
| 
 | |
|       let handleResize = () => {
 | |
|         this._updateCloseButtons();
 | |
|         this._handleTabSelect(true);
 | |
|       };
 | |
|       window.addEventListener("resize", handleResize);
 | |
|       this._fullscreenMutationObserver = new MutationObserver(handleResize);
 | |
|       this._fullscreenMutationObserver.observe(document.documentElement, {
 | |
|         attributeFilter: ["inFullscreen", "inDOMFullscreen"],
 | |
|       });
 | |
| 
 | |
|       this.boundObserve = (...args) => this.observe(...args);
 | |
|       Services.prefs.addObserver("privacy.userContext", this.boundObserve);
 | |
|       this.observe(null, "nsPref:changed", "privacy.userContext.enabled");
 | |
| 
 | |
|       XPCOMUtils.defineLazyPreferenceGetter(
 | |
|         this,
 | |
|         "_tabMinWidthPref",
 | |
|         "browser.tabs.tabMinWidth",
 | |
|         null,
 | |
|         (pref, prevValue, newValue) => (this._tabMinWidth = newValue),
 | |
|         newValue => {
 | |
|           const LIMIT = 50;
 | |
|           return Math.max(newValue, LIMIT);
 | |
|         }
 | |
|       );
 | |
| 
 | |
|       this._tabMinWidth = this._tabMinWidthPref;
 | |
| 
 | |
|       this._setPositionalAttributes();
 | |
| 
 | |
|       CustomizableUI.addListener(this);
 | |
|       this._updateNewTabVisibility();
 | |
|       this._initializeArrowScrollbox();
 | |
| 
 | |
|       XPCOMUtils.defineLazyPreferenceGetter(
 | |
|         this,
 | |
|         "_closeTabByDblclick",
 | |
|         "browser.tabs.closeTabByDblclick",
 | |
|         false
 | |
|       );
 | |
| 
 | |
|       if (gMultiProcessBrowser) {
 | |
|         this.tabbox.tabpanels.setAttribute("async", "true");
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     on_TabSelect(event) {
 | |
|       this._handleTabSelect();
 | |
|     }
 | |
| 
 | |
|     on_TabClose(event) {
 | |
|       this._hiddenSoundPlayingStatusChanged(event.target, { closed: true });
 | |
|     }
 | |
| 
 | |
|     on_TabAttrModified(event) {
 | |
|       if (
 | |
|         ["soundplaying", "muted", "activemedia-blocked", "sharing"].some(attr =>
 | |
|           event.detail.changed.includes(attr)
 | |
|         )
 | |
|       ) {
 | |
|         this.updateTabIndicatorAttr(event.target);
 | |
|       }
 | |
| 
 | |
|       if (
 | |
|         event.detail.changed.includes("soundplaying") &&
 | |
|         event.target.hidden
 | |
|       ) {
 | |
|         this._hiddenSoundPlayingStatusChanged(event.target);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     on_TabHide(event) {
 | |
|       if (event.target.soundPlaying) {
 | |
|         this._hiddenSoundPlayingStatusChanged(event.target);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     on_TabShow(event) {
 | |
|       if (event.target.soundPlaying) {
 | |
|         this._hiddenSoundPlayingStatusChanged(event.target);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     on_TabPinned(event) {
 | |
|       this.updateTabIndicatorAttr(event.target);
 | |
|     }
 | |
| 
 | |
|     on_TabUnpinned(event) {
 | |
|       this.updateTabIndicatorAttr(event.target);
 | |
|     }
 | |
| 
 | |
|     on_transitionend(event) {
 | |
|       if (event.propertyName != "max-width") {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       let tab = event.target ? event.target.closest("tab") : null;
 | |
| 
 | |
|       if (tab.getAttribute("fadein") == "true") {
 | |
|         if (tab._fullyOpen) {
 | |
|           this._updateCloseButtons();
 | |
|         } else {
 | |
|           this._handleNewTab(tab);
 | |
|         }
 | |
|       } else if (tab.closing) {
 | |
|         gBrowser._endRemoveTab(tab);
 | |
|       }
 | |
| 
 | |
|       let evt = new CustomEvent("TabAnimationEnd", { bubbles: true });
 | |
|       tab.dispatchEvent(evt);
 | |
|     }
 | |
| 
 | |
|     on_dblclick(event) {
 | |
|       // When the tabbar has an unified appearance with the titlebar
 | |
|       // and menubar, a double-click in it should have the same behavior
 | |
|       // as double-clicking the titlebar
 | |
|       if (TabsInTitlebar.enabled) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       if (event.button != 0 || event.originalTarget.localName != "scrollbox") {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       if (!this._blockDblClick) {
 | |
|         BrowserOpenTab();
 | |
|       }
 | |
| 
 | |
|       event.preventDefault();
 | |
|     }
 | |
| 
 | |
|     on_click(event) {
 | |
|       if (event.eventPhase == Event.CAPTURING_PHASE && event.button == 0) {
 | |
|         /* Catches extra clicks meant for the in-tab close button.
 | |
|          * Placed here to avoid leaking (a temporary handler added from the
 | |
|          * in-tab close button binding would close over the tab and leak it
 | |
|          * until the handler itself was removed). (bug 897751)
 | |
|          *
 | |
|          * The only sequence in which a second click event (i.e. dblclik)
 | |
|          * can be dispatched on an in-tab close button is when it is shown
 | |
|          * after the first click (i.e. the first click event was dispatched
 | |
|          * on the tab). This happens when we show the close button only on
 | |
|          * the active tab. (bug 352021)
 | |
|          * The only sequence in which a third click event can be dispatched
 | |
|          * on an in-tab close button is when the tab was opened with a
 | |
|          * double click on the tabbar. (bug 378344)
 | |
|          * In both cases, it is most likely that the close button area has
 | |
|          * been accidentally clicked, therefore we do not close the tab.
 | |
|          *
 | |
|          * We don't want to ignore processing of more than one click event,
 | |
|          * though, since the user might actually be repeatedly clicking to
 | |
|          * close many tabs at once.
 | |
|          */
 | |
|         let target = event.originalTarget;
 | |
|         if (target.classList.contains("tab-close-button")) {
 | |
|           // We preemptively set this to allow the closing-multiple-tabs-
 | |
|           // in-a-row case.
 | |
|           if (this._blockDblClick) {
 | |
|             target._ignoredCloseButtonClicks = true;
 | |
|           } else if (event.detail > 1 && !target._ignoredCloseButtonClicks) {
 | |
|             target._ignoredCloseButtonClicks = true;
 | |
|             event.stopPropagation();
 | |
|             return;
 | |
|           } else {
 | |
|             // Reset the "ignored click" flag
 | |
|             target._ignoredCloseButtonClicks = false;
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         /* Protects from close-tab-button errant doubleclick:
 | |
|          * Since we're removing the event target, if the user
 | |
|          * double-clicks the button, the dblclick event will be dispatched
 | |
|          * with the tabbar as its event target (and explicit/originalTarget),
 | |
|          * which treats that as a mouse gesture for opening a new tab.
 | |
|          * In this context, we're manually blocking the dblclick event.
 | |
|          */
 | |
|         if (this._blockDblClick) {
 | |
|           if (!("_clickedTabBarOnce" in this)) {
 | |
|             this._clickedTabBarOnce = true;
 | |
|             return;
 | |
|           }
 | |
|           delete this._clickedTabBarOnce;
 | |
|           this._blockDblClick = false;
 | |
|         }
 | |
|       } else if (
 | |
|         event.eventPhase == Event.BUBBLING_PHASE &&
 | |
|         event.button == 1
 | |
|       ) {
 | |
|         let tab = event.target ? event.target.closest("tab") : null;
 | |
|         if (tab) {
 | |
|           gBrowser.removeTab(tab, {
 | |
|             animate: true,
 | |
|             byMouse: event.mozInputSource == MouseEvent.MOZ_SOURCE_MOUSE,
 | |
|           });
 | |
|         } else if (event.originalTarget.closest("scrollbox")) {
 | |
|           // The user middleclicked on the tabstrip. Check whether the click
 | |
|           // was dispatched on the open space of it.
 | |
|           let visibleTabs = this._getVisibleTabs();
 | |
|           let lastTab = visibleTabs[visibleTabs.length - 1];
 | |
|           let winUtils = window.windowUtils;
 | |
|           let endOfTab = winUtils.getBoundsWithoutFlushing(lastTab)[
 | |
|             RTL_UI ? "left" : "right"
 | |
|           ];
 | |
|           if (
 | |
|             (!RTL_UI && event.clientX > endOfTab) ||
 | |
|             (RTL_UI && event.clientX < endOfTab)
 | |
|           ) {
 | |
|             BrowserOpenTab();
 | |
|           }
 | |
|         } else {
 | |
|           return;
 | |
|         }
 | |
| 
 | |
|         event.preventDefault();
 | |
|         event.stopPropagation();
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     on_keydown(event) {
 | |
|       let { altKey, shiftKey } = event;
 | |
|       let [accel, nonAccel] =
 | |
|         AppConstants.platform == "macosx"
 | |
|           ? [event.metaKey, event.ctrlKey]
 | |
|           : [event.ctrlKey, event.metaKey];
 | |
| 
 | |
|       let keyComboForMove = accel && shiftKey && !altKey && !nonAccel;
 | |
|       let keyComboForFocus = accel && !shiftKey && !altKey && !nonAccel;
 | |
| 
 | |
|       if (!keyComboForMove && !keyComboForFocus) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       // Don't check if the event was already consumed because tab navigation
 | |
|       // should work always for better user experience.
 | |
|       let { visibleTabs, selectedTab } = gBrowser;
 | |
|       let { arrowKeysShouldWrap } = this;
 | |
|       let focusedTabIndex = this.ariaFocusedIndex;
 | |
|       if (focusedTabIndex == -1) {
 | |
|         focusedTabIndex = visibleTabs.indexOf(selectedTab);
 | |
|       }
 | |
|       let lastFocusedTabIndex = focusedTabIndex;
 | |
|       switch (event.keyCode) {
 | |
|         case KeyEvent.DOM_VK_UP:
 | |
|           if (keyComboForMove) {
 | |
|             gBrowser.moveTabBackward();
 | |
|           } else {
 | |
|             focusedTabIndex--;
 | |
|           }
 | |
|           break;
 | |
|         case KeyEvent.DOM_VK_DOWN:
 | |
|           if (keyComboForMove) {
 | |
|             gBrowser.moveTabForward();
 | |
|           } else {
 | |
|             focusedTabIndex++;
 | |
|           }
 | |
|           break;
 | |
|         case KeyEvent.DOM_VK_RIGHT:
 | |
|         case KeyEvent.DOM_VK_LEFT:
 | |
|           if (keyComboForMove) {
 | |
|             gBrowser.moveTabOver(event);
 | |
|           } else if (
 | |
|             (!RTL_UI && event.keyCode == KeyEvent.DOM_VK_RIGHT) ||
 | |
|             (RTL_UI && event.keyCode == KeyEvent.DOM_VK_LEFT)
 | |
|           ) {
 | |
|             focusedTabIndex++;
 | |
|           } else {
 | |
|             focusedTabIndex--;
 | |
|           }
 | |
|           break;
 | |
|         case KeyEvent.DOM_VK_HOME:
 | |
|           if (keyComboForMove) {
 | |
|             gBrowser.moveTabToStart();
 | |
|           } else {
 | |
|             focusedTabIndex = 0;
 | |
|           }
 | |
|           break;
 | |
|         case KeyEvent.DOM_VK_END:
 | |
|           if (keyComboForMove) {
 | |
|             gBrowser.moveTabToEnd();
 | |
|           } else {
 | |
|             focusedTabIndex = visibleTabs.length - 1;
 | |
|           }
 | |
|           break;
 | |
|         case KeyEvent.DOM_VK_SPACE:
 | |
|           if (visibleTabs[lastFocusedTabIndex].multiselected) {
 | |
|             gBrowser.removeFromMultiSelectedTabs(
 | |
|               visibleTabs[lastFocusedTabIndex]
 | |
|             );
 | |
|           } else {
 | |
|             gBrowser.addToMultiSelectedTabs(visibleTabs[lastFocusedTabIndex]);
 | |
|           }
 | |
|           break;
 | |
|         default:
 | |
|           // Consume the keydown event for the above keyboard
 | |
|           // shortcuts only.
 | |
|           return;
 | |
|       }
 | |
| 
 | |
|       if (arrowKeysShouldWrap) {
 | |
|         if (focusedTabIndex >= visibleTabs.length) {
 | |
|           focusedTabIndex = 0;
 | |
|         } else if (focusedTabIndex < 0) {
 | |
|           focusedTabIndex = visibleTabs.length - 1;
 | |
|         }
 | |
|       } else {
 | |
|         focusedTabIndex = Math.min(
 | |
|           visibleTabs.length - 1,
 | |
|           Math.max(0, focusedTabIndex)
 | |
|         );
 | |
|       }
 | |
| 
 | |
|       if (keyComboForFocus && focusedTabIndex != lastFocusedTabIndex) {
 | |
|         this.ariaFocusedItem = visibleTabs[focusedTabIndex];
 | |
|       }
 | |
| 
 | |
|       event.preventDefault();
 | |
|     }
 | |
| 
 | |
|     on_dragstart(event) {
 | |
|       var tab = this._getDragTargetTab(event, false);
 | |
|       if (!tab || this._isCustomizing) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       this.startTabDrag(event, tab);
 | |
|     }
 | |
| 
 | |
|     startTabDrag(event, tab, { fromTabList = false } = {}) {
 | |
|       let selectedTabs = gBrowser.selectedTabs;
 | |
|       let otherSelectedTabs = selectedTabs.filter(
 | |
|         selectedTab => selectedTab != tab
 | |
|       );
 | |
|       let dataTransferOrderedTabs = [tab].concat(otherSelectedTabs);
 | |
| 
 | |
|       let dt = event.dataTransfer;
 | |
|       for (let i = 0; i < dataTransferOrderedTabs.length; i++) {
 | |
|         let dtTab = dataTransferOrderedTabs[i];
 | |
| 
 | |
|         dt.mozSetDataAt(TAB_DROP_TYPE, dtTab, i);
 | |
|         let dtBrowser = dtTab.linkedBrowser;
 | |
| 
 | |
|         // We must not set text/x-moz-url or text/plain data here,
 | |
|         // otherwise trying to detach the tab by dropping it on the desktop
 | |
|         // may result in an "internet shortcut"
 | |
|         dt.mozSetDataAt(
 | |
|           "text/x-moz-text-internal",
 | |
|           dtBrowser.currentURI.spec,
 | |
|           i
 | |
|         );
 | |
|       }
 | |
| 
 | |
|       // Set the cursor to an arrow during tab drags.
 | |
|       dt.mozCursor = "default";
 | |
| 
 | |
|       // Set the tab as the source of the drag, which ensures we have a stable
 | |
|       // node to deliver the `dragend` event.  See bug 1345473.
 | |
|       dt.addElement(tab);
 | |
| 
 | |
|       if (tab.multiselected) {
 | |
|         this._groupSelectedTabs(tab);
 | |
|       }
 | |
| 
 | |
|       // Create a canvas to which we capture the current tab.
 | |
|       // Until canvas is HiDPI-aware (bug 780362), we need to scale the desired
 | |
|       // canvas size (in CSS pixels) to the window's backing resolution in order
 | |
|       // to get a full-resolution drag image for use on HiDPI displays.
 | |
|       let scale = window.devicePixelRatio;
 | |
|       let canvas = this._dndCanvas;
 | |
|       if (!canvas) {
 | |
|         this._dndCanvas = canvas = document.createElementNS(
 | |
|           "http://www.w3.org/1999/xhtml",
 | |
|           "canvas"
 | |
|         );
 | |
|         canvas.style.width = "100%";
 | |
|         canvas.style.height = "100%";
 | |
|         canvas.mozOpaque = true;
 | |
|       }
 | |
| 
 | |
|       canvas.width = 160 * scale;
 | |
|       canvas.height = 90 * scale;
 | |
|       let toDrag = canvas;
 | |
|       let dragImageOffset = -16;
 | |
|       let browser = tab.linkedBrowser;
 | |
|       if (gMultiProcessBrowser) {
 | |
|         var context = canvas.getContext("2d");
 | |
|         context.fillStyle = "white";
 | |
|         context.fillRect(0, 0, canvas.width, canvas.height);
 | |
| 
 | |
|         let captureListener;
 | |
|         let platform = AppConstants.platform;
 | |
|         // On Windows and Mac we can update the drag image during a drag
 | |
|         // using updateDragImage. On Linux, we can use a panel.
 | |
|         if (platform == "win" || platform == "macosx") {
 | |
|           captureListener = function() {
 | |
|             dt.updateDragImage(canvas, dragImageOffset, dragImageOffset);
 | |
|           };
 | |
|         } else {
 | |
|           // Create a panel to use it in setDragImage
 | |
|           // which will tell xul to render a panel that follows
 | |
|           // the pointer while a dnd session is on.
 | |
|           if (!this._dndPanel) {
 | |
|             this._dndCanvas = canvas;
 | |
|             this._dndPanel = document.createXULElement("panel");
 | |
|             this._dndPanel.className = "dragfeedback-tab";
 | |
|             this._dndPanel.setAttribute("type", "drag");
 | |
|             let wrapper = document.createElementNS(
 | |
|               "http://www.w3.org/1999/xhtml",
 | |
|               "div"
 | |
|             );
 | |
|             wrapper.style.width = "160px";
 | |
|             wrapper.style.height = "90px";
 | |
|             wrapper.appendChild(canvas);
 | |
|             this._dndPanel.appendChild(wrapper);
 | |
|             document.documentElement.appendChild(this._dndPanel);
 | |
|           }
 | |
|           toDrag = this._dndPanel;
 | |
|         }
 | |
|         // PageThumb is async with e10s but that's fine
 | |
|         // since we can update the image during the dnd.
 | |
|         PageThumbs.captureToCanvas(browser, canvas)
 | |
|           .then(captureListener)
 | |
|           .catch(e => console.error(e));
 | |
|       } else {
 | |
|         // For the non e10s case we can just use PageThumbs
 | |
|         // sync, so let's use the canvas for setDragImage.
 | |
|         PageThumbs.captureToCanvas(browser, canvas).catch(e =>
 | |
|           console.error(e)
 | |
|         );
 | |
|         dragImageOffset = dragImageOffset * scale;
 | |
|       }
 | |
|       dt.setDragImage(toDrag, dragImageOffset, dragImageOffset);
 | |
| 
 | |
|       // _dragData.offsetX/Y give the coordinates that the mouse should be
 | |
|       // positioned relative to the corner of the new window created upon
 | |
|       // dragend such that the mouse appears to have the same position
 | |
|       // relative to the corner of the dragged tab.
 | |
|       function clientX(ele) {
 | |
|         return ele.getBoundingClientRect().left;
 | |
|       }
 | |
|       let tabOffsetX = clientX(tab) - clientX(this);
 | |
|       tab._dragData = {
 | |
|         offsetX: event.screenX - window.screenX - tabOffsetX,
 | |
|         offsetY: event.screenY - window.screenY,
 | |
|         scrollX: this.arrowScrollbox.scrollbox.scrollLeft,
 | |
|         screenX: event.screenX,
 | |
|         movingTabs: (tab.multiselected ? gBrowser.selectedTabs : [tab]).filter(
 | |
|           t => t.pinned == tab.pinned
 | |
|         ),
 | |
|         fromTabList,
 | |
|       };
 | |
| 
 | |
|       event.stopPropagation();
 | |
|     }
 | |
| 
 | |
|     on_dragover(event) {
 | |
|       var effects = this.getDropEffectForTabDrag(event);
 | |
| 
 | |
|       var ind = this._tabDropIndicator;
 | |
|       if (effects == "" || effects == "none") {
 | |
|         ind.hidden = true;
 | |
|         return;
 | |
|       }
 | |
|       event.preventDefault();
 | |
|       event.stopPropagation();
 | |
| 
 | |
|       var arrowScrollbox = this.arrowScrollbox;
 | |
| 
 | |
|       // autoscroll the tab strip if we drag over the scroll
 | |
|       // buttons, even if we aren't dragging a tab, but then
 | |
|       // return to avoid drawing the drop indicator
 | |
|       var pixelsToScroll = 0;
 | |
|       if (this.getAttribute("overflow") == "true") {
 | |
|         switch (event.originalTarget) {
 | |
|           case arrowScrollbox._scrollButtonUp:
 | |
|             pixelsToScroll = arrowScrollbox.scrollIncrement * -1;
 | |
|             break;
 | |
|           case arrowScrollbox._scrollButtonDown:
 | |
|             pixelsToScroll = arrowScrollbox.scrollIncrement;
 | |
|             break;
 | |
|         }
 | |
|         if (pixelsToScroll) {
 | |
|           arrowScrollbox.scrollByPixels(
 | |
|             (RTL_UI ? -1 : 1) * pixelsToScroll,
 | |
|             true
 | |
|           );
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
 | |
|       if (
 | |
|         (effects == "move" || effects == "copy") &&
 | |
|         this == draggedTab.container &&
 | |
|         !draggedTab._dragData.fromTabList
 | |
|       ) {
 | |
|         ind.hidden = true;
 | |
| 
 | |
|         if (!this._isGroupTabsAnimationOver()) {
 | |
|           // Wait for grouping tabs animation to finish
 | |
|           return;
 | |
|         }
 | |
|         this._finishGroupSelectedTabs(draggedTab);
 | |
| 
 | |
|         if (effects == "move") {
 | |
|           this._animateTabMove(event);
 | |
|           return;
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       this._finishAnimateTabMove();
 | |
| 
 | |
|       if (effects == "link") {
 | |
|         let tab = this._getDragTargetTab(event, true);
 | |
|         if (tab) {
 | |
|           if (!this._dragTime) {
 | |
|             this._dragTime = Date.now();
 | |
|           }
 | |
|           if (Date.now() >= this._dragTime + this._dragOverDelay) {
 | |
|             this.selectedItem = tab;
 | |
|           }
 | |
|           ind.hidden = true;
 | |
|           return;
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       var rect = arrowScrollbox.getBoundingClientRect();
 | |
|       var newMargin;
 | |
|       if (pixelsToScroll) {
 | |
|         // if we are scrolling, put the drop indicator at the edge
 | |
|         // so that it doesn't jump while scrolling
 | |
|         let scrollRect = arrowScrollbox.scrollClientRect;
 | |
|         let minMargin = scrollRect.left - rect.left;
 | |
|         let maxMargin = Math.min(
 | |
|           minMargin + scrollRect.width,
 | |
|           scrollRect.right
 | |
|         );
 | |
|         if (RTL_UI) {
 | |
|           [minMargin, maxMargin] = [
 | |
|             this.clientWidth - maxMargin,
 | |
|             this.clientWidth - minMargin,
 | |
|           ];
 | |
|         }
 | |
|         newMargin = pixelsToScroll > 0 ? maxMargin : minMargin;
 | |
|       } else {
 | |
|         let newIndex = this._getDropIndex(event, effects == "link");
 | |
|         let children = this.allTabs;
 | |
|         if (newIndex == children.length) {
 | |
|           let tabRect = this._getVisibleTabs()
 | |
|             .at(-1)
 | |
|             .getBoundingClientRect();
 | |
|           if (RTL_UI) {
 | |
|             newMargin = rect.right - tabRect.left;
 | |
|           } else {
 | |
|             newMargin = tabRect.right - rect.left;
 | |
|           }
 | |
|         } else {
 | |
|           let tabRect = children[newIndex].getBoundingClientRect();
 | |
|           if (RTL_UI) {
 | |
|             newMargin = rect.right - tabRect.right;
 | |
|           } else {
 | |
|             newMargin = tabRect.left - rect.left;
 | |
|           }
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       ind.hidden = false;
 | |
|       newMargin += ind.clientWidth / 2;
 | |
|       if (RTL_UI) {
 | |
|         newMargin *= -1;
 | |
|       }
 | |
|       ind.style.transform = "translate(" + Math.round(newMargin) + "px)";
 | |
|     }
 | |
| 
 | |
|     on_drop(event) {
 | |
|       var dt = event.dataTransfer;
 | |
|       var dropEffect = dt.dropEffect;
 | |
|       var draggedTab;
 | |
|       let movingTabs;
 | |
|       if (dt.mozTypesAt(0)[0] == TAB_DROP_TYPE) {
 | |
|         // tab copy or move
 | |
|         draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
 | |
|         // not our drop then
 | |
|         if (!draggedTab) {
 | |
|           return;
 | |
|         }
 | |
|         movingTabs = draggedTab._dragData.movingTabs;
 | |
|         draggedTab.container._finishGroupSelectedTabs(draggedTab);
 | |
|       }
 | |
| 
 | |
|       this._tabDropIndicator.hidden = true;
 | |
|       event.stopPropagation();
 | |
|       if (draggedTab && dropEffect == "copy") {
 | |
|         // copy the dropped tab (wherever it's from)
 | |
|         let newIndex = this._getDropIndex(event, false);
 | |
|         let draggedTabCopy;
 | |
|         for (let tab of movingTabs) {
 | |
|           let newTab = gBrowser.duplicateTab(tab);
 | |
|           gBrowser.moveTabTo(newTab, newIndex++);
 | |
|           if (tab == draggedTab) {
 | |
|             draggedTabCopy = newTab;
 | |
|           }
 | |
|         }
 | |
|         if (draggedTab.container != this || event.shiftKey) {
 | |
|           this.selectedItem = draggedTabCopy;
 | |
|         }
 | |
|       } else if (draggedTab && draggedTab.container == this) {
 | |
|         let oldTranslateX = Math.round(draggedTab._dragData.translateX);
 | |
|         let tabWidth = Math.round(draggedTab._dragData.tabWidth);
 | |
|         let translateOffset = oldTranslateX % tabWidth;
 | |
|         let newTranslateX = oldTranslateX - translateOffset;
 | |
|         if (oldTranslateX > 0 && translateOffset > tabWidth / 2) {
 | |
|           newTranslateX += tabWidth;
 | |
|         } else if (oldTranslateX < 0 && -translateOffset > tabWidth / 2) {
 | |
|           newTranslateX -= tabWidth;
 | |
|         }
 | |
| 
 | |
|         let dropIndex;
 | |
|         if (draggedTab._dragData.fromTabList) {
 | |
|           dropIndex = this._getDropIndex(event, false);
 | |
|         } else {
 | |
|           dropIndex =
 | |
|             "animDropIndex" in draggedTab._dragData &&
 | |
|             draggedTab._dragData.animDropIndex;
 | |
|         }
 | |
|         let incrementDropIndex = true;
 | |
|         if (dropIndex && dropIndex > movingTabs[0]._tPos) {
 | |
|           dropIndex--;
 | |
|           incrementDropIndex = false;
 | |
|         }
 | |
| 
 | |
|         if (oldTranslateX && oldTranslateX != newTranslateX && !gReduceMotion) {
 | |
|           for (let tab of movingTabs) {
 | |
|             tab.setAttribute("tabdrop-samewindow", "true");
 | |
|             tab.style.transform = "translateX(" + newTranslateX + "px)";
 | |
|             let postTransitionCleanup = () => {
 | |
|               tab.removeAttribute("tabdrop-samewindow");
 | |
| 
 | |
|               this._finishAnimateTabMove();
 | |
|               if (dropIndex !== false) {
 | |
|                 gBrowser.moveTabTo(tab, dropIndex);
 | |
|                 if (incrementDropIndex) {
 | |
|                   dropIndex++;
 | |
|                 }
 | |
|               }
 | |
| 
 | |
|               gBrowser.syncThrobberAnimations(tab);
 | |
|             };
 | |
|             if (gReduceMotion) {
 | |
|               postTransitionCleanup();
 | |
|             } else {
 | |
|               let onTransitionEnd = transitionendEvent => {
 | |
|                 if (
 | |
|                   transitionendEvent.propertyName != "transform" ||
 | |
|                   transitionendEvent.originalTarget != tab
 | |
|                 ) {
 | |
|                   return;
 | |
|                 }
 | |
|                 tab.removeEventListener("transitionend", onTransitionEnd);
 | |
| 
 | |
|                 postTransitionCleanup();
 | |
|               };
 | |
|               tab.addEventListener("transitionend", onTransitionEnd);
 | |
|             }
 | |
|           }
 | |
|         } else {
 | |
|           this._finishAnimateTabMove();
 | |
|           if (dropIndex !== false) {
 | |
|             for (let tab of movingTabs) {
 | |
|               gBrowser.moveTabTo(tab, dropIndex);
 | |
|               if (incrementDropIndex) {
 | |
|                 dropIndex++;
 | |
|               }
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|       } else if (draggedTab) {
 | |
|         // Move the tabs. To avoid multiple tab-switches in the original window,
 | |
|         // the selected tab should be adopted last.
 | |
|         const dropIndex = this._getDropIndex(event, false);
 | |
|         let newIndex = dropIndex;
 | |
|         let selectedTab;
 | |
|         let indexForSelectedTab;
 | |
|         for (let i = 0; i < movingTabs.length; ++i) {
 | |
|           const tab = movingTabs[i];
 | |
|           if (tab.selected) {
 | |
|             selectedTab = tab;
 | |
|             indexForSelectedTab = newIndex;
 | |
|           } else {
 | |
|             const newTab = gBrowser.adoptTab(tab, newIndex, tab == draggedTab);
 | |
|             if (newTab) {
 | |
|               ++newIndex;
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|         if (selectedTab) {
 | |
|           const newTab = gBrowser.adoptTab(
 | |
|             selectedTab,
 | |
|             indexForSelectedTab,
 | |
|             selectedTab == draggedTab
 | |
|           );
 | |
|           if (newTab) {
 | |
|             ++newIndex;
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         // Restore tab selection
 | |
|         gBrowser.addRangeToMultiSelectedTabs(
 | |
|           gBrowser.tabs[dropIndex],
 | |
|           gBrowser.tabs[newIndex - 1]
 | |
|         );
 | |
|       } else {
 | |
|         // Pass true to disallow dropping javascript: or data: urls
 | |
|         let links;
 | |
|         try {
 | |
|           links = browserDragAndDrop.dropLinks(event, true);
 | |
|         } catch (ex) {}
 | |
| 
 | |
|         if (!links || links.length === 0) {
 | |
|           return;
 | |
|         }
 | |
| 
 | |
|         let inBackground = Services.prefs.getBoolPref(
 | |
|           "browser.tabs.loadInBackground"
 | |
|         );
 | |
|         if (event.shiftKey) {
 | |
|           inBackground = !inBackground;
 | |
|         }
 | |
| 
 | |
|         let targetTab = this._getDragTargetTab(event, true);
 | |
|         let userContextId = this.selectedItem.getAttribute("usercontextid");
 | |
|         let replace = !!targetTab;
 | |
|         let newIndex = this._getDropIndex(event, true);
 | |
|         let urls = links.map(link => link.url);
 | |
|         let csp = browserDragAndDrop.getCsp(event);
 | |
|         let triggeringPrincipal = browserDragAndDrop.getTriggeringPrincipal(
 | |
|           event
 | |
|         );
 | |
| 
 | |
|         (async () => {
 | |
|           if (
 | |
|             urls.length >=
 | |
|             Services.prefs.getIntPref("browser.tabs.maxOpenBeforeWarn")
 | |
|           ) {
 | |
|             // Sync dialog cannot be used inside drop event handler.
 | |
|             let answer = await OpenInTabsUtils.promiseConfirmOpenInTabs(
 | |
|               urls.length,
 | |
|               window
 | |
|             );
 | |
|             if (!answer) {
 | |
|               return;
 | |
|             }
 | |
|           }
 | |
| 
 | |
|           gBrowser.loadTabs(urls, {
 | |
|             inBackground,
 | |
|             replace,
 | |
|             allowThirdPartyFixup: true,
 | |
|             targetTab,
 | |
|             newIndex,
 | |
|             userContextId,
 | |
|             triggeringPrincipal,
 | |
|             csp,
 | |
|           });
 | |
|         })();
 | |
|       }
 | |
| 
 | |
|       if (draggedTab) {
 | |
|         delete draggedTab._dragData;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     on_dragend(event) {
 | |
|       var dt = event.dataTransfer;
 | |
|       var draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
 | |
| 
 | |
|       // Prevent this code from running if a tabdrop animation is
 | |
|       // running since calling _finishAnimateTabMove would clear
 | |
|       // any CSS transition that is running.
 | |
|       if (draggedTab.hasAttribute("tabdrop-samewindow")) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       this._finishGroupSelectedTabs(draggedTab);
 | |
|       this._finishAnimateTabMove();
 | |
| 
 | |
|       if (
 | |
|         dt.mozUserCancelled ||
 | |
|         dt.dropEffect != "none" ||
 | |
|         this._isCustomizing
 | |
|       ) {
 | |
|         delete draggedTab._dragData;
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       // Check if tab detaching is enabled
 | |
|       if (!Services.prefs.getBoolPref("browser.tabs.allowTabDetach")) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       // Disable detach within the browser toolbox
 | |
|       var eX = event.screenX;
 | |
|       var eY = event.screenY;
 | |
|       var wX = window.screenX;
 | |
|       // check if the drop point is horizontally within the window
 | |
|       if (eX > wX && eX < wX + window.outerWidth) {
 | |
|         // also avoid detaching if the the tab was dropped too close to
 | |
|         // the tabbar (half a tab)
 | |
|         let rect = window.windowUtils.getBoundsWithoutFlushing(
 | |
|           this.arrowScrollbox
 | |
|         );
 | |
|         let detachTabThresholdY = window.screenY + rect.top + 1.5 * rect.height;
 | |
|         if (eY < detachTabThresholdY && eY > window.screenY) {
 | |
|           return;
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       // screen.availLeft et. al. only check the screen that this window is on,
 | |
|       // but we want to look at the screen the tab is being dropped onto.
 | |
|       var screen = event.screen;
 | |
|       var availX = {},
 | |
|         availY = {},
 | |
|         availWidth = {},
 | |
|         availHeight = {};
 | |
|       // Get available rect in desktop pixels.
 | |
|       screen.GetAvailRectDisplayPix(availX, availY, availWidth, availHeight);
 | |
|       availX = availX.value;
 | |
|       availY = availY.value;
 | |
|       availWidth = availWidth.value;
 | |
|       availHeight = availHeight.value;
 | |
| 
 | |
|       // Compute the final window size in desktop pixels ensuring that the new
 | |
|       // window entirely fits within `screen`.
 | |
|       let ourCssToDesktopScale =
 | |
|         window.devicePixelRatio / window.desktopToDeviceScale;
 | |
|       let screenCssToDesktopScale =
 | |
|         screen.defaultCSSScaleFactor / screen.contentsScaleFactor;
 | |
| 
 | |
|       // NOTE(emilio): Multiplying the sizes here for screenCssToDesktopScale
 | |
|       // means that we'll try to create a window that has the same amount of CSS
 | |
|       // pixels than our current window, not the same amount of device pixels.
 | |
|       // There are pros and cons of both conversions, though this matches the
 | |
|       // pre-existing intended behavior.
 | |
|       var winWidth = Math.min(
 | |
|         window.outerWidth * screenCssToDesktopScale,
 | |
|         availWidth
 | |
|       );
 | |
|       var winHeight = Math.min(
 | |
|         window.outerHeight * screenCssToDesktopScale,
 | |
|         availHeight
 | |
|       );
 | |
| 
 | |
|       // This is slightly tricky: _dragData.offsetX/Y is an offset in CSS
 | |
|       // pixels. Since we're doing the sizing above based on those, we also need
 | |
|       // to apply the offset with pixels relative to the screen's scale rather
 | |
|       // than our scale.
 | |
|       var left = Math.min(
 | |
|         Math.max(
 | |
|           eX * ourCssToDesktopScale -
 | |
|             draggedTab._dragData.offsetX * screenCssToDesktopScale,
 | |
|           availX
 | |
|         ),
 | |
|         availX + availWidth - winWidth
 | |
|       );
 | |
|       var top = Math.min(
 | |
|         Math.max(
 | |
|           eY * ourCssToDesktopScale -
 | |
|             draggedTab._dragData.offsetY * screenCssToDesktopScale,
 | |
|           availY
 | |
|         ),
 | |
|         availY + availHeight - winHeight
 | |
|       );
 | |
| 
 | |
|       // Convert back left and top to our CSS pixel space.
 | |
|       left /= ourCssToDesktopScale;
 | |
|       top /= ourCssToDesktopScale;
 | |
| 
 | |
|       delete draggedTab._dragData;
 | |
| 
 | |
|       if (gBrowser.tabs.length == 1) {
 | |
|         // resize _before_ move to ensure the window fits the new screen.  if
 | |
|         // the window is too large for its screen, the window manager may do
 | |
|         // automatic repositioning.
 | |
|         //
 | |
|         // Since we're resizing before moving to our new screen, we need to use
 | |
|         // sizes relative to the current screen. If we moved, then resized, then
 | |
|         // we could avoid this special-case and share this with the else branch
 | |
|         // below...
 | |
|         winWidth /= ourCssToDesktopScale;
 | |
|         winHeight /= ourCssToDesktopScale;
 | |
| 
 | |
|         window.resizeTo(winWidth, winHeight);
 | |
|         window.moveTo(left, top);
 | |
|         window.focus();
 | |
|       } else {
 | |
|         // We're opening a new window in a new screen, so make sure to use sizes
 | |
|         // relative to the new screen.
 | |
|         winWidth /= screenCssToDesktopScale;
 | |
|         winHeight /= screenCssToDesktopScale;
 | |
| 
 | |
|         let props = { screenX: left, screenY: top, suppressanimation: 1 };
 | |
|         if (AppConstants.platform != "win") {
 | |
|           props.outerWidth = winWidth;
 | |
|           props.outerHeight = winHeight;
 | |
|         }
 | |
|         gBrowser.replaceTabsWithWindow(draggedTab, props);
 | |
|       }
 | |
|       event.stopPropagation();
 | |
|     }
 | |
| 
 | |
|     on_dragleave(event) {
 | |
|       this._dragTime = 0;
 | |
| 
 | |
|       // This does not work at all (see bug 458613)
 | |
|       var target = event.relatedTarget;
 | |
|       while (target && target != this) {
 | |
|         target = target.parentNode;
 | |
|       }
 | |
|       if (target) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       this._tabDropIndicator.hidden = true;
 | |
|       event.stopPropagation();
 | |
|     }
 | |
| 
 | |
|     on_wheel(event) {
 | |
|       if (
 | |
|         Services.prefs.getBoolPref("toolkit.tabbox.switchByScrolling", false)
 | |
|       ) {
 | |
|         event.stopImmediatePropagation();
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     get emptyTabTitle() {
 | |
|       // Normal tab title is used also in the permanent private browsing mode.
 | |
|       const l10nId =
 | |
|         PrivateBrowsingUtils.isWindowPrivate(window) &&
 | |
|         !Services.prefs.getBoolPref("browser.privatebrowsing.autostart")
 | |
|           ? "tabbrowser-empty-private-tab-title"
 | |
|           : "tabbrowser-empty-tab-title";
 | |
|       return gBrowser.tabLocalization.formatValueSync(l10nId);
 | |
|     }
 | |
| 
 | |
|     get tabbox() {
 | |
|       return document.getElementById("tabbrowser-tabbox");
 | |
|     }
 | |
| 
 | |
|     get newTabButton() {
 | |
|       return this.querySelector("#tabs-newtab-button");
 | |
|     }
 | |
| 
 | |
|     // Accessor for tabs.  arrowScrollbox has a container for non-tab elements
 | |
|     // at the end, everything else is <tab>s.
 | |
|     get allTabs() {
 | |
|       let children = Array.from(this.arrowScrollbox.children);
 | |
|       children.pop();
 | |
|       return children;
 | |
|     }
 | |
| 
 | |
|     appendChild(tab) {
 | |
|       return this.insertBefore(tab, null);
 | |
|     }
 | |
| 
 | |
|     insertBefore(tab, node) {
 | |
|       if (!this.arrowScrollbox) {
 | |
|         throw new Error("Shouldn't call this without arrowscrollbox");
 | |
|       }
 | |
| 
 | |
|       let { arrowScrollbox } = this;
 | |
|       if (node == null) {
 | |
|         // We have a container for non-tab elements at the end of the scrollbox.
 | |
|         node = arrowScrollbox.lastChild;
 | |
|       }
 | |
|       return arrowScrollbox.insertBefore(tab, node);
 | |
|     }
 | |
| 
 | |
|     set _tabMinWidth(val) {
 | |
|       this.style.setProperty("--tab-min-width", val + "px");
 | |
|     }
 | |
| 
 | |
|     get _isCustomizing() {
 | |
|       return document.documentElement.getAttribute("customizing") == "true";
 | |
|     }
 | |
| 
 | |
|     // This overrides the TabsBase _selectNewTab method so that we can
 | |
|     // potentially interrupt keyboard tab switching when sharing the
 | |
|     // window or screen.
 | |
|     _selectNewTab(aNewTab, aFallbackDir, aWrap) {
 | |
|       if (!gSharedTabWarning.willShowSharedTabWarning(aNewTab)) {
 | |
|         super._selectNewTab(aNewTab, aFallbackDir, aWrap);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     _initializeArrowScrollbox() {
 | |
|       let arrowScrollbox = this.arrowScrollbox;
 | |
|       arrowScrollbox.shadowRoot.addEventListener(
 | |
|         "underflow",
 | |
|         event => {
 | |
|           // Ignore underflow events:
 | |
|           // - from nested scrollable elements
 | |
|           // - for vertical orientation
 | |
|           // - corresponding to an overflow event that we ignored
 | |
|           if (
 | |
|             event.originalTarget != arrowScrollbox.scrollbox ||
 | |
|             event.detail == 0 ||
 | |
|             !this.hasAttribute("overflow")
 | |
|           ) {
 | |
|             return;
 | |
|           }
 | |
| 
 | |
|           this.removeAttribute("overflow");
 | |
| 
 | |
|           if (this._lastTabClosedByMouse) {
 | |
|             this._expandSpacerBy(this._scrollButtonWidth);
 | |
|           }
 | |
| 
 | |
|           for (let tab of gBrowser._removingTabs) {
 | |
|             gBrowser.removeTab(tab);
 | |
|           }
 | |
| 
 | |
|           this._positionPinnedTabs();
 | |
|           this._updateCloseButtons();
 | |
|         },
 | |
|         true
 | |
|       );
 | |
| 
 | |
|       arrowScrollbox.shadowRoot.addEventListener("overflow", event => {
 | |
|         // Ignore overflow events:
 | |
|         // - from nested scrollable elements
 | |
|         // - for vertical orientation
 | |
|         if (
 | |
|           event.originalTarget != arrowScrollbox.scrollbox ||
 | |
|           event.detail == 0
 | |
|         ) {
 | |
|           return;
 | |
|         }
 | |
| 
 | |
|         this.setAttribute("overflow", "true");
 | |
|         this._positionPinnedTabs();
 | |
|         this._updateCloseButtons();
 | |
|         this._handleTabSelect(true);
 | |
|       });
 | |
| 
 | |
|       // Override arrowscrollbox.js method, since our scrollbox's children are
 | |
|       // inherited from the scrollbox binding parent (this).
 | |
|       arrowScrollbox._getScrollableElements = () => {
 | |
|         return this.allTabs.filter(arrowScrollbox._canScrollToElement);
 | |
|       };
 | |
|       arrowScrollbox._canScrollToElement = tab => {
 | |
|         return !tab._pinnedUnscrollable && !tab.hidden;
 | |
|       };
 | |
|     }
 | |
| 
 | |
|     observe(aSubject, aTopic, aData) {
 | |
|       switch (aTopic) {
 | |
|         case "nsPref:changed":
 | |
|           // This is has to deal with changes in
 | |
|           // privacy.userContext.enabled and
 | |
|           // privacy.userContext.newTabContainerOnLeftClick.enabled.
 | |
|           let containersEnabled =
 | |
|             Services.prefs.getBoolPref("privacy.userContext.enabled") &&
 | |
|             !PrivateBrowsingUtils.isWindowPrivate(window);
 | |
| 
 | |
|           // This pref won't change so often, so just recreate the menu.
 | |
|           const newTabLeftClickOpensContainersMenu = Services.prefs.getBoolPref(
 | |
|             "privacy.userContext.newTabContainerOnLeftClick.enabled"
 | |
|           );
 | |
| 
 | |
|           // There are separate "new tab" buttons for when the tab strip
 | |
|           // is overflowed and when it is not.  Attach the long click
 | |
|           // popup to both of them.
 | |
|           const newTab = document.getElementById("new-tab-button");
 | |
|           const newTab2 = this.newTabButton;
 | |
| 
 | |
|           for (let parent of [newTab, newTab2]) {
 | |
|             if (!parent) {
 | |
|               continue;
 | |
|             }
 | |
| 
 | |
|             parent.removeAttribute("type");
 | |
|             if (parent.menupopup) {
 | |
|               parent.menupopup.remove();
 | |
|             }
 | |
| 
 | |
|             if (containersEnabled) {
 | |
|               parent.setAttribute("context", "new-tab-button-popup");
 | |
| 
 | |
|               let popup = document
 | |
|                 .getElementById("new-tab-button-popup")
 | |
|                 .cloneNode(true);
 | |
|               popup.removeAttribute("id");
 | |
|               popup.className = "new-tab-popup";
 | |
|               popup.setAttribute("position", "after_end");
 | |
|               parent.prepend(popup);
 | |
|               parent.setAttribute("type", "menu");
 | |
|               // Update tooltip text
 | |
|               nodeToTooltipMap[parent.id] = newTabLeftClickOpensContainersMenu
 | |
|                 ? "newTabAlwaysContainer.tooltip"
 | |
|                 : "newTabContainer.tooltip";
 | |
|             } else {
 | |
|               nodeToTooltipMap[parent.id] = "newTabButton.tooltip";
 | |
|               parent.removeAttribute("context", "new-tab-button-popup");
 | |
|             }
 | |
|             // evict from tooltip cache
 | |
|             gDynamicTooltipCache.delete(parent.id);
 | |
| 
 | |
|             // If containers and press-hold container menu are both used,
 | |
|             // add to gClickAndHoldListenersOnElement; otherwise, remove.
 | |
|             if (containersEnabled && !newTabLeftClickOpensContainersMenu) {
 | |
|               gClickAndHoldListenersOnElement.add(parent);
 | |
|             } else {
 | |
|               gClickAndHoldListenersOnElement.remove(parent);
 | |
|             }
 | |
|           }
 | |
| 
 | |
|           break;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     _getVisibleTabs() {
 | |
|       // Cannot access gBrowser before it's initialized.
 | |
|       if (!gBrowser) {
 | |
|         return this.allTabs[0];
 | |
|       }
 | |
| 
 | |
|       return gBrowser.visibleTabs;
 | |
|     }
 | |
| 
 | |
|     _setPositionalAttributes() {
 | |
|       let visibleTabs = this._getVisibleTabs();
 | |
|       if (!visibleTabs.length) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       this._firstUnpinnedTab?.removeAttribute("first-visible-unpinned-tab");
 | |
|       this._firstUnpinnedTab = visibleTabs.find(t => !t.pinned);
 | |
|       this._firstUnpinnedTab?.setAttribute(
 | |
|         "first-visible-unpinned-tab",
 | |
|         "true"
 | |
|       );
 | |
| 
 | |
|       let hoveredTab = this._hoveredTab;
 | |
|       if (hoveredTab) {
 | |
|         hoveredTab._mouseleave();
 | |
|       }
 | |
|       hoveredTab = this.querySelector("tab:hover");
 | |
|       if (hoveredTab) {
 | |
|         hoveredTab._mouseenter();
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     _updateCloseButtons() {
 | |
|       // If we're overflowing, tabs are at their minimum widths.
 | |
|       if (this.getAttribute("overflow") == "true") {
 | |
|         this.setAttribute("closebuttons", "activetab");
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       if (this._closeButtonsUpdatePending) {
 | |
|         return;
 | |
|       }
 | |
|       this._closeButtonsUpdatePending = true;
 | |
| 
 | |
|       // Wait until after the next paint to get current layout data from
 | |
|       // getBoundsWithoutFlushing.
 | |
|       window.requestAnimationFrame(() => {
 | |
|         window.requestAnimationFrame(() => {
 | |
|           this._closeButtonsUpdatePending = false;
 | |
| 
 | |
|           // The scrollbox may have started overflowing since we checked
 | |
|           // overflow earlier, so check again.
 | |
|           if (this.getAttribute("overflow") == "true") {
 | |
|             this.setAttribute("closebuttons", "activetab");
 | |
|             return;
 | |
|           }
 | |
| 
 | |
|           // Check if tab widths are below the threshold where we want to
 | |
|           // remove close buttons from background tabs so that people don't
 | |
|           // accidentally close tabs by selecting them.
 | |
|           let rect = ele => {
 | |
|             return window.windowUtils.getBoundsWithoutFlushing(ele);
 | |
|           };
 | |
|           let tab = this._getVisibleTabs()[gBrowser._numPinnedTabs];
 | |
|           if (tab && rect(tab).width <= this._tabClipWidth) {
 | |
|             this.setAttribute("closebuttons", "activetab");
 | |
|           } else {
 | |
|             this.removeAttribute("closebuttons");
 | |
|           }
 | |
|         });
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     _updateHiddenTabsStatus() {
 | |
|       if (gBrowser.visibleTabs.length < gBrowser.tabs.length) {
 | |
|         this.setAttribute("hashiddentabs", "true");
 | |
|       } else {
 | |
|         this.removeAttribute("hashiddentabs");
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     _handleTabSelect(aInstant) {
 | |
|       let selectedTab = this.selectedItem;
 | |
|       if (this.getAttribute("overflow") == "true") {
 | |
|         this.arrowScrollbox.ensureElementIsVisible(selectedTab, aInstant);
 | |
|       }
 | |
| 
 | |
|       selectedTab._notselectedsinceload = false;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Try to keep the active tab's close button under the mouse cursor
 | |
|      */
 | |
|     _lockTabSizing(aTab, aTabWidth) {
 | |
|       let tabs = this._getVisibleTabs();
 | |
|       if (!tabs.length) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       var isEndTab = aTab._tPos > tabs[tabs.length - 1]._tPos;
 | |
| 
 | |
|       if (!this._tabDefaultMaxWidth) {
 | |
|         this._tabDefaultMaxWidth = parseFloat(
 | |
|           window.getComputedStyle(aTab).maxWidth
 | |
|         );
 | |
|       }
 | |
|       this._lastTabClosedByMouse = true;
 | |
|       this._scrollButtonWidth = window.windowUtils.getBoundsWithoutFlushing(
 | |
|         this.arrowScrollbox._scrollButtonDown
 | |
|       ).width;
 | |
| 
 | |
|       if (this.getAttribute("overflow") == "true") {
 | |
|         // Don't need to do anything if we're in overflow mode and aren't scrolled
 | |
|         // all the way to the right, or if we're closing the last tab.
 | |
|         if (isEndTab || !this.arrowScrollbox._scrollButtonDown.disabled) {
 | |
|           return;
 | |
|         }
 | |
|         // If the tab has an owner that will become the active tab, the owner will
 | |
|         // be to the left of it, so we actually want the left tab to slide over.
 | |
|         // This can't be done as easily in non-overflow mode, so we don't bother.
 | |
|         if (aTab.owner) {
 | |
|           return;
 | |
|         }
 | |
|         this._expandSpacerBy(aTabWidth);
 | |
|       } else {
 | |
|         // non-overflow mode
 | |
|         // Locking is neither in effect nor needed, so let tabs expand normally.
 | |
|         if (isEndTab && !this._hasTabTempMaxWidth) {
 | |
|           return;
 | |
|         }
 | |
|         let numPinned = gBrowser._numPinnedTabs;
 | |
|         // Force tabs to stay the same width, unless we're closing the last tab,
 | |
|         // which case we need to let them expand just enough so that the overall
 | |
|         // tabbar width is the same.
 | |
|         if (isEndTab) {
 | |
|           let numNormalTabs = tabs.length - numPinned;
 | |
|           aTabWidth = (aTabWidth * (numNormalTabs + 1)) / numNormalTabs;
 | |
|           if (aTabWidth > this._tabDefaultMaxWidth) {
 | |
|             aTabWidth = this._tabDefaultMaxWidth;
 | |
|           }
 | |
|         }
 | |
|         aTabWidth += "px";
 | |
|         let tabsToReset = [];
 | |
|         for (let i = numPinned; i < tabs.length; i++) {
 | |
|           let tab = tabs[i];
 | |
|           tab.style.setProperty("max-width", aTabWidth, "important");
 | |
|           if (!isEndTab) {
 | |
|             // keep tabs the same width
 | |
|             tab.style.transition = "none";
 | |
|             tabsToReset.push(tab);
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         if (tabsToReset.length) {
 | |
|           window
 | |
|             .promiseDocumentFlushed(() => {})
 | |
|             .then(() => {
 | |
|               window.requestAnimationFrame(() => {
 | |
|                 for (let tab of tabsToReset) {
 | |
|                   tab.style.transition = "";
 | |
|                 }
 | |
|               });
 | |
|             });
 | |
|         }
 | |
| 
 | |
|         this._hasTabTempMaxWidth = true;
 | |
|         gBrowser.addEventListener("mousemove", this);
 | |
|         window.addEventListener("mouseout", this);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     _expandSpacerBy(pixels) {
 | |
|       let spacer = this._closingTabsSpacer;
 | |
|       spacer.style.width = parseFloat(spacer.style.width) + pixels + "px";
 | |
|       this.setAttribute("using-closing-tabs-spacer", "true");
 | |
|       gBrowser.addEventListener("mousemove", this);
 | |
|       window.addEventListener("mouseout", this);
 | |
|     }
 | |
| 
 | |
|     _unlockTabSizing() {
 | |
|       gBrowser.removeEventListener("mousemove", this);
 | |
|       window.removeEventListener("mouseout", this);
 | |
| 
 | |
|       if (this._hasTabTempMaxWidth) {
 | |
|         this._hasTabTempMaxWidth = false;
 | |
|         let tabs = this._getVisibleTabs();
 | |
|         for (let i = 0; i < tabs.length; i++) {
 | |
|           tabs[i].style.maxWidth = "";
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       if (this.hasAttribute("using-closing-tabs-spacer")) {
 | |
|         this.removeAttribute("using-closing-tabs-spacer");
 | |
|         this._closingTabsSpacer.style.width = 0;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     uiDensityChanged() {
 | |
|       this._positionPinnedTabs();
 | |
|       this._updateCloseButtons();
 | |
|       this._handleTabSelect(true);
 | |
|     }
 | |
| 
 | |
|     _positionPinnedTabs() {
 | |
|       let tabs = this._getVisibleTabs();
 | |
|       let numPinned = gBrowser._numPinnedTabs;
 | |
|       let doPosition =
 | |
|         this.getAttribute("overflow") == "true" &&
 | |
|         tabs.length > numPinned &&
 | |
|         numPinned > 0;
 | |
| 
 | |
|       this.toggleAttribute("haspinnedtabs", !!numPinned);
 | |
| 
 | |
|       if (doPosition) {
 | |
|         this.setAttribute("positionpinnedtabs", "true");
 | |
| 
 | |
|         let layoutData = this._pinnedTabsLayoutCache;
 | |
|         let uiDensity = document.documentElement.getAttribute("uidensity");
 | |
|         if (!layoutData || layoutData.uiDensity != uiDensity) {
 | |
|           let arrowScrollbox = this.arrowScrollbox;
 | |
|           layoutData = this._pinnedTabsLayoutCache = {
 | |
|             uiDensity,
 | |
|             pinnedTabWidth: tabs[0].getBoundingClientRect().width,
 | |
|             scrollStartOffset:
 | |
|               arrowScrollbox.scrollbox.getBoundingClientRect().left -
 | |
|               arrowScrollbox.getBoundingClientRect().left +
 | |
|               parseFloat(
 | |
|                 getComputedStyle(arrowScrollbox.scrollbox).paddingInlineStart
 | |
|               ),
 | |
|           };
 | |
|         }
 | |
| 
 | |
|         let width = 0;
 | |
|         for (let i = numPinned - 1; i >= 0; i--) {
 | |
|           let tab = tabs[i];
 | |
|           width += layoutData.pinnedTabWidth;
 | |
|           tab.style.setProperty(
 | |
|             "margin-inline-start",
 | |
|             -(width + layoutData.scrollStartOffset) + "px",
 | |
|             "important"
 | |
|           );
 | |
|           tab._pinnedUnscrollable = true;
 | |
|         }
 | |
|         this.style.setProperty(
 | |
|           "--tab-overflow-pinned-tabs-width",
 | |
|           width + "px"
 | |
|         );
 | |
|       } else {
 | |
|         this.removeAttribute("positionpinnedtabs");
 | |
| 
 | |
|         for (let i = 0; i < numPinned; i++) {
 | |
|           let tab = tabs[i];
 | |
|           tab.style.marginInlineStart = "";
 | |
|           tab._pinnedUnscrollable = false;
 | |
|         }
 | |
| 
 | |
|         this.style.removeProperty("--tab-overflow-pinned-tabs-width");
 | |
|       }
 | |
| 
 | |
|       if (this._lastNumPinned != numPinned) {
 | |
|         this._lastNumPinned = numPinned;
 | |
|         this._handleTabSelect(true);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     _animateTabMove(event) {
 | |
|       let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
 | |
|       let movingTabs = draggedTab._dragData.movingTabs;
 | |
| 
 | |
|       if (this.getAttribute("movingtab") != "true") {
 | |
|         this.setAttribute("movingtab", "true");
 | |
|         gNavToolbox.setAttribute("movingtab", "true");
 | |
|         if (!draggedTab.multiselected) {
 | |
|           this.selectedItem = draggedTab;
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       if (!("animLastScreenX" in draggedTab._dragData)) {
 | |
|         draggedTab._dragData.animLastScreenX = draggedTab._dragData.screenX;
 | |
|       }
 | |
| 
 | |
|       let screenX = event.screenX;
 | |
|       if (screenX == draggedTab._dragData.animLastScreenX) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       // Direction of the mouse movement.
 | |
|       let ltrMove = screenX > draggedTab._dragData.animLastScreenX;
 | |
| 
 | |
|       draggedTab._dragData.animLastScreenX = screenX;
 | |
| 
 | |
|       let pinned = draggedTab.pinned;
 | |
|       let numPinned = gBrowser._numPinnedTabs;
 | |
|       let tabs = this._getVisibleTabs().slice(
 | |
|         pinned ? 0 : numPinned,
 | |
|         pinned ? numPinned : undefined
 | |
|       );
 | |
| 
 | |
|       if (RTL_UI) {
 | |
|         tabs.reverse();
 | |
|         // Copy moving tabs array to avoid infinite reversing.
 | |
|         movingTabs = [...movingTabs].reverse();
 | |
|       }
 | |
|       let tabWidth = draggedTab.getBoundingClientRect().width;
 | |
|       let shiftWidth = tabWidth * movingTabs.length;
 | |
|       draggedTab._dragData.tabWidth = tabWidth;
 | |
| 
 | |
|       // Move the dragged tab based on the mouse position.
 | |
| 
 | |
|       let leftTab = tabs[0];
 | |
|       let rightTab = tabs[tabs.length - 1];
 | |
|       let rightMovingTabScreenX = movingTabs[movingTabs.length - 1].screenX;
 | |
|       let leftMovingTabScreenX = movingTabs[0].screenX;
 | |
|       let translateX = screenX - draggedTab._dragData.screenX;
 | |
|       if (!pinned) {
 | |
|         translateX +=
 | |
|           this.arrowScrollbox.scrollbox.scrollLeft -
 | |
|           draggedTab._dragData.scrollX;
 | |
|       }
 | |
|       let leftBound = leftTab.screenX - leftMovingTabScreenX;
 | |
|       let rightBound =
 | |
|         rightTab.screenX +
 | |
|         rightTab.getBoundingClientRect().width -
 | |
|         (rightMovingTabScreenX + tabWidth);
 | |
|       translateX = Math.min(Math.max(translateX, leftBound), rightBound);
 | |
| 
 | |
|       for (let tab of movingTabs) {
 | |
|         tab.style.transform = "translateX(" + translateX + "px)";
 | |
|       }
 | |
| 
 | |
|       draggedTab._dragData.translateX = translateX;
 | |
| 
 | |
|       // Determine what tab we're dragging over.
 | |
|       // * Single tab dragging: Point of reference is the center of the dragged tab. If that
 | |
|       //   point touches a background tab, the dragged tab would take that
 | |
|       //   tab's position when dropped.
 | |
|       // * Multiple tabs dragging: All dragged tabs are one "giant" tab with two
 | |
|       //   points of reference (center of tabs on the extremities). When
 | |
|       //   mouse is moving from left to right, the right reference gets activated,
 | |
|       //   otherwise the left reference will be used. Everything else works the same
 | |
|       //   as single tab dragging.
 | |
|       // * We're doing a binary search in order to reduce the amount of
 | |
|       //   tabs we need to check.
 | |
| 
 | |
|       tabs = tabs.filter(t => !movingTabs.includes(t) || t == draggedTab);
 | |
|       let leftTabCenter = leftMovingTabScreenX + translateX + tabWidth / 2;
 | |
|       let rightTabCenter = rightMovingTabScreenX + translateX + tabWidth / 2;
 | |
|       let tabCenter = ltrMove ? rightTabCenter : leftTabCenter;
 | |
|       let newIndex = -1;
 | |
|       let oldIndex =
 | |
|         "animDropIndex" in draggedTab._dragData
 | |
|           ? draggedTab._dragData.animDropIndex
 | |
|           : movingTabs[0]._tPos;
 | |
|       let low = 0;
 | |
|       let high = tabs.length - 1;
 | |
|       while (low <= high) {
 | |
|         let mid = Math.floor((low + high) / 2);
 | |
|         if (tabs[mid] == draggedTab && ++mid > high) {
 | |
|           break;
 | |
|         }
 | |
|         screenX = tabs[mid].screenX + getTabShift(tabs[mid], oldIndex);
 | |
|         if (screenX > tabCenter) {
 | |
|           high = mid - 1;
 | |
|         } else if (
 | |
|           screenX + tabs[mid].getBoundingClientRect().width <
 | |
|           tabCenter
 | |
|         ) {
 | |
|           low = mid + 1;
 | |
|         } else {
 | |
|           newIndex = tabs[mid]._tPos;
 | |
|           break;
 | |
|         }
 | |
|       }
 | |
|       if (newIndex >= oldIndex) {
 | |
|         newIndex++;
 | |
|       }
 | |
|       if (newIndex < 0 || newIndex == oldIndex) {
 | |
|         return;
 | |
|       }
 | |
|       draggedTab._dragData.animDropIndex = newIndex;
 | |
| 
 | |
|       // Shift background tabs to leave a gap where the dragged tab
 | |
|       // would currently be dropped.
 | |
| 
 | |
|       for (let tab of tabs) {
 | |
|         if (tab != draggedTab) {
 | |
|           let shift = getTabShift(tab, newIndex);
 | |
|           tab.style.transform = shift ? "translateX(" + shift + "px)" : "";
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       function getTabShift(tab, dropIndex) {
 | |
|         if (tab._tPos < draggedTab._tPos && tab._tPos >= dropIndex) {
 | |
|           return RTL_UI ? -shiftWidth : shiftWidth;
 | |
|         }
 | |
|         if (tab._tPos > draggedTab._tPos && tab._tPos < dropIndex) {
 | |
|           return RTL_UI ? shiftWidth : -shiftWidth;
 | |
|         }
 | |
|         return 0;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     _finishAnimateTabMove() {
 | |
|       if (this.getAttribute("movingtab") != "true") {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       for (let tab of this._getVisibleTabs()) {
 | |
|         tab.style.transform = "";
 | |
|       }
 | |
| 
 | |
|       this.removeAttribute("movingtab");
 | |
|       gNavToolbox.removeAttribute("movingtab");
 | |
| 
 | |
|       this._handleTabSelect();
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Regroup all selected tabs around the
 | |
|      * tab in param
 | |
|      */
 | |
|     _groupSelectedTabs(tab) {
 | |
|       let draggedTabPos = tab._tPos;
 | |
|       let selectedTabs = gBrowser.selectedTabs;
 | |
|       let animate = !gReduceMotion;
 | |
| 
 | |
|       tab.groupingTabsData = {
 | |
|         finished: !animate,
 | |
|       };
 | |
| 
 | |
|       // Animate left selected tabs
 | |
| 
 | |
|       let insertAtPos = draggedTabPos - 1;
 | |
|       for (let i = selectedTabs.indexOf(tab) - 1; i > -1; i--) {
 | |
|         let movingTab = selectedTabs[i];
 | |
|         insertAtPos = newIndex(movingTab, insertAtPos);
 | |
| 
 | |
|         if (animate) {
 | |
|           movingTab.groupingTabsData = {};
 | |
|           addAnimationData(movingTab, insertAtPos, "left");
 | |
|         } else {
 | |
|           gBrowser.moveTabTo(movingTab, insertAtPos);
 | |
|         }
 | |
|         insertAtPos--;
 | |
|       }
 | |
| 
 | |
|       // Animate right selected tabs
 | |
| 
 | |
|       insertAtPos = draggedTabPos + 1;
 | |
|       for (
 | |
|         let i = selectedTabs.indexOf(tab) + 1;
 | |
|         i < selectedTabs.length;
 | |
|         i++
 | |
|       ) {
 | |
|         let movingTab = selectedTabs[i];
 | |
|         insertAtPos = newIndex(movingTab, insertAtPos);
 | |
| 
 | |
|         if (animate) {
 | |
|           movingTab.groupingTabsData = {};
 | |
|           addAnimationData(movingTab, insertAtPos, "right");
 | |
|         } else {
 | |
|           gBrowser.moveTabTo(movingTab, insertAtPos);
 | |
|         }
 | |
|         insertAtPos++;
 | |
|       }
 | |
| 
 | |
|       // Slide the relevant tabs to their new position.
 | |
|       for (let t of this._getVisibleTabs()) {
 | |
|         if (t.groupingTabsData && t.groupingTabsData.translateX) {
 | |
|           let translateX = (RTL_UI ? -1 : 1) * t.groupingTabsData.translateX;
 | |
|           t.style.transform = "translateX(" + translateX + "px)";
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       function newIndex(aTab, index) {
 | |
|         // Don't allow mixing pinned and unpinned tabs.
 | |
|         if (aTab.pinned) {
 | |
|           return Math.min(index, gBrowser._numPinnedTabs - 1);
 | |
|         }
 | |
|         return Math.max(index, gBrowser._numPinnedTabs);
 | |
|       }
 | |
| 
 | |
|       function addAnimationData(movingTab, movingTabNewIndex, side) {
 | |
|         let movingTabOldIndex = movingTab._tPos;
 | |
| 
 | |
|         if (movingTabOldIndex == movingTabNewIndex) {
 | |
|           // movingTab is already at the right position
 | |
|           // and thus don't need to be animated.
 | |
|           return;
 | |
|         }
 | |
| 
 | |
|         let movingTabWidth = movingTab.getBoundingClientRect().width;
 | |
|         let shift = (movingTabNewIndex - movingTabOldIndex) * movingTabWidth;
 | |
| 
 | |
|         movingTab.groupingTabsData.animate = true;
 | |
|         movingTab.setAttribute("tab-grouping", "true");
 | |
| 
 | |
|         movingTab.groupingTabsData.translateX = shift;
 | |
| 
 | |
|         let postTransitionCleanup = () => {
 | |
|           movingTab.groupingTabsData.newIndex = movingTabNewIndex;
 | |
|           movingTab.groupingTabsData.animate = false;
 | |
|         };
 | |
|         if (gReduceMotion) {
 | |
|           postTransitionCleanup();
 | |
|         } else {
 | |
|           let onTransitionEnd = transitionendEvent => {
 | |
|             if (
 | |
|               transitionendEvent.propertyName != "transform" ||
 | |
|               transitionendEvent.originalTarget != movingTab
 | |
|             ) {
 | |
|               return;
 | |
|             }
 | |
|             movingTab.removeEventListener("transitionend", onTransitionEnd);
 | |
|             postTransitionCleanup();
 | |
|           };
 | |
| 
 | |
|           movingTab.addEventListener("transitionend", onTransitionEnd);
 | |
|         }
 | |
| 
 | |
|         // Add animation data for tabs between movingTab (selected
 | |
|         // tab moving towards the dragged tab) and draggedTab.
 | |
|         // Those tabs in the middle should move in
 | |
|         // the opposite direction of movingTab.
 | |
| 
 | |
|         let lowerIndex = Math.min(movingTabOldIndex, draggedTabPos);
 | |
|         let higherIndex = Math.max(movingTabOldIndex, draggedTabPos);
 | |
| 
 | |
|         for (let i = lowerIndex + 1; i < higherIndex; i++) {
 | |
|           let middleTab = gBrowser.visibleTabs[i];
 | |
| 
 | |
|           if (middleTab.pinned != movingTab.pinned) {
 | |
|             // Don't mix pinned and unpinned tabs
 | |
|             break;
 | |
|           }
 | |
| 
 | |
|           if (middleTab.multiselected) {
 | |
|             // Skip because this selected tab should
 | |
|             // be shifted towards the dragged Tab.
 | |
|             continue;
 | |
|           }
 | |
| 
 | |
|           if (
 | |
|             !middleTab.groupingTabsData ||
 | |
|             !middleTab.groupingTabsData.translateX
 | |
|           ) {
 | |
|             middleTab.groupingTabsData = { translateX: 0 };
 | |
|           }
 | |
|           if (side == "left") {
 | |
|             middleTab.groupingTabsData.translateX -= movingTabWidth;
 | |
|           } else {
 | |
|             middleTab.groupingTabsData.translateX += movingTabWidth;
 | |
|           }
 | |
| 
 | |
|           middleTab.setAttribute("tab-grouping", "true");
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     _finishGroupSelectedTabs(tab) {
 | |
|       if (!tab.groupingTabsData || tab.groupingTabsData.finished) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       tab.groupingTabsData.finished = true;
 | |
| 
 | |
|       let selectedTabs = gBrowser.selectedTabs;
 | |
|       let tabIndex = selectedTabs.indexOf(tab);
 | |
| 
 | |
|       // Moving left tabs
 | |
|       for (let i = tabIndex - 1; i > -1; i--) {
 | |
|         let movingTab = selectedTabs[i];
 | |
|         if (movingTab.groupingTabsData.newIndex) {
 | |
|           gBrowser.moveTabTo(movingTab, movingTab.groupingTabsData.newIndex);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       // Moving right tabs
 | |
|       for (let i = tabIndex + 1; i < selectedTabs.length; i++) {
 | |
|         let movingTab = selectedTabs[i];
 | |
|         if (movingTab.groupingTabsData.newIndex) {
 | |
|           gBrowser.moveTabTo(movingTab, movingTab.groupingTabsData.newIndex);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       for (let t of this._getVisibleTabs()) {
 | |
|         t.style.transform = "";
 | |
|         t.removeAttribute("tab-grouping");
 | |
|         delete t.groupingTabsData;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     _isGroupTabsAnimationOver() {
 | |
|       for (let tab of gBrowser.selectedTabs) {
 | |
|         if (tab.groupingTabsData && tab.groupingTabsData.animate) {
 | |
|           return false;
 | |
|         }
 | |
|       }
 | |
|       return true;
 | |
|     }
 | |
| 
 | |
|     handleEvent(aEvent) {
 | |
|       switch (aEvent.type) {
 | |
|         case "mouseout":
 | |
|           // If the "related target" (the node to which the pointer went) is not
 | |
|           // a child of the current document, the mouse just left the window.
 | |
|           let relatedTarget = aEvent.relatedTarget;
 | |
|           if (relatedTarget && relatedTarget.ownerDocument == document) {
 | |
|             break;
 | |
|           }
 | |
|         // fall through
 | |
|         case "mousemove":
 | |
|           if (document.getElementById("tabContextMenu").state != "open") {
 | |
|             this._unlockTabSizing();
 | |
|           }
 | |
|           break;
 | |
|         default:
 | |
|           let methodName = `on_${aEvent.type}`;
 | |
|           if (methodName in this) {
 | |
|             this[methodName](aEvent);
 | |
|           } else {
 | |
|             throw new Error(`Unexpected event ${aEvent.type}`);
 | |
|           }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     _notifyBackgroundTab(aTab) {
 | |
|       if (
 | |
|         aTab.pinned ||
 | |
|         aTab.hidden ||
 | |
|         this.getAttribute("overflow") != "true"
 | |
|       ) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       this._lastTabToScrollIntoView = aTab;
 | |
|       if (!this._backgroundTabScrollPromise) {
 | |
|         this._backgroundTabScrollPromise = window
 | |
|           .promiseDocumentFlushed(() => {
 | |
|             let lastTabRect = this._lastTabToScrollIntoView.getBoundingClientRect();
 | |
|             let selectedTab = this.selectedItem;
 | |
|             if (selectedTab.pinned) {
 | |
|               selectedTab = null;
 | |
|             } else {
 | |
|               selectedTab = selectedTab.getBoundingClientRect();
 | |
|               selectedTab = {
 | |
|                 left: selectedTab.left,
 | |
|                 right: selectedTab.right,
 | |
|               };
 | |
|             }
 | |
|             return [
 | |
|               this._lastTabToScrollIntoView,
 | |
|               this.arrowScrollbox.scrollClientRect,
 | |
|               { left: lastTabRect.left, right: lastTabRect.right },
 | |
|               selectedTab,
 | |
|             ];
 | |
|           })
 | |
|           .then(([tabToScrollIntoView, scrollRect, tabRect, selectedRect]) => {
 | |
|             // First off, remove the promise so we can re-enter if necessary.
 | |
|             delete this._backgroundTabScrollPromise;
 | |
|             // Then, if the layout info isn't for the last-scrolled-to-tab, re-run
 | |
|             // the code above to get layout info for *that* tab, and don't do
 | |
|             // anything here, as we really just want to run this for the last-opened tab.
 | |
|             if (this._lastTabToScrollIntoView != tabToScrollIntoView) {
 | |
|               this._notifyBackgroundTab(this._lastTabToScrollIntoView);
 | |
|               return;
 | |
|             }
 | |
|             delete this._lastTabToScrollIntoView;
 | |
|             // Is the new tab already completely visible?
 | |
|             if (
 | |
|               scrollRect.left <= tabRect.left &&
 | |
|               tabRect.right <= scrollRect.right
 | |
|             ) {
 | |
|               return;
 | |
|             }
 | |
| 
 | |
|             if (this.arrowScrollbox.smoothScroll) {
 | |
|               // Can we make both the new tab and the selected tab completely visible?
 | |
|               if (
 | |
|                 !selectedRect ||
 | |
|                 Math.max(
 | |
|                   tabRect.right - selectedRect.left,
 | |
|                   selectedRect.right - tabRect.left
 | |
|                 ) <= scrollRect.width
 | |
|               ) {
 | |
|                 this.arrowScrollbox.ensureElementIsVisible(tabToScrollIntoView);
 | |
|                 return;
 | |
|               }
 | |
| 
 | |
|               this.arrowScrollbox.scrollByPixels(
 | |
|                 RTL_UI
 | |
|                   ? selectedRect.right - scrollRect.right
 | |
|                   : selectedRect.left - scrollRect.left
 | |
|               );
 | |
|             }
 | |
| 
 | |
|             if (!this._animateElement.hasAttribute("highlight")) {
 | |
|               this._animateElement.setAttribute("highlight", "true");
 | |
|               setTimeout(
 | |
|                 function(ele) {
 | |
|                   ele.removeAttribute("highlight");
 | |
|                 },
 | |
|                 150,
 | |
|                 this._animateElement
 | |
|               );
 | |
|             }
 | |
|           });
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     _getDragTargetTab(event, isLink) {
 | |
|       let tab = event.target;
 | |
|       while (tab && tab.localName != "tab") {
 | |
|         tab = tab.parentNode;
 | |
|       }
 | |
|       if (tab && isLink) {
 | |
|         let { width } = tab.getBoundingClientRect();
 | |
|         if (
 | |
|           event.screenX < tab.screenX + width * 0.25 ||
 | |
|           event.screenX > tab.screenX + width * 0.75
 | |
|         ) {
 | |
|           return null;
 | |
|         }
 | |
|       }
 | |
|       return tab;
 | |
|     }
 | |
| 
 | |
|     _getDropIndex(event, isLink) {
 | |
|       var tabs = this.allTabs;
 | |
|       var tab = this._getDragTargetTab(event, isLink);
 | |
|       if (!RTL_UI) {
 | |
|         for (let i = tab ? tab._tPos : 0; i < tabs.length; i++) {
 | |
|           if (
 | |
|             !tabs[i].hidden &&
 | |
|             event.screenX <
 | |
|               tabs[i].screenX + tabs[i].getBoundingClientRect().width / 2
 | |
|           ) {
 | |
|             return i;
 | |
|           }
 | |
|         }
 | |
|       } else {
 | |
|         for (let i = tab ? tab._tPos : 0; i < tabs.length; i++) {
 | |
|           if (
 | |
|             !tabs[i].hidden &&
 | |
|             event.screenX >
 | |
|               tabs[i].screenX + tabs[i].getBoundingClientRect().width / 2
 | |
|           ) {
 | |
|             return i;
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|       return tabs.length;
 | |
|     }
 | |
| 
 | |
|     getDropEffectForTabDrag(event) {
 | |
|       var dt = event.dataTransfer;
 | |
| 
 | |
|       let isMovingTabs = dt.mozItemCount > 0;
 | |
|       for (let i = 0; i < dt.mozItemCount; i++) {
 | |
|         // tabs are always added as the first type
 | |
|         let types = dt.mozTypesAt(0);
 | |
|         if (types[0] != TAB_DROP_TYPE) {
 | |
|           isMovingTabs = false;
 | |
|           break;
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       if (isMovingTabs) {
 | |
|         let sourceNode = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
 | |
|         if (
 | |
|           XULElement.isInstance(sourceNode) &&
 | |
|           sourceNode.localName == "tab" &&
 | |
|           sourceNode.ownerGlobal.isChromeWindow &&
 | |
|           sourceNode.ownerDocument.documentElement.getAttribute("windowtype") ==
 | |
|             "navigator:browser" &&
 | |
|           sourceNode.ownerGlobal.gBrowser.tabContainer == sourceNode.container
 | |
|         ) {
 | |
|           // Do not allow transfering a private tab to a non-private window
 | |
|           // and vice versa.
 | |
|           if (
 | |
|             PrivateBrowsingUtils.isWindowPrivate(window) !=
 | |
|             PrivateBrowsingUtils.isWindowPrivate(sourceNode.ownerGlobal)
 | |
|           ) {
 | |
|             return "none";
 | |
|           }
 | |
| 
 | |
|           if (
 | |
|             window.gMultiProcessBrowser !=
 | |
|             sourceNode.ownerGlobal.gMultiProcessBrowser
 | |
|           ) {
 | |
|             return "none";
 | |
|           }
 | |
| 
 | |
|           if (
 | |
|             window.gFissionBrowser != sourceNode.ownerGlobal.gFissionBrowser
 | |
|           ) {
 | |
|             return "none";
 | |
|           }
 | |
| 
 | |
|           return dt.dropEffect == "copy" ? "copy" : "move";
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       if (browserDragAndDrop.canDropLink(event)) {
 | |
|         return "link";
 | |
|       }
 | |
|       return "none";
 | |
|     }
 | |
| 
 | |
|     _handleNewTab(tab) {
 | |
|       if (tab.container != this) {
 | |
|         return;
 | |
|       }
 | |
|       tab._fullyOpen = true;
 | |
|       gBrowser.tabAnimationsInProgress--;
 | |
| 
 | |
|       this._updateCloseButtons();
 | |
| 
 | |
|       if (tab.getAttribute("selected") == "true") {
 | |
|         this._handleTabSelect();
 | |
|       } else if (!tab.hasAttribute("skipbackgroundnotify")) {
 | |
|         this._notifyBackgroundTab(tab);
 | |
|       }
 | |
| 
 | |
|       // XXXmano: this is a temporary workaround for bug 345399
 | |
|       // We need to manually update the scroll buttons disabled state
 | |
|       // if a tab was inserted to the overflow area or removed from it
 | |
|       // without any scrolling and when the tabbar has already
 | |
|       // overflowed.
 | |
|       this.arrowScrollbox._updateScrollButtonsDisabledState();
 | |
| 
 | |
|       // If this browser isn't lazy (indicating it's probably created by
 | |
|       // session restore), preload the next about:newtab if we don't
 | |
|       // already have a preloaded browser.
 | |
|       if (tab.linkedPanel) {
 | |
|         NewTabPagePreloading.maybeCreatePreloadedBrowser(window);
 | |
|       }
 | |
| 
 | |
|       if (UserInteraction.running("browser.tabs.opening", window)) {
 | |
|         UserInteraction.finish("browser.tabs.opening", window);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     _canAdvanceToTab(aTab) {
 | |
|       return !aTab.closing;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Returns the panel associated with a tab if it has a connected browser
 | |
|      * and/or it is the selected tab.
 | |
|      * For background lazy browsers, this will return null.
 | |
|      */
 | |
|     getRelatedElement(aTab) {
 | |
|       if (!aTab) {
 | |
|         return null;
 | |
|       }
 | |
| 
 | |
|       // Cannot access gBrowser before it's initialized.
 | |
|       if (!gBrowser._initialized) {
 | |
|         return this.tabbox.tabpanels.firstElementChild;
 | |
|       }
 | |
| 
 | |
|       // If the tab's browser is lazy, we need to `_insertBrowser` in order
 | |
|       // to have a linkedPanel.  This will also serve to bind the browser
 | |
|       // and make it ready to use. We only do this if the tab is selected
 | |
|       // because otherwise, callers might end up unintentionally binding the
 | |
|       // browser for lazy background tabs.
 | |
|       if (!aTab.linkedPanel) {
 | |
|         if (!aTab.selected) {
 | |
|           return null;
 | |
|         }
 | |
|         gBrowser._insertBrowser(aTab);
 | |
|       }
 | |
|       return document.getElementById(aTab.linkedPanel);
 | |
|     }
 | |
| 
 | |
|     _updateNewTabVisibility() {
 | |
|       // Helper functions to help deal with customize mode wrapping some items
 | |
|       let wrap = n =>
 | |
|         n.parentNode.localName == "toolbarpaletteitem" ? n.parentNode : n;
 | |
|       let unwrap = n =>
 | |
|         n && n.localName == "toolbarpaletteitem" ? n.firstElementChild : n;
 | |
| 
 | |
|       // Starting from the tabs element, find the next sibling that:
 | |
|       // - isn't hidden; and
 | |
|       // - isn't the all-tabs button.
 | |
|       // If it's the new tab button, consider the new tab button adjacent to the tabs.
 | |
|       // If the new tab button is marked as adjacent and the tabstrip doesn't
 | |
|       // overflow, we'll display the 'new tab' button inline in the tabstrip.
 | |
|       // In all other cases, the separate new tab button is displayed in its
 | |
|       // customized location.
 | |
|       let sib = this;
 | |
|       do {
 | |
|         sib = unwrap(wrap(sib).nextElementSibling);
 | |
|       } while (sib && (sib.hidden || sib.id == "alltabs-button"));
 | |
| 
 | |
|       const kAttr = "hasadjacentnewtabbutton";
 | |
|       if (sib && sib.id == "new-tab-button") {
 | |
|         this.setAttribute(kAttr, "true");
 | |
|       } else {
 | |
|         this.removeAttribute(kAttr);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     onWidgetAfterDOMChange(aNode, aNextNode, aContainer) {
 | |
|       if (
 | |
|         aContainer.ownerDocument == document &&
 | |
|         aContainer.id == "TabsToolbar-customization-target"
 | |
|       ) {
 | |
|         this._updateNewTabVisibility();
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     onAreaNodeRegistered(aArea, aContainer) {
 | |
|       if (aContainer.ownerDocument == document && aArea == "TabsToolbar") {
 | |
|         this._updateNewTabVisibility();
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     onAreaReset(aArea, aContainer) {
 | |
|       this.onAreaNodeRegistered(aArea, aContainer);
 | |
|     }
 | |
| 
 | |
|     _hiddenSoundPlayingStatusChanged(tab, opts) {
 | |
|       let closed = opts && opts.closed;
 | |
|       if (!closed && tab.soundPlaying && tab.hidden) {
 | |
|         this._hiddenSoundPlayingTabs.add(tab);
 | |
|         this.setAttribute("hiddensoundplaying", "true");
 | |
|       } else {
 | |
|         this._hiddenSoundPlayingTabs.delete(tab);
 | |
|         if (this._hiddenSoundPlayingTabs.size == 0) {
 | |
|           this.removeAttribute("hiddensoundplaying");
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     destroy() {
 | |
|       if (this.boundObserve) {
 | |
|         Services.prefs.removeObserver("privacy.userContext", this.boundObserve);
 | |
|       }
 | |
|       CustomizableUI.removeListener(this);
 | |
|     }
 | |
| 
 | |
|     updateTabIndicatorAttr(tab) {
 | |
|       const theseAttributes = ["soundplaying", "muted", "activemedia-blocked"];
 | |
|       const notTheseAttributes = ["pinned", "sharing", "crashed"];
 | |
| 
 | |
|       if (notTheseAttributes.some(attr => tab.getAttribute(attr))) {
 | |
|         tab.removeAttribute("indicator-replaces-favicon");
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       if (theseAttributes.some(attr => tab.getAttribute(attr))) {
 | |
|         tab.setAttribute("indicator-replaces-favicon", true);
 | |
|       } else {
 | |
|         tab.removeAttribute("indicator-replaces-favicon");
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   customElements.define("tabbrowser-tabs", MozTabbrowserTabs, {
 | |
|     extends: "tabs",
 | |
|   });
 | |
| }
 |