Created
February 1, 2023 14:48
-
-
Save lveillard/a82ef7a64ce238d07e43e6a293d9cb72 to your computer and use it in GitHub Desktop.
Tree with sortablejs example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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