pdf-tei-editor / app /src /plugins /tools.js
cmboulanger's picture
disaster-recovery deploy of pdf-tei-editor
6a49f21 verified
Raw
History Blame Contribute Delete
6.64 kB
/**
* Tools Plugin
*
* Provides the "Tools" button group in the toolbar. The group contains:
* - A native split button whose menu is populated at runtime by frontend plugins
* via `addMenuItems()` / `clearMenuItems()`.
* - Any additional split buttons or controls added by other plugins via
* `addButton()` / `removeButton()` (e.g. the backend-plugins plugin).
*
* The button group is hidden when all its children are hidden, and visible
* as soon as any child becomes visible.
*
* Items added to the native menu can optionally be grouped under a named category
* label. The label is created automatically the first time a category is used, and
* is hidden automatically via MutationObserver when all items in that category are
* hidden by the calling plugin.
*/
/**
* @import { PluginContext } from '../modules/plugin-context.js'
* @import { ApplicationState } from '../state.js'
* @import { toolsGroupPart } from '../templates/tools-button.types.js'
*/
import { Plugin } from '../modules/plugin-base.js'
import ep from '../extension-points.js'
import { registerTemplate, createSingleFromTemplate } from '../modules/ui-system.js'
// Register template
await registerTemplate('tools-button', 'tools-button.html')
class ToolsPlugin extends Plugin {
/** @param {PluginContext} context */
constructor(context) {
super(context, { name: 'tools', deps: ['logger'] })
}
static extensionPoints = [ep.toolbar.contentItems];
/**
* Extension point handler for `ep.toolbar.contentItems`.
* Called by ToolbarPlugin during start() to collect this plugin's toolbar contribution.
* Creates the tools button and wires the scoped UI on first call.
* @returns {Array<{element: HTMLElement, priority: number, position: string}>}
*/
[ep.toolbar.contentItems]() {
const buttonElement = createSingleFromTemplate('tools-button')
this.#ui = this.createUi(buttonElement)
return [{ element: buttonElement, priority: 0, position: "right" }]
}
/** @type {HTMLElement & toolsGroupPart} */
#ui = null
/** @type {HTMLElement[]} */
#uncategorizedItems = []
/**
* @type {Map<string, {label: HTMLElement, divider: HTMLElement|null, items: HTMLElement[]}>}
*/
#categoryGroups = new Map()
/** MutationObserver that hides/shows category labels when items are toggled. */
#observer = new MutationObserver(() => {
this.#updateAllCategoryLabels()
this.#updateNativeDropdown()
this.#updateGroupVisibility()
})
/** @returns {Element} The native frontend tools menu element */
get menu() {
return this.#ui.toolsDropdown.toolsMenu
}
/**
* @param {ApplicationState} _state
*/
async install(_state) {
await super.install(_state)
const logger = this.getDependency('logger')
logger.debug(`Installing plugin "tools"`)
}
async start() {
this.getDependency('logger').debug(`Starting plugin "tools"`)
}
/**
* Add menu items to the native frontend dropdown.
* @param {HTMLElement[]} elements - Items to add
* @param {string} [category] - Optional category name. A formatted label is inserted
* automatically and hidden via MutationObserver when all its items are hidden.
*/
addMenuItems(elements, category) {
const menu = this.#ui.toolsDropdown.toolsMenu
if (category) {
let group = this.#categoryGroups.get(category)
if (!group) {
let divider = null
if (menu.children.length > 0) {
divider = document.createElement('sl-divider')
menu.appendChild(divider)
}
const label = document.createElement('small')
label.textContent = this.#formatCategoryName(category)
menu.appendChild(label)
group = { label, divider, items: [] }
this.#categoryGroups.set(category, group)
}
elements.forEach(el => {
menu.appendChild(el)
group.items.push(el)
this.#observer.observe(el, { attributes: true, attributeFilter: ['style'] })
})
} else {
elements.forEach(el => {
menu.appendChild(el)
this.#uncategorizedItems.push(el)
this.#observer.observe(el, { attributes: true, attributeFilter: ['style'] })
})
}
this.#updateAllCategoryLabels()
this.#updateNativeDropdown()
}
/**
* Clear all items (and category labels) from the native frontend dropdown.
*/
clearMenuItems() {
this.#observer.disconnect()
const menu = this.#ui.toolsDropdown.toolsMenu
menu.innerHTML = ''
this.#uncategorizedItems.length = 0
this.#categoryGroups.clear()
this.#updateNativeDropdown()
}
/**
* Add a button or split-button element to the tools button group.
* @param {HTMLElement} element
*/
addButton(element) {
this.#ui.appendChild(element)
this.#updateGroupVisibility()
}
/**
* Remove a button or split-button element from the tools button group.
* @param {HTMLElement} element
*/
removeButton(element) {
if (element.parentNode === this.#ui) {
this.#ui.removeChild(element)
}
this.#updateGroupVisibility()
}
/**
* Show/hide the tools button group based on child visibility.
*/
updateVisibility() {
this.#updateGroupVisibility()
}
/** Show/hide category labels based on whether their items are all hidden. */
#updateAllCategoryLabels() {
this.#categoryGroups.forEach(group => {
const allHidden = group.items.every(el => el.style.display === 'none')
group.label.style.display = allHidden ? 'none' : ''
if (group.divider) group.divider.style.display = allHidden ? 'none' : ''
})
}
/** Show/hide the native dropdown based on whether any tracked menu item is visible. */
#updateNativeDropdown() {
const dropdown = this.#ui.toolsDropdown
const allItems = [...this.#uncategorizedItems, ...[...this.#categoryGroups.values()].flatMap(g => g.items)]
const hasVisibleItem = allItems.some(el => el.style.display !== 'none')
dropdown.style.display = hasVisibleItem ? '' : 'none'
this.#updateGroupVisibility()
}
/** Show the button group when at least one child is visible; hide it otherwise. */
#updateGroupVisibility() {
const hasVisibleChild = Array.from(this.#ui.children).some(el => el.style.display !== 'none')
this.#ui.style.display = hasVisibleChild ? 'inline-flex' : 'none'
}
/**
* Format a category key into a display label.
* @param {string} category
* @returns {string}
*/
#formatCategoryName(category) {
return category
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
}
}
export default ToolsPlugin