import { Editor } from '../../editor/Editor.js'; import { ContextMenuDirectory, ContextMenuEntry, ContextMenuSeparator } from '../context-menu/context-menu.js'; interface TabEntry { id: string; label: string; panelType: string; element: HTMLElement; dirty: boolean; factory: () => HTMLElement; } export interface EditorPanel extends HTMLElement { addContextMenuEntries(dir: ContextMenuDirectory): void; } class TabContainer extends HTMLElement { tabs: TabEntry[] = []; activeId: string | null = null; uid = Math.random().toString(36).slice(2); connectedCallback(): void { if (!this.id) this.id = 'tc-' + this.uid; this.classList.add('tab-container'); this.innerHTML = `
`; this.querySelector('.tc-menu')!.addEventListener('click', (e: Event) => { this.openMenu(e as MouseEvent); }); this.addEventListener('dragover', (e: DragEvent) => { if (e.dataTransfer?.types.includes('application/editor-tab')) { e.preventDefault(); this.classList.add('tc-drop-target'); } }); this.addEventListener('dragleave', () => this.classList.remove('tc-drop-target')); this.addEventListener('drop', (e: DragEvent) => { e.preventDefault(); this.classList.remove('tc-drop-target'); const raw = e.dataTransfer?.getData('application/editor-tab'); if (!raw) return; const { tabId, sourceContainerId } = JSON.parse(raw) as { tabId: string; sourceContainerId: string }; if (sourceContainerId === this.id) return; const source = document.getElementById(sourceContainerId) as TabContainer | null; if (!source) return; const tab = source.extractTab(tabId); if (tab) this.receiveTab(tab); }); this.addEventListener( 'panel:label-change', ( e: Event ) => { const panel = e.target as HTMLElement; const tab = this.tabs.find( t => t.element === panel ); if ( tab ) { tab.label = ( e as CustomEvent ).detail.label; this.renderBar(); } } ); Editor.get().onDocumentDirty.addListener( ( e ) => { const tab = this.tabs.find( t => ( t.element as any ).currentPath === e.path ); if ( tab ) { tab.dirty = true; this.renderBar(); } } ); Editor.get().onDocumentSaved.addListener( ( e ) => { const tab = this.tabs.find( t => ( t.element as any ).currentPath === e.path ); if ( tab ) { tab.dirty = false; this.renderBar(); } } ); } openMenu(e: MouseEvent): void { const btn = e.currentTarget as HTMLElement; const rect = btn.getBoundingClientRect(); const root = new ContextMenuDirectory(null); const addDir = new ContextMenuDirectory(root, 'Add'); const panelTypes = [ { label: 'HTML Editor', panelType: 'html-editor', tag: 'html-editor-panel' }, { label: 'Code Editor', panelType: 'code-panel', tag: 'code-panel' }, { label: 'File Tree', panelType: 'file-tree', tag: 'file-tree-panel' }, { label: 'Rojo Chat', panelType: 'rojo-chat', tag: 'rojo-chat-panel' }, { label: 'Rojo Settings', panelType: 'rojo-settings', tag: 'rojo-settings-panel' }, ]; for (const pt of panelTypes) { addDir.add(new ContextMenuEntry(addDir, pt.label, () => { this.dispatchEvent(new CustomEvent('tab-container:add-panel', { bubbles: true, detail: { containerId: this.id, panelType: pt.panelType, tag: pt.tag, label: pt.label }, })); })); } root.add(addDir); root.add(new ContextMenuEntry(root, 'Duplicate', () => this.duplicateActive())); root.add(new ContextMenuEntry(root, 'Split', () => { this.dispatchEvent(new CustomEvent('tab-container:split', { bubbles: true, detail: { containerId: this.id } })); })); const activeTab = this.tabs.find(t => t.id === this.activeId); if (activeTab) { const panel = activeTab.element as EditorPanel; if (typeof panel.addContextMenuEntries === 'function') { root.add(new ContextMenuSeparator(root)); panel.addContextMenuEntries(root); } } root.add(new ContextMenuSeparator(root)); root.add(new ContextMenuEntry(root, 'Close', () => this.closeActive())); root.show(rect.left, rect.bottom); } duplicateActive(): void { if ( ! this.activeId ) { return; } const tab = this.tabs.find( t => t.id === this.activeId ); if ( ! tab ) { return; } const newId = tab.id + '-' + Math.random().toString( 36 ).slice( 2 ); this.addTab( { id: newId, label: tab.label, panelType: tab.panelType }, tab.factory ); } closeActive(): void { if ( ! this.activeId ) { return; } this.extractTab( this.activeId ); } addTab(config: { id: string; label: string; panelType: string }, factory: () => HTMLElement): void { const element = factory(); element.style.display = 'none'; this.querySelector('.tc-content')!.appendChild(element); this.tabs.push({ ...config, element, dirty: false, factory }); this.activateTab(config.id); } extractTab(tabId: string): TabEntry | null { const idx = this.tabs.findIndex(t => t.id === tabId); if (idx === -1) return null; const [tab] = this.tabs.splice(idx, 1); tab.element.remove(); if (this.activeId === tabId) { this.activeId = this.tabs[0]?.id ?? null; } this.renderBar(); this.showActive(); return tab; } receiveTab(tab: TabEntry): void { tab.element.style.display = 'none'; this.querySelector('.tc-content')!.appendChild(tab.element); this.tabs.push(tab); this.activateTab(tab.id); } activateTab(tabId: string): void { this.activeId = tabId; this.renderBar(); this.showActive(); } renderBar(): void { const bar = this.querySelector('.tc-tabs')!; bar.innerHTML = this.tabs.map(t => `
${t.dirty ? '' : ''}${t.label}
`).join(''); bar.querySelectorAll('.tc-tab').forEach(el => { el.addEventListener('click', () => this.activateTab((el as HTMLElement).dataset.tabId!)); el.addEventListener('dragstart', (e: Event) => { const de = e as DragEvent; const tabId = (el as HTMLElement).dataset.tabId!; de.dataTransfer?.setData('application/editor-tab', JSON.stringify({ tabId, sourceContainerId: this.id })); de.dataTransfer!.effectAllowed = 'move'; }); }); } showActive(): void { this.tabs.forEach(t => { t.element.style.display = t.id === this.activeId ? '' : 'none'; }); } } customElements.define('tab-container', TabContainer);