UNPKG

melt

Version:

The next generation of Melt UI. Built for Svelte 5.

241 lines (240 loc) 7.11 kB
import { SvelteSet } from "svelte/reactivity"; import { extract } from "./extract"; import { first, forEach, last } from "./iterator"; import { isFunction, isIterable, isSvelteSet } from "./is"; import { watch } from "runed"; import { dequal } from "dequal"; /** * Converts a value or iterable to a SvelteSet * @template T - The type of values in the set * @param v - The value or iterable to convert * @returns A new SvelteSet containing the value(s) * @internal */ function toSet(v) { if (v === undefined) return new SvelteSet(); if (isSvelteSet(v)) return v; if (!isIterable(v)) return new SvelteSet([v]); return new SvelteSet(v); } /** * Extracts a single value from a value or iterable * @template T - The type of the value * @param v - The value or iterable to extract from * @returns The single value or undefined * @internal */ function toSingle(v) { if (!isIterable(v) || v === undefined) return v; return last(v); } /** * Manages selection state with support for single or multiple selection * @template T - The type of values that can be selected (defaults to string) * @template Multiple - Boolean flag indicating if multiple selection is enabled (defaults to false) * * @example * ```ts * // Single string selection * const singleSelect = new SelectionState<string>(); * * // Multiple string selection * const multiSelect = new SelectionState<string, true>({ multiple: true }); * * // Custom type selection * interface User { id: number; name: string } * const userSelect = new SelectionState<User>(); * ``` */ export class SelectionState { #props; #internal_set = new SvelteSet(); isControlled = $derived(isSvelteSet(this.#props.value) || isFunction(this.#props.value)); isMultiple = $derived(extract(this.#props.multiple, false)); constructor(props) { this.#props = props; if (this.isControlled) return; if (!isIterable(props.value) && props.value !== undefined) { this.#internal_set.add(props.value); } else if (isIterable(props.value)) { forEach(props.value, (v) => this.#internal_set.add(v)); } watch(() => this.isMultiple, (isMultiple) => { if (isMultiple) return; const curr = this.current; this.#internal_set.clear(); if (curr === undefined) return; this.#internal_set.add(curr); }, { lazy: true, }); } /** * Gets the current selection value(s) * @returns For multiple selection, returns a SvelteSet of values. For single selection, returns a single value or undefined. */ get current() { let value; if (isFunction(this.#props.value)) { value = this.#props.value(); } else if (isSvelteSet(this.#props.value)) { value = this.#props.value; } else { value = this.#internal_set; } if (this.isMultiple) { return toSet(value); } return toSingle(value); } /** * Sets the current selection value(s) * @param value - The new selection value(s) */ set current(value) { this.onChange(value); if (this.isControlled) return; this.#internal_set.clear(); if (isSvelteSet(value)) { value.forEach((v) => this.#internal_set.add(v)); } else if (value !== undefined) { this.#internal_set.add(value); } } /** * Manipulates the selection set through a callback * @param cb - Callback function that receives the selection set for manipulation * @internal */ manipulate(cb) { const set = this.isControlled ? toSet(this.current) : this.#internal_set; const prevValue = $state.snapshot(this.isMultiple ? set : toSingle(set)); cb(set); const newValue = this.isMultiple ? set : toSingle(set); if (dequal(prevValue, $state.snapshot(newValue))) return; this.onChange(newValue); } /** * Triggers the onChange callback with the current selection * @param value - The current selection value(s) * @internal */ onChange(value) { if (!this.#props.onChange) return; this.#props.onChange(value); } /** * Checks if an item is currently selected * @param item - The item to check * @returns True if the item is selected, false otherwise */ has(item) { return toSet(this.current).has(item); } /** * Adds an item to the selection * For single selection, this replaces the current selection * For multiple selection, this adds to the current selection * @param value - The item to add */ add(value) { this.manipulate((set) => { if (!this.isMultiple) { set.clear(); } set.add(value); }); } /** * Adds multiple items to the selection * For single selection, only the first item is selected * For multiple selection, all items are added to the selection * @param items - The items to add */ addAll(items) { this.manipulate((set) => { if (!this.isMultiple) { set.clear(); set.add(first(items)); } else { forEach(items, (i) => set.add(i)); } }); } /** * Removes an item from the selection * @param value - The item to remove */ delete(value) { this.manipulate((set) => { set.delete(value); }); } /** * Removes multiple items from the selection * @param items - The items to remove */ deleteAll(items) { this.manipulate((set) => forEach(items, set.delete)); } /** * Clears all selections */ clear() { this.manipulate((set) => set.clear()); } /** * Gets the number of selected items * @returns The number of selected items */ size() { return toSet(this.current).size; } /** * Toggles the selection state of an item * If the item is selected, it will be deselected * If the item is not selected, it will be selected * @param item - The item to toggle */ toggle(item) { this.manipulate((set) => { if (set.has(item)) { set.delete(item); } else { if (!this.isMultiple) set.clear(); set.add(item); } }); } /** * Converts the current selection to a SvelteSet * @returns A SvelteSet containing the current selection */ toSet() { return toSet(this.current); } /** * Converts the current selection to an array * @returns An array containing the current selection */ toArray() { return Array.from(this.toSet()); } }