quasar
Version:
Build high-performance VueJS user interfaces (SPA, PWA, SSR, Mobile and Desktop) in record time
846 lines (747 loc) • 23.1 kB
JavaScript
import {
h,
ref,
computed,
watch,
withDirectives,
vShow,
nextTick,
getCurrentInstance,
onBeforeUpdate
} 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 useDark, {
useDarkProps
} from '../../composables/private.use-dark/use-dark.js'
import { createComponent } from '../../utils/private.create/create.js'
import { stopAndPrevent } from '../../utils/event/event.js'
import { shouldIgnoreKey } from '../../utils/private.keyboard/key-composition.js'
import { injectProp } from '../../utils/private.inject-obj-prop/inject-obj-prop.js'
const tickStrategyOptions = ['none', 'strict', 'leaf', 'leaf-filtered']
export default createComponent({
name: 'QTree',
props: {
...useDarkProps,
nodes: {
type: Array,
required: true
},
nodeKey: {
type: String,
required: true
},
labelKey: {
type: String,
default: 'label'
},
childrenKey: {
type: String,
default: 'children'
},
dense: Boolean,
color: String,
controlColor: String,
textColor: String,
selectedColor: String,
icon: String,
tickStrategy: {
type: String,
default: 'none',
validator: v => tickStrategyOptions.includes(v)
},
ticked: Array, // v-model:ticked
expanded: Array, // v-model:expanded
selected: {}, // v-model:selected
noSelectionUnset: Boolean,
defaultExpandAll: Boolean,
accordion: Boolean,
filter: String,
filterMethod: Function,
duration: {},
noConnectors: Boolean,
noTransition: Boolean,
noNodesLabel: String,
noResultsLabel: String
},
emits: [
'update:expanded',
'update:ticked',
'update:selected',
'lazyLoad',
'afterShow',
'afterHide'
],
setup(props, { slots, emit }) {
const { proxy } = getCurrentInstance()
const { $q } = proxy
const isDark = useDark(props, $q)
const lazy = ref({})
const innerTicked = ref(props.ticked || [])
const innerExpanded = ref(props.expanded || [])
let blurTargets = {}
onBeforeUpdate(() => {
blurTargets = {}
})
const classes = computed(
() =>
`q-tree q-tree--${props.dense === true ? 'dense' : 'standard'}` +
(props.noConnectors === true ? ' q-tree--no-connectors' : '') +
(isDark.value === true ? ' q-tree--dark' : '') +
(props.color !== void 0 ? ` text-${props.color}` : '')
)
const hasSelection = computed(() => props.selected !== void 0)
const computedIcon = computed(() => props.icon || $q.iconSet.tree.icon)
const computedControlColor = computed(
() => props.controlColor || props.color
)
const textColorClass = computed(() =>
props.textColor !== void 0 ? ` text-${props.textColor}` : ''
)
const selectedColorClass = computed(() => {
const color = props.selectedColor || props.color
return color ? ` text-${color}` : ''
})
const computedFilterMethod = computed(() =>
props.filterMethod !== void 0
? props.filterMethod
: (node, filter) => {
const filt = filter.toLowerCase()
return (
node[props.labelKey] &&
node[props.labelKey].toLowerCase().indexOf(filt) !== -1
)
}
)
const meta = computed(() => {
const acc = {}
const travel = (node, parent) => {
const tickStrategy =
node.tickStrategy ||
(parent ? parent.tickStrategy : props.tickStrategy)
const key = node[props.nodeKey],
isParent =
node[props.childrenKey] &&
Array.isArray(node[props.childrenKey]) &&
node[props.childrenKey].length !== 0,
selectable =
node.disabled !== true &&
hasSelection.value === 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 localLazy = node.lazy
if (
localLazy === true &&
lazy.value[key] !== void 0 &&
Array.isArray(node[props.childrenKey]) === true
) {
localLazy = lazy.value[key]
}
const m = {
key,
parent,
isParent,
lazy: localLazy,
disabled: node.disabled,
link:
node.disabled !== true &&
(selectable === true ||
(expandable === true &&
(isParent === true || localLazy === true))),
children: [],
matchesFilter: props.filter
? computedFilterMethod.value(node, props.filter)
: true,
selected: key === props.selected && selectable === true,
selectable,
expanded:
isParent === true ? innerExpanded.value.includes(key) : false,
expandable,
noTick:
node.noTick === true ||
(strictTicking !== true && localLazy && localLazy !== 'loaded'),
tickable,
tickStrategy,
hasTicking,
strictTicking,
leafFilteredTicking,
leafTicking,
ticked:
strictTicking === true
? innerTicked.value.includes(key)
: isParent === true
? false
: innerTicked.value.includes(key)
}
acc[key] = m
if (isParent === true) {
m.children = node[props.childrenKey].map(n => travel(n, m))
if (props.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(
entry => entry.indeterminate === true
)
m.tickable =
m.tickable === true && m.children.some(entry => entry.tickable)
if (m.indeterminate !== true) {
const sel = m.children.reduce(
(localAcc, entry) =>
entry.ticked === true ? localAcc + 1 : localAcc,
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(
entry => entry.tickable !== true || entry.ticked !== true
)
}
}
}
}
return m
}
props.nodes.forEach(node => travel(node, null))
return acc
})
watch(
() => props.ticked,
val => {
innerTicked.value = val
}
)
watch(
() => props.expanded,
val => {
innerExpanded.value = val
}
)
function 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[props.nodeKey] === key) {
return node
}
if (node[props.childrenKey]) {
return find(null, node[props.childrenKey])
}
}
return find(null, props.nodes)
}
function getTickedNodes() {
return innerTicked.value.map(key => getNodeByKey(key))
}
function getExpandedNodes() {
return innerExpanded.value.map(key => getNodeByKey(key))
}
function isExpanded(key) {
return key && meta.value[key] ? meta.value[key].expanded : false
}
function collapseAll() {
if (props.expanded !== void 0) {
emit('update:expanded', [])
} else {
innerExpanded.value = []
}
}
function expandAll() {
const expanded = []
const travel = node => {
if (node[props.childrenKey] && node[props.childrenKey].length !== 0) {
if (node.expandable !== false && node.disabled !== true) {
expanded.push(node[props.nodeKey])
node[props.childrenKey].forEach(travel)
}
}
}
props.nodes.forEach(travel)
if (props.expanded !== void 0) {
emit('update:expanded', expanded)
} else {
innerExpanded.value = expanded
}
}
function setExpanded(
key,
state,
node = getNodeByKey(key),
m = meta.value[key]
) {
if (m.lazy && m.lazy !== 'loaded') {
if (m.lazy === 'loading') return
lazy.value[key] = 'loading'
if (Array.isArray(node[props.childrenKey]) !== true) {
node[props.childrenKey] = []
}
emit('lazyLoad', {
node,
key,
done: children => {
lazy.value[key] = 'loaded'
node[props.childrenKey] =
Array.isArray(children) === true ? children : []
nextTick(() => {
const localMeta = meta.value[key]
if (localMeta?.isParent === true) {
localSetExpanded(key, true)
}
})
},
fail: () => {
delete lazy.value[key]
if (node[props.childrenKey].length === 0) {
delete node[props.childrenKey]
}
}
})
} else if (m.isParent === true && m.expandable === true) {
localSetExpanded(key, state)
}
}
function localSetExpanded(key, state) {
let target = innerExpanded.value
const shouldEmit = props.expanded !== void 0
if (shouldEmit === true) {
target = target.slice()
}
if (state) {
if (props.accordion) {
if (meta.value[key]) {
const collapse = []
if (meta.value[key].parent) {
meta.value[key].parent.children.forEach(m => {
if (m.key !== key && m.expandable === true) {
collapse.push(m.key)
}
})
} else {
props.nodes.forEach(node => {
const k = node[props.nodeKey]
if (k !== key) {
collapse.push(k)
}
})
}
if (collapse.length !== 0) {
target = target.filter(k => collapse.includes(k) === false)
}
}
}
target = target
.concat([key])
.filter((entryKey, index, self) => self.indexOf(entryKey) === index)
} else {
target = target.filter(k => k !== key)
}
if (shouldEmit === true) {
emit('update:expanded', target)
} else {
innerExpanded.value = target
}
}
function isTicked(key) {
return key && meta.value[key] ? meta.value[key].ticked : false
}
function setTicked(keys, state) {
let target = innerTicked.value
const shouldEmit = props.ticked !== void 0
if (shouldEmit === 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 (shouldEmit === true) {
emit('update:ticked', target)
}
}
function getSlotScope(node, localMeta, key) {
const scope = {
tree: proxy,
node,
key,
color: props.color,
dark: isDark.value
}
injectProp(
scope,
'expanded',
() => localMeta.expanded,
val => {
if (val !== localMeta.expanded) {
setExpanded(key, val)
}
}
)
injectProp(
scope,
'ticked',
() => localMeta.ticked,
val => {
if (val !== localMeta.ticked) {
setTicked([key], val)
}
}
)
return scope
}
function getChildren(nodes) {
return (
props.filter
? nodes.filter(n => meta.value[n[props.nodeKey]].matchesFilter)
: nodes
).map(child => getNode(child))
}
function getNodeMedia(node) {
if (node.icon !== void 0) {
return h(QIcon, {
class: 'q-tree__icon q-mr-sm',
name: node.icon,
color: node.iconColor
})
}
const src = node.img || node.avatar
if (src) {
return h('img', {
class: `q-tree__${node.img ? 'img' : 'avatar'} q-mr-sm`,
src
})
}
}
function onShow() {
emit('afterShow')
}
function onHide() {
emit('afterHide')
}
function getNode(node) {
const key = node[props.nodeKey],
m = meta.value[key],
header = node.header
? slots[`header-${node.header}`] || slots['default-header']
: slots['default-header']
const children =
m.isParent === true ? getChildren(node[props.childrenKey]) : []
const isParent = children.length !== 0 || (m.lazy && m.lazy !== 'loaded')
let body = node.body
? slots[`body-${node.body}`] || slots['default-body']
: slots['default-body']
const slotScope =
header !== void 0 || body !== void 0 ? getSlotScope(node, m, key) : null
if (body !== void 0) {
body = h('div', { class: 'q-tree__node-body relative-position' }, [
h('div', { class: textColorClass.value }, [body(slotScope)])
])
}
return h(
'div',
{
key,
class:
'q-tree__node relative-position' +
` q-tree__node--${isParent === true ? 'parent' : 'child'}`
},
[
h(
'div',
{
class:
'q-tree__node-header relative-position row no-wrap items-center' +
(m.link === true
? ' q-tree__node--link q-hoverable q-focusable'
: '') +
(m.selected === true ? ' q-tree__node--selected' : '') +
(m.disabled === true ? ' q-tree__node--disabled' : ''),
tabindex: m.link === true ? 0 : -1,
ariaExpanded: children.length > 0 ? m.expanded : null,
role: 'treeitem',
onClick: e => {
onClick(node, m, e)
},
onKeypress(e) {
if (shouldIgnoreKey(e) !== true) {
if (e.keyCode === 13) {
onClick(node, m, e, true)
} else if (e.keyCode === 32) {
onExpandClick(node, m, e, true)
}
}
}
},
[
h('div', {
class: 'q-focus-helper',
tabindex: -1,
ref: el => {
blurTargets[m.key] = el
}
}),
m.lazy === 'loading'
? h(QSpinner, {
class: 'q-tree__spinner',
color: computedControlColor.value
})
: isParent === true
? h(QIcon, {
class:
'q-tree__arrow' +
(m.expanded === true ? ' q-tree__arrow--rotate' : ''),
name: computedIcon.value,
onClick(e) {
onExpandClick(node, m, e)
}
})
: null,
m.hasTicking === true && m.noTick !== true
? h(QCheckbox, {
class: 'q-tree__tickbox',
modelValue: m.indeterminate === true ? null : m.ticked,
color: computedControlColor.value,
dark: isDark.value,
dense: true,
keepColor: true,
disable: m.tickable !== true,
onKeydown: stopAndPrevent,
'onUpdate:modelValue': v => {
onTickedClick(m, v)
}
})
: null,
h(
'div',
{
class:
'q-tree__node-header-content col row no-wrap items-center' +
(m.selected === true
? selectedColorClass.value
: textColorClass.value)
},
[
header
? header(slotScope)
: [getNodeMedia(node), h('div', node[props.labelKey])]
]
)
]
),
isParent === true
? props.noTransition === true
? m.expanded === true
? h(
'div',
{
class: 'q-tree__node-collapsible' + textColorClass.value,
key: `${key}__q`
},
[
body,
h(
'div',
{
class:
'q-tree__children' +
(m.disabled === true
? ' q-tree__node--disabled'
: ''),
role: 'group'
},
children
)
]
)
: null
: h(
QSlideTransition,
{
duration: props.duration,
onShow,
onHide
},
() =>
withDirectives(
h(
'div',
{
class:
'q-tree__node-collapsible' + textColorClass.value,
key: `${key}__q`
},
[
body,
h(
'div',
{
class:
'q-tree__children' +
(m.disabled === true
? ' q-tree__node--disabled'
: ''),
role: 'group'
},
children
)
]
),
[[vShow, m.expanded]]
)
)
: body
]
)
}
function blur(key) {
blurTargets[key]?.focus()
}
function onClick(node, localMeta, e, keyboard) {
if (keyboard !== true && localMeta.selectable !== false) {
blur(localMeta.key)
}
if (hasSelection.value && localMeta.selectable) {
if (props.noSelectionUnset === false) {
emit(
'update:selected',
localMeta.key !== props.selected ? localMeta.key : null
)
} else if (localMeta.key !== props.selected) {
emit(
'update:selected',
localMeta.key === void 0 ? null : localMeta.key
)
}
} else {
onExpandClick(node, localMeta, e, keyboard)
}
if (typeof node.handler === 'function') {
node.handler(node)
}
}
function onExpandClick(node, localMeta, e, keyboard) {
if (e !== void 0) {
stopAndPrevent(e)
}
if (keyboard !== true && localMeta.selectable !== false) {
blur(localMeta.key)
}
setExpanded(localMeta.key, !localMeta.expanded, node, localMeta)
}
function onTickedClick(localMeta, state) {
if (localMeta.indeterminate === true) {
state = localMeta.indeterminateNextState
}
if (localMeta.strictTicking) {
setTicked([localMeta.key], state)
} else if (localMeta.leafTicking) {
const keys = []
const travel = nodeMeta => {
if (nodeMeta.isParent) {
if (
state !== true &&
nodeMeta.noTick !== true &&
nodeMeta.tickable === true
) {
keys.push(nodeMeta.key)
}
if (nodeMeta.leafTicking === true) {
nodeMeta.children.forEach(travel)
}
} else if (
nodeMeta.noTick !== true &&
nodeMeta.tickable === true &&
(nodeMeta.leafFilteredTicking !== true ||
nodeMeta.matchesFilter === true)
) {
keys.push(nodeMeta.key)
}
}
travel(localMeta)
setTicked(keys, state)
}
}
if (props.defaultExpandAll === true) {
expandAll()
}
// expose public methods
Object.assign(proxy, {
getNodeByKey,
getTickedNodes,
getExpandedNodes,
isExpanded,
collapseAll,
expandAll,
setExpanded,
isTicked,
setTicked
})
return () => {
const children = getChildren(props.nodes)
return h(
'div',
{
class: classes.value,
role: 'tree'
},
children.length === 0
? props.filter
? props.noResultsLabel || $q.lang.tree.noResults
: props.noNodesLabel || $q.lang.tree.noNodes
: children
)
}
}
})