@enigmax/primitives 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-3BQVOOAM.js +388 -0
- package/dist/{chunk-QQFNAKMY.js → chunk-45UHLZYT.js} +1 -1
- package/dist/chunk-4VUHQFAT.js +130 -0
- package/dist/{chunk-D5A2ZMAG.js → chunk-4ZPMP47J.js} +6 -1
- package/dist/chunk-DSBYVA7V.js +713 -0
- package/dist/{chunk-FWVWX67R.js → chunk-FGBIZDV2.js} +3 -3
- package/dist/{chunk-R4ZAEE7V.js → chunk-KEVZ5XQV.js} +1 -1
- package/dist/chunk-N6PDHMAX.js +213 -0
- package/dist/chunk-NONGREXC.js +248 -0
- package/dist/chunk-VKL3DEIQ.js +804 -0
- package/dist/chunk-WSQC3PCC.js +466 -0
- package/dist/context-menu-D3FtTn7v.d.ts +174 -0
- package/dist/{index-BHpOZncw.d.ts → index-DQNnohoo.d.ts} +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +8 -4
- package/dist/keys-D2zJs1uB.d.ts +100 -0
- package/dist/next/index.d.ts +9 -2
- package/dist/next/index.js +20 -13
- package/dist/react/context-menu.d.ts +202 -0
- package/dist/react/context-menu.js +6 -0
- package/dist/react/index.d.ts +10 -3
- package/dist/react/index.js +19 -12
- package/dist/react/input.d.ts +2 -2
- package/dist/react/input.js +1 -1
- package/dist/react/palette.d.ts +1 -1
- package/dist/react/palette.js +3 -3
- package/dist/react/search.d.ts +1 -1
- package/dist/react/search.js +2 -2
- package/dist/react/select.d.ts +217 -0
- package/dist/react/select.js +7 -0
- package/dist/react/selection.d.ts +104 -0
- package/dist/react/selection.js +4 -0
- package/dist/react-router/index.d.ts +9 -2
- package/dist/react-router/index.js +20 -13
- package/dist/search/index.d.ts +2 -2
- package/dist/search/index.js +1 -1
- package/dist/{search-BD9-5O5U.d.ts → search-DYgqRp37.d.ts} +13 -1
- package/dist/{search-UQEXAPQB.js → search-PBZORZ7P.js} +1 -1
- package/dist/select-ClSy-J1f.d.ts +100 -0
- package/dist/selection-B_pmzHpy.d.ts +150 -0
- package/package.json +21 -2
- package/recipes/context-menu/styles.css +177 -0
- package/recipes/input/styles.css +9 -0
- package/recipes/palette/styles.css +3 -0
- package/recipes/search/tailwind.tsx +3 -2
- package/recipes/select/styles.css +230 -0
- package/recipes/toast/styles.css +1 -1
- package/registry.json +358 -1
- package/src/core/context-menu.ts +694 -0
- package/src/core/keys.ts +264 -0
- package/src/core/search.ts +19 -0
- package/src/core/select.ts +404 -0
- package/src/core/selection.ts +648 -0
- package/src/index.ts +60 -1
- package/src/react/context-menu/context.ts +57 -0
- package/src/react/context-menu/index.tsx +94 -0
- package/src/react/context-menu/root.tsx +846 -0
- package/src/react/context-menu/styles.ts +186 -0
- package/src/react/index.ts +68 -1
- package/src/react/palette/root.tsx +2 -2
- package/src/react/select/context.ts +56 -0
- package/src/react/select/index.tsx +96 -0
- package/src/react/select/root.tsx +839 -0
- package/src/react/select/styles.ts +238 -0
- package/src/react/selection/index.tsx +115 -0
- package/src/react/selection/use-selection.ts +260 -0
- /package/dist/{chunk-U3V4EHOB.js → chunk-3HDEZ2E7.js} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { createRecentStore, shortcutLabel, moveActive, groupRows, isPaletteShortcut } from './chunk-DTWZDONY.js';
|
|
2
|
-
import { subsequenceMatcher, createSearch } from './chunk-D5A2ZMAG.js';
|
|
3
1
|
import { Slot } from './chunk-HS3X3XCW.js';
|
|
2
|
+
import { createRecentStore, shortcutLabel, moveActive, groupRows, isPaletteShortcut } from './chunk-DTWZDONY.js';
|
|
3
|
+
import { subsequenceMatcher, createSearch, shortenQuery } from './chunk-4ZPMP47J.js';
|
|
4
4
|
import { createPortal } from 'react-dom';
|
|
5
5
|
import { createContext, useContext, useState, useRef, useId, useMemo, useCallback, useEffect } from 'react';
|
|
6
6
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
@@ -373,7 +373,7 @@ function PaletteList({ children, empty, heading, ...props }) {
|
|
|
373
373
|
role: "listbox",
|
|
374
374
|
"aria-label": "Results",
|
|
375
375
|
"data-enigma-palette-list": "",
|
|
376
|
-
children: palette.rows.length === 0 ? empty ?? /* @__PURE__ */ jsx("p", { "data-enigma-palette-empty": "", children: palette.query.trim() ? `Nothing matches "${palette.query
|
|
376
|
+
children: palette.rows.length === 0 ? empty ?? /* @__PURE__ */ jsx("p", { "data-enigma-palette-empty": "", children: palette.query.trim() ? `Nothing matches "${shortenQuery(palette.query)}".` : "Type to search." }) : groups.map((group) => /* @__PURE__ */ jsxs("div", { role: "group", "aria-label": group.label, "data-enigma-palette-group": "", children: [
|
|
377
377
|
heading !== null && (heading?.(group.label) ?? // The group carries the name already, so announcing the
|
|
378
378
|
// heading again would only repeat it.
|
|
379
379
|
/* @__PURE__ */ jsx("p", { "data-enigma-palette-group-label": "", "aria-hidden": "true", children: group.label })),
|
|
@@ -25,7 +25,7 @@ function writeValue(input, next) {
|
|
|
25
25
|
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
26
26
|
}
|
|
27
27
|
var PasswordExtras = lazy(() => import('./password-3DRQYAYQ.js').then((module) => ({ default: module.PasswordExtras })));
|
|
28
|
-
var SearchExtras = lazy(() => import('./search-
|
|
28
|
+
var SearchExtras = lazy(() => import('./search-PBZORZ7P.js').then((module) => ({ default: module.SearchExtras })));
|
|
29
29
|
var Input = forwardRef(function Input2(props, forwardedRef) {
|
|
30
30
|
const {
|
|
31
31
|
reveal,
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { createSelection } from './chunk-WSQC3PCC.js';
|
|
2
|
+
import { useId, useRef, useMemo, useState, useEffect, useCallback } from 'react';
|
|
3
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
var MARQUEE_SLOP = 4;
|
|
6
|
+
function useSelection(options) {
|
|
7
|
+
const { items, scrollIntoView = true, ...rest } = options;
|
|
8
|
+
const id = useId();
|
|
9
|
+
const listRef = useRef(null);
|
|
10
|
+
const latest = useRef(options);
|
|
11
|
+
latest.current = options;
|
|
12
|
+
const instance = useMemo(() => createSelection({
|
|
13
|
+
items,
|
|
14
|
+
getId: (item, index) => (latest.current.getId ?? ((_, position) => String(position)))(item, index),
|
|
15
|
+
disabled: (item, index) => Boolean(latest.current.disabled?.(item, index)),
|
|
16
|
+
onCommand: (event) => latest.current.onCommand?.(event),
|
|
17
|
+
onSelectionChange: (ids, chosen) => latest.current.onSelectionChange?.(ids, chosen)
|
|
18
|
+
// Built once: rebuilding it would drop the selection and the cursor on every render.
|
|
19
|
+
// Everything below is pushed in through update().
|
|
20
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
21
|
+
}), []);
|
|
22
|
+
const [state, setState] = useState(() => instance.state);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const unsubscribe = instance.subscribe(setState);
|
|
25
|
+
setState(instance.state);
|
|
26
|
+
return () => {
|
|
27
|
+
unsubscribe();
|
|
28
|
+
instance.destroy();
|
|
29
|
+
};
|
|
30
|
+
}, [instance]);
|
|
31
|
+
const { multiple, columns, page, shortcuts } = rest;
|
|
32
|
+
const shortcutSignature = JSON.stringify(shortcuts ?? null);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
instance.update({ items, multiple, columns, page, shortcuts: latest.current.shortcuts });
|
|
35
|
+
}, [instance, items, multiple, columns, page, shortcutSignature]);
|
|
36
|
+
const cursor = state.cursor;
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!scrollIntoView || cursor < 0) return;
|
|
39
|
+
const row = listRef.current?.querySelector(`[data-enigma-selection-index="${cursor}"]`);
|
|
40
|
+
row?.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
41
|
+
}, [cursor, scrollIntoView]);
|
|
42
|
+
const isSelected = useCallback((rowId) => state.selectedSet.has(rowId), [state.selectedSet]);
|
|
43
|
+
const itemId = useCallback((index) => `${id}-item-${index}`, [id]);
|
|
44
|
+
const getListProps = useCallback((props = {}) => ({
|
|
45
|
+
...props,
|
|
46
|
+
ref: (node) => {
|
|
47
|
+
listRef.current = node;
|
|
48
|
+
},
|
|
49
|
+
id: `${id}-list`,
|
|
50
|
+
role: props.role ?? "listbox",
|
|
51
|
+
"aria-multiselectable": (latest.current.multiple ?? true) || void 0,
|
|
52
|
+
// A single tab stop, with the arrows moving a cursor INSIDE it: a list of two hundred
|
|
53
|
+
// rows that are each tabbable is a list nobody can tab past.
|
|
54
|
+
tabIndex: props.tabIndex ?? 0,
|
|
55
|
+
"aria-activedescendant": state.cursor >= 0 ? itemId(state.cursor) : void 0,
|
|
56
|
+
"data-enigma-selection-list": "",
|
|
57
|
+
"data-selected-count": String(state.count),
|
|
58
|
+
onKeyDown: (event) => {
|
|
59
|
+
props.onKeyDown?.(event);
|
|
60
|
+
if (event.defaultPrevented) return;
|
|
61
|
+
if (instance.keyDown(event)) event.preventDefault();
|
|
62
|
+
}
|
|
63
|
+
}), [id, instance, itemId, state.cursor, state.count]);
|
|
64
|
+
const getItemProps = useCallback((index, props = {}) => {
|
|
65
|
+
const item = latest.current.items[index];
|
|
66
|
+
const rowId = item === void 0 ? null : (latest.current.getId ?? ((_, position) => String(position)))(item, index);
|
|
67
|
+
const disabled = item !== void 0 && Boolean(latest.current.disabled?.(item, index));
|
|
68
|
+
const chosen = rowId !== null && state.selectedSet.has(rowId);
|
|
69
|
+
return {
|
|
70
|
+
...props,
|
|
71
|
+
id: itemId(index),
|
|
72
|
+
role: props.role ?? "option",
|
|
73
|
+
"aria-selected": chosen,
|
|
74
|
+
"aria-disabled": disabled || void 0,
|
|
75
|
+
"data-enigma-selection-item": "",
|
|
76
|
+
"data-enigma-selection-index": String(index),
|
|
77
|
+
"data-selected": chosen ? "" : void 0,
|
|
78
|
+
"data-cursor": state.cursor === index ? "" : void 0,
|
|
79
|
+
"data-disabled": disabled ? "" : void 0,
|
|
80
|
+
onPointerDown: (event) => {
|
|
81
|
+
props.onPointerDown?.(event);
|
|
82
|
+
if (event.defaultPrevented || disabled || event.button !== 0) return;
|
|
83
|
+
instance.click(index, event);
|
|
84
|
+
},
|
|
85
|
+
onContextMenu: (event) => {
|
|
86
|
+
props.onContextMenu?.(event);
|
|
87
|
+
if (event.defaultPrevented || disabled) return;
|
|
88
|
+
if (rowId !== null && !state.selectedSet.has(rowId)) instance.click(index);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}, [instance, itemId, state.selectedSet, state.cursor]);
|
|
92
|
+
const [marquee, setMarquee] = useState(null);
|
|
93
|
+
const drag = useRef(null);
|
|
94
|
+
const getMarqueeProps = useCallback((props = {}) => ({
|
|
95
|
+
...props,
|
|
96
|
+
onPointerDown: (event) => {
|
|
97
|
+
props.onPointerDown?.(event);
|
|
98
|
+
if (event.defaultPrevented || event.button !== 0) return;
|
|
99
|
+
if (event.target.closest("[data-enigma-selection-item]")) return;
|
|
100
|
+
drag.current = { x: event.clientX, y: event.clientY, pointer: event.pointerId };
|
|
101
|
+
}
|
|
102
|
+
}), []);
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
const list = listRef.current;
|
|
105
|
+
if (!list) return;
|
|
106
|
+
const move = (event) => {
|
|
107
|
+
const start = drag.current;
|
|
108
|
+
if (!start || event.pointerId !== start.pointer) return;
|
|
109
|
+
if (!marquee && Math.abs(event.clientX - start.x) < MARQUEE_SLOP && Math.abs(event.clientY - start.y) < MARQUEE_SLOP) return;
|
|
110
|
+
if (!marquee) instance.beginMarquee(event.ctrlKey || event.metaKey);
|
|
111
|
+
const bounds = list.getBoundingClientRect();
|
|
112
|
+
const left = Math.min(start.x, event.clientX);
|
|
113
|
+
const top = Math.min(start.y, event.clientY);
|
|
114
|
+
const rect = {
|
|
115
|
+
left: left - bounds.left + list.scrollLeft,
|
|
116
|
+
top: top - bounds.top + list.scrollTop,
|
|
117
|
+
width: Math.abs(event.clientX - start.x),
|
|
118
|
+
height: Math.abs(event.clientY - start.y)
|
|
119
|
+
};
|
|
120
|
+
setMarquee(rect);
|
|
121
|
+
const covered = [];
|
|
122
|
+
for (const row of list.querySelectorAll("[data-enigma-selection-item]")) {
|
|
123
|
+
const box = row.getBoundingClientRect();
|
|
124
|
+
const hits = box.right >= Math.min(start.x, event.clientX) && box.left <= Math.max(start.x, event.clientX) && box.bottom >= Math.min(start.y, event.clientY) && box.top <= Math.max(start.y, event.clientY);
|
|
125
|
+
if (hits) covered.push(Number(row.dataset.enigmaSelectionIndex));
|
|
126
|
+
}
|
|
127
|
+
instance.marqueeTo(covered);
|
|
128
|
+
};
|
|
129
|
+
const up = () => {
|
|
130
|
+
if (!drag.current) return;
|
|
131
|
+
drag.current = null;
|
|
132
|
+
setMarquee(null);
|
|
133
|
+
instance.endMarquee();
|
|
134
|
+
};
|
|
135
|
+
window.addEventListener("pointermove", move);
|
|
136
|
+
window.addEventListener("pointerup", up);
|
|
137
|
+
window.addEventListener("pointercancel", up);
|
|
138
|
+
return () => {
|
|
139
|
+
window.removeEventListener("pointermove", move);
|
|
140
|
+
window.removeEventListener("pointerup", up);
|
|
141
|
+
window.removeEventListener("pointercancel", up);
|
|
142
|
+
};
|
|
143
|
+
}, [instance, marquee]);
|
|
144
|
+
return {
|
|
145
|
+
instance,
|
|
146
|
+
state,
|
|
147
|
+
isSelected,
|
|
148
|
+
getListProps,
|
|
149
|
+
getItemProps,
|
|
150
|
+
getMarqueeProps,
|
|
151
|
+
marquee,
|
|
152
|
+
ids: { list: `${id}-list` },
|
|
153
|
+
itemId
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function SelectionList(props) {
|
|
157
|
+
const {
|
|
158
|
+
items,
|
|
159
|
+
getId,
|
|
160
|
+
disabled,
|
|
161
|
+
multiple,
|
|
162
|
+
columns,
|
|
163
|
+
page,
|
|
164
|
+
shortcuts,
|
|
165
|
+
onCommand,
|
|
166
|
+
onSelectionChange,
|
|
167
|
+
scrollIntoView,
|
|
168
|
+
children,
|
|
169
|
+
empty,
|
|
170
|
+
marquee = true,
|
|
171
|
+
...rest
|
|
172
|
+
} = props;
|
|
173
|
+
const selection = useSelection({
|
|
174
|
+
items,
|
|
175
|
+
getId,
|
|
176
|
+
disabled,
|
|
177
|
+
multiple,
|
|
178
|
+
columns,
|
|
179
|
+
page,
|
|
180
|
+
shortcuts,
|
|
181
|
+
onCommand,
|
|
182
|
+
onSelectionChange,
|
|
183
|
+
scrollIntoView
|
|
184
|
+
});
|
|
185
|
+
const listProps = selection.getListProps(rest);
|
|
186
|
+
const containerProps = marquee ? selection.getMarqueeProps(listProps) : listProps;
|
|
187
|
+
return (
|
|
188
|
+
// `position: relative` is the one style this component sets, and it is not a look: the
|
|
189
|
+
// rubber band is absolutely positioned inside, and without a positioned ancestor it
|
|
190
|
+
// would be drawn against the page instead of against the list.
|
|
191
|
+
/* @__PURE__ */ jsxs("div", { ...containerProps, style: { position: "relative", ...rest.style }, children: [
|
|
192
|
+
items.length === 0 && empty !== void 0 ? /* @__PURE__ */ jsx("div", { "data-enigma-selection-empty": "", children: empty }) : items.map((item, index) => /* @__PURE__ */ jsx("div", { ...selection.getItemProps(index), children: children({
|
|
193
|
+
item,
|
|
194
|
+
index,
|
|
195
|
+
selected: selection.state.selectedSet.has(getId ? getId(item, index) : String(index)),
|
|
196
|
+
cursor: selection.state.cursor === index,
|
|
197
|
+
disabled: Boolean(disabled?.(item, index))
|
|
198
|
+
}) }, getId ? getId(item, index) : index)),
|
|
199
|
+
selection.marquee && // The band is drawn, not just computed: without something on screen a drag over
|
|
200
|
+
// empty space looks like the list selecting rows at random.
|
|
201
|
+
/* @__PURE__ */ jsx(
|
|
202
|
+
"div",
|
|
203
|
+
{
|
|
204
|
+
"aria-hidden": "true",
|
|
205
|
+
"data-enigma-selection-marquee": "",
|
|
206
|
+
style: { position: "absolute", pointerEvents: "none", ...selection.marquee }
|
|
207
|
+
}
|
|
208
|
+
)
|
|
209
|
+
] })
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export { SelectionList, useSelection };
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { typeaheadStep } from './chunk-4VUHQFAT.js';
|
|
2
|
+
import { createSearch } from './chunk-4ZPMP47J.js';
|
|
3
|
+
|
|
4
|
+
// src/core/select.ts
|
|
5
|
+
var SELECT_SEARCH_KEYS = ["label", "description", "group", "keywords"];
|
|
6
|
+
var PAGE = 5;
|
|
7
|
+
function fold(value) {
|
|
8
|
+
return value.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase();
|
|
9
|
+
}
|
|
10
|
+
function toList(value) {
|
|
11
|
+
if (value == null) return [];
|
|
12
|
+
const list = Array.isArray(value) ? [...value] : [value];
|
|
13
|
+
return list.filter((entry) => entry !== "");
|
|
14
|
+
}
|
|
15
|
+
function sameValues(left, right) {
|
|
16
|
+
return left.length === right.length && left.every((entry, index) => entry === right[index]);
|
|
17
|
+
}
|
|
18
|
+
function sameRows(left, right) {
|
|
19
|
+
return left.length === right.length && left.every((option, index) => option === right[index]);
|
|
20
|
+
}
|
|
21
|
+
function sameState(left, right) {
|
|
22
|
+
return left.open === right.open && left.query === right.query && left.active === right.active && left.multiple === right.multiple && left.empty === right.empty && sameValues(left.value, right.value) && sameRows(left.visible, right.visible) && sameRows(left.selected, right.selected);
|
|
23
|
+
}
|
|
24
|
+
function createSelect(options = {}) {
|
|
25
|
+
let opts = { ...options };
|
|
26
|
+
let all = opts.options ?? [];
|
|
27
|
+
let value = toList(opts.value);
|
|
28
|
+
let open = false;
|
|
29
|
+
let query = "";
|
|
30
|
+
let active = -1;
|
|
31
|
+
let visible = [...all];
|
|
32
|
+
let typed = "";
|
|
33
|
+
let typedAt = 0;
|
|
34
|
+
let destroyed = false;
|
|
35
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
36
|
+
const engine = createSearch({
|
|
37
|
+
items: all,
|
|
38
|
+
keys: opts.searchKeys ?? SELECT_SEARCH_KEYS,
|
|
39
|
+
fuse: opts.fuse,
|
|
40
|
+
fuseOptions: opts.fuseOptions,
|
|
41
|
+
matcher: opts.matcher,
|
|
42
|
+
// A select filters as you type. There is nothing to debounce: the data is already
|
|
43
|
+
// in memory, and a delay here is felt as the list lagging behind the field.
|
|
44
|
+
debounce: 0,
|
|
45
|
+
// An empty query in a select means "everything", not "nothing" - the opposite of a
|
|
46
|
+
// search field, where an empty query has nothing to show.
|
|
47
|
+
empty: "all"
|
|
48
|
+
});
|
|
49
|
+
function optionOf(candidate) {
|
|
50
|
+
return all.find((option) => option.value === candidate);
|
|
51
|
+
}
|
|
52
|
+
function selectedOptions() {
|
|
53
|
+
return value.map(optionOf).filter((option) => Boolean(option));
|
|
54
|
+
}
|
|
55
|
+
function snapshot() {
|
|
56
|
+
return {
|
|
57
|
+
open,
|
|
58
|
+
query,
|
|
59
|
+
active,
|
|
60
|
+
visible,
|
|
61
|
+
selected: selectedOptions(),
|
|
62
|
+
value: [...value],
|
|
63
|
+
multiple: Boolean(opts.multiple),
|
|
64
|
+
empty: visible.length === 0
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function emit() {
|
|
68
|
+
const state = snapshot();
|
|
69
|
+
opts.onChange?.(state);
|
|
70
|
+
for (const listener of listeners) listener(state);
|
|
71
|
+
}
|
|
72
|
+
function wrap(index) {
|
|
73
|
+
const length = visible.length;
|
|
74
|
+
return (index % length + length) % length;
|
|
75
|
+
}
|
|
76
|
+
function enabledIndex(from, step) {
|
|
77
|
+
if (visible.length === 0) return -1;
|
|
78
|
+
for (let attempt = 0; attempt < visible.length; attempt++) {
|
|
79
|
+
const index = wrap(from + step * attempt);
|
|
80
|
+
if (!visible[index]?.disabled) return index;
|
|
81
|
+
}
|
|
82
|
+
return -1;
|
|
83
|
+
}
|
|
84
|
+
let lastActive;
|
|
85
|
+
function refilter() {
|
|
86
|
+
visible = opts.searchable && query.trim() ? engine.searchNow(query).map((match) => match.item) : [...all];
|
|
87
|
+
const current = active >= 0 ? visible.findIndex((option) => option === lastActive) : -1;
|
|
88
|
+
active = current >= 0 && !visible[current]?.disabled ? current : enabledIndex(0, 1);
|
|
89
|
+
lastActive = visible[active];
|
|
90
|
+
}
|
|
91
|
+
function reportValue() {
|
|
92
|
+
const chosen = selectedOptions();
|
|
93
|
+
opts.onValueChange?.(opts.multiple ? [...value] : value[0] ?? "", chosen);
|
|
94
|
+
}
|
|
95
|
+
function closesOnSelect() {
|
|
96
|
+
return opts.closeOnSelect ?? !opts.multiple;
|
|
97
|
+
}
|
|
98
|
+
refilter();
|
|
99
|
+
return {
|
|
100
|
+
get state() {
|
|
101
|
+
return snapshot();
|
|
102
|
+
},
|
|
103
|
+
setOpen(next) {
|
|
104
|
+
if (destroyed || open === next) return;
|
|
105
|
+
open = next;
|
|
106
|
+
if (open) {
|
|
107
|
+
query = "";
|
|
108
|
+
refilter();
|
|
109
|
+
const chosen = value[0] ? visible.findIndex((option) => option.value === value[0]) : -1;
|
|
110
|
+
active = chosen >= 0 && !visible[chosen]?.disabled ? chosen : enabledIndex(0, 1);
|
|
111
|
+
lastActive = visible[active];
|
|
112
|
+
} else {
|
|
113
|
+
query = "";
|
|
114
|
+
typed = "";
|
|
115
|
+
refilter();
|
|
116
|
+
}
|
|
117
|
+
emit();
|
|
118
|
+
},
|
|
119
|
+
toggleOpen() {
|
|
120
|
+
this.setOpen(!open);
|
|
121
|
+
},
|
|
122
|
+
setQuery(next) {
|
|
123
|
+
if (destroyed) return;
|
|
124
|
+
query = next;
|
|
125
|
+
refilter();
|
|
126
|
+
emit();
|
|
127
|
+
},
|
|
128
|
+
move(key) {
|
|
129
|
+
if (destroyed || visible.length === 0) return;
|
|
130
|
+
const from = active < 0 ? -1 : active;
|
|
131
|
+
switch (key) {
|
|
132
|
+
// Wrapping, because a select is a short list and the row after the last one
|
|
133
|
+
// is the first: clamping leaves the arrow key doing nothing, which reads as
|
|
134
|
+
// the panel having frozen.
|
|
135
|
+
case "ArrowDown":
|
|
136
|
+
active = enabledIndex(from + 1, 1);
|
|
137
|
+
break;
|
|
138
|
+
case "ArrowUp":
|
|
139
|
+
active = enabledIndex(from - 1 + visible.length, -1);
|
|
140
|
+
break;
|
|
141
|
+
case "Home":
|
|
142
|
+
active = enabledIndex(0, 1);
|
|
143
|
+
break;
|
|
144
|
+
case "End":
|
|
145
|
+
active = enabledIndex(visible.length - 1, -1);
|
|
146
|
+
break;
|
|
147
|
+
case "PageDown":
|
|
148
|
+
active = enabledIndex(Math.min(visible.length - 1, from + PAGE), 1);
|
|
149
|
+
break;
|
|
150
|
+
case "PageUp":
|
|
151
|
+
active = enabledIndex(Math.max(0, from - PAGE), -1);
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
lastActive = visible[active];
|
|
155
|
+
emit();
|
|
156
|
+
},
|
|
157
|
+
setActive(index) {
|
|
158
|
+
if (destroyed || index === active || index < 0 || index >= visible.length || visible[index]?.disabled) return;
|
|
159
|
+
active = index;
|
|
160
|
+
lastActive = visible[active];
|
|
161
|
+
emit();
|
|
162
|
+
},
|
|
163
|
+
select(next) {
|
|
164
|
+
if (destroyed) return;
|
|
165
|
+
const option = optionOf(next);
|
|
166
|
+
if (!option || option.disabled) return;
|
|
167
|
+
if (opts.multiple) {
|
|
168
|
+
value = value.includes(next) ? value.filter((entry) => entry !== next) : [...value, next];
|
|
169
|
+
} else {
|
|
170
|
+
value = [next];
|
|
171
|
+
}
|
|
172
|
+
reportValue();
|
|
173
|
+
if (closesOnSelect()) {
|
|
174
|
+
open = false;
|
|
175
|
+
query = "";
|
|
176
|
+
refilter();
|
|
177
|
+
}
|
|
178
|
+
emit();
|
|
179
|
+
},
|
|
180
|
+
selectActive() {
|
|
181
|
+
const option = visible[active];
|
|
182
|
+
if (option) this.select(option.value);
|
|
183
|
+
},
|
|
184
|
+
remove(next) {
|
|
185
|
+
if (destroyed || !value.includes(next)) return;
|
|
186
|
+
value = value.filter((entry) => entry !== next);
|
|
187
|
+
reportValue();
|
|
188
|
+
emit();
|
|
189
|
+
},
|
|
190
|
+
clear() {
|
|
191
|
+
if (destroyed || value.length === 0) return;
|
|
192
|
+
value = [];
|
|
193
|
+
reportValue();
|
|
194
|
+
emit();
|
|
195
|
+
},
|
|
196
|
+
typeahead(character) {
|
|
197
|
+
if (destroyed || character.length !== 1) return;
|
|
198
|
+
const step = typeaheadStep({ typed, at: typedAt }, character);
|
|
199
|
+
typed = step.typed;
|
|
200
|
+
typedAt = step.at;
|
|
201
|
+
const needle = fold(step.needle);
|
|
202
|
+
const from = step.cycle ? 1 : 0;
|
|
203
|
+
for (let step2 = from; step2 < visible.length + from; step2++) {
|
|
204
|
+
const index = wrap(Math.max(active, 0) + step2);
|
|
205
|
+
const option = visible[index];
|
|
206
|
+
if (!option || option.disabled) continue;
|
|
207
|
+
if (fold(option.label).startsWith(needle)) {
|
|
208
|
+
active = index;
|
|
209
|
+
lastActive = option;
|
|
210
|
+
emit();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
update(next) {
|
|
216
|
+
if (destroyed) return;
|
|
217
|
+
const before = snapshot();
|
|
218
|
+
const hadOptions = next.options !== void 0;
|
|
219
|
+
opts = { ...opts, ...next };
|
|
220
|
+
if (hadOptions) all = next.options ?? [];
|
|
221
|
+
if (next.value !== void 0) value = toList(next.value);
|
|
222
|
+
if (hadOptions || next.searchKeys || next.fuse || next.fuseOptions || next.matcher) {
|
|
223
|
+
engine.update({
|
|
224
|
+
items: all,
|
|
225
|
+
keys: opts.searchKeys ?? SELECT_SEARCH_KEYS,
|
|
226
|
+
fuse: opts.fuse,
|
|
227
|
+
fuseOptions: opts.fuseOptions,
|
|
228
|
+
matcher: opts.matcher
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
refilter();
|
|
232
|
+
if (!sameState(before, snapshot())) emit();
|
|
233
|
+
},
|
|
234
|
+
subscribe(listener) {
|
|
235
|
+
listeners.add(listener);
|
|
236
|
+
return () => {
|
|
237
|
+
listeners.delete(listener);
|
|
238
|
+
};
|
|
239
|
+
},
|
|
240
|
+
destroy() {
|
|
241
|
+
destroyed = true;
|
|
242
|
+
listeners.clear();
|
|
243
|
+
engine.destroy();
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export { SELECT_SEARCH_KEYS, createSelect };
|