Files
sui/src/lib/Toolbar.ts
2025-07-10 14:26:33 -07:00

175 lines
4.0 KiB
TypeScript

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<ToolbarToggleState>;
};
/** 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<ToolbarToggleState>;
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);
}
}