diff --git a/demo/client.ts b/demo/client.ts index 8605be81..5885388e 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -19,6 +19,7 @@ if ('WebAssembly' in window) { import { Terminal, ITerminalOptions, type IDisposable, type ITheme } from '@xterm/xterm'; import { AttachAddon } from '@xterm/addon-attach'; +import { AddonsWindow } from './components/window/addonsWindow'; import { ControlBar } from './components/controlBar'; import { GpuWindow } from './components/window/gpuWindow'; import { OptionsWindow } from './components/window/optionsWindow'; @@ -61,6 +62,8 @@ let protocol; let socketURL; let socket; let pid; +let controlBar: ControlBar; +let addonsWindow: AddonsWindow; let gpuWindow: GpuWindow; let optionsWindow: OptionsWindow; let styleWindow: StyleWindow; @@ -120,11 +123,10 @@ const addons: { [T in AddonType]: IDemoAddon } = { }; let terminalContainer = document.getElementById('terminal-container'); -const actionElements = { - find: document.querySelector('#find') as HTMLInputElement, - findNext: document.querySelector('#find-next') as HTMLInputElement, - findPrevious: document.querySelector('#find-previous') as HTMLInputElement, - findResults: document.querySelector('#find-results') +let actionElements: { + findNext: HTMLInputElement; + findPrevious: HTMLInputElement; + findResults: HTMLElement; }; let paddingElement: HTMLInputElement; @@ -240,13 +242,18 @@ if (document.location.pathname === '/test') { window.WebLinksAddon = WebLinksAddon; window.WebglAddon = WebglAddon; } else { - const controlBar = new ControlBar(document.getElementById('sidebar'), document.querySelector('.banner-tabs'), [ - { id: 'addons', label: 'Addons' } - ]); + controlBar = new ControlBar(document.getElementById('sidebar'), document.querySelector('.banner-tabs'), []); + addonsWindow = new AddonsWindow(); + controlBar.registerWindow(addonsWindow); + actionElements = { + findNext: addonsWindow.findNextInput, + findPrevious: addonsWindow.findPreviousInput, + findResults: addonsWindow.findResultsSpan + }; + gpuWindow = new GpuWindow(); + controlBar.registerWindow(gpuWindow, { afterId: 'addons', hidden: true, smallTab: true }); optionsWindow = new OptionsWindow(updateTerminalSize, updateTerminalContainerBackground); controlBar.registerWindow(optionsWindow); - gpuWindow = new GpuWindow(); - controlBar.registerWindow(gpuWindow); styleWindow = new StyleWindow(); controlBar.registerWindow(styleWindow); paddingElement = styleWindow.paddingElement; @@ -254,6 +261,7 @@ if (document.location.pathname === '/test') { controlBar.registerWindow(testWindow); vtWindow = new VtWindow(); controlBar.registerWindow(vtWindow); + controlBar.activateDefaultTab(); createTerminal(); document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler); document.getElementById('create-new-window').addEventListener('click', createNewWindowButtonHandler); @@ -344,6 +352,7 @@ function createTerminal(): void { try { typedTerm.loadAddon(addons.webgl.instance); term.open(terminalContainer); + controlBar.setTabVisible('gpu', true); gpuWindow.setTextureAtlas(addons.webgl.instance.textureAtlas); addons.webgl.instance.onChangeTextureAtlas(e => gpuWindow.setTextureAtlas(e)); addons.webgl.instance.onAddTextureAtlasCanvas(e => gpuWindow.appendTextureAtlas(e)); @@ -472,6 +481,7 @@ function initAddons(term: Terminal): void { const fragment = document.createDocumentFragment(); function postInitWebgl(): void { + controlBar.setTabVisible('gpu', true); setTimeout(() => { gpuWindow.setTextureAtlas(addons.webgl.instance.textureAtlas); addons.webgl.instance.onChangeTextureAtlas(e => gpuWindow.setTextureAtlas(e)); @@ -479,6 +489,7 @@ function initAddons(term: Terminal): void { }, 500); } function preDisposeWebgl(): void { + controlBar.setTabVisible('gpu', false); if (addons.webgl.instance.textureAtlas) { addons.webgl.instance.textureAtlas.remove(); } @@ -594,7 +605,7 @@ function initAddons(term: Terminal): void { fragment.appendChild(wrapper); }); - const container = document.getElementById('addons-container'); + const container = addonsWindow.addonsContainer; container.innerHTML = ''; container.appendChild(fragment); } diff --git a/demo/components/controlBar.ts b/demo/components/controlBar.ts index ab55891e..b82d9a0d 100644 --- a/demo/components/controlBar.ts +++ b/demo/components/controlBar.ts @@ -146,14 +146,35 @@ export class ControlBar { } } - public registerWindow(window: IControlWindow): void { + public registerWindow(window: IControlWindow, options?: { afterId?: string; hidden?: boolean; smallTab?: boolean }): void { // 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)); - this._tabContainer.appendChild(button); + + // Apply small tab styling + if (options?.smallTab) { + button.style.fontSize = '0.85em'; + } + + // 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'); @@ -168,7 +189,42 @@ export class ControlBar { this._tabs.set(window.id, { button, content }); } + 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; + } + } + } } diff --git a/demo/components/window/addonsWindow.ts b/demo/components/window/addonsWindow.ts new file mode 100644 index 00000000..c94a6f03 --- /dev/null +++ b/demo/components/window/addonsWindow.ts @@ -0,0 +1,246 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import type { Terminal } from '@xterm/xterm'; +import type { IControlWindow } from '../controlBar'; + +export class AddonsWindow implements IControlWindow { + public readonly id = 'addons'; + public readonly label = 'Addons'; + + private _addonsContainer: HTMLElement; + private _findNextInput: HTMLInputElement; + private _findPreviousInput: HTMLInputElement; + private _findResultsSpan: HTMLElement; + private _regexCheckbox: HTMLInputElement; + private _caseSensitiveCheckbox: HTMLInputElement; + private _wholeWordCheckbox: HTMLInputElement; + private _highlightAllMatchesCheckbox: HTMLInputElement; + private _serializeOutputPre: HTMLPreElement; + private _htmlSerializeOutputPre: HTMLPreElement; + private _htmlSerializeOutputResult: HTMLElement; + private _writeToTerminalCheckbox: HTMLInputElement; + private _imageStorageLimitInput: HTMLInputElement; + private _imageShowPlaceholderCheckbox: HTMLInputElement; + private _imageOptionsTextarea: HTMLTextAreaElement; + + public build(container: HTMLElement): void { + // Heading + const heading = document.createElement('h3'); + heading.textContent = 'Addons'; + container.appendChild(heading); + + // Description + const description = document.createElement('p'); + description.textContent = 'Addons can be loaded and unloaded on a particular terminal to extend its functionality.'; + container.appendChild(description); + + // Addons container (checkboxes go here) + this._addonsContainer = document.createElement('div'); + this._addonsContainer.id = 'addons-container'; + container.appendChild(this._addonsContainer); + + // Addons Control section + const controlHeading = document.createElement('h3'); + controlHeading.textContent = 'Addons Control'; + container.appendChild(controlHeading); + + // SearchAddon section + this._buildSearchSection(container); + + // SerializeAddon section + this._buildSerializeSection(container); + + // ImageAddon section + this._buildImageSection(container); + } + + private _buildSearchSection(container: HTMLElement): void { + const h4 = document.createElement('h4'); + h4.textContent = 'SearchAddon'; + container.appendChild(h4); + + const wrapper = document.createElement('div'); + wrapper.style.display = 'flex'; + wrapper.style.flexDirection = 'column'; + + // Find next + const findNextLabel = document.createElement('label'); + findNextLabel.textContent = 'Find next '; + this._findNextInput = document.createElement('input'); + this._findNextInput.id = 'find-next'; + findNextLabel.appendChild(this._findNextInput); + wrapper.appendChild(findNextLabel); + + // Find previous + const findPrevLabel = document.createElement('label'); + findPrevLabel.textContent = 'Find previous '; + this._findPreviousInput = document.createElement('input'); + this._findPreviousInput.id = 'find-previous'; + findPrevLabel.appendChild(this._findPreviousInput); + wrapper.appendChild(findPrevLabel); + + // Results + const resultsDiv = document.createElement('div'); + resultsDiv.textContent = 'Results: '; + this._findResultsSpan = document.createElement('span'); + this._findResultsSpan.id = 'find-results'; + resultsDiv.appendChild(this._findResultsSpan); + wrapper.appendChild(resultsDiv); + + // Regex checkbox + const regexLabel = document.createElement('label'); + this._regexCheckbox = document.createElement('input'); + this._regexCheckbox.type = 'checkbox'; + this._regexCheckbox.id = 'regex'; + regexLabel.appendChild(this._regexCheckbox); + regexLabel.appendChild(document.createTextNode('Use regex')); + wrapper.appendChild(regexLabel); + + // Case sensitive checkbox + const caseLabel = document.createElement('label'); + this._caseSensitiveCheckbox = document.createElement('input'); + this._caseSensitiveCheckbox.type = 'checkbox'; + this._caseSensitiveCheckbox.id = 'case-sensitive'; + caseLabel.appendChild(this._caseSensitiveCheckbox); + caseLabel.appendChild(document.createTextNode('Case sensitive')); + wrapper.appendChild(caseLabel); + + // Whole word checkbox + const wholeWordLabel = document.createElement('label'); + this._wholeWordCheckbox = document.createElement('input'); + this._wholeWordCheckbox.type = 'checkbox'; + this._wholeWordCheckbox.id = 'whole-word'; + wholeWordLabel.appendChild(this._wholeWordCheckbox); + wholeWordLabel.appendChild(document.createTextNode('Whole word')); + wrapper.appendChild(wholeWordLabel); + + // Highlight all matches checkbox + const highlightLabel = document.createElement('label'); + this._highlightAllMatchesCheckbox = document.createElement('input'); + this._highlightAllMatchesCheckbox.type = 'checkbox'; + this._highlightAllMatchesCheckbox.id = 'highlight-all-matches'; + this._highlightAllMatchesCheckbox.checked = true; + highlightLabel.appendChild(this._highlightAllMatchesCheckbox); + highlightLabel.appendChild(document.createTextNode('Highlight All Matches')); + wrapper.appendChild(highlightLabel); + + container.appendChild(wrapper); + } + + private _buildSerializeSection(container: HTMLElement): void { + const h4 = document.createElement('h4'); + h4.textContent = 'SerializeAddon'; + container.appendChild(h4); + + const wrapper = document.createElement('div'); + + // Serialize button + const serializeBtn = document.createElement('button'); + serializeBtn.id = 'serialize'; + serializeBtn.textContent = 'Serialize the content of terminal'; + wrapper.appendChild(serializeBtn); + + // Write to terminal checkbox + const writeLabel = document.createElement('label'); + this._writeToTerminalCheckbox = document.createElement('input'); + this._writeToTerminalCheckbox.type = 'checkbox'; + this._writeToTerminalCheckbox.id = 'write-to-terminal'; + writeLabel.appendChild(this._writeToTerminalCheckbox); + writeLabel.appendChild(document.createTextNode('Write back to terminal')); + wrapper.appendChild(writeLabel); + + // Serialize output + const outputDiv = document.createElement('div'); + this._serializeOutputPre = document.createElement('pre'); + this._serializeOutputPre.id = 'serialize-output'; + outputDiv.appendChild(this._serializeOutputPre); + wrapper.appendChild(outputDiv); + + // HTML serialize button + const htmlSerializeBtn = document.createElement('button'); + htmlSerializeBtn.id = 'htmlserialize'; + htmlSerializeBtn.textContent = 'Serialize the content of terminal in HTML'; + wrapper.appendChild(htmlSerializeBtn); + + // HTML serialize result + this._htmlSerializeOutputResult = document.createElement('span'); + this._htmlSerializeOutputResult.id = 'htmlserialize-output-result'; + wrapper.appendChild(this._htmlSerializeOutputResult); + + // HTML serialize output + const htmlOutputDiv = document.createElement('div'); + this._htmlSerializeOutputPre = document.createElement('pre'); + this._htmlSerializeOutputPre.id = 'htmlserialize-output'; + htmlOutputDiv.appendChild(this._htmlSerializeOutputPre); + wrapper.appendChild(htmlOutputDiv); + + container.appendChild(wrapper); + } + + private _buildImageSection(container: HTMLElement): void { + const h4 = document.createElement('h4'); + h4.textContent = 'Image Addon'; + container.appendChild(h4); + + const details = document.createElement('details'); + const summary = document.createElement('summary'); + summary.textContent = 'image addon settings'; + details.appendChild(summary); + + const wrapper = document.createElement('div'); + + // Storage limit + const storageLimitLabel = document.createElement('label'); + storageLimitLabel.textContent = 'Storage Limit (in MB) '; + this._imageStorageLimitInput = document.createElement('input'); + this._imageStorageLimitInput.type = 'number'; + this._imageStorageLimitInput.id = 'image-storagelimit'; + storageLimitLabel.appendChild(this._imageStorageLimitInput); + wrapper.appendChild(storageLimitLabel); + wrapper.appendChild(document.createElement('br')); + + // Show placeholder + const placeholderLabel = document.createElement('label'); + placeholderLabel.textContent = 'Show Placeholder '; + this._imageShowPlaceholderCheckbox = document.createElement('input'); + this._imageShowPlaceholderCheckbox.type = 'checkbox'; + this._imageShowPlaceholderCheckbox.id = 'image-showplaceholder'; + placeholderLabel.appendChild(this._imageShowPlaceholderCheckbox); + wrapper.appendChild(placeholderLabel); + wrapper.appendChild(document.createElement('br')); + wrapper.appendChild(document.createElement('br')); + + // Ctor options + const optionsLabel = document.createElement('label'); + optionsLabel.appendChild(document.createTextNode('Ctor options (applied on addon relaunch)')); + optionsLabel.appendChild(document.createElement('br')); + this._imageOptionsTextarea = document.createElement('textarea'); + this._imageOptionsTextarea.id = 'image-options'; + this._imageOptionsTextarea.cols = 40; + this._imageOptionsTextarea.rows = 12; + optionsLabel.appendChild(this._imageOptionsTextarea); + wrapper.appendChild(optionsLabel); + + details.appendChild(wrapper); + container.appendChild(details); + } + + public get addonsContainer(): HTMLElement { + return this._addonsContainer; + } + + public get findNextInput(): HTMLInputElement { + return this._findNextInput; + } + + public get findPreviousInput(): HTMLInputElement { + return this._findPreviousInput; + } + + public get findResultsSpan(): HTMLElement { + return this._findResultsSpan; + } +} diff --git a/demo/index.html b/demo/index.html index 389b85e6..72556fc0 100644 --- a/demo/index.html +++ b/demo/index.html @@ -21,45 +21,6 @@