UNPKG

element3

Version:

A Component Library for Vue3

500 lines (446 loc) 10.9 kB
/* eslint-disable @typescript-eslint/no-this-alias */ const typeFlag = Symbol('TREE_NODE') /** * * @param {*} id * @param {string} label * @param {TreeNode[]} childNodes * @param {object} param3 */ export class TreeNode { constructor( id, label, childNodes = [], { /* 默认值 */ parent = null, isAsync = false, isVisable = true, isChecked = false, isIndeterminate = false, isExpanded = false, isDisabled = false, isDraggable = false, isLeaf = false, data = {}, asyncLoadFn = () => null } = {}, { /* 拦截函数 */ insertChild = null, appendChild = null, removeChild = null } = {} ) { this.id = id || label this.label = label this.parent = parent this.childNodes = childNodes this.isVisable = isVisable this.isChecked = isChecked this.isIndeterminate = isIndeterminate this.isExpanded = isExpanded this.isDisabled = isDisabled this.isDraggable = isDraggable this.isRendered = false this.data = data // Additional data carried by the node this.isLeaf = isLeaf this.isAsync = isAsync // Load child only at expand time this.asyncState = 'notload' // notload || loaded || loading this.asyncLoadFn = asyncLoadFn // (currentNode, resolveFn) async load child node this.interceptHandler = { insertChild, appendChild, removeChild } // this.cache = { // level: null // } // not use, You can cache read-only properties here this.updateChildParent() this.updateChildChecked() this.updateCheckedState() this.updateExpandedState() } get root() { // readonly let root = this this.upwardEach((node) => { root = node }) return root } get isLeaf() { return this.isAsync ? this.asyncState === 'loaded' && this.childNodes.length === 0 : this.childNodes.length === 0 } set isLeaf(v) { if (v) this.asyncState = 'loaded' } get isRoot() { // readonly return this.root === this } get level() { // readonly if (!this.parent) return 0 return this.parent.level + 1 } get type() { // readonly return typeFlag } get index() { // readonly const parent = this.parent if (!parent) return -1 return parent.findChildIndex(this) } get checkedNodes() { return this.childNodes.filter( (treeNode) => treeNode.isChecked && !treeNode.isIndeterminate ) } loadAsync() { if (!this.isAsync || this.asyncState !== 'notload') { return } const resolveFn = (childNodes = []) => { // this.childNodes = [] this.append(...childNodes) this.asyncState = 'loaded' } this.asyncState = 'loading' this.asyncLoadFn(this, resolveFn) } updateChildParent() { this.childNodes.forEach((node) => { node.parent = this }) } updateChildChecked() { if (this.isChecked) { this.setChildChecked(true) } } updateCheckedState() { if (this.isLeaf) { return } // not leaf node exec const checkedNodeLen = this.getCheckedNode().length const childrenNodeLen = this.childNodes.length if (childrenNodeLen === 0) { return } if (checkedNodeLen === childrenNodeLen) { // full select this.setCheckedState(1) } else if (checkedNodeLen === 0) { // not full select this.setCheckedState(0) } else { // Half select this.setCheckedState(2) } } updateExpandedState() { const childNodes = this.childNodes for (let i = 0; i < childNodes.length; i++) { const node = childNodes[i] if (node.isExpanded) { this.isExpanded = true return } } } /** * * @param {TreeNode} node */ appendChild(node) { if (this.interceptHandler.appendChild) { const [_node] = this.interceptHandler.appendChild.apply(this, arguments) if (typeof _node !== 'undefined') node = _node } if (!TreeNode.isType(node)) { return false } node.parent = this if (this.isChecked) { node.setChecked(true) } this.childNodes.push(node) return true } append(...nodes) { nodes.forEach((node) => { this.appendChild(node) }) return true } /** * * @param {number} index * @param {TreeNode} node */ insertChild(index, node) { if (this.interceptHandler.appendChild) { const [_index, _node] = this.interceptHandler.insertChild.apply( this, arguments ) if (typeof _index !== 'undefined') index = _index if (typeof _node !== 'undefined') node = _node } if (!TreeNode.isType(node)) { return false } if (this.isChecked) { node.setChecked(true) } node.parent = this this.childNodes.splice(index, 0, node) return true } insert() { this.insertChild.apply(this, arguments) } /** * * @param {number} index */ removeChild(index) { if (this.interceptHandler.appendChild) { this.interceptHandler.removeChild.apply(this, arguments) } if (index < 0 || index >= this.childNodes.length) { return false } this.childNodes.splice(index, 1) return true } remove() { if (!this.parent) { return false } return this.parent.removeChild(this.index) } /** * * @param {boolean} value */ setChecked(value, strictly = false) { this.isIndeterminate = false let _value = !this.isChecked if (typeof value !== 'undefined') { _value = value } this.isChecked = _value if (!strictly) { this.upwardEach((node) => { if (!node.isDisabled) this.updateCheckedState.call(node) }) this.depthEach((node) => { if (!node.isDisabled && node.isVisable) node.isChecked = _value }) } return this.isChecked } setChildChecked(value) { this.childNodes.forEach((node) => (node.isChecked = value)) } /** * Traverse upward * @param {Function} callback( node:TreeNode ) if returns true then stop each, else not stop */ upwardEach(callback, { isSkipSelf = true } = {}) { let current = isSkipSelf ? this.parent : this while (current) { if (callback(current)) { return } current = current.parent } } /** * from current node start, down each * @param {Function} callback( node:TreeNode, parentNode:TreeNode, deep: number) if returns true then stop each, else not stop */ depthEach(upToDownCallBack = () => false, downToUpCallBack = () => false) { const dfs = (node, deep) => { if (!TreeNode.isType(node)) { return } for (let i = 0; i < node.childNodes.length; i++) { const _node = node.childNodes[i] if (upToDownCallBack(_node, node, deep)) return dfs(_node, deep + 1) if (downToUpCallBack(_node, node, deep)) return } } upToDownCallBack(this, this.parent, 0) dfs(this, 1) downToUpCallBack(this, this.parent, 0) } getCheckedNode() { return this.childNodes.filter((item) => item.isChecked) } /** * * @param {number} state 0:not checked, 1:checked, 2:indeterminate */ setCheckedState(state) { const eunmState = [ { isChecked: false, isIndeterminate: false }, { isChecked: true, isIndeterminate: false }, { isChecked: true, isIndeterminate: true } ] if (!eunmState[state]) { return false } this.isChecked = eunmState[state].isChecked this.isIndeterminate = eunmState[state].isIndeterminate return true } /** * Look for node in the subtree * @param {ID|TreeNode} target */ findOne(target) { let res = null this.depthEach((node) => { if (node.id == target || node === target) { res = node return true } }) return res } /** * find nodes * @param {Function | string} target */ findMany(target) { const res = [] this.depthEach((node, parent, deep) => { if ( (typeof target === 'function' && target(node, parent, deep)) || (typeof target === 'string' && node.label.search(target) !== -1) ) { res.push(node) } }) return res } findChildIndex(target) { for (let i = 0; i < this.childNodes.length; i++) { const node = this.childNodes[i] if (node.id == target || node === target) { return i } } return -1 } expand(value, ...extraNodes) { let _value = this.isExpanded _value = typeof value === 'undefined' ? !_value : value this.isExpanded = _value this.upwardEach((node) => { node.expand(true) }) this.loadAsync() this.append(...extraNodes) this.isRendered = true } filter(callback = () => true) { const arr = [] this.setSubTreeVisable(false) this.depthEach((node, parentNode, deep) => { const isShow = callback(node, parentNode, deep) if (isShow) { node.setVsiable(true) arr.push(node) } }) return arr } setSubTreeVisable(value) { this.depthEach((node) => { node.isVisable = value }) } setVsiable(value) { this.isVisable = value this.upwardEach((node) => { node.isVisable = true }) } separation() { const parent = this.parent if (!parent) return null parent.removeChild(this.index) this.parent = null return this } /** * is allow move to target * @param {TreeNode} target * @param {string} relative top, bottom, inner */ isAllowMove(target) { if (target === this) { return false } if (this.findOne(target)) { return false } return true } /** * currentNode move to targetNode relative location * @param {TreeNode} target * @param {string} relative top, bottom, inner */ move(target, relative) { if (!this.isAllowMove(target, relative)) { return false } this.separation() switch (relative) { case 'top': // top target.parent.insertChild(target.index, this) return true case 'inner': // inner target.expand(true, this) return true case 'bottom': // bottom target.parent.insertChild(target.index + 1, this) return true } } collapse() { const parent = this.parent if (!parent) { return } parent.childNodes.forEach((node) => { if (node === this) { node.expand() } else { node.expand(false) } }) } static isType(node) { if (typeof node !== 'object') { return false } return node.type === typeFlag } static create({ id, label, childNodes, interceptHandler, ...otherParams }) { return new TreeNode(id, label, childNodes, otherParams, interceptHandler) } }