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
3 changes: 3 additions & 0 deletions packages/docs/src/data/new-in.json
Original file line number Diff line number Diff line change
Expand Up @@ -279,12 +279,15 @@
"VTreeview": {
"props": {
"hideActions": "3.9.0",
"hideNoData": "3.10.0",
"noDataText": "3.10.0",
"separateRoots": "3.9.0",
"indentLines": "3.9.0"
},
"slots": {
"header": "3.10.0",
"footer": "3.11.0",
"no-data": "3.11.0",
"toggle": "3.10.0"
}
},
Expand Down
188 changes: 188 additions & 0 deletions packages/docs/src/examples/v-treeview/slot-no-data.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
<template>
<v-treeview
v-model:activated="activated"
:items="items"
item-key="id"
item-value="id"
activatable
open-all
>
<template v-slot:append="{ item, depth, isFirst, isLast }">
<v-icon-btn :disabled="!depth" icon="mdi-arrow-left" @click.stop="move(item, 'left')"></v-icon-btn>
<v-icon-btn :disabled="isFirst" icon="mdi-arrow-up" @click.stop="move(item, 'up')"></v-icon-btn>
<v-icon-btn :disabled="isLast" icon="mdi-arrow-down" @click.stop="move(item, 'down')"></v-icon-btn>
<v-icon-btn :disabled="isFirst" icon="mdi-arrow-right" @click.stop="move(item, 'right')"></v-icon-btn>
</template>
</v-treeview>
</template>

<script setup>
import { ref, shallowRef } from 'vue'

const activated = ref([])
const root = {
id: 0,
children: [
{
id: 1,
title: 'Office Tools',
children: [
{ id: 2, title: 'Calendar' },
{ id: 3, title: 'Notepad' },
],
},
{
id: 4,
title: 'Dev Tools',
children: [
{ id: 5, title: 'VS Code' },
{ id: 6, title: 'Figma' },
{ id: 7, title: 'Webstorm' },
],
},
],
}
const items = shallowRef([...root.children])

function findParent (id, items = [root]) {
if (items.length === 0) return null
return items.find(item => item.children?.some(c => c.id === id)) ??
findParent(id, items.flatMap(item => item.children ?? []))
}

function findItemBefore (item) {
return findParent(item.id).children
.find((_, i, all) => all[i + 1]?.id === item.id)
}

function findItemAfter (item) {
return findParent(item.id).children
.find((_, i, all) => all[i - 1]?.id === item.id)
}

function detach (item) {
const parent = findParent(item.id)
parent.children.splice(parent.children.indexOf(item), 1)
if (parent.children.length === 0) parent.children = undefined
}

function injectNextTo (item, target, after = true) {
if (!target || target === root) return
detach(item)
const targetParent = findParent(target.id)
targetParent.children.splice(targetParent.children.indexOf(target) + (after ? 1 : 0), 0, item)
activated.value = [item.id]
}

function appendTo (item, target) {
if (!target) return
detach(item)
target.children ??= []
target.children.push(item)
activated.value = [item.id]
}

function move (item, direction) {
switch (direction) {
case 'left':
injectNextTo(item, findParent(item.id))
break
case 'up':
injectNextTo(item, findItemBefore(item), false)
break
case 'right':
appendTo(item, findItemBefore(item))
break
case 'down':
injectNextTo(item, findItemAfter(item))
break
}
items.value = [...root.children]
}
</script>

<script>
export default {
data: () => ({
activated: [],
root: {
id: 0,
children: [
{
id: 1,
title: 'Office Tools',
children: [
{ id: 2, title: 'Calendar' },
{ id: 3, title: 'Notepad' },
],
},
{
id: 4,
title: 'Dev Tools',
children: [
{ id: 5, title: 'VS Code' },
{ id: 6, title: 'Figma' },
{ id: 7, title: 'Webstorm' },
],
},
],
},
items: [],
}),
mounted () {
this.items = [...this.root.children]
},
methods: {
findParent (id, items) {
items ??= [this.root]
if (items.length === 0) return null
return items.find(item => item.children?.some(c => c.id === id)) ??
this.findParent(id, items.flatMap(item => item.children ?? []))
},
findItemBefore (item) {
return this.findParent(item.id).children
.find((_, i, all) => all[i + 1]?.id === item.id)
},
findItemAfter (item) {
return this.findParent(item.id).children
.find((_, i, all) => all[i - 1]?.id === item.id)
},
detach (item) {
const parent = this.findParent(item.id)
parent.children.splice(parent.children.indexOf(item), 1)
if (parent.children.length === 0) parent.children = undefined
},
injectNextTo (item, target, after = true) {
if (!target || target === this.root) return
this.detach(item)
const targetParent = this.findParent(target.id)
targetParent.children.splice(targetParent.children.indexOf(target) + (after ? 1 : 0), 0, item)
this.activated = [item.id]
},
appendTo (item, target) {
if (!target) return
this.detach(item)
target.children ??= []
target.children.push(item)
this.activated = [item.id]
},
move (item, direction) {
switch (direction) {
case 'left':
this.injectNextTo(item, this.findParent(item.id))
break
case 'up':
this.injectNextTo(item, this.findItemBefore(item), false)
break
case 'right':
this.appendTo(item, this.findItemBefore(item))
break
case 'down':
this.injectNextTo(item, this.findItemAfter(item))
break
}
this.items = [...this.root.children]
},
},
}
</script>
6 changes: 6 additions & 0 deletions packages/docs/src/pages/en/components/treeview.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,12 @@ Both **append**, and **prepend** slots get additional information about the item

<ExamplesExample file="v-treeview/slot-append-and-prepend-item" />

#### No data

When searching within the treeview, you might want to show custom **no-data** slot to provide context or immediate action.

<ExamplesExample file="v-treeview/slot-no-data" />

#### Title

In this example we use a custom **title** slot to apply a line-through the treeview item's text when selected.
Expand Down
15 changes: 14 additions & 1 deletion packages/vuetify/src/components/VTreeview/VTreeview.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// Components
import { makeVTreeviewChildrenProps, VTreeviewChildren } from './VTreeviewChildren'
import { makeVListProps, useListItems, VList } from '@/components/VList/VList'
import { VListItem } from '@/components/VList/VListItem'

// Composables
import { useLocale } from '@/composables'
import { provideDefaults } from '@/composables/defaults'
import { makeFilterProps, useFilter } from '@/composables/filter'
import { useProxiedModel } from '@/composables/proxiedModel'
Expand Down Expand Up @@ -31,6 +33,11 @@ export const makeVTreeviewProps = propsFactory({
openAll: Boolean,
indentLines: [Boolean, String] as PropType<boolean | IndentLinesVariant>,
search: String,
hideNoData: Boolean,
noDataText: {
type: String,
default: '$vuetify.noDataText',
},

...makeFilterProps({ filterKeys: ['title'] }),
...omit(makeVTreeviewChildrenProps(), [
Expand All @@ -53,7 +60,9 @@ export const VTreeview = genericComponent<new <T>(
props: {
items?: T[]
},
slots: VTreeviewChildrenSlots<T>
slots: VTreeviewChildrenSlots<T> & {
'no-data': never
}
) => GenericProps<typeof props, typeof slots>>()({
name: 'VTreeview',

Expand All @@ -69,6 +78,7 @@ export const VTreeview = genericComponent<new <T>(
},

setup (props, { slots, emit }) {
const { t } = useLocale()
const { items } = useListItems(props)
const activeColor = toRef(() => props.activeColor)
const baseColor = toRef(() => props.baseColor)
Expand Down Expand Up @@ -175,6 +185,9 @@ export const VTreeview = genericComponent<new <T>(
v-model:activated={ activated.value }
v-model:selected={ selected.value }
>
{ visibleIds.value?.size === 0 && !props.hideNoData && (
slots['no-data']?.() ?? (<VListItem key="no-data" title={ t(props.noDataText) } />)
)}
<VTreeviewChildren
{ ...treeviewChildrenProps }
density={ props.density }
Expand Down