import { createAttachmentKey, type Attachment } from 'svelte/attachments'; import { get, writable, type Writable } from 'svelte/store'; export type ToolbarToggleState = 'on' | 'off'; /** * Defines options that may be passed when creating a toggleable toolbar item. */ export type ToolbarToggleOptions = { state?: Writable; }; /** toggleValue swaps a toolbar toggle state to on or off, based on current state. */ const toggleValue = (current: ToolbarToggleState): ToolbarToggleState => current === 'on' ? 'off' : 'on'; /** * A single toggleable toolbar item. Can coordinate with other toggles in a group. */ export class ToolbarToggle { private _state: Writable; private _parent: ToolbarGroup | undefined; constructor(opts: ToolbarToggleOptions = {}) { this._state = opts.state ?? writable('off'); } /** * Creates a new toggleable toolbar item with a parent group. * * @param parent The parent group to which this toggle belongs. * @param opts Options for the toggle. * @returns A new ToolbarToggle instance. */ static withParent(parent: ToolbarGroup, opts: ToolbarToggleOptions = {}) { const toggle = new ToolbarToggle(opts); toggle._parent = parent; return toggle; } /** * Returns the store that manages the state of the toggle. */ get store() { return this._state; } /** * Returns the current state of the toggle. */ get state() { return get(this._state); } /** * Sets the state of the toggle. */ set state(value: ToolbarToggleState) { this._state.set(value); } /** * Returns a Svelte 5 Attachment for the toggle. */ get attachment(): Attachment { return (node) => { const el = node as HTMLElement; // Subscribe to store const unsubscribe = this._state.subscribe((state) => { el.dataset.state = state; }); // Add a listener for the click event el.addEventListener('click', (e: MouseEvent) => { const target = e.currentTarget as HTMLElement; if (!target) return; this._state.update(toggleValue); // If this is a group, turn off all other toggles if (this._parent) { this._parent.disableAll(this); } }); return unsubscribe; }; } /** * Returns props including the attachment for the toggle, allowing easy spreading * to an HTML element. Should NOT be used in addition to manually attaching the toggle. * * Can be advantageous as spreading seems to allow CSS tree-shaking to work properly. */ get props() { return { [createAttachmentKey()]: this.attachment }; } } /** * A group of toggles in a toolbar. Makes each toggle exclusive of the others, disabling all other * toggles when any given toggle is enabled. */ export class ToolbarGroup { private _toggles: ToolbarToggle[] = []; /** * Creates a new toolbar toogle item in the group. * @param opts * @returns ToolbarToggle instance */ toggle(opts: ToolbarToggleOptions = {}) { const toggle = ToolbarToggle.withParent(this, opts); this._toggles.push(toggle); return toggle; } /** * Disables all toggles in the group except the one specified. * @param except - The toggle to exclude from disabling. */ disableAll(except?: ToolbarToggle) { this._toggles.forEach((t) => { if (t !== except) { t.state = 'off'; } }); } /** * Enables the specified toggle and disables all other toggles in the group. * @param toggle - The toggle to enable. */ enable(toggle: ToolbarToggle) { this._toggles.forEach((t) => { if (t == toggle) { t.state = 'on'; } else { t.state = 'off'; } }); } } /** * Manages the top-level toolbar groups. */ export class Toolbar { private _groups: ToolbarGroup[] = []; constructor() {} /** * Creates a new toolbar group. * @returns The newly created toolbar group. */ group() { const g = new ToolbarGroup(); this._groups.push(g); return g; } /** * Creates a new toolbar toggle item. * @param opts * @returns ToolbarToggle instance */ toggle(opts: ToolbarToggleOptions = {}) { return new ToolbarToggle(opts); } }