Files

236 lines
7.4 KiB
TypeScript

/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import type { Terminal } from '@xterm/xterm';
export interface ITabConfig {
id: string;
label: string;
}
export interface IControlWindow {
readonly id: string;
readonly label: string;
build(container: HTMLElement): void;
setTerminal(terminal: Terminal): void;
}
export class ControlBar {
private readonly _sidebar: HTMLElement;
private readonly _resizeHandleH: HTMLElement;
private readonly _resizeHandleV: HTMLElement;
private readonly _resizeHandleCorner: HTMLElement;
private _resizeMode: 'none' | 'horizontal' | 'vertical' | 'corner' = 'none';
private readonly _tabContainer: HTMLElement;
private readonly _tabs: Map<string, { button: HTMLButtonElement, content: HTMLElement }> = new Map();
private _activeTabId: string | null = null;
constructor(sidebar: HTMLElement, tabContainer: HTMLElement, tabs: ITabConfig[]) {
this._sidebar = sidebar;
this._tabContainer = tabContainer;
// Create resize handles
this._resizeHandleH = document.createElement('div');
this._resizeHandleH.id = 'sidebar-resize-handle-horizontal';
this._resizeHandleV = document.createElement('div');
this._resizeHandleV.id = 'sidebar-resize-handle-vertical';
this._resizeHandleCorner = document.createElement('div');
this._resizeHandleCorner.id = 'sidebar-resize-handle-corner';
// Insert handles at the beginning of the sidebar
this._sidebar.prepend(this._resizeHandleCorner);
this._sidebar.prepend(this._resizeHandleV);
this._sidebar.prepend(this._resizeHandleH);
this._initResizeListeners();
this._initTabs(tabs);
}
private _initResizeListeners(): void {
this._resizeHandleH.addEventListener('mousedown', (e: MouseEvent) => this._startResize('horizontal', e));
this._resizeHandleV.addEventListener('mousedown', (e: MouseEvent) => this._startResize('vertical', e));
this._resizeHandleCorner.addEventListener('mousedown', (e: MouseEvent) => this._startResize('corner', e));
document.addEventListener('mousemove', (e: MouseEvent) => {
if (this._resizeMode === 'none') return;
if (this._resizeMode === 'horizontal' || this._resizeMode === 'corner') {
const newWidth = window.innerWidth - e.clientX - 10;
this._sidebar.style.width = `${Math.max(200, newWidth)}px`;
}
if (this._resizeMode === 'vertical' || this._resizeMode === 'corner') {
const rect = this._sidebar.getBoundingClientRect();
const newHeight = e.clientY - rect.top;
this._sidebar.style.height = `${Math.max(100, newHeight)}px`;
}
});
document.addEventListener('mouseup', () => {
this._resizeMode = 'none';
document.body.style.cursor = '';
document.body.style.userSelect = '';
});
}
private _startResize(mode: 'horizontal' | 'vertical' | 'corner', e: MouseEvent): void {
this._resizeMode = mode;
document.body.style.cursor = mode === 'horizontal' ? 'ew-resize' : mode === 'vertical' ? 'ns-resize' : 'nesw-resize';
document.body.style.userSelect = 'none';
e.preventDefault();
}
private _initTabs(tabs: ITabConfig[]): void {
// Clear existing buttons
this._tabContainer.innerHTML = '';
// Create tab buttons
for (const tab of tabs) {
const content = document.getElementById(tab.id);
if (!content) {
console.warn(`Tab content element not found: ${tab.id}`);
continue;
}
const button = document.createElement('button');
button.id = `${tab.id}button`;
button.className = 'tabLinks';
button.textContent = tab.label;
button.addEventListener('click', (e) => this._openSection(e, tab.id));
this._tabContainer.appendChild(button);
this._tabs.set(tab.id, { button, content });
// Hide content initially
content.style.display = 'none';
}
// Restore saved tab or default to first
const savedTab = localStorage.getItem('tab');
const tabId = savedTab && this._tabs.has(savedTab) ? savedTab : tabs[0]?.id;
if (tabId) {
this._activateTab(tabId);
}
}
private _openSection(event: MouseEvent, tabId: string): void {
const tab = this._tabs.get(tabId);
if (!tab) return;
// If clicking active tab, toggle sidebar visibility
if (tab.button.classList.contains('active')) {
this._sidebar.classList.toggle('sidebar-hidden');
return;
}
// Show sidebar if hidden
this._sidebar.classList.remove('sidebar-hidden');
this._activateTab(tabId);
}
private _activateTab(tabId: string): void {
// Deactivate all tabs
for (const [, { button, content }] of this._tabs) {
button.classList.remove('active');
content.style.display = 'none';
}
// Activate the selected tab
const tab = this._tabs.get(tabId);
if (tab) {
tab.button.classList.add('active');
tab.content.style.display = 'block';
this._activeTabId = tabId;
localStorage.setItem('tab', tabId);
}
}
public registerWindow<T extends IControlWindow>(window: T, options?: { afterId?: string, hidden?: boolean, italics?: boolean }): T {
// Create button
const button = document.createElement('button');
button.id = `${window.id}button`;
button.className = 'tabLinks';
button.textContent = window.label;
button.addEventListener('click', (e) => this._openSection(e, window.id));
// Apply small tab styling
if (options?.italics) {
button.style.fontStyle = 'italic';
}
// Insert after specified tab or append at end
if (options?.afterId) {
const afterTab = this._tabs.get(options.afterId);
if (afterTab?.button.nextSibling) {
this._tabContainer.insertBefore(button, afterTab.button.nextSibling);
} else {
this._tabContainer.appendChild(button);
}
} else {
this._tabContainer.appendChild(button);
}
// Hide if specified
if (options?.hidden) {
button.style.display = 'none';
}
// Create content container
const content = document.createElement('div');
content.id = window.id;
content.className = 'tabContent';
content.style.display = 'none';
this._sidebar.appendChild(content);
// Let the window build its content
window.build(content);
this._tabs.set(window.id, { button, content });
return window;
}
public setTabVisible(tabId: string, visible: boolean): void {
const tab = this._tabs.get(tabId);
if (tab) {
tab.button.style.display = visible ? '' : 'none';
// If hiding the active tab, switch to first visible tab
if (!visible && this._activeTabId === tabId) {
for (const [id, t] of this._tabs) {
if (t.button.style.display !== 'none') {
this._activateTab(id);
break;
}
}
}
}
}
public get activeTabId(): string | null {
return this._activeTabId;
}
public activateDefaultTab(): void {
// Restore saved tab or default to first visible tab
const savedTab = localStorage.getItem('tab');
if (savedTab && this._tabs.has(savedTab)) {
const tab = this._tabs.get(savedTab);
if (tab && tab.button.style.display !== 'none') {
this._activateTab(savedTab);
return;
}
}
// Fall back to first visible tab
for (const [id, tab] of this._tabs) {
if (tab.button.style.display !== 'none') {
this._activateTab(id);
return;
}
}
}
}