@sia.soul/sia-react-ui 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -87,7 +87,7 @@ import {
87
87
  message,
88
88
  notification,
89
89
  useOverlayLifecycle
90
- } from "./chunk-ULEGTKXR.js";
90
+ } from "./chunk-ZPYGBMBH.js";
91
91
  import {
92
92
  TextArea
93
93
  } from "./chunk-NZAT2RUM.js";
@@ -112,7 +112,7 @@ import {
112
112
  DatePicker,
113
113
  SelectDateRange,
114
114
  TimePanel
115
- } from "./chunk-34TTF4Y7.js";
115
+ } from "./chunk-QEDWRVDA.js";
116
116
  import {
117
117
  Select
118
118
  } from "./chunk-JKZGAZMB.js";
@@ -1376,6 +1376,21 @@ function setValue(source, path, value) {
1376
1376
  function isEmpty(value) {
1377
1377
  return value === void 0 || value === null || value === "" || Array.isArray(value) && value.length === 0;
1378
1378
  }
1379
+ function cloneFormValue(value, seen = /* @__PURE__ */ new WeakMap()) {
1380
+ if (!value || typeof value !== "object") return value;
1381
+ if (seen.has(value)) return seen.get(value);
1382
+ if (value instanceof Date) return new Date(value.getTime());
1383
+ if (value instanceof RegExp) return new RegExp(value.source, value.flags);
1384
+ const cloneable = value;
1385
+ if (typeof cloneable.clone === "function") return cloneable.clone();
1386
+ if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) return value;
1387
+ const next = Array.isArray(value) ? [] : {};
1388
+ seen.set(value, next);
1389
+ Object.keys(value).forEach((key) => {
1390
+ next[key] = cloneFormValue(value[key], seen);
1391
+ });
1392
+ return next;
1393
+ }
1379
1394
  var FormStore = class {
1380
1395
  values = {};
1381
1396
  initialValues = {};
@@ -1384,11 +1399,13 @@ var FormStore = class {
1384
1399
  listeners = /* @__PURE__ */ new Set();
1385
1400
  callbacks = {};
1386
1401
  version = 0;
1402
+ initialized = false;
1387
1403
  configure(initialValues, callbacks) {
1388
1404
  this.callbacks = callbacks;
1389
- if (initialValues && Object.keys(this.initialValues).length === 0) {
1390
- this.initialValues = structuredClone(initialValues);
1391
- this.values = structuredClone(initialValues);
1405
+ if (!this.initialized) {
1406
+ this.initialized = true;
1407
+ this.initialValues = cloneFormValue(initialValues ?? {});
1408
+ this.values = cloneFormValue(initialValues ?? {});
1392
1409
  }
1393
1410
  }
1394
1411
  subscribe = (listener) => {
@@ -1410,7 +1427,7 @@ var FormStore = class {
1410
1427
  };
1411
1428
  }
1412
1429
  getFieldValue = (name) => getValue(this.values, pathArray(name));
1413
- getFieldsValue = () => structuredClone(this.values);
1430
+ getFieldsValue = () => cloneFormValue(this.values);
1414
1431
  getFieldError = (name) => this.errors.get(pathKey2(name)) ?? [];
1415
1432
  setFieldValue = (name, value) => {
1416
1433
  const path = pathArray(name);
@@ -1428,7 +1445,7 @@ var FormStore = class {
1428
1445
  };
1429
1446
  resetFields = (names) => {
1430
1447
  if (!names) {
1431
- this.values = structuredClone(this.initialValues);
1448
+ this.values = cloneFormValue(this.initialValues);
1432
1449
  this.errors.clear();
1433
1450
  } else {
1434
1451
  names.forEach((name) => {
@@ -1617,7 +1634,7 @@ function FormItem({
1617
1634
  content = children;
1618
1635
  }
1619
1636
  }
1620
- if (noStyle) return content;
1637
+ if (noStyle) return /* @__PURE__ */ jsx9(Fragment3, { children: content });
1621
1638
  const inputWrap = /* @__PURE__ */ jsx9("div", { className: "sia-form-item__input-wrap", children: content });
1622
1639
  const hasError = status === "error" && Boolean(helpContent);
1623
1640
  return /* @__PURE__ */ jsxs9("div", { ref: itemRef, className: `sia-form-item${status ? ` sia-form-item--${status}` : ""} ${className}`.trim(), "data-sia-form-field": key || void 0, children: [
@@ -2360,14 +2377,14 @@ function TimeRangePicker({ value, defaultValue = ["", ""], placeholder = ["\u5F0
2360
2377
  // src/components/TreeSelect.tsx
2361
2378
  import { useEffect as useEffect6, useMemo as useMemo6, useRef as useRef7, useState as useState10 } from "react";
2362
2379
  import { Fragment as Fragment4, jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
2363
- function flattenTree(nodes, expanded, search, depth = 0, parents = []) {
2380
+ function flattenTree(nodes, expanded, search, filter, depth = 0, parents = [], ancestorMatched = false) {
2364
2381
  const result = [];
2365
2382
  for (const node of nodes) {
2366
- const matches = !search || String(node.title).toLowerCase().includes(search.toLowerCase());
2367
- const childMatches = search && node.children ? flattenTree(node.children, new Set(node.children.map((child) => child.value)), search, depth + 1, [...parents, node.value]) : [];
2383
+ const matches = !search || ancestorMatched || filter === false || (filter ? filter(search, node) : String(node.title).toLowerCase().includes(search.toLowerCase()));
2384
+ const childMatches = search && node.children ? flattenTree(node.children, expanded, search, filter, depth + 1, [...parents, node.value], matches) : [];
2368
2385
  if (matches || childMatches.length) result.push({ ...node, depth, parentValues: parents });
2369
- if (node.children && (expanded.has(node.value) || search)) {
2370
- result.push(...search ? childMatches : flattenTree(node.children, expanded, search, depth + 1, [...parents, node.value]));
2386
+ if (node.children && (expanded.has(node.key ?? node.value) || search)) {
2387
+ result.push(...search ? childMatches : flattenTree(node.children, expanded, search, filter, depth + 1, [...parents, node.value]));
2371
2388
  }
2372
2389
  }
2373
2390
  return result;
@@ -2398,25 +2415,54 @@ function TreeSelect({
2398
2415
  onChange,
2399
2416
  onSearch,
2400
2417
  onDropdownVisibleChange,
2418
+ open: controlledOpen,
2419
+ defaultOpen = false,
2420
+ searchValue,
2421
+ treeExpandedKeys,
2422
+ treeDefaultExpandedKeys = [],
2423
+ onTreeExpand,
2424
+ filterTreeNode,
2425
+ treeCheckStrictly = true,
2426
+ showCheckedStrategy = "all",
2427
+ loading = false,
2428
+ listHeight = 300,
2429
+ popupClassName = "",
2430
+ popupStyle,
2431
+ dropdownFooter,
2432
+ displayRender,
2433
+ renderTrigger,
2434
+ onClear,
2435
+ onSelect,
2436
+ onDeselect,
2401
2437
  ...props
2402
2438
  }) {
2403
2439
  const rootRef = useRef7(null);
2404
2440
  const popupRef = useRef7(null);
2441
+ const nodeIndex = useMemo6(() => {
2442
+ const index = /* @__PURE__ */ new Map();
2443
+ const visit = (nodes) => nodes.forEach((node) => {
2444
+ index.set(node.value, node);
2445
+ if (node.children) visit(node.children);
2446
+ });
2447
+ visit(treeData);
2448
+ return index;
2449
+ }, [treeData]);
2450
+ const resolveNodes = (values) => values.map((value2) => nodeIndex.get(value2) ?? { value: value2, title: String(value2) });
2405
2451
  const multi = multiple || treeCheckable;
2406
2452
  const initial = defaultValue ?? (multi ? [] : void 0);
2407
2453
  const [currentValue, setCurrentValue] = useControllableState({
2408
- value,
2454
+ value: value === null ? [] : value,
2409
2455
  defaultValue: initial,
2410
- onChange: (next) => onChange?.(next, findNodes(treeData, Array.isArray(next) ? [...next] : next === void 0 ? [] : [next]))
2456
+ onChange: (next) => onChange?.(next, resolveNodes(Array.isArray(next) ? [...next] : next === void 0 ? [] : [next]))
2411
2457
  });
2412
- const [open, setOpen] = useState10(false);
2413
- const [search, setSearch] = useState10("");
2458
+ const [open, setOpen] = useControllableState({ value: controlledOpen, defaultValue: defaultOpen, onChange: onDropdownVisibleChange });
2459
+ const [search, setSearch] = useControllableState({ value: searchValue, defaultValue: "", onChange: onSearch });
2414
2460
  const [expanded, setExpanded] = useState10(() => {
2415
- if (!treeDefaultExpandAll) return /* @__PURE__ */ new Set();
2461
+ if (!treeDefaultExpandAll) return new Set(treeDefaultExpandedKeys);
2416
2462
  const values = [];
2417
2463
  const collect = (nodes) => nodes.forEach((node) => {
2418
2464
  if (node.children?.length) {
2419
- values.push(node.value);
2465
+ values.push(node.key ?? node.value);
2420
2466
  collect(node.children);
2421
2467
  }
2422
2468
  });
@@ -2424,99 +2470,229 @@ function TreeSelect({
2424
2470
  return new Set(values);
2425
2471
  });
2426
2472
  const selectedValues = Array.isArray(currentValue) ? [...currentValue] : currentValue === void 0 ? [] : [currentValue];
2427
- const selectedNodes = useMemo6(() => findNodes(treeData, selectedValues), [treeData, selectedValues.join("|")]);
2428
- const visibleNodes = useMemo6(() => flattenTree(treeData, expanded, search), [expanded, search, treeData]);
2473
+ const expandedSet = treeExpandedKeys ? new Set(treeExpandedKeys) : expanded;
2474
+ const selectedNodes = resolveNodes(selectedValues);
2475
+ const visibleNodes = flattenTree(treeData, expandedSet, search, filterTreeNode);
2476
+ useEffect6(() => {
2477
+ if (!open || treeExpandedKeys) return;
2478
+ const keys = new Set(expanded);
2479
+ const walk = (nodes, parents) => nodes.forEach((node) => {
2480
+ if (selectedValues.includes(node.value)) parents.forEach((key) => keys.add(key));
2481
+ if (node.children) walk(node.children, [...parents, node.key ?? node.value]);
2482
+ });
2483
+ walk(treeData, []);
2484
+ if (keys.size !== expanded.size) setExpanded(keys);
2485
+ const frame = requestAnimationFrame(() => popupRef.current?.querySelector('[aria-selected="true"]')?.scrollIntoView({ block: "nearest" }));
2486
+ return () => cancelAnimationFrame(frame);
2487
+ }, [open, treeData, currentValue, treeExpandedKeys]);
2488
+ function toggleNode(node) {
2489
+ const key = node.key ?? node.value;
2490
+ const next = new Set(expandedSet);
2491
+ if (next.has(key)) next.delete(key);
2492
+ else next.add(key);
2493
+ if (!treeExpandedKeys) setExpanded(next);
2494
+ onTreeExpand?.([...next]);
2495
+ }
2496
+ const cascade = treeCheckable && !treeCheckStrictly;
2497
+ function checkState(values) {
2498
+ const checked2 = new Set(values);
2499
+ const affected2 = (node) => node.disabled || node.disableCheckbox ? [] : [
2500
+ ...node.checkable === false ? [] : [node.value],
2501
+ ...node.children?.flatMap(affected2) ?? []
2502
+ ];
2503
+ if (cascade) {
2504
+ findNodes(treeData, values).forEach((node) => affected2(node).forEach((value2) => checked2.add(value2)));
2505
+ const fold = (nodes) => nodes.forEach((node) => {
2506
+ if (node.disabled || node.disableCheckbox) return;
2507
+ if (node.children) fold(node.children);
2508
+ const children = node.children?.filter((child) => !child.disabled && !child.disableCheckbox && child.checkable !== false) ?? [];
2509
+ if (children.length && node.checkable !== false) {
2510
+ if (children.every((child) => checked2.has(child.value))) checked2.add(node.value);
2511
+ else checked2.delete(node.value);
2512
+ }
2513
+ });
2514
+ fold(treeData);
2515
+ }
2516
+ return { checked: checked2, affected: affected2 };
2517
+ }
2518
+ const { checked, affected } = checkState(selectedValues);
2519
+ function outputValues(values) {
2520
+ if (!cascade || showCheckedStrategy === "all") return [...values];
2521
+ const output = [...values].filter((value2) => !nodeIndex.has(value2));
2522
+ const walk = (nodes, parentChecked = false) => nodes.forEach((node) => {
2523
+ const selected = values.has(node.value);
2524
+ const enabledChildren = node.children?.filter((child) => !child.disabled && !child.disableCheckbox) ?? [];
2525
+ if (selected && (showCheckedStrategy === "parent" ? !parentChecked : !enabledChildren.length)) output.push(node.value);
2526
+ if (node.children) walk(node.children, selected && !node.disabled && !node.disableCheckbox);
2527
+ });
2528
+ walk(treeData);
2529
+ return output;
2530
+ }
2531
+ function clear() {
2532
+ if (disabled || loading) return;
2533
+ setCurrentValue(multi ? [] : void 0);
2534
+ onClear?.();
2535
+ }
2429
2536
  useEffect6(() => {
2430
2537
  if (!open) return void 0;
2431
2538
  const close = (event) => {
2432
2539
  const target = event.target;
2433
2540
  if (!rootRef.current?.contains(target) && !popupRef.current?.contains(target)) {
2434
2541
  setOpen(false);
2435
- onDropdownVisibleChange?.(false);
2542
+ setSearch("");
2436
2543
  }
2437
2544
  };
2438
2545
  document.addEventListener("pointerdown", close);
2439
2546
  return () => document.removeEventListener("pointerdown", close);
2440
2547
  }, [onDropdownVisibleChange, open]);
2441
2548
  function changeOpen(next) {
2549
+ if (disabled) return;
2442
2550
  setOpen(next);
2443
- onDropdownVisibleChange?.(next);
2551
+ if (!next) setSearch("");
2444
2552
  }
2445
2553
  function select(node) {
2446
- if (node.disabled || node.selectable === false) return;
2554
+ if (disabled || loading || node.disabled) return;
2555
+ if (treeCheckable ? node.disableCheckbox || node.checkable === false : node.selectable === false) {
2556
+ if (node.children?.length) toggleNode(node);
2557
+ return;
2558
+ }
2447
2559
  if (multi) {
2448
- const next = selectedValues.includes(node.value) ? selectedValues.filter((item) => item !== node.value) : [...selectedValues, node.value];
2449
- setCurrentValue(next);
2560
+ const exists = checked.has(node.value);
2561
+ const next = new Set(checked);
2562
+ (cascade ? affected(node) : [node.value]).forEach((value2) => {
2563
+ if (exists) next.delete(value2);
2564
+ else next.add(value2);
2565
+ });
2566
+ if (cascade && exists) {
2567
+ const removeParents = (nodes) => {
2568
+ let found = false;
2569
+ nodes.forEach((item) => {
2570
+ const childFound = item.children ? removeParents(item.children) : false;
2571
+ if (childFound) next.delete(item.value);
2572
+ if (item.value === node.value || childFound) found = true;
2573
+ });
2574
+ return found;
2575
+ };
2576
+ removeParents(treeData);
2577
+ }
2578
+ setCurrentValue(outputValues(checkState([...next]).checked));
2579
+ if (exists) onDeselect?.(node.value, node);
2580
+ else onSelect?.(node.value, node);
2450
2581
  } else {
2451
2582
  setCurrentValue(node.value);
2583
+ onSelect?.(node.value, node);
2452
2584
  changeOpen(false);
2453
2585
  }
2454
2586
  }
2455
- return /* @__PURE__ */ jsxs14("div", { ref: rootRef, className: `sia-tree-select sia-tree-select--${size} sia-tree-select--${status}${open ? " is-open" : ""}${disabled ? " is-disabled" : ""} ${className}`.trim(), ...props, children: [
2456
- /* @__PURE__ */ jsxs14(
2457
- "button",
2458
- {
2459
- type: "button",
2460
- className: "sia-tree-select__trigger",
2461
- disabled,
2462
- "aria-haspopup": "tree",
2463
- "aria-expanded": open,
2464
- onClick: () => changeOpen(!open),
2465
- onKeyDown: (event) => {
2466
- if (event.key === "ArrowDown") changeOpen(true);
2467
- if (event.key === "Escape") changeOpen(false);
2468
- },
2469
- children: [
2470
- /* @__PURE__ */ jsx14("span", { className: "sia-tree-select__value", children: selectedNodes.length ? multi ? /* @__PURE__ */ jsxs14(Fragment4, { children: [
2471
- selectedNodes.slice(0, maxTagCount).map((node) => /* @__PURE__ */ jsx14("span", { className: "sia-tree-select__tag", children: node.title }, node.value)),
2472
- selectedNodes.length > maxTagCount ? /* @__PURE__ */ jsxs14("span", { className: "sia-tree-select__tag", children: [
2473
- "+",
2474
- selectedNodes.length - maxTagCount
2475
- ] }) : null
2476
- ] }) : selectedNodes[0]?.title : /* @__PURE__ */ jsx14("span", { className: "sia-tree-select__placeholder", children: placeholder }) }),
2477
- allowClear && selectedValues.length && !disabled ? /* @__PURE__ */ jsx14("span", { role: "button", tabIndex: -1, className: "sia-tree-select__clear", "aria-label": "\u6E05\u9664\u9009\u62E9", onClick: (event) => {
2478
- event.stopPropagation();
2479
- setCurrentValue(multi ? [] : void 0);
2480
- }, children: /* @__PURE__ */ jsx14(Icon, { name: "circle-close", variant: "filled", size: 15 }) }) : null,
2481
- /* @__PURE__ */ jsx14(Icon, { name: open ? "chevron-up" : "chevron-down", size: 14 })
2482
- ]
2587
+ return /* @__PURE__ */ jsxs14(
2588
+ "div",
2589
+ {
2590
+ ref: rootRef,
2591
+ className: `sia-tree-select sia-tree-select--${size} sia-tree-select--${status}${open ? " is-open" : ""}${disabled ? " is-disabled" : ""} ${className}`.trim(),
2592
+ ...props,
2593
+ onKeyDown: (event) => {
2594
+ props.onKeyDown?.(event);
2595
+ if (event.key === "Escape") {
2596
+ event.stopPropagation();
2597
+ changeOpen(false);
2598
+ }
2599
+ },
2600
+ children: [
2601
+ renderTrigger ? renderTrigger({ open, toggle: () => changeOpen(!open), clear }) : /* @__PURE__ */ jsxs14(Fragment4, { children: [
2602
+ /* @__PURE__ */ jsxs14(
2603
+ "button",
2604
+ {
2605
+ type: "button",
2606
+ className: "sia-tree-select__trigger",
2607
+ disabled,
2608
+ "aria-haspopup": "tree",
2609
+ "aria-expanded": open,
2610
+ "aria-label": props["aria-label"],
2611
+ onClick: () => changeOpen(!open),
2612
+ onKeyDown: (event) => {
2613
+ if (event.key === "ArrowDown") {
2614
+ event.preventDefault();
2615
+ changeOpen(true);
2616
+ }
2617
+ if (event.key === "Escape") changeOpen(false);
2618
+ },
2619
+ children: [
2620
+ /* @__PURE__ */ jsx14("span", { className: "sia-tree-select__value", children: selectedNodes.length ? displayRender ? displayRender(selectedNodes) : multi ? /* @__PURE__ */ jsxs14(Fragment4, { children: [
2621
+ selectedNodes.slice(0, maxTagCount).map((node) => /* @__PURE__ */ jsx14("span", { className: "sia-tree-select__tag", children: node.title }, node.value)),
2622
+ selectedNodes.length > maxTagCount ? /* @__PURE__ */ jsxs14("span", { className: "sia-tree-select__tag", children: [
2623
+ "+",
2624
+ selectedNodes.length - maxTagCount
2625
+ ] }) : null
2626
+ ] }) : selectedNodes[0]?.title : /* @__PURE__ */ jsx14("span", { className: "sia-tree-select__placeholder", children: placeholder }) }),
2627
+ /* @__PURE__ */ jsx14(Icon, { name: open ? "chevron-up" : "chevron-down", size: 14 })
2628
+ ]
2629
+ }
2630
+ ),
2631
+ allowClear && selectedValues.length > 0 && !disabled ? /* @__PURE__ */ jsx14("button", { type: "button", className: "sia-tree-select__clear", "aria-label": "\u6E05\u9664\u9009\u62E9", onClick: clear, children: /* @__PURE__ */ jsx14(Icon, { name: "circle-close", variant: "filled", size: 15 }) }) : null
2632
+ ] }),
2633
+ /* @__PURE__ */ jsxs14(PopupPortal, { ref: popupRef, anchorRef: rootRef, open: open && !disabled, className: `sia-tree-select__dropdown ${popupClassName}`, style: popupStyle, children: [
2634
+ showSearch ? /* @__PURE__ */ jsxs14("label", { className: "sia-tree-select__search", children: [
2635
+ /* @__PURE__ */ jsx14(Icon, { name: "search", size: 15 }),
2636
+ /* @__PURE__ */ jsx14("input", { "aria-label": searchPlaceholder, autoFocus: true, value: search, placeholder: searchPlaceholder, onChange: (event) => setSearch(event.currentTarget.value) })
2637
+ ] }) : null,
2638
+ /* @__PURE__ */ jsx14("div", { className: "sia-tree-select__tree", role: "tree", "aria-multiselectable": multi || void 0, style: { maxHeight: listHeight }, children: loading ? /* @__PURE__ */ jsx14("div", { role: "status", className: "sia-tree-select__empty", children: "\u52A0\u8F7D\u4E2D\u2026" }) : visibleNodes.length ? visibleNodes.map((node) => {
2639
+ const hasChildren = Boolean(node.children?.length);
2640
+ const selected = checked.has(node.value);
2641
+ const partial = cascade && !selected && affected(node).some((value2) => checked.has(value2));
2642
+ return /* @__PURE__ */ jsxs14(
2643
+ "div",
2644
+ {
2645
+ className: `sia-tree-select__node${selected ? " is-selected" : ""}${node.disabled ? " is-disabled" : ""}`,
2646
+ style: { paddingInlineStart: 8 + node.depth * 20 },
2647
+ role: "treeitem",
2648
+ "aria-level": node.depth + 1,
2649
+ "aria-disabled": node.disabled || void 0,
2650
+ "aria-selected": selected,
2651
+ "aria-expanded": hasChildren ? !!search || expandedSet.has(node.key ?? node.value) : void 0,
2652
+ "aria-checked": treeCheckable ? partial ? "mixed" : selected : void 0,
2653
+ children: [
2654
+ hasChildren ? /* @__PURE__ */ jsx14("button", { type: "button", className: "sia-tree-select__expand", "aria-label": expandedSet.has(node.key ?? node.value) ? "\u6536\u8D77\u8282\u70B9" : "\u5C55\u5F00\u8282\u70B9", disabled: node.disabled, onClick: () => toggleNode(node), children: /* @__PURE__ */ jsx14(Icon, { name: expandedSet.has(node.key ?? node.value) ? "chevron-down" : "chevron-right", size: 13 }) }) : /* @__PURE__ */ jsx14("span", { className: "sia-tree-select__indent" }),
2655
+ treeCheckable && node.checkable !== false ? /* @__PURE__ */ jsx14(Checkbox, { "aria-label": typeof node.title === "string" ? node.title : void 0, checked: selected, indeterminate: partial, disabled: node.disabled || node.disableCheckbox, onChange: () => select(node) }) : null,
2656
+ /* @__PURE__ */ jsx14("button", { type: "button", className: "sia-tree-select__title", disabled: node.disabled, onClick: () => select(node), children: node.title })
2657
+ ]
2658
+ },
2659
+ node.key ?? node.value
2660
+ );
2661
+ }) : /* @__PURE__ */ jsx14("div", { className: "sia-tree-select__empty", children: "\u6682\u65E0\u5339\u914D\u8282\u70B9" }) }),
2662
+ dropdownFooter
2663
+ ] })
2664
+ ]
2665
+ }
2666
+ );
2667
+ }
2668
+
2669
+ // src/components/flatTree.ts
2670
+ function buildFlatTree(items, options = {}) {
2671
+ const { idField = "id", parentIdField = "pId", rootParentId } = options;
2672
+ const keyOf = (value) => String(value ?? "");
2673
+ const nodes = new Map(items.map((item) => [keyOf(item[idField]), { ...item, children: [] }]));
2674
+ const roots = [];
2675
+ const isRoot = (parent) => rootParentId !== void 0 ? keyOf(parent) === keyOf(rootParentId) : parent === null || parent === void 0 || parent === "" || parent === 0;
2676
+ for (const node of nodes.values()) {
2677
+ const parentKey = keyOf(node[parentIdField]);
2678
+ const parent = nodes.get(parentKey);
2679
+ let cyclic = false;
2680
+ const visited = /* @__PURE__ */ new Set([keyOf(node[idField])]);
2681
+ let ancestor = parent;
2682
+ while (ancestor) {
2683
+ const key = keyOf(ancestor[idField]);
2684
+ if (visited.has(key)) {
2685
+ cyclic = true;
2686
+ break;
2483
2687
  }
2484
- ),
2485
- /* @__PURE__ */ jsxs14(PopupPortal, { ref: popupRef, anchorRef: rootRef, open, className: "sia-tree-select__dropdown", children: [
2486
- showSearch ? /* @__PURE__ */ jsxs14("label", { className: "sia-tree-select__search", children: [
2487
- /* @__PURE__ */ jsx14(Icon, { name: "search", size: 15 }),
2488
- /* @__PURE__ */ jsx14("input", { value: search, placeholder: searchPlaceholder, onChange: (event) => {
2489
- setSearch(event.currentTarget.value);
2490
- onSearch?.(event.currentTarget.value);
2491
- } })
2492
- ] }) : null,
2493
- /* @__PURE__ */ jsx14("div", { className: "sia-tree-select__tree", role: "tree", "aria-multiselectable": multi || void 0, children: visibleNodes.length ? visibleNodes.map((node) => {
2494
- const hasChildren = Boolean(node.children?.length);
2495
- const selected = selectedValues.includes(node.value);
2496
- return /* @__PURE__ */ jsxs14(
2497
- "div",
2498
- {
2499
- className: `sia-tree-select__node${selected ? " is-selected" : ""}${node.disabled ? " is-disabled" : ""}`,
2500
- style: { paddingInlineStart: 8 + node.depth * 20 },
2501
- role: "treeitem",
2502
- "aria-selected": selected,
2503
- "aria-expanded": hasChildren ? expanded.has(node.value) : void 0,
2504
- children: [
2505
- hasChildren ? /* @__PURE__ */ jsx14("button", { type: "button", className: "sia-tree-select__expand", "aria-label": expanded.has(node.value) ? "\u6536\u8D77\u8282\u70B9" : "\u5C55\u5F00\u8282\u70B9", onClick: () => setExpanded((current) => {
2506
- const next = new Set(current);
2507
- if (next.has(node.value)) next.delete(node.value);
2508
- else next.add(node.value);
2509
- return next;
2510
- }), children: /* @__PURE__ */ jsx14(Icon, { name: expanded.has(node.value) ? "chevron-down" : "chevron-right", size: 13 }) }) : /* @__PURE__ */ jsx14("span", { className: "sia-tree-select__indent" }),
2511
- treeCheckable ? /* @__PURE__ */ jsx14(Checkbox, { checked: selected, disabled: node.disabled, onChange: () => select(node) }) : null,
2512
- /* @__PURE__ */ jsx14("button", { type: "button", className: "sia-tree-select__title", disabled: node.disabled, onClick: () => select(node), children: node.title })
2513
- ]
2514
- },
2515
- node.key ?? node.value
2516
- );
2517
- }) : /* @__PURE__ */ jsx14("div", { className: "sia-tree-select__empty", children: "\u6682\u65E0\u5339\u914D\u8282\u70B9" }) })
2518
- ] })
2519
- ] });
2688
+ visited.add(key);
2689
+ if (isRoot(ancestor[parentIdField])) break;
2690
+ ancestor = nodes.get(keyOf(ancestor[parentIdField]));
2691
+ }
2692
+ if (isRoot(node[parentIdField]) || !parent || cyclic) roots.push(node);
2693
+ else parent.children.push(node);
2694
+ }
2695
+ return roots;
2520
2696
  }
2521
2697
 
2522
2698
  // src/components/Editor.tsx
@@ -6690,6 +6866,378 @@ function applySiaTheme(mode, target = document.documentElement) {
6690
6866
  target.dataset.siaTheme = mode;
6691
6867
  target.style.colorScheme = mode;
6692
6868
  }
6869
+
6870
+ // src/components/SelectionPanel.tsx
6871
+ import { useEffect as useEffect14, useId as useId7, useMemo as useMemo10, useRef as useRef14, useState as useState17 } from "react";
6872
+ import { jsx as jsx22, jsxs as jsxs22 } from "react/jsx-runtime";
6873
+ function SelectionPanel({
6874
+ options,
6875
+ value,
6876
+ onChange,
6877
+ multiple = false,
6878
+ disabled = false,
6879
+ loading = false,
6880
+ showSearch = true,
6881
+ searchValue,
6882
+ onSearch,
6883
+ selectedOnly = false,
6884
+ showValue = false,
6885
+ displayField = "label",
6886
+ columns = 1,
6887
+ maxHeight = 250,
6888
+ autoFocusSearch = false
6889
+ }) {
6890
+ const [localSearch, setLocalSearch] = useState17("");
6891
+ const search = searchValue ?? localSearch;
6892
+ const [scrollTop, setScrollTop] = useState17(0);
6893
+ const [active, setActive] = useState17();
6894
+ const listRef = useRef14(null);
6895
+ const id = useId7();
6896
+ const selected = useMemo10(() => new Set(value), [value]);
6897
+ const count = Math.max(1, Math.floor(columns));
6898
+ const allOptions = useMemo10(() => {
6899
+ const known = new Set(options.filter((option) => option.optionType !== "divider").map((option) => option.value));
6900
+ return [...value.filter((item) => !known.has(item)).map((item) => ({ value: item, label: String(item), group: "\u81EA\u5B9A\u4E49\u503C" })), ...options];
6901
+ }, [options, value]);
6902
+ const filtered = useMemo10(
6903
+ () => allOptions.filter((option) => (!selectedOnly || selected.has(option.value)) && (!search || option.optionType !== "divider" && [option.label, option.value].some((text) => String(text ?? "").toLowerCase().includes(search.toLowerCase())))),
6904
+ [allOptions, selectedOnly, selected, search]
6905
+ );
6906
+ const rows = useMemo10(() => {
6907
+ const groups = /* @__PURE__ */ new Map();
6908
+ filtered.forEach((option) => {
6909
+ const items = groups.get(option.group) ?? [];
6910
+ items.push(option);
6911
+ groups.set(option.group, items);
6912
+ });
6913
+ const rows2 = [];
6914
+ let top = 0;
6915
+ for (const [group, items] of groups) {
6916
+ if (group) {
6917
+ rows2.push({ key: `group-${rows2.length}`, title: group, top, height: 30 });
6918
+ top += 30;
6919
+ }
6920
+ for (let index = 0; index < items.length; index += count) {
6921
+ const height = showValue ? 48 : 34;
6922
+ rows2.push({ key: `row-${rows2.length}`, items: items.slice(index, index + count), top, height });
6923
+ top += height;
6924
+ }
6925
+ }
6926
+ return rows2;
6927
+ }, [filtered, count, showValue]);
6928
+ const totalHeight = rows.length ? rows[rows.length - 1].top + rows[rows.length - 1].height : 0;
6929
+ const virtual = filtered.length > 100;
6930
+ const renderedRows = virtual ? rows.filter((row) => row.top + row.height >= scrollTop - 100 && row.top <= scrollTop + maxHeight + 100) : rows;
6931
+ const enabled = filtered.filter((option) => !option.disabled && option.optionType !== "divider");
6932
+ useEffect14(() => {
6933
+ setScrollTop(0);
6934
+ if (listRef.current) listRef.current.scrollTop = 0;
6935
+ setActive(void 0);
6936
+ }, [search, selectedOnly, count]);
6937
+ function toggle(option) {
6938
+ if (disabled || loading || option.disabled || option.optionType === "divider") return;
6939
+ setActive(option.value);
6940
+ onChange(multiple ? selected.has(option.value) ? value.filter((item) => item !== option.value) : [...value, option.value] : [option.value]);
6941
+ }
6942
+ function batch(operation) {
6943
+ if (disabled || loading) return;
6944
+ if (operation === "clear") {
6945
+ onChange([]);
6946
+ return;
6947
+ }
6948
+ const editable = new Set(enabled.map((option) => option.value));
6949
+ const retained = value.filter((item) => !editable.has(item));
6950
+ const add = enabled.filter((option) => operation === "all" || !selected.has(option.value)).map((option) => option.value);
6951
+ onChange([...retained, ...add]);
6952
+ }
6953
+ return /* @__PURE__ */ jsxs22("div", { className: "sia-selection-panel", children: [
6954
+ showSearch && /* @__PURE__ */ jsx22(
6955
+ Input,
6956
+ {
6957
+ "aria-label": "\u641C\u7D22\u9009\u9879",
6958
+ placeholder: "\u641C\u7D22",
6959
+ autoFocus: autoFocusSearch,
6960
+ value: search,
6961
+ allowClear: true,
6962
+ onChange: (event) => {
6963
+ setLocalSearch(event.target.value);
6964
+ onSearch?.(event.target.value);
6965
+ }
6966
+ }
6967
+ ),
6968
+ multiple && /* @__PURE__ */ jsxs22("div", { className: "sia-selection-panel__actions", children: [
6969
+ /* @__PURE__ */ jsx22(Button, { variant: "link", size: "small", disabled: disabled || loading || !enabled.length, onClick: () => batch("all"), children: "\u5168\u9009" }),
6970
+ /* @__PURE__ */ jsx22(Button, { variant: "link", size: "small", disabled: disabled || loading || !enabled.length, onClick: () => batch("invert"), children: "\u53CD\u9009" }),
6971
+ /* @__PURE__ */ jsx22(Button, { variant: "link", size: "small", disabled: disabled || loading || !value.length, onClick: () => batch("clear"), children: "\u6E05\u7A7A" }),
6972
+ /* @__PURE__ */ jsxs22("span", { children: [
6973
+ "\u5DF2\u9009 ",
6974
+ value.length,
6975
+ " \u9879"
6976
+ ] })
6977
+ ] }),
6978
+ /* @__PURE__ */ jsx22(
6979
+ "div",
6980
+ {
6981
+ ref: listRef,
6982
+ role: "listbox",
6983
+ "aria-label": "\u9009\u9879\u5217\u8868",
6984
+ "aria-multiselectable": multiple || void 0,
6985
+ tabIndex: disabled ? -1 : 0,
6986
+ className: "sia-selection-panel__list",
6987
+ style: { maxHeight },
6988
+ onScroll: (event) => setScrollTop(event.currentTarget.scrollTop),
6989
+ "aria-activedescendant": active !== void 0 ? `${id}-${typeof active}-${active}` : void 0,
6990
+ onKeyDown: (event) => {
6991
+ if (disabled || loading || !enabled.length) return;
6992
+ const index = enabled.findIndex((option) => option.value === active);
6993
+ if (["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) {
6994
+ event.preventDefault();
6995
+ const next = event.key === "Home" ? 0 : event.key === "End" ? enabled.length - 1 : Math.max(0, Math.min(enabled.length - 1, index + (event.key === "ArrowDown" ? 1 : -1)));
6996
+ const option = enabled[next];
6997
+ setActive(option.value);
6998
+ const row = rows.find((row2) => row2.items?.includes(option));
6999
+ if (row && listRef.current) {
7000
+ listRef.current.scrollTop = Math.max(0, row.top - maxHeight / 2);
7001
+ setScrollTop(listRef.current.scrollTop);
7002
+ }
7003
+ } else if ((event.key === "Enter" || event.key === " ") && index >= 0) {
7004
+ event.preventDefault();
7005
+ toggle(enabled[index]);
7006
+ }
7007
+ },
7008
+ children: loading ? /* @__PURE__ */ jsx22("div", { role: "status", children: "\u52A0\u8F7D\u4E2D\u2026" }) : !rows.length ? /* @__PURE__ */ jsx22("div", { className: "sia-selection-panel__empty", children: "\u6682\u65E0\u5339\u914D\u9009\u9879" }) : /* @__PURE__ */ jsx22("div", { style: { height: totalHeight, position: "relative" }, children: renderedRows.map((row) => /* @__PURE__ */ jsx22(
7009
+ "div",
7010
+ {
7011
+ className: row.title !== void 0 ? "sia-selection-panel__group" : "sia-selection-panel__row",
7012
+ style: { position: "absolute", top: row.top, height: row.height, width: "100%", gridTemplateColumns: `repeat(${count}, minmax(0, 1fr))` },
7013
+ children: row.title ?? row.items?.map((option, index) => option.optionType === "divider" ? /* @__PURE__ */ jsx22("div", { role: "separator", className: "sia-selection-panel__divider" }, `divider-${index}`) : /* @__PURE__ */ jsx22(Tooltip, { title: option.disabled ? option.disabledTip : void 0, placement: "right", children: /* @__PURE__ */ jsxs22(
7014
+ "button",
7015
+ {
7016
+ type: "button",
7017
+ role: "option",
7018
+ id: `${id}-${typeof option.value}-${option.value}`,
7019
+ tabIndex: -1,
7020
+ "aria-selected": selected.has(option.value),
7021
+ "aria-disabled": disabled || option.disabled || void 0,
7022
+ className: `sia-selection-panel__option${active === option.value ? " is-active" : ""}`,
7023
+ onClick: () => toggle(option),
7024
+ children: [
7025
+ multiple && /* @__PURE__ */ jsx22("span", { "aria-hidden": "true", className: "sia-selection-panel__check", children: selected.has(option.value) ? "\u2713" : "" }),
7026
+ /* @__PURE__ */ jsxs22("span", { className: "sia-selection-panel__text", children: [
7027
+ showValue || displayField === "label" ? option.label : String(option.value),
7028
+ showValue && /* @__PURE__ */ jsx22("small", { children: String(option.value) })
7029
+ ] })
7030
+ ]
7031
+ }
7032
+ ) }, `${typeof option.value}-${option.value}`))
7033
+ },
7034
+ row.key
7035
+ )) })
7036
+ }
7037
+ )
7038
+ ] });
7039
+ }
7040
+
7041
+ // src/components/InputSelect.tsx
7042
+ import { forwardRef as forwardRef7, useEffect as useEffect15, useRef as useRef15, useState as useState18 } from "react";
7043
+ import { jsx as jsx23, jsxs as jsxs23 } from "react/jsx-runtime";
7044
+ var InputSelect = forwardRef7(function InputSelect2({
7045
+ options,
7046
+ value,
7047
+ defaultValue,
7048
+ onChange,
7049
+ multiple = false,
7050
+ displayField = "label",
7051
+ showValue = false,
7052
+ placeholder = "\u8BF7\u9009\u62E9",
7053
+ size = "medium",
7054
+ status,
7055
+ disabled = false,
7056
+ loading = false,
7057
+ allowClear = true,
7058
+ hideSearch,
7059
+ inputMaxLength,
7060
+ parseInput,
7061
+ enableModal,
7062
+ suffix,
7063
+ style,
7064
+ className = "",
7065
+ onOpenChange,
7066
+ ...inputProps
7067
+ }, forwardedRef) {
7068
+ const [internal, setInternal] = useState18(defaultValue);
7069
+ const current = value !== void 0 ? value : internal;
7070
+ const values = current == null ? [] : Array.isArray(current) ? current : [current];
7071
+ const text = values.map((item) => displayField === "label" ? String(options.find((option) => option.optionType !== "divider" && option.value === item)?.label ?? item) : String(item)).join(",");
7072
+ const [draft, setDraft] = useState18(text);
7073
+ useEffect15(() => {
7074
+ setDraft(text);
7075
+ }, [text, current]);
7076
+ const [open, setOpen] = useState18(false);
7077
+ const [search, setSearch] = useState18("");
7078
+ const [modalOpen, setModalOpen] = useState18(false);
7079
+ const [modalValues, setModalValues] = useState18([]);
7080
+ const [selectedOnly, setSelectedOnly] = useState18(false);
7081
+ const rootRef = useRef15(null);
7082
+ const popupRef = useRef15(null);
7083
+ const inputRef = useRef15(null);
7084
+ const config = typeof enableModal === "object" ? enableModal : {};
7085
+ function changeOpen(next) {
7086
+ if (next && (disabled || loading)) return;
7087
+ setOpen(next);
7088
+ if (next !== open) onOpenChange?.(next);
7089
+ if (!next) setSearch("");
7090
+ }
7091
+ function emit(next) {
7092
+ if (disabled || loading) return;
7093
+ if (value === void 0) setInternal(next);
7094
+ onChange?.(next);
7095
+ }
7096
+ function select(next) {
7097
+ emit(multiple ? next : next[0]);
7098
+ if (!multiple) {
7099
+ changeOpen(false);
7100
+ inputRef.current?.focus();
7101
+ }
7102
+ }
7103
+ useEffect15(() => {
7104
+ if (!open) return;
7105
+ const outside = (event) => {
7106
+ if (!rootRef.current?.contains(event.target) && !popupRef.current?.contains(event.target)) changeOpen(false);
7107
+ };
7108
+ document.addEventListener("pointerdown", outside);
7109
+ return () => document.removeEventListener("pointerdown", outside);
7110
+ }, [open, onOpenChange]);
7111
+ useEffect15(() => {
7112
+ if (disabled || loading) {
7113
+ changeOpen(false);
7114
+ setModalOpen(false);
7115
+ }
7116
+ }, [disabled, loading]);
7117
+ function openModal() {
7118
+ if (disabled || loading) return;
7119
+ changeOpen(false);
7120
+ setModalValues([...values]);
7121
+ setSelectedOnly(false);
7122
+ setModalOpen(true);
7123
+ }
7124
+ return /* @__PURE__ */ jsxs23("div", { ref: rootRef, style, className: `sia-input-select ${className}`, onKeyDown: (event) => {
7125
+ if (event.key === "Escape" && open) {
7126
+ event.stopPropagation();
7127
+ changeOpen(false);
7128
+ inputRef.current?.focus();
7129
+ }
7130
+ }, children: [
7131
+ /* @__PURE__ */ jsx23(
7132
+ Input,
7133
+ {
7134
+ ...inputProps,
7135
+ ref: (node) => {
7136
+ inputRef.current = node;
7137
+ if (typeof forwardedRef === "function") forwardedRef(node);
7138
+ else if (forwardedRef) forwardedRef.current = node;
7139
+ },
7140
+ role: "combobox",
7141
+ "aria-label": inputProps["aria-label"] ?? placeholder,
7142
+ "aria-expanded": open,
7143
+ "aria-haspopup": "listbox",
7144
+ size,
7145
+ status,
7146
+ disabled,
7147
+ readOnly: displayField === "label" || loading,
7148
+ placeholder,
7149
+ value: draft,
7150
+ maxLength: inputMaxLength && inputMaxLength > 0 ? inputMaxLength : void 0,
7151
+ showCount: !!inputMaxLength && inputMaxLength > 0,
7152
+ onClick: () => changeOpen(!open),
7153
+ autoComplete: "off",
7154
+ onKeyDown: (event) => {
7155
+ if (event.key === "ArrowDown" || event.key === "Enter" && displayField === "label") {
7156
+ event.preventDefault();
7157
+ if (open) popupRef.current?.querySelector('[role="listbox"]')?.focus();
7158
+ else changeOpen(true);
7159
+ }
7160
+ },
7161
+ onChange: (event) => {
7162
+ const next = event.target.value;
7163
+ setDraft(next);
7164
+ emit(parseInput ? parseInput(next) : multiple ? next.split(",").map((item) => item.trim()).filter(Boolean) : next);
7165
+ changeOpen(true);
7166
+ },
7167
+ suffix: /* @__PURE__ */ jsxs23("span", { className: "sia-input-select__suffix", children: [
7168
+ allowClear && draft && !disabled && /* @__PURE__ */ jsx23(Button, { variant: "text", size: "small", disabled: loading, "aria-label": "\u6E05\u9664\u8F93\u5165\u9009\u62E9", onClick: (event) => {
7169
+ event.stopPropagation();
7170
+ setDraft("");
7171
+ emit(multiple ? [] : void 0);
7172
+ inputRef.current?.focus();
7173
+ }, children: "\xD7" }),
7174
+ suffix,
7175
+ enableModal && /* @__PURE__ */ jsx23(Button, { variant: "text", size: "small", disabled: disabled || loading, "aria-label": "\u6253\u5F00\u9009\u62E9\u5F39\u7A97", onClick: (event) => {
7176
+ event.stopPropagation();
7177
+ openModal();
7178
+ }, children: "\u2026" }),
7179
+ /* @__PURE__ */ jsx23(Button, { variant: "text", size: "small", disabled: disabled || loading, "aria-label": "\u5C55\u5F00\u9009\u9879", onClick: (event) => {
7180
+ event.stopPropagation();
7181
+ changeOpen(!open);
7182
+ }, children: /* @__PURE__ */ jsx23(Icon, { name: loading ? "refresh" : open ? "chevron-up" : "chevron-down", size: 13 }) })
7183
+ ] })
7184
+ }
7185
+ ),
7186
+ /* @__PURE__ */ jsx23(PopupPortal, { ref: popupRef, anchorRef: rootRef, open: open && !disabled && !loading, className: "sia-input-select__popup", children: /* @__PURE__ */ jsx23(
7187
+ SelectionPanel,
7188
+ {
7189
+ options,
7190
+ value: values,
7191
+ multiple,
7192
+ onChange: select,
7193
+ showValue,
7194
+ displayField,
7195
+ showSearch: !(hideSearch ?? (!multiple && displayField === "value")),
7196
+ autoFocusSearch: displayField === "label",
7197
+ searchValue: search || (!multiple && displayField === "value" ? draft : ""),
7198
+ onSearch: setSearch
7199
+ }
7200
+ ) }),
7201
+ enableModal && /* @__PURE__ */ jsxs23(
7202
+ Modal,
7203
+ {
7204
+ open: modalOpen,
7205
+ title: config.title ?? "\u9009\u62E9",
7206
+ width: config.width ?? 900,
7207
+ onCancel: () => setModalOpen(false),
7208
+ onOpenChange: setModalOpen,
7209
+ onOk: () => {
7210
+ emit(multiple ? modalValues : modalValues[0]);
7211
+ setModalOpen(false);
7212
+ },
7213
+ children: [
7214
+ /* @__PURE__ */ jsxs23("div", { className: "sia-selection-panel__actions", role: "tablist", "aria-label": "\u9009\u9879\u8303\u56F4", children: [
7215
+ /* @__PURE__ */ jsx23(Button, { role: "tab", "aria-selected": !selectedOnly, variant: !selectedOnly ? "primary" : "text", onClick: () => setSelectedOnly(false), children: "\u5168\u90E8" }),
7216
+ /* @__PURE__ */ jsxs23(Button, { role: "tab", "aria-selected": selectedOnly, variant: selectedOnly ? "primary" : "text", onClick: () => setSelectedOnly(true), children: [
7217
+ "\u5DF2\u9009\u9879(",
7218
+ modalValues.length,
7219
+ ")"
7220
+ ] })
7221
+ ] }),
7222
+ modalOpen && /* @__PURE__ */ jsx23(
7223
+ SelectionPanel,
7224
+ {
7225
+ options,
7226
+ value: modalValues,
7227
+ multiple,
7228
+ onChange: setModalValues,
7229
+ selectedOnly,
7230
+ columns: config.columns ?? 3,
7231
+ maxHeight: config.maxHeight ?? 450,
7232
+ showValue,
7233
+ displayField
7234
+ }
7235
+ )
7236
+ ]
7237
+ }
7238
+ )
7239
+ ] });
7240
+ });
6693
7241
  export {
6694
7242
  Alert,
6695
7243
  Badge,
@@ -6747,6 +7295,7 @@ export {
6747
7295
  ImagePreview,
6748
7296
  Input,
6749
7297
  InputNumber,
7298
+ InputSelect,
6750
7299
  KLineChart,
6751
7300
  LineChart,
6752
7301
  LineShareChart,
@@ -6772,6 +7321,7 @@ export {
6772
7321
  Segmented,
6773
7322
  Select,
6774
7323
  SelectDateRange,
7324
+ SelectionPanel,
6775
7325
  SiaChart,
6776
7326
  Slider,
6777
7327
  Space,
@@ -6806,6 +7356,7 @@ export {
6806
7356
  WordCloudChart,
6807
7357
  XYChart,
6808
7358
  applySiaTheme,
7359
+ buildFlatTree,
6809
7360
  chartColor,
6810
7361
  chartTicks,
6811
7362
  createBaiduMapHost,