UNPKG

vuetify

Version:

Vue.js 2 Semantic Component Framework

341 lines (284 loc) 10.2 kB
// 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) } })