@darksnow-ui/node-tree-react
Version:
Ready-to-use React component for node-tree-headless
545 lines (536 loc) • 19.7 kB
JavaScript
;
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