Skip to content

Commit 7d72efd

Browse files
committed
vue
1 parent 883cbbb commit 7d72efd

File tree

10 files changed

+507
-469
lines changed

10 files changed

+507
-469
lines changed

Vue/src/App.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ import { RouterView } from 'vue-router';
66
<div class="main">
77
<RouterView/>
88
</div>
9-
</template>
9+
</template>

Vue/src/assets/main.css

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@
33
-webkit-font-smoothing: antialiased;
44
-moz-osx-font-smoothing: grayscale;
55
color: #2c3e50;
6-
margin: 50px 50px;
7-
width: 90vh;
6+
margin: 50px;
7+
width: 90vw;
8+
}
9+
10+
.dragged-item {
11+
padding: 10px;
12+
}
13+
14+
.tab-item-content {
15+
margin: auto;
816
}

Vue/src/components/HomeContent.vue

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,47 @@
11
<script setup lang="ts">
2-
import { computed, ref } from 'vue';
3-
2+
import { ref } from 'vue';
43
import 'devextreme/dist/css/dx.material.blue.light.compact.css';
5-
import DxButton from 'devextreme-vue/button';
4+
import DxTabPanel, { DxItem } from 'devextreme-vue/tab-panel';
5+
import DxSwitch from 'devextreme-vue/switch';
6+
import TreeViewPlain from './TreeViewPlain.vue';
7+
import TreeViewHierarchy from './TreeViewHierarchy.vue';
68
7-
const props = defineProps({
8-
text: {
9-
type: String,
10-
default: 'count',
11-
},
12-
});
13-
const count = ref(0);
14-
const buttonText = computed<string>(
15-
() => `Click ${props.text}: ${count.value}`
16-
);
17-
function clickHandler() {
18-
count.value += 1;
19-
}
9+
const shouldClearSelection = ref(false);
2010
</script>
2111
<template>
2212
<div>
23-
<DxButton
24-
:text="buttonText"
25-
@click="clickHandler"
26-
/>
13+
<div className="demo-header">
14+
<h3>TreeView - Select multiple items and drag'n'drop</h3>
15+
<div id="toggle-container">
16+
<span>Clear selection after drop</span>
17+
<DxSwitch
18+
id="clear-after-drop-switch"
19+
v-model:value="shouldClearSelection"
20+
/>
21+
</div>
22+
</div>
23+
<DxTabPanel>
24+
<DxItem title="Plain Data">
25+
<TreeViewPlain :should-clear-selection="shouldClearSelection"/>
26+
</DxItem>
27+
<DxItem title="Hierarchical Data">
28+
<TreeViewHierarchy :should-clear-selection="shouldClearSelection"/>
29+
</DxItem>
30+
</DxTabPanel>
2731
</div>
2832
</template>
33+
<style scoped>
34+
.demo-header {
35+
display: flex;
36+
justify-content: space-between;
37+
}
38+
#toggle-container {
39+
padding-top: 20px;
40+
}
41+
#clear-after-drop-switch {
42+
vertical-align: text-bottom;
43+
}
44+
#toggle-container span {
45+
padding-right: 10px;
46+
}
47+
</style>
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
<script setup lang="ts">
2+
import { ref } from 'vue';
3+
import DxTreeView from 'devextreme-vue/tree-view';
4+
import DxSortable, { type DxSortableTypes } from 'devextreme-vue/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+
const props = defineProps({
14+
shouldClearSelection: {
15+
type: Boolean,
16+
default: false,
17+
},
18+
});
19+
20+
const treeViewRef = ref<InstanceType<typeof DxTreeView>>();
21+
22+
function canDrag(treeView: dxTreeView, e: DxSortableTypes.DragStartEvent): boolean {
23+
const fromNode = getNodeByVisualIndex(treeView, e.fromIndex);
24+
return !!(fromNode?.selected && e.itemData?.length);
25+
}
26+
27+
function canDrop(
28+
treeView: dxTreeView,
29+
e: DxSortableTypes.DragChangeEvent | DxSortableTypes.DragEndEvent
30+
): boolean {
31+
const toNode: Node | null = getNodeByVisualIndex(treeView, e.toIndex ?? 0);
32+
if (!toNode) return false;
33+
const canAcceptChildren =
34+
(e.dropInsideItem && toNode.itemData?.hasItems) || !e.dropInsideItem;
35+
const toNodeIsChild = toNode && e.itemData.some((i: Node) => isParent(toNode, i));
36+
const fromIndices =
37+
e.itemData.map((node: Node) => getVisualIndexByNode(treeView, node));
38+
const targetThemselves =
39+
toNode &&
40+
(e.itemData.some((i: Node) => i.key === toNode.key) ||
41+
fromIndices.includes(e.toIndex));
42+
return canAcceptChildren && !toNodeIsChild && !targetThemselves;
43+
}
44+
45+
function moveNodes(
46+
items: TreeItem[],
47+
e: DxSortableTypes.DragEndEvent,
48+
toNode: Node | null,
49+
treeFieldExpr: TreeFieldExpr
50+
): void {
51+
const nodesToMove = getTopNodes(e.itemData);
52+
nodesToMove.forEach((nodeToMove: Node) => {
53+
const fromNodeContainingArray =
54+
getNodeContainingArray(nodeToMove, items, treeFieldExpr.items);
55+
const fromIndex =
56+
getLocalIndex(fromNodeContainingArray, nodeToMove.key, treeFieldExpr.key);
57+
fromNodeContainingArray.splice(fromIndex, 1);
58+
});
59+
if (e.dropInsideItem) {
60+
if (!toNode?.itemData) return;
61+
const toIndex = toNode.itemData[treeFieldExpr.items].length;
62+
toNode.itemData[treeFieldExpr.items].splice(
63+
toIndex,
64+
0,
65+
...nodesToMove.map((i: Node) => i.itemData)
66+
);
67+
} else {
68+
const toNodeContainingArray =
69+
getNodeContainingArray(toNode, items, treeFieldExpr.items);
70+
const toIndex = toNode === null
71+
? items.length
72+
: getLocalIndex(toNodeContainingArray, toNode.key, treeFieldExpr.key);
73+
toNodeContainingArray.splice(
74+
toIndex,
75+
0,
76+
...nodesToMove.map((i: Node) => i.itemData as TreeItem)
77+
);
78+
}
79+
}
80+
81+
function isParent(node: Node, possibleParentNode: Node): boolean {
82+
if (!node.parent) return false;
83+
return node.parent.key !== possibleParentNode.key
84+
? isParent(node.parent, possibleParentNode)
85+
: true;
86+
}
87+
88+
function getTopNodes(nodes: Node[]): Node[] {
89+
return nodes.filter(
90+
(nodeToCheck: Node) =>
91+
!nodes.some((n: Node) => isParent(nodeToCheck, n))
92+
);
93+
}
94+
95+
function getNodeContainingArray(
96+
node: Node | null,
97+
rootArray: TreeItem[],
98+
itemsExpr: string
99+
): TreeItem[] {
100+
return node === null || !node.parent || !node.parent.itemData
101+
? rootArray
102+
: (node.parent.itemData[itemsExpr] as TreeItem[]);
103+
}
104+
105+
function getVisualIndexByNode(treeView: dxTreeView, node: Node): number {
106+
const nodeElements = Array.from(treeView.element().querySelectorAll('.dx-treeview-node'));
107+
const nodeElement = nodeElements.find(n => n.getAttribute('data-item-id') === node.key);
108+
return nodeElements.indexOf(nodeElement as Element);
109+
}
110+
111+
function getNodeByVisualIndex(treeView: dxTreeView, index: number): Node | null {
112+
const nodeElement = treeView.element().querySelectorAll('.dx-treeview-node')[index];
113+
if (nodeElement) {
114+
return getNodeByKey(treeView.getNodes(), nodeElement.getAttribute('data-item-id'));
115+
}
116+
return null;
117+
}
118+
119+
function getNodeByKey(nodes: Node[], key: string | number | null): Node | null {
120+
for (let i = 0; i < nodes.length; i++) {
121+
if (nodes[i].key == key) {
122+
return nodes[i];
123+
}
124+
if (nodes[i].children) {
125+
const node = getNodeByKey(nodes[i].children as Node[], key);
126+
if (node != null) {
127+
return node;
128+
}
129+
}
130+
}
131+
return null;
132+
}
133+
134+
function calculateToIndex(
135+
e: DxSortableTypes.DragChangeEvent | DxSortableTypes.DragEndEvent
136+
): number {
137+
if (e.dropInsideItem) return e.toIndex ?? 0;
138+
const fromIndex = e.fromIndex ?? 0;
139+
const toIndex = e.toIndex ?? 0;
140+
return fromIndex >= toIndex ? toIndex : toIndex + 1;
141+
}
142+
143+
function getLocalIndex(
144+
array: TreeItem[],
145+
key: string | number,
146+
keyExpr: string
147+
): number {
148+
const idsArray = array.map((elem) => elem[keyExpr]);
149+
return idsArray.indexOf(key);
150+
}
151+
152+
function dragStart(e: DxSortableTypes.DragStartEvent) {
153+
const treeView = treeViewRef.value?.instance;
154+
if (!treeView) return;
155+
e.itemData = treeView.getSelectedNodes();
156+
e.cancel = !canDrag(treeView, e);
157+
}
158+
159+
function dragChange(e: DxSortableTypes.DragChangeEvent) {
160+
const treeView = treeViewRef.value?.instance;
161+
if (!treeView) return;
162+
e.cancel = !canDrop(treeView, e);
163+
}
164+
165+
function dragEnd(e: DxSortableTypes.DragEndEvent) {
166+
const treeView = treeViewRef.value?.instance;
167+
if (!treeView) return;
168+
const allItems = treeView.option('items') as TreeItem[];
169+
if (canDrop(treeView, e)) {
170+
const toNode = getNodeByVisualIndex(treeView, calculateToIndex(e));
171+
const treeViewExpr = {
172+
items: treeView.option('itemsExpr') as string,
173+
key: treeView.option('keyExpr') as string,
174+
};
175+
moveNodes(allItems, e, toNode, treeViewExpr);
176+
}
177+
treeView.option('items', allItems);
178+
if (props.shouldClearSelection) {
179+
treeView.unselectAll();
180+
}
181+
}
182+
</script>
183+
<template>
184+
<DxSortable
185+
filter=".dx-treeview-item"
186+
:allow-drop-inside-item="true"
187+
:allow-reordering="true"
188+
@drag-start="dragStart"
189+
@drag-change="dragChange"
190+
@drag-end="dragEnd"
191+
drag-render="draggedItemsRender"
192+
>
193+
<template #draggedItemsRender="{ data }">
194+
<div>
195+
<div
196+
v-for="node in data.itemData"
197+
class="dragged-item"
198+
:key="node.text"
199+
>
200+
{{ node.text }}
201+
</div>
202+
</div>
203+
</template>
204+
<DxTreeView
205+
ref="treeViewRef"
206+
:items="treeData"
207+
class="tab-item-content"
208+
:expand-nodes-recursive="false"
209+
:select-nodes-recursive="false"
210+
show-check-boxes-mode="normal"
211+
data-structure="tree"
212+
display-expr="name"
213+
:width="300"
214+
/>
215+
</DxSortable>
216+
</template>

0 commit comments

Comments
 (0)