exsorted-react 1.0.3
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/LICENSE +21 -0
- package/README.md +173 -0
- package/dist/index.cjs +286 -0
- package/dist/index.d.cts +222 -0
- package/dist/index.d.ts +222 -0
- package/dist/index.js +266 -0
- package/package.json +88 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ink01101011
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# exsorted-react
|
|
2
|
+
|
|
3
|
+
TypeScript-first React sorting hook powered by [exsorted](https://www.npmjs.com/package/exsorted).
|
|
4
|
+
|
|
5
|
+
- React support: 18 and 19
|
|
6
|
+
- Public API: useSortedList, singleKeyAccessors, and related public types
|
|
7
|
+
- Exsorted resolution: uses peer version when available, otherwise falls back to bundled dependency
|
|
8
|
+
|
|
9
|
+
## API Surface
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { singleKeyAccessors, useSortedList } from "exsorted-react";
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## singleKeyAccessors
|
|
16
|
+
|
|
17
|
+
Minimal helper for one-field sorting while keeping accessors required.
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
const sorted = useSortedList(products, {
|
|
21
|
+
accessors: singleKeyAccessors("price", (item: Product) => item.price),
|
|
22
|
+
initialKey: "price",
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## useSortedList
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
const result = useSortedList(items, options);
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Next.js SSR / RSC
|
|
33
|
+
|
|
34
|
+
`useSortedList` is a client hook. In Next.js App Router, call it only in Client Components.
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
"use client";
|
|
38
|
+
|
|
39
|
+
import { useSortedList } from "exsorted-react";
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`singleKeyAccessors` is a pure helper and can be called in server code, but it does not make
|
|
43
|
+
`useSortedList` callable in Server Components.
|
|
44
|
+
|
|
45
|
+
If external read-only state provides a key that is not present in `accessors`, the hook safely falls
|
|
46
|
+
back to the first accessor key to avoid error-triggered re-render loops.
|
|
47
|
+
|
|
48
|
+
### Modes
|
|
49
|
+
|
|
50
|
+
- Standalone mode: pass accessors with optional initialKey and initialDirection
|
|
51
|
+
- Controlled mode: pass sort controller object
|
|
52
|
+
- Read-only state mode: pass state object
|
|
53
|
+
|
|
54
|
+
### Options
|
|
55
|
+
|
|
56
|
+
- accessors (required): map of key to value accessor
|
|
57
|
+
- comparator (optional): comparator used for sorting
|
|
58
|
+
- sorter (optional): custom compare-based exsorted sorter, default is mergeSort
|
|
59
|
+
- initialKey and initialDirection (standalone mode only)
|
|
60
|
+
- sort (controlled mode only)
|
|
61
|
+
- state (read-only state mode only)
|
|
62
|
+
|
|
63
|
+
### Sorter Compatibility
|
|
64
|
+
|
|
65
|
+
sorter must match compare-based signature:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
(arr, compareFn?) => arr;
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Not supported because function interfaces differ:
|
|
72
|
+
|
|
73
|
+
- exsorted/non-compare: countingSort, radixSort, bucketSort, pigeonholeSort
|
|
74
|
+
- exsorted/standard: introSort
|
|
75
|
+
|
|
76
|
+
### Return Value
|
|
77
|
+
|
|
78
|
+
- items: visible items (sorted or original depending on mode/actions)
|
|
79
|
+
- previousItems: previous visible snapshot
|
|
80
|
+
- originalItems: input source reference
|
|
81
|
+
- isSorting: transition pending state
|
|
82
|
+
- isSorted: true when sorted view is active and not pending
|
|
83
|
+
- sort, sortKey, direction: effective sort state
|
|
84
|
+
- setSortKey, setDirection, toggleDirection, setSort, reset: sort controls
|
|
85
|
+
- restoreOriginal, restoreSorted: view-mode controls
|
|
86
|
+
|
|
87
|
+
## Examples
|
|
88
|
+
|
|
89
|
+
### Standalone Mode
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { useSortedList } from "exsorted-react";
|
|
93
|
+
import { quickSort } from "exsorted";
|
|
94
|
+
|
|
95
|
+
type Product = {
|
|
96
|
+
id: string;
|
|
97
|
+
name: string;
|
|
98
|
+
price: number;
|
|
99
|
+
rating: number;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const sorted = useSortedList(products, {
|
|
103
|
+
accessors: {
|
|
104
|
+
name: (item: Product) => item.name,
|
|
105
|
+
price: (item: Product) => item.price,
|
|
106
|
+
rating: (item: Product) => item.rating,
|
|
107
|
+
},
|
|
108
|
+
sorter: quickSort,
|
|
109
|
+
initialKey: "price",
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
sorted.setSortKey("name");
|
|
113
|
+
sorted.toggleDirection();
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Single-key Minimal Mode
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import { singleKeyAccessors, useSortedList } from "exsorted-react";
|
|
120
|
+
|
|
121
|
+
const sorted = useSortedList(products, {
|
|
122
|
+
accessors: singleKeyAccessors("price", (item: Product) => item.price),
|
|
123
|
+
initialKey: "price",
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Controlled Mode
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
import { useState } from "react";
|
|
131
|
+
import { useSortedList } from "exsorted-react";
|
|
132
|
+
|
|
133
|
+
const [sort, setSort] = useState({ key: "name" as const, direction: "asc" as const });
|
|
134
|
+
|
|
135
|
+
const sorted = useSortedList(products, {
|
|
136
|
+
accessors: {
|
|
137
|
+
name: (item: Product) => item.name,
|
|
138
|
+
price: (item: Product) => item.price,
|
|
139
|
+
},
|
|
140
|
+
sort: {
|
|
141
|
+
sort,
|
|
142
|
+
setSort,
|
|
143
|
+
reset: () => setSort({ key: "name", direction: "asc" }),
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Read-only State Mode
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
const sorted = useSortedList(products, {
|
|
152
|
+
accessors: {
|
|
153
|
+
name: (item: Product) => item.name,
|
|
154
|
+
price: (item: Product) => item.price,
|
|
155
|
+
},
|
|
156
|
+
state: { key: "price", direction: "desc" },
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Custom Comparator
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
const sorted = useSortedList(products, {
|
|
164
|
+
accessors: {
|
|
165
|
+
name: (item: Product) => item.name,
|
|
166
|
+
price: (item: Product) => item.price,
|
|
167
|
+
},
|
|
168
|
+
initialKey: "name",
|
|
169
|
+
comparator: (a, b) => a.name.localeCompare(b.name, "en", { sensitivity: "base" }),
|
|
170
|
+
});
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Tree-shaking note: import and pass only the sorter algorithm you need.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
singleKeyAccessors: () => singleKeyAccessors,
|
|
24
|
+
useSortedList: () => useSortedList
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
|
|
28
|
+
// src/hooks/useSortedList.ts
|
|
29
|
+
var import_react2 = require("react");
|
|
30
|
+
|
|
31
|
+
// src/hooks/useSortState.ts
|
|
32
|
+
var import_react = require("react");
|
|
33
|
+
var defaultGetNextDirection = (current) => current === "asc" ? "desc" : "asc";
|
|
34
|
+
var resolveInitialKey = (options) => {
|
|
35
|
+
if ("keys" in options) {
|
|
36
|
+
if (options.initialKey !== void 0) {
|
|
37
|
+
return options.initialKey;
|
|
38
|
+
}
|
|
39
|
+
const firstKey = options.keys[0];
|
|
40
|
+
if (firstKey === void 0) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
"useSortState expected at least one key in options.keys when initialKey is omitted."
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return firstKey;
|
|
46
|
+
}
|
|
47
|
+
return options.initialKey;
|
|
48
|
+
};
|
|
49
|
+
function useSortState(options) {
|
|
50
|
+
const initialDirection = options.initialDirection ?? "asc";
|
|
51
|
+
const initialKey = (0, import_react.useMemo)(() => resolveInitialKey(options), [options]);
|
|
52
|
+
const initialSort = (0, import_react.useMemo)(
|
|
53
|
+
() => ({ key: initialKey, direction: initialDirection }),
|
|
54
|
+
[initialDirection, initialKey]
|
|
55
|
+
);
|
|
56
|
+
const [sort, setSort] = (0, import_react.useState)(initialSort);
|
|
57
|
+
const setSortKey = (0, import_react.useCallback)((nextKey) => {
|
|
58
|
+
setSort((previous) => ({ ...previous, key: nextKey }));
|
|
59
|
+
}, []);
|
|
60
|
+
const setDirection = (0, import_react.useCallback)((nextDirection) => {
|
|
61
|
+
setSort((previous) => ({ ...previous, direction: nextDirection }));
|
|
62
|
+
}, []);
|
|
63
|
+
const toggleDirection = (0, import_react.useCallback)(() => {
|
|
64
|
+
const getNextDirection = options.getNextDirection ?? defaultGetNextDirection;
|
|
65
|
+
setSort((previous) => ({
|
|
66
|
+
...previous,
|
|
67
|
+
direction: getNextDirection(previous.direction)
|
|
68
|
+
}));
|
|
69
|
+
}, [options]);
|
|
70
|
+
const reset = (0, import_react.useCallback)(() => {
|
|
71
|
+
setSort(initialSort);
|
|
72
|
+
}, [initialSort]);
|
|
73
|
+
return {
|
|
74
|
+
sort,
|
|
75
|
+
sortKey: sort.key,
|
|
76
|
+
direction: sort.direction,
|
|
77
|
+
setSortKey,
|
|
78
|
+
setDirection,
|
|
79
|
+
toggleDirection,
|
|
80
|
+
setSort,
|
|
81
|
+
reset
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/utils/useSortedList.utils.ts
|
|
86
|
+
var import_merge = require("exsorted/merge");
|
|
87
|
+
var collator = new Intl.Collator(void 0, {
|
|
88
|
+
numeric: true,
|
|
89
|
+
sensitivity: "base"
|
|
90
|
+
});
|
|
91
|
+
var comparePrimitiveValues = (left, right) => {
|
|
92
|
+
if (left === right) {
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
if (left == null) {
|
|
96
|
+
return 1;
|
|
97
|
+
}
|
|
98
|
+
if (right == null) {
|
|
99
|
+
return -1;
|
|
100
|
+
}
|
|
101
|
+
if (left instanceof Date && right instanceof Date) {
|
|
102
|
+
return left.getTime() - right.getTime();
|
|
103
|
+
}
|
|
104
|
+
if (typeof left === "boolean" && typeof right === "boolean") {
|
|
105
|
+
return Number(left) - Number(right);
|
|
106
|
+
}
|
|
107
|
+
if (typeof left === "number" && typeof right === "number") {
|
|
108
|
+
return left - right;
|
|
109
|
+
}
|
|
110
|
+
return collator.compare(String(left), String(right));
|
|
111
|
+
};
|
|
112
|
+
var withDirection = (comparison, direction) => direction === "asc" ? comparison : -comparison;
|
|
113
|
+
var singleKeyAccessors = (key, accessor) => ({ [key]: accessor });
|
|
114
|
+
var toggleSortDirection = (direction) => direction === "asc" ? "desc" : "asc";
|
|
115
|
+
var buildComparator = (accessor, state, customComparator) => {
|
|
116
|
+
const compareBase = customComparator ?? ((left, right) => comparePrimitiveValues(accessor(left), accessor(right)));
|
|
117
|
+
return (left, right) => withDirection(compareBase(left, right), state.direction);
|
|
118
|
+
};
|
|
119
|
+
var sortList = (items, state, accessor, comparator, sorter) => {
|
|
120
|
+
if (!accessor) {
|
|
121
|
+
return [...items];
|
|
122
|
+
}
|
|
123
|
+
const selectedSorter = sorter ?? import_merge.mergeSort;
|
|
124
|
+
const directionalComparator = buildComparator(accessor, state, comparator);
|
|
125
|
+
return selectedSorter([...items], directionalComparator);
|
|
126
|
+
};
|
|
127
|
+
var resolveDefaultKey = (accessors) => {
|
|
128
|
+
const firstKey = Object.keys(accessors)[0];
|
|
129
|
+
if (firstKey === void 0) {
|
|
130
|
+
throw new Error("useSortedList expected at least one accessor key.");
|
|
131
|
+
}
|
|
132
|
+
return firstKey;
|
|
133
|
+
};
|
|
134
|
+
var resolveStandaloneInitialDirection = (options) => {
|
|
135
|
+
if ("initialDirection" in options && options.initialDirection !== void 0) {
|
|
136
|
+
return options.initialDirection;
|
|
137
|
+
}
|
|
138
|
+
return "asc";
|
|
139
|
+
};
|
|
140
|
+
var resolveStandaloneInitialKey = (options, fallbackKey) => {
|
|
141
|
+
if ("initialKey" in options && options.initialKey !== void 0) {
|
|
142
|
+
return options.initialKey;
|
|
143
|
+
}
|
|
144
|
+
return fallbackKey;
|
|
145
|
+
};
|
|
146
|
+
var resolveAccessorWithFallback = (accessors, key, fallbackKey) => {
|
|
147
|
+
const directAccessor = accessors[key];
|
|
148
|
+
if (directAccessor !== void 0) {
|
|
149
|
+
return { accessor: directAccessor, resolvedKey: key };
|
|
150
|
+
}
|
|
151
|
+
return { accessor: accessors[fallbackKey], resolvedKey: fallbackKey };
|
|
152
|
+
};
|
|
153
|
+
var resolveExternalController = (options) => "sort" in options ? options.sort : void 0;
|
|
154
|
+
var resolveExternalState = (options) => "state" in options ? options.state : void 0;
|
|
155
|
+
var areArraysShallowEqual = (left, right) => {
|
|
156
|
+
if (left.length !== right.length) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
160
|
+
if (!Object.is(left[index], right[index])) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return true;
|
|
165
|
+
};
|
|
166
|
+
var resolveVisibleItems = (params) => {
|
|
167
|
+
const { isOriginalMode, sourceItems, isPending, previousItems, sortedItems } = params;
|
|
168
|
+
if (isOriginalMode) {
|
|
169
|
+
return [...sourceItems];
|
|
170
|
+
}
|
|
171
|
+
if (isPending) {
|
|
172
|
+
return previousItems;
|
|
173
|
+
}
|
|
174
|
+
return sortedItems;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// src/hooks/useSortedList.ts
|
|
178
|
+
function useSortedList(items, options) {
|
|
179
|
+
const { accessors, comparator, sorter } = options;
|
|
180
|
+
const [isPending, startTransition] = (0, import_react2.useTransition)();
|
|
181
|
+
const defaultKey = (0, import_react2.useMemo)(() => resolveDefaultKey(accessors), [accessors]);
|
|
182
|
+
const externalSortController = resolveExternalController(options);
|
|
183
|
+
const externalState = resolveExternalState(options);
|
|
184
|
+
const standaloneInitialDirection = resolveStandaloneInitialDirection(options);
|
|
185
|
+
const standaloneInitialKey = resolveStandaloneInitialKey(options, defaultKey);
|
|
186
|
+
const localSortController = useSortState({
|
|
187
|
+
initialKey: standaloneInitialKey,
|
|
188
|
+
initialDirection: standaloneInitialDirection
|
|
189
|
+
});
|
|
190
|
+
const activeSortController = externalSortController ?? (externalState === void 0 ? localSortController : void 0);
|
|
191
|
+
const activeSort = externalState ?? activeSortController?.sort ?? localSortController.sort;
|
|
192
|
+
const setSort = (0, import_react2.useCallback)(
|
|
193
|
+
(next) => {
|
|
194
|
+
activeSortController?.setSort(next);
|
|
195
|
+
},
|
|
196
|
+
[activeSortController]
|
|
197
|
+
);
|
|
198
|
+
const setSortKey = (0, import_react2.useCallback)(
|
|
199
|
+
(nextKey) => {
|
|
200
|
+
setSort((previous) => ({ ...previous, key: nextKey }));
|
|
201
|
+
},
|
|
202
|
+
[setSort]
|
|
203
|
+
);
|
|
204
|
+
const setDirection = (0, import_react2.useCallback)(
|
|
205
|
+
(nextDirection) => {
|
|
206
|
+
setSort((previous) => ({ ...previous, direction: nextDirection }));
|
|
207
|
+
},
|
|
208
|
+
[setSort]
|
|
209
|
+
);
|
|
210
|
+
const toggleDirection = (0, import_react2.useCallback)(() => {
|
|
211
|
+
setSort((previous) => ({
|
|
212
|
+
...previous,
|
|
213
|
+
direction: toggleSortDirection(previous.direction)
|
|
214
|
+
}));
|
|
215
|
+
}, [setSort]);
|
|
216
|
+
const reset = (0, import_react2.useCallback)(() => {
|
|
217
|
+
activeSortController?.reset();
|
|
218
|
+
}, [activeSortController]);
|
|
219
|
+
const deferredItems = (0, import_react2.useDeferredValue)(items);
|
|
220
|
+
const deferredState = (0, import_react2.useDeferredValue)(activeSort);
|
|
221
|
+
const effectiveItems = deferredItems;
|
|
222
|
+
const effectiveState = deferredState;
|
|
223
|
+
const effectiveComparator = comparator;
|
|
224
|
+
const { accessor, resolvedKey } = resolveAccessorWithFallback(
|
|
225
|
+
accessors,
|
|
226
|
+
effectiveState.key,
|
|
227
|
+
defaultKey
|
|
228
|
+
);
|
|
229
|
+
const effectiveSort = resolvedKey === effectiveState.key ? effectiveState : { ...effectiveState, key: resolvedKey };
|
|
230
|
+
const nextSorted = (0, import_react2.useMemo)(
|
|
231
|
+
() => sortList(effectiveItems, effectiveSort, accessor, effectiveComparator, sorter),
|
|
232
|
+
[accessor, effectiveComparator, effectiveItems, effectiveSort, sorter]
|
|
233
|
+
);
|
|
234
|
+
const [sorted, setSorted] = (0, import_react2.useState)(nextSorted);
|
|
235
|
+
const [isOriginalMode, setIsOriginalMode] = (0, import_react2.useState)(false);
|
|
236
|
+
const previousItemsRef = (0, import_react2.useRef)(nextSorted);
|
|
237
|
+
const restoreOriginal = (0, import_react2.useCallback)(() => {
|
|
238
|
+
setIsOriginalMode(true);
|
|
239
|
+
}, []);
|
|
240
|
+
const restoreSorted = (0, import_react2.useCallback)(() => {
|
|
241
|
+
setIsOriginalMode(false);
|
|
242
|
+
}, []);
|
|
243
|
+
(0, import_react2.useEffect)(() => {
|
|
244
|
+
if (areArraysShallowEqual(sorted, nextSorted)) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
startTransition(() => {
|
|
248
|
+
setSorted((current) => {
|
|
249
|
+
if (areArraysShallowEqual(current, nextSorted)) {
|
|
250
|
+
return current;
|
|
251
|
+
}
|
|
252
|
+
previousItemsRef.current = current;
|
|
253
|
+
return nextSorted;
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
}, [nextSorted, sorted, startTransition]);
|
|
257
|
+
const visibleItems = resolveVisibleItems({
|
|
258
|
+
isOriginalMode,
|
|
259
|
+
sourceItems: items,
|
|
260
|
+
isPending,
|
|
261
|
+
previousItems: previousItemsRef.current,
|
|
262
|
+
sortedItems: sorted
|
|
263
|
+
});
|
|
264
|
+
return {
|
|
265
|
+
items: visibleItems,
|
|
266
|
+
previousItems: previousItemsRef.current,
|
|
267
|
+
originalItems: items,
|
|
268
|
+
isSorting: isPending,
|
|
269
|
+
isSorted: !isPending && !isOriginalMode,
|
|
270
|
+
sort: activeSort,
|
|
271
|
+
sortKey: activeSort.key,
|
|
272
|
+
direction: activeSort.direction,
|
|
273
|
+
setSortKey,
|
|
274
|
+
setDirection,
|
|
275
|
+
toggleDirection,
|
|
276
|
+
setSort,
|
|
277
|
+
reset,
|
|
278
|
+
restoreOriginal,
|
|
279
|
+
restoreSorted
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
283
|
+
0 && (module.exports = {
|
|
284
|
+
singleKeyAccessors,
|
|
285
|
+
useSortedList
|
|
286
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { CompareFn } from 'exsorted/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Direction used when applying a sort operation.
|
|
5
|
+
*/
|
|
6
|
+
type SortDirection = "asc" | "desc";
|
|
7
|
+
/**
|
|
8
|
+
* Primitive values supported by the default comparator.
|
|
9
|
+
*/
|
|
10
|
+
type SortPrimitive$1 = string | number | boolean | Date | null | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Current sort state containing the active key and direction.
|
|
13
|
+
*
|
|
14
|
+
* @template TKey key union for sortable fields
|
|
15
|
+
*/
|
|
16
|
+
interface SortState<TKey extends PropertyKey = string> {
|
|
17
|
+
key: TKey;
|
|
18
|
+
direction: SortDirection;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Map of sort keys to accessor functions.
|
|
22
|
+
*
|
|
23
|
+
* Each accessor extracts a comparable primitive from an item.
|
|
24
|
+
*
|
|
25
|
+
* @template TItem item type in the source list
|
|
26
|
+
* @template TKey union of valid sort keys
|
|
27
|
+
*/
|
|
28
|
+
type SortAccessors<TItem, TKey extends PropertyKey> = Record<TKey, (item: TItem) => SortPrimitive$1>;
|
|
29
|
+
/**
|
|
30
|
+
* External controller contract for controlled sorting mode.
|
|
31
|
+
*
|
|
32
|
+
* @template TKey key union for sortable fields
|
|
33
|
+
*/
|
|
34
|
+
interface SortController<TKey extends PropertyKey> {
|
|
35
|
+
sort: SortState<TKey>;
|
|
36
|
+
setSort: (next: SortState<TKey> | ((previous: SortState<TKey>) => SortState<TKey>)) => void;
|
|
37
|
+
reset: () => void;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Exsorted-compatible sorting function signature.
|
|
41
|
+
*
|
|
42
|
+
* Accepts only strict comparator-based sorters.
|
|
43
|
+
*
|
|
44
|
+
* Excluded intentionally:
|
|
45
|
+
* - non-compare family from `exsorted/non-compare`:
|
|
46
|
+
* `countingSort`, `radixSort`, `bucketSort`, `pigeonholeSort`
|
|
47
|
+
* - `introSort` from `exsorted/standard` (overload includes threshold variants)
|
|
48
|
+
*
|
|
49
|
+
* The trailing `...extra: never[]` prevents passing sorters with additional
|
|
50
|
+
* non-compatible parameters.
|
|
51
|
+
*/
|
|
52
|
+
type ExsortedSorter<TItem> = (arr: TItem[], compareFn?: CompareFn<TItem>, ...extra: never[]) => TItem[];
|
|
53
|
+
/**
|
|
54
|
+
* Configuration object for useSortedList.
|
|
55
|
+
*
|
|
56
|
+
* Supports three modes:
|
|
57
|
+
* - standalone mode: no external state/controller, optional initialKey/initialDirection
|
|
58
|
+
* - controller mode: provide `sort` to fully control state and actions
|
|
59
|
+
* - state mode: provide `state` for read-only external state observation
|
|
60
|
+
*
|
|
61
|
+
* @template TItem item type in the source list
|
|
62
|
+
* @template TKey union of valid sort keys
|
|
63
|
+
*/
|
|
64
|
+
type UseSortedListOptions<TItem, TKey extends PropertyKey> = {
|
|
65
|
+
/**
|
|
66
|
+
* Accessors used to derive sortable values from each item.
|
|
67
|
+
*/
|
|
68
|
+
accessors: SortAccessors<TItem, TKey>;
|
|
69
|
+
/**
|
|
70
|
+
* Custom comparator used for all sort keys.
|
|
71
|
+
*/
|
|
72
|
+
comparator?: CompareFn<TItem>;
|
|
73
|
+
/**
|
|
74
|
+
* Custom Exsorted sorter (defaults to mergeSort in the hook).
|
|
75
|
+
*/
|
|
76
|
+
sorter?: ExsortedSorter<TItem>;
|
|
77
|
+
} & ({
|
|
78
|
+
/**
|
|
79
|
+
* Controlled mode: external controller with state + mutators.
|
|
80
|
+
*/
|
|
81
|
+
sort: SortController<TKey>;
|
|
82
|
+
state?: never;
|
|
83
|
+
initialKey?: never;
|
|
84
|
+
initialDirection?: never;
|
|
85
|
+
} | {
|
|
86
|
+
/**
|
|
87
|
+
* Read-only external state mode.
|
|
88
|
+
*/
|
|
89
|
+
state: SortState<TKey>;
|
|
90
|
+
sort?: never;
|
|
91
|
+
initialKey?: never;
|
|
92
|
+
initialDirection?: never;
|
|
93
|
+
} | {
|
|
94
|
+
sort?: never;
|
|
95
|
+
state?: never;
|
|
96
|
+
/**
|
|
97
|
+
* Standalone mode initial key.
|
|
98
|
+
*
|
|
99
|
+
* If omitted, the first accessor key is used.
|
|
100
|
+
*/
|
|
101
|
+
initialKey?: TKey;
|
|
102
|
+
/**
|
|
103
|
+
* Standalone mode initial direction.
|
|
104
|
+
*
|
|
105
|
+
* Defaults to `asc`.
|
|
106
|
+
*/
|
|
107
|
+
initialDirection?: SortDirection;
|
|
108
|
+
});
|
|
109
|
+
/**
|
|
110
|
+
* Return shape for useSortedList.
|
|
111
|
+
*
|
|
112
|
+
* @template TItem item type in the source list
|
|
113
|
+
* @template TKey union of valid sort keys
|
|
114
|
+
*/
|
|
115
|
+
interface UseSortedListResult<TItem, TKey extends PropertyKey = string> {
|
|
116
|
+
/**
|
|
117
|
+
* Current visible list, which may be sorted or original depending on mode/actions.
|
|
118
|
+
*/
|
|
119
|
+
items: TItem[];
|
|
120
|
+
/**
|
|
121
|
+
* Last visible list snapshot prior to the most recent sorted update.
|
|
122
|
+
*/
|
|
123
|
+
previousItems: TItem[];
|
|
124
|
+
/**
|
|
125
|
+
* Original source list reference from hook input.
|
|
126
|
+
*/
|
|
127
|
+
originalItems: readonly TItem[];
|
|
128
|
+
/**
|
|
129
|
+
* True while a deferred transition is applying a new sorted result.
|
|
130
|
+
*/
|
|
131
|
+
isSorting: boolean;
|
|
132
|
+
/**
|
|
133
|
+
* True when sorted view is active and no transition is pending.
|
|
134
|
+
*/
|
|
135
|
+
isSorted: boolean;
|
|
136
|
+
/**
|
|
137
|
+
* Current effective sort state.
|
|
138
|
+
*/
|
|
139
|
+
sort: SortState<TKey>;
|
|
140
|
+
/**
|
|
141
|
+
* Shortcut to `sort.key`.
|
|
142
|
+
*/
|
|
143
|
+
sortKey: TKey;
|
|
144
|
+
/**
|
|
145
|
+
* Shortcut to `sort.direction`.
|
|
146
|
+
*/
|
|
147
|
+
direction: SortDirection;
|
|
148
|
+
/**
|
|
149
|
+
* Updates only the active sort key.
|
|
150
|
+
*/
|
|
151
|
+
setSortKey: (nextKey: TKey) => void;
|
|
152
|
+
/**
|
|
153
|
+
* Updates only the active sort direction.
|
|
154
|
+
*/
|
|
155
|
+
setDirection: (nextDirection: SortDirection) => void;
|
|
156
|
+
/**
|
|
157
|
+
* Toggles direction between `asc` and `desc`.
|
|
158
|
+
*/
|
|
159
|
+
toggleDirection: () => void;
|
|
160
|
+
/**
|
|
161
|
+
* Replaces sort state directly or via an updater callback.
|
|
162
|
+
*/
|
|
163
|
+
setSort: (next: SortState<TKey> | ((previous: SortState<TKey>) => SortState<TKey>)) => void;
|
|
164
|
+
/**
|
|
165
|
+
* Resets sorting state to the initial standalone/controller state.
|
|
166
|
+
*/
|
|
167
|
+
reset: () => void;
|
|
168
|
+
/**
|
|
169
|
+
* Shows original input order in `items`.
|
|
170
|
+
*/
|
|
171
|
+
restoreOriginal: () => void;
|
|
172
|
+
/**
|
|
173
|
+
* Switches back to sorted view in `items`.
|
|
174
|
+
*/
|
|
175
|
+
restoreSorted: () => void;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Internal primitive union used by accessor and comparator helpers.
|
|
180
|
+
*/
|
|
181
|
+
type SortPrimitive = string | number | boolean | Date | null | undefined;
|
|
182
|
+
/**
|
|
183
|
+
* Internal accessor map used to infer key unions for hook APIs.
|
|
184
|
+
*/
|
|
185
|
+
type SortAccessorRecord<TItem> = Record<string, (item: TItem) => SortPrimitive>;
|
|
186
|
+
/**
|
|
187
|
+
* Extracts string keys from accessor maps for stable public API typing.
|
|
188
|
+
*/
|
|
189
|
+
type SortKey<TAccessors extends Record<string, (...args: any[]) => unknown>> = Extract<keyof TAccessors, string>;
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Sorts a readonly list using Exsorted with stable, non-mutating behavior.
|
|
193
|
+
*
|
|
194
|
+
* Supports 3 modes:
|
|
195
|
+
* - standalone mode: pass initialKey/initialDirection (or neither) and the hook owns sort state
|
|
196
|
+
* - controller mode: pass sort controller from outside for fully controlled state/actions
|
|
197
|
+
* - state mode: pass state only for read-only external state (actions become no-ops)
|
|
198
|
+
*
|
|
199
|
+
* @template TItem item type in the source list
|
|
200
|
+
* @template TAccessors accessor map used for sort keys and inferred key union
|
|
201
|
+
* @param items source list (never mutated)
|
|
202
|
+
* @param options sorting config, accessors, and state mode controls
|
|
203
|
+
* @returns visible items, previous/original snapshots, status flags, and sort actions
|
|
204
|
+
* @example
|
|
205
|
+
* const sorted = useSortedList(products, {
|
|
206
|
+
* accessors: {
|
|
207
|
+
* name: (item) => item.name,
|
|
208
|
+
* price: (item) => item.price,
|
|
209
|
+
* },
|
|
210
|
+
* initialKey: "price",
|
|
211
|
+
* });
|
|
212
|
+
*/
|
|
213
|
+
declare function useSortedList<TItem, const TAccessors extends SortAccessorRecord<TItem>>(items: readonly TItem[], options: UseSortedListOptions<TItem, SortKey<TAccessors>> & {
|
|
214
|
+
accessors: TAccessors;
|
|
215
|
+
}): UseSortedListResult<TItem, SortKey<TAccessors>>;
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Builds a single-key accessor map for simpler one-field sorting cases.
|
|
219
|
+
*/
|
|
220
|
+
declare const singleKeyAccessors: <TItem, const TKey extends string>(key: TKey, accessor: (item: TItem) => SortPrimitive$1) => Record<TKey, (item: TItem) => SortPrimitive$1>;
|
|
221
|
+
|
|
222
|
+
export { type ExsortedSorter, type SortAccessors, type SortController, type SortDirection, type SortPrimitive$1 as SortPrimitive, type SortState, type UseSortedListOptions, type UseSortedListResult, singleKeyAccessors, useSortedList };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { CompareFn } from 'exsorted/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Direction used when applying a sort operation.
|
|
5
|
+
*/
|
|
6
|
+
type SortDirection = "asc" | "desc";
|
|
7
|
+
/**
|
|
8
|
+
* Primitive values supported by the default comparator.
|
|
9
|
+
*/
|
|
10
|
+
type SortPrimitive$1 = string | number | boolean | Date | null | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Current sort state containing the active key and direction.
|
|
13
|
+
*
|
|
14
|
+
* @template TKey key union for sortable fields
|
|
15
|
+
*/
|
|
16
|
+
interface SortState<TKey extends PropertyKey = string> {
|
|
17
|
+
key: TKey;
|
|
18
|
+
direction: SortDirection;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Map of sort keys to accessor functions.
|
|
22
|
+
*
|
|
23
|
+
* Each accessor extracts a comparable primitive from an item.
|
|
24
|
+
*
|
|
25
|
+
* @template TItem item type in the source list
|
|
26
|
+
* @template TKey union of valid sort keys
|
|
27
|
+
*/
|
|
28
|
+
type SortAccessors<TItem, TKey extends PropertyKey> = Record<TKey, (item: TItem) => SortPrimitive$1>;
|
|
29
|
+
/**
|
|
30
|
+
* External controller contract for controlled sorting mode.
|
|
31
|
+
*
|
|
32
|
+
* @template TKey key union for sortable fields
|
|
33
|
+
*/
|
|
34
|
+
interface SortController<TKey extends PropertyKey> {
|
|
35
|
+
sort: SortState<TKey>;
|
|
36
|
+
setSort: (next: SortState<TKey> | ((previous: SortState<TKey>) => SortState<TKey>)) => void;
|
|
37
|
+
reset: () => void;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Exsorted-compatible sorting function signature.
|
|
41
|
+
*
|
|
42
|
+
* Accepts only strict comparator-based sorters.
|
|
43
|
+
*
|
|
44
|
+
* Excluded intentionally:
|
|
45
|
+
* - non-compare family from `exsorted/non-compare`:
|
|
46
|
+
* `countingSort`, `radixSort`, `bucketSort`, `pigeonholeSort`
|
|
47
|
+
* - `introSort` from `exsorted/standard` (overload includes threshold variants)
|
|
48
|
+
*
|
|
49
|
+
* The trailing `...extra: never[]` prevents passing sorters with additional
|
|
50
|
+
* non-compatible parameters.
|
|
51
|
+
*/
|
|
52
|
+
type ExsortedSorter<TItem> = (arr: TItem[], compareFn?: CompareFn<TItem>, ...extra: never[]) => TItem[];
|
|
53
|
+
/**
|
|
54
|
+
* Configuration object for useSortedList.
|
|
55
|
+
*
|
|
56
|
+
* Supports three modes:
|
|
57
|
+
* - standalone mode: no external state/controller, optional initialKey/initialDirection
|
|
58
|
+
* - controller mode: provide `sort` to fully control state and actions
|
|
59
|
+
* - state mode: provide `state` for read-only external state observation
|
|
60
|
+
*
|
|
61
|
+
* @template TItem item type in the source list
|
|
62
|
+
* @template TKey union of valid sort keys
|
|
63
|
+
*/
|
|
64
|
+
type UseSortedListOptions<TItem, TKey extends PropertyKey> = {
|
|
65
|
+
/**
|
|
66
|
+
* Accessors used to derive sortable values from each item.
|
|
67
|
+
*/
|
|
68
|
+
accessors: SortAccessors<TItem, TKey>;
|
|
69
|
+
/**
|
|
70
|
+
* Custom comparator used for all sort keys.
|
|
71
|
+
*/
|
|
72
|
+
comparator?: CompareFn<TItem>;
|
|
73
|
+
/**
|
|
74
|
+
* Custom Exsorted sorter (defaults to mergeSort in the hook).
|
|
75
|
+
*/
|
|
76
|
+
sorter?: ExsortedSorter<TItem>;
|
|
77
|
+
} & ({
|
|
78
|
+
/**
|
|
79
|
+
* Controlled mode: external controller with state + mutators.
|
|
80
|
+
*/
|
|
81
|
+
sort: SortController<TKey>;
|
|
82
|
+
state?: never;
|
|
83
|
+
initialKey?: never;
|
|
84
|
+
initialDirection?: never;
|
|
85
|
+
} | {
|
|
86
|
+
/**
|
|
87
|
+
* Read-only external state mode.
|
|
88
|
+
*/
|
|
89
|
+
state: SortState<TKey>;
|
|
90
|
+
sort?: never;
|
|
91
|
+
initialKey?: never;
|
|
92
|
+
initialDirection?: never;
|
|
93
|
+
} | {
|
|
94
|
+
sort?: never;
|
|
95
|
+
state?: never;
|
|
96
|
+
/**
|
|
97
|
+
* Standalone mode initial key.
|
|
98
|
+
*
|
|
99
|
+
* If omitted, the first accessor key is used.
|
|
100
|
+
*/
|
|
101
|
+
initialKey?: TKey;
|
|
102
|
+
/**
|
|
103
|
+
* Standalone mode initial direction.
|
|
104
|
+
*
|
|
105
|
+
* Defaults to `asc`.
|
|
106
|
+
*/
|
|
107
|
+
initialDirection?: SortDirection;
|
|
108
|
+
});
|
|
109
|
+
/**
|
|
110
|
+
* Return shape for useSortedList.
|
|
111
|
+
*
|
|
112
|
+
* @template TItem item type in the source list
|
|
113
|
+
* @template TKey union of valid sort keys
|
|
114
|
+
*/
|
|
115
|
+
interface UseSortedListResult<TItem, TKey extends PropertyKey = string> {
|
|
116
|
+
/**
|
|
117
|
+
* Current visible list, which may be sorted or original depending on mode/actions.
|
|
118
|
+
*/
|
|
119
|
+
items: TItem[];
|
|
120
|
+
/**
|
|
121
|
+
* Last visible list snapshot prior to the most recent sorted update.
|
|
122
|
+
*/
|
|
123
|
+
previousItems: TItem[];
|
|
124
|
+
/**
|
|
125
|
+
* Original source list reference from hook input.
|
|
126
|
+
*/
|
|
127
|
+
originalItems: readonly TItem[];
|
|
128
|
+
/**
|
|
129
|
+
* True while a deferred transition is applying a new sorted result.
|
|
130
|
+
*/
|
|
131
|
+
isSorting: boolean;
|
|
132
|
+
/**
|
|
133
|
+
* True when sorted view is active and no transition is pending.
|
|
134
|
+
*/
|
|
135
|
+
isSorted: boolean;
|
|
136
|
+
/**
|
|
137
|
+
* Current effective sort state.
|
|
138
|
+
*/
|
|
139
|
+
sort: SortState<TKey>;
|
|
140
|
+
/**
|
|
141
|
+
* Shortcut to `sort.key`.
|
|
142
|
+
*/
|
|
143
|
+
sortKey: TKey;
|
|
144
|
+
/**
|
|
145
|
+
* Shortcut to `sort.direction`.
|
|
146
|
+
*/
|
|
147
|
+
direction: SortDirection;
|
|
148
|
+
/**
|
|
149
|
+
* Updates only the active sort key.
|
|
150
|
+
*/
|
|
151
|
+
setSortKey: (nextKey: TKey) => void;
|
|
152
|
+
/**
|
|
153
|
+
* Updates only the active sort direction.
|
|
154
|
+
*/
|
|
155
|
+
setDirection: (nextDirection: SortDirection) => void;
|
|
156
|
+
/**
|
|
157
|
+
* Toggles direction between `asc` and `desc`.
|
|
158
|
+
*/
|
|
159
|
+
toggleDirection: () => void;
|
|
160
|
+
/**
|
|
161
|
+
* Replaces sort state directly or via an updater callback.
|
|
162
|
+
*/
|
|
163
|
+
setSort: (next: SortState<TKey> | ((previous: SortState<TKey>) => SortState<TKey>)) => void;
|
|
164
|
+
/**
|
|
165
|
+
* Resets sorting state to the initial standalone/controller state.
|
|
166
|
+
*/
|
|
167
|
+
reset: () => void;
|
|
168
|
+
/**
|
|
169
|
+
* Shows original input order in `items`.
|
|
170
|
+
*/
|
|
171
|
+
restoreOriginal: () => void;
|
|
172
|
+
/**
|
|
173
|
+
* Switches back to sorted view in `items`.
|
|
174
|
+
*/
|
|
175
|
+
restoreSorted: () => void;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Internal primitive union used by accessor and comparator helpers.
|
|
180
|
+
*/
|
|
181
|
+
type SortPrimitive = string | number | boolean | Date | null | undefined;
|
|
182
|
+
/**
|
|
183
|
+
* Internal accessor map used to infer key unions for hook APIs.
|
|
184
|
+
*/
|
|
185
|
+
type SortAccessorRecord<TItem> = Record<string, (item: TItem) => SortPrimitive>;
|
|
186
|
+
/**
|
|
187
|
+
* Extracts string keys from accessor maps for stable public API typing.
|
|
188
|
+
*/
|
|
189
|
+
type SortKey<TAccessors extends Record<string, (...args: any[]) => unknown>> = Extract<keyof TAccessors, string>;
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Sorts a readonly list using Exsorted with stable, non-mutating behavior.
|
|
193
|
+
*
|
|
194
|
+
* Supports 3 modes:
|
|
195
|
+
* - standalone mode: pass initialKey/initialDirection (or neither) and the hook owns sort state
|
|
196
|
+
* - controller mode: pass sort controller from outside for fully controlled state/actions
|
|
197
|
+
* - state mode: pass state only for read-only external state (actions become no-ops)
|
|
198
|
+
*
|
|
199
|
+
* @template TItem item type in the source list
|
|
200
|
+
* @template TAccessors accessor map used for sort keys and inferred key union
|
|
201
|
+
* @param items source list (never mutated)
|
|
202
|
+
* @param options sorting config, accessors, and state mode controls
|
|
203
|
+
* @returns visible items, previous/original snapshots, status flags, and sort actions
|
|
204
|
+
* @example
|
|
205
|
+
* const sorted = useSortedList(products, {
|
|
206
|
+
* accessors: {
|
|
207
|
+
* name: (item) => item.name,
|
|
208
|
+
* price: (item) => item.price,
|
|
209
|
+
* },
|
|
210
|
+
* initialKey: "price",
|
|
211
|
+
* });
|
|
212
|
+
*/
|
|
213
|
+
declare function useSortedList<TItem, const TAccessors extends SortAccessorRecord<TItem>>(items: readonly TItem[], options: UseSortedListOptions<TItem, SortKey<TAccessors>> & {
|
|
214
|
+
accessors: TAccessors;
|
|
215
|
+
}): UseSortedListResult<TItem, SortKey<TAccessors>>;
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Builds a single-key accessor map for simpler one-field sorting cases.
|
|
219
|
+
*/
|
|
220
|
+
declare const singleKeyAccessors: <TItem, const TKey extends string>(key: TKey, accessor: (item: TItem) => SortPrimitive$1) => Record<TKey, (item: TItem) => SortPrimitive$1>;
|
|
221
|
+
|
|
222
|
+
export { type ExsortedSorter, type SortAccessors, type SortController, type SortDirection, type SortPrimitive$1 as SortPrimitive, type SortState, type UseSortedListOptions, type UseSortedListResult, singleKeyAccessors, useSortedList };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// src/hooks/useSortedList.ts
|
|
2
|
+
import {
|
|
3
|
+
useCallback as useCallback2,
|
|
4
|
+
useDeferredValue,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo as useMemo2,
|
|
7
|
+
useRef,
|
|
8
|
+
useState as useState2,
|
|
9
|
+
useTransition
|
|
10
|
+
} from "react";
|
|
11
|
+
|
|
12
|
+
// src/hooks/useSortState.ts
|
|
13
|
+
import { useCallback, useMemo, useState } from "react";
|
|
14
|
+
var defaultGetNextDirection = (current) => current === "asc" ? "desc" : "asc";
|
|
15
|
+
var resolveInitialKey = (options) => {
|
|
16
|
+
if ("keys" in options) {
|
|
17
|
+
if (options.initialKey !== void 0) {
|
|
18
|
+
return options.initialKey;
|
|
19
|
+
}
|
|
20
|
+
const firstKey = options.keys[0];
|
|
21
|
+
if (firstKey === void 0) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
"useSortState expected at least one key in options.keys when initialKey is omitted."
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
return firstKey;
|
|
27
|
+
}
|
|
28
|
+
return options.initialKey;
|
|
29
|
+
};
|
|
30
|
+
function useSortState(options) {
|
|
31
|
+
const initialDirection = options.initialDirection ?? "asc";
|
|
32
|
+
const initialKey = useMemo(() => resolveInitialKey(options), [options]);
|
|
33
|
+
const initialSort = useMemo(
|
|
34
|
+
() => ({ key: initialKey, direction: initialDirection }),
|
|
35
|
+
[initialDirection, initialKey]
|
|
36
|
+
);
|
|
37
|
+
const [sort, setSort] = useState(initialSort);
|
|
38
|
+
const setSortKey = useCallback((nextKey) => {
|
|
39
|
+
setSort((previous) => ({ ...previous, key: nextKey }));
|
|
40
|
+
}, []);
|
|
41
|
+
const setDirection = useCallback((nextDirection) => {
|
|
42
|
+
setSort((previous) => ({ ...previous, direction: nextDirection }));
|
|
43
|
+
}, []);
|
|
44
|
+
const toggleDirection = useCallback(() => {
|
|
45
|
+
const getNextDirection = options.getNextDirection ?? defaultGetNextDirection;
|
|
46
|
+
setSort((previous) => ({
|
|
47
|
+
...previous,
|
|
48
|
+
direction: getNextDirection(previous.direction)
|
|
49
|
+
}));
|
|
50
|
+
}, [options]);
|
|
51
|
+
const reset = useCallback(() => {
|
|
52
|
+
setSort(initialSort);
|
|
53
|
+
}, [initialSort]);
|
|
54
|
+
return {
|
|
55
|
+
sort,
|
|
56
|
+
sortKey: sort.key,
|
|
57
|
+
direction: sort.direction,
|
|
58
|
+
setSortKey,
|
|
59
|
+
setDirection,
|
|
60
|
+
toggleDirection,
|
|
61
|
+
setSort,
|
|
62
|
+
reset
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/utils/useSortedList.utils.ts
|
|
67
|
+
import { mergeSort } from "exsorted/merge";
|
|
68
|
+
var collator = new Intl.Collator(void 0, {
|
|
69
|
+
numeric: true,
|
|
70
|
+
sensitivity: "base"
|
|
71
|
+
});
|
|
72
|
+
var comparePrimitiveValues = (left, right) => {
|
|
73
|
+
if (left === right) {
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
if (left == null) {
|
|
77
|
+
return 1;
|
|
78
|
+
}
|
|
79
|
+
if (right == null) {
|
|
80
|
+
return -1;
|
|
81
|
+
}
|
|
82
|
+
if (left instanceof Date && right instanceof Date) {
|
|
83
|
+
return left.getTime() - right.getTime();
|
|
84
|
+
}
|
|
85
|
+
if (typeof left === "boolean" && typeof right === "boolean") {
|
|
86
|
+
return Number(left) - Number(right);
|
|
87
|
+
}
|
|
88
|
+
if (typeof left === "number" && typeof right === "number") {
|
|
89
|
+
return left - right;
|
|
90
|
+
}
|
|
91
|
+
return collator.compare(String(left), String(right));
|
|
92
|
+
};
|
|
93
|
+
var withDirection = (comparison, direction) => direction === "asc" ? comparison : -comparison;
|
|
94
|
+
var singleKeyAccessors = (key, accessor) => ({ [key]: accessor });
|
|
95
|
+
var toggleSortDirection = (direction) => direction === "asc" ? "desc" : "asc";
|
|
96
|
+
var buildComparator = (accessor, state, customComparator) => {
|
|
97
|
+
const compareBase = customComparator ?? ((left, right) => comparePrimitiveValues(accessor(left), accessor(right)));
|
|
98
|
+
return (left, right) => withDirection(compareBase(left, right), state.direction);
|
|
99
|
+
};
|
|
100
|
+
var sortList = (items, state, accessor, comparator, sorter) => {
|
|
101
|
+
if (!accessor) {
|
|
102
|
+
return [...items];
|
|
103
|
+
}
|
|
104
|
+
const selectedSorter = sorter ?? mergeSort;
|
|
105
|
+
const directionalComparator = buildComparator(accessor, state, comparator);
|
|
106
|
+
return selectedSorter([...items], directionalComparator);
|
|
107
|
+
};
|
|
108
|
+
var resolveDefaultKey = (accessors) => {
|
|
109
|
+
const firstKey = Object.keys(accessors)[0];
|
|
110
|
+
if (firstKey === void 0) {
|
|
111
|
+
throw new Error("useSortedList expected at least one accessor key.");
|
|
112
|
+
}
|
|
113
|
+
return firstKey;
|
|
114
|
+
};
|
|
115
|
+
var resolveStandaloneInitialDirection = (options) => {
|
|
116
|
+
if ("initialDirection" in options && options.initialDirection !== void 0) {
|
|
117
|
+
return options.initialDirection;
|
|
118
|
+
}
|
|
119
|
+
return "asc";
|
|
120
|
+
};
|
|
121
|
+
var resolveStandaloneInitialKey = (options, fallbackKey) => {
|
|
122
|
+
if ("initialKey" in options && options.initialKey !== void 0) {
|
|
123
|
+
return options.initialKey;
|
|
124
|
+
}
|
|
125
|
+
return fallbackKey;
|
|
126
|
+
};
|
|
127
|
+
var resolveAccessorWithFallback = (accessors, key, fallbackKey) => {
|
|
128
|
+
const directAccessor = accessors[key];
|
|
129
|
+
if (directAccessor !== void 0) {
|
|
130
|
+
return { accessor: directAccessor, resolvedKey: key };
|
|
131
|
+
}
|
|
132
|
+
return { accessor: accessors[fallbackKey], resolvedKey: fallbackKey };
|
|
133
|
+
};
|
|
134
|
+
var resolveExternalController = (options) => "sort" in options ? options.sort : void 0;
|
|
135
|
+
var resolveExternalState = (options) => "state" in options ? options.state : void 0;
|
|
136
|
+
var areArraysShallowEqual = (left, right) => {
|
|
137
|
+
if (left.length !== right.length) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
141
|
+
if (!Object.is(left[index], right[index])) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return true;
|
|
146
|
+
};
|
|
147
|
+
var resolveVisibleItems = (params) => {
|
|
148
|
+
const { isOriginalMode, sourceItems, isPending, previousItems, sortedItems } = params;
|
|
149
|
+
if (isOriginalMode) {
|
|
150
|
+
return [...sourceItems];
|
|
151
|
+
}
|
|
152
|
+
if (isPending) {
|
|
153
|
+
return previousItems;
|
|
154
|
+
}
|
|
155
|
+
return sortedItems;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// src/hooks/useSortedList.ts
|
|
159
|
+
function useSortedList(items, options) {
|
|
160
|
+
const { accessors, comparator, sorter } = options;
|
|
161
|
+
const [isPending, startTransition] = useTransition();
|
|
162
|
+
const defaultKey = useMemo2(() => resolveDefaultKey(accessors), [accessors]);
|
|
163
|
+
const externalSortController = resolveExternalController(options);
|
|
164
|
+
const externalState = resolveExternalState(options);
|
|
165
|
+
const standaloneInitialDirection = resolveStandaloneInitialDirection(options);
|
|
166
|
+
const standaloneInitialKey = resolveStandaloneInitialKey(options, defaultKey);
|
|
167
|
+
const localSortController = useSortState({
|
|
168
|
+
initialKey: standaloneInitialKey,
|
|
169
|
+
initialDirection: standaloneInitialDirection
|
|
170
|
+
});
|
|
171
|
+
const activeSortController = externalSortController ?? (externalState === void 0 ? localSortController : void 0);
|
|
172
|
+
const activeSort = externalState ?? activeSortController?.sort ?? localSortController.sort;
|
|
173
|
+
const setSort = useCallback2(
|
|
174
|
+
(next) => {
|
|
175
|
+
activeSortController?.setSort(next);
|
|
176
|
+
},
|
|
177
|
+
[activeSortController]
|
|
178
|
+
);
|
|
179
|
+
const setSortKey = useCallback2(
|
|
180
|
+
(nextKey) => {
|
|
181
|
+
setSort((previous) => ({ ...previous, key: nextKey }));
|
|
182
|
+
},
|
|
183
|
+
[setSort]
|
|
184
|
+
);
|
|
185
|
+
const setDirection = useCallback2(
|
|
186
|
+
(nextDirection) => {
|
|
187
|
+
setSort((previous) => ({ ...previous, direction: nextDirection }));
|
|
188
|
+
},
|
|
189
|
+
[setSort]
|
|
190
|
+
);
|
|
191
|
+
const toggleDirection = useCallback2(() => {
|
|
192
|
+
setSort((previous) => ({
|
|
193
|
+
...previous,
|
|
194
|
+
direction: toggleSortDirection(previous.direction)
|
|
195
|
+
}));
|
|
196
|
+
}, [setSort]);
|
|
197
|
+
const reset = useCallback2(() => {
|
|
198
|
+
activeSortController?.reset();
|
|
199
|
+
}, [activeSortController]);
|
|
200
|
+
const deferredItems = useDeferredValue(items);
|
|
201
|
+
const deferredState = useDeferredValue(activeSort);
|
|
202
|
+
const effectiveItems = deferredItems;
|
|
203
|
+
const effectiveState = deferredState;
|
|
204
|
+
const effectiveComparator = comparator;
|
|
205
|
+
const { accessor, resolvedKey } = resolveAccessorWithFallback(
|
|
206
|
+
accessors,
|
|
207
|
+
effectiveState.key,
|
|
208
|
+
defaultKey
|
|
209
|
+
);
|
|
210
|
+
const effectiveSort = resolvedKey === effectiveState.key ? effectiveState : { ...effectiveState, key: resolvedKey };
|
|
211
|
+
const nextSorted = useMemo2(
|
|
212
|
+
() => sortList(effectiveItems, effectiveSort, accessor, effectiveComparator, sorter),
|
|
213
|
+
[accessor, effectiveComparator, effectiveItems, effectiveSort, sorter]
|
|
214
|
+
);
|
|
215
|
+
const [sorted, setSorted] = useState2(nextSorted);
|
|
216
|
+
const [isOriginalMode, setIsOriginalMode] = useState2(false);
|
|
217
|
+
const previousItemsRef = useRef(nextSorted);
|
|
218
|
+
const restoreOriginal = useCallback2(() => {
|
|
219
|
+
setIsOriginalMode(true);
|
|
220
|
+
}, []);
|
|
221
|
+
const restoreSorted = useCallback2(() => {
|
|
222
|
+
setIsOriginalMode(false);
|
|
223
|
+
}, []);
|
|
224
|
+
useEffect(() => {
|
|
225
|
+
if (areArraysShallowEqual(sorted, nextSorted)) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
startTransition(() => {
|
|
229
|
+
setSorted((current) => {
|
|
230
|
+
if (areArraysShallowEqual(current, nextSorted)) {
|
|
231
|
+
return current;
|
|
232
|
+
}
|
|
233
|
+
previousItemsRef.current = current;
|
|
234
|
+
return nextSorted;
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
}, [nextSorted, sorted, startTransition]);
|
|
238
|
+
const visibleItems = resolveVisibleItems({
|
|
239
|
+
isOriginalMode,
|
|
240
|
+
sourceItems: items,
|
|
241
|
+
isPending,
|
|
242
|
+
previousItems: previousItemsRef.current,
|
|
243
|
+
sortedItems: sorted
|
|
244
|
+
});
|
|
245
|
+
return {
|
|
246
|
+
items: visibleItems,
|
|
247
|
+
previousItems: previousItemsRef.current,
|
|
248
|
+
originalItems: items,
|
|
249
|
+
isSorting: isPending,
|
|
250
|
+
isSorted: !isPending && !isOriginalMode,
|
|
251
|
+
sort: activeSort,
|
|
252
|
+
sortKey: activeSort.key,
|
|
253
|
+
direction: activeSort.direction,
|
|
254
|
+
setSortKey,
|
|
255
|
+
setDirection,
|
|
256
|
+
toggleDirection,
|
|
257
|
+
setSort,
|
|
258
|
+
reset,
|
|
259
|
+
restoreOriginal,
|
|
260
|
+
restoreSorted
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
export {
|
|
264
|
+
singleKeyAccessors,
|
|
265
|
+
useSortedList
|
|
266
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "exsorted-react",
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"description": "React hooks for sorting with Exsorted and TypeScript-first inference",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"custom-hook",
|
|
7
|
+
"exsorted",
|
|
8
|
+
"react",
|
|
9
|
+
"typescript"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://github.com/Ink01101011/exsorted-react#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/Ink01101011/exsorted-react/issues"
|
|
14
|
+
},
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "Ink01101011",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/Ink01101011/exsorted-react.git"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE"
|
|
25
|
+
],
|
|
26
|
+
"type": "module",
|
|
27
|
+
"sideEffects": false,
|
|
28
|
+
"main": "./dist/index.cjs",
|
|
29
|
+
"module": "./dist/index.js",
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"import": "./dist/index.js",
|
|
35
|
+
"require": "./dist/index.cjs"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
|
43
|
+
"dev": "tsup src/index.ts --format esm,cjs --dts --watch",
|
|
44
|
+
"lint": "pnpm dlx oxlint@latest .",
|
|
45
|
+
"lint:fix": "pnpm dlx oxlint@latest --fix .",
|
|
46
|
+
"format": "pnpm dlx oxfmt@latest --write .",
|
|
47
|
+
"format:check": "pnpm dlx oxfmt@latest --check .",
|
|
48
|
+
"typecheck": "tsc --noEmit",
|
|
49
|
+
"test": "jest --runInBand",
|
|
50
|
+
"test:watch": "jest --watch",
|
|
51
|
+
"size": "node ./scripts/check-bundle-size.mjs",
|
|
52
|
+
"audit:cve": "pnpm audit --audit-level=high",
|
|
53
|
+
"check": "pnpm run lint && pnpm run format:check && pnpm run typecheck && pnpm run build && pnpm run size",
|
|
54
|
+
"prepare": "husky",
|
|
55
|
+
"precommit:check": "pnpm run lint && pnpm run format:check && pnpm run typecheck",
|
|
56
|
+
"prepublishOnly": "pnpm run build"
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"exsorted": "^1.1.0"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@testing-library/react": "^16.2.0",
|
|
63
|
+
"@types/jest": "^29.5.14",
|
|
64
|
+
"@types/react": "^18.0.0 || ^19.0.0",
|
|
65
|
+
"@types/react-dom": "^18.0.0 || ^19.0.0",
|
|
66
|
+
"husky": "^9.1.7",
|
|
67
|
+
"jest": "^29.7.0",
|
|
68
|
+
"jest-environment-jsdom": "^29.7.0",
|
|
69
|
+
"jsdom": "^25.0.1",
|
|
70
|
+
"react": "^18.2.0 || ^19.0.0",
|
|
71
|
+
"react-dom": "^18.2.0 || ^19.0.0",
|
|
72
|
+
"ts-jest": "^29.2.5",
|
|
73
|
+
"tsup": "^8.3.5",
|
|
74
|
+
"typescript": "^5.6.3"
|
|
75
|
+
},
|
|
76
|
+
"peerDependencies": {
|
|
77
|
+
"exsorted": "^1.1.0",
|
|
78
|
+
"react": "^18.2.0 || ^19.0.0"
|
|
79
|
+
},
|
|
80
|
+
"peerDependenciesMeta": {
|
|
81
|
+
"exsorted": {
|
|
82
|
+
"optional": true
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"engines": {
|
|
86
|
+
"node": ">=18"
|
|
87
|
+
}
|
|
88
|
+
}
|