Skip to content

Commit fcf6117

Browse files
feat(Tree): expose $el for drag and drop example (#5239)
1 parent 08c30cf commit fcf6117

File tree

3 files changed

+95
-4
lines changed

3 files changed

+95
-4
lines changed
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<script setup lang="ts">
2+
import type { TreeItem } from '@nuxt/ui'
3+
import { useSortable } from '@vueuse/integrations/useSortable'
4+
5+
const items = shallowRef<TreeItem[]>([
6+
{
7+
label: 'app/',
8+
defaultExpanded: true,
9+
children: [
10+
{
11+
label: 'composables/',
12+
children: [
13+
{ label: 'useAuth.ts', icon: 'i-vscode-icons-file-type-typescript' },
14+
{ label: 'useUser.ts', icon: 'i-vscode-icons-file-type-typescript' }
15+
]
16+
},
17+
{
18+
label: 'components/',
19+
defaultExpanded: true,
20+
children: [
21+
{ label: 'Card.vue', icon: 'i-vscode-icons-file-type-vue' },
22+
{ label: 'Button.vue', icon: 'i-vscode-icons-file-type-vue' }
23+
]
24+
}
25+
]
26+
},
27+
{ label: 'app.vue', icon: 'i-vscode-icons-file-type-vue' },
28+
{ label: 'nuxt.config.ts', icon: 'i-vscode-icons-file-type-nuxt' }
29+
])
30+
31+
function flatten(items: TreeItem[], parent = items): { item: TreeItem, parent: TreeItem[], index: number }[] {
32+
return items.flatMap((item, index) => [
33+
{ item, parent, index },
34+
...(item.children?.length && item.defaultExpanded ? flatten(item.children, item.children) : [])
35+
])
36+
}
37+
38+
function moveItem(oldIndex: number, newIndex: number) {
39+
if (oldIndex === newIndex) return
40+
41+
const flat = flatten(items.value)
42+
const source = flat[oldIndex]
43+
const target = flat[newIndex]
44+
45+
if (!source || !target) return
46+
47+
const [moved] = source.parent.splice(source.index, 1)
48+
if (!moved) return
49+
50+
const updatedFlat = flatten(items.value)
51+
const updatedTarget = updatedFlat.find(({ item }) => item === target.item)
52+
if (!updatedTarget) return
53+
54+
const insertIndex = oldIndex < newIndex ? updatedTarget.index + 1 : updatedTarget.index
55+
updatedTarget.parent.splice(insertIndex, 0, moved)
56+
}
57+
58+
const tree = useTemplateRef<HTMLElement>('tree')
59+
60+
useSortable(tree, items, {
61+
animation: 150,
62+
ghostClass: 'opacity-50',
63+
onUpdate: (e: any) => moveItem(e.oldIndex, e.newIndex)
64+
})
65+
</script>
66+
67+
<template>
68+
<UTree ref="tree" :nested="false" :unmount-on-hide="false" :items="items" />
69+
</template>

docs/content/docs/2.components/tree.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,20 @@ props:
518518
This example uses the `as` prop to change the items from `button` to `div` as the [`Checkbox`](/docs/components/checkbox) is also rendered as a `button`.
519519
::
520520

521+
### With drag and drop :badge{label="Soon"}
522+
523+
Use the [`useSortable`](https://vueuse.org/integrations/useSortable/) composable from [`@vueuse/integrations`](https://vueuse.org/integrations/README.html) to enable drag and drop functionality on the Tree. This integration wraps [Sortable.js](https://sortablejs.github.io/Sortable/) to provide a seamless drag and drop experience.
524+
525+
::component-example
526+
---
527+
name: 'tree-drag-and-drop-example'
528+
---
529+
::
530+
531+
::note
532+
This example sets the `nested` prop to `false` to have a flat list of items so that the items can be dragged and dropped.
533+
::
534+
521535
### With virtualization :badge{label="Soon"}
522536

523537
Use the `virtualize` prop to enable virtualization for large lists as a boolean or an object with options like `{ estimateSize: 32, overscan: 12 }`.

src/runtime/components/Tree.vue

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,8 @@ export type TreeSlots<
139139
</script>
140140

141141
<script setup lang="ts" generic="T extends TreeItem[], M extends boolean = false">
142-
import { computed, toRef } from 'vue'
142+
import type { ComponentPublicInstance } from 'vue'
143+
import { computed, toRef, ref } from 'vue'
143144
import { TreeRoot, TreeItem, TreeVirtualizer, useForwardPropsEmits } from 'reka-ui'
144145
import { reactivePick, createReusableTemplate } from '@vueuse/core'
145146
import { defu } from 'defu'
@@ -218,6 +219,8 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.tree || {})
218219
virtualize: !!props.virtualize
219220
}))
220221
222+
const rootRef = ref<ComponentPublicInstance>()
223+
221224
function getItemLabel<Item extends T[number]>(item: Item): string {
222225
return get(item, props.labelKey as string)
223226
}
@@ -235,9 +238,13 @@ function getDefaultOpenedItems(item: T[number]): string[] {
235238
return [currentItem, ...childItems].filter(Boolean) as string[]
236239
}
237240
238-
const defaultExpanded = computed(() =>
239-
props.defaultExpanded ?? props.items?.flatMap(item => getDefaultOpenedItems(item))
240-
)
241+
const defaultExpanded = computed(() => props.defaultExpanded ?? props.items?.flatMap(item => getDefaultOpenedItems(item)))
242+
243+
defineExpose({
244+
get $el() {
245+
return rootRef.value?.$el
246+
}
247+
})
241248
</script>
242249

243250
<!-- eslint-disable vue/no-template-shadow -->
@@ -343,6 +350,7 @@ const defaultExpanded = computed(() =>
343350
</DefineTreeTemplate>
344351

345352
<TreeRoot
353+
ref="rootRef"
346354
v-slot="{ flattenItems }"
347355
v-bind="{ ...rootProps, ...$attrs }"
348356
:as="as.root"

0 commit comments

Comments
 (0)