browser-allTabsMenu.js (8226B)
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/. */ 4 5 ChromeUtils.defineESModuleGetters(this, { 6 BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.sys.mjs", 7 GroupsPanel: "moz-src:///browser/components/tabbrowser/GroupsList.sys.mjs", 8 NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", 9 TabsPanel: "moz-src:///browser/components/tabbrowser/TabsList.sys.mjs", 10 }); 11 12 var gTabsPanel = { 13 kElements: { 14 allTabsButton: "alltabs-button", 15 allTabsView: "allTabsMenu-allTabsView", 16 allTabsViewTabs: "allTabsMenu-allTabsView-tabs", 17 dropIndicator: "allTabsMenu-dropIndicator", 18 containerTabsView: "allTabsMenu-containerTabsView", 19 hiddenTabsButton: "allTabsMenu-hiddenTabsButton", 20 hiddenTabsView: "allTabsMenu-hiddenTabsView", 21 hiddenTabsViewTabs: "allTabsMenu-hiddenTabsView-tabs", 22 hiddenAudioTabs: "allTabsMenu-allTabsView-hiddenAudio-tabs", 23 groupsView: "allTabsMenu-groupsView", 24 groupsSubView: "allTabsMenu-groupsSubView", 25 }, 26 _initialized: false, 27 _initializedElements: false, 28 29 initElements() { 30 if (this._initializedElements) { 31 return; 32 } 33 let template = document.getElementById("allTabsMenu-container"); 34 template.replaceWith(template.content); 35 36 for (let [name, id] of Object.entries(this.kElements)) { 37 this[name] = document.getElementById(id); 38 } 39 this._initializedElements = true; 40 }, 41 42 hasHiddenTabsExcludingFxView() { 43 // Exclude Firefox View, see Bug 1880138. 44 return gBrowser.tabs.some( 45 tab => tab.hidden && tab != FirefoxViewHandler.tab 46 ); 47 }, 48 49 init() { 50 if (this._initialized) { 51 return; 52 } 53 54 this.initElements(); 55 56 this.hiddenAudioTabsPopup = new TabsPanel({ 57 view: this.allTabsView, 58 containerNode: this.hiddenAudioTabs, 59 filterFn: tab => tab.soundPlaying, 60 onlyHiddenTabs: true, 61 }); 62 this.allTabsPanel = new TabsPanel({ 63 view: this.allTabsView, 64 containerNode: this.allTabsViewTabs, 65 filterFn: tab => !tab.hidden, 66 dropIndicator: this.dropIndicator, 67 onlyHiddenTabs: false, 68 }); 69 this.groupsPanel = new GroupsPanel({ 70 view: this.allTabsView, 71 containerNode: this.groupsView, 72 }); 73 this.showAllGroupsPanel = new GroupsPanel({ 74 view: this.groupsSubView, 75 containerNode: document.getElementById("allTabsMenu-groupsSubView-body"), 76 showAll: true, 77 }); 78 79 this.allTabsView.addEventListener("ViewShowing", () => { 80 PanelUI._ensureShortcutsShown(this.allTabsView); 81 82 let containersEnabled = 83 Services.prefs.getBoolPref("privacy.userContext.enabled") && 84 !PrivateBrowsingUtils.isWindowPrivate(window); 85 document.getElementById("allTabsMenu-containerTabsButton").hidden = 86 !containersEnabled; 87 88 const hasHiddenTabs = this.hasHiddenTabsExcludingFxView(); 89 document.getElementById("allTabsMenu-hiddenTabsButton").hidden = 90 !hasHiddenTabs; 91 document.getElementById("allTabsMenu-hiddenTabsSeparator").hidden = 92 !hasHiddenTabs; 93 94 let closeDuplicateEnabled = Services.prefs.getBoolPref( 95 "browser.tabs.context.close-duplicate.enabled" 96 ); 97 let closeDuplicateTabsItem = document.getElementById( 98 "allTabsMenu-closeDuplicateTabs" 99 ); 100 closeDuplicateTabsItem.hidden = !closeDuplicateEnabled; 101 closeDuplicateTabsItem.disabled = 102 !closeDuplicateEnabled || !gBrowser.getAllDuplicateTabsToClose().length; 103 104 let syncedTabs = document.getElementById("allTabsMenu-syncedTabs"); 105 syncedTabs.hidden = 106 !PlacesUIUtils.shouldShowTabsFromOtherComputersMenuitem(); 107 }); 108 109 this.allTabsView.addEventListener("ViewShown", () => 110 this.allTabsView 111 .querySelector(".all-tabs-item[selected]") 112 ?.scrollIntoView({ block: "center" }) 113 ); 114 115 this.allTabsView.addEventListener("command", event => { 116 let { target } = event; 117 let { PanelUI } = target.ownerGlobal; 118 switch (target.id) { 119 case "allTabsMenu-searchTabs": 120 Glean.browserUiInteraction.listAllTabsAction.search_tabs.add(1); 121 this.searchTabs(); 122 break; 123 case "allTabsMenu-closeDuplicateTabs": 124 Glean.browserUiInteraction.listAllTabsAction.close_all_duplicates.add( 125 1 126 ); 127 gBrowser.removeAllDuplicateTabs(); 128 break; 129 case "allTabsMenu-containerTabsButton": 130 PanelUI.showSubView(this.kElements.containerTabsView, target); 131 break; 132 case "allTabsMenu-hiddenTabsButton": 133 PanelUI.showSubView(this.kElements.hiddenTabsView, target); 134 break; 135 case "allTabsMenu-syncedTabs": 136 Glean.browserUiInteraction.listAllTabsAction.tabs_from_devices.add(1); 137 SidebarController.show("viewTabsSidebar"); 138 break; 139 case "allTabsMenu-groupsViewShowMore": 140 PanelUI.showSubView(this.kElements.groupsSubView, target); 141 break; 142 } 143 }); 144 145 let containerTabsMenuSeparator = 146 this.containerTabsView.querySelector("toolbarseparator"); 147 this.containerTabsView.addEventListener("ViewShowing", e => { 148 let elements = []; 149 let frag = document.createDocumentFragment(); 150 151 ContextualIdentityService.getPublicIdentities().forEach(identity => { 152 let menuitem = document.createXULElement("toolbarbutton"); 153 menuitem.setAttribute("class", "subviewbutton subviewbutton-iconic"); 154 if (identity.name) { 155 menuitem.setAttribute("label", identity.name); 156 } else { 157 document.l10n.setAttributes(menuitem, identity.l10nId); 158 } 159 // The styles depend on this. 160 menuitem.setAttribute("usercontextid", identity.userContextId); 161 // The command handler depends on this. 162 menuitem.setAttribute("data-usercontextid", identity.userContextId); 163 menuitem.classList.add("identity-icon-" + identity.icon); 164 menuitem.classList.add("identity-color-" + identity.color); 165 166 menuitem.setAttribute("command", "Browser:NewUserContextTab"); 167 168 frag.appendChild(menuitem); 169 elements.push(menuitem); 170 }); 171 172 e.target.addEventListener( 173 "ViewHiding", 174 () => { 175 for (let element of elements) { 176 element.remove(); 177 } 178 }, 179 { once: true } 180 ); 181 containerTabsMenuSeparator.parentNode.insertBefore( 182 frag, 183 containerTabsMenuSeparator 184 ); 185 }); 186 187 this.hiddenTabsPopup = new TabsPanel({ 188 view: this.hiddenTabsView, 189 containerNode: this.hiddenTabsViewTabs, 190 filterFn: tab => tab != FirefoxViewHandler.tab, 191 onlyHiddenTabs: true, 192 }); 193 194 this._initialized = true; 195 }, 196 197 get canOpen() { 198 this.initElements(); 199 return isElementVisible(this.allTabsButton); 200 }, 201 202 showAllTabsPanel(event, entrypoint = "unknown") { 203 // Note that event may be null. 204 205 // Only space and enter should open the popup, ignore other keypresses: 206 if (event?.type == "keypress" && event.key != "Enter" && event.key != " ") { 207 return; 208 } 209 this.init(); 210 if (this.canOpen) { 211 Glean.browserUiInteraction.allTabsPanelEntrypoint[entrypoint].add(1); 212 BrowserUsageTelemetry.recordInteractionEvent( 213 entrypoint, 214 "all-tabs-panel-entrypoint" 215 ); 216 PanelUI.showSubView( 217 this.kElements.allTabsView, 218 this.allTabsButton, 219 event 220 ); 221 } 222 }, 223 224 hideAllTabsPanel() { 225 let panel = this.allTabsView?.closest("panel"); 226 if (panel) { 227 PanelMultiView.hidePopup(panel); 228 } 229 }, 230 231 showHiddenTabsPanel(event, entrypoint = "unknown") { 232 this.init(); 233 if (!this.canOpen) { 234 return; 235 } 236 this.allTabsView.addEventListener( 237 "ViewShown", 238 () => { 239 PanelUI.showSubView( 240 this.kElements.hiddenTabsView, 241 this.hiddenTabsButton 242 ); 243 }, 244 { once: true } 245 ); 246 this.showAllTabsPanel(event, entrypoint); 247 }, 248 249 searchTabs() { 250 gURLBar.search(UrlbarTokenizer.RESTRICT.OPENPAGE, { 251 searchModeEntry: "tabmenu", 252 }); 253 }, 254 };