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
188 changes: 188 additions & 0 deletions packages/docs/src/examples/v-treeview/slot-append-and-prepend-item.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: 5 additions & 1 deletion packages/docs/src/pages/en/components/treeview.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,16 @@ You can dynamically load child data by supplying a _Promise_ callback to the **l

The `v-treeview` component has several slots that allow you to customize the appearance and behavior of its items.

#### Prepend
#### Append and prepend

Using the the **prepend** slot we are able to create an intuitive file explorer.

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

Both **append**, and **prepend** slots get additional information about the item: `depth`, `path` (from indexes), `isFirst`, `isLast` and the `index` within the children list.

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

#### Title

In this example we use a custom **title** slot to apply a line-through the treeview item's text when selected.
Expand Down
1 change: 1 addition & 0 deletions packages/docs/src/plugins/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ export {
mdiCrosshairsGps,
mdiCrown,
mdiCupcake,
mdiCursorPointer,
mdiDatabaseSearchOutline,
mdiDelete,
mdiDeleteOutline,
Expand Down
5 changes: 5 additions & 0 deletions packages/vuetify/src/components/VList/VListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ import type { PropType } from 'vue'
import type { RippleDirectiveBinding } from '@/directives/ripple'

export type ListItemSlot = {
index?: Number
depth?: Number
path?: Number[]
isFirst?: boolean
isLast?: boolean
isActive: boolean
isOpen: boolean
isSelected: boolean
Expand Down
2 changes: 1 addition & 1 deletion packages/vuetify/src/labs/VTreeview/VTreeview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const makeVTreeviewProps = propsFactory({
search: String,

...makeFilterProps({ filterKeys: ['title'] }),
...makeVTreeviewChildrenProps(),
...omit(makeVTreeviewChildrenProps(), ['index', 'path']),
...omit(makeVListProps({
collapseIcon: '$treeviewCollapse',
expandIcon: '$treeviewExpand',
Expand Down
25 changes: 20 additions & 5 deletions packages/vuetify/src/labs/VTreeview/VTreeviewChildren.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@ export const makeVTreeviewChildrenProps = propsFactory({
selectable: Boolean,
selectedColor: String,
selectStrategy: [String, Function, Object] as PropType<SelectStrategyProp>,

index: Number,
path: {
type: Array as PropType<Number[]>,
default: () => [],
},
...makeDensityProps(),
}, 'VTreeviewChildren')

Expand Down Expand Up @@ -92,9 +96,18 @@ export const VTreeviewChildren = genericComponent<new <T extends InternalListIte
}
}

return () => slots.default?.() ?? props.items?.map(item => {
return () => slots.default?.() ?? props.items?.map((item, index) => {
const { children, props: itemProps } = item
const loading = isLoading.has(item.value)

const treeItemProps = {
index,
depth: props.path?.length ?? 0,
isFirst: index === 0,
isLast: props.items ? props.items.length - 1 === index : false,
path: [...props.path, index],
}

const slotsWithItem = {
prepend: slotProps => (
<>
Expand All @@ -121,16 +134,18 @@ export const VTreeviewChildren = genericComponent<new <T extends InternalListIte
</div>
)}

{ slots.prepend?.({ ...slotProps, item: item.raw, internalItem: item }) }
{ slots.prepend?.({ ...slotProps, ...treeItemProps, item: item.raw, internalItem: item }) }
</>
),
append: slots.append ? slotProps => slots.append?.({ ...slotProps, item: item.raw, internalItem: item }) : undefined,
append: slots.append
? slotProps => slots.append?.({ ...slotProps, ...treeItemProps, item: item.raw, internalItem: item })
: undefined,
title: slots.title ? slotProps => slots.title?.({ ...slotProps, item: item.raw, internalItem: item }) : undefined,
subtitle: slots.subtitle ? slotProps => slots.subtitle?.({ ...slotProps, item: item.raw, internalItem: item }) : undefined,
} satisfies VTreeviewItem['$props']['$children']

const treeviewGroupProps = VTreeviewGroup.filterProps(itemProps)
const treeviewChildrenProps = VTreeviewChildren.filterProps(props)
const treeviewChildrenProps = VTreeviewChildren.filterProps({ ...props, ...treeItemProps })

return children ? (
<VTreeviewGroup
Expand Down