364 lines
No EOL
11 KiB
JavaScript
364 lines
No EOL
11 KiB
JavaScript
// Styles
|
|
import "../../../src/components/VTreeview/VTreeview.sass"; // Components
|
|
|
|
import VTreeviewNode, { VTreeviewNodeProps } from './VTreeviewNode'; // Mixins
|
|
|
|
import Themeable from '../../mixins/themeable';
|
|
import { provide as RegistrableProvide } from '../../mixins/registrable'; // Utils
|
|
|
|
import { arrayDiff, deepEqual, getObjectValueByPath } from '../../util/helpers';
|
|
import mixins from '../../util/mixins';
|
|
import { consoleWarn } from '../../util/console';
|
|
import { filterTreeItems, filterTreeItem } from './util/filterTreeItems';
|
|
export default mixins(RegistrableProvide('treeview'), Themeable
|
|
/* @vue/component */
|
|
).extend({
|
|
name: 'v-treeview',
|
|
|
|
provide() {
|
|
return {
|
|
treeview: this
|
|
};
|
|
},
|
|
|
|
props: {
|
|
active: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
dense: Boolean,
|
|
filter: Function,
|
|
hoverable: Boolean,
|
|
items: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
multipleActive: Boolean,
|
|
open: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
openAll: Boolean,
|
|
returnObject: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
search: String,
|
|
selectionType: {
|
|
type: String,
|
|
default: 'leaf',
|
|
validator: v => ['leaf', 'independent'].includes(v)
|
|
},
|
|
value: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
...VTreeviewNodeProps
|
|
},
|
|
data: () => ({
|
|
activeCache: new Set(),
|
|
nodes: {},
|
|
openCache: new Set(),
|
|
selectedCache: new Set()
|
|
}),
|
|
computed: {
|
|
excludedItems() {
|
|
const excluded = new Set();
|
|
if (!this.search) return excluded;
|
|
|
|
for (let i = 0; i < this.items.length; i++) {
|
|
filterTreeItems(this.filter || filterTreeItem, this.items[i], this.search, this.itemKey, this.itemText, this.itemChildren, excluded);
|
|
}
|
|
|
|
return excluded;
|
|
}
|
|
|
|
},
|
|
watch: {
|
|
items: {
|
|
handler() {
|
|
const oldKeys = Object.keys(this.nodes).map(k => getObjectValueByPath(this.nodes[k].item, this.itemKey));
|
|
const newKeys = this.getKeys(this.items);
|
|
const diff = arrayDiff(newKeys, oldKeys); // We only want to do stuff if items have changed
|
|
|
|
if (!diff.length && newKeys.length < oldKeys.length) return; // If nodes are removed we need to clear them from this.nodes
|
|
|
|
diff.forEach(k => delete this.nodes[k]);
|
|
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) {
|
|
this.handleNodeCacheWatcher(value, this.activeCache, this.updateActive, this.emitActive);
|
|
},
|
|
|
|
value(value) {
|
|
this.handleNodeCacheWatcher(value, this.selectedCache, this.updateSelected, this.emitSelected);
|
|
},
|
|
|
|
open(value) {
|
|
this.handleNodeCacheWatcher(value, this.openCache, this.updateOpen, this.emitOpen);
|
|
}
|
|
|
|
},
|
|
|
|
created() {
|
|
this.buildTree(this.items);
|
|
this.value.forEach(key => this.updateSelected(key, true));
|
|
this.active.forEach(key => this.updateActive(key, true));
|
|
},
|
|
|
|
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) {
|
|
this.updateAll(true);
|
|
} else {
|
|
this.open.forEach(key => this.updateOpen(key, true));
|
|
this.emitOpen();
|
|
}
|
|
},
|
|
|
|
methods: {
|
|
/** @public */
|
|
updateAll(value) {
|
|
Object.keys(this.nodes).forEach(key => this.updateOpen(getObjectValueByPath(this.nodes[key].item, this.itemKey), value));
|
|
this.emitOpen();
|
|
},
|
|
|
|
getKeys(items, keys = []) {
|
|
for (let i = 0; i < items.length; i++) {
|
|
const key = getObjectValueByPath(items[i], this.itemKey);
|
|
keys.push(key);
|
|
const children = getObjectValueByPath(items[i], this.itemChildren);
|
|
|
|
if (children) {
|
|
keys.push(...this.getKeys(children));
|
|
}
|
|
}
|
|
|
|
return keys;
|
|
},
|
|
|
|
buildTree(items, parent = 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
|
|
};
|
|
const node = {
|
|
vnode: oldNode.vnode,
|
|
parent,
|
|
children: children.map(c => getObjectValueByPath(c, this.itemKey)),
|
|
item
|
|
};
|
|
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);
|
|
}
|
|
},
|
|
|
|
calculateState(node, state) {
|
|
const counts = node.children.reduce((counts, child) => {
|
|
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.emitNodeCache('update:open', this.openCache);
|
|
},
|
|
|
|
emitSelected() {
|
|
this.emitNodeCache('input', this.selectedCache);
|
|
},
|
|
|
|
emitActive() {
|
|
this.emitNodeCache('update:active', this.activeCache);
|
|
},
|
|
|
|
emitNodeCache(event, cache) {
|
|
this.$emit(event, this.returnObject ? [...cache].map(key => this.nodes[key].item) : [...cache]);
|
|
},
|
|
|
|
handleNodeCacheWatcher(value, cache, updateFn, emitFn) {
|
|
value = this.returnObject ? value.map(v => getObjectValueByPath(v, this.itemKey)) : value;
|
|
const old = [...cache];
|
|
if (deepEqual(old, value)) return;
|
|
old.forEach(key => updateFn(key, false));
|
|
value.forEach(key => updateFn(key, true));
|
|
emitFn();
|
|
},
|
|
|
|
getDescendants(key, descendants = []) {
|
|
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) {
|
|
let parent = this.nodes[key].parent;
|
|
const parents = [];
|
|
|
|
while (parent !== null) {
|
|
parents.push(parent);
|
|
parent = this.nodes[parent].parent;
|
|
}
|
|
|
|
return parents;
|
|
},
|
|
|
|
register(node) {
|
|
const key = getObjectValueByPath(node.item, this.itemKey);
|
|
this.nodes[key].vnode = node;
|
|
this.updateVnodeState(key);
|
|
},
|
|
|
|
unregister(node) {
|
|
const key = getObjectValueByPath(node.item, this.itemKey);
|
|
if (this.nodes[key]) this.nodes[key].vnode = null;
|
|
},
|
|
|
|
isParent(key) {
|
|
return this.nodes[key].children && this.nodes[key].children.length;
|
|
},
|
|
|
|
updateActive(key, isActive) {
|
|
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, isSelected) {
|
|
if (!this.nodes.hasOwnProperty(key)) return;
|
|
const changed = new Map();
|
|
|
|
if (this.selectionType !== 'independent') {
|
|
const descendants = [key, ...this.getDescendants(key)];
|
|
descendants.forEach(descendant => {
|
|
this.nodes[descendant].isSelected = isSelected;
|
|
this.nodes[descendant].isIndeterminate = false;
|
|
changed.set(descendant, isSelected);
|
|
});
|
|
const parents = this.getParents(key);
|
|
parents.forEach(parent => {
|
|
this.nodes[parent] = this.calculateState(this.nodes[parent], this.nodes);
|
|
changed.set(parent, this.nodes[parent].isSelected);
|
|
});
|
|
} else {
|
|
this.nodes[key].isSelected = isSelected;
|
|
this.nodes[key].isIndeterminate = false;
|
|
changed.set(key, isSelected);
|
|
}
|
|
|
|
for (const [key, value] of changed.entries()) {
|
|
this.updateVnodeState(key);
|
|
if (this.selectionType === 'leaf' && this.isParent(key)) continue;
|
|
value === true ? this.selectedCache.add(key) : this.selectedCache.delete(key);
|
|
}
|
|
},
|
|
|
|
updateOpen(key, isOpen) {
|
|
if (!this.nodes.hasOwnProperty(key)) return;
|
|
const node = this.nodes[key];
|
|
const children = getObjectValueByPath(node.item, this.itemChildren);
|
|
|
|
if (children && !children.length && node.vnode && !node.vnode.hasLoaded) {
|
|
node.vnode.checkChildren().then(() => this.updateOpen(key, isOpen));
|
|
} else if (children && children.length) {
|
|
node.isOpen = isOpen;
|
|
node.isOpen ? this.openCache.add(key) : this.openCache.delete(key);
|
|
this.updateVnodeState(key);
|
|
}
|
|
},
|
|
|
|
updateVnodeState(key) {
|
|
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;
|
|
}
|
|
},
|
|
|
|
isExcluded(key) {
|
|
return !!this.search && this.excludedItems.has(key);
|
|
}
|
|
|
|
},
|
|
|
|
render(h) {
|
|
const children = this.items.length ? this.items.map(VTreeviewNode.options.methods.genChild.bind(this))
|
|
/* istanbul ignore next */
|
|
: this.$slots.default; // TODO: remove type annotation with TS 3.2
|
|
|
|
return h('div', {
|
|
staticClass: 'v-treeview',
|
|
class: {
|
|
'v-treeview--hoverable': this.hoverable,
|
|
'v-treeview--dense': this.dense,
|
|
...this.themeClasses
|
|
}
|
|
}, children);
|
|
}
|
|
|
|
});
|
|
//# sourceMappingURL=VTreeview.js.map
|