UNPKG

quasar

Version:

Build high-performance VueJS user interfaces (SPA, PWA, SSR, Mobile and Desktop) in record time

665 lines (580 loc) 18.5 kB
import Vue from 'vue' import QIcon from '../icon/QIcon.js' import QCheckbox from '../checkbox/QCheckbox.js' import QSlideTransition from '../slide-transition/QSlideTransition.js' import QSpinner from '../spinner/QSpinner.js' import DarkMixin from '../../mixins/dark.js' import { stopAndPrevent } from '../../utils/event.js' import { shouldIgnoreKey } from '../../utils/key-composition.js' import { cache } from '../../utils/vm.js' export default Vue.extend({ name: 'QTree', mixins: [ DarkMixin ], props: { nodes: { type: Array, required: true }, nodeKey: { type: String, required: true }, labelKey: { type: String, default: 'label' }, color: String, controlColor: String, textColor: String, selectedColor: String, icon: String, tickStrategy: { type: String, default: 'none', validator: v => ['none', 'strict', 'leaf', 'leaf-filtered'].includes(v) }, ticked: Array, // sync expanded: Array, // sync selected: {}, // sync defaultExpandAll: Boolean, accordion: Boolean, filter: String, filterMethod: { type: Function, default (node, filter) { const filt = filter.toLowerCase() return node[this.labelKey] && node[this.labelKey].toLowerCase().indexOf(filt) > -1 } }, duration: Number, noConnectors: Boolean, noNodesLabel: String, noResultsLabel: String }, computed: { classes () { return `q-tree` + (this.noConnectors === true ? ` q-tree--no-connectors` : '') + (this.isDark === true ? ` q-tree--dark` : '') + (this.color !== void 0 ? ` text-${this.color}` : '') }, hasSelection () { return this.selected !== void 0 }, computedIcon () { return this.icon || this.$q.iconSet.tree.icon }, computedControlColor () { return this.controlColor || this.color }, textColorClass () { if (this.textColor !== void 0) { return `text-${this.textColor}` } }, selectedColorClass () { const color = this.selectedColor || this.color if (color) { return `text-${color}` } }, meta () { const meta = {} const travel = (node, parent) => { const tickStrategy = node.tickStrategy || (parent ? parent.tickStrategy : this.tickStrategy) const key = node[this.nodeKey], isParent = node.children && node.children.length > 0, isLeaf = isParent !== true, selectable = node.disabled !== true && this.hasSelection === true && node.selectable !== false, expandable = node.disabled !== true && node.expandable !== false, hasTicking = tickStrategy !== 'none', strictTicking = tickStrategy === 'strict', leafFilteredTicking = tickStrategy === 'leaf-filtered', leafTicking = tickStrategy === 'leaf' || tickStrategy === 'leaf-filtered' let tickable = node.disabled !== true && node.tickable !== false if (leafTicking === true && tickable === true && parent && parent.tickable !== true) { tickable = false } let lazy = node.lazy if (lazy && this.lazy[key]) { lazy = this.lazy[key] } const m = { key, parent, isParent, isLeaf, lazy, disabled: node.disabled, link: node.disabled !== true && (selectable === true || (expandable === true && (isParent === true || lazy === true))), children: [], matchesFilter: this.filter ? this.filterMethod(node, this.filter) : true, selected: key === this.selected && selectable === true, selectable, expanded: isParent === true ? this.innerExpanded.includes(key) : false, expandable, noTick: node.noTick === true || (strictTicking !== true && lazy && lazy !== 'loaded'), tickable, tickStrategy, hasTicking, strictTicking, leafFilteredTicking, leafTicking, ticked: strictTicking === true ? this.innerTicked.includes(key) : (isLeaf === true ? this.innerTicked.includes(key) : false) } meta[key] = m if (isParent === true) { m.children = node.children.map(n => travel(n, m)) if (this.filter) { if (m.matchesFilter !== true) { m.matchesFilter = m.children.some(n => n.matchesFilter) } else if ( m.noTick !== true && m.disabled !== true && m.tickable === true && leafFilteredTicking === true && m.children.every(n => n.matchesFilter !== true || n.noTick === true || n.tickable !== true) === true ) { m.tickable = false } } if (m.matchesFilter === true) { if (m.noTick !== true && strictTicking !== true && m.children.every(n => n.noTick) === true) { m.noTick = true } if (leafTicking) { m.ticked = false m.indeterminate = m.children.some(node => node.indeterminate === true) m.tickable = m.tickable === true && m.children.some(node => node.tickable) if (m.indeterminate !== true) { const sel = m.children .reduce((acc, meta) => meta.ticked === true ? acc + 1 : acc, 0) if (sel === m.children.length) { m.ticked = true } else if (sel > 0) { m.indeterminate = true } } if (m.indeterminate === true) { m.indeterminateNextState = m.children .every(meta => meta.tickable !== true || meta.ticked !== true) } } } } return m } this.nodes.forEach(node => travel(node, null)) return meta } }, data () { return { lazy: {}, innerTicked: this.ticked || [], innerExpanded: this.expanded || [] } }, watch: { ticked (val) { this.innerTicked = val }, expanded (val) { this.innerExpanded = val } }, methods: { getNodeByKey (key) { const reduce = [].reduce const find = (result, node) => { if (result || !node) { return result } if (Array.isArray(node) === true) { return reduce.call(Object(node), find, result) } if (node[this.nodeKey] === key) { return node } if (node.children) { return find(null, node.children) } } return find(null, this.nodes) }, getTickedNodes () { return this.innerTicked.map(key => this.getNodeByKey(key)) }, getExpandedNodes () { return this.innerExpanded.map(key => this.getNodeByKey(key)) }, isExpanded (key) { return key && this.meta[key] ? this.meta[key].expanded : false }, collapseAll () { if (this.expanded !== void 0) { this.$emit('update:expanded', []) } else { this.innerExpanded = [] } }, expandAll () { const expanded = this.innerExpanded, travel = node => { if (node.children && node.children.length > 0) { if (node.expandable !== false && node.disabled !== true) { expanded.push(node[this.nodeKey]) node.children.forEach(travel) } } } this.nodes.forEach(travel) if (this.expanded !== void 0) { this.$emit('update:expanded', expanded) } else { this.innerExpanded = expanded } }, setExpanded (key, state, node = this.getNodeByKey(key), meta = this.meta[key]) { if (meta.lazy && meta.lazy !== 'loaded') { if (meta.lazy === 'loading') { return } this.$set(this.lazy, key, 'loading') this.$emit('lazy-load', { node, key, done: children => { this.lazy[key] = 'loaded' if (children) { this.$set(node, 'children', children) } this.$nextTick(() => { const m = this.meta[key] if (m && m.isParent === true) { this.__setExpanded(key, true) } }) }, fail: () => { this.$delete(this.lazy, key) } }) } else if (meta.isParent === true && meta.expandable === true) { this.__setExpanded(key, state) } }, __setExpanded (key, state) { let target = this.innerExpanded const emit = this.expanded !== void 0 if (emit === true) { target = target.slice() } if (state) { if (this.accordion) { if (this.meta[key]) { const collapse = [] if (this.meta[key].parent) { this.meta[key].parent.children.forEach(m => { if (m.key !== key && m.expandable === true) { collapse.push(m.key) } }) } else { this.nodes.forEach(node => { const k = node[this.nodeKey] if (k !== key) { collapse.push(k) } }) } if (collapse.length > 0) { target = target.filter(k => collapse.includes(k) === false) } } } target = target.concat([ key ]) .filter((key, index, self) => self.indexOf(key) === index) } else { target = target.filter(k => k !== key) } if (emit === true) { this.$emit(`update:expanded`, target) } else { this.innerExpanded = target } }, isTicked (key) { return key && this.meta[key] ? this.meta[key].ticked : false }, setTicked (keys, state) { let target = this.innerTicked const emit = this.ticked !== void 0 if (emit === true) { target = target.slice() } if (state) { target = target.concat(keys) .filter((key, index, self) => self.indexOf(key) === index) } else { target = target.filter(k => keys.includes(k) === false) } if (emit === true) { this.$emit(`update:ticked`, target) } }, __getSlotScope (node, meta, key) { const scope = { tree: this, node, key, color: this.color, dark: this.isDark } Object.defineProperty(scope, 'expanded', { get: () => { return meta.expanded }, set: val => { val !== meta.expanded && this.setExpanded(key, val) }, configurable: true, enumerable: true }) Object.defineProperty(scope, 'ticked', { get: () => { return meta.ticked }, set: val => { val !== meta.ticked && this.setTicked([ key ], val) }, configurable: true, enumerable: true }) return scope }, __getChildren (h, nodes) { return ( this.filter ? nodes.filter(n => this.meta[n[this.nodeKey]].matchesFilter) : nodes ).map(child => this.__getNode(h, child)) }, __getNodeMedia (h, node) { if (node.icon !== void 0) { return h(QIcon, { staticClass: `q-tree__icon q-mr-sm`, props: { name: node.icon, color: node.iconColor } }) } const src = node.img || node.avatar if (src) { return h('img', { staticClass: `q-tree__${node.img ? 'img' : 'avatar'} q-mr-sm`, attrs: { src } }) } }, __getNode (h, node) { const key = node[this.nodeKey], meta = this.meta[key], header = node.header ? this.$scopedSlots[`header-${node.header}`] || this.$scopedSlots['default-header'] : this.$scopedSlots['default-header'] const children = meta.isParent === true ? this.__getChildren(h, node.children) : [] const isParent = children.length > 0 || (meta.lazy && meta.lazy !== 'loaded') let body = node.body ? this.$scopedSlots[`body-${node.body}`] || this.$scopedSlots['default-body'] : this.$scopedSlots['default-body'], slotScope = header !== void 0 || body !== void 0 ? this.__getSlotScope(node, meta, key) : null if (body !== void 0) { body = h('div', { staticClass: 'q-tree__node-body relative-position' }, [ h('div', { class: this.textColorClass }, [ body(slotScope) ]) ]) } return h('div', { key, staticClass: 'q-tree__node relative-position', class: { 'q-tree__node--parent': isParent, 'q-tree__node--child': !isParent } }, [ h('div', { staticClass: 'q-tree__node-header relative-position row no-wrap items-center', class: { 'q-tree__node--link q-hoverable q-focusable': meta.link, 'q-tree__node--selected': meta.selected, 'q-tree__node--disabled': meta.disabled }, attrs: { tabindex: meta.link ? 0 : -1 }, on: { click: (e) => { this.__onClick(node, meta, e) }, keypress: e => { if (shouldIgnoreKey(e) !== true) { if (e.keyCode === 13) { this.__onClick(node, meta, e, true) } else if (e.keyCode === 32) { this.__onExpandClick(node, meta, e, true) } } } } }, [ h('div', { staticClass: 'q-focus-helper', attrs: { tabindex: -1 }, ref: `blurTarget_${meta.key}` }), meta.lazy === 'loading' ? h(QSpinner, { staticClass: 'q-tree__spinner q-mr-xs', props: { color: this.computedControlColor } }) : ( isParent === true ? h(QIcon, { staticClass: 'q-tree__arrow q-mr-xs', class: { 'q-tree__arrow--rotate': meta.expanded }, props: { name: this.computedIcon }, nativeOn: { click: e => { this.__onExpandClick(node, meta, e) } } }) : null ), meta.hasTicking === true && meta.noTick !== true ? h(QCheckbox, { staticClass: 'q-mr-xs', props: { value: meta.indeterminate === true ? null : meta.ticked, color: this.computedControlColor, dark: this.isDark, dense: true, keepColor: true, disable: meta.tickable !== true }, on: { keydown: stopAndPrevent, input: v => { this.__onTickedClick(meta, v) } } }) : null, h('div', { 'staticClass': 'q-tree__node-header-content col row no-wrap items-center', class: meta.selected ? this.selectedColorClass : this.textColorClass }, [ header ? header(slotScope) : [ this.__getNodeMedia(h, node), h('div', node[this.labelKey]) ] ]) ]), isParent === true ? h(QSlideTransition, { props: { duration: this.duration }, on: cache(this, 'slide', { show: () => { this.$emit('after-show') }, hide: () => { this.$emit('after-hide') } }) }, [ h('div', { staticClass: 'q-tree__node-collapsible', class: this.textColorClass, directives: [{ name: 'show', value: meta.expanded }] }, [ body, h('div', { staticClass: 'q-tree__children', class: { 'q-tree__node--disabled': meta.disabled } }, children) ]) ]) : body ]) }, __blur (key) { const blurTarget = this.$refs[`blurTarget_${key}`] blurTarget !== void 0 && blurTarget.focus() }, __onClick (node, meta, e, keyboard) { keyboard !== true && this.__blur(meta.key) if (this.hasSelection) { if (meta.selectable) { this.$emit('update:selected', meta.key !== this.selected ? meta.key : null) } } else { this.__onExpandClick(node, meta, e, keyboard) } if (typeof node.handler === 'function') { node.handler(node) } }, __onExpandClick (node, meta, e, keyboard) { if (e !== void 0) { stopAndPrevent(e) } keyboard !== true && this.__blur(meta.key) this.setExpanded(meta.key, !meta.expanded, node, meta) }, __onTickedClick (meta, state) { if (meta.indeterminate === true) { state = meta.indeterminateNextState } if (meta.strictTicking) { this.setTicked([ meta.key ], state) } else if (meta.leafTicking) { const keys = [] const travel = meta => { if (meta.isParent) { if (state !== true && meta.noTick !== true && meta.tickable === true) { keys.push(meta.key) } if (meta.leafTicking === true) { meta.children.forEach(travel) } } else if ( meta.noTick !== true && meta.tickable === true && (meta.leafFilteredTicking !== true || meta.matchesFilter === true) ) { keys.push(meta.key) } } travel(meta) this.setTicked(keys, state) } } }, render (h) { const children = this.__getChildren(h, this.nodes) return h( 'div', { class: this.classes }, children.length === 0 ? ( this.filter ? this.noResultsLabel || this.$q.lang.tree.noResults : this.noNodesLabel || this.$q.lang.tree.noNodes ) : children ) }, created () { this.defaultExpandAll === true && this.expandAll() } })