lukasbach/react-complex-tree

On Custom Render Item Folder expansion not working

Closed this issue · 2 comments

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.

Screenshot from 2025-01-15 00-50-06

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.

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.

thanks !