vuetify
Version:
Vue.js 2 Semantic Component Framework
341 lines (284 loc) • 10.2 kB
text/typescript
// Styles
import '../../stylus/components/_treeview.styl'
// Types
import { VNode, VNodeChildrenArrayContents } from 'vue'
import { PropValidator } from 'vue/types/options'
// Components
import VTreeviewNode, { VTreeviewNodeProps } from './VTreeviewNode'
// Mixins
import Themeable from '../../mixins/themeable'
import { provide as RegistrableProvide } from '../../mixins/registrable'
// Utils
import { getObjectValueByPath, deepEqual } from '../../util/helpers'
import mixins from '../../util/mixins'
import { consoleWarn } from '../../util/console'
type VTreeviewNodeInstance = InstanceType<typeof VTreeviewNode>
type NodeCache = Set<string | number>
type NodeArray = (string | number)[]
type NodeState = {
parent: number | string | null
children: (number | string)[]
vnode: VTreeviewNodeInstance | null
isActive: boolean
isSelected: boolean
isIndeterminate: boolean
isOpen: boolean
}
function ston (s: string | number) {
const n = Number(s)
return !isNaN(n) ? n : s
}
export default mixins(
RegistrableProvide('treeview'),
Themeable
/* @vue/component */
).extend({
name: 'v-treeview',
provide (): object {
return { treeview: this }
},
props: {
active: {
type: Array,
default: () => ([])
} as PropValidator<NodeArray>,
items: {
type: Array,
default: () => ([])
} as PropValidator<any[]>,
hoverable: Boolean,
multipleActive: Boolean,
open: {
type: Array,
default: () => ([])
} as PropValidator<NodeArray>,
openAll: Boolean,
value: {
type: Array,
default: () => ([])
} as PropValidator<NodeArray>,
...VTreeviewNodeProps
},
data: () => ({
nodes: {} as Record<string | number, NodeState>,
selectedCache: new Set() as NodeCache,
activeCache: new Set() as NodeCache,
openCache: new Set() as NodeCache
}),
watch: {
items: {
handler () {
// We only care if nodes are removed or added
if (Object.keys(this.nodes).length === this.countItems(this.items)) return
const oldSelectedCache = [...this.selectedCache]
this.selectedCache = new Set()
this.activeCache = new Set()
this.openCache = new Set()
this.buildTree(this.items)
// Only emit selected if selection has changed
// as a result of items changing. This fixes a
// potential double emit when selecting a node
// with dynamic children
if (!deepEqual(oldSelectedCache, [...this.selectedCache])) this.emitSelected()
},
deep: true
},
active (value: (string | number)[]) {
const old = [...this.activeCache]
if (!value || deepEqual(old, value)) return
old.forEach(key => this.updateActive(key, false))
value.forEach(key => this.updateActive(key, true))
this.emitActive()
},
value (value: (string | number)[]) {
const old = [...this.selectedCache]
if (!value || deepEqual(old, value)) return
old.forEach(key => this.updateSelected(key, false))
value.forEach(key => this.updateSelected(key, true))
this.emitSelected()
},
open (value: (string | number)[]) {
const old = [...this.openCache]
if (deepEqual(old, value)) return
old.forEach(key => this.updateOpen(key, false))
value.forEach(key => this.updateOpen(key, true))
this.emitOpen()
}
},
created () {
this.buildTree(this.items)
this.value.forEach(key => this.updateSelected(key, true))
this.emitSelected()
this.active.forEach(key => this.updateActive(key, true))
this.emitActive()
},
mounted () {
// Save the developer from themselves
if (this.$slots.prepend || this.$slots.append) {
consoleWarn('The prepend and append slots require a slot-scope attribute', this)
}
if (this.openAll) {
Object.keys(this.nodes).forEach(key => this.updateOpen(ston(key), true))
} else {
this.open.forEach(key => this.updateOpen(key, true))
}
this.emitOpen()
},
methods: {
buildTree (items: any[], parent: (string | number | null) = null) {
for (let i = 0; i < items.length; i++) {
const item = items[i]
const key = getObjectValueByPath(item, this.itemKey)
const children = getObjectValueByPath(item, this.itemChildren, [])
const oldNode = this.nodes.hasOwnProperty(key) ? this.nodes[key] : {
isSelected: false, isIndeterminate: false, isActive: false, isOpen: false, vnode: null
} as NodeState
const node: any = {
vnode: oldNode.vnode,
parent,
children: children.map((c: any) => getObjectValueByPath(c, this.itemKey))
}
this.buildTree(children, key)
// This fixed bug with dynamic children resetting selected parent state
if (!this.nodes.hasOwnProperty(key) && parent !== null && this.nodes.hasOwnProperty(parent)) {
node.isSelected = this.nodes[parent].isSelected
node.isIndeterminate = this.nodes[parent].isIndeterminate
} else {
node.isSelected = oldNode.isSelected
node.isIndeterminate = oldNode.isIndeterminate
}
node.isActive = oldNode.isActive
node.isOpen = oldNode.isOpen
this.nodes[key] = !children.length ? node : this.calculateState(node, this.nodes)
// Don't forget to rebuild cache
if (this.nodes[key].isSelected) this.selectedCache.add(key)
if (this.nodes[key].isActive) this.activeCache.add(key)
if (this.nodes[key].isOpen) this.openCache.add(key)
this.updateVnodeState(key)
}
},
countItems (items: any[]) {
let count = 0
for (let i = 0; i < items.length; i++) {
const item = items[i]
count += 1
count += item.children ? this.countItems(item.children) : 0
}
return count
},
calculateState (node: NodeState, state: Record<string | number, NodeState>) {
const counts = node.children.reduce((counts: number[], child: string | number) => {
counts[0] += +Boolean(state[child].isSelected)
counts[1] += +Boolean(state[child].isIndeterminate)
return counts
}, [0, 0])
node.isSelected = !!node.children.length && counts[0] === node.children.length
node.isIndeterminate = !node.isSelected && (counts[0] > 0 || counts[1] > 0)
return node
},
emitOpen () {
this.$emit('update:open', [...this.openCache])
},
emitSelected () {
this.$emit('input', [...this.selectedCache])
},
emitActive () {
this.$emit('update:active', [...this.activeCache])
},
getDescendants (key: string | number, descendants: NodeArray = []) {
const children = this.nodes[key].children
descendants.push(...children)
for (let i = 0; i < children.length; i++) {
descendants = this.getDescendants(children[i], descendants)
}
return descendants
},
getParents (key: string | number) {
let parent = this.nodes[key].parent
const parents = []
while (parent !== null) {
parents.push(parent)
parent = this.nodes[parent].parent
}
return parents
},
register (node: VTreeviewNodeInstance) {
const key = getObjectValueByPath(node.item, this.itemKey)
this.nodes[key].vnode = node
this.updateVnodeState(key)
},
unregister (node: VTreeviewNodeInstance) {
const key = getObjectValueByPath(node.item, this.itemKey)
this.nodes[key].vnode = null
},
updateActive (key: string | number, isActive: boolean) {
if (!this.nodes.hasOwnProperty(key)) return
if (!this.multipleActive) {
this.activeCache.forEach(active => {
this.nodes[active].isActive = false
this.updateVnodeState(active)
this.activeCache.delete(active)
})
}
const node = this.nodes[key]
if (!node) return
if (isActive) this.activeCache.add(key)
else this.activeCache.delete(key)
node.isActive = isActive
this.updateVnodeState(key)
},
updateSelected (key: string | number, isSelected: boolean) {
if (!this.nodes.hasOwnProperty(key)) return
const changed: Record<string | number, boolean> = {}
const descendants = [key, ...this.getDescendants(key)]
descendants.forEach(descendant => {
this.nodes[descendant].isSelected = isSelected
this.nodes[descendant].isIndeterminate = false
changed[descendant] = isSelected
})
const parents = this.getParents(key)
parents.forEach(parent => {
this.nodes[parent] = this.calculateState(this.nodes[parent], this.nodes)
changed[parent] = this.nodes[parent].isSelected
})
const all = [key, ...descendants, ...parents]
all.forEach(this.updateVnodeState)
Object.keys(changed).forEach(k => {
changed[k] === true ? this.selectedCache.add(ston(k)) : this.selectedCache.delete(ston(k))
})
},
updateOpen (key: string | number, isOpen: boolean) {
if (!this.nodes.hasOwnProperty(key)) return
const node = this.nodes[key]
if (node.children && !node.children.length && node.vnode && !node.vnode.hasLoaded) {
node.vnode.checkChildren().then(() => this.updateOpen(key, isOpen))
} else {
node.isOpen = isOpen
node.isOpen ? this.openCache.add(key) : this.openCache.delete(key)
this.updateVnodeState(key)
}
},
updateVnodeState (key: string | number) {
const node = this.nodes[key]
if (node && node.vnode) {
node.vnode.isSelected = node.isSelected
node.vnode.isIndeterminate = node.isIndeterminate
node.vnode.isActive = node.isActive
node.vnode.isOpen = node.isOpen
}
}
},
render (h): VNode {
const children: VNodeChildrenArrayContents = this.items.length
? this.items.map(VTreeviewNode.options.methods.genChild.bind(this))
/* istanbul ignore next */
: this.$slots.default
return h('div', {
staticClass: 'v-treeview',
class: {
'v-treeview--hoverable': this.hoverable,
...this.themeClasses
}
}, children)
}
})