/** * Toolbar Plugin * * Creates and manages the main toolbar layout. The toolbar menu is inserted at * the beginning during install() so dependent plugins can add menu items, then * moved to the end in start() after all plugins have run. Also wires up a * toolbar-wide z-index fix for sl-dropdown / sl-select elements. */ /** * @import { PluginContext } from '../modules/plugin-context.js' * @import { ApplicationState } from '../state.js' * @import { SlSelect, SlButton, SlButtonGroup, SlDropdown, UIPart } from '../ui.js' * @import { documentActionsPart } from '../templates/document-action-buttons.types.js' * @import { toolbarMenuPart } from '../templates/toolbar-menu-button.types.js' * @import { extractionActionsPart } from './extraction.js' * @import { fileDrawerTriggerPart } from './file-selection-drawer.js' * @import { toolsGroupPart } from './tools.js' */ /** * @import { PanelContribution } from '../modules/panels/base-panel.js' */ import { Plugin } from '../modules/plugin-base.js' import ui, { updateUi } from '../ui.js' import { registerTemplate, createSingleFromTemplate } from '../modules/ui-system.js' import ep from '../extension-points.js' import { resolveContributions } from '../modules/panels/base-panel.js' // Register template await registerTemplate('toolbar-menu-button', 'toolbar-menu-button.html') /** * The main toolbar navigation properties. * This documents the structure created by various plugins that add controls to the toolbar. * @typedef {object} toolbarPart * @property {SlSelect} collection - The selectbox for the collection filter (added by file-selection plugin) * @property {SlSelect} variant - The selectbox for the variant filter (added by file-selection plugin) * @property {SlSelect} pdf - The selectbox for the pdf document (added by file-selection plugin) * @property {SlSelect} xml - The selectbox for the xml document (added by file-selection plugin) * @property {SlSelect} diff - The selectbox for the xml-diff document (added by file-selection plugin) * @property {UIPart} documentActions - Document action buttons (added by document-actions plugin) * @property {UIPart} extractionActions - Extraction action buttons (added by extraction plugin) * @property {UIPart} toolsGroup - Tools dropdown button group (added by tools plugin) * @property {UIPart} toolbarMenu - Generic toolbar menu dropdown (added by toolbar plugin) * @property {UIPart} fileDrawerTrigger - File drawer trigger button (added by file-selection-drawer plugin) * @property {SlButton} searchBtn - Document search button (added by document-search extension) */ class ToolbarPlugin extends Plugin { /** @param {PluginContext} context */ constructor(context) { super(context, { name: 'toolbar', deps: ['logger'] }) } get #logger() { return this.getDependency('logger') } /** @type {SlDropdown & toolbarMenuPart} */ #menuUi = null /** @type {number} */ #openDropdownCount = 0 /** @type {WeakSet} */ #zIndexFixAttached = new WeakSet() /** * @param {ApplicationState} state * @returns {Promise} */ async install(state) { await super.install(state) this.#logger.debug(`Installing plugin "toolbar"`) // Create toolbar menu early so dependent plugins can add menu items during their install phase const menuElement = createSingleFromTemplate('toolbar-menu-button') this.#menuUi = this.createUi(menuElement) // Add to toolbar at beginning (will be moved to end in start phase) ui.toolbar.insertAdjacentElement('afterbegin', menuElement) updateUi() this.#logger.debug('Toolbar menu created at beginning of toolbar (will be moved to end in start phase)') } /** * Moves toolbar menu to end of toolbar after all plugins have installed, * collects toolbar content/menu contributions from all plugins via extension * points, then initialises the toolbar-wide z-index fix for all dropdowns. * @returns {Promise} */ async start() { this.#logger.debug(`Starting plugin "toolbar"`) // Collect toolbar content items from all plugins const contentResults = await this.context.invokePluginEndpoint( ep.toolbar.contentItems, [], { result: 'values', throws: false } ) /** @type {PanelContribution[]} */ const allContributions = contentResults .flatMap(items => Array.isArray(items) ? items : []) .filter(({ element }) => element instanceof HTMLElement && !element.isConnected) for (const { element, priority = 0 } of resolveContributions(allContributions)) { ui.toolbar.add(element, priority) } // Collect toolbar menu items from all plugins const menuResults = await this.context.invokePluginEndpoint( ep.toolbar.menuItems, [], { result: 'values', throws: false } ) for (const items of menuResults) { if (!Array.isArray(items)) continue for (const { element } of items) { if (element instanceof HTMLElement) { this.#menuUi.menu.appendChild(element) } } } // Move the toolbar menu to the end of the toolbar ui.toolbar.appendChild(this.#menuUi) updateUi() this.#initToolbarZIndexAutoFix() this.#logger.debug('Toolbar started: collected plugin contributions, menu moved to end') } /** * Enable or disable the toolbar menu button. * @param {boolean} disabled */ setMenuButtonDisabled(disabled) { this.#menuUi.menuBtn.disabled = disabled } /** * Add a widget to the toolbar. Use this for dynamic toolbar items * that are added or removed at runtime based on application state. * For static items that exist for the life of the app, use the * `toolbar.contentItems` extension point instead. * @param {HTMLElement} element - The widget element to add * @param {number} [priority=0] - Higher priority widgets stay visible longer * @param {'left'|'center'|'right'} [position='center'] - Where to insert the widget * @returns {string} The widget ID */ add(element, priority = 0, position = 'center') { return ui.toolbar.add(element, priority, position) } /** * Remove a widget from the toolbar by its ID. * @param {string} widgetId - The ID returned by add() * @returns {boolean} True if the widget was found and removed */ remove(widgetId) { return ui.toolbar.removeById(widgetId) } // --------------------------------------------------------------------------- // Toolbar-wide z-index fix // // sl-dropdown and sl-select elements inside tool-bar are affected by the // toolbar's z-index: 0 stacking context and can appear behind other content // when open. The fix elevates the toolbar's z-index via the `dropdown-open` // CSS class while any dropdown is open. // // Listening via event delegation on tool-bar does not work reliably because // sl-show / sl-hide events from sl-dropdown elements inside sl-button-group // shadow DOM slots do not bubble up consistently. Instead, listeners are // attached directly to each sl-dropdown / sl-select element. // // #initToolbarZIndexAutoFix() scans the toolbar once and then watches for // newly added elements with a MutationObserver, so every plugin's dropdowns // are covered automatically without any per-plugin wiring. // --------------------------------------------------------------------------- /** * Attach the z-index fix to all current and future sl-dropdown / sl-select * elements in the toolbar's light DOM tree. */ #initToolbarZIndexAutoFix = () => { const toolbar = ui.toolbar toolbar.querySelectorAll('sl-dropdown, sl-select').forEach(this.#attachZIndexFix) new MutationObserver(mutations => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType !== Node.ELEMENT_NODE) continue const el = /** @type {Element} */(node) if (el.matches('sl-dropdown, sl-select')) this.#attachZIndexFix(el) el.querySelectorAll('sl-dropdown, sl-select').forEach(this.#attachZIndexFix) } } }).observe(toolbar, { childList: true, subtree: true }) } /** * Attach sl-show / sl-hide listeners to one element. No-op if already attached. * @param {Element} element */ #attachZIndexFix = (element) => { if (this.#zIndexFixAttached.has(element)) return this.#zIndexFixAttached.add(element) element.addEventListener('sl-show', () => { this.#openDropdownCount++ element.closest('tool-bar')?.classList.add('dropdown-open') }) element.addEventListener('sl-hide', () => { this.#openDropdownCount-- if (this.#openDropdownCount <= 0) { this.#openDropdownCount = 0 element.closest('tool-bar')?.classList.remove('dropdown-open') } }) } } export default ToolbarPlugin