Skip to content

Instantly share code, notes, and snippets.

@lveillard
Created February 1, 2023 14:48
Show Gist options
  • Save lveillard/a82ef7a64ce238d07e43e6a293d9cb72 to your computer and use it in GitHub Desktop.
Save lveillard/a82ef7a64ce238d07e43e6a293d9cb72 to your computer and use it in GitHub Desktop.
Tree with sortablejs example
import { useCallback } from "react";
import { ReactSortable } from "react-sortablejs";
export interface TreeItem {
id: string;
component: React.ReactElement;
// chosen: boolean;
children?: TreeItem[];
}
const performItemAction = (
items: TreeItem[],
itemIndex: string,
action:
| "up"
| "down"
| "left"
| "right"
| "new"
| "delete"
| "deleteChildren",
setItems: (newItems: TreeItem[]) => void,
createNewItem: (parentId: string) => void,
isRoot = true
) => {
let item = items.find((i) => i.id === itemIndex);
if (!item) {
// item not found in this level, search in children
const newItems: TreeItem[] = [];
items.forEach((childItem) => {
item = childItem.children?.find((c) => c.id === itemIndex);
if (childItem.children && action === "left" && item) {
// LEFT - move item to parent level
newItems.push({
...childItem,
children: childItem.children.filter((c) => c.id !== itemIndex)
});
newItems.push(item);
} else if (childItem.children) {
// recurse
newItems.push({
...childItem,
children: performItemAction(
childItem.children,
itemIndex,
action,
setItems,
createNewItem,
false
)
});
} else {
newItems.push(childItem);
}
});
if (isRoot) {
setItems(newItems);
}
return newItems;
}
// item found in this level
const newItems = [...items];
item = newItems.find((i) => i.id === itemIndex) as TreeItem;
const itemIndexInArray = newItems.indexOf(item);
if (action === "up") {
// UP - move item up in the same level
newItems.splice(
itemIndexInArray - 1,
0,
newItems.splice(itemIndexInArray, 1)[0] as TreeItem
);
} else if (action === "down") {
// DOWN - move item down in the same level
newItems.splice(
itemIndexInArray + 1,
0,
newItems.splice(itemIndexInArray, 1)[0] as TreeItem
);
} else if (action === "right") {
// RIGHT - move item to children of previous item
const previousItem = newItems[itemIndexInArray - 1];
if (previousItem) {
previousItem.children = [...(previousItem.children || []), item];
newItems.splice(itemIndexInArray, 1);
}
} else if (action === "new") {
// NEW - add new item to children of current item
createNewItem(item.id);
return newItems;
} else if (action === "delete") {
// DELETE - delete item and move children to parent level
newItems.splice(itemIndexInArray, 1, ...(item.children || []));
} else if (action === "deleteChildren") {
// DELETE CHILDREN - delete all children
item.children = undefined;
}
if (isRoot) {
setItems(newItems);
}
return newItems;
};
interface TreeItemProps extends TreeProps {
item: TreeItem;
}
const TreeItemComponent = ({
item,
items,
setItems,
createNewItem
}: TreeItemProps) => {
const handleMoveUp = useCallback(
() => performItemAction(items, item.id, "up", setItems, createNewItem),
[items, item.id, setItems, createNewItem]
);
const handleMoveDown = useCallback(
() => performItemAction(items, item.id, "down", setItems, createNewItem),
[items, item.id, setItems, createNewItem]
);
const handleMoveLeft = useCallback(
() => performItemAction(items, item.id, "left", setItems, createNewItem),
[items, item.id, setItems, createNewItem]
);
const handleMoveRight = useCallback(
() => performItemAction(items, item.id, "right", setItems, createNewItem),
[items, item.id, setItems, createNewItem]
);
const handleNew = useCallback(
() => performItemAction(items, item.id, "new", setItems, createNewItem),
[items, item.id, setItems, createNewItem]
);
const handleDelete = useCallback(
() => performItemAction(items, item.id, "delete", setItems, createNewItem),
[items, item.id, setItems, createNewItem]
);
const handleDeleteChildren = useCallback(
() =>
performItemAction(
items,
item.id,
"deleteChildren",
setItems,
createNewItem
),
[items, item.id, setItems, createNewItem]
);
return (
<div key={item.id}>
<span className="inline-flex w-full">
{/* eslint-disable-next-line tailwindcss/no-custom-classname */}
{/* <div className="tree-drag-handle w-5 cursor-pointer bg-gray-300 hover:bg-gray-400" /> */}
{item.component}
</span>
<div className="ml-2 mb-2 inline-flex text-gray-500">
<button onClick={handleMoveUp} className="mr-1 hover:text-gray-700">
</button>
<button onClick={handleMoveDown} className="mr-1 hover:text-gray-700">
</button>
<button onClick={handleMoveLeft} className="mr-1 hover:text-gray-700">
</button>
<button onClick={handleMoveRight} className="mr-1 hover:text-gray-700">
</button>
<button
onClick={handleNew}
className="ml-1 mr-2 text-xs hover:text-gray-700"
>
New
</button>
<button
onClick={handleDelete}
className="mr-2 text-xs hover:text-gray-700"
>
Delete
</button>
<button
onClick={handleDeleteChildren}
className="mr-2 text-xs hover:text-gray-700"
>
Delete Children
</button>
</div>
<div className="ml-3 mb-3 border-l-4 pl-4">
{item.children && (
// eslint-disable-next-line @typescript-eslint/no-use-before-define
<Tree
items={items}
setItems={setItems}
createNewItem={createNewItem}
parent={item}
/>
)}
</div>
</div>
);
};
interface TreeProps {
items: TreeItem[];
setItems: (newItems: TreeItem[]) => void;
createNewItem: (parentId: string) => void;
parent?: TreeItem;
}
const Tree = ({ items, setItems, createNewItem, parent }: TreeProps) => {
const list = parent?.children ?? items;
return (
<ReactSortable
list={list}
setList={() => {}} // TODO - setItems handling
group={parent?.id || "root"}
animation={150}
ghostClass="tree-ghost"
handle=".tree-drag-handle"
dragClass="tree-drag"
>
{list.map((item) => (
<TreeItemComponent
key={item.id}
item={item}
items={items}
setItems={setItems}
createNewItem={createNewItem}
/>
))}
</ReactSortable>
);
};
export default Tree;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment