diff --git a/ui/playground/src/pages/components/tree.vue b/ui/playground/src/pages/components/tree.vue index d74308922c6..26394d26f70 100644 --- a/ui/playground/src/pages/components/tree.vue +++ b/ui/playground/src/pages/components/tree.vue @@ -19,6 +19,7 @@ +
@@ -36,7 +37,7 @@ - +
@@ -45,6 +46,7 @@
this.$q.notify('Tapped on node 1.3') return { + virtualScroll: true, noConnectors: false, noTransition: false, selected: null, diff --git a/ui/src/components/tree/QTree.js b/ui/src/components/tree/QTree.js index a9266198957..2323d41bb0a 100644 --- a/ui/src/components/tree/QTree.js +++ b/ui/src/components/tree/QTree.js @@ -1,6 +1,7 @@ import { h, ref, computed, watch, - withDirectives, vShow, nextTick, getCurrentInstance, onBeforeUpdate + withDirectives, vShow, nextTick, getCurrentInstance, onBeforeUpdate, + withMemo } from 'vue' import QIcon from '../icon/QIcon.js' @@ -14,6 +15,7 @@ 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' +import QVirtualScroll from '../virtual-scroll/QVirtualScroll.js' const tickStrategyOptions = [ 'none', 'strict', 'leaf', 'leaf-filtered' ] @@ -41,6 +43,7 @@ export default createComponent({ }, dense: Boolean, + virtualScroll: Boolean, color: String, controlColor: String, @@ -83,21 +86,18 @@ export default createComponent({ 'afterHide' ], - setup (props, { slots, emit }) { + setup (props, { slots, emit, attrs }) { const { proxy } = getCurrentInstance() const { $q } = proxy const isDark = useDark(props, $q) const lazy = ref({}) - const innerTicked = ref(props.ticked || []) - const innerExpanded = ref(props.expanded || []) + const innerTicked = ref(new Set(props.ticked || [])) + const innerExpanded = ref(new Set(props.expanded || [])) + const innerNodes = ref(new Map(getNodesPairs(props.nodes))) - let blurTargets = {} - - onBeforeUpdate(() => { - blurTargets = {} - }) + const blurTargets = {} const classes = computed(() => `q-tree q-tree--${ props.dense === true ? 'dense' : 'standard' }` @@ -132,6 +132,35 @@ export default createComponent({ && node[ props.labelKey ].toLowerCase().indexOf(filt) !== -1 } )) + const virtSlots = { + default: props => { + console.log(props) + const key = getNodeKey(props.item.node) + const m = meta.value[key] + + const node = getNode(props.item.node, true) + + if (props.item.level > 0) { + let level = node; + for (let index = 0; index < props.item.level; index++) { + level = h('div', { + class: 'q-tree__node-collapsible' + textColorClass.value, + key: `${ key }__q` + }, [ + h('div', { + class: 'q-tree__children' + + (m.disabled === true ? ' q-tree__node--disabled' : ''), + role: 'group' + }, [level]) + ]); + } + + return level; + } + + return node + } + } const meta = computed(() => { const meta = {} @@ -139,7 +168,7 @@ export default createComponent({ const travel = (node, parent) => { const tickStrategy = node.tickStrategy || (parent ? parent.tickStrategy : props.tickStrategy) const - key = node[ props.nodeKey ], + key = getNodeKey(node), 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, @@ -174,7 +203,7 @@ export default createComponent({ selected: key === props.selected && selectable === true, selectable, - expanded: isParent === true ? innerExpanded.value.includes(key) : false, + expanded: isParent === true ? innerExpanded.value.has(key) : false, expandable, noTick: node.noTick === true || (strictTicking !== true && localLazy && localLazy !== 'loaded'), tickable, @@ -184,8 +213,8 @@ export default createComponent({ leafFilteredTicking, leafTicking, ticked: strictTicking === true - ? innerTicked.value.includes(key) - : (isParent === true ? false : innerTicked.value.includes(key)) + ? innerTicked.value.has(key) + : (isParent === true ? false : innerTicked.value.has(key)) } meta[ key ] = m @@ -242,36 +271,44 @@ export default createComponent({ } props.nodes.forEach(node => travel(node, null)) + return meta }) watch(() => props.ticked, val => { - innerTicked.value = val + innerTicked.value = new Set(val) }) watch(() => props.expanded, val => { - innerExpanded.value = val + innerExpanded.value = new Set(val) }) - function getNodeByKey (key) { - const reduce = [].reduce + watch(() => props.nodes, val => { + innerNodes.value = new Map(getNodesPairs(val)) + }) - 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 ]) + function getNodesPairs (nodes) { + const nodePairs = [] + + const travel = (node) => { + if (Array.isArray(node[ props.childrenKey ])) { + node[ props.childrenKey ].forEach(travel) } + + nodePairs.push([ getNodeKey(node), node ]) } - return find(null, props.nodes) + nodes.forEach(travel) + + return nodePairs + } + + function getNodeKey (node) { + return node[ props.nodeKey ] + } + + function getNodeByKey (key) { + return innerNodes.value.get(key) ?? null } function getTickedNodes () { @@ -293,28 +330,20 @@ export default createComponent({ emit('update:expanded', []) } else { - innerExpanded.value = [] + innerExpanded.value = new Set([]) } } 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) - } - } - } + const expanded = [ ...innerNodes.value.keys() ] - props.nodes.forEach(travel) + const shouldEmit = props.expanded !== void 0 - if (props.expanded !== void 0) { + if (shouldEmit) { emit('update:expanded', expanded) } else { - innerExpanded.value = expanded + innerExpanded.value = new Set(expanded) } } @@ -353,13 +382,8 @@ export default createComponent({ } 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 ]) { @@ -373,30 +397,25 @@ export default createComponent({ } else { props.nodes.forEach(node => { - const k = node[ props.nodeKey ] + const k = getNodeKey(node) if (k !== key) { collapse.push(k) } }) } if (collapse.length !== 0) { - target = target.filter(k => collapse.includes(k) === false) + collapse.forEach(key => innerExpanded.value.delete(key)) } } } - - target = target.concat([ key ]) - .filter((key, index, self) => self.indexOf(key) === index) + innerExpanded.value.add(key) } else { - target = target.filter(k => k !== key) + innerExpanded.value.delete(key) } if (shouldEmit === true) { - emit('update:expanded', target) - } - else { - innerExpanded.value = target + emit('update:expanded', [ ...innerExpanded.value ]) } } @@ -407,23 +426,17 @@ export default createComponent({ } 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) + keys.forEach(key => innerTicked.value.add(key)) } else { - target = target.filter(k => keys.includes(k) === false) + keys.forEach(key => innerTicked.value.delete(key)) } if (shouldEmit === true) { - emit('update:ticked', target) + emit('update:ticked', [ ...innerTicked.value ]) } } @@ -448,11 +461,30 @@ export default createComponent({ } function getChildren (nodes) { + if (props.virtualScroll) { + const virtualChildren = [] + + const travel = (node, level = 0) => { + const key = getNodeKey(node) + const m = meta.value[key] + + virtualChildren.push({node, level}) + + if (m.expanded && Array.isArray(node[ props.childrenKey ])) { + node[ props.childrenKey ].forEach(child => travel(child, level + 1)) + } + } + + nodes.forEach(node => travel(node, 0)) + + return h(QVirtualScroll, {style: attrs.style, separator: true, type: 'list', items: virtualChildren}, virtSlots) + } + return ( props.filter ? nodes.filter(n => meta.value[ n[ props.nodeKey ] ].matchesFilter) : nodes - ).map(child => getNode(child)) + ).map((child) => getNode(child)) } function getNodeMedia (node) { @@ -480,19 +512,21 @@ export default createComponent({ emit('afterHide') } - function getNode (node) { + function getNode (node, omitChildren) { const - key = node[ props.nodeKey ], + key = getNodeKey(node), 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 childrenMeta = node[props.childrenKey] ?? []; + + const children = m.isParent === true + ? getChildren(childrenMeta) : [] - const isParent = children.length !== 0 || (m.lazy && m.lazy !== 'loaded') + const isParent = childrenMeta.length !== 0 || (m.lazy && m.lazy !== 'loaded') let body = node.body ? slots[ `body-${ node.body }` ] || slots[ 'default-body' ] @@ -584,7 +618,7 @@ export default createComponent({ ]) ]), - isParent === true + isParent === true && !omitChildren ? ( props.noTransition === true ? ( @@ -705,7 +739,7 @@ export default createComponent({ }) return () => { - const children = getChildren(props.nodes) + const children = getChildren(props.nodes); return h( 'div', {