@particle-academy/react-fancy 1.9.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +217 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -2
- package/dist/index.d.ts +19 -2
- package/dist/index.js +217 -26
- package/dist/index.js.map +1 -1
- package/docs/TreeNav.md +78 -0
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -10564,12 +10564,67 @@ function ChevronIcon({ open }) {
|
|
|
10564
10564
|
}
|
|
10565
10565
|
);
|
|
10566
10566
|
}
|
|
10567
|
+
function isDescendantOf(nodes, ancestorId, targetId) {
|
|
10568
|
+
function findNode(haystack, id) {
|
|
10569
|
+
for (const n of haystack) {
|
|
10570
|
+
if (n.id === id) return n;
|
|
10571
|
+
if (n.children) {
|
|
10572
|
+
const found = findNode(n.children, id);
|
|
10573
|
+
if (found) return found;
|
|
10574
|
+
}
|
|
10575
|
+
}
|
|
10576
|
+
return void 0;
|
|
10577
|
+
}
|
|
10578
|
+
function hasDescendant(node, id) {
|
|
10579
|
+
if (!node.children) return false;
|
|
10580
|
+
for (const child of node.children) {
|
|
10581
|
+
if (child.id === id) return true;
|
|
10582
|
+
if (hasDescendant(child, id)) return true;
|
|
10583
|
+
}
|
|
10584
|
+
return false;
|
|
10585
|
+
}
|
|
10586
|
+
const ancestor = findNode(nodes, ancestorId);
|
|
10587
|
+
return ancestor ? hasDescendant(ancestor, targetId) : false;
|
|
10588
|
+
}
|
|
10589
|
+
function computeDropPosition(e, isFolder) {
|
|
10590
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
10591
|
+
const offsetY = e.clientY - rect.top;
|
|
10592
|
+
const third = rect.height / 3;
|
|
10593
|
+
if (offsetY < third) return "before";
|
|
10594
|
+
if (offsetY > third * 2) return "after";
|
|
10595
|
+
return isFolder ? "inside" : "after";
|
|
10596
|
+
}
|
|
10567
10597
|
function TreeNode({ node, depth }) {
|
|
10568
|
-
const {
|
|
10598
|
+
const {
|
|
10599
|
+
selectedId,
|
|
10600
|
+
onSelect,
|
|
10601
|
+
onNodeContextMenu,
|
|
10602
|
+
expandedIds,
|
|
10603
|
+
toggle,
|
|
10604
|
+
indentSize,
|
|
10605
|
+
showIcons,
|
|
10606
|
+
draggable,
|
|
10607
|
+
dragState,
|
|
10608
|
+
setDragState,
|
|
10609
|
+
onNodeMove,
|
|
10610
|
+
nodes,
|
|
10611
|
+
expandNode
|
|
10612
|
+
} = useTreeNav();
|
|
10569
10613
|
const isFolder = node.type === "folder" || node.children && node.children.length > 0;
|
|
10570
10614
|
const isExpanded = expandedIds.includes(node.id);
|
|
10571
10615
|
const isSelected = selectedId === node.id;
|
|
10572
10616
|
const paddingLeft = depth * indentSize + 4;
|
|
10617
|
+
const isDragging = dragState.draggedNodeId === node.id;
|
|
10618
|
+
const isDropTarget = dragState.dropTargetId === node.id;
|
|
10619
|
+
const dropPosition = isDropTarget ? dragState.dropPosition : null;
|
|
10620
|
+
const autoExpandTimer = react.useRef(null);
|
|
10621
|
+
const clearAutoExpand = react.useCallback(() => {
|
|
10622
|
+
if (autoExpandTimer.current) {
|
|
10623
|
+
clearTimeout(autoExpandTimer.current);
|
|
10624
|
+
autoExpandTimer.current = null;
|
|
10625
|
+
}
|
|
10626
|
+
}, []);
|
|
10627
|
+
react.useEffect(() => clearAutoExpand, [clearAutoExpand]);
|
|
10573
10628
|
const handleClick = () => {
|
|
10574
10629
|
if (node.disabled) return;
|
|
10575
10630
|
if (isFolder) {
|
|
@@ -10583,30 +10638,116 @@ function TreeNode({ node, depth }) {
|
|
|
10583
10638
|
onNodeContextMenu(e, node);
|
|
10584
10639
|
}
|
|
10585
10640
|
};
|
|
10586
|
-
|
|
10587
|
-
|
|
10588
|
-
|
|
10589
|
-
|
|
10590
|
-
|
|
10591
|
-
|
|
10592
|
-
|
|
10593
|
-
|
|
10594
|
-
|
|
10595
|
-
|
|
10596
|
-
|
|
10597
|
-
|
|
10598
|
-
|
|
10599
|
-
|
|
10600
|
-
|
|
10601
|
-
|
|
10602
|
-
|
|
10603
|
-
|
|
10604
|
-
|
|
10605
|
-
|
|
10641
|
+
const handleDragStart = react.useCallback((e) => {
|
|
10642
|
+
e.dataTransfer.setData("text/plain", node.id);
|
|
10643
|
+
e.dataTransfer.effectAllowed = "move";
|
|
10644
|
+
requestAnimationFrame(() => {
|
|
10645
|
+
setDragState({ draggedNodeId: node.id, dropTargetId: null, dropPosition: null });
|
|
10646
|
+
});
|
|
10647
|
+
}, [node.id, setDragState]);
|
|
10648
|
+
const handleDragEnd = react.useCallback(() => {
|
|
10649
|
+
clearAutoExpand();
|
|
10650
|
+
setDragState({ draggedNodeId: null, dropTargetId: null, dropPosition: null });
|
|
10651
|
+
}, [clearAutoExpand, setDragState]);
|
|
10652
|
+
const handleDragOver = react.useCallback((e) => {
|
|
10653
|
+
if (!dragState.draggedNodeId) return;
|
|
10654
|
+
const sourceId = dragState.draggedNodeId;
|
|
10655
|
+
if (sourceId === node.id) return;
|
|
10656
|
+
if (isDescendantOf(nodes, sourceId, node.id)) return;
|
|
10657
|
+
e.preventDefault();
|
|
10658
|
+
e.stopPropagation();
|
|
10659
|
+
e.dataTransfer.dropEffect = "move";
|
|
10660
|
+
const position = computeDropPosition(e, !!isFolder);
|
|
10661
|
+
if (isFolder && !isExpanded && position === "inside") {
|
|
10662
|
+
if (!autoExpandTimer.current) {
|
|
10663
|
+
autoExpandTimer.current = setTimeout(() => {
|
|
10664
|
+
expandNode(node.id);
|
|
10665
|
+
autoExpandTimer.current = null;
|
|
10666
|
+
}, 500);
|
|
10606
10667
|
}
|
|
10607
|
-
|
|
10608
|
-
|
|
10609
|
-
|
|
10668
|
+
} else {
|
|
10669
|
+
clearAutoExpand();
|
|
10670
|
+
}
|
|
10671
|
+
if (dragState.dropTargetId !== node.id || dragState.dropPosition !== position) {
|
|
10672
|
+
setDragState({ draggedNodeId: sourceId, dropTargetId: node.id, dropPosition: position });
|
|
10673
|
+
}
|
|
10674
|
+
}, [dragState, node.id, isFolder, isExpanded, nodes, setDragState, expandNode, clearAutoExpand]);
|
|
10675
|
+
const handleDragLeave = react.useCallback((e) => {
|
|
10676
|
+
if (!e.currentTarget.contains(e.relatedTarget)) {
|
|
10677
|
+
clearAutoExpand();
|
|
10678
|
+
if (dragState.dropTargetId === node.id) {
|
|
10679
|
+
setDragState({ ...dragState, dropTargetId: null, dropPosition: null });
|
|
10680
|
+
}
|
|
10681
|
+
}
|
|
10682
|
+
}, [dragState, node.id, setDragState, clearAutoExpand]);
|
|
10683
|
+
const handleDrop = react.useCallback((e) => {
|
|
10684
|
+
e.preventDefault();
|
|
10685
|
+
e.stopPropagation();
|
|
10686
|
+
clearAutoExpand();
|
|
10687
|
+
const sourceId = dragState.draggedNodeId;
|
|
10688
|
+
const position = dragState.dropPosition;
|
|
10689
|
+
if (!sourceId || !position) return;
|
|
10690
|
+
if (sourceId === node.id) return;
|
|
10691
|
+
if (isDescendantOf(nodes, sourceId, node.id)) return;
|
|
10692
|
+
onNodeMove?.(sourceId, node.id, position);
|
|
10693
|
+
setDragState({ draggedNodeId: null, dropTargetId: null, dropPosition: null });
|
|
10694
|
+
}, [dragState, node.id, nodes, onNodeMove, setDragState, clearAutoExpand]);
|
|
10695
|
+
const canDrag = draggable && !node.disabled;
|
|
10696
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
10697
|
+
"div",
|
|
10698
|
+
{
|
|
10699
|
+
"data-react-fancy-tree-node": "",
|
|
10700
|
+
onDragOver: draggable ? handleDragOver : void 0,
|
|
10701
|
+
onDragLeave: draggable ? handleDragLeave : void 0,
|
|
10702
|
+
onDrop: draggable ? handleDrop : void 0,
|
|
10703
|
+
children: [
|
|
10704
|
+
isDropTarget && dropPosition === "before" && /* @__PURE__ */ jsxRuntime.jsx(
|
|
10705
|
+
"div",
|
|
10706
|
+
{
|
|
10707
|
+
"data-react-fancy-tree-drop-indicator": "before",
|
|
10708
|
+
className: "pointer-events-none h-0.5 rounded-full bg-blue-500",
|
|
10709
|
+
style: { marginLeft: paddingLeft }
|
|
10710
|
+
}
|
|
10711
|
+
),
|
|
10712
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
10713
|
+
"button",
|
|
10714
|
+
{
|
|
10715
|
+
type: "button",
|
|
10716
|
+
draggable: canDrag,
|
|
10717
|
+
onDragStart: canDrag ? handleDragStart : void 0,
|
|
10718
|
+
onDragEnd: canDrag ? handleDragEnd : void 0,
|
|
10719
|
+
onClick: handleClick,
|
|
10720
|
+
onContextMenu: handleContextMenu,
|
|
10721
|
+
disabled: node.disabled,
|
|
10722
|
+
className: cn(
|
|
10723
|
+
"flex w-full items-center gap-1 rounded-md py-0.5 text-left text-[13px] transition-colors",
|
|
10724
|
+
isSelected ? "bg-blue-500/15 text-blue-600 dark:text-blue-400" : "text-zinc-700 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800",
|
|
10725
|
+
node.disabled && "pointer-events-none opacity-40",
|
|
10726
|
+
isDragging && "opacity-50",
|
|
10727
|
+
canDrag && "cursor-grab active:cursor-grabbing",
|
|
10728
|
+
isDropTarget && dropPosition === "inside" && "bg-blue-500/10 ring-1 ring-blue-500/30 ring-inset"
|
|
10729
|
+
),
|
|
10730
|
+
style: { paddingLeft },
|
|
10731
|
+
children: [
|
|
10732
|
+
isFolder && /* @__PURE__ */ jsxRuntime.jsx(ChevronIcon, { open: isExpanded }),
|
|
10733
|
+
!isFolder && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "w-3.5 shrink-0" }),
|
|
10734
|
+
showIcons && (node.icon ?? (isFolder ? /* @__PURE__ */ jsxRuntime.jsx(FolderIcon, { open: isExpanded }) : /* @__PURE__ */ jsxRuntime.jsx(FileIcon, { ext: node.ext ?? node.label.split(".").pop() }))),
|
|
10735
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: node.label })
|
|
10736
|
+
]
|
|
10737
|
+
}
|
|
10738
|
+
),
|
|
10739
|
+
isDropTarget && dropPosition === "after" && /* @__PURE__ */ jsxRuntime.jsx(
|
|
10740
|
+
"div",
|
|
10741
|
+
{
|
|
10742
|
+
"data-react-fancy-tree-drop-indicator": "after",
|
|
10743
|
+
className: "pointer-events-none h-0.5 rounded-full bg-blue-500",
|
|
10744
|
+
style: { marginLeft: paddingLeft }
|
|
10745
|
+
}
|
|
10746
|
+
),
|
|
10747
|
+
isFolder && isExpanded && node.children && /* @__PURE__ */ jsxRuntime.jsx("div", { "data-react-fancy-tree-node-children": "", children: node.children.map((child) => /* @__PURE__ */ jsxRuntime.jsx(TreeNode, { node: child, depth: depth + 1 }, child.id)) })
|
|
10748
|
+
]
|
|
10749
|
+
}
|
|
10750
|
+
);
|
|
10610
10751
|
}
|
|
10611
10752
|
TreeNode.displayName = "TreeNode";
|
|
10612
10753
|
function collectFolderIds(nodes) {
|
|
@@ -10619,11 +10760,18 @@ function collectFolderIds(nodes) {
|
|
|
10619
10760
|
}
|
|
10620
10761
|
return ids;
|
|
10621
10762
|
}
|
|
10763
|
+
var EMPTY_DRAG_STATE = {
|
|
10764
|
+
draggedNodeId: null,
|
|
10765
|
+
dropTargetId: null,
|
|
10766
|
+
dropPosition: null
|
|
10767
|
+
};
|
|
10622
10768
|
function TreeNavRoot({
|
|
10623
10769
|
nodes,
|
|
10624
10770
|
selectedId,
|
|
10625
10771
|
onSelect,
|
|
10626
10772
|
onNodeContextMenu,
|
|
10773
|
+
draggable = false,
|
|
10774
|
+
onNodeMove,
|
|
10627
10775
|
expandedIds: controlledExpanded,
|
|
10628
10776
|
defaultExpandedIds,
|
|
10629
10777
|
onExpandedChange,
|
|
@@ -10649,15 +10797,58 @@ function TreeNavRoot({
|
|
|
10649
10797
|
},
|
|
10650
10798
|
[expandedIds, isControlled, onExpandedChange]
|
|
10651
10799
|
);
|
|
10800
|
+
const expandNode = react.useCallback(
|
|
10801
|
+
(id) => {
|
|
10802
|
+
if (expandedIds.includes(id)) return;
|
|
10803
|
+
const next = [...expandedIds, id];
|
|
10804
|
+
if (!isControlled) {
|
|
10805
|
+
setInternalExpanded(next);
|
|
10806
|
+
}
|
|
10807
|
+
onExpandedChange?.(next);
|
|
10808
|
+
},
|
|
10809
|
+
[expandedIds, isControlled, onExpandedChange]
|
|
10810
|
+
);
|
|
10811
|
+
const [dragState, setDragState] = react.useState(EMPTY_DRAG_STATE);
|
|
10812
|
+
const handleDragEnd = react.useCallback(() => {
|
|
10813
|
+
setDragState(EMPTY_DRAG_STATE);
|
|
10814
|
+
}, []);
|
|
10652
10815
|
const ctx = react.useMemo(
|
|
10653
|
-
() => ({
|
|
10654
|
-
|
|
10816
|
+
() => ({
|
|
10817
|
+
selectedId,
|
|
10818
|
+
onSelect,
|
|
10819
|
+
onNodeContextMenu,
|
|
10820
|
+
expandedIds,
|
|
10821
|
+
toggle,
|
|
10822
|
+
indentSize,
|
|
10823
|
+
showIcons,
|
|
10824
|
+
draggable,
|
|
10825
|
+
dragState,
|
|
10826
|
+
setDragState,
|
|
10827
|
+
onNodeMove,
|
|
10828
|
+
nodes,
|
|
10829
|
+
expandNode
|
|
10830
|
+
}),
|
|
10831
|
+
[
|
|
10832
|
+
selectedId,
|
|
10833
|
+
onSelect,
|
|
10834
|
+
onNodeContextMenu,
|
|
10835
|
+
expandedIds,
|
|
10836
|
+
toggle,
|
|
10837
|
+
indentSize,
|
|
10838
|
+
showIcons,
|
|
10839
|
+
draggable,
|
|
10840
|
+
dragState,
|
|
10841
|
+
onNodeMove,
|
|
10842
|
+
nodes,
|
|
10843
|
+
expandNode
|
|
10844
|
+
]
|
|
10655
10845
|
);
|
|
10656
10846
|
return /* @__PURE__ */ jsxRuntime.jsx(TreeNavContext.Provider, { value: ctx, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
10657
10847
|
"nav",
|
|
10658
10848
|
{
|
|
10659
10849
|
"data-react-fancy-tree-nav": "",
|
|
10660
10850
|
className: cn("flex flex-col gap-0.5 py-1 text-sm", className),
|
|
10851
|
+
onDragEnd: draggable ? handleDragEnd : void 0,
|
|
10661
10852
|
children: nodes.map((node) => /* @__PURE__ */ jsxRuntime.jsx(TreeNode, { node, depth: 0 }, node.id))
|
|
10662
10853
|
}
|
|
10663
10854
|
) });
|