Skip to content

Commit 63724a3

Browse files
committed
feat: Drag and drop virtual tree
1 parent 53d431d commit 63724a3

2 files changed

Lines changed: 201 additions & 2 deletions

File tree

ui/src/components/folder-virtualized-tree/HeTree.vue

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
v-bind="$attrs"
77
class="maxkb-virtualized-tree"
88
@click:node="handleNodeClick"
9+
@after-drop="onAfterDrop"
10+
:rootDroppable="false"
11+
@enter="enter"
912
>
1013
<template #default="{ node, stat }">
1114
<div
@@ -31,7 +34,7 @@
3134

3235
<script lang="ts" setup>
3336
import { ref, watch, nextTick } from 'vue'
34-
import { Draggable } from '@he-tree/vue'
37+
import { Draggable, dragContext } from '@he-tree/vue'
3538
import '@he-tree/vue/style/default.css'
3639
const props = defineProps({
3740
currentNodeKey: {
@@ -42,12 +45,64 @@ const props = defineProps({
4245
4346
type DraggableInstance = InstanceType<typeof Draggable>
4447
const treeRef = ref<DraggableInstance | null>(null)
45-
const emit = defineEmits(['handleNodeClick'])
48+
const emit = defineEmits(['handleNodeClick', 'node-drop'])
4649
4750
const handleNodeClick = (node: any) => {
4851
node.open = !node.open
4952
emit('handleNodeClick', node.data)
5053
}
54+
55+
type DropType = 'before' | 'after' | 'inner'
56+
const buildNodeDropArgs = () => {
57+
const draggingNode = dragContext.dragNode as any
58+
const targetInfo = dragContext.targetInfo as any
59+
60+
if (!draggingNode || !targetInfo) {
61+
return null
62+
}
63+
64+
const newParent = targetInfo.parent ?? null
65+
const siblings = Array.isArray(targetInfo.siblings) ? targetInfo.siblings : []
66+
67+
let newIndex =
68+
typeof targetInfo.indexBeforeDrop === 'number'
69+
? targetInfo.indexBeforeDrop
70+
: siblings.indexOf(draggingNode)
71+
72+
if (newIndex < 0) {
73+
newIndex = siblings.indexOf(draggingNode)
74+
}
75+
76+
let dropNode: any | null = null
77+
let dropType: DropType = 'after'
78+
79+
if (newParent && siblings.length === 1 && siblings[0] === draggingNode) {
80+
dropNode = newParent
81+
dropType = 'inner'
82+
return [draggingNode, dropNode, dropType] as const
83+
}
84+
85+
if (siblings.length <= 1) {
86+
return [draggingNode, newParent, 'inner'] as const
87+
}
88+
89+
if (newIndex === 0) {
90+
dropNode = siblings[1]
91+
dropType = 'before'
92+
return [draggingNode, dropNode, dropType] as const
93+
}
94+
95+
dropNode = siblings[newIndex - 1]
96+
dropType = 'after'
97+
98+
return [draggingNode, dropNode, dropType] as const
99+
}
100+
function onAfterDrop() {
101+
const args = buildNodeDropArgs()
102+
if (args) {
103+
emit('node-drop', args[0], args[1], args[2])
104+
}
105+
}
51106
</script>
52107

53108
<style lang="scss">

ui/src/components/folder-virtualized-tree/index.vue

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
:class="
5757
showShared && hasPermission(EditionConst.IS_EE, 'OR') ? 'tree-height-shared' : 'tree-height'
5858
"
59+
@node-drop="handleDrop"
5960
@handleNodeClick="handleNodeClick"
6061
:current-node-key="currentNodeKey"
6162
>
@@ -231,6 +232,149 @@ const MoreFilledPermission = (node: any) => {
231232
232233
const emit = defineEmits(['handleNodeClick', 'refreshTree'])
233234
235+
const handleDrop = (draggingNode: any, dropNode: any, dropType: string) => {
236+
const dragData = draggingNode.data
237+
const dropData = dropNode.data
238+
console.log(draggingNode, dropNode, dropType)
239+
240+
const oldParentId = dragData.parent_id
241+
let newParentId: string
242+
if (dropType === 'inner') {
243+
newParentId = dropData.id
244+
} else if (dropType === 'prev' || dropType === 'next') {
245+
newParentId = dropData.parent_id
246+
} else {
247+
newParentId = dropData.parent_id
248+
}
249+
250+
const isCrossNode: boolean = oldParentId !== newParentId
251+
252+
if (isCrossNode) {
253+
const obj = {
254+
...dragData,
255+
parent_id: newParentId,
256+
}
257+
folderApi
258+
.putFolder(dragData.id, props.source, obj, loading)
259+
.then(() => {
260+
emit('refreshTree')
261+
262+
MsgSuccess(t('common.saveSuccess'))
263+
})
264+
.catch(() => {
265+
emit('refreshTree')
266+
})
267+
} else {
268+
// 同级拖拽,直接放置
269+
sortAfterDrop(dragData, dropData, dropType, newParentId)
270+
}
271+
}
272+
273+
const savePositions = debounce(doSave, 300)
274+
function sortAfterDrop(
275+
draggingNodeData: any,
276+
dropNodeData: any,
277+
dropType: string,
278+
newParentId: string,
279+
) {
280+
const sortMethod = localStorage.getItem(FOLDER_SORT_TYPE)
281+
currentSort.value = sortMethod as SortType
282+
283+
if (sortMethod === SORT_TYPES.CUSTOM) {
284+
const positions = getPositions(newParentId)
285+
let prevPos: number
286+
let nextPos: number
287+
if (dropType === 'inner') {
288+
const childrenPositions: number[] = Object.values(positions)
289+
if (childrenPositions.length === 0) {
290+
positions[draggingNodeData.id] = encode(1, 0)
291+
savePositions(newParentId, positions)
292+
return
293+
}
294+
// 放到最后
295+
const maxPos = Math.max(...childrenPositions)
296+
positions[draggingNodeData.id] = maxPos + encode(1, 0)
297+
savePositions(newParentId, positions)
298+
} else if (dropType === 'before') {
299+
const { dropPos, sortedNodes, dropIndex } = getSortContext(positions, dropNodeData.id)
300+
const prevNode: any[] = sortedNodes[dropIndex - 1]
301+
302+
prevPos = prevNode ? prevNode[1] : 0
303+
nextPos = dropPos
304+
305+
const newPos = mid(prevPos, nextPos)
306+
307+
if (newPos === null) {
308+
// rebalance
309+
rebalanceAndInsert(newParentId, draggingNodeData.id, dropNodeData.id, 'before')
310+
return
311+
}
312+
positions[draggingNodeData.id] = newPos
313+
savePositions(newParentId, positions)
314+
} else if (dropType === 'after') {
315+
const { dropPos, sortedNodes, dropIndex } = getSortContext(positions, dropNodeData.id)
316+
const nextNode: any[] = sortedNodes[dropIndex + 1]
317+
318+
prevPos = dropPos
319+
nextPos = nextNode ? nextNode[1] : Infinity
320+
321+
if (nextPos === Infinity) {
322+
positions[draggingNodeData.id] = prevPos + encode(1, 0)
323+
} else {
324+
const newPos = mid(prevPos, nextPos)
325+
326+
if (newPos === null) {
327+
rebalanceAndInsert(newParentId, draggingNodeData.id, dropNodeData.id, 'after')
328+
return
329+
}
330+
positions[draggingNodeData.id] = newPos
331+
}
332+
333+
savePositions(newParentId, positions)
334+
}
335+
} else {
336+
emit('refreshTree')
337+
}
338+
}
339+
340+
function rebalanceAndInsert(
341+
parentId: string,
342+
dragNodeId: string,
343+
dropNodeId: string,
344+
position: 'before' | 'after',
345+
) {
346+
const positions = getPositions(parentId)
347+
const sortedIds = Object.entries(positions)
348+
.sort((a: any[], b: any[]) => a[1] - b[1])
349+
.map(([id]) => id)
350+
351+
const dragIndex = sortedIds.indexOf(dragNodeId)
352+
if (dragIndex > -1) {
353+
sortedIds.splice(dragIndex, 1)
354+
}
355+
const dropIndex = sortedIds.indexOf(dropNodeId)
356+
if (position === 'before') {
357+
sortedIds.splice(dropIndex, 0, dragNodeId)
358+
} else {
359+
sortedIds.splice(dropIndex + 1, 0, dragNodeId)
360+
}
361+
362+
const tempPositions: Record<string, number> = {}
363+
sortedIds.forEach((id, index) => {
364+
tempPositions[id] = index
365+
})
366+
367+
const newPositions = rebalance(tempPositions)
368+
savePositionsInit(parentId, newPositions)
369+
// rebalance finish
370+
}
371+
function getSortContext(positions: Record<string, number>, nodeId: string) {
372+
const dropPos = positions[nodeId]
373+
const sortedNodes = Object.entries(positions).sort((a: any[], b: any[]) => a[1] - b[1])
374+
const dropIndex = sortedNodes.findIndex(([id]) => id === nodeId)
375+
376+
return { dropPos, sortedNodes, dropIndex }
377+
}
234378
const treeRef = ref()
235379
const filterText = ref('')
236380
const hoverNodeId = ref<string | undefined>('')

0 commit comments

Comments
 (0)