import { useState } from "react"; const NONE: ReadonlySet = new Set(); /** Which rows of a table are ticked, for the bulk actions over them. `all` is every row the list * holds and `shown` the ones on screen, in screen order. A row a filter hides stays selected and * comes back ticked, but `shownSelected`, what an action covers, leaves it out: nothing happens to a * row the reader cannot see. A row that left the list drops out for good, or an Undo would bring it * back ticked. A change of `scope` starts over: the same video in another list is another row. */ export function useRowSelection(all: readonly string[], shown: readonly string[], scope: unknown) { const [selected, setSelected] = useState(NONE); // The last row toggled: where a Shift range starts. const [anchor, setAnchor] = useState(null); const [selectedFor, setSelectedFor] = useState(scope); if (selectedFor !== scope) { setSelectedFor(scope); setSelected(NONE); setAnchor(null); } const held = new Set(all); const kept = [...selected].filter((id) => held.has(id)); if (kept.length !== selected.size) setSelected(new Set(kept)); const shownSelected = shown.filter((id) => selected.has(id)); function toggle(id: string, range = false) { const on = !selected.has(id); const from = range && anchor != null ? shown.indexOf(anchor) : -1; const to = shown.indexOf(id); // Taken from where the range ends, the way a mail list does: it can clear a run of rows too. const rows = from >= 0 && to >= 0 ? shown.slice(Math.min(from, to), Math.max(from, to) + 1) : [id]; const next = new Set(selected); for (const row of rows) { if (on) next.add(row); else next.delete(row); } setSelected(next); setAnchor(id); } function toggleAllShown() { const next = new Set(selected); const on = shownSelected.length < shown.length; for (const row of shown) { if (on) next.add(row); else next.delete(row); } setSelected(next); setAnchor(null); } /** Says whether there was anything to clear: an Esc that cleared nothing is not spent. */ function clear(): boolean { const had = selected.size > 0; if (had) setSelected(NONE); setAnchor(null); return had; } /** Tick these again: an action that took them off and failed. */ function select(ids: readonly string[]) { setSelected((now) => new Set([...now, ...ids])); } return { shownSelected, hiddenCount: selected.size - shownSelected.length, isSelected: (id: string) => selected.has(id), headerState: shownSelected.length === 0 ? ("none" as const) : shownSelected.length === shown.length ? ("all" as const) : ("some" as const), toggle, toggleAllShown, clear, select, }; }