summaryrefslogtreecommitdiffstatshomepage
path: root/frontend/src/lib/selection/Selection.svelte.ts
blob: dc294d04919b3077ac8f7056a0c6b73a4746488b (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
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);
	}
}