diff options
Diffstat (limited to 'frontend/src/lib/selection/Selection.svelte.ts')
-rw-r--r-- | frontend/src/lib/selection/Selection.svelte.ts | 121 |
1 files changed, 121 insertions, 0 deletions
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); + } +} |