|
| 1 | +import { useCallback, useRef } from 'react'; |
| 2 | +import TreeView from 'devextreme-react/tree-view'; |
| 3 | +import Sortable from 'devextreme-react/sortable'; |
| 4 | +import { type SortableTypes } from 'devextreme-react/sortable'; |
| 5 | +import dxTreeView, { type Node, type Item as TreeItem } from 'devextreme/ui/tree_view'; |
| 6 | +import { itemsDriveHierarchy as treeData } from '../data'; |
| 7 | + |
| 8 | +interface TreeFieldExpr { |
| 9 | + key: string; |
| 10 | + items: string; |
| 11 | +} |
| 12 | + |
| 13 | +interface TreeViewHierarchyProps { |
| 14 | + shouldClearSelection: boolean; |
| 15 | +} |
| 16 | + |
| 17 | +function draggedItemsRender(data: SortableTypes.DragTemplateData): JSX.Element { |
| 18 | + const draggedItems = data.itemData.map((node: Node) => <div key={node.text} className='dragged-item'>{node.text}</div>); |
| 19 | + return (<div>{draggedItems}</div>); |
| 20 | +} |
| 21 | + |
| 22 | +function canDrag(treeView: dxTreeView, e: SortableTypes.DragStartEvent): boolean { |
| 23 | + const fromNode = getNodeByVisualIndex(treeView, e.fromIndex); |
| 24 | + return !!(fromNode?.selected && e.itemData?.length); |
| 25 | +} |
| 26 | + |
| 27 | +function canDrop(treeView: dxTreeView, e: SortableTypes.DragChangeEvent | SortableTypes.DragEndEvent): boolean { |
| 28 | + const toNode: Node | null = getNodeByVisualIndex(treeView, e.toIndex ?? 0); |
| 29 | + if (!toNode) return false; |
| 30 | + const canAcceptChildren = (e.dropInsideItem && toNode.itemData?.hasItems) || !e.dropInsideItem; |
| 31 | + const toNodeIsChild = toNode && e.itemData.some((i: Node) => isParent(toNode, i)); |
| 32 | + const fromIndices = e.itemData.map((node: Node) => getVisualIndexByNode(treeView, node)); |
| 33 | + const targetThemselves = toNode && (e.itemData.some((i: Node) => i.key === toNode.key) || fromIndices.includes(e.toIndex)); |
| 34 | + return canAcceptChildren && !toNodeIsChild && !targetThemselves; |
| 35 | +} |
| 36 | + |
| 37 | +function moveNodes(items: TreeItem[], e: SortableTypes.DragEndEvent, toNode: Node | null, treeFieldExpr: TreeFieldExpr): void { |
| 38 | + const nodesToMove = getTopNodes(e.itemData); |
| 39 | + nodesToMove.forEach((nodeToMove: Node) => { |
| 40 | + const fromNodeContainingArray = getNodeContainingArray(nodeToMove, items, treeFieldExpr.items); |
| 41 | + const fromIndex = getLocalIndex(fromNodeContainingArray, nodeToMove.key, treeFieldExpr.key); |
| 42 | + fromNodeContainingArray.splice(fromIndex, 1); |
| 43 | + }); |
| 44 | + if (e.dropInsideItem) { |
| 45 | + if (!toNode?.itemData) return; |
| 46 | + const toIndex = toNode.itemData[treeFieldExpr.items].length; |
| 47 | + toNode.itemData[treeFieldExpr.items].splice(toIndex, 0, ...nodesToMove.map((i: Node) => i.itemData)); |
| 48 | + } else { |
| 49 | + const toNodeContainingArray = getNodeContainingArray(toNode, items, treeFieldExpr.items); |
| 50 | + const toIndex = toNode === null |
| 51 | + ? items.length |
| 52 | + : getLocalIndex(toNodeContainingArray, toNode.key, treeFieldExpr.key); |
| 53 | + toNodeContainingArray.splice(toIndex, 0, ...nodesToMove.map((i: Node) => i.itemData as TreeItem)); |
| 54 | + } |
| 55 | +} |
| 56 | + |
| 57 | +function isParent(node: Node, possibleParentNode: Node): boolean { |
| 58 | + if (!node.parent) return false; |
| 59 | + return node.parent.key !== possibleParentNode.key ? isParent(node.parent, possibleParentNode) : true; |
| 60 | +} |
| 61 | + |
| 62 | +function getTopNodes(nodes: Node[]): Node[] { |
| 63 | + return nodes.filter((nodeToCheck: Node) => !nodes.some((n: Node) => isParent(nodeToCheck, n))); |
| 64 | +} |
| 65 | + |
| 66 | +function getNodeContainingArray(node: Node | null, rootArray: TreeItem[], itemsExpr: string): TreeItem[] { |
| 67 | + return node === null || !node.parent || !node.parent.itemData |
| 68 | + ? rootArray |
| 69 | + : (node.parent.itemData[itemsExpr] as TreeItem[]); |
| 70 | +} |
| 71 | + |
| 72 | +function getVisualIndexByNode(treeView: dxTreeView, node: TreeItem): number { |
| 73 | + const nodeElements = Array.from(treeView.element().querySelectorAll('.dx-treeview-node')); |
| 74 | + const nodeElement = nodeElements.find((n: Element) => n.getAttribute('data-item-id') === node.key); |
| 75 | + return nodeElements.indexOf(nodeElement as Element); |
| 76 | +} |
| 77 | + |
| 78 | +function getNodeByVisualIndex(treeView: dxTreeView, index: number): Node | null { |
| 79 | + const nodeElement = treeView.element().querySelectorAll('.dx-treeview-node')[index]; |
| 80 | + if (nodeElement) { |
| 81 | + return getNodeByKey(treeView.getNodes(), nodeElement.getAttribute('data-item-id')); |
| 82 | + } |
| 83 | + return null; |
| 84 | +} |
| 85 | + |
| 86 | +function getNodeByKey(nodes: Node[], key: string | number | null): Node | null { |
| 87 | + for (const node of nodes) { |
| 88 | + if (node.key === key) { |
| 89 | + return node; |
| 90 | + } |
| 91 | + if (node.children) { |
| 92 | + const foundNode = getNodeByKey(node.children as Node[], key); |
| 93 | + if (foundNode != null) { |
| 94 | + return foundNode; |
| 95 | + } |
| 96 | + } |
| 97 | + } |
| 98 | + return null; |
| 99 | +} |
| 100 | + |
| 101 | +function calculateToIndex(e: SortableTypes.DragChangeEvent | SortableTypes.DragEndEvent): number { |
| 102 | + if (e.dropInsideItem) return e.toIndex ?? 0; |
| 103 | + const fromIndex = e.fromIndex ?? 0; |
| 104 | + const toIndex = e.toIndex ?? 0; |
| 105 | + return fromIndex >= toIndex ? toIndex : toIndex + 1; |
| 106 | +} |
| 107 | + |
| 108 | +function getLocalIndex(array: TreeItem[], key: string | number, keyExpr: string): number { |
| 109 | + const idsArray = array.map((elem: TreeItem) => elem[keyExpr] as string | number); |
| 110 | + return idsArray.indexOf(key); |
| 111 | +} |
| 112 | + |
| 113 | +function TreeViewHierarchy(props: TreeViewHierarchyProps): JSX.Element { |
| 114 | + const treeViewRef = useRef<dxTreeView>(null); |
| 115 | + |
| 116 | + const dragStart = useCallback((e: SortableTypes.DragStartEvent) => { |
| 117 | + const treeView = treeViewRef.current?.instance(); |
| 118 | + if (!treeView) return; |
| 119 | + e.itemData = treeView.getSelectedNodes(); |
| 120 | + e.cancel = !canDrag(treeView, e); |
| 121 | + }, []); |
| 122 | + |
| 123 | + const dragChange = useCallback((e: SortableTypes.DragChangeEvent) => { |
| 124 | + const treeView = treeViewRef.current?.instance(); |
| 125 | + if (!treeView) return; |
| 126 | + e.cancel = !canDrop(treeView, e); |
| 127 | + }, []); |
| 128 | + |
| 129 | + const dragEnd = useCallback((e: SortableTypes.DragEndEvent) => { |
| 130 | + const treeView = treeViewRef.current?.instance(); |
| 131 | + if (!treeView) return; |
| 132 | + const allItems = treeView.option('items') as TreeItem[]; |
| 133 | + if (canDrop(treeView, e)) { |
| 134 | + const toNode = getNodeByVisualIndex(treeView, calculateToIndex(e)); |
| 135 | + const treeViewExpr = { |
| 136 | + items: treeView.option('itemsExpr') as string, |
| 137 | + key: treeView.option('keyExpr') as string, |
| 138 | + }; |
| 139 | + moveNodes(allItems, e, toNode, treeViewExpr); |
| 140 | + } |
| 141 | + treeView.option('items', allItems); |
| 142 | + if (props.shouldClearSelection) { |
| 143 | + treeView.unselectAll(); |
| 144 | + } |
| 145 | + }, [props.shouldClearSelection]); |
| 146 | + |
| 147 | + return ( |
| 148 | + <Sortable filter='.dx-treeview-item' |
| 149 | + allowDropInsideItem={true} |
| 150 | + allowReordering={true} |
| 151 | + onDragStart={dragStart} |
| 152 | + onDragChange={dragChange} |
| 153 | + onDragEnd={dragEnd} |
| 154 | + dragRender={draggedItemsRender} |
| 155 | + > |
| 156 | + <TreeView ref={treeViewRef} |
| 157 | + items={treeData} |
| 158 | + className='tab-item-content' |
| 159 | + expandNodesRecursive={false} |
| 160 | + selectNodesRecursive={false} |
| 161 | + showCheckBoxesMode='normal' |
| 162 | + dataStructure='tree' |
| 163 | + displayExpr='name' |
| 164 | + width={300} |
| 165 | + ></TreeView> |
| 166 | + </Sortable> |
| 167 | + ); |
| 168 | +} |
| 169 | + |
| 170 | +export default TreeViewHierarchy; |
0 commit comments