feat(playlists): select rows with a box, Shift for a range, x and Esc
A box first in every row and one over the column for the rows shown, the selected row tinted. A filter keeps what it hides selected; another list starts over; a row that left the list drops out. Watch later has none. A box no longer counts as a typing target: a click on one left the focus there and the page's keys went dead.
This commit is contained in:
@@ -36,6 +36,15 @@ interface NavList {
|
||||
readonly reveal: (index: number) => void;
|
||||
/** Takes the item at `index` out of the list (Delete), or null for a list that cannot. */
|
||||
readonly remove: () => ((index: number) => void) | null;
|
||||
/** The list's selection (`x`, Esc), or null for a list whose items cannot be selected. */
|
||||
readonly selection: () => NavSelection | null;
|
||||
}
|
||||
|
||||
/** What `x` and Esc do in a list whose items can be selected. `clear` says whether it cleared
|
||||
* anything: an Esc that did not is left for whoever else answers it. */
|
||||
export interface NavSelection {
|
||||
readonly toggle: (index: number) => void;
|
||||
readonly clear: () => boolean;
|
||||
}
|
||||
|
||||
const lists = new Map<HTMLElement, NavList>();
|
||||
@@ -432,6 +441,27 @@ function onKey(e: KeyboardEvent): void {
|
||||
},
|
||||
{ inTopLayer },
|
||||
);
|
||||
dispatch(
|
||||
e,
|
||||
"listSelect",
|
||||
{
|
||||
// Anywhere in the item, unlike Delete: a tick is undone by the same key, and after a click on
|
||||
// the box the focus sits inside the row. Held down, it must not flip back and forth.
|
||||
select: (ev) => {
|
||||
const cur = current();
|
||||
const selection = cur?.list.selection();
|
||||
if (!cur || !selection || ev.repeat) return;
|
||||
ev.preventDefault();
|
||||
selection.toggle(cur.index);
|
||||
},
|
||||
clearSelection: (ev) => {
|
||||
let cleared = false;
|
||||
for (const list of ordered()) if (list.selection()?.clear()) cleared = true;
|
||||
if (cleared) ev.preventDefault();
|
||||
},
|
||||
},
|
||||
{ inTopLayer },
|
||||
);
|
||||
}
|
||||
|
||||
// How long a removal gets to take its row out of the DOM before the focus gives up following it.
|
||||
@@ -599,6 +629,8 @@ export function useListNav(
|
||||
reveal?: (index: number) => void;
|
||||
/** A list whose items the Delete key takes out (a playlist's rows): the item's index. */
|
||||
onDelete?: (index: number) => void;
|
||||
/** A list whose items can be selected, by `x` and Esc. */
|
||||
selection?: NavSelection;
|
||||
},
|
||||
): void {
|
||||
// Read when a key is pressed, so the registration below never re-runs for a new `opts` object.
|
||||
@@ -609,6 +641,7 @@ export function useListNav(
|
||||
const [hasItems, setHasItems] = useState(false);
|
||||
useKeyScope("list", opts.enabled && hasItems);
|
||||
useKeyScope("listDelete", opts.enabled && hasItems && !!opts.onDelete);
|
||||
useKeyScope("listSelect", opts.enabled && hasItems && !!opts.selection);
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!opts.enabled || !el) return;
|
||||
@@ -618,6 +651,7 @@ export function useListNav(
|
||||
cols: () => latestOpts().cols ?? measuredCols(el),
|
||||
reveal: (i) => latestOpts().reveal?.(i),
|
||||
remove: () => latestOpts().onDelete ?? null,
|
||||
selection: () => latestOpts().selection ?? null,
|
||||
});
|
||||
el.setAttribute(LIST, "");
|
||||
const check = () => setHasItems(el.querySelector(`[${INDEX}]`) !== null);
|
||||
|
||||
Reference in New Issue
Block a user