@proyecto-viviana/solid-stately 0.2.3 → 0.2.7

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.
Files changed (82) hide show
  1. package/LICENSE +21 -0
  2. package/dist/autocomplete/createAutocompleteState.d.ts +2 -1
  3. package/dist/checkbox/createCheckboxGroupState.d.ts +10 -1
  4. package/dist/collections/types.d.ts +11 -0
  5. package/dist/color/getColorChannels.d.ts +20 -0
  6. package/dist/data/createAsyncList.d.ts +111 -0
  7. package/dist/data/createListData.d.ts +65 -0
  8. package/dist/data/createTreeData.d.ts +61 -0
  9. package/dist/data/index.d.ts +3 -0
  10. package/dist/datepicker/index.d.ts +10 -0
  11. package/dist/grid/types.d.ts +5 -1
  12. package/dist/index.d.ts +6 -1
  13. package/dist/index.js +3737 -2697
  14. package/dist/index.js.map +1 -7
  15. package/dist/menu/index.d.ts +8 -0
  16. package/dist/radio/createRadioGroupState.d.ts +10 -1
  17. package/dist/select/createSelectState.d.ts +17 -0
  18. package/dist/selection/index.d.ts +11 -0
  19. package/dist/toast/createToastState.d.ts +7 -1
  20. package/dist/toggle/createToggleGroupState.d.ts +45 -0
  21. package/dist/toggle/index.d.ts +1 -0
  22. package/dist/tree/TreeCollection.d.ts +3 -2
  23. package/package.json +6 -5
  24. package/src/autocomplete/createAutocompleteState.ts +10 -11
  25. package/src/calendar/createDateFieldState.ts +24 -1
  26. package/src/checkbox/createCheckboxGroupState.ts +42 -6
  27. package/src/collections/ListCollection.ts +152 -146
  28. package/src/collections/createListState.ts +266 -264
  29. package/src/collections/createMenuState.ts +106 -106
  30. package/src/collections/createSelectionState.ts +336 -336
  31. package/src/collections/index.ts +46 -46
  32. package/src/collections/types.ts +181 -169
  33. package/src/color/Color.ts +951 -951
  34. package/src/color/createColorAreaState.ts +293 -293
  35. package/src/color/createColorFieldState.ts +292 -292
  36. package/src/color/createColorSliderState.ts +241 -241
  37. package/src/color/createColorWheelState.ts +211 -211
  38. package/src/color/getColorChannels.ts +34 -0
  39. package/src/color/index.ts +47 -47
  40. package/src/color/types.ts +127 -127
  41. package/src/combobox/createComboBoxState.ts +703 -703
  42. package/src/combobox/index.ts +13 -13
  43. package/src/data/createAsyncList.ts +377 -0
  44. package/src/data/createListData.ts +298 -0
  45. package/src/data/createTreeData.ts +433 -0
  46. package/src/data/index.ts +25 -0
  47. package/src/datepicker/index.ts +36 -0
  48. package/src/disclosure/createDisclosureState.ts +4 -4
  49. package/src/dnd/createDragState.ts +153 -153
  50. package/src/dnd/createDraggableCollectionState.ts +165 -165
  51. package/src/dnd/createDropState.ts +212 -212
  52. package/src/dnd/createDroppableCollectionState.ts +357 -357
  53. package/src/dnd/index.ts +76 -76
  54. package/src/dnd/types.ts +317 -317
  55. package/src/form/createFormValidationState.ts +389 -389
  56. package/src/form/index.ts +15 -15
  57. package/src/grid/types.ts +5 -0
  58. package/src/index.ts +49 -0
  59. package/src/menu/index.ts +19 -0
  60. package/src/numberfield/createNumberFieldState.ts +427 -383
  61. package/src/numberfield/index.ts +5 -5
  62. package/src/overlays/createOverlayTriggerState.ts +67 -67
  63. package/src/overlays/index.ts +5 -5
  64. package/src/radio/createRadioGroupState.ts +44 -6
  65. package/src/searchfield/createSearchFieldState.ts +62 -62
  66. package/src/searchfield/index.ts +5 -5
  67. package/src/select/createSelectState.ts +290 -181
  68. package/src/select/index.ts +5 -5
  69. package/src/selection/index.ts +28 -0
  70. package/src/slider/createSliderState.ts +211 -211
  71. package/src/slider/index.ts +6 -6
  72. package/src/tabs/createTabListState.ts +37 -11
  73. package/src/toast/createToastState.d.ts +6 -1
  74. package/src/toast/createToastState.ts +8 -1
  75. package/src/toggle/createToggleGroupState.ts +127 -0
  76. package/src/toggle/index.ts +6 -0
  77. package/src/tooltip/createTooltipTriggerState.ts +183 -183
  78. package/src/tooltip/index.ts +6 -6
  79. package/src/tree/TreeCollection.ts +208 -175
  80. package/src/tree/createTreeState.ts +392 -392
  81. package/src/tree/index.ts +13 -13
  82. package/src/tree/types.ts +174 -174
@@ -1,13 +1,13 @@
1
- /**
2
- * ComboBox state management for Solid Stately.
3
- */
4
-
5
- export {
6
- createComboBoxState,
7
- defaultContainsFilter,
8
- type ComboBoxState,
9
- type ComboBoxStateProps,
10
- type FilterFn,
11
- type MenuTriggerAction,
12
- type FocusStrategy,
13
- } from './createComboBoxState';
1
+ /**
2
+ * ComboBox state management for Solid Stately.
3
+ */
4
+
5
+ export {
6
+ createComboBoxState,
7
+ defaultContainsFilter,
8
+ type ComboBoxState,
9
+ type ComboBoxStateProps,
10
+ type FilterFn,
11
+ type MenuTriggerAction,
12
+ type FocusStrategy,
13
+ } from './createComboBoxState';
@@ -0,0 +1,377 @@
1
+ /**
2
+ * createAsyncList - SolidJS port of React Spectrum's useAsyncList
3
+ *
4
+ * Manages state for an immutable async loaded list data structure, and provides
5
+ * convenience methods to update the data over time. Manages loading and error
6
+ * states, pagination, and sorting.
7
+ */
8
+
9
+ import { createSignal, untrack } from 'solid-js';
10
+
11
+ export type Key = string | number;
12
+ export type Selection = 'all' | Set<Key>;
13
+ export type LoadingState = 'idle' | 'loading' | 'loadingMore' | 'sorting' | 'filtering' | 'error';
14
+
15
+ export interface SortDescriptor {
16
+ /** The key of the column to sort by. */
17
+ column?: Key;
18
+ /** The direction to sort by. */
19
+ direction?: 'ascending' | 'descending';
20
+ }
21
+
22
+ export interface AsyncListLoadOptions<T, C> {
23
+ /** The items currently in the list. */
24
+ items: T[];
25
+ /** The keys of the currently selected items in the list. */
26
+ selectedKeys: Selection;
27
+ /** The current sort descriptor for the list. */
28
+ sortDescriptor?: SortDescriptor;
29
+ /** An abort signal used to notify the load function that the request has been aborted. */
30
+ signal: AbortSignal;
31
+ /** The pagination cursor returned from the last page load. */
32
+ cursor?: C;
33
+ /** The current filter text used to perform server side filtering. */
34
+ filterText?: string;
35
+ /** The current loading state of the list. */
36
+ loadingState?: LoadingState;
37
+ }
38
+
39
+ export interface AsyncListStateUpdate<T, C> {
40
+ /** The new items to append to the list. */
41
+ items: Iterable<T>;
42
+ /** The keys to add to the selection. */
43
+ selectedKeys?: Iterable<Key>;
44
+ /** The sort descriptor to set. */
45
+ sortDescriptor?: SortDescriptor;
46
+ /** The pagination cursor to be used for the next page load. */
47
+ cursor?: C;
48
+ /** The updated filter text for the list. */
49
+ filterText?: string;
50
+ }
51
+
52
+ export type AsyncListLoadFunction<T, C> = (
53
+ state: AsyncListLoadOptions<T, C>
54
+ ) => AsyncListStateUpdate<T, C> | Promise<AsyncListStateUpdate<T, C>>;
55
+
56
+ export interface AsyncListOptions<T, C> {
57
+ /** The keys for the initially selected items. */
58
+ initialSelectedKeys?: 'all' | Iterable<Key>;
59
+ /** The initial sort descriptor. */
60
+ initialSortDescriptor?: SortDescriptor;
61
+ /** The initial filter text. */
62
+ initialFilterText?: string;
63
+ /** A function that returns a unique key for an item object. */
64
+ getKey?: (item: T) => Key;
65
+ /** A function that loads the data for the items in the list. */
66
+ load: AsyncListLoadFunction<T, C>;
67
+ /** An optional function that performs sorting. */
68
+ sort?: AsyncListLoadFunction<T, C>;
69
+ }
70
+
71
+ export interface AsyncListData<T> {
72
+ /** The items in the list. */
73
+ readonly items: T[];
74
+ /** The keys of the currently selected items in the list. */
75
+ readonly selectedKeys: Selection;
76
+ /** The current sort descriptor for the list. */
77
+ readonly sortDescriptor: SortDescriptor | undefined;
78
+ /** Whether data is currently being loaded. */
79
+ readonly isLoading: boolean;
80
+ /** If loading data failed, then this contains the error that occurred. */
81
+ readonly error: Error | undefined;
82
+ /** The current filter text. */
83
+ readonly filterText: string;
84
+ /** The current loading state for the list. */
85
+ readonly loadingState: LoadingState;
86
+
87
+ /** Reloads the data in the list. */
88
+ reload(): void;
89
+ /** Loads the next page of data in the list. */
90
+ loadMore(): void;
91
+ /** Triggers sorting for the list. */
92
+ sort(descriptor: SortDescriptor): void;
93
+
94
+ /** Sets the selected keys. */
95
+ setSelectedKeys(keys: Selection): void;
96
+ /** Sets the filter text. */
97
+ setFilterText(filterText: string): void;
98
+
99
+ /** Gets an item from the list by key. */
100
+ getItem(key: Key): T | undefined;
101
+ /** Inserts items into the list at the given index. */
102
+ insert(index: number, ...values: T[]): void;
103
+ /** Inserts items before a given key. */
104
+ insertBefore(key: Key, ...values: T[]): void;
105
+ /** Inserts items after a given key. */
106
+ insertAfter(key: Key, ...values: T[]): void;
107
+ /** Appends items to the list. */
108
+ append(...values: T[]): void;
109
+ /** Prepends items to the list. */
110
+ prepend(...values: T[]): void;
111
+ /** Removes items from the list by their keys. */
112
+ remove(...keys: Key[]): void;
113
+ /** Removes all items from the list that are currently in the set of selected items. */
114
+ removeSelectedItems(): void;
115
+ /** Moves an item within the list. */
116
+ move(key: Key, toIndex: number): void;
117
+ /** Updates an item in the list. */
118
+ update(key: Key, newValue: T): void;
119
+ }
120
+
121
+ interface InternalState<T, C> {
122
+ loadingState: LoadingState;
123
+ items: T[];
124
+ selectedKeys: Selection;
125
+ sortDescriptor?: SortDescriptor;
126
+ error?: Error;
127
+ cursor?: C;
128
+ filterText: string;
129
+ abortController?: AbortController;
130
+ }
131
+
132
+ /**
133
+ * Manages state for an immutable async loaded list data structure, and provides
134
+ * convenience methods to update the data over time. Manages loading and error
135
+ * states, pagination, and sorting.
136
+ */
137
+ export function createAsyncList<T, C = string>(options: AsyncListOptions<T, C>): AsyncListData<T> {
138
+ const {
139
+ load,
140
+ sort: sortFn,
141
+ initialSelectedKeys,
142
+ initialSortDescriptor,
143
+ getKey = (item: any) => item.id || item.key,
144
+ initialFilterText = '',
145
+ } = options;
146
+
147
+ const [state, setState] = createSignal<InternalState<T, C>>({
148
+ loadingState: 'idle',
149
+ items: [],
150
+ selectedKeys: initialSelectedKeys === 'all' ? 'all' : new Set(initialSelectedKeys || []),
151
+ sortDescriptor: initialSortDescriptor,
152
+ filterText: initialFilterText,
153
+ });
154
+
155
+ async function dispatchFetch(
156
+ type: 'loading' | 'loadingMore' | 'sorting' | 'filtering',
157
+ fn: AsyncListLoadFunction<T, C>,
158
+ opts?: { sortDescriptor?: SortDescriptor; filterText?: string },
159
+ ) {
160
+ const abortController = new AbortController();
161
+ const currentState = untrack(state);
162
+
163
+ // Update to loading state
164
+ setState(s => {
165
+ // Abort previous request if one is in progress
166
+ if (s.abortController) {
167
+ s.abortController.abort();
168
+ }
169
+ return {
170
+ ...s,
171
+ loadingState: type,
172
+ items: type === 'loading' ? [] : s.items,
173
+ sortDescriptor: opts?.sortDescriptor ?? s.sortDescriptor,
174
+ filterText: opts?.filterText ?? s.filterText,
175
+ abortController,
176
+ };
177
+ });
178
+
179
+ try {
180
+ const previousFilterText = opts?.filterText ?? currentState.filterText;
181
+ const response = await fn({
182
+ items: currentState.items.slice(),
183
+ selectedKeys: currentState.selectedKeys,
184
+ sortDescriptor: opts?.sortDescriptor ?? currentState.sortDescriptor,
185
+ signal: abortController.signal,
186
+ cursor: type === 'loadingMore' ? currentState.cursor : undefined,
187
+ filterText: previousFilterText,
188
+ loadingState: currentState.loadingState,
189
+ });
190
+
191
+ setState(s => {
192
+ // Ignore stale response
193
+ if (s.abortController !== abortController) return s;
194
+
195
+ const newItems = [...(response.items ?? [])];
196
+ const items = type === 'loadingMore'
197
+ ? [...s.items, ...newItems]
198
+ : newItems;
199
+
200
+ let selectedKeys: Selection;
201
+ if (type === 'loadingMore') {
202
+ selectedKeys = (s.selectedKeys === 'all' || response.selectedKeys === 'all')
203
+ ? 'all'
204
+ : new Set([
205
+ ...(s.selectedKeys as Set<Key>),
206
+ ...(response.selectedKeys ?? []),
207
+ ]);
208
+ } else {
209
+ selectedKeys = response.selectedKeys
210
+ ? (response.selectedKeys === 'all' ? 'all' : new Set(response.selectedKeys))
211
+ : s.selectedKeys;
212
+ }
213
+
214
+ return {
215
+ ...s,
216
+ loadingState: 'idle',
217
+ items,
218
+ selectedKeys,
219
+ sortDescriptor: response.sortDescriptor ?? s.sortDescriptor,
220
+ cursor: response.cursor as C | undefined,
221
+ filterText: response.filterText ?? s.filterText,
222
+ error: undefined,
223
+ abortController: undefined,
224
+ };
225
+ });
226
+
227
+ // Trigger another filter if filterText changed in the response
228
+ const responseFilterText = response.filterText ?? previousFilterText;
229
+ if (responseFilterText && responseFilterText !== previousFilterText && !abortController.signal.aborted) {
230
+ dispatchFetch('filtering', load, { filterText: responseFilterText });
231
+ }
232
+ } catch (e) {
233
+ setState(s => {
234
+ if (s.abortController !== abortController) return s;
235
+ return {
236
+ ...s,
237
+ loadingState: 'error',
238
+ error: e as Error,
239
+ abortController: undefined,
240
+ };
241
+ });
242
+ }
243
+ }
244
+
245
+ // Trigger initial load immediately
246
+ dispatchFetch('loading', load);
247
+
248
+ return {
249
+ get items() { return state().items; },
250
+ get selectedKeys() { return state().selectedKeys; },
251
+ get sortDescriptor() { return state().sortDescriptor; },
252
+ get isLoading() {
253
+ const ls = state().loadingState;
254
+ return ls === 'loading' || ls === 'loadingMore' || ls === 'sorting' || ls === 'filtering';
255
+ },
256
+ get error() { return state().error; },
257
+ get filterText() { return state().filterText; },
258
+ get loadingState() { return state().loadingState; },
259
+
260
+ reload() {
261
+ dispatchFetch('loading', load);
262
+ },
263
+
264
+ loadMore() {
265
+ const s = state();
266
+ if (s.loadingState === 'loading' || s.loadingState === 'loadingMore' || s.loadingState === 'filtering' || s.cursor == null) {
267
+ return;
268
+ }
269
+ dispatchFetch('loadingMore', load);
270
+ },
271
+
272
+ sort(descriptor: SortDescriptor) {
273
+ dispatchFetch('sorting', (sortFn || load) as AsyncListLoadFunction<T, C>, { sortDescriptor: descriptor });
274
+ },
275
+
276
+ setSelectedKeys(keys: Selection) {
277
+ setState(s => ({ ...s, selectedKeys: keys }));
278
+ },
279
+
280
+ setFilterText(filterText: string) {
281
+ dispatchFetch('filtering', load, { filterText });
282
+ },
283
+
284
+ getItem(key: Key) {
285
+ return state().items.find(item => getKey(item) === key);
286
+ },
287
+
288
+ insert(index: number, ...values: T[]) {
289
+ setState(s => ({
290
+ ...s,
291
+ items: [...s.items.slice(0, index), ...values, ...s.items.slice(index)],
292
+ }));
293
+ },
294
+
295
+ insertBefore(key: Key, ...values: T[]) {
296
+ setState(s => {
297
+ const index = s.items.findIndex(item => getKey(item) === key);
298
+ if (index === -1) return s;
299
+ return {
300
+ ...s,
301
+ items: [...s.items.slice(0, index), ...values, ...s.items.slice(index)],
302
+ };
303
+ });
304
+ },
305
+
306
+ insertAfter(key: Key, ...values: T[]) {
307
+ setState(s => {
308
+ const index = s.items.findIndex(item => getKey(item) === key);
309
+ if (index === -1) return s;
310
+ return {
311
+ ...s,
312
+ items: [...s.items.slice(0, index + 1), ...values, ...s.items.slice(index + 1)],
313
+ };
314
+ });
315
+ },
316
+
317
+ append(...values: T[]) {
318
+ setState(s => ({ ...s, items: [...s.items, ...values] }));
319
+ },
320
+
321
+ prepend(...values: T[]) {
322
+ setState(s => ({ ...s, items: [...values, ...s.items] }));
323
+ },
324
+
325
+ remove(...keys: Key[]) {
326
+ setState(s => {
327
+ const keySet = new Set(keys);
328
+ const items = s.items.filter(item => !keySet.has(getKey(item)));
329
+ let selectedKeys = s.selectedKeys;
330
+ if (selectedKeys !== 'all') {
331
+ const newSelection = new Set(selectedKeys);
332
+ for (const key of keys) {
333
+ newSelection.delete(key);
334
+ }
335
+ selectedKeys = newSelection;
336
+ }
337
+ return { ...s, items, selectedKeys };
338
+ });
339
+ },
340
+
341
+ removeSelectedItems() {
342
+ setState(s => {
343
+ if (s.selectedKeys === 'all') {
344
+ return { ...s, items: [], selectedKeys: new Set() };
345
+ }
346
+ const sel = s.selectedKeys;
347
+ return {
348
+ ...s,
349
+ items: s.items.filter(item => !sel.has(getKey(item))),
350
+ selectedKeys: new Set(),
351
+ };
352
+ });
353
+ },
354
+
355
+ move(key: Key, toIndex: number) {
356
+ setState(s => {
357
+ const index = s.items.findIndex(item => getKey(item) === key);
358
+ if (index === -1) return s;
359
+ const copy = s.items.slice();
360
+ const [item] = copy.splice(index, 1);
361
+ copy.splice(toIndex, 0, item);
362
+ return { ...s, items: copy };
363
+ });
364
+ },
365
+
366
+ update(key: Key, newValue: T) {
367
+ setState(s => {
368
+ const index = s.items.findIndex(item => getKey(item) === key);
369
+ if (index === -1) return s;
370
+ return {
371
+ ...s,
372
+ items: [...s.items.slice(0, index), newValue, ...s.items.slice(index + 1)],
373
+ };
374
+ });
375
+ },
376
+ };
377
+ }
@@ -0,0 +1,298 @@
1
+ /**
2
+ * createListData - SolidJS port of React Spectrum's useListData
3
+ *
4
+ * Manages state for an immutable list data structure, and provides
5
+ * convenience methods to update the data over time.
6
+ */
7
+
8
+ import { createSignal, createMemo } from 'solid-js';
9
+
10
+ export type Key = string | number;
11
+ export type Selection = 'all' | Set<Key>;
12
+
13
+ export interface ListOptions<T> {
14
+ /** Initial items in the list. */
15
+ initialItems?: T[];
16
+ /** The keys for the initially selected items. */
17
+ initialSelectedKeys?: 'all' | Iterable<Key>;
18
+ /** The initial text to filter the list by. */
19
+ initialFilterText?: string;
20
+ /** A function that returns a unique key for an item object. */
21
+ getKey?: (item: T) => Key;
22
+ /** A function that returns whether an item matches the current filter text. */
23
+ filter?: (item: T, filterText: string) => boolean;
24
+ }
25
+
26
+ export interface ListData<T> {
27
+ /** The items in the list. */
28
+ readonly items: T[];
29
+ /** The keys of the currently selected items in the list. */
30
+ readonly selectedKeys: Selection;
31
+ /** The current filter text. */
32
+ readonly filterText: string;
33
+ /** Sets the selected keys. */
34
+ setSelectedKeys(keys: Selection): void;
35
+ /** Adds the given keys to the current selected keys. */
36
+ addKeysToSelection(keys: Selection): void;
37
+ /** Removes the given keys from the current selected keys. */
38
+ removeKeysFromSelection(keys: Selection): void;
39
+ /** Sets the filter text. */
40
+ setFilterText(filterText: string): void;
41
+ /** Gets an item from the list by key. */
42
+ getItem(key: Key): T | undefined;
43
+ /** Inserts items into the list at the given index. */
44
+ insert(index: number, ...values: T[]): void;
45
+ /** Inserts items into the list before the item at the given key. */
46
+ insertBefore(key: Key, ...values: T[]): void;
47
+ /** Inserts items into the list after the item at the given key. */
48
+ insertAfter(key: Key, ...values: T[]): void;
49
+ /** Appends items to the list. */
50
+ append(...values: T[]): void;
51
+ /** Prepends items to the list. */
52
+ prepend(...values: T[]): void;
53
+ /** Removes items from the list by their keys. */
54
+ remove(...keys: Key[]): void;
55
+ /** Removes all items from the list that are currently in the set of selected items. */
56
+ removeSelectedItems(): void;
57
+ /** Moves an item within the list. */
58
+ move(key: Key, toIndex: number): void;
59
+ /** Moves one or more items before a given key. */
60
+ moveBefore(key: Key, keys: Iterable<Key>): void;
61
+ /** Moves one or more items after a given key. */
62
+ moveAfter(key: Key, keys: Iterable<Key>): void;
63
+ /** Updates an item in the list. */
64
+ update(key: Key, newValue: T | ((prev: T) => T)): void;
65
+ }
66
+
67
+ interface ListState<T> {
68
+ items: T[];
69
+ selectedKeys: Selection;
70
+ filterText: string;
71
+ }
72
+
73
+ /**
74
+ * Manages state for an immutable list data structure, and provides
75
+ * convenience methods to update the data over time.
76
+ */
77
+ export function createListData<T>(options: ListOptions<T>): ListData<T> {
78
+ const {
79
+ initialItems = [],
80
+ initialSelectedKeys,
81
+ getKey = (item: any) => item.id ?? item.key,
82
+ filter,
83
+ initialFilterText = '',
84
+ } = options;
85
+
86
+ const [state, setState] = createSignal<ListState<T>>({
87
+ items: initialItems,
88
+ selectedKeys: initialSelectedKeys === 'all' ? 'all' : new Set(initialSelectedKeys || []),
89
+ filterText: initialFilterText,
90
+ });
91
+
92
+ const filteredItems = createMemo(() => {
93
+ const s = state();
94
+ return filter ? s.items.filter(item => filter(item, s.filterText)) : s.items;
95
+ });
96
+
97
+ return {
98
+ get items() { return filteredItems(); },
99
+ get selectedKeys() { return state().selectedKeys; },
100
+ get filterText() { return state().filterText; },
101
+
102
+ setSelectedKeys(selectedKeys: Selection) {
103
+ setState(s => ({ ...s, selectedKeys }));
104
+ },
105
+
106
+ addKeysToSelection(selectedKeys: Selection) {
107
+ setState(s => {
108
+ if (s.selectedKeys === 'all') return s;
109
+ if (selectedKeys === 'all') return { ...s, selectedKeys: 'all' };
110
+ return { ...s, selectedKeys: new Set([...s.selectedKeys, ...selectedKeys]) };
111
+ });
112
+ },
113
+
114
+ removeKeysFromSelection(selectedKeys: Selection) {
115
+ setState(s => {
116
+ if (selectedKeys === 'all') return { ...s, selectedKeys: new Set() };
117
+ const selection: Set<Key> = s.selectedKeys === 'all'
118
+ ? new Set(s.items.map(getKey))
119
+ : new Set(s.selectedKeys);
120
+ for (const key of selectedKeys) {
121
+ selection.delete(key);
122
+ }
123
+ return { ...s, selectedKeys: selection };
124
+ });
125
+ },
126
+
127
+ setFilterText(filterText: string) {
128
+ setState(s => ({ ...s, filterText }));
129
+ },
130
+
131
+ getItem(key: Key) {
132
+ return state().items.find(item => getKey(item) === key);
133
+ },
134
+
135
+ insert(index: number, ...values: T[]) {
136
+ setState(s => insertItems(s, index, ...values));
137
+ },
138
+
139
+ insertBefore(key: Key, ...values: T[]) {
140
+ setState(s => {
141
+ let index = s.items.findIndex(item => getKey(item) === key);
142
+ if (index === -1) {
143
+ index = s.items.length === 0 ? 0 : -1;
144
+ if (index === -1) return s;
145
+ }
146
+ return insertItems(s, index, ...values);
147
+ });
148
+ },
149
+
150
+ insertAfter(key: Key, ...values: T[]) {
151
+ setState(s => {
152
+ let index = s.items.findIndex(item => getKey(item) === key);
153
+ if (index === -1) {
154
+ index = s.items.length === 0 ? -1 : -1;
155
+ if (index === -1 && s.items.length > 0) return s;
156
+ if (s.items.length === 0) return insertItems(s, 0, ...values);
157
+ }
158
+ return insertItems(s, index + 1, ...values);
159
+ });
160
+ },
161
+
162
+ prepend(...values: T[]) {
163
+ setState(s => insertItems(s, 0, ...values));
164
+ },
165
+
166
+ append(...values: T[]) {
167
+ setState(s => insertItems(s, s.items.length, ...values));
168
+ },
169
+
170
+ remove(...keys: Key[]) {
171
+ setState(s => {
172
+ const keySet = new Set(keys);
173
+ const items = s.items.filter(item => !keySet.has(getKey(item)));
174
+ let selection: Selection = 'all';
175
+ if (s.selectedKeys !== 'all') {
176
+ selection = new Set(s.selectedKeys);
177
+ for (const key of keys) {
178
+ (selection as Set<Key>).delete(key);
179
+ }
180
+ }
181
+ if (items.length === 0) selection = new Set();
182
+ return { ...s, items, selectedKeys: selection };
183
+ });
184
+ },
185
+
186
+ removeSelectedItems() {
187
+ setState(s => {
188
+ if (s.selectedKeys === 'all') {
189
+ return { ...s, items: [], selectedKeys: new Set() };
190
+ }
191
+ const selectedKeys = s.selectedKeys;
192
+ const items = s.items.filter(item => !selectedKeys.has(getKey(item)));
193
+ return { ...s, items, selectedKeys: new Set() };
194
+ });
195
+ },
196
+
197
+ move(key: Key, toIndex: number) {
198
+ setState(s => {
199
+ const index = s.items.findIndex(item => getKey(item) === key);
200
+ if (index === -1) return s;
201
+ const copy = s.items.slice();
202
+ const [item] = copy.splice(index, 1);
203
+ copy.splice(toIndex, 0, item);
204
+ return { ...s, items: copy };
205
+ });
206
+ },
207
+
208
+ moveBefore(key: Key, keys: Iterable<Key>) {
209
+ setState(s => {
210
+ const toIndex = s.items.findIndex(item => getKey(item) === key);
211
+ if (toIndex === -1) return s;
212
+ const keyArray = Array.isArray(keys) ? keys : [...keys];
213
+ const indices = keyArray
214
+ .map(k => s.items.findIndex(item => getKey(item) === k))
215
+ .sort((a, b) => a - b);
216
+ return moveItems(s, indices, toIndex);
217
+ });
218
+ },
219
+
220
+ moveAfter(key: Key, keys: Iterable<Key>) {
221
+ setState(s => {
222
+ const toIndex = s.items.findIndex(item => getKey(item) === key);
223
+ if (toIndex === -1) return s;
224
+ const keyArray = Array.isArray(keys) ? keys : [...keys];
225
+ const indices = keyArray
226
+ .map(k => s.items.findIndex(item => getKey(item) === k))
227
+ .sort((a, b) => a - b);
228
+ return moveItems(s, indices, toIndex + 1);
229
+ });
230
+ },
231
+
232
+ update(key: Key, newValue: T | ((prev: T) => T)) {
233
+ setState(s => {
234
+ const index = s.items.findIndex(item => getKey(item) === key);
235
+ if (index === -1) return s;
236
+ const updatedValue = typeof newValue === 'function'
237
+ ? (newValue as (prev: T) => T)(s.items[index])
238
+ : newValue;
239
+ return {
240
+ ...s,
241
+ items: [
242
+ ...s.items.slice(0, index),
243
+ updatedValue,
244
+ ...s.items.slice(index + 1),
245
+ ],
246
+ };
247
+ });
248
+ },
249
+ };
250
+ }
251
+
252
+ function insertItems<T>(state: ListState<T>, index: number, ...values: T[]): ListState<T> {
253
+ return {
254
+ ...state,
255
+ items: [
256
+ ...state.items.slice(0, index),
257
+ ...values,
258
+ ...state.items.slice(index),
259
+ ],
260
+ };
261
+ }
262
+
263
+ function moveItems<T>(state: ListState<T>, indices: number[], toIndex: number): ListState<T> {
264
+ toIndex -= indices.filter(index => index < toIndex).length;
265
+
266
+ const moves = indices.map(from => ({
267
+ from,
268
+ to: toIndex++,
269
+ }));
270
+
271
+ for (let i = 0; i < moves.length; i++) {
272
+ const a = moves[i].from;
273
+ for (let j = i; j < moves.length; j++) {
274
+ const b = moves[j].from;
275
+ if (b > a) moves[j].from--;
276
+ }
277
+ }
278
+
279
+ for (let i = 0; i < moves.length; i++) {
280
+ const a = moves[i];
281
+ for (let j = moves.length - 1; j > i; j--) {
282
+ const b = moves[j];
283
+ if (b.from < a.to) {
284
+ a.to++;
285
+ } else {
286
+ b.from++;
287
+ }
288
+ }
289
+ }
290
+
291
+ const copy = state.items.slice();
292
+ for (const m of moves) {
293
+ const [item] = copy.splice(m.from, 1);
294
+ copy.splice(m.to, 0, item);
295
+ }
296
+
297
+ return { ...state, items: copy };
298
+ }