On Custom Render Item Folder expansion not working
Closed this issue · 2 comments
ghoshpushpendu commented
import React, { useState, useEffect, useRef } from "react";
import { UncontrolledTreeEnvironment, Tree, StaticTreeDataProvider } from "react-complex-tree";
import "react-complex-tree/lib/style-modern.css";
import "./FileBrowser.scss";
// Initial file tree data
const initialFileTreeData = {
root: {
index: "root",
isFolder: true,
children: ["src", "public", "package.json"],
data: "Project",
},
src: {
index: "src",
isFolder: true,
children: ["index.js", "styles.css", "components"],
data: "src",
},
"index.js": {
index: "index.js",
data: "index.js",
},
"styles.css": {
index: "styles.css",
data: "styles.css",
},
components: {
index: "components",
isFolder: true,
children: ["Header.js", "Footer.js"],
data: "components",
},
"Header.js": {
index: "Header.js",
data: "Header.js",
},
"Footer.js": {
index: "Footer.js",
data: "Footer.js",
},
public: {
index: "public",
isFolder: true,
children: ["index.html", "favicon.ico"],
data: "public",
},
"index.html": {
index: "index.html",
data: "index.html",
},
"favicon.ico": {
index: "favicon.ico",
data: "favicon.ico",
},
"package.json": {
index: "package.json",
data: "package.json",
},
};
const FileBrowser = () => {
const [fileTreeData, setFileTreeData] = useState(initialFileTreeData);
const [editingItem, setEditingItem] = useState(null); // Track which item is being edited
const [newItemName, setNewItemName] = useState(""); // Track new item name input
const [isCreatingFile, setIsCreatingFile] = useState(false); // Track if creating a new file
const [isCreatingFolder, setIsCreatingFolder] = useState(false); // Track if creating a new folder
const [contextMenu, setContextMenu] = useState({ visible: false, x: 0, y: 0, itemId: null }); // Context menu state
const [expandedItems, setExpandedItems] = useState([]); // Track expanded items
// Refs for input fields
const renameInputRef = useRef(null);
const createInputRef = useRef(null);
// Convert the file tree data into a format that React Complex Tree can understand
const dataProvider = new StaticTreeDataProvider(fileTreeData, (item, newName) => ({
...item,
data: newName,
}));
// Handle renaming an item
const handleRename = (itemId, newName) => {
if (!newName.trim()) return; // Prevent empty names
const updatedData = { ...fileTreeData };
updatedData[itemId].data = newName;
setFileTreeData(updatedData);
setEditingItem(null); // Stop editing after renaming
};
// Handle adding a new file or folder
const handleAddItem = (parentId, newItem, isFolder) => {
if (!newItem.trim()) return; // Prevent empty names
const updatedData = { ...fileTreeData };
const newItemId = `${newItem.toLowerCase().replace(/\s+/g, "-")}-${Date.now()}`;
// Add the new item to the parent's children
updatedData[parentId].children.push(newItemId);
// Add the new item to the tree data
updatedData[newItemId] = {
index: newItemId,
data: newItem,
isFolder: isFolder,
children: isFolder ? [] : undefined,
};
setFileTreeData(updatedData);
setIsCreatingFile(false);
setIsCreatingFolder(false);
setNewItemName("");
};
// Handle right-click to show context menu
const handleContextMenu = (e, item) => {
e.preventDefault();
console.log("Right-clicked on:", item); // Debugging
setContextMenu({ visible: true, x: e.clientX, y: e.clientY, itemId: item.index });
};
// Handle context menu actions
const handleContextMenuAction = (action, itemId) => {
switch (action) {
case "newFile":
setIsCreatingFile(true);
break;
case "newFolder":
setIsCreatingFolder(true);
break;
case "rename":
setEditingItem(itemId);
setNewItemName(fileTreeData[itemId].data);
break;
default:
break;
}
setContextMenu({ visible: false, x: 0, y: 0, itemId: null });
};
// Close input fields when clicking outside
useEffect(() => {
const handleClickOutside = (e) => {
// Close rename input if clicked outside
if (renameInputRef.current && !renameInputRef.current.contains(e.target)) {
setEditingItem(null);
}
// Close create input if clicked outside
if (createInputRef.current && !createInputRef.current.contains(e.target)) {
setIsCreatingFile(false);
setIsCreatingFolder(false);
}
};
// Attach the event listener
document.addEventListener("mousedown", handleClickOutside);
// Cleanup the event listener
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
// Handle primary action (click or tap)
const handlePrimaryAction = (item) => {
console.log("Primary action triggered for:", item); // Debugging
if (item.isFolder) {
// Toggle expanded state for folders
setExpandedItems((prevExpanded) =>
prevExpanded.includes(item.index)
? prevExpanded.filter((id) => id !== item.index)
: [...prevExpanded, item.index]
);
} else {
// Enter edit mode for files
setEditingItem(item.index);
setNewItemName(item.data);
}
};
// Custom renderer for tree items
const renderItem = ({ item, context }) => {
return (
<div
onClick={() => handlePrimaryAction(item)}
onContextMenu={(e) => handleContextMenu(e, item)} // Attach onContextMenu here
style={{ padding: "4px 8px", cursor: "pointer" }}
>
{item.isFolder ? (
<span style={{ marginRight: "4px" }}>
{expandedItems.includes(item.index) ? "▼" : "▶"} {/* Arrow icon for folders */}
</span>
) : null}
{item.data}
</div>
);
};
return (
<div className="file-browser">
<h4>File Browser</h4>
{/* Context Menu */}
{contextMenu.visible && (
<div
className="context-menu"
style={{ top: contextMenu.y, left: contextMenu.x }}
>
<button onClick={() => handleContextMenuAction("newFile", contextMenu.itemId)}>
New File
</button>
<button onClick={() => handleContextMenuAction("newFolder", contextMenu.itemId)}>
New Folder
</button>
<button onClick={() => handleContextMenuAction("rename", contextMenu.itemId)}>
Rename
</button>
</div>
)}
{/* Create New File/Folder Input */}
{(isCreatingFile || isCreatingFolder) && (
<div className="create-item" ref={createInputRef}>
<input
type="text"
placeholder={isCreatingFile ? "New file name" : "New folder name"}
value={newItemName}
onChange={(e) => setNewItemName(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
handleAddItem(contextMenu.itemId, newItemName, isCreatingFolder);
}
}}
autoFocus
/>
<button onClick={() => handleAddItem(contextMenu.itemId, newItemName, isCreatingFolder)}>
Create
</button>
<button onClick={() => { setIsCreatingFile(false); setIsCreatingFolder(false); }}>
Cancel
</button>
</div>
)}
{/* Rename Input */}
{editingItem && (
<div className="rename-item" ref={renameInputRef}>
<input
type="text"
value={newItemName}
onChange={(e) => setNewItemName(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
handleRename(editingItem, newItemName);
}
}}
onBlur={() => handleRename(editingItem, newItemName)}
autoFocus
/>
</div>
)}
{/* File Tree */}
<UncontrolledTreeEnvironment
dataProvider={dataProvider}
getItemTitle={(item) => item.data}
viewState={{
"file-browser": { expandedItems }, // Sync expandedItems with viewState
}}
canDragAndDrop={true}
canDropOnFolder={true}
canReorderItems={true}
canSearch={true}
canSearchByStartingTyping={true}
onPrimaryAction={handlePrimaryAction} // Use the updated handler
>
<Tree
treeId="file-browser"
rootItem="root"
treeLabel="File Browser"
renderItem={renderItem} // Use the custom renderer
/>
</UncontrolledTreeEnvironment>
</div>
);
};
export default FileBrowser;
This is whole code yoi can give it a try.
Here you can see if if the folder says its expaned its not showing the fil
I need to custom render item to manage additional features.
lukasbach commented
Hi! Please have a look at the guide on custom render hooks, and the minimal viable sample for a custom renderItem implementation: https://rct.lukasbach.com/docs/guides/rendering#minimalistic-example-for-custom-render-hooks
There are some properties that are required for the Tree logic to work, that seem to be missing in your implementation, such as context.interactiveElementProps and context.itemContainerWithoutChildrenProps/context.itemContainerWithChildrenProps.
ghoshpushpendu commented
thanks !
