This commit is contained in:
darenhsu
2022-07-17 13:16:16 +08:00
parent 84759556ff
commit befd344ab0
28070 changed files with 4008428 additions and 1 deletions
+392
View File
@@ -0,0 +1,392 @@
// 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,
value: {
type: Array,
default: () => []
},
...VTreeviewNodeProps
},
data: () => ({
level: -1,
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() {
const getValue = key => this.returnObject ? getObjectValueByPath(key, this.itemKey) : key;
this.buildTree(this.items);
for (const value of this.value.map(getValue)) {
this.updateSelected(value, true, true);
}
for (const active of this.active.map(getValue)) {
this.updateActive(active, 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(this.returnObject ? getObjectValueByPath(key, this.itemKey) : 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;
} else {
node.isSelected = oldNode.isSelected;
node.isIndeterminate = oldNode.isIndeterminate;
}
node.isActive = oldNode.isActive;
node.isOpen = oldNode.isOpen;
this.nodes[key] = node;
if (children.length) {
const {
isSelected,
isIndeterminate
} = this.calculateState(key, this.nodes);
node.isSelected = isSelected;
node.isIndeterminate = isIndeterminate;
} // Don't forget to rebuild cache
if (this.nodes[key].isSelected && (this.selectionType === 'independent' || node.children.length === 0)) 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 children = state[node].children;
const counts = children.reduce((counts, child) => {
counts[0] += +Boolean(state[child].isSelected);
counts[1] += +Boolean(state[child].isIndeterminate);
return counts;
}, [0, 0]);
const isSelected = !!children.length && counts[0] === children.length;
const isIndeterminate = !isSelected && (counts[0] > 0 || counts[1] > 0);
return {
isSelected,
isIndeterminate
};
},
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, isForced = false) {
if (!this.nodes.hasOwnProperty(key)) return;
const changed = new Map();
if (this.selectionType !== 'independent') {
for (const descendant of this.getDescendants(key)) {
if (!getObjectValueByPath(this.nodes[descendant].item, this.itemDisabled) || isForced) {
this.nodes[descendant].isSelected = isSelected;
this.nodes[descendant].isIndeterminate = false;
changed.set(descendant, isSelected);
}
}
const calculated = this.calculateState(key, this.nodes);
this.nodes[key].isSelected = isSelected;
this.nodes[key].isIndeterminate = calculated.isIndeterminate;
changed.set(key, isSelected);
for (const parent of this.getParents(key)) {
const calculated = this.calculateState(parent, this.nodes);
this.nodes[parent].isSelected = calculated.isSelected;
this.nodes[parent].isIndeterminate = calculated.isIndeterminate;
changed.set(parent, calculated.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(item => {
const genChild = VTreeviewNode.options.methods.genChild.bind(this);
return genChild(item, getObjectValueByPath(item, this.itemDisabled));
})
/* 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
File diff suppressed because one or more lines are too long
+342
View File
@@ -0,0 +1,342 @@
// Components
import { VExpandTransition } from '../transitions';
import { VIcon } from '../VIcon'; // Mixins
import { inject as RegistrableInject } from '../../mixins/registrable';
import Colorable from '../../mixins/colorable'; // Utils
import mixins from '../../util/mixins';
import { getObjectValueByPath, createRange } from '../../util/helpers';
const baseMixins = mixins(Colorable, RegistrableInject('treeview'));
export const VTreeviewNodeProps = {
activatable: Boolean,
activeClass: {
type: String,
default: 'v-treeview-node--active'
},
color: {
type: String,
default: 'primary'
},
expandIcon: {
type: String,
default: '$subgroup'
},
indeterminateIcon: {
type: String,
default: '$checkboxIndeterminate'
},
itemChildren: {
type: String,
default: 'children'
},
itemDisabled: {
type: String,
default: 'disabled'
},
itemKey: {
type: String,
default: 'id'
},
itemText: {
type: String,
default: 'name'
},
loadChildren: Function,
loadingIcon: {
type: String,
default: '$loading'
},
offIcon: {
type: String,
default: '$checkboxOff'
},
onIcon: {
type: String,
default: '$checkboxOn'
},
openOnClick: Boolean,
rounded: Boolean,
selectable: Boolean,
selectedColor: {
type: String,
default: 'accent'
},
shaped: Boolean,
transition: Boolean,
selectionType: {
type: String,
default: 'leaf',
validator: v => ['leaf', 'independent'].includes(v)
}
};
/* @vue/component */
const VTreeviewNode = baseMixins.extend().extend({
name: 'v-treeview-node',
inject: {
treeview: {
default: null
}
},
props: {
level: Number,
item: {
type: Object,
default: () => null
},
parentIsDisabled: Boolean,
...VTreeviewNodeProps
},
data: () => ({
hasLoaded: false,
isActive: false,
isIndeterminate: false,
isLoading: false,
isOpen: false,
isSelected: false
}),
computed: {
disabled() {
return getObjectValueByPath(this.item, this.itemDisabled) || this.parentIsDisabled && this.selectionType === 'leaf';
},
key() {
return getObjectValueByPath(this.item, this.itemKey);
},
children() {
return getObjectValueByPath(this.item, this.itemChildren);
},
text() {
return getObjectValueByPath(this.item, this.itemText);
},
scopedProps() {
return {
item: this.item,
leaf: !this.children,
selected: this.isSelected,
indeterminate: this.isIndeterminate,
active: this.isActive,
open: this.isOpen
};
},
computedIcon() {
if (this.isIndeterminate) return this.indeterminateIcon;else if (this.isSelected) return this.onIcon;else return this.offIcon;
},
hasChildren() {
return !!this.children && (!!this.children.length || !!this.loadChildren);
}
},
created() {
this.treeview.register(this);
},
beforeDestroy() {
this.treeview.unregister(this);
},
methods: {
checkChildren() {
return new Promise(resolve => {
// TODO: Potential issue with always trying
// to load children if response is empty?
if (!this.children || this.children.length || !this.loadChildren || this.hasLoaded) return resolve();
this.isLoading = true;
resolve(this.loadChildren(this.item));
}).then(() => {
this.isLoading = false;
this.hasLoaded = true;
});
},
open() {
this.isOpen = !this.isOpen;
this.treeview.updateOpen(this.key, this.isOpen);
this.treeview.emitOpen();
},
genLabel() {
const children = [];
if (this.$scopedSlots.label) children.push(this.$scopedSlots.label(this.scopedProps));else children.push(this.text);
return this.$createElement('div', {
slot: 'label',
staticClass: 'v-treeview-node__label'
}, children);
},
genPrependSlot() {
if (!this.$scopedSlots.prepend) return null;
return this.$createElement('div', {
staticClass: 'v-treeview-node__prepend'
}, this.$scopedSlots.prepend(this.scopedProps));
},
genAppendSlot() {
if (!this.$scopedSlots.append) return null;
return this.$createElement('div', {
staticClass: 'v-treeview-node__append'
}, this.$scopedSlots.append(this.scopedProps));
},
genContent() {
const children = [this.genPrependSlot(), this.genLabel(), this.genAppendSlot()];
return this.$createElement('div', {
staticClass: 'v-treeview-node__content'
}, children);
},
genToggle() {
return this.$createElement(VIcon, {
staticClass: 'v-treeview-node__toggle',
class: {
'v-treeview-node__toggle--open': this.isOpen,
'v-treeview-node__toggle--loading': this.isLoading
},
slot: 'prepend',
on: {
click: e => {
e.stopPropagation();
if (this.isLoading) return;
this.checkChildren().then(() => this.open());
}
}
}, [this.isLoading ? this.loadingIcon : this.expandIcon]);
},
genCheckbox() {
return this.$createElement(VIcon, {
staticClass: 'v-treeview-node__checkbox',
props: {
color: this.isSelected || this.isIndeterminate ? this.selectedColor : undefined,
disabled: this.disabled
},
on: {
click: e => {
e.stopPropagation();
if (this.isLoading) return;
this.checkChildren().then(() => {
// We nextTick here so that items watch in VTreeview has a chance to run first
this.$nextTick(() => {
this.isSelected = !this.isSelected;
this.isIndeterminate = false;
this.treeview.updateSelected(this.key, this.isSelected);
this.treeview.emitSelected();
});
});
}
}
}, [this.computedIcon]);
},
genLevel(level) {
return createRange(level).map(() => this.$createElement('div', {
staticClass: 'v-treeview-node__level'
}));
},
genNode() {
const children = [this.genContent()];
if (this.selectable) children.unshift(this.genCheckbox());
if (this.hasChildren) {
children.unshift(this.genToggle());
} else {
children.unshift(...this.genLevel(1));
}
children.unshift(...this.genLevel(this.level));
return this.$createElement('div', this.setTextColor(this.isActive && this.color, {
staticClass: 'v-treeview-node__root',
class: {
[this.activeClass]: this.isActive
},
on: {
click: () => {
if (this.openOnClick && this.hasChildren) {
this.checkChildren().then(this.open);
} else if (this.activatable && !this.disabled) {
this.isActive = !this.isActive;
this.treeview.updateActive(this.key, this.isActive);
this.treeview.emitActive();
}
}
}
}), children);
},
genChild(item, parentIsDisabled) {
return this.$createElement(VTreeviewNode, {
key: getObjectValueByPath(item, this.itemKey),
props: {
activatable: this.activatable,
activeClass: this.activeClass,
item,
selectable: this.selectable,
selectedColor: this.selectedColor,
color: this.color,
expandIcon: this.expandIcon,
indeterminateIcon: this.indeterminateIcon,
offIcon: this.offIcon,
onIcon: this.onIcon,
loadingIcon: this.loadingIcon,
itemKey: this.itemKey,
itemText: this.itemText,
itemDisabled: this.itemDisabled,
itemChildren: this.itemChildren,
loadChildren: this.loadChildren,
transition: this.transition,
openOnClick: this.openOnClick,
rounded: this.rounded,
shaped: this.shaped,
level: this.level + 1,
selectionType: this.selectionType,
parentIsDisabled
},
scopedSlots: this.$scopedSlots
});
},
genChildrenWrapper() {
if (!this.isOpen || !this.children) return null;
const children = [this.children.map(c => this.genChild(c, this.disabled))];
return this.$createElement('div', {
staticClass: 'v-treeview-node__children'
}, children);
},
genTransition() {
return this.$createElement(VExpandTransition, [this.genChildrenWrapper()]);
}
},
render(h) {
const children = [this.genNode()];
if (this.transition) children.push(this.genTransition());else children.push(this.genChildrenWrapper());
return h('div', {
staticClass: 'v-treeview-node',
class: {
'v-treeview-node--leaf': !this.hasChildren,
'v-treeview-node--click': this.openOnClick,
'v-treeview-node--disabled': this.disabled,
'v-treeview-node--rounded': this.rounded,
'v-treeview-node--shaped': this.shaped,
'v-treeview-node--selected': this.isSelected,
'v-treeview-node--excluded': this.treeview.isExcluded(this.key)
},
attrs: {
'aria-expanded': String(this.isOpen)
}
}, children);
}
});
export default VTreeviewNode;
//# sourceMappingURL=VTreeviewNode.js.map
File diff suppressed because one or more lines are too long
+10
View File
@@ -0,0 +1,10 @@
import VTreeview from './VTreeview';
import VTreeviewNode from './VTreeviewNode';
export { VTreeview, VTreeviewNode };
export default {
$_vuetify_subcomponents: {
VTreeview,
VTreeviewNode
}
};
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["../../../src/components/VTreeview/index.ts"],"names":[],"mappings":"AAAA,OAAO,SAAP,MAAsB,aAAtB;AACA,OAAO,aAAP,MAA0B,iBAA1B;AAEA,SAAS,SAAT,EAAoB,aAApB;AAEA,eAAe;AACb,EAAA,uBAAuB,EAAE;AACvB,IAAA,SADuB;AAEvB,IAAA;AAFuB;AADZ,CAAf","sourcesContent":["import VTreeview from './VTreeview'\nimport VTreeviewNode from './VTreeviewNode'\n\nexport { VTreeview, VTreeviewNode }\n\nexport default {\n $_vuetify_subcomponents: {\n VTreeview,\n VTreeviewNode,\n },\n}\n"],"sourceRoot":"","file":"index.js"}
+28
View File
@@ -0,0 +1,28 @@
import { getObjectValueByPath } from '../../../util/helpers';
export function filterTreeItem(item, search, textKey) {
const text = getObjectValueByPath(item, textKey);
return text.toLocaleLowerCase().indexOf(search.toLocaleLowerCase()) > -1;
}
export function filterTreeItems(filter, item, search, idKey, textKey, childrenKey, excluded) {
if (filter(item, search, textKey)) {
return true;
}
const children = getObjectValueByPath(item, childrenKey);
if (children) {
let match = false;
for (let i = 0; i < children.length; i++) {
if (filterTreeItems(filter, children[i], search, idKey, textKey, childrenKey, excluded)) {
match = true;
}
}
if (match) return true;
}
excluded.add(getObjectValueByPath(item, idKey));
return false;
}
//# sourceMappingURL=filterTreeItems.js.map
@@ -0,0 +1 @@
{"version":3,"sources":["../../../../src/components/VTreeview/util/filterTreeItems.ts"],"names":[],"mappings":"AAAA,SAAS,oBAAT,QAAqC,uBAArC;AAGA,OAAM,SAAU,cAAV,CAA0B,IAA1B,EAAwC,MAAxC,EAAwD,OAAxD,EAAuE;AAC3E,QAAM,IAAI,GAAG,oBAAoB,CAAC,IAAD,EAAO,OAAP,CAAjC;AAEA,SAAO,IAAI,CAAC,iBAAL,GAAyB,OAAzB,CAAiC,MAAM,CAAC,iBAAP,EAAjC,IAA+D,CAAC,CAAvE;AACD;AAED,OAAM,SAAU,eAAV,CACJ,MADI,EAEJ,IAFI,EAGJ,MAHI,EAIJ,KAJI,EAKJ,OALI,EAMJ,WANI,EAOJ,QAPI,EAO0B;AAE9B,MAAI,MAAM,CAAC,IAAD,EAAO,MAAP,EAAe,OAAf,CAAV,EAAmC;AACjC,WAAO,IAAP;AACD;;AAED,QAAM,QAAQ,GAAG,oBAAoB,CAAC,IAAD,EAAO,WAAP,CAArC;;AAEA,MAAI,QAAJ,EAAc;AACZ,QAAI,KAAK,GAAG,KAAZ;;AACA,SAAK,IAAI,CAAC,GAAG,CAAb,EAAgB,CAAC,GAAG,QAAQ,CAAC,MAA7B,EAAqC,CAAC,EAAtC,EAA0C;AACxC,UAAI,eAAe,CAAC,MAAD,EAAS,QAAQ,CAAC,CAAD,CAAjB,EAAsB,MAAtB,EAA8B,KAA9B,EAAqC,OAArC,EAA8C,WAA9C,EAA2D,QAA3D,CAAnB,EAAyF;AACvF,QAAA,KAAK,GAAG,IAAR;AACD;AACF;;AAED,QAAI,KAAJ,EAAW,OAAO,IAAP;AACZ;;AAED,EAAA,QAAQ,CAAC,GAAT,CAAa,oBAAoB,CAAC,IAAD,EAAO,KAAP,CAAjC;AAEA,SAAO,KAAP;AACD","sourcesContent":["import { getObjectValueByPath } from '../../../util/helpers'\nimport { TreeviewItemFunction } from 'vuetify/types'\n\nexport function filterTreeItem (item: object, search: string, textKey: string): boolean {\n const text = getObjectValueByPath(item, textKey)\n\n return text.toLocaleLowerCase().indexOf(search.toLocaleLowerCase()) > -1\n}\n\nexport function filterTreeItems (\n filter: TreeviewItemFunction,\n item: any,\n search: string,\n idKey: string,\n textKey: string,\n childrenKey: string,\n excluded: Set<string | number>\n): boolean {\n if (filter(item, search, textKey)) {\n return true\n }\n\n const children = getObjectValueByPath(item, childrenKey)\n\n if (children) {\n let match = false\n for (let i = 0; i < children.length; i++) {\n if (filterTreeItems(filter, children[i], search, idKey, textKey, childrenKey, excluded)) {\n match = true\n }\n }\n\n if (match) return true\n }\n\n excluded.add(getObjectValueByPath(item, idKey))\n\n return false\n}\n"],"sourceRoot":"","file":"filterTreeItems.js"}