tabsplitview.js (8127B)
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 "use strict"; 6 7 // This is loaded into chrome windows with the subscript loader. Wrap in 8 // a block to prevent accidentally leaking globals onto `window`. 9 { 10 ChromeUtils.defineESModuleGetters(this, { 11 DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs", 12 }); 13 14 /** 15 * A shared task which updates the urlbar indicator whenever: 16 * - A split view is activated or deactivated. 17 * - The active tab of a split view changes. 18 * - The order of tabs in a split view changes. 19 * 20 * @type {DeferredTask} 21 */ 22 const updateUrlbarButton = new DeferredTask(() => { 23 const { activeSplitView, selectedTab } = gBrowser; 24 const button = document.getElementById("split-view-button"); 25 if (activeSplitView) { 26 const activeIndex = activeSplitView.tabs.indexOf(selectedTab); 27 button.hidden = false; 28 button.setAttribute("data-active-index", activeIndex); 29 } else { 30 button.hidden = true; 31 button.removeAttribute("data-active-index"); 32 } 33 }, 0); 34 35 class MozTabSplitViewWrapper extends MozXULElement { 36 /** @type {MutationObserver} */ 37 #tabChangeObserver; 38 39 /** @type {MozTabbrowserTab[]} */ 40 #tabs = []; 41 42 #storedPanelWidths = new WeakMap(); 43 44 /** 45 * @returns {boolean} 46 */ 47 get hasActiveTab() { 48 return this.hasAttribute("hasactivetab"); 49 } 50 51 /** 52 * @returns {MozTabbrowserGroup} 53 */ 54 get group() { 55 return gBrowser.isTabGroup(this.parentElement) 56 ? this.parentElement 57 : null; 58 } 59 60 /** 61 * @param {boolean} val 62 */ 63 set hasActiveTab(val) { 64 this.toggleAttribute("hasactivetab", val); 65 } 66 67 constructor() { 68 super(); 69 } 70 71 connectedCallback() { 72 // Set up TabSelect listener, as this gets 73 // removed in disconnectedCallback 74 this.ownerGlobal.addEventListener("TabSelect", this); 75 76 this.#observeTabChanges(); 77 this.#restorePanelWidths(); 78 79 if (this.hasActiveTab) { 80 this.#activate(); 81 } 82 83 if (this._initialized) { 84 return; 85 } 86 87 this._initialized = true; 88 89 this.textContent = ""; 90 91 // Mirroring MozTabbrowserTab 92 this.container = gBrowser.tabContainer; 93 } 94 95 disconnectedCallback() { 96 this.#tabChangeObserver?.disconnect(); 97 this.ownerGlobal.removeEventListener("TabSelect", this); 98 this.#deactivate(); 99 this.#resetPanelWidths(); 100 this.container.dispatchEvent( 101 new CustomEvent("SplitViewRemoved", { 102 bubbles: true, 103 composed: true, 104 }) 105 ); 106 } 107 108 #observeTabChanges() { 109 if (!this.#tabChangeObserver) { 110 this.#tabChangeObserver = new window.MutationObserver(() => { 111 if (this.tabs.length) { 112 this.hasActiveTab = this.tabs.some(tab => tab.selected); 113 this.tabs.forEach((tab, index) => { 114 // Renumber tabs so that a11y tools can tell users that a given 115 // tab is "1 of 2" in the split view, for example. 116 tab.setAttribute("aria-posinset", index + 1); 117 tab.setAttribute("aria-setsize", this.tabs.length); 118 tab.updateSplitViewAriaLabel(index); 119 }); 120 } else { 121 this.remove(); 122 } 123 124 if (this.tabs.length < 2) { 125 this.unsplitTabs(); 126 } 127 }); 128 } 129 this.#tabChangeObserver.observe(this, { 130 childList: true, 131 }); 132 } 133 134 get splitViewId() { 135 return this.getAttribute("splitViewId"); 136 } 137 138 set splitViewId(val) { 139 this.setAttribute("splitViewId", val); 140 } 141 142 /** 143 * @returns {MozTabbrowserTab[]} 144 */ 145 get tabs() { 146 return Array.from(this.children).filter(node => node.matches("tab")); 147 } 148 149 get visible() { 150 return this.tabs.every(tab => tab.visible); 151 } 152 153 /** 154 * Get the list of tab panels from this split view. 155 * 156 * @returns {XULElement[]} 157 */ 158 get panels() { 159 const panels = []; 160 for (const { linkedPanel } of this.#tabs) { 161 const el = document.getElementById(linkedPanel); 162 if (el) { 163 panels.push(el); 164 } 165 } 166 return panels; 167 } 168 169 /** 170 * Show all Split View tabs in the content area. 171 */ 172 #activate(skipShowPanels = false) { 173 updateUrlbarButton.arm(); 174 if (!skipShowPanels) { 175 gBrowser.showSplitViewPanels(this.#tabs); 176 } 177 this.container.dispatchEvent( 178 new CustomEvent("TabSplitViewActivate", { 179 detail: { tabs: this.#tabs, splitview: this }, 180 bubbles: true, 181 }) 182 ); 183 } 184 185 /** 186 * Remove Split View tabs from the content area. 187 */ 188 #deactivate(skipHidePanels = false) { 189 if (!skipHidePanels) { 190 gBrowser.hideSplitViewPanels(this.#tabs); 191 } 192 updateUrlbarButton.arm(); 193 this.container.dispatchEvent( 194 new CustomEvent("TabSplitViewDeactivate", { 195 detail: { tabs: this.#tabs, splitview: this }, 196 bubbles: true, 197 }) 198 ); 199 } 200 201 /** 202 * Remove customized panel widths. Cache width values so that they can be 203 * restored if this Split View is later reactivated. 204 */ 205 #resetPanelWidths() { 206 for (const panel of this.panels) { 207 const width = panel.getAttribute("width"); 208 if (width) { 209 this.#storedPanelWidths.set(panel, width); 210 panel.removeAttribute("width"); 211 panel.style.removeProperty("width"); 212 } 213 } 214 } 215 216 /** 217 * Resize panel widths back to cached values. 218 */ 219 #restorePanelWidths() { 220 for (const panel of this.panels) { 221 const width = this.#storedPanelWidths.get(panel); 222 if (width) { 223 panel.setAttribute("width", width); 224 panel.style.setProperty("width", width + "px"); 225 } 226 } 227 } 228 229 /** 230 * add tabs to the split view wrapper 231 * 232 * @param {MozTabbrowserTab[]} tabs 233 */ 234 addTabs(tabs) { 235 for (let tab of tabs) { 236 if (tab.pinned) { 237 return; 238 } 239 let tabToMove = 240 this.ownerGlobal === tab.ownerGlobal 241 ? tab 242 : gBrowser.adoptTab(tab, { 243 tabIndex: gBrowser.tabs.at(-1)._tPos + 1, 244 selectTab: tab.selected, 245 }); 246 this.#tabs.push(tabToMove); 247 gBrowser.moveTabToSplitView(tabToMove, this); 248 if (tab === gBrowser.selectedTab) { 249 this.hasActiveTab = true; 250 } 251 } 252 if (this.hasActiveTab) { 253 this.#activate(); 254 gBrowser.setIsSplitViewActive(true, this.#tabs); 255 } 256 } 257 258 /** 259 * Remove all tabs from the split view wrapper and delete the split view. 260 */ 261 unsplitTabs() { 262 gBrowser.unsplitTabs(this); 263 gBrowser.setIsSplitViewActive(false, this.#tabs); 264 } 265 266 /** 267 * Replace a tab in the split view with another tab 268 */ 269 replaceTab(tabToReplace, newTab) { 270 this.#tabs = this.#tabs.filter(tab => tab != tabToReplace); 271 this.addTabs([newTab]); 272 gBrowser.removeTab(tabToReplace); 273 } 274 275 /** 276 * Reverse order of the tabs in the split view wrapper. 277 */ 278 reverseTabs() { 279 const [firstTab, secondTab] = this.#tabs; 280 gBrowser.moveTabBefore(secondTab, firstTab); 281 this.#tabs = [secondTab, firstTab]; 282 gBrowser.showSplitViewPanels(this.#tabs); 283 updateUrlbarButton.arm(); 284 } 285 286 /** 287 * Close all tabs in the split view wrapper and delete the split view. 288 */ 289 close() { 290 gBrowser.removeTabs(this.#tabs); 291 } 292 293 /** 294 * @param {CustomEvent} event 295 */ 296 on_TabSelect(event) { 297 this.hasActiveTab = event.target.splitview === this; 298 gBrowser.setIsSplitViewActive(this.hasActiveTab, this.#tabs); 299 if (this.hasActiveTab) { 300 this.#activate(); 301 } else { 302 this.#deactivate(true); 303 } 304 } 305 } 306 307 customElements.define("tab-split-view-wrapper", MozTabSplitViewWrapper); 308 }