/** * PaneManager + PaneTabManager — Multi-pane split view for ObsiGate (v2.0.0-dev) * * Architecture: * Single-pane (default): uses existing singleton TabManager + #content-area / #tab-bar. * Multi-pane: builds .pane-grid inside .content-wrapper. Each pane gets its OWN * independent PaneTabManager instance with its own tabs, tab bar, and content area. * Drag & drop tabs between panes. * Persisted to localStorage. */ import { state } from './state.js'; import { api } from './auth.js'; import { escapeHtml, getFileIcon, safeCreateIcons } from './utils.js'; const PANE_MAX = 4; // ============================================================================= // PaneTabManager factory — one per pane // ============================================================================= function createPaneTabManager(paneId) { const DEFAULT_BAR = document.getElementById('tab-bar'); const DEFAULT_LIST = document.getElementById('tab-list'); const DEFAULT_AREA = document.getElementById('content-area'); const tm = { paneId, _tabs: [], _activeTabId: null, _previewTabId: null, _tabCache: {}, _dirtyTabs: new Set(), _tabBar: paneId === 0 ? DEFAULT_BAR : null, _tabList: paneId === 0 ? DEFAULT_LIST : null, _contentArea: paneId === 0 ? DEFAULT_AREA : null, // ------------------------------------------------------------------ // Init — bind to DOM // ------------------------------------------------------------------ init(tabBar, tabList, contentArea) { if (tabBar) this._tabBar = tabBar; if (tabList) this._tabList = tabList; if (contentArea) this._contentArea = contentArea; if (!this._tabBar) this._tabBar = document.getElementById('tab-bar-' + paneId); if (!this._tabList) this._tabList = document.getElementById('tab-list-' + paneId); if (!this._contentArea) this._contentArea = document.getElementById('content-area-' + paneId); }, getContentArea() { return this._contentArea || DEFAULT_AREA; }, getTabBar() { return this._tabBar; }, getTabList() { return this._tabList; }, // ------------------------------------------------------------------ // Open / Activate / Close // ------------------------------------------------------------------ async openPreview(vault, path) { const tabId = `${vault}::${path}`; const existing = this._tabs.find(t => t.id === tabId && !t.preview); if (existing) { this.activate(tabId); return; } if (this._previewTabId && this._previewTabId !== tabId) this.close(this._previewTabId); const previewExisting = this._tabs.find(t => t.id === tabId && t.preview); if (previewExisting) { this.activate(tabId); return; } const name = path.split('/').pop().replace(/\.md$/i, ''); const icon = getFileIcon(name + '.md'); this._tabs.push({ id: tabId, vault, path, name, icon, preview: true }); this._tabCache[tabId] = { vault, path, title: name, data: null, rawSource: null, sourceView: false, scrollTop: 0, icon }; this._previewTabId = tabId; this._renderTabs(); this.activate(tabId); }, async openPersistent(vault, path) { const tabId = `${vault}::${path}`; const previewTab = this._tabs.find(t => t.id === tabId && t.preview); if (previewTab) { previewTab.preview = false; if (this._previewTabId === tabId) this._previewTabId = null; this._renderTabs(); this.activate(tabId); return; } const existing = this._tabs.find(t => t.id === tabId && !t.preview); if (existing) { this.activate(tabId); return; } this.open(vault, path); }, async open(vault, path) { const tabId = `${vault}::${path}`; const existing = this._tabs.find(t => t.id === tabId); if (existing) { if (existing.preview) { existing.preview = false; if (this._previewTabId === tabId) this._previewTabId = null; this._renderTabs(); } this.activate(tabId); return; } const name = path.split('/').pop().replace(/\.md$/i, ''); const icon = getFileIcon(name + '.md'); this._tabs.push({ id: tabId, vault, path, name, icon }); this._tabCache[tabId] = { vault, path, title: name, data: null, rawSource: null, sourceView: false, scrollTop: 0, icon }; this._renderTabs(); this.activate(tabId); }, async activate(tabId) { if (this._activeTabId === tabId && this._tabs.length > 0) return; if (this._activeTabId && this._tabCache[this._activeTabId]) this._saveCurrentTabState(); this._activeTabId = tabId; this._renderTabs(); const cache = this._tabCache[tabId]; if (!cache) return; state.currentVault = cache.vault; state.currentPath = cache.path; // syncActiveFileTreeItem is imported below to avoid circular deps if (typeof syncActiveFileTreeItem !== 'undefined') syncActiveFileTreeItem(cache.vault, cache.path); const area = this._contentArea || DEFAULT_AREA; if (cache.data) { this._restoreTabContent(cache, area); } else { area.innerHTML = '
Chargement...
'; try { const data = await api(`/api/file/${encodeURIComponent(cache.vault)}?path=${encodeURIComponent(cache.path)}`); cache.data = data; cache.title = data.title; this._renderFileInPane(cache.data, area); if (cache.sourceView) await this._toggleSourceView(cache, area); if (cache.scrollTop) area.scrollTop = cache.scrollTop; } catch (err) { area.innerHTML = `
Erreur: ${escapeHtml(err.message)}
`; } } if (history.pushState) history.pushState(null, '', `#/file/${encodeURIComponent(cache.vault)}/${encodeURIComponent(cache.path)}`); const db = document.getElementById('dashboard-home'); if (db) db.style.display = 'none'; }, close(tabId) { const idx = this._tabs.findIndex(t => t.id === tabId); if (idx === -1) return; this._tabs.splice(idx, 1); delete this._tabCache[tabId]; this._dirtyTabs.delete(tabId); if (this._tabs.length === 0) { this._activeTabId = null; // If this pane has no more tabs and isn't the last pane, close it if (window.PaneManager && window.PaneManager.isSplit() && window.PaneManager.panes.length > 1) { this._renderTabs(); window.PaneManager.closePane(this.paneId); return; } this._showDashboard(); if (this._tabBar) this._tabBar.hidden = true; } else if (this._activeTabId === tabId) { const newIdx = Math.min(idx, this._tabs.length - 1); this.activate(this._tabs[newIdx].id); } this._renderTabs(); }, closeAll() { this._tabs = []; this._tabCache = {}; this._dirtyTabs.clear(); this._activeTabId = null; this._showDashboard(); if (this._tabBar) this._tabBar.hidden = true; }, closeRight(tabId) { const idx = this._tabs.findIndex(t => t.id === tabId); if (idx === -1) return; const toClose = this._tabs.slice(idx + 1); for (const tab of toClose) { delete this._tabCache[tab.id]; this._dirtyTabs.delete(tab.id); } this._tabs = this._tabs.slice(0, idx + 1); if (!this._tabs.find(t => t.id === this._activeTabId)) this.activate(tabId); this._renderTabs(); }, closeOthers(tabId) { const tab = this._tabs.find(t => t.id === tabId); if (!tab) return; for (const t of this._tabs) { if (t.id !== tabId) { delete this._tabCache[t.id]; this._dirtyTabs.delete(t.id); } } this._tabs = [tab]; this.activate(tabId); this._renderTabs(); }, moveTab(fromIdx, toIdx) { if (fromIdx === toIdx || fromIdx < 0 || toIdx < 0) return; const tab = this._tabs.splice(fromIdx, 1)[0]; this._tabs.splice(toIdx, 0, tab); this._renderTabs(); }, // ------------------------------------------------------------------ // Transfer a tab to another pane's TabManager // ------------------------------------------------------------------ transferTabTo(tabId, targetTm) { const idx = this._tabs.findIndex(t => t.id === tabId); if (idx === -1) return false; const tab = this._tabs[idx]; const cache = this._tabCache[tabId]; // Remove from source this._tabs.splice(idx, 1); if (this._previewTabId === tabId) this._previewTabId = null; delete this._tabCache[tabId]; this._dirtyTabs.delete(tabId); // Add to target targetTm._tabs.push({ ...tab, preview: false }); targetTm._tabCache[tabId] = cache || { vault: tab.vault, path: tab.path, title: tab.name, data: null, rawSource: null, sourceView: false, scrollTop: 0, icon: tab.icon }; // If source is now empty and not the last pane, close it if (this._tabs.length === 0) { this._activeTabId = null; if (window.PaneManager && window.PaneManager.isSplit() && window.PaneManager.panes.length > 1) { this._renderTabs(); window.PaneManager.closePane(this.paneId); return true; } this._showDashboard(); if (this._tabBar) this._tabBar.hidden = true; } else if (this._activeTabId === tabId) { const newIdx = Math.min(idx, this._tabs.length - 1); this.activate(this._tabs[newIdx].id); } this._renderTabs(); return true; }, // ------------------------------------------------------------------ // Internal rendering // ------------------------------------------------------------------ _renderFileInPane(data, area) { // Set temporary override so getContentArea() returns this pane's area const prev = window._activePaneContentArea; window._activePaneContentArea = area; // Use dynamic import to call viewer.js renderFile in the right context import('./viewer.js').then(m => { m.renderFile(data); window._activePaneContentArea = prev; }).catch(() => { window._activePaneContentArea = prev; }); }, _saveCurrentTabState() { const cache = this._tabCache[this._activeTabId]; if (!cache) return; const area = this._contentArea || DEFAULT_AREA; const rendered = document.getElementById('file-rendered-content'); cache.scrollTop = area ? area.scrollTop : 0; cache.sourceView = rendered ? rendered.style.display === 'none' : false; }, _restoreTabContent(cache, area) { this._renderFileInPane(cache.data, area); if (cache.sourceView) this._restoreSourceView(cache, area); if (cache.scrollTop && area) area.scrollTop = cache.scrollTop; }, async _toggleSourceView(cache, area) { const rendered = document.getElementById('file-rendered-content'); const raw = document.getElementById('file-raw-content'); if (!rendered || !raw) return; if (!cache.rawSource) { const rawData = await api(`/api/file/${encodeURIComponent(cache.vault)}/raw?path=${encodeURIComponent(cache.path)}`); cache.rawSource = rawData.raw; } raw.textContent = cache.rawSource; rendered.style.display = 'none'; raw.style.display = 'block'; }, _restoreSourceView(cache, area) { requestAnimationFrame(() => { const rendered = document.getElementById('file-rendered-content'); const raw = document.getElementById('file-raw-content'); if (rendered && raw && cache.rawSource) { raw.textContent = cache.rawSource; rendered.style.display = 'none'; raw.style.display = 'block'; } }); }, _showDashboard() { const area = this._contentArea || DEFAULT_AREA; let dashboard = document.getElementById('dashboard-home'); if (!dashboard) { // Dashboard was destroyed — rebuild area.innerHTML = ''; if (typeof showWelcome !== 'undefined') showWelcome(); return; } area.innerHTML = ''; dashboard.style.display = ''; area.appendChild(dashboard); if (typeof DashboardStatsWidget !== 'undefined') DashboardStatsWidget.load(); if (typeof DashboardConflictsWidget !== 'undefined') DashboardConflictsWidget.load(); if (history.pushState) history.pushState(null, '', '#'); }, // ------------------------------------------------------------------ // Render tab bar // ------------------------------------------------------------------ _renderTabs() { if (!this._tabList) return; this._tabList.innerHTML = ''; if (this._tabs.length === 0) { if (this._tabBar) this._tabBar.hidden = true; return; } if (this._tabBar) this._tabBar.hidden = false; this._tabs.forEach((tab, idx) => { const el = document.createElement('div'); el.className = 'tab-item' + (tab.id === this._activeTabId ? ' active' : '') + (tab.preview ? ' preview' : ''); el.draggable = true; el.dataset.tabId = tab.id; el.dataset.index = idx; el.dataset.paneId = String(this.paneId); const iconEl = document.createElement('i'); iconEl.setAttribute('data-lucide', tab.icon); iconEl.className = 'tab-icon'; iconEl.style.width = '14px'; iconEl.style.height = '14px'; el.appendChild(iconEl); const nameEl = document.createElement('span'); nameEl.className = 'tab-name'; nameEl.textContent = tab.name; nameEl.title = `${tab.vault}/${tab.path}`; el.appendChild(nameEl); const closeEl = document.createElement('span'); closeEl.className = 'tab-close'; closeEl.innerHTML = ''; closeEl.addEventListener('click', (e) => { e.stopPropagation(); this.close(tab.id); }); el.appendChild(closeEl); el.addEventListener('click', () => { // Set this pane as active in PaneManager if (window.PaneManager) window.PaneManager.setActivePane(this.paneId); this.activate(tab.id); }); el.addEventListener('dblclick', (e) => { e.preventDefault(); this.close(tab.id); }); el.addEventListener('mousedown', (e) => { if (e.button === 1) { e.preventDefault(); this.close(tab.id); } }); el.addEventListener('contextmenu', (e) => { e.preventDefault(); this._showTabContextMenu(e.clientX, e.clientY, tab.id); }); // Drag within same pane el.addEventListener('dragstart', (e) => { e.dataTransfer.setData('application/obsigate-tab', JSON.stringify({ paneId: this.paneId, tabId: tab.id, index: idx })); el.classList.add('dragging'); }); el.addEventListener('dragend', () => { el.classList.remove('dragging'); document.querySelectorAll('.tab-drop-indicator').forEach(d => d.remove()); }); el.addEventListener('dragover', (e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; const rect = el.getBoundingClientRect(); const mid = rect.left + rect.width / 2; document.querySelectorAll('.tab-drop-indicator').forEach(d => d.remove()); const indicator = document.createElement('div'); indicator.className = 'tab-drop-indicator'; if (e.clientX < mid) el.before(indicator); else el.after(indicator); }); el.addEventListener('drop', (e) => { e.preventDefault(); document.querySelectorAll('.tab-drop-indicator').forEach(d => d.remove()); try { const raw = e.dataTransfer.getData('application/obsigate-tab'); if (!raw) return; const dragData = JSON.parse(raw); // Same pane — reorder if (dragData.paneId === this.paneId) { const rect = el.getBoundingClientRect(); const mid = rect.left + rect.width / 2; const toIdx = e.clientX < mid ? idx : idx + 1; if (dragData.index !== toIdx && dragData.index !== toIdx - 1) this.moveTab(dragData.index, toIdx); } else { // Cross-pane transfer if (window.PaneManager) { window.PaneManager.transferTab(dragData.paneId, dragData.tabId, this.paneId); } } } catch (e2) { /* ignore */ } }); this._tabList.appendChild(el); }); safeCreateIcons(); }, _showTabContextMenu(x, y, tabId) { const existing = document.getElementById('tab-context-menu'); if (existing) existing.remove(); const menu = document.createElement('div'); menu.id = 'tab-context-menu'; menu.className = 'context-menu active'; menu.style.left = x + 'px'; menu.style.top = y + 'px'; const isSplit = window.PaneManager && window.PaneManager.isSplit(); let closePaneHtml = ''; if (isSplit) { closePaneHtml = `
Fermer le panneau
`; } menu.innerHTML = `
Diviser à droite
Diviser en bas
Fermer
Fermer les autres
Fermer à droite
Fermer tout
${closePaneHtml} `; document.body.appendChild(menu); safeCreateIcons(); menu.addEventListener('click', (e) => { const action = e.target.closest('.context-menu-item')?.dataset.action; if (action === 'splitRight') { if (window.PaneManager) { window.PaneManager.splitRight(); menu.remove(); return; } } else if (action === 'splitDown') { if (window.PaneManager) { window.PaneManager.splitDown(); menu.remove(); return; } } else if (action === 'closePane') { if (window.PaneManager) { window.PaneManager.closeActivePane(); menu.remove(); return; } } else if (action === 'close') this.close(tabId); else if (action === 'closeOthers') this.closeOthers(tabId); else if (action === 'closeRight') this.closeRight(tabId); else if (action === 'closeAll') this.closeAll(); menu.remove(); }); const closeMenu = () => menu.remove(); document.addEventListener('click', closeMenu, { once: true }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') menu.remove(); }, { once: true }); }, }; return tm; } // ============================================================================= // PaneManager — orchestrates panes and their TabManagers // ============================================================================= const PaneManager = { panes: [], tabManagers: {}, // { paneId: PaneTabManager instance } activePaneId: 0, layout: 'horizontal', _paneCounter: 1, init() { // Create default pane-0 TabManager (will bind to existing DOM) this.tabManagers[0] = createPaneTabManager(0); this.tabManagers[0].init(); const saved = localStorage.getItem('obsigate-panes'); if (saved) { try { const data = JSON.parse(saved); if (data.panes && data.panes.length >= 2) { this.layout = data.layout || 'horizontal'; this._paneCounter = data.panes.length; this._buildGrid(data.panes.length, data.panes); this.activePaneId = 0; this._highlightActive(); this._applyResize(); // Restore per-pane state data.panes.forEach((p, i) => { if (this.tabManagers[i] && p.tabs) { const tm = this.tabManagers[i]; tm._tabs = p.tabs.map(t => ({ ...t })); tm._activeTabId = p.activeTab; tm._previewTabId = p.previewTab || null; // Rebuild tab cache from tabs p.tabs.forEach(t => { if (!tm._tabCache[t.id]) { tm._tabCache[t.id] = { vault: t.vault, path: t.path, title: t.name, data: null, rawSource: null, sourceView: false, scrollTop: 0, icon: t.icon }; } }); tm._renderTabs(); if (tm._activeTabId) tm.activate(tm._activeTabId); } }); } } catch (e) { /* ignore corrupt state */ } } }, setActivePane(paneId) { if (this.activePaneId === paneId) return; this.activePaneId = paneId; this._highlightActive(); // Sync global state from this pane's active tab const tm = this.tabManagers[paneId]; if (tm && tm._activeTabId) { const cache = tm._tabCache[tm._activeTabId]; if (cache) { state.currentVault = cache.vault; state.currentPath = cache.path; } } this._persist(); }, getActiveTabManager() { return this.tabManagers[this.activePaneId] || this.tabManagers[0]; }, getTabManager(paneId) { return this.tabManagers[paneId] || this.tabManagers[0]; }, getActiveContentArea() { const tm = this.getActiveTabManager(); return tm ? tm.getContentArea() : document.getElementById('content-area'); }, transferTab(fromPaneId, tabId, toPaneId) { const fromTm = this.tabManagers[fromPaneId]; const toTm = this.tabManagers[toPaneId]; if (!fromTm || !toTm) return false; const ok = fromTm.transferTabTo(tabId, toTm); if (ok) { toTm._renderTabs(); toTm.activate(tabId); this._persist(); } return ok; }, isSplit() { return this.panes.length >= 2; }, // ------------------------------------------------------------------ // Split actions // ------------------------------------------------------------------ splitRight() { if (this.panes.length >= PANE_MAX) return; this.layout = 'horizontal'; if (this.panes.length === 0) this._buildGrid(2); else this._addPane(); this._persist(); }, splitDown() { if (this.panes.length >= PANE_MAX) return; this.layout = 'vertical'; if (this.panes.length === 0) this._buildGrid(2); else this._addPane(); this._persist(); }, // ------------------------------------------------------------------ // Close pane // ------------------------------------------------------------------ closePane(paneId) { if (this.panes.length <= 1) return; const idx = this.panes.findIndex(p => p.id === paneId); if (idx < 0) return; // Clean up tab manager const tm = this.tabManagers[paneId]; if (tm) { tm._tabs = []; tm._tabCache = {}; tm._activeTabId = null; } delete this.tabManagers[paneId]; this.panes[idx].element.remove(); const handles = document.querySelectorAll('.pane-resize-handle'); if (handles[idx]) handles[idx].remove(); else if (handles[idx - 1]) handles[idx - 1].remove(); this.panes.splice(idx, 1); if (this.panes.length <= 1) { this._collapseToSingle(); } else { this.activePaneId = Math.min(this.activePaneId, this.panes.length - 1); this._highlightActive(); } this._persist(); }, closeActivePane() { if (this.panes.length <= 1) return; this.closePane(this.activePaneId); }, closeOtherPanes() { if (this.panes.length <= 1) return; const keepId = this.activePaneId; const toClose = this.panes.filter(p => p.id !== keepId); for (const p of toClose.reverse()) this.closePane(p.id); }, // ------------------------------------------------------------------ // Build / Destroy pane grid // ------------------------------------------------------------------ _buildGrid(n, savedPanes) { const wrapper = document.querySelector('.content-wrapper'); const origTabBar = document.getElementById('tab-bar'); const origContent = document.getElementById('content-area'); if (!wrapper || !origContent || n < 2) return; const dashboard = document.getElementById('dashboard-home'); const grid = document.createElement('div'); grid.className = 'pane-grid'; if (n === 2) { if (this.layout === 'horizontal') grid.style.gridTemplateColumns = '1fr 4px 1fr'; else grid.style.gridTemplateRows = '1fr 4px 1fr'; } else if (n === 3) { grid.style.gridTemplateColumns = '1fr 4px 1fr'; grid.style.gridTemplateRows = '1fr 4px 1fr'; } else if (n === 4) { grid.style.gridTemplateColumns = '1fr 4px 1fr'; grid.style.gridTemplateRows = '1fr 4px 1fr'; } wrapper.innerHTML = ''; wrapper.appendChild(grid); this.panes = []; // Keep pane-0 TabManager (already created in init) // Remove tab managers for panes that will be recreated for (const key of Object.keys(this.tabManagers)) { if (Number(key) >= n) delete this.tabManagers[key]; } for (let i = 0; i < Math.min(n, PANE_MAX); i++) { const pane = this._createPane(i, grid, i === 0 ? origTabBar : null, i === 0 ? origContent : null); if (dashboard && i === 0 && pane) { const ca = document.getElementById('content-area-0') || origContent; if (ca && !ca.querySelector('#dashboard-home')) { ca.innerHTML = ''; ca.appendChild(dashboard); dashboard.style.display = ''; } } this.panes.push(pane); // Create or update TabManager for this pane if (!this.tabManagers[i]) { this.tabManagers[i] = createPaneTabManager(i); } const tabBar = document.getElementById('tab-bar-' + i) || origTabBar; const tabList = document.getElementById('tab-list-' + i) || tabBar.querySelector('.tab-list'); const contentArea = document.getElementById('content-area-' + i) || origContent; this.tabManagers[i].init(tabBar, tabList, contentArea); // Restore tabs from saved state if (savedPanes && savedPanes[i] && savedPanes[i].tabs) { const tm = this.tabManagers[i]; tm._tabs = savedPanes[i].tabs.map(t => ({ ...t })); tm._activeTabId = savedPanes[i].activeTab; tm._previewTabId = savedPanes[i].previewTab || null; savedPanes[i].tabs.forEach(t => { if (!tm._tabCache[t.id]) tm._tabCache[t.id] = { vault: t.vault, path: t.path, title: t.name, data: null, rawSource: null, sourceView: false, scrollTop: 0, icon: t.icon }; }); setTimeout(() => { tm._renderTabs(); if (tm._activeTabId) tm.activate(tm._activeTabId); }, 200); } if (i < n - 1) this._addResizeHandle(grid, i, i + 1); } this.activePaneId = 0; this._highlightActive(); }, _createPane(idx, grid, reuseTabBar, reuseContent) { const container = document.createElement('div'); container.className = 'pane-container'; container.dataset.pane = idx; if (reuseTabBar && reuseContent) { container.appendChild(reuseTabBar); container.appendChild(reuseContent); reuseContent.id = 'content-area-0'; reuseTabBar.id = 'tab-bar-0'; const tl = reuseTabBar.querySelector('.tab-list') || reuseTabBar.querySelector('#tab-list'); if (tl) tl.id = 'tab-list-0'; } else { const tabBar = document.createElement('div'); tabBar.className = 'tab-bar pane-tab-bar'; tabBar.id = 'tab-bar-' + idx; const tabList = document.createElement('div'); tabList.className = 'tab-list'; tabList.id = 'tab-list-' + idx; tabBar.appendChild(tabList); const content = document.createElement('main'); content.className = 'content-area pane-content'; content.id = 'content-area-' + idx; content.setAttribute('aria-label', 'Contenu panneau ' + (idx + 1)); container.appendChild(tabBar); container.appendChild(content); } container.addEventListener('click', (e) => { // Don't capture clicks on tab items (they have their own handler) if (e.target.closest('.tab-item')) return; this.setActivePane(idx); }); grid.appendChild(container); return { id: idx, element: container, width: null }; }, _addPane() { const grid = document.querySelector('.pane-grid'); if (!grid) return; const n = this.panes.length + 1; if (n === 3) { grid.style.gridTemplateColumns = '1fr 4px 1fr'; grid.style.gridTemplateRows = '1fr 4px 1fr'; } if (n === 4) { grid.style.gridTemplateColumns = '1fr 4px 1fr'; grid.style.gridTemplateRows = '1fr 4px 1fr'; } this._addResizeHandle(grid, n - 2, n - 1); const pane = this._createPane(n - 1, grid, null, null); this.panes.push(pane); // Create TabManager for new pane const tm = createPaneTabManager(n - 1); const tabBar = document.getElementById('tab-bar-' + (n - 1)); const tabList = document.getElementById('tab-list-' + (n - 1)); const contentArea = document.getElementById('content-area-' + (n - 1)); tm.init(tabBar, tabList, contentArea); this.tabManagers[n - 1] = tm; // Copy active tab from previous pane to new pane const prevTm = this.tabManagers[this.activePaneId]; if (prevTm && prevTm._activeTabId) { const prevTabId = prevTm._activeTabId; const prevCache = prevTm._tabCache[prevTabId]; if (prevCache) { tm.open(prevCache.vault, prevCache.path); } } this.activePaneId = n - 1; this._highlightActive(); this._persist(); }, _addResizeHandle(grid, leftIdx, rightIdx) { const handle = document.createElement('div'); handle.className = 'pane-resize-handle'; handle.style.cursor = this.layout === 'horizontal' ? 'col-resize' : 'row-resize'; handle.addEventListener('mousedown', (e) => this._onResizeStart(e, leftIdx, rightIdx)); handle.addEventListener('dblclick', () => this._resetResize(leftIdx, rightIdx)); grid.appendChild(handle); }, _collapseToSingle() { const grid = document.querySelector('.pane-grid'); if (!grid) return; const wrapper = grid.parentElement; const pane0 = this.panes[0]; wrapper.innerHTML = ''; let tabBar = null, content = null; if (pane0 && pane0.element) { tabBar = pane0.element.querySelector('.tab-bar'); content = pane0.element.querySelector('.content-area'); } if (!tabBar) tabBar = document.getElementById('tab-bar-0'); if (!content) content = document.getElementById('content-area-0'); if (tabBar) { tabBar.id = 'tab-bar'; wrapper.appendChild(tabBar); } if (content) { content.id = 'content-area'; wrapper.appendChild(content); } const tl = wrapper.querySelector('.tab-list'); if (tl) tl.id = 'tab-list'; // Re-init pane-0 TabManager to point to restored DOM if (this.tabManagers[0]) { this.tabManagers[0]._tabBar = tabBar; this.tabManagers[0]._tabList = tl; this.tabManagers[0]._contentArea = content; } // Clean up other tab managers for (const key of Object.keys(this.tabManagers)) { if (Number(key) !== 0) delete this.tabManagers[key]; } this.panes = []; this.activePaneId = 0; this._persist(); }, // ------------------------------------------------------------------ // Resize // ------------------------------------------------------------------ _onResizeStart(e, leftIdx, rightIdx) { e.preventDefault(); const grid = document.querySelector('.pane-grid'); const startX = e.clientX; const startY = e.clientY; const totalSize = this.layout === 'horizontal' ? grid.offsetWidth : grid.offsetHeight; const onMove = (ev) => { const delta = this.layout === 'horizontal' ? ev.clientX - startX : ev.clientY - startY; const pct = (delta / totalSize) * 100; const leftEl = this.panes[leftIdx].element; const rightEl = this.panes[rightIdx].element; const curLeft = parseFloat(leftEl.style.flexBasis || leftEl.style.width || '50'); const curRight = parseFloat(rightEl.style.flexBasis || rightEl.style.width || '50'); const newLeft = Math.max(20, Math.min(80, curLeft + pct)); const newRight = 100 - newLeft; leftEl.style.flexBasis = newLeft + '%'; rightEl.style.flexBasis = newRight + '%'; this.panes[leftIdx].width = newLeft + '%'; this.panes[rightIdx].width = newRight + '%'; }; const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); this._persist(); }; document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); }, _resetResize(leftIdx, rightIdx) { if (this.panes[leftIdx]) this.panes[leftIdx].element.style.flexBasis = '50%'; if (this.panes[rightIdx]) this.panes[rightIdx].element.style.flexBasis = '50%'; this._persist(); }, _applyResize() { this.panes.forEach(p => { if (p.width && p.element) p.element.style.flexBasis = p.width; }); }, _highlightActive() { this.panes.forEach((p, i) => { if (p.element) p.element.classList.toggle('pane-active', i === this.activePaneId); }); }, // ------------------------------------------------------------------ // Persistence — save per-pane tab state // ------------------------------------------------------------------ _persist() { if (this.panes.length <= 1) { localStorage.removeItem('obsigate-panes'); return; } const data = { layout: this.layout, panes: this.panes.map((p) => { const tm = this.tabManagers[p.id]; return { id: p.id, width: p.width, activeTab: tm ? tm._activeTabId : null, previewTab: tm ? tm._previewTabId : null, tabs: tm ? tm._tabs.map(t => ({ id: t.id, vault: t.vault, path: t.path, name: t.name, icon: t.icon, preview: t.preview })) : [], }; }), }; localStorage.setItem('obsigate-panes', JSON.stringify(data)); }, }; // ============================================================================= // Global helpers // ============================================================================= function getActiveTabManager() { if (window.PaneManager && window.PaneManager.isSplit()) return window.PaneManager.getActiveTabManager(); return window._singletonTabManager; } function getActiveContentArea() { if (window.PaneManager && window.PaneManager.isSplit()) return window.PaneManager.getActiveContentArea(); return document.getElementById('content-area'); } // ============================================================================= // Export + auto-init // ============================================================================= if (typeof window !== 'undefined') { window.PaneManager = PaneManager; window.getActiveTabManager = getActiveTabManager; window.getActiveContentArea = getActiveContentArea; document.addEventListener('DOMContentLoaded', () => { // Keyboard shortcuts document.addEventListener('keydown', (e) => { if (e.ctrlKey && e.altKey && !e.shiftKey && e.key === '\\') { e.preventDefault(); PaneManager.splitRight(); } if (e.ctrlKey && e.altKey && e.shiftKey && e.key === '\\') { e.preventDefault(); PaneManager.splitDown(); } if (e.ctrlKey && e.altKey && !e.shiftKey && (e.key === 'w' || e.key === 'W') && PaneManager.isSplit()) { e.preventDefault(); PaneManager.closeActivePane(); } }); // Init — wait for DOM + singleton TabManager to be ready setTimeout(() => { // Use window.TabManager (set by ui.js init) as the singleton fallback if (!window._singletonTabManager && window.TabManager) window._singletonTabManager = window.TabManager; PaneManager.init(); }, 150); }); } export { PaneManager, getActiveTabManager, getActiveContentArea, createPaneTabManager }; export default PaneManager;