@darksnow-ui/node-tree-react
Version:
Ready-to-use React component for node-tree-headless
549 lines (542 loc) • 21.3 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
});
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 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: "flex-shrink-0 mr-2", 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"
// isFocus && "font-semibold",
),
"aria-selected": isSelected,
children: renderLabel ? renderLabel(node) : node.label
}
)
]
}
);
}
// src/components/ContextMenu.tsx
var import_node_tree_headless3 = require("@darksnow-ui/node-tree-headless");
var import_react2 = require("react");
var import_lucide_react2 = require("lucide-react");
var import_jsx_runtime3 = require("react/jsx-runtime");
function ContextMenu({
x,
y,
nodeId,
containerElementRef,
onClose,
className,
onAction
}) {
const { services, controller } = (0, import_node_tree_headless3.useNodeTree)();
const menuRef = (0, import_react2.useRef)(null);
const nodes = services.store.getState().nodes;
const isRoot = nodeId === "root" || !nodeId;
const node = isRoot ? null : nodes.get(nodeId);
const isExpanded = !isRoot && services.store.getState().expandedNodes.has(nodeId);
(0, import_react2.useEffect)(() => {
menuRef.current?.focus();
return () => {
containerElementRef?.current?.focus();
};
}, [containerElementRef]);
(0, import_react2.useEffect)(() => {
const handleKeyDown = (e) => {
if (e.key === "Escape") {
onClose();
}
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [onClose]);
const handleAction = async (action, actionName) => {
await action();
if (actionName && onAction) {
onAction(actionName, nodeId);
}
onClose();
};
if (isRoot) {
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
"div",
{
ref: menuRef,
className: className || "context-menu",
style: { left: x, top: y },
onClick: (e) => e.stopPropagation(),
tabIndex: -1,
children: [
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
"button",
{
className: "context-menu-item",
onClick: () => handleAction(async () => {
const name = prompt("New folder name:");
if (name) {
await controller.createNode(void 0, name);
}
}, "create-folder"),
children: [
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.FolderPlus, { className: "w-4 h-4" }),
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "New Folder" })
]
}
),
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
"button",
{
className: "context-menu-item",
onClick: () => handleAction(async () => {
const name = prompt("New file name:");
if (name) {
await controller.createNode(void 0, name);
}
}, "create-file"),
children: [
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.FilePlus, { className: "w-4 h-4" }),
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "New File" })
]
}
),
services.store.getState().clipboardNodes?.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "context-menu-divider" }),
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
"button",
{
className: "context-menu-item",
onClick: () => handleAction(async () => {
await controller.pasteNodes(void 0);
}, "paste"),
children: [
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.Clipboard, { className: "w-4 h-4" }),
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "Paste" })
]
}
)
] })
]
}
);
}
if (!node) return null;
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
"div",
{
ref: menuRef,
className: className || "context-menu",
style: { left: x, top: y },
onClick: (e) => e.stopPropagation(),
tabIndex: -1,
children: [
node.expandable && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
"button",
{
className: "context-menu-item",
onClick: () => handleAction(
() => {
if (isExpanded) {
services.navigationService.collapseNode(nodeId);
} else {
services.navigationService.expandNode(nodeId);
}
},
isExpanded ? "collapse" : "expand"
),
children: isExpanded ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.FolderClosed, { className: "w-4 h-4" }),
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "Collapse" })
] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.FolderOpen, { className: "w-4 h-4" }),
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "Expand" })
] })
}
),
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "context-menu-divider" })
] }),
node.expandable && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
"button",
{
className: "context-menu-item",
onClick: () => handleAction(async () => {
const name = prompt("New folder name:");
if (name) {
await controller.createNode(nodeId, name);
}
}, "create-folder"),
children: [
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.FolderPlus, { className: "w-4 h-4" }),
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "New Folder" })
]
}
),
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
"button",
{
className: "context-menu-item",
onClick: () => handleAction(async () => {
const name = prompt("New file name:");
if (name) {
await controller.createNode(nodeId, name);
}
}, "create-file"),
children: [
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.FilePlus, { className: "w-4 h-4" }),
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "New File" })
]
}
),
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "context-menu-divider" })
] }),
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
"button",
{
className: "context-menu-item",
onClick: () => handleAction(() => {
controller.copyNodes([nodeId]);
}, "copy"),
children: [
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.Copy, { className: "w-4 h-4" }),
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "Copy" })
]
}
),
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
"button",
{
className: "context-menu-item",
onClick: () => handleAction(() => {
controller.cutNodes([nodeId]);
}, "cut"),
children: [
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.Scissors, { className: "w-4 h-4" }),
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "Cut" })
]
}
),
node.expandable && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
"button",
{
className: "context-menu-item",
onClick: () => handleAction(async () => {
await controller.pasteNodes(nodeId);
}, "paste"),
children: [
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.Clipboard, { className: "w-4 h-4" }),
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "Paste" })
]
}
),
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "context-menu-divider" }),
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
"button",
{
className: "context-menu-item",
onClick: () => handleAction(async () => {
await controller.renameNode(nodeId);
}, "rename"),
children: [
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.Edit2, { className: "w-4 h-4" }),
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "Rename" })
]
}
),
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
"button",
{
className: "context-menu-item context-menu-item-danger",
onClick: () => handleAction(async () => {
await controller.deleteNodes([nodeId]);
}, "delete"),
children: [
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.Trash2, { className: "w-4 h-4" }),
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "Delete" })
]
}
)
]
}
);
}
// src/components/NodeTree.tsx
var import_react3 = require("react");
var import_clsx3 = require("clsx");
var import_jsx_runtime4 = require("react/jsx-runtime");
function NodeTree({
className,
style,
renderNode,
renderEmptyState,
onContextMenuAction,
indentSize = 20,
showContextMenu = true,
contextMenuClassName,
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_react3.useState)(false);
const defaultRenderNode = (0, import_react3.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_runtime4.jsx)(
TreeNode,
{
row: enhancedRow,
indentSize,
resolveIcon
},
row.id
);
},
[nodes, context.services.store, indentSize]
);
const defaultRenderEmptyState = () => /* @__PURE__ */ (0, import_jsx_runtime4.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_runtime4.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_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
/* @__PURE__ */ (0, import_jsx_runtime4.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_runtime4.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_runtime4.jsx)(
"div",
{
...context.handlers.rootDropProps,
style: { flex: 1, display: "flex" },
children: renderRootDropArea ? renderRootDropArea(dragOverId === "root") : defaultRenderRootDropArea(dragOverId === "root")
}
)
]
}
),
showContextMenu && contextMenu && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
ContextMenu,
{
x: contextMenu.x,
y: contextMenu.y,
nodeId: contextMenu.nodeId,
containerElementRef: contextMenu.containerElementRef,
onClose: closeContextMenu,
className: contextMenuClassName,
onAction: onContextMenuAction
}
)
] });
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
ContextMenu,
NodeTree,
NodeWrapper,
TreeNode
});
//# sourceMappingURL=index.js.map