diff --git a/src/lib/Toolbar.ts b/src/lib/Toolbar.ts index e7ffada..e40f294 100644 --- a/src/lib/Toolbar.ts +++ b/src/lib/Toolbar.ts @@ -1,5 +1,5 @@ import { createAttachmentKey, type Attachment } from 'svelte/attachments'; -import { writable, type Writable } from 'svelte/store'; +import { get, writable, type Writable } from 'svelte/store'; export type ToolbarToggleState = 'on' | 'off'; @@ -14,62 +14,98 @@ export type ToolbarToggleOptions = { const toggleValue = (current: ToolbarToggleState): ToolbarToggleState => current === 'on' ? 'off' : 'on'; -/** createGenericToggle creates a toggle. If a list of toggles is passed, the toggle is created as a group item */ -const createGenericToggle = ( - toggles: Writable[] | null, - opts: ToolbarToggleOptions = {} -) => { - const toggleState = opts.state ?? writable('off'); - // If this is a group, add this toggle to it - if (toggles !== null) toggles.push(toggleState); +/** + * A single toggleable toolbar item. Can coordinate with other toggles in a group. + */ +export class ToolbarToggle { + private _state: Writable; + private _parent: ToolbarGroup | undefined; - return { - state: toggleState, - [createAttachmentKey()]: ((node) => { + 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 = toggleState.subscribe((state) => { + 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; - }) as Attachment, - onclick: (e: MouseEvent) => { - const target = e.currentTarget as HTMLElement; - if (!target) return; + }; + } - toggleState.update(toggleValue); - - // If this is a group, turn off all other toggles - if (toggles !== null) { - toggles.forEach((toggle) => { - if (toggle !== toggleState) { - toggle.update(() => 'off'); - } - }); - } - } - }; -}; + /** + * 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 _name: string; - private _toggles: Writable[] = []; - - constructor(name: string) { - this._name = name; - } - - /** - * The name of the toolbar group. - */ - get name() { - return this._name; - } + private _toggles: ToolbarToggle[] = []; /** * Creates a new toolbar toogle item in the group. @@ -77,7 +113,35 @@ export class ToolbarGroup { * @returns object containing state and properties */ toggle(opts: ToolbarToggleOptions = {}) { - return createGenericToggle(this._toggles, opts); + 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'; + } + }); } } @@ -91,14 +155,10 @@ export class Toolbar { /** * Creates a new toolbar group. - * @param name The name of the group. * @returns The newly created toolbar group. */ - group(name: string) { - if (this._groups.some((g) => g.name === name)) { - throw new Error(`Toolbar group "${name}" already exists.`); - } - const g = new ToolbarGroup(name); + group() { + const g = new ToolbarGroup(); this._groups.push(g); return g; } @@ -109,6 +169,6 @@ export class Toolbar { * @returns object containing state and properties */ toggle(opts: ToolbarToggleOptions = {}) { - return createGenericToggle(null, opts); + return new ToolbarToggle(opts); } } diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index ae6c7db..6d39b2b 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -32,8 +32,9 @@ let dialogOpen = $state(false); const toolbar = new Toolbar(); - const fontGroup = toolbar.group('font'); - const { state: boldState, ...boldProps } = fontGroup.toggle(); + const fontGroup = toolbar.group(); + const boldToggle = fontGroup.toggle(); + const boldStore = boldToggle.store; sui @@ -219,7 +220,7 @@

Toolbar

-

Bold is enabled: {$boldState}

+

Bold is enabled: {$boldStore}

- - -