diff options
Diffstat (limited to 'frontend/src/lib/selection')
-rw-r--r-- | frontend/src/lib/selection/Selectable.svelte | 26 | ||||
-rw-r--r-- | frontend/src/lib/selection/Selection.svelte.ts | 121 | ||||
-rw-r--r-- | frontend/src/lib/selection/SelectionOverlay.svelte | 12 |
3 files changed, 143 insertions, 16 deletions
diff --git a/frontend/src/lib/selection/Selectable.svelte b/frontend/src/lib/selection/Selectable.svelte index 48b6ac7..4705f44 100644 --- a/frontend/src/lib/selection/Selectable.svelte +++ b/frontend/src/lib/selection/Selectable.svelte @@ -1,18 +1,20 @@ <script lang="ts"> - import { getSelectionContext } from '$lib/Selection'; + import type { Snippet } from 'svelte'; + import { getSelectionContext } from './Selection.svelte'; - export let id: number; - export let index: number; + interface Props { + id: number; + index: number; + edit?: ((id: number) => void) | undefined; + children?: Snippet<[{ onclick: (event: MouseEvent) => void; selected: boolean }]>; + } - export let edit: ((id: number) => void) | undefined = undefined; + let { id, index, edit = undefined, children }: Props = $props(); + let selection = getSelectionContext(); - const selection = getSelectionContext(); - - $: selected = $selection.contains(id); - - const handle = (event: MouseEvent) => { - if ($selection.active) { - $selection = $selection.update(index, event.shiftKey); + const onclick = (event: MouseEvent) => { + if (selection.active) { + selection.update(index, event.shiftKey); event.preventDefault(); } else if (edit) { edit(id); @@ -21,4 +23,4 @@ }; </script> -<slot {handle} {selected} /> +{@render children?.({ onclick, selected: selection.contains(id) })} diff --git a/frontend/src/lib/selection/Selection.svelte.ts b/frontend/src/lib/selection/Selection.svelte.ts new file mode 100644 index 0000000..dc294d0 --- /dev/null +++ b/frontend/src/lib/selection/Selection.svelte.ts @@ -0,0 +1,121 @@ +import { getContext, setContext } from 'svelte'; +import { SvelteSet } from 'svelte/reactivity'; +import { range } from '../Utils'; + +interface Selectable { + id: number; +} + +export function initSelectionContext<T extends Selectable>( + typename: string, + toName: (item: T) => string, + selectable?: (item: T) => boolean +) { + return setContext('selection', new ItemSelection(typename, toName, selectable)); +} + +export function getSelectionContext<T extends Selectable>() { + return getContext<ItemSelection<T>>('selection'); +} + +export class ItemSelection<T extends Selectable> { + active = $state(false); + view: T[] = $state([]); + + #ids = $state(new SvelteSet<number>()); + #masked = $derived(new SvelteSet([...this.#ids].filter((i) => this.#indexOf(i) >= 0))); + + typename: string; + #toName: (item: T) => string; + selectable: (item: T) => boolean; + + constructor( + typename: string, + toName: (item: T) => string, + selectable: (item: T) => boolean = () => true + ) { + this.typename = typename; + this.#toName = toName; + this.selectable = selectable; + } + + #indexOf = (id: number) => this.view.findIndex((v) => v.id === id); + + update = (index: number, shift: boolean) => { + const id = this.view[index].id; + + const selectableRange = (first: number, last: number) => + range(first, last) + .filter((i) => this.selectable(this.view[i])) + .map((i) => this.view[i].id); + + if (shift) { + const indices = this.indices; + + const first = indices.at(0); + const last = indices.at(-1); + + if (first === undefined || last === undefined) { + this.#ids.add(id); + } else if (index === first || index === last) { + this.#ids.clear(); + } else if (index > last) { + this.#ids = new SvelteSet([...this.#ids, ...selectableRange(last, index)]); + } else if (index < last) { + this.#ids = new SvelteSet([...this.#ids, ...selectableRange(index, last)]); + } + } else { + if (this.#ids.has(id)) { + this.#ids.delete(id); + } else { + this.#ids.add(id); + } + } + }; + + toggle = () => { + this.active = !this.active; + + if (!this.active) { + this.none(); + } + }; + + all = () => { + this.#ids = new SvelteSet(this.view.filter(this.selectable).map((i) => i.id)); + }; + + none = () => { + this.#ids.clear(); + this.#masked.clear(); + }; + + clear = () => { + this.active = false; + this.none(); + }; + + contains(id: number) { + return this.#masked.has(id); + } + + get ids() { + return [...this.#masked]; + } + + get size() { + return this.#masked.size; + } + + get indices() { + return [...this.#ids].map(this.#indexOf).filter((i) => i >= 0); + } + + get items() { + return this.indices.map((i) => this.view[i]); + } + + get names() { + return this.items.map(this.#toName); + } +} diff --git a/frontend/src/lib/selection/SelectionOverlay.svelte b/frontend/src/lib/selection/SelectionOverlay.svelte index 04ff382..97421b0 100644 --- a/frontend/src/lib/selection/SelectionOverlay.svelte +++ b/frontend/src/lib/selection/SelectionOverlay.svelte @@ -1,7 +1,11 @@ <script lang="ts"> - export let selected: boolean; - export let position: 'top' | 'right' | 'left' | 'bottom'; - export let centered = false; + interface Props { + selected: boolean; + position: 'top' | 'right' | 'left' | 'bottom'; + centered?: boolean; + } + + let { selected, position, centered = false }: Props = $props(); </script> {#if selected} @@ -9,7 +13,7 @@ class:items-center={centered} class="{position} pointer-events-none absolute z-[1] flex bg-emerald-700/95" > - <span class="icon-base icon-[material-symbols--check] text-[2rem]" /> + <span class="icon-base icon-[material-symbols--check] text-[2rem]"></span> </div> {/if} |