summaryrefslogtreecommitdiffstatshomepage
path: root/frontend/src/lib/Selection.ts
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/lib/Selection.ts')
-rw-r--r--frontend/src/lib/Selection.ts141
1 files changed, 141 insertions, 0 deletions
diff --git a/frontend/src/lib/Selection.ts b/frontend/src/lib/Selection.ts
new file mode 100644
index 0000000..0ea85cc
--- /dev/null
+++ b/frontend/src/lib/Selection.ts
@@ -0,0 +1,141 @@
+import { getContext, hasContext, setContext } from 'svelte';
+import { writable, type Writable } from 'svelte/store';
+import { range } from './Utils';
+
+interface Item {
+ id: number;
+}
+
+export const hasSelectionContext = () => hasContext('selection');
+
+export function getSelectionContext<T extends Item>() {
+ return getContext<Writable<ItemSelection<T>>>('selection');
+}
+
+export function initSelectionContext<T extends Item>(
+ typename?: string,
+ toName?: (item: T) => string
+) {
+ return setContext<Writable<ItemSelection<T>>>(
+ 'selection',
+ writable(new ItemSelection(typename, toName))
+ );
+}
+
+export class ItemSelection<T extends Item> {
+ active = false;
+ typename: string;
+ #toName: (item: T) => string;
+
+ #view: T[] = [];
+ selectable: (item: T) => boolean = () => true;
+
+ #ids = new Set<number>();
+ #masked = new Set<number>();
+
+ constructor(typename?: string, toName?: (item: T) => string) {
+ this.typename = typename ?? 'unknown';
+ this.#toName = toName ?? (() => 'unknown');
+ }
+
+ set view(view: T[]) {
+ this.#view = view;
+ this.#updateMasked();
+ }
+
+ #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 Set([...this.#ids, ...selectableRange(last, index)]);
+ } else if (index < last) {
+ this.#ids = new Set([...this.#ids, ...selectableRange(index, last)]);
+ }
+ } else {
+ if (this.#ids.has(id)) {
+ this.#ids.delete(id);
+ } else {
+ this.#ids.add(id);
+ }
+ }
+
+ this.#updateMasked();
+
+ return this;
+ }
+
+ toggle() {
+ this.active = !this.active;
+
+ if (!this.active) {
+ return this.none();
+ }
+
+ return this;
+ }
+
+ all() {
+ this.#ids = new Set(this.#view.filter(this.selectable).map((i) => i.id));
+ this.#updateMasked();
+
+ return this;
+ }
+
+ none() {
+ this.#ids.clear();
+ this.#masked.clear();
+
+ return this;
+ }
+
+ clear() {
+ this.active = false;
+
+ return this.none();
+ }
+
+ contains(id: number) {
+ return this.#masked.has(id);
+ }
+
+ #updateMasked() {
+ this.#masked = new Set([...this.#ids].filter((i) => this.#indexOf(i) >= 0));
+ }
+
+ 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);
+ }
+}