Skip to content

Commit 883cbbb

Browse files
committed
react
1 parent 574227a commit 883cbbb

File tree

13 files changed

+426
-403
lines changed

13 files changed

+426
-403
lines changed

Angular/src/app/components/tree-view-plain/tree-view-plain.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export class TreeViewPlainComponent {
6767

6868
canDrop(treeView: dxTreeView, e: DxSortableTypes.DragChangeEvent | DxSortableTypes.DragEndEvent, toNode: Node | null): boolean {
6969
if (!toNode) return false;
70-
const canAcceptChildren = (e.dropInsideItem && toNode.itemData && (toNode.itemData as Record<string, unknown>)['hasItems']) || !e.dropInsideItem;
70+
const canAcceptChildren = (e.dropInsideItem && toNode.itemData && (toNode.itemData as TreeItem).hasItems) || !e.dropInsideItem;
7171
const toNodeIsChild = toNode && e.itemData.some((i: Node) => this.isParent(toNode, i));
7272
const fromIndices = e.itemData.map((i: Node) => this.getVisualIndexByKey(treeView, i.key));
7373
const targetThemselves = toNode && (e.itemData.some((i: Node) => i.key === toNode.key) || fromIndices.includes(e.toIndex ?? 0));

Angular/src/app/services/data.service.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { publishFacade } from '@angular/compiler';
21
import { Injectable } from '@angular/core';
32
import { Item as TreeItem } from 'devextreme/ui/tree_view';
43

React/src/App.css

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,21 @@
11
.main {
22
margin: 50px;
33
width: 90vw;
4+
}
5+
6+
.demo-header {
7+
display: flex;
8+
justify-content: space-between;
9+
}
10+
11+
#toggle-container {
12+
padding-top: 20px;
13+
}
14+
15+
#clear-after-drop-switch {
16+
vertical-align: text-bottom;
17+
}
18+
19+
#toggle-container span {
20+
padding-right: 10px;
421
}

React/src/App.tsx

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,41 @@
11
import { useCallback, useState } from 'react';
22
import './App.css';
33
import 'devextreme/dist/css/dx.material.blue.light.compact.css';
4-
import Button from 'devextreme-react/button';
4+
import TabPanel, { Item } from 'devextreme-react/tab-panel';
5+
import Switch, { type SwitchTypes } from 'devextreme-react/switch';
6+
import TreeViewPlain from './components/TreeViewPlain';
7+
import TreeViewHierarchy from './components/TreeViewHierarchy';
58

69
function App(): JSX.Element {
7-
var [count, setCount] = useState<number>(0);
8-
const clickHandler = useCallback(() => {
9-
setCount((prev) => prev + 1);
10-
}, [setCount]);
10+
const [shouldClearSelection, setShouldClearSelection] = useState(false);
11+
12+
const switchValueChanged = useCallback((e: SwitchTypes.ValueChangedEvent) => {
13+
setShouldClearSelection(e.value);
14+
}, [shouldClearSelection]);
15+
16+
const treeViewPlainRender = useCallback(
17+
() => <TreeViewPlain shouldClearSelection={shouldClearSelection}></TreeViewPlain>,
18+
[shouldClearSelection],
19+
);
20+
21+
const treeViewHierarchyRender = useCallback(
22+
() => <TreeViewHierarchy shouldClearSelection={shouldClearSelection}></TreeViewHierarchy>,
23+
[shouldClearSelection],
24+
);
25+
1126
return (
12-
<div className="main">
13-
<Button text={`Click count: ${count}`} onClick={clickHandler} />
27+
<div className='main'>
28+
<div className='demo-header'>
29+
<h3>TreeView - Select multiple items and drag&apos;n&apos;drop</h3>
30+
<div id='toggle-container'>
31+
<span>Clear selection after drop</span>
32+
<Switch id='clear-after-drop-switch' value={shouldClearSelection} onValueChanged={switchValueChanged}></Switch>
33+
</div>
34+
</div>
35+
<TabPanel>
36+
<Item title='Plain Data' render={treeViewPlainRender}></Item>
37+
<Item title='Hierarchical Data' render={treeViewHierarchyRender}></Item>
38+
</TabPanel>
1439
</div>
1540
);
1641
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
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

Comments
 (0)