UNPKG

@darksnow-ui/node-tree-react

Version:
545 lines (536 loc) 19.7 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { ContextMenu: () => ContextMenu, NodeTree: () => NodeTree, NodeWrapper: () => NodeWrapper, TreeNode: () => TreeNode, useNodeTreeMenuItems: () => useNodeTreeMenuItems }); module.exports = __toCommonJS(index_exports); // src/components/NodeTree.tsx var import_node_tree_headless4 = require("@darksnow-ui/node-tree-headless"); // src/components/TreeNode.tsx var import_node_tree_headless2 = require("@darksnow-ui/node-tree-headless"); var import_lucide_react = require("lucide-react"); var import_clsx2 = require("clsx"); // src/components/NodeWrapper.tsx var import_node_tree_headless = require("@darksnow-ui/node-tree-headless"); var import_react = require("react"); var import_clsx = require("clsx"); var import_jsx_runtime = require("react/jsx-runtime"); var NodeWrapper = (0, import_react.memo)( ({ id, children, className }) => { const { handlers } = (0, import_node_tree_headless.useNodeTree)(); const wrapperProps = (0, import_react.useMemo)( () => handlers.wrapperProps(id), [handlers, id] ); const isSelected = (0, import_node_tree_headless.useNodeTreeState)((s) => s.selectedNodes.has(id)); const isFocused = (0, import_node_tree_headless.useNodeTreeState)((s) => s.focusId === id); const isCursor = (0, import_node_tree_headless.useNodeTreeState)((s) => s.cursorId === id); const isDragOver = (0, import_node_tree_headless.useNodeTreeState)((s) => s.dragOverId === id); const isDragging = (0, import_node_tree_headless.useNodeTreeState)((s) => s.dragStartId === id); return /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "div", { ...wrapperProps, "data-node-id": id, className: (0, import_clsx.clsx)( "node-wrapper", className, isSelected && "selected", isFocused && "focused", isCursor && "cursor", isDragOver && "drag-over", isDragging && "dragging" ), children } ); } ); NodeWrapper.displayName = "NodeWrapper"; // src/components/TreeNode.tsx var import_jsx_runtime2 = require("react/jsx-runtime"); function TreeNode({ row, renderLabel, resolveIcon, indentSize = 20 }) { const { node } = row; const isExpanded = (0, import_node_tree_headless2.useNodeTreeState)((s) => s.expandedNodes.has(node.id)); const isSelected = (0, import_node_tree_headless2.useNodeTreeState)((s) => s.selectedNodes.has(node.id)); const isCursor = (0, import_node_tree_headless2.useNodeTreeState)((s) => s.cursorId === node.id); const isFocus = (0, import_node_tree_headless2.useNodeTreeState)((s) => s.focusId === node.id); const isDragOver = (0, import_node_tree_headless2.useNodeTreeState)((s) => s.dragOverId === node.id); const isCut = (0, import_node_tree_headless2.useNodeTreeState)( (s) => s.clipboardOperation === "cut" && s.clipboardNodeIds?.has(node.id) ); const defaultIconResolver = (node2, isExpanded2) => { if (!node2.expandable) { const ext = node2.label.split(".").pop()?.toLowerCase(); switch (ext) { case "ts": return "typescript"; case "tsx": return "react_ts"; case "js": return "javascript"; case "jsx": return "react"; case "json": return "json"; case "md": return "markdown"; default: return "file"; } } return isExpanded2 ? "folder-open" : "folder"; }; const iconName = resolveIcon ? resolveIcon(node, isExpanded) : defaultIconResolver(node, isExpanded); return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)( NodeWrapper, { id: node.id, className: (0, import_clsx2.clsx)( "group flex items-center px-2 cursor-pointer relative", "hover:bg-vscode-list-hoverBackground", isSelected && "bg-vscode-list-activeSelectionBackground", isCursor && "outline outline-1 outline-vscode-focusBorder", isDragOver && "bg-vscode-list-dropBackground", node.expandable && "expandable" ), children: [ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { width: `${row.level * indentSize}px` } }), /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "flex items-center justify-center w-5 h-5", children: node.expandable ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( "span", { className: (0, import_clsx2.clsx)( "flex items-center justify-center w-full h-full", "hover:bg-vscode-border/50 rounded transition-colors" ), "aria-expanded": isExpanded, children: isExpanded ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react.ChevronDown, { className: "w-3 h-3" }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react.ChevronRight, { className: "w-3 h-3" }) } ) : null }), /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: (0, import_clsx2.clsx)("flex-shrink-0 mr-2", isCut && "opacity-50"), children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("i", { className: `icon icon-${iconName}` }) }), /* @__PURE__ */ (0, import_jsx_runtime2.jsx)( "span", { className: (0, import_clsx2.clsx)( "flex-1 text-sm truncate", isSelected ? "text-vscode-list-activeSelectionForeground" : "text-vscode-foreground", isCut && "opacity-50" // isFocus && "font-semibold", ), "aria-selected": isSelected, children: renderLabel ? renderLabel(node) : node.label } ) ] } ); } // src/components/ContextMenu.tsx var import_react3 = require("react"); var import_menus = require("@darksnow-ui/menus"); // src/hooks/useNodeTreeMenuItems.tsx var import_react2 = require("react"); var import_node_tree_headless3 = require("@darksnow-ui/node-tree-headless"); var import_lucide_react2 = require("lucide-react"); var import_jsx_runtime3 = require("react/jsx-runtime"); function useNodeTreeMenuItems(nodeId, options = {}) { const { services, controller } = (0, import_node_tree_headless3.useNodeTree)(); const nodes = (0, import_node_tree_headless3.useNodeTreeState)((state) => state.nodes); const expandedNodes = (0, import_node_tree_headless3.useNodeTreeState)((state) => state.expandedNodes); const clipboardNodeIds = (0, import_node_tree_headless3.useNodeTreeState)((state) => state.clipboardNodeIds); const isRoot = nodeId === "root" || !nodeId; const node = isRoot ? null : nodes.get(nodeId) || null; const isExpanded = !isRoot && expandedNodes.has(nodeId); const hasClipboard = clipboardNodeIds && clipboardNodeIds.size > 0; const context = { nodeId, node, isRoot, isExpanded, controller, services }; const defaultItems = (0, import_react2.useMemo)(() => { if (isRoot) { const items2 = [ { type: "item", label: "New Folder", icon: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.FolderPlus, { className: "h-4 w-4" }), handle: async (ctx) => { const name = prompt("New folder name:"); if (name) { await ctx.controller.createNode(void 0, name); options.onAction?.("create-folder", ctx.nodeId); } } }, { type: "item", label: "New File", icon: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.FilePlus, { className: "h-4 w-4" }), handle: async (ctx) => { const name = prompt("New file name:"); if (name) { await ctx.controller.createNode(void 0, name); options.onAction?.("create-file", ctx.nodeId); } } } ]; if (hasClipboard) { items2.push( { type: "separator" }, { type: "item", label: "Paste", icon: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.Clipboard, { className: "h-4 w-4" }), shortcut: "Ctrl+V", handle: async (ctx) => { await ctx.controller.pasteNodes(void 0); options.onAction?.("paste", ctx.nodeId); } } ); } return items2; } const items = []; if (node?.expandable) { items.push({ type: "item", label: isExpanded ? "Collapse" : "Expand", icon: isExpanded ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.FolderClosed, { className: "h-4 w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.FolderOpen, { className: "h-4 w-4" }), handle: (ctx) => { if (ctx.isExpanded) { ctx.services.navigationService.collapseNode(ctx.nodeId); } else { ctx.services.navigationService.expandNode(ctx.nodeId); } options.onAction?.( ctx.isExpanded ? "collapse" : "expand", ctx.nodeId ); } }); items.push({ type: "separator" }); } if (node?.expandable) { items.push( { type: "item", label: "New Folder", icon: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.FolderPlus, { className: "h-4 w-4" }), handle: async (ctx) => { const name = prompt("New folder name:"); if (name) { await ctx.controller.createNode(ctx.nodeId, name); options.onAction?.("create-folder", ctx.nodeId); } } }, { type: "item", label: "New File", icon: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.FilePlus, { className: "h-4 w-4" }), handle: async (ctx) => { const name = prompt("New file name:"); if (name) { await ctx.controller.createNode(ctx.nodeId, name); options.onAction?.("create-file", ctx.nodeId); } } }, { type: "separator" } ); } items.push( { type: "item", label: "Copy", icon: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.Copy, { className: "h-4 w-4" }), shortcut: "Ctrl+C", handle: (ctx) => { ctx.controller.copyNodes([ctx.nodeId]); options.onAction?.("copy", ctx.nodeId); } }, { type: "item", label: "Cut", icon: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.Scissors, { className: "h-4 w-4" }), shortcut: "Ctrl+X", handle: (ctx) => { ctx.controller.cutNodes([ctx.nodeId]); options.onAction?.("cut", ctx.nodeId); } } ); if (node?.expandable && hasClipboard) { items.push({ type: "item", label: "Paste", icon: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.Clipboard, { className: "h-4 w-4" }), shortcut: "Ctrl+V", handle: async (ctx) => { await ctx.controller.pasteNodes(ctx.nodeId); options.onAction?.("paste", ctx.nodeId); } }); } items.push( { type: "separator" }, { type: "item", label: "Rename", icon: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.Edit2, { className: "h-4 w-4" }), shortcut: "F2", handle: async (ctx) => { await ctx.controller.renameNode(ctx.nodeId); options.onAction?.("rename", ctx.nodeId); } }, { type: "item", label: "Delete", icon: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.Trash2, { className: "h-4 w-4" }), className: "text-destructive", shortcut: "Delete", handle: async (ctx) => { await ctx.controller.deleteNodes([ctx.nodeId]); options.onAction?.("delete", ctx.nodeId); } } ); return items; }, [isRoot, node, isExpanded, hasClipboard, options.onAction]); const finalItems = (0, import_react2.useMemo)(() => { if (!options.customItems) { return defaultItems; } const customItems = typeof options.customItems === "function" ? options.customItems(context) : options.customItems; return [...defaultItems, ...customItems]; }, [defaultItems, options.customItems, context]); return { items: finalItems, context }; } // src/components/ContextMenu.tsx var import_jsx_runtime4 = require("react/jsx-runtime"); function ContextMenu({ x, y, nodeId, containerElementRef, onClose, className, onAction, items, useDefaultItems = true }) { const elementRef = (0, import_react3.useRef)(null); const { items: defaultItems, context } = useNodeTreeMenuItems(nodeId, { onAction }); const finalItems = items && items.length > 0 ? items : useDefaultItems ? defaultItems : []; (0, import_react3.useEffect)(() => { return () => { containerElementRef?.current?.focus(); }; }, [containerElementRef]); if (finalItems.length === 0) { return null; } return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)( import_menus.FloatingMenu, { items: finalItems, context, open: true, onOpenChange: (open) => !open && onClose(), x, y, className } ); } // src/components/NodeTree.tsx var import_react4 = require("react"); var import_clsx3 = require("clsx"); var import_jsx_runtime5 = require("react/jsx-runtime"); function NodeTree({ className, style, renderNode, renderEmptyState, onContextMenuAction, indentSize = 20, showContextMenu = true, contextMenuClassName, contextMenuItems, emptyStateClassName, showRootDropArea = true, rootDropAreaClassName, renderRootDropArea, resolveIcon }) { const context = (0, import_node_tree_headless4.useNodeTree)(); const indexedNodes = (0, import_node_tree_headless4.useNodeTreeState)((state) => state.indexedNodes); const nodes = (0, import_node_tree_headless4.useNodeTreeState)((state) => state.nodes); const dragOverId = (0, import_node_tree_headless4.useNodeTreeState)((state) => state.dragOverId); const dragStartId = (0, import_node_tree_headless4.useNodeTreeState)((state) => state.dragStartId); const { contextMenu, closeContextMenu } = (0, import_node_tree_headless4.useContextMenu)(); const [hasFocus, setHasFocus] = (0, import_react4.useState)(false); const defaultRenderNode = (0, import_react4.useCallback)( (row) => { const node = nodes.get(row.id); if (!node) return null; const state = context.services.store.getState(); const enhancedRow = { ...row, node, selected: state.selectedNodes.has(row.id), expanded: state.expandedNodes.has(row.id), visible: true, isCursor: state.cursorId === row.id, isFocus: state.focusId === row.id }; return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)( TreeNode, { row: enhancedRow, indentSize, resolveIcon }, row.id ); }, [nodes, context.services.store, indentSize] ); const defaultRenderEmptyState = () => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)( "div", { className: emptyStateClassName || "text-center py-8 text-gray-500 dark:text-gray-400", children: "No nodes to display" } ); const defaultRenderRootDropArea = (isDragOver) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)( "div", { className: (0, import_clsx3.clsx)( rootDropAreaClassName || "node-tree-root-drop-area", isDragOver && "drag-over" ), style: { flex: 1, minHeight: "20px", display: "flex", alignItems: "center", justifyContent: "center", transition: "all 0.2s ease", cursor: "pointer" } } ); return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)( "div", { ref: context.elementRef, tabIndex: 0, className: (0, import_clsx3.clsx)( "node-tree-container", hasFocus && "has-focus", className ), style: { ...style, display: "flex", flexDirection: "column", height: "100%" }, onFocus: () => setHasFocus(true), onBlur: () => setHasFocus(false), children: [ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "node-tree-content", style: { flexShrink: 0 }, children: indexedNodes && indexedNodes.length > 0 ? indexedNodes.map((row) => { if (renderNode) { const node = nodes.get(row.id); if (!node) return null; const state = context.services.store.getState(); const enhancedRow = { ...row, node, selected: state.selectedNodes.has(row.id), expanded: state.expandedNodes.has(row.id), visible: true, isCursor: state.cursorId === row.id, isFocus: state.focusId === row.id }; return renderNode({ row: enhancedRow }); } return defaultRenderNode(row); }) : renderEmptyState ? renderEmptyState() : defaultRenderEmptyState() }), showRootDropArea && indexedNodes && indexedNodes.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)( "div", { ...context.handlers.rootDropProps, style: { flex: 1, display: "flex" }, children: renderRootDropArea ? renderRootDropArea(dragOverId === "root") : defaultRenderRootDropArea(dragOverId === "root") } ) ] } ), showContextMenu && contextMenu && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)( ContextMenu, { x: contextMenu.x, y: contextMenu.y, nodeId: contextMenu.nodeId, containerElementRef: contextMenu.containerElementRef, onClose: closeContextMenu, className: contextMenuClassName, onAction: onContextMenuAction, items: contextMenuItems ? typeof contextMenuItems === "function" ? contextMenuItems(contextMenu.nodeId) : contextMenuItems : void 0, useDefaultItems: !contextMenuItems } ) ] }); } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { ContextMenu, NodeTree, NodeWrapper, TreeNode, useNodeTreeMenuItems }); //# sourceMappingURL=index.js.map