summaryrefslogblamecommitdiffstatshomepage
path: root/frontend/src/lib/Selection.ts
blob: 0ea85cca079d9a04cf817d7c1a2bb1c50eda3561 (plain) (tree)












































































































































                                                                                                     
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);
	}
}