class SplitViewsUtils { /** * @returns {HTMLDivElement} */ get tabBrowser() { if (!this._tabBrowser) { this._tabBrowser = document.getElementById('tabbrowser-tabpanels'); } // @ts-ignore return this._tabBrowser; } } class SplitViewsBase extends SplitViewsUtils { /** * @type {SplitView[]} */ data; /** * @param {SplitViewConfig} config */ constructor(config) { super(); this.config = config; this.data = []; this.currentView = -1; this.globalIdCounter = 0; // Added to "navigator-toolbox" element this.parentSplitIndicator = this.config.splitIndicator + '-view'; this.log('SplitViewsBase initialized'); } /** * @param {string} message * @protected */ log(message) { console.log(`SplitViews: ${message}`); } get isActivated() { return this.currentView !== -1; } get activeView() { if (!this.isActivated) { throw new Error('No active view'); } return this.data[this.currentView]; } /** * @param {MockedExports.BrowserTab} tab */ getTabView(tab) { return this.data.find(view => view.tabs.includes(tab)); } /** * @param {MockedExports.BrowserTab} tab */ isTabSplit(tab) { return tab.hasAttribute(this.config.splitIndicator); } /** * @param {MockedExports.BrowserTab} tab * @param {SplitType} type * @param {MockedExports.BrowserTab[]} tabs */ changeSplitViewBase(tab, type, tabs) { let view = this.getTabView(tab); if (!view) { return -1; } view.type = type; view.tabs.push(...tabs.filter(t => !view.tabs.includes(t))); return view.id; } /** * @param {MockedExports.BrowserTab[]} tabs * @param {SplitType} type */ createSplitViewBase(tabs, type) { let view = { id: this.globalIdCounter++, type, tabs, }; this.data.push(view); this.currentView = this.data.length - 1; return view.id; } /** * Applies the grid layout to the tabs. * * @param {MockedExports.BrowserTab[]} tabs - The tabs to apply the grid layout to. * @param {string} gridType - The type of grid layout. * @param {MockedExports.BrowserTab} activeTab - The active tab. */ applyGridLayout(tabs, gridType, activeTab) { const gridAreas = this.calculateGridAreas(tabs, gridType); this.tabBrowser.style.gridTemplateAreas = gridAreas; tabs.forEach((tab, index) => { tab.setAttribute(this.config.splitIndicator, "true"); const container = tab.linkedBrowser.closest(".browserSidebarContainer"); if (!container) { throw new Error("Container not found"); } this.styleContainer(container, tab === activeTab, index, gridType); }); } /** * Styles the container for a tab. * * @param {Element} container - The container element. * @param {boolean} isActive - Indicates if the tab is active. * @param {number} index - The index of the tab. * @param {string} gridType - The type of grid layout. */ styleContainer(container, isActive, index, gridType) { container.removeAttribute("split-active"); container.setAttribute(this.config.splitIndicator, "true"); if (isActive) { container.setAttribute("split-active", "true"); } container.setAttribute("split-anim", "true"); container.addEventListener("click", this.handleTabClick); if (gridType === "grid") { // @ts-ignore container.style.gridArea = `tab${index + 1}`; } } /** * Calculates the grid areas for the tabs. * * @param {MockedExports.BrowserTab[]} tabs - The tabs. * @param {string} gridType - The type of grid layout. * @returns {string} The calculated grid areas. */ calculateGridAreas(tabs, gridType) { if (gridType === "grid") { return this.calculateGridAreasForGrid(tabs); } if (gridType === "vsep") { return `'${tabs.map((_, j) => `tab${j + 1}`).join(" ")}'`; } if (gridType === "hsep") { return tabs.map((_, j) => `'tab${j + 1}'`).join(" "); } return ""; } /** * Handles the tab click event. * * @param {Event} event - The click event. */ handleTabClick(event) { const container = event.currentTarget; // @ts-ignore const tab = window.gBrowser.tabs.find( // @ts-ignore t => t.linkedBrowser.closest(".browserSidebarContainer") === container ); if (tab) { // @ts-ignore window.gBrowser.selectedTab = tab; } }; /** * Calculates the grid areas for the tabs in a grid layout. * * @param {MockedExports.BrowserTab[]} tabs - The tabs. * @returns {string} The calculated grid areas. */ calculateGridAreasForGrid(tabs) { const rows = ["", ""]; tabs.forEach((_, i) => { if (i % 2 === 0) { rows[0] += ` tab${i + 1}`; } else { rows[1] += ` tab${i + 1}`; } }); if (tabs.length === 2) { return "'tab1 tab2'"; } if (tabs.length % 2 !== 0) { rows[1] += ` tab${tabs.length}`; } return `'${rows[0].trim()}' '${rows[1].trim()}'`; } /** * @param {number} viewId * @protected */ updateSplitView(viewId) { let view = this.data.find(view => view.id === viewId); this.log(`updateSplitView: ${viewId}`); this.currentView = viewId; if (!view) { this.tabBrowser.removeAttribute(this.parentSplitIndicator); throw new Error('TODO: Remove split view'); return; } this.tabBrowser.setAttribute(this.parentSplitIndicator, "true"); this.applyGridLayout(view.tabs, view.type, view.tabs[0]); } /** * @param {MockedExports.BrowserTab[]} tabs * @param {SplitType} type * @protected */ createOrChangeSplitView(tabs, type) { let activeTab = tabs.find(tab => this.isTabSplit(tab)); this.log(`createOrChangeSplitView: ${type}`); let viewId = -1; if (activeTab) { viewId = this.changeSplitViewBase(activeTab, type, tabs); } else { viewId = this.createSplitViewBase(tabs, type); } this.updateSplitView(viewId); } } // Public API exposed by the module export class SplitViews extends SplitViewsBase { /** * @param {SplitViewConfig} config */ constructor(config) { super(config); this.addEventListeners(); } addEventListeners() { window.addEventListener('TabClose', this); } /** * @param {Event} event */ handleEvent(event) { switch (event.type) { case 'TabClose': this.onTabClose(event); break; } } /** * @param {Event} event */ // @ts-ignore // @ts-ignore onTabClose(event) { } /** * @param {MockedExports.Browser} browser */ // @ts-ignore // @ts-ignore onLocationChange(browser) { this.log('onLocationChange'); } /** * @param {SplitType} type */ // @ts-ignore // @ts-ignore tileCurrentView(type) { this.log('tileCurrentView'); } closeCurrentView() { this.log('closeCurrentView'); } /** * @param {MockedExports.BrowserTab} tab */ // @ts-ignore // @ts-ignore tabIsInActiveView(tab) { this.log('tabIsInActiveView'); return false; } getActiveViewTabs() { this.log('getActiveViewTabs'); return []; } getActiveViewType() { if (!this.isActivated) { return undefined; } return this.activeView.type; } /** * @param {MockedExports.BrowserTab[]} tabs * @param {SplitType} type * @public */ createSplitView(tabs, type = this.config.defaultSplitView) { if (tabs.length < 2) { return; } this.createOrChangeSplitView(tabs, type); } };