Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<script setup lang="ts">
import type { TreeItem } from '@nuxt/ui'
import { useSortable } from '@vueuse/integrations/useSortable'

const items = shallowRef<TreeItem[]>([
{
label: 'app/',
defaultExpanded: true,
children: [
{
label: 'composables/',
children: [
{ label: 'useAuth.ts', icon: 'i-vscode-icons-file-type-typescript' },
{ label: 'useUser.ts', icon: 'i-vscode-icons-file-type-typescript' }
]
},
{
label: 'components/',
defaultExpanded: true,
children: [
{ label: 'Card.vue', icon: 'i-vscode-icons-file-type-vue' },
{ label: 'Button.vue', icon: 'i-vscode-icons-file-type-vue' }
]
}
]
},
{ label: 'app.vue', icon: 'i-vscode-icons-file-type-vue' },
{ label: 'nuxt.config.ts', icon: 'i-vscode-icons-file-type-nuxt' }
])

function flatten(items: TreeItem[], parent = items): { item: TreeItem, parent: TreeItem[], index: number }[] {
return items.flatMap((item, index) => [
{ item, parent, index },
...(item.children?.length && item.defaultExpanded ? flatten(item.children, item.children) : [])
])
}

function moveItem(oldIndex: number, newIndex: number) {
if (oldIndex === newIndex) return

const flat = flatten(items.value)
const source = flat[oldIndex]
const target = flat[newIndex]

if (!source || !target) return

const [moved] = source.parent.splice(source.index, 1)
if (!moved) return

const updatedFlat = flatten(items.value)
const updatedTarget = updatedFlat.find(({ item }) => item === target.item)
if (!updatedTarget) return

const insertIndex = oldIndex < newIndex ? updatedTarget.index + 1 : updatedTarget.index
updatedTarget.parent.splice(insertIndex, 0, moved)
}

const tree = useTemplateRef<HTMLElement>('tree')

useSortable(tree, items, {
animation: 150,
ghostClass: 'opacity-50',
onUpdate: (e: any) => moveItem(e.oldIndex, e.newIndex)
})
</script>

<template>
<UTree ref="tree" :nested="false" :unmount-on-hide="false" :items="items" />
</template>
14 changes: 14 additions & 0 deletions docs/content/docs/2.components/tree.md
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,20 @@ props:
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`.
::

### With drag and drop :badge{label="Soon"}

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.

::component-example
---
name: 'tree-drag-and-drop-example'
---
::

::note
This example sets the `nested` prop to `false` to have a flat list of items so that the items can be dragged and dropped.
::

### With virtualization :badge{label="Soon"}

Use the `virtualize` prop to enable virtualization for large lists as a boolean or an object with options like `{ estimateSize: 32, overscan: 12 }`.
Expand Down
16 changes: 12 additions & 4 deletions src/runtime/components/Tree.vue
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,8 @@ export type TreeSlots<
</script>

<script setup lang="ts" generic="T extends TreeItem[], M extends boolean = false">
import { computed, toRef } from 'vue'
import type { ComponentPublicInstance } from 'vue'
import { computed, toRef, ref } from 'vue'
import { TreeRoot, TreeItem, TreeVirtualizer, useForwardPropsEmits } from 'reka-ui'
import { reactivePick, createReusableTemplate } from '@vueuse/core'
import { defu } from 'defu'
Expand Down Expand Up @@ -218,6 +219,8 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.tree || {})
virtualize: !!props.virtualize
}))

const rootRef = ref<ComponentPublicInstance>()

function getItemLabel<Item extends T[number]>(item: Item): string {
return get(item, props.labelKey as string)
}
Expand All @@ -235,9 +238,13 @@ function getDefaultOpenedItems(item: T[number]): string[] {
return [currentItem, ...childItems].filter(Boolean) as string[]
}

const defaultExpanded = computed(() =>
props.defaultExpanded ?? props.items?.flatMap(item => getDefaultOpenedItems(item))
)
const defaultExpanded = computed(() => props.defaultExpanded ?? props.items?.flatMap(item => getDefaultOpenedItems(item)))

defineExpose({
get $el() {
return rootRef.value?.$el
}
})
</script>

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

<TreeRoot
ref="rootRef"
v-slot="{ flattenItems }"
v-bind="{ ...rootProps, ...$attrs }"
:as="as.root"
Expand Down
Loading