rework toolbar to use attachment pattern, simpler external control
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { createAttachmentKey, type Attachment } from 'svelte/attachments';
|
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';
|
export type ToolbarToggleState = 'on' | 'off';
|
||||||
|
|
||||||
@@ -14,62 +14,98 @@ export type ToolbarToggleOptions = {
|
|||||||
const toggleValue = (current: ToolbarToggleState): ToolbarToggleState =>
|
const toggleValue = (current: ToolbarToggleState): ToolbarToggleState =>
|
||||||
current === 'on' ? 'off' : 'on';
|
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 = (
|
* A single toggleable toolbar item. Can coordinate with other toggles in a group.
|
||||||
toggles: Writable<ToolbarToggleState>[] | null,
|
*/
|
||||||
opts: ToolbarToggleOptions = {}
|
export class ToolbarToggle {
|
||||||
) => {
|
private _state: Writable<ToolbarToggleState>;
|
||||||
const toggleState = opts.state ?? writable<ToolbarToggleState>('off');
|
private _parent: ToolbarGroup | undefined;
|
||||||
// If this is a group, add this toggle to it
|
|
||||||
if (toggles !== null) toggles.push(toggleState);
|
|
||||||
|
|
||||||
return {
|
constructor(opts: ToolbarToggleOptions = {}) {
|
||||||
state: toggleState,
|
this._state = opts.state ?? writable('off');
|
||||||
[createAttachmentKey()]: ((node) => {
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
const el = node as HTMLElement;
|
||||||
// Subscribe to store
|
// Subscribe to store
|
||||||
const unsubscribe = toggleState.subscribe((state) => {
|
const unsubscribe = this._state.subscribe((state) => {
|
||||||
el.dataset.state = state;
|
el.dataset.state = state;
|
||||||
});
|
});
|
||||||
|
|
||||||
return unsubscribe;
|
// Add a listener for the click event
|
||||||
}) as Attachment,
|
el.addEventListener('click', (e: MouseEvent) => {
|
||||||
onclick: (e: MouseEvent) => {
|
|
||||||
const target = e.currentTarget as HTMLElement;
|
const target = e.currentTarget as HTMLElement;
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
|
|
||||||
toggleState.update(toggleValue);
|
this._state.update(toggleValue);
|
||||||
|
|
||||||
// If this is a group, turn off all other toggles
|
// If this is a group, turn off all other toggles
|
||||||
if (toggles !== null) {
|
if (this._parent) {
|
||||||
toggles.forEach((toggle) => {
|
this._parent.disableAll(this);
|
||||||
if (toggle !== toggleState) {
|
|
||||||
toggle.update(() => 'off');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
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
|
* A group of toggles in a toolbar. Makes each toggle exclusive of the others, disabling all other
|
||||||
* toggles when any given toggle is enabled.
|
* toggles when any given toggle is enabled.
|
||||||
*/
|
*/
|
||||||
export class ToolbarGroup {
|
export class ToolbarGroup {
|
||||||
private _name: string;
|
private _toggles: ToolbarToggle[] = [];
|
||||||
private _toggles: Writable<ToolbarToggleState>[] = [];
|
|
||||||
|
|
||||||
constructor(name: string) {
|
|
||||||
this._name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The name of the toolbar group.
|
|
||||||
*/
|
|
||||||
get name() {
|
|
||||||
return this._name;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new toolbar toogle item in the group.
|
* Creates a new toolbar toogle item in the group.
|
||||||
@@ -77,7 +113,35 @@ export class ToolbarGroup {
|
|||||||
* @returns object containing state and properties
|
* @returns object containing state and properties
|
||||||
*/
|
*/
|
||||||
toggle(opts: ToolbarToggleOptions = {}) {
|
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.
|
* Creates a new toolbar group.
|
||||||
* @param name The name of the group.
|
|
||||||
* @returns The newly created toolbar group.
|
* @returns The newly created toolbar group.
|
||||||
*/
|
*/
|
||||||
group(name: string) {
|
group() {
|
||||||
if (this._groups.some((g) => g.name === name)) {
|
const g = new ToolbarGroup();
|
||||||
throw new Error(`Toolbar group "${name}" already exists.`);
|
|
||||||
}
|
|
||||||
const g = new ToolbarGroup(name);
|
|
||||||
this._groups.push(g);
|
this._groups.push(g);
|
||||||
return g;
|
return g;
|
||||||
}
|
}
|
||||||
@@ -109,6 +169,6 @@ export class Toolbar {
|
|||||||
* @returns object containing state and properties
|
* @returns object containing state and properties
|
||||||
*/
|
*/
|
||||||
toggle(opts: ToolbarToggleOptions = {}) {
|
toggle(opts: ToolbarToggleOptions = {}) {
|
||||||
return createGenericToggle(null, opts);
|
return new ToolbarToggle(opts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,8 +32,9 @@
|
|||||||
let dialogOpen = $state(false);
|
let dialogOpen = $state(false);
|
||||||
|
|
||||||
const toolbar = new Toolbar();
|
const toolbar = new Toolbar();
|
||||||
const fontGroup = toolbar.group('font');
|
const fontGroup = toolbar.group();
|
||||||
const { state: boldState, ...boldProps } = fontGroup.toggle();
|
const boldToggle = fontGroup.toggle();
|
||||||
|
const boldStore = boldToggle.store;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<title>sui</title>
|
<title>sui</title>
|
||||||
@@ -219,7 +220,7 @@
|
|||||||
<p class="title">Toolbar</p>
|
<p class="title">Toolbar</p>
|
||||||
|
|
||||||
<div class="my-2">
|
<div class="my-2">
|
||||||
<p>Bold is enabled: {$boldState}</p>
|
<p>Bold is enabled: {$boldStore}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -236,20 +237,30 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="bg-sui-text/50 w-[1px] self-stretch"></div>
|
<div class="bg-sui-text/50 w-[1px] self-stretch"></div>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<button class="item" title="Toggle Bold" aria-label="bold" {...boldProps}>
|
<button class="item" title="Toggle Bold" aria-label="bold" {@attach boldToggle.attachment}>
|
||||||
<TextB size="1.25em" />
|
<TextB size="1.25em" />
|
||||||
</button>
|
</button>
|
||||||
<button class="item" title="Toggle Italic" aria-label="italic" {...fontGroup.toggle()}>
|
<button
|
||||||
|
class="item"
|
||||||
|
title="Toggle Italic"
|
||||||
|
aria-label="italic"
|
||||||
|
{@attach fontGroup.toggle().attachment}
|
||||||
|
>
|
||||||
<TextItalic size="1.25em" />
|
<TextItalic size="1.25em" />
|
||||||
</button>
|
</button>
|
||||||
<button class="item" title="Toggle Underline" aria-label="underline" {...fontGroup.toggle()}>
|
<button
|
||||||
|
class="item"
|
||||||
|
title="Toggle Underline"
|
||||||
|
aria-label="underline"
|
||||||
|
{@attach fontGroup.toggle().attachment}
|
||||||
|
>
|
||||||
<TextUnderline size="1.25em" />
|
<TextUnderline size="1.25em" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="item"
|
class="item"
|
||||||
title="Toggle Strikethrough"
|
title="Toggle Strikethrough"
|
||||||
aria-label="strikethrough"
|
aria-label="strikethrough"
|
||||||
{...fontGroup.toggle()}
|
{...fontGroup.toggle().props}
|
||||||
>
|
>
|
||||||
<TextStrikethrough size="1.25em" />
|
<TextStrikethrough size="1.25em" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user