@deftui/react 0.2.5 → 0.3.1
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/README.md +2 -2
- package/dist/index.d.mts +7 -5
- package/dist/index.mjs +4 -4
- package/dist/styles.css +3 -0
- package/dist/vendor/README.md +10 -9
- package/dist/vendor/coss/calendar.d.mts +4 -0
- package/dist/vendor/coss/calendar.mjs +70 -0
- package/dist/vendor/coss/card.d.mts +23 -0
- package/dist/vendor/coss/card.mjs +160 -0
- package/dist/vendor/coss/dialog.d.mts +1 -1
- package/dist/vendor/coss/dialog.mjs +1 -1
- package/dist/vendor/coss/table.d.mts +15 -0
- package/dist/vendor/coss/table.mjs +73 -0
- package/dist/vendor/reui.io/cascader/cascader-async.d.mts +20 -0
- package/dist/vendor/reui.io/cascader/cascader-async.mjs +578 -0
- package/dist/vendor/reui.io/cascader/cascader-context.d.mts +12 -0
- package/dist/vendor/reui.io/cascader/cascader-context.mjs +56 -0
- package/dist/vendor/reui.io/cascader/cascader-footer.d.mts +60 -0
- package/dist/vendor/reui.io/cascader/cascader-footer.mjs +445 -0
- package/dist/vendor/reui.io/cascader/cascader-i18n.mjs +64 -0
- package/dist/vendor/reui.io/cascader/cascader-item.d.mts +92 -0
- package/dist/vendor/reui.io/cascader/cascader-item.mjs +533 -0
- package/dist/vendor/reui.io/cascader/cascader-lib.d.mts +3 -0
- package/dist/vendor/reui.io/cascader/cascader-lib.mjs +688 -0
- package/dist/vendor/reui.io/cascader/cascader-nav.d.mts +56 -0
- package/dist/vendor/reui.io/cascader/cascader-nav.mjs +310 -0
- package/dist/vendor/reui.io/cascader/cascader-types.d.mts +140 -0
- package/dist/vendor/reui.io/cascader/cascader-virtual.d.mts +18 -0
- package/dist/vendor/reui.io/cascader/cascader-virtual.mjs +159 -0
- package/dist/vendor/reui.io/cascader/cascader.d.mts +299 -0
- package/dist/vendor/reui.io/cascader/cascader.mjs +1470 -0
- package/dist/vendor/reui.io/timeline.d.mts +21 -0
- package/dist/vendor/reui.io/timeline.mjs +127 -0
- package/dist/vendor/tailored/lightbox.d.mts +1 -1
- package/dist/vendor/tailored/lightbox.mjs +1 -1
- package/dist/widgets/datetime.d.mts +1 -1
- package/dist/widgets/datetime.mjs +6 -1
- package/dist/widgets/export.d.mts +28 -0
- package/dist/widgets/index.d.mts +69 -20
- package/dist/widgets/index.mjs +61 -11
- package/package.json +2 -2
- package/dist/vendor/shadcn/calendar.d.mts +0 -10
- package/dist/vendor/shadcn/calendar.mjs +0 -107
- package/dist/vendor/shadcn/card.d.mts +0 -11
- package/dist/vendor/shadcn/card.mjs +0 -55
- package/dist/vendor/shadcn/table.d.mts +0 -9
- package/dist/vendor/shadcn/table.mjs +0 -59
- package/dist/vendor/tailored/file-tree.d.mts +0 -21
- package/dist/vendor/tailored/file-tree.mjs +0 -146
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
// src/vendor/reui.io/cascader/cascader-lib.tsx
|
|
2
|
+
/** Root level key in `childrenOf`. NUL-prefixed so no real value collides. */
|
|
3
|
+
const CASCADER_ROOT_KEY = "\0root";
|
|
4
|
+
/**
|
|
5
|
+
* Paging pseudo-node prefix. Written as an escape sequence, not a raw 0x00
|
|
6
|
+
* byte: a literal NUL makes this module read as binary and `grep -I` skips it.
|
|
7
|
+
*/
|
|
8
|
+
const CASCADER_MORE_PREFIX = "\0more:";
|
|
9
|
+
/**
|
|
10
|
+
* The four scroll classes. In the lib because `cascader-columns.tsx` needs them
|
|
11
|
+
* too and must not import `cascader.tsx`. Shell > bound > scrollport > rows.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* The bound. `min()` over an undefined custom property is an INVALID
|
|
15
|
+
* declaration that drops the whole max-height, hence the `100vh` fallback: an
|
|
16
|
+
* inline panel has no positioner, so no `--available-height`. `24rem` is the
|
|
17
|
+
* default cap, so the common case sets no variable at all.
|
|
18
|
+
*/
|
|
19
|
+
const CASCADER_LIST_HEIGHT_CLASS = "max-h-[min(var(--available-height,100vh),var(--cascader-max-height,24rem))]";
|
|
20
|
+
/**
|
|
21
|
+
* Each style's list padding: rows take it as `padding`, the scrollport as
|
|
22
|
+
* `scroll-padding` floored at 4px so lyra's `0` does not strand a row.
|
|
23
|
+
* Mirrored per style from `registry/styles/style-*.css`; keep them in step.
|
|
24
|
+
*/
|
|
25
|
+
const CASCADER_LIST_PAD_CLASS = "[--cascader-list-pad:4px]";
|
|
26
|
+
/** The scrollport: a `max-h-*` on the ScrollArea ROOT bounds nothing. */
|
|
27
|
+
const CASCADER_SCROLL_CLASS = "size-full min-h-0 **:data-[slot=scroll-area-viewport]:h-full **:data-[slot=scroll-area-viewport]:overscroll-contain **:data-[slot=scroll-area-viewport]:scroll-py-[max(var(--cascader-list-pad,4px),4px)]";
|
|
28
|
+
/** The rows' box; `data-empty:p-0` keeps an empty state from double inset. */
|
|
29
|
+
const CASCADER_ROWS_CLASS = "p-(--cascader-list-pad,4px) data-empty:p-0";
|
|
30
|
+
/**
|
|
31
|
+
* What counts as a keyboard stop. Base UI's ScrollArea viewport makes ITSELF
|
|
32
|
+
* tabbable when content overflows (`hiddenState.x && hiddenState.y ? -1 : 0`),
|
|
33
|
+
* an unnamed stop per level that `ui/scroll-area.tsx` will not let us suppress.
|
|
34
|
+
*/
|
|
35
|
+
const CASCADER_TAB_STOP_SELECTOR = "a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex=\"-1\"])";
|
|
36
|
+
/** The stop to step over: nothing here focuses the scrollport on purpose. */
|
|
37
|
+
const CASCADER_TAB_SKIP_SELECTOR = "[data-slot=\"scroll-area-viewport\"]";
|
|
38
|
+
/** Out of the tab order whatever they contain; layout is never consulted. */
|
|
39
|
+
const CASCADER_TAB_HIDDEN_SELECTOR = "[hidden],[inert],[aria-hidden='true']";
|
|
40
|
+
/** The panel's real keyboard stops, in DOM order. */
|
|
41
|
+
function getCascaderTabStops(panel) {
|
|
42
|
+
return Array.from(panel.querySelectorAll(CASCADER_TAB_STOP_SELECTOR)).filter((stop) => stop.getAttribute("tabindex") !== "-1" && !stop.matches(CASCADER_TAB_SKIP_SELECTOR) && !stop.closest(CASCADER_TAB_HIDDEN_SELECTOR));
|
|
43
|
+
}
|
|
44
|
+
/** The footer's own stops; an `aria-disabled` command still counts as one. */
|
|
45
|
+
function getCascaderFooterStops(scope) {
|
|
46
|
+
const footer = scope.matches("[data-slot=\"cascader-footer\"]") ? scope : scope.querySelector("[data-slot=\"cascader-footer\"]");
|
|
47
|
+
if (!footer) return [];
|
|
48
|
+
return getCascaderTabStops(footer);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Hands focus to the panel's search field, or the popup without one. A Retry
|
|
52
|
+
* button is replaced by the loading state as the retry starts, and a focused
|
|
53
|
+
* node that unmounts drops focus to the body.
|
|
54
|
+
*/
|
|
55
|
+
function focusCascaderHome(from) {
|
|
56
|
+
const panel = from.closest("[data-slot=\"cascader-panel\"]");
|
|
57
|
+
const home = panel?.querySelector("[data-slot=\"cascader-input\"]") ?? from.closest("[data-slot=\"cascader-content\"]") ?? panel;
|
|
58
|
+
if (home && home === panel && !home.hasAttribute("tabindex")) home.setAttribute("tabindex", "-1");
|
|
59
|
+
home?.focus();
|
|
60
|
+
}
|
|
61
|
+
/** Where Tab lands, or `null` to let the browser out: focus is never trapped. */
|
|
62
|
+
function getCascaderTabTarget(panel, from, backwards) {
|
|
63
|
+
const stops = getCascaderTabStops(panel);
|
|
64
|
+
if (stops.length === 0 || !from) return null;
|
|
65
|
+
const index = stops.indexOf(from);
|
|
66
|
+
if (index !== -1) return stops[index + (backwards ? -1 : 1)] ?? null;
|
|
67
|
+
if (backwards) {
|
|
68
|
+
for (let i = stops.length - 1; i >= 0; i -= 1) if (from.compareDocumentPosition(stops[i]) & Node.DOCUMENT_POSITION_PRECEDING) return stops[i];
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
for (const stop of stops) if (from.compareDocumentPosition(stop) & Node.DOCUMENT_POSITION_FOLLOWING) return stop;
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The writing direction at `element`. `DirectionProvider` may only vote yes:
|
|
76
|
+
* unmounted it answers `"ltr"`, which would override a real `<html dir="rtl">`.
|
|
77
|
+
*/
|
|
78
|
+
function isCascaderRtl(element, provided) {
|
|
79
|
+
if (provided === "rtl") return true;
|
|
80
|
+
const explicit = element.closest("[dir]")?.getAttribute("dir")?.toLowerCase();
|
|
81
|
+
if (explicit === "rtl") return true;
|
|
82
|
+
if (explicit === "ltr") return false;
|
|
83
|
+
return element.ownerDocument?.defaultView?.getComputedStyle(element).direction === "rtl";
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The paging row as a REAL node, not a DOM-only row after the list: every index
|
|
87
|
+
* Base UI hands out indexes the RENDERED array, so a DOM-only row shifts them.
|
|
88
|
+
*/
|
|
89
|
+
function createCascaderMoreNode(parentKey, loadedCount = 0) {
|
|
90
|
+
return {
|
|
91
|
+
value: `${CASCADER_MORE_PREFIX}${parentKey}`,
|
|
92
|
+
label: "",
|
|
93
|
+
count: loadedCount
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function isCascaderMoreNode(node) {
|
|
97
|
+
return node.value.startsWith(CASCADER_MORE_PREFIX);
|
|
98
|
+
}
|
|
99
|
+
/** The level key a paging pseudo-node belongs to, or `null` for a real node. */
|
|
100
|
+
function getCascaderMoreParent(node) {
|
|
101
|
+
if (!isCascaderMoreNode(node)) return null;
|
|
102
|
+
return node.value.slice(6);
|
|
103
|
+
}
|
|
104
|
+
/** Either input shape into one index. Cycle-guarded depths; first wins. */
|
|
105
|
+
function buildCascaderIndex(items, getParent) {
|
|
106
|
+
const byValue = /* @__PURE__ */ new Map();
|
|
107
|
+
const childrenOf = /* @__PURE__ */ new Map();
|
|
108
|
+
const parentOf = /* @__PURE__ */ new Map();
|
|
109
|
+
const depthOf = /* @__PURE__ */ new Map();
|
|
110
|
+
const all = [];
|
|
111
|
+
const push = (parentKey, node) => {
|
|
112
|
+
const bucket = childrenOf.get(parentKey);
|
|
113
|
+
if (bucket) bucket.push(node);
|
|
114
|
+
else childrenOf.set(parentKey, [node]);
|
|
115
|
+
};
|
|
116
|
+
if (getParent) {
|
|
117
|
+
for (const node of items ?? []) {
|
|
118
|
+
if (node == null) {
|
|
119
|
+
if (process.env.NODE_ENV !== "production") warnCascaderOnce("nullish-entry", "Ignored a null or undefined entry in `items` or `children`. Check the arrays you pass in.");
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (byValue.has(node.value)) continue;
|
|
123
|
+
byValue.set(node.value, node);
|
|
124
|
+
all.push(node);
|
|
125
|
+
}
|
|
126
|
+
for (const node of all) {
|
|
127
|
+
const rawParent = getParent(node);
|
|
128
|
+
const parent = rawParent != null && byValue.has(rawParent) ? rawParent : null;
|
|
129
|
+
parentOf.set(node.value, parent);
|
|
130
|
+
push(parent ?? "\0root", node);
|
|
131
|
+
}
|
|
132
|
+
for (const node of all) {
|
|
133
|
+
let depth = 0;
|
|
134
|
+
let cursor = parentOf.get(node.value) ?? null;
|
|
135
|
+
const seen = /* @__PURE__ */ new Set([node.value]);
|
|
136
|
+
while (cursor != null && !seen.has(cursor)) {
|
|
137
|
+
seen.add(cursor);
|
|
138
|
+
depth += 1;
|
|
139
|
+
cursor = parentOf.get(cursor) ?? null;
|
|
140
|
+
}
|
|
141
|
+
depthOf.set(node.value, depth);
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
const walk = (nodes, parent, depth) => {
|
|
145
|
+
for (const node of nodes ?? []) {
|
|
146
|
+
if (node == null) {
|
|
147
|
+
if (process.env.NODE_ENV !== "production") warnCascaderOnce("nullish-entry", "Ignored a null or undefined entry in `items` or `children`. Check the arrays you pass in.");
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (byValue.has(node.value)) continue;
|
|
151
|
+
byValue.set(node.value, node);
|
|
152
|
+
parentOf.set(node.value, parent);
|
|
153
|
+
depthOf.set(node.value, depth);
|
|
154
|
+
all.push(node);
|
|
155
|
+
push(parent ?? "\0root", node);
|
|
156
|
+
if (node.children?.length) walk(node.children, node.value, depth + 1);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
walk(items, null, 0);
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
byValue,
|
|
163
|
+
childrenOf,
|
|
164
|
+
parentOf,
|
|
165
|
+
depthOf,
|
|
166
|
+
roots: childrenOf.get("\0root") ?? [],
|
|
167
|
+
all
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/** One branch-flagged copy per node, so a re-merge keeps row identity. */
|
|
171
|
+
const BRANCH_COPIES = /* @__PURE__ */ new WeakMap();
|
|
172
|
+
function asCascaderBranch(node) {
|
|
173
|
+
if (node.hasChildren) return node;
|
|
174
|
+
let copy = BRANCH_COPIES.get(node);
|
|
175
|
+
if (!copy) {
|
|
176
|
+
copy = {
|
|
177
|
+
...node,
|
|
178
|
+
hasChildren: true
|
|
179
|
+
};
|
|
180
|
+
BRANCH_COPIES.set(node, copy);
|
|
181
|
+
}
|
|
182
|
+
return copy;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Folds loaded pages into an index built from `items`, so a re-render with a
|
|
186
|
+
* new `items` array keeps what the user drilled into. Static `items` wins;
|
|
187
|
+
* `detached` is `byValue` ONLY, or deep search doubles it and `childrenOf` lies.
|
|
188
|
+
*
|
|
189
|
+
* A resolved chain node in `links` that no page placed still gets its parent,
|
|
190
|
+
* so the trigger keeps its path. While its parent's level is unloaded it is
|
|
191
|
+
* searchable but no row; while that level has pages left (`paging`) it is
|
|
192
|
+
* LISTED, last, and in `pinned`; once the level loaded completely without it
|
|
193
|
+
* (moved, deleted) it is `hidden`: no row, hit or cascade child. A node some
|
|
194
|
+
* link names as a parent reads as a branch, whatever chain wrote it last.
|
|
195
|
+
*/
|
|
196
|
+
function mergeCascaderIndex(base, pages, detached, links, paging) {
|
|
197
|
+
if (pages.size === 0 && !detached?.size) return base;
|
|
198
|
+
const byValue = new Map(base.byValue);
|
|
199
|
+
const childrenOf = new Map(base.childrenOf);
|
|
200
|
+
const parentOf = new Map(base.parentOf);
|
|
201
|
+
const depthOf = new Map(base.depthOf);
|
|
202
|
+
const copied = /* @__PURE__ */ new Set();
|
|
203
|
+
const append = (parentKey, node) => {
|
|
204
|
+
let bucket = childrenOf.get(parentKey);
|
|
205
|
+
if (!copied.has(parentKey)) {
|
|
206
|
+
bucket = bucket ? bucket.slice() : [];
|
|
207
|
+
childrenOf.set(parentKey, bucket);
|
|
208
|
+
copied.add(parentKey);
|
|
209
|
+
}
|
|
210
|
+
bucket.push(node);
|
|
211
|
+
};
|
|
212
|
+
const insert = (parentKey, nodes) => {
|
|
213
|
+
for (const node of nodes) {
|
|
214
|
+
if (byValue.has(node.value)) continue;
|
|
215
|
+
byValue.set(node.value, node);
|
|
216
|
+
parentOf.set(node.value, parentKey === "\0root" ? null : parentKey);
|
|
217
|
+
append(parentKey, node);
|
|
218
|
+
if (node.children?.length) insert(node.value, node.children);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
for (const [parentKey, nodes] of pages) insert(parentKey, nodes);
|
|
222
|
+
const pinned = /* @__PURE__ */ new Set();
|
|
223
|
+
const hidden = /* @__PURE__ */ new Set();
|
|
224
|
+
const linkParents = new Set(links?.values());
|
|
225
|
+
for (const [value, parentKey] of links ?? []) {
|
|
226
|
+
const linked = detached?.get(value);
|
|
227
|
+
if (!linked || byValue.has(value)) continue;
|
|
228
|
+
const node = linkParents.has(value) ? asCascaderBranch(linked) : linked;
|
|
229
|
+
byValue.set(value, node);
|
|
230
|
+
parentOf.set(value, parentKey === "\0root" ? null : parentKey);
|
|
231
|
+
if (paging?.has(parentKey)) {
|
|
232
|
+
append(parentKey, node);
|
|
233
|
+
pinned.add(value);
|
|
234
|
+
} else if (pages.has(parentKey) || base.childrenOf.has(parentKey)) hidden.add(value);
|
|
235
|
+
}
|
|
236
|
+
for (const value of byValue.keys()) {
|
|
237
|
+
if (depthOf.has(value)) continue;
|
|
238
|
+
let depth = 0;
|
|
239
|
+
let cursor = parentOf.get(value) ?? null;
|
|
240
|
+
const seen = /* @__PURE__ */ new Set([value]);
|
|
241
|
+
while (cursor != null && !seen.has(cursor)) {
|
|
242
|
+
seen.add(cursor);
|
|
243
|
+
depth += 1;
|
|
244
|
+
cursor = parentOf.get(cursor) ?? null;
|
|
245
|
+
}
|
|
246
|
+
depthOf.set(value, depth);
|
|
247
|
+
}
|
|
248
|
+
const roots = childrenOf.get("\0root") ?? [];
|
|
249
|
+
const all = [];
|
|
250
|
+
const visited = /* @__PURE__ */ new Set();
|
|
251
|
+
const walk = (nodes) => {
|
|
252
|
+
for (const node of nodes) {
|
|
253
|
+
if (visited.has(node.value)) continue;
|
|
254
|
+
visited.add(node.value);
|
|
255
|
+
all.push(node);
|
|
256
|
+
const children = childrenOf.get(node.value);
|
|
257
|
+
if (children?.length) walk(children);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
walk(roots);
|
|
261
|
+
for (const [value, node] of byValue) {
|
|
262
|
+
if (visited.has(value) || hidden.has(value)) continue;
|
|
263
|
+
visited.add(value);
|
|
264
|
+
all.push(node);
|
|
265
|
+
}
|
|
266
|
+
if (detached) for (const [value, node] of detached) {
|
|
267
|
+
if (byValue.has(value)) continue;
|
|
268
|
+
byValue.set(value, node);
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
byValue,
|
|
272
|
+
childrenOf,
|
|
273
|
+
parentOf,
|
|
274
|
+
depthOf,
|
|
275
|
+
roots,
|
|
276
|
+
all,
|
|
277
|
+
pinned,
|
|
278
|
+
hidden
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
/** Per index, so a query does not rebuild it. */
|
|
282
|
+
const LISTED_VALUES = /* @__PURE__ */ new WeakMap();
|
|
283
|
+
/**
|
|
284
|
+
* Values reachable from the roots through `childrenOf`: the nodes a level can
|
|
285
|
+
* show. A tree query keeps only these, or a resolved node no level lists left
|
|
286
|
+
* its ancestors expanded over nothing.
|
|
287
|
+
*/
|
|
288
|
+
function getCascaderListedValues(index) {
|
|
289
|
+
let listed = LISTED_VALUES.get(index);
|
|
290
|
+
if (listed) return listed;
|
|
291
|
+
const seen = /* @__PURE__ */ new Set();
|
|
292
|
+
const walk = (nodes) => {
|
|
293
|
+
for (const node of nodes) {
|
|
294
|
+
if (seen.has(node.value)) continue;
|
|
295
|
+
seen.add(node.value);
|
|
296
|
+
const children = index.childrenOf.get(node.value);
|
|
297
|
+
if (children?.length) walk(children);
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
walk(index.roots);
|
|
301
|
+
listed = seen;
|
|
302
|
+
LISTED_VALUES.set(index, listed);
|
|
303
|
+
return listed;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* A level's rows plus its paging row, placed BEFORE the `pinned` rows, which
|
|
307
|
+
* sit last: the first row a page appends then takes the paging row's index,
|
|
308
|
+
* so the highlight Base UI keeps by index lands on it.
|
|
309
|
+
*/
|
|
310
|
+
function withCascaderMoreRow(rows, more, pinned) {
|
|
311
|
+
const at = pinned?.size ? rows.findIndex((row) => pinned.has(row.value)) : -1;
|
|
312
|
+
if (at === -1) return [...rows, more];
|
|
313
|
+
return [
|
|
314
|
+
...rows.slice(0, at),
|
|
315
|
+
more,
|
|
316
|
+
...rows.slice(at)
|
|
317
|
+
];
|
|
318
|
+
}
|
|
319
|
+
/** Children of `parent`, or the root level when `parent` is nullish. */
|
|
320
|
+
function getCascaderChildren(index, parent) {
|
|
321
|
+
return index.childrenOf.get(parent ?? "\0root") ?? [];
|
|
322
|
+
}
|
|
323
|
+
/** A branch has known children, or `hasChildren` for an unfetched level. */
|
|
324
|
+
function isCascaderBranch(index, node) {
|
|
325
|
+
if (node.hasChildren) return true;
|
|
326
|
+
return (index.childrenOf.get(node.value)?.length ?? 0) > 0;
|
|
327
|
+
}
|
|
328
|
+
/** Trailing count for the default row. Explicit `count` wins over the tree. */
|
|
329
|
+
function getCascaderCount(index, node) {
|
|
330
|
+
if (typeof node.count === "number") return node.count;
|
|
331
|
+
return index.childrenOf.get(node.value)?.length ?? 0;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Whether a node is disabled: its own flag, or any ancestor's, since a disabled
|
|
335
|
+
* branch keeps its whole subtree shut. The index's copy counts as its own, so a
|
|
336
|
+
* server hit that omits the flag `items` sets is still caught. Ancestry is
|
|
337
|
+
* `parentOf`: a detached hit the index has never placed answers for itself.
|
|
338
|
+
*/
|
|
339
|
+
function isCascaderDisabled(index, node) {
|
|
340
|
+
if (node.disabled || index.byValue.get(node.value)?.disabled) return true;
|
|
341
|
+
const seen = /* @__PURE__ */ new Set([node.value]);
|
|
342
|
+
let cursor = index.parentOf.get(node.value) ?? null;
|
|
343
|
+
while (cursor != null && !seen.has(cursor)) {
|
|
344
|
+
seen.add(cursor);
|
|
345
|
+
if (index.byValue.get(cursor)?.disabled) return true;
|
|
346
|
+
cursor = index.parentOf.get(cursor) ?? null;
|
|
347
|
+
}
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
/** Whether a node may be committed as a selection. */
|
|
351
|
+
function isCascaderSelectable(index, node, selectable) {
|
|
352
|
+
if (isCascaderMoreNode(node)) return false;
|
|
353
|
+
if (isCascaderDisabled(index, node)) return false;
|
|
354
|
+
if (typeof selectable === "function") return selectable(node);
|
|
355
|
+
if (selectable === "any") return true;
|
|
356
|
+
return !isCascaderBranch(index, node);
|
|
357
|
+
}
|
|
358
|
+
/** Ancestor chain for `value`, root first. Empty while async data loads. */
|
|
359
|
+
function getCascaderPath(index, value) {
|
|
360
|
+
if (value == null) return [];
|
|
361
|
+
const chain = [];
|
|
362
|
+
const seen = /* @__PURE__ */ new Set();
|
|
363
|
+
let cursor = value;
|
|
364
|
+
while (cursor != null && !seen.has(cursor)) {
|
|
365
|
+
seen.add(cursor);
|
|
366
|
+
const node = index.byValue.get(cursor);
|
|
367
|
+
if (!node) break;
|
|
368
|
+
chain.push(node);
|
|
369
|
+
cursor = index.parentOf.get(cursor) ?? null;
|
|
370
|
+
}
|
|
371
|
+
return chain.reverse();
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* `toLocaleLowerCase`, not `toLowerCase`: the invariant mapping turns Turkish
|
|
375
|
+
* "I" into "i" not "ı", so "ışık" would never match "IŞIK". Both sides fold here.
|
|
376
|
+
*/
|
|
377
|
+
function foldCascaderText(text) {
|
|
378
|
+
return text.toLocaleLowerCase();
|
|
379
|
+
}
|
|
380
|
+
/** Folds once so callers can hoist the cost out of a per-node loop. */
|
|
381
|
+
function normalizeCascaderQuery(query) {
|
|
382
|
+
return foldCascaderText(query.trim());
|
|
383
|
+
}
|
|
384
|
+
/** Case-insensitive substring over label and keywords; pre-fold the query. */
|
|
385
|
+
function matchesCascaderQuery(node, normalized) {
|
|
386
|
+
if (!normalized) return true;
|
|
387
|
+
if (foldCascaderText(node.label ?? "").includes(normalized)) return true;
|
|
388
|
+
if (node.keywords) {
|
|
389
|
+
for (const keyword of node.keywords) if (foldCascaderText(keyword).includes(normalized)) return true;
|
|
390
|
+
}
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
/** Filters one level in place-order. Returns the input when the query is empty. */
|
|
394
|
+
function filterCascaderLevel(nodes, query, matches = matchesCascaderQuery) {
|
|
395
|
+
const normalized = normalizeCascaderQuery(query);
|
|
396
|
+
if (!normalized) return nodes;
|
|
397
|
+
return nodes.filter((node) => matches(node, normalized));
|
|
398
|
+
}
|
|
399
|
+
/** Searches every node, optionally under `within`, in one pass over `all`. */
|
|
400
|
+
function searchCascaderDeep(index, query, options = {}) {
|
|
401
|
+
const normalized = normalizeCascaderQuery(query);
|
|
402
|
+
if (!normalized) return [];
|
|
403
|
+
const { within, limit = 200, matches = matchesCascaderQuery } = options;
|
|
404
|
+
const results = [];
|
|
405
|
+
const ancestry = within == null ? null : /* @__PURE__ */ new Map();
|
|
406
|
+
const isWithin = (node) => {
|
|
407
|
+
if (within == null || !ancestry) return true;
|
|
408
|
+
const trail = [];
|
|
409
|
+
let answer = false;
|
|
410
|
+
let cursor = index.parentOf.get(node.value);
|
|
411
|
+
while (cursor != null) {
|
|
412
|
+
if (cursor === within) {
|
|
413
|
+
answer = true;
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
const cached = ancestry.get(cursor);
|
|
417
|
+
if (cached !== void 0) {
|
|
418
|
+
answer = cached;
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
ancestry.set(cursor, false);
|
|
422
|
+
trail.push(cursor);
|
|
423
|
+
cursor = index.parentOf.get(cursor) ?? null;
|
|
424
|
+
}
|
|
425
|
+
if (answer) for (const value of trail) ancestry.set(value, true);
|
|
426
|
+
return answer;
|
|
427
|
+
};
|
|
428
|
+
for (const node of index.all) {
|
|
429
|
+
if (results.length >= limit) break;
|
|
430
|
+
if (!matches(node, normalized)) continue;
|
|
431
|
+
if (!isWithin(node)) continue;
|
|
432
|
+
results.push(node);
|
|
433
|
+
}
|
|
434
|
+
return results;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Flattens the tree into tree mode's visible rows. A `sentinels` paging row
|
|
438
|
+
* COUNTS as a sibling in `aria-setsize`: it holds an index of its own.
|
|
439
|
+
*/
|
|
440
|
+
function flattenCascaderTree(index, expanded, sentinels) {
|
|
441
|
+
const rows = [];
|
|
442
|
+
const walk = (nodes, parentKey, depth) => {
|
|
443
|
+
const items = sentinels?.has(parentKey) ? withCascaderMoreRow(nodes, createCascaderMoreNode(parentKey, nodes.length), index.pinned) : nodes;
|
|
444
|
+
const setSize = items.length;
|
|
445
|
+
for (let i = 0; i < items.length; i += 1) {
|
|
446
|
+
const node = items[i];
|
|
447
|
+
if (isCascaderMoreNode(node)) {
|
|
448
|
+
rows.push({
|
|
449
|
+
node,
|
|
450
|
+
depth,
|
|
451
|
+
branch: false,
|
|
452
|
+
expanded: false,
|
|
453
|
+
setSize,
|
|
454
|
+
posInSet: i + 1
|
|
455
|
+
});
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
const branch = isCascaderBranch(index, node);
|
|
459
|
+
const isExpanded = branch && expanded.has(node.value);
|
|
460
|
+
rows.push({
|
|
461
|
+
node,
|
|
462
|
+
depth,
|
|
463
|
+
branch,
|
|
464
|
+
expanded: isExpanded,
|
|
465
|
+
setSize,
|
|
466
|
+
posInSet: i + 1
|
|
467
|
+
});
|
|
468
|
+
if (isExpanded) walk(index.childrenOf.get(node.value) ?? [], node.value, depth + 1);
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
walk(index.roots, CASCADER_ROOT_KEY, 0);
|
|
472
|
+
return rows;
|
|
473
|
+
}
|
|
474
|
+
/** Shortens a path; the ellipsis segment still carries the hidden nodes. */
|
|
475
|
+
function collapseCascaderPath(path, options = {}) {
|
|
476
|
+
const { maxSegments = 3, collapse = "middle" } = options;
|
|
477
|
+
const asNodes = (nodes) => nodes.map((node) => ({
|
|
478
|
+
type: "node",
|
|
479
|
+
node
|
|
480
|
+
}));
|
|
481
|
+
if (collapse === "none" || maxSegments <= 0 || path.length <= maxSegments) return asNodes(path);
|
|
482
|
+
if (collapse === "start") {
|
|
483
|
+
const tail = path.slice(path.length - maxSegments);
|
|
484
|
+
return [{
|
|
485
|
+
type: "ellipsis",
|
|
486
|
+
hidden: path.slice(0, path.length - maxSegments)
|
|
487
|
+
}, ...asNodes(tail)];
|
|
488
|
+
}
|
|
489
|
+
const head = path.slice(0, 1);
|
|
490
|
+
const tailCount = maxSegments - 1;
|
|
491
|
+
const tail = path.slice(path.length - tailCount);
|
|
492
|
+
const hidden = path.slice(1, path.length - tailCount);
|
|
493
|
+
if (hidden.length === 0) return asNodes(path);
|
|
494
|
+
return [
|
|
495
|
+
...asNodes(head),
|
|
496
|
+
{
|
|
497
|
+
type: "ellipsis",
|
|
498
|
+
hidden
|
|
499
|
+
},
|
|
500
|
+
...asNodes(tail)
|
|
501
|
+
];
|
|
502
|
+
}
|
|
503
|
+
/** Per index, so a press does not rebuild it. */
|
|
504
|
+
const SELECTION_CHILDREN = /* @__PURE__ */ new WeakMap();
|
|
505
|
+
/**
|
|
506
|
+
* Children by parent, built from `parentOf`. Unlike `childrenOf` it also holds
|
|
507
|
+
* a resolved chain node whose level has not loaded, which the selection walks
|
|
508
|
+
* must see: unchecking an ancestor has to take it along. A `hidden` one is left
|
|
509
|
+
* out, since its level loaded without it.
|
|
510
|
+
*/
|
|
511
|
+
function getSelectionChildren(index) {
|
|
512
|
+
let map = SELECTION_CHILDREN.get(index);
|
|
513
|
+
if (map) return map;
|
|
514
|
+
map = /* @__PURE__ */ new Map();
|
|
515
|
+
for (const [value, parent] of index.parentOf) {
|
|
516
|
+
const node = index.byValue.get(value);
|
|
517
|
+
if (!node || index.hidden?.has(value)) continue;
|
|
518
|
+
const key = parent ?? "\0root";
|
|
519
|
+
const bucket = map.get(key);
|
|
520
|
+
if (bucket) bucket.push(node);
|
|
521
|
+
else map.set(key, [node]);
|
|
522
|
+
}
|
|
523
|
+
SELECTION_CHILDREN.set(index, map);
|
|
524
|
+
return map;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Every KNOWN node under `value`, root first: loaded, or on a resolved chain.
|
|
528
|
+
* Reaching an unfetched descendant would mean a network request per level of
|
|
529
|
+
* the subtree on every selection.
|
|
530
|
+
*/
|
|
531
|
+
function collectCascaderSubtree(index, value) {
|
|
532
|
+
const root = index.byValue.get(value);
|
|
533
|
+
if (!root) return [];
|
|
534
|
+
const childrenOf = getSelectionChildren(index);
|
|
535
|
+
const out = [];
|
|
536
|
+
const seen = /* @__PURE__ */ new Set();
|
|
537
|
+
const walk = (node) => {
|
|
538
|
+
if (seen.has(node.value)) return;
|
|
539
|
+
seen.add(node.value);
|
|
540
|
+
out.push(node);
|
|
541
|
+
const children = childrenOf.get(node.value);
|
|
542
|
+
if (children) for (const child of children) walk(child);
|
|
543
|
+
};
|
|
544
|
+
walk(root);
|
|
545
|
+
return out;
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Toggles `value` with its known subtree (loaded, or on a resolved chain), then
|
|
549
|
+
* reconciles ancestors. One flat array, no hidden "checked" state: a branch is
|
|
550
|
+
* in the selection exactly when every selectable known child of it is, which
|
|
551
|
+
* keeps a row's checked state an O(1) lookup. Nodes `isSelectable` rejects are skipped down AND ignored up,
|
|
552
|
+
* the pressed node excepted.
|
|
553
|
+
*/
|
|
554
|
+
function applyCascadeSelection(index, selected, value, select, isSelectable = () => true) {
|
|
555
|
+
const next = new Set(selected);
|
|
556
|
+
const subtree = collectCascaderSubtree(index, value);
|
|
557
|
+
if (subtree.length === 0) {
|
|
558
|
+
if (select) next.add(value);
|
|
559
|
+
else next.delete(value);
|
|
560
|
+
}
|
|
561
|
+
for (const node of subtree) {
|
|
562
|
+
if (node.value !== value && !isSelectable(node)) continue;
|
|
563
|
+
if (select) next.add(node.value);
|
|
564
|
+
else next.delete(node.value);
|
|
565
|
+
}
|
|
566
|
+
const seen = /* @__PURE__ */ new Set([value]);
|
|
567
|
+
let cursor = index.parentOf.get(value) ?? null;
|
|
568
|
+
while (cursor != null && !seen.has(cursor)) {
|
|
569
|
+
seen.add(cursor);
|
|
570
|
+
const parent = index.byValue.get(cursor);
|
|
571
|
+
const selectable = (getSelectionChildren(index).get(cursor) ?? []).filter((child) => isSelectable(child));
|
|
572
|
+
if (selectable.length > 0 && selectable.every((child) => next.has(child.value)) && !!parent && isSelectable(parent)) next.add(cursor);
|
|
573
|
+
else next.delete(cursor);
|
|
574
|
+
cursor = index.parentOf.get(cursor) ?? null;
|
|
575
|
+
}
|
|
576
|
+
return Array.from(next);
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* How many selected nodes each value has BELOW it, itself excluded. Walks UP
|
|
580
|
+
* from each selection: a per-row subtree scan would be quadratic and re-paid.
|
|
581
|
+
*/
|
|
582
|
+
function getCascaderSelectedDescendants(index, selected) {
|
|
583
|
+
const counts = /* @__PURE__ */ new Map();
|
|
584
|
+
for (const value of selected) {
|
|
585
|
+
const seen = /* @__PURE__ */ new Set([value]);
|
|
586
|
+
let cursor = index.parentOf.get(value) ?? null;
|
|
587
|
+
while (cursor != null && !seen.has(cursor)) {
|
|
588
|
+
seen.add(cursor);
|
|
589
|
+
counts.set(cursor, (counts.get(cursor) ?? 0) + 1);
|
|
590
|
+
cursor = index.parentOf.get(cursor) ?? null;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return counts;
|
|
594
|
+
}
|
|
595
|
+
/** The PARTIALLY selected values, read off counts the caller already holds. */
|
|
596
|
+
function getCascaderIndeterminateFrom(counts, selected) {
|
|
597
|
+
const selectedSet = new Set(selected);
|
|
598
|
+
const partial = /* @__PURE__ */ new Set();
|
|
599
|
+
for (const [value, count] of counts) if (count > 0 && !selectedSet.has(value)) partial.add(value);
|
|
600
|
+
return partial;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* The cascade selection under a reporting strategy. DERIVED OUTPUT ONLY: the
|
|
604
|
+
* STORED value stays the full closure, which keeps a checked state an O(1)
|
|
605
|
+
* lookup. `"all"` is the closure; `"parent"` drops a value whose parent is
|
|
606
|
+
* selected; `"child"` drops one with a selected child.
|
|
607
|
+
*/
|
|
608
|
+
function getCascaderCheckedValues(index, selected, strategy) {
|
|
609
|
+
if (strategy === "all") return selected;
|
|
610
|
+
const set = new Set(selected);
|
|
611
|
+
if (strategy === "parent") return selected.filter((value) => {
|
|
612
|
+
const parent = index.parentOf.get(value);
|
|
613
|
+
return parent == null || !set.has(parent);
|
|
614
|
+
});
|
|
615
|
+
const withSelectedChild = /* @__PURE__ */ new Set();
|
|
616
|
+
for (const value of selected) {
|
|
617
|
+
if (index.hidden?.has(value)) continue;
|
|
618
|
+
const parent = index.parentOf.get(value);
|
|
619
|
+
if (parent != null) withSelectedChild.add(parent);
|
|
620
|
+
}
|
|
621
|
+
return selected.filter((value) => !withSelectedChild.has(value));
|
|
622
|
+
}
|
|
623
|
+
/** Values in `nodes` whose label collides, so the chip must show its path. */
|
|
624
|
+
function findAmbiguousCascaderLabels(nodes) {
|
|
625
|
+
const firstByLabel = /* @__PURE__ */ new Map();
|
|
626
|
+
const ambiguous = /* @__PURE__ */ new Set();
|
|
627
|
+
for (const node of nodes) {
|
|
628
|
+
const first = firstByLabel.get(node.label);
|
|
629
|
+
if (first === void 0) {
|
|
630
|
+
firstByLabel.set(node.label, node.value);
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
ambiguous.add(node.value);
|
|
634
|
+
ambiguous.add(first);
|
|
635
|
+
}
|
|
636
|
+
return ambiguous;
|
|
637
|
+
}
|
|
638
|
+
/** Warnings already emitted. Module scoped, or they repeat once per render. */
|
|
639
|
+
const CASCADER_WARNED = /* @__PURE__ */ new Set();
|
|
640
|
+
/** Warns once per `key`, never in production, never by throwing. */
|
|
641
|
+
function warnCascaderOnce(key, message) {
|
|
642
|
+
if (process.env.NODE_ENV === "production") return;
|
|
643
|
+
if (CASCADER_WARNED.has(key)) return;
|
|
644
|
+
CASCADER_WARNED.add(key);
|
|
645
|
+
console.warn(`[Cascader] ${message}`);
|
|
646
|
+
}
|
|
647
|
+
/** Dev-time scan, kept out of `buildCascaderIndex` so the build stays hot. */
|
|
648
|
+
function findCascaderDataIssues(items, getParent) {
|
|
649
|
+
const seen = /* @__PURE__ */ new Set();
|
|
650
|
+
const duplicates = /* @__PURE__ */ new Set();
|
|
651
|
+
const flat = [];
|
|
652
|
+
const visit = (nodes) => {
|
|
653
|
+
for (const node of nodes ?? []) {
|
|
654
|
+
if (node == null) continue;
|
|
655
|
+
if (seen.has(node.value)) duplicates.add(node.value);
|
|
656
|
+
else seen.add(node.value);
|
|
657
|
+
flat.push(node);
|
|
658
|
+
if (node.children?.length) visit(node.children);
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
visit(items);
|
|
662
|
+
const cycles = [];
|
|
663
|
+
if (getParent) {
|
|
664
|
+
const parentOf = /* @__PURE__ */ new Map();
|
|
665
|
+
for (const node of flat) {
|
|
666
|
+
if (parentOf.has(node.value)) continue;
|
|
667
|
+
const raw = getParent(node);
|
|
668
|
+
parentOf.set(node.value, raw != null && seen.has(raw) ? raw : null);
|
|
669
|
+
}
|
|
670
|
+
for (const node of flat) {
|
|
671
|
+
const walked = /* @__PURE__ */ new Set([node.value]);
|
|
672
|
+
let cursor = parentOf.get(node.value) ?? null;
|
|
673
|
+
while (cursor != null) {
|
|
674
|
+
if (walked.has(cursor)) {
|
|
675
|
+
cycles.push(node.value);
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
678
|
+
walked.add(cursor);
|
|
679
|
+
cursor = parentOf.get(cursor) ?? null;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return {
|
|
684
|
+
duplicates: Array.from(duplicates),
|
|
685
|
+
cycles
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
export { CASCADER_LIST_HEIGHT_CLASS, CASCADER_LIST_PAD_CLASS, CASCADER_MORE_PREFIX, CASCADER_ROOT_KEY, CASCADER_ROWS_CLASS, CASCADER_SCROLL_CLASS, applyCascadeSelection, buildCascaderIndex, collapseCascaderPath, collectCascaderSubtree, createCascaderMoreNode, filterCascaderLevel, findAmbiguousCascaderLabels, findCascaderDataIssues, flattenCascaderTree, focusCascaderHome, foldCascaderText, getCascaderCheckedValues, getCascaderChildren, getCascaderCount, getCascaderFooterStops, getCascaderIndeterminateFrom, getCascaderListedValues, getCascaderMoreParent, getCascaderPath, getCascaderSelectedDescendants, getCascaderTabTarget, isCascaderBranch, isCascaderDisabled, isCascaderMoreNode, isCascaderRtl, isCascaderSelectable, matchesCascaderQuery, mergeCascaderIndex, normalizeCascaderQuery, searchCascaderDeep, warnCascaderOnce, withCascaderMoreRow };
|