vue-tree-polyfill
Version:
A vue tree component using virtual list.
1,333 lines (1,164 loc) • 40.6 kB
text/typescript
import TreeNode, { ITreeNodeOptions } from './tree-node'
import { ignoreEnum } from '../constants'
import { TreeNodeKeyType, IgnoreType } from '../types'
import TreeEventTarget from './tree-event-target'
//#region Interfaces
interface ITreeStoreOptions {
[key: string]: any
keyField: string
ignoreMode?: IgnoreType
filteredNodeCheckable?: boolean
cascade?: boolean
defaultExpandAll?: boolean
load?: Function
expandOnFilter?: boolean
}
interface IMapData {
[key: string]: TreeNode
[key: number]: TreeNode
}
//#endregion Interfaces
export type FilterFunctionType = (keyword: string, node: TreeNode) => boolean
export default class TreeStore extends TreeEventTarget {
//#region Properties
/** 树数据 */
data: TreeNode[] = []
/** 扁平化的树数据 */
flatData: TreeNode[] = []
/** 树数据 map */
mapData: IMapData = Object.create(null)
/** 未加载但已选中的节点 key ,每次 flattenData 的时候进行检查,将加载的节点从此数组中剔除 */
private unloadCheckedKeys: TreeNodeKeyType[] = []
/** 未加载但选中的单选节点 key */
private unloadSelectedKey: TreeNodeKeyType | null = null
/** 当前单选选中节点 key */
private currentSelectedKey: TreeNodeKeyType | null = null
//#endregion Properties
constructor(private readonly options: ITreeStoreOptions) {
super()
}
/**
* Use this function to insert nodes into flatData to avoid 'maximun call stack size exceeded' error
* @param insertIndex The index to insert, the same usage as `this.flatData.splice(insertIndex, 0, insertNodes)`
* @param insertNodes Tree nodes to insert
*/
private insertIntoFlatData(insertIndex: number, insertNodes: TreeNode[]) {
this.flatData = this.flatData.slice(0, insertIndex).concat(insertNodes, this.flatData.slice(insertIndex))
}
setData(
data: ITreeNodeOptions[],
selectableUnloadKey: TreeNodeKeyType | null = null,
checkableUnloadKeys: TreeNodeKeyType[] | null = null
): void {
this.data = data.map(
(nodeData: ITreeNodeOptions): TreeNode =>
new TreeNode(nodeData, null, this.options.keyField, !!this.options.load)
)
// 清空 mapData
for (let key in this.mapData) delete this.mapData[key]
// 扁平化之前清空单选选中,如果 value 有值,则是 selectableUnloadKey 有值,会重新设置 currentSelectedKey ;多选选中没有存储在 store 中,因此不必事先清空。
this.currentSelectedKey = null
// 扁平化节点数据
this.flatData = this.flattenData(this.data)
// 更新未载入多选选中节点
this.setUnloadCheckedKeys(checkableUnloadKeys || [])
if (selectableUnloadKey) {
// 更新未载入单选选中节点
this.currentSelectedKey = null
this.setUnloadSelectedKey(selectableUnloadKey)
}
this.emit('visible-data-change')
this.emit('set-data')
}
//#region Set api
/**
* 设置单个节点多选选中
* @param key 设置的节点 key
* @param value 是否选中
* @param triggerEvent 是否触发事件
* @param triggerDataChange 是否触发 `data-change` 事件以通知外部刷新视图
* @param filtering 是否正在过滤,如果是,则考虑 `filteredNodeCheckable` Prop,用于判断是否是用户点击节点触发的勾选
*/
setChecked(
key: TreeNodeKeyType,
value: boolean,
triggerEvent: boolean = true,
triggerDataChange: boolean = true,
filtering: boolean = false
): void {
const node = this.mapData[key]
if (!node)
return this.setUnloadChecked(key, value, triggerEvent, triggerDataChange)
if (node.disabled) return
// 首先确定没有变化的情况
if (node.checked && value) return // 已经勾选的再次勾选,直接返回
if (!node.checked && !node.indeterminate && !value) return // 未勾选且不是半选状态再次设置未勾选,直接返回
if (this.options.cascade) {
// 向下勾选,包括自身
this.checkNodeDownward(node, value, filtering)
// 向上勾选父节点直到根节点
this.checkNodeUpward(node)
} else {
node.checked = value
}
if (triggerEvent) {
if (node.checked) {
this.emit('check', node)
} else {
this.emit('uncheck', node)
}
}
this.triggerCheckedChange(triggerEvent, triggerDataChange)
}
/**
* 设置单个未加载节点选中,不公开此 API
*/
private setUnloadChecked(
key: TreeNodeKeyType,
value: boolean,
triggerEvent: boolean = true,
triggerDataChange: boolean = true
): void {
const index = this.findIndex(key, this.unloadCheckedKeys)
if (value) {
if (index === -1) {
this.unloadCheckedKeys.push(key)
}
} else {
if (index !== -1) {
this.unloadCheckedKeys.splice(index, 1)
}
}
this.triggerCheckedChange(triggerEvent, triggerDataChange)
}
/**
* 批量选中/取消多选选中节点
* @param keys 选中的节点 key 数组
* @param value 是否选中
* @param triggerEvent 是否触发事件
*/
setCheckedKeys(
keys: TreeNodeKeyType[],
value: boolean,
triggerEvent: boolean = true,
triggerDataChange: boolean = true
): void {
keys.forEach(key => {
this.setChecked(key, value, false, false)
})
this.triggerCheckedChange(triggerEvent, triggerDataChange)
}
/**
* 多选勾选全部节点
* @param triggerEvent 是否触发事件
* @param triggerDataChange 是否触发视图刷新
*/
checkAll(
triggerEvent: boolean = true,
triggerDataChange: boolean = true
): void {
if (this.options.cascade) {
// 级联时,只要勾选第一层节点即可,如果第一层被禁用了,则往下一层勾选
const checkCascade = (nodes: TreeNode[]): void => {
nodes.forEach(node => {
if (node.disabled) {
checkCascade(node.children)
} else {
this.setChecked(node[this.options.keyField], true, false, false)
}
})
}
checkCascade(this.data)
} else {
const length = this.flatData.length
for (let i = 0; i < length; i++) {
const node = this.flatData[i]
this.setChecked(node[this.options.keyField], true, false, false)
}
}
this.triggerCheckedChange(triggerEvent, triggerDataChange)
}
/**
* 清除所有多选已选
* @param triggerEvent 是否触发事件
* @param triggerDataChange 是否触发视图刷新
*/
clearChecked(
triggerEvent: boolean = true,
triggerDataChange: boolean = true
): void {
const currentCheckedNodes = this.getCheckedNodes()
currentCheckedNodes.forEach(checkedNode => {
this.setChecked(checkedNode[this.options.keyField], false, false, false)
})
// 清空未加载多选选中节点
this.unloadCheckedKeys = []
this.triggerCheckedChange(triggerEvent, triggerDataChange)
}
/**
* 触发 checked-change 的快捷方法
* @param triggerEvent 是否触发事件
* @param triggerDataChange 是否触发视图刷新
*/
private triggerCheckedChange(
triggerEvent = true,
triggerDataChange = true,
) {
if (triggerEvent) {
this.emit('checked-change', this.getCheckedNodes(), this.getCheckedKeys())
}
if (triggerDataChange) {
this.emit('render-data-change')
}
}
/**
* 触发 selected-change 的快捷方法
* @param triggerEvent 是否触发事件
* @param triggerDataChange 是否触发视图刷新
*/
private triggerSelectedChange(
triggerEvent = true,
triggerDataChange = true,
) {
if (triggerEvent) {
this.emit('selected-change', this.getSelectedNode(), this.getSelectedKey())
}
if (triggerDataChange) {
this.emit('render-data-change')
}
}
/**
* 设置单选选中
* @param key 选中节点 key
* @param value 是否选中
* @param triggerEvent 是否触发事件
*/
setSelected(
key: TreeNodeKeyType,
value: boolean,
triggerEvent: boolean = true,
triggerDataChange: boolean = true
): void {
const node = this.mapData[key]
if (!node)
return this.setUnloadSelected(key, value, triggerEvent, triggerDataChange)
if (node.disabled) return
if (node.selected === value) return // 当前节点已经是将要设置的状态,直接返回
if (key === this.currentSelectedKey) {
// 设置的节点即当前已选中节点
if (!value) {
// 取消当前选中节点
node.selected = value
this.currentSelectedKey = null
}
} else {
// 设置的节点不是当前已选中节点,要么当前没有选中节点,要么当前有选中节点
if (value) {
if (this.currentSelectedKey !== null) {
// 取消当前已选中,设置新的选中节点
if (this.mapData[this.currentSelectedKey]) {
this.mapData[this.currentSelectedKey].selected = false
}
}
node.selected = value
this.currentSelectedKey = node[this.options.keyField]
this.unloadSelectedKey = null
}
}
if (triggerEvent) {
if (node.selected) {
this.emit('select', node)
} else {
this.emit('unselect', node)
}
}
this.triggerSelectedChange(triggerEvent, triggerDataChange)
}
/**
* 设置未加载单选选中节点,不公开此 API
*/
private setUnloadSelected(
key: TreeNodeKeyType,
value: boolean,
triggerEvent: boolean = true,
triggerDataChange: boolean = true
): void {
if (value) {
this.currentSelectedKey = null
this.unloadSelectedKey = key
} else {
if (this.unloadSelectedKey === key) {
this.unloadSelectedKey = null
}
}
this.triggerSelectedChange(triggerEvent, triggerDataChange)
}
/**
* 清除当前单选选中
* @param triggerEvent 是否触发事件
* @param triggerDataChange 是否触发视图刷新
*/
clearSelected(
triggerEvent: boolean = true,
triggerDataChange: boolean = true
): void {
if (this.currentSelectedKey && this.mapData[this.currentSelectedKey]) {
this.setSelected(
this.currentSelectedKey,
false,
triggerEvent,
triggerDataChange
)
} else if (this.unloadSelectedKey !== null) {
this.unloadSelectedKey = null
this.triggerSelectedChange(triggerEvent, triggerDataChange)
}
}
/**
* 设置节点展开
* @param key 节点 key
* @param value 是否展开
* @param expandParent 展开节点时是否同时展开父节点
* @param triggerEvent 是否触发事件
* @param triggerDataChange 是否触发 `data-change` 事件以通知外部刷新视图
*/
setExpand(
key: TreeNodeKeyType,
value: boolean,
expandParent: boolean = false,
triggerEvent: boolean = true,
triggerDataChange: boolean = true
): void {
const node = this.mapData[key]
if (!node || (!expandParent && node.isLeaf)) return
if (node.expand === value) return // 当前节点已经是将要设置的状态,直接返回
if (!node.isLeaf) {
if (typeof this.options.load === 'function') {
// 如果节点未加载过且将要设置为加载,则调用 load 方法
if (!node._loaded && !node._loading && value) {
node._loading = true
if (triggerDataChange) {
this.emit('visible-data-change')
}
new Promise((resolve, reject) => {
const load = this.options.load as Function
load(node, resolve, reject)
})
.then(children => {
if (Array.isArray(children)) {
this.loadChildren(node, children, value)
this.emit('set-data')
}
})
.catch((e: Error | string) => {
let err = e
if (!(e instanceof Error)) {
err = new Error(e)
}
console.error('[VTree] load tree nodes error.', err)
})
.then(() => {
node._loading = false
if (triggerEvent) {
this.emit('expand', node)
}
if (triggerDataChange) {
this.emit('visible-data-change')
}
})
return
} else if (node._loading) return // 正在加载的节点,直接返回
}
node.expand = value
// Set children visibility
let queue = node.children.concat()
while (queue.length) {
const nodeFromQueue = queue.pop()
if (!nodeFromQueue) continue
if (nodeFromQueue.expand && nodeFromQueue.children.length) {
queue = nodeFromQueue.children.concat(queue)
}
if (nodeFromQueue._filterVisible === false) {
nodeFromQueue.visible = false
} else {
nodeFromQueue.visible =
nodeFromQueue._parent === null ||
(nodeFromQueue._parent.expand && nodeFromQueue._parent.visible)
}
}
if (triggerEvent) {
this.emit('expand', node)
}
if (triggerDataChange) {
this.emit('visible-data-change')
}
}
if (expandParent && node._parent && value) {
this.setExpand(
node._parent[this.options.keyField],
value,
expandParent,
false,
triggerDataChange
)
}
}
/**
* 批量设置节点展开/折叠
* @param keys 展开的节点 key 数组
* @param value 是否展开
*/
setExpandKeys(
keys: TreeNodeKeyType[],
value: boolean,
triggerDataChange: boolean = true
): void {
keys.forEach(key => {
this.setExpand(key, value, false, false, false)
})
if (triggerDataChange) {
this.emit('visible-data-change')
}
}
setExpandAll(value: boolean, triggerDataChange: boolean = true): void {
this.flatData.forEach(node => {
if (!this.options.load || node._loaded) {
this.setExpand(node[this.options.keyField], value, false, false, false)
}
})
if (triggerDataChange) {
this.emit('visible-data-change')
}
}
private isChildrenChanged(node: TreeNode, newNode: ITreeNodeOptions): boolean {
return ('children' in newNode) && (!!node.children.length || !!newNode.children?.length)
}
updateNode(key: TreeNodeKeyType, newNode: ITreeNodeOptions, triggerEvent = true, triggerDataChange = true) {
const node = this.mapData[key]
if (!node) return
const newNodeCopy: ITreeNodeOptions = {}
const notAllowedFields = [
this.options.keyField,
'indeterminate',
'visible',
'isLeaf',
]
// Exclude key field and fields starting with '_'
Object.keys(newNode).forEach((field) => {
if (!field.startsWith('_') && !notAllowedFields.includes(field)) {
newNodeCopy[field] = newNode[field]
}
})
const previousCheckedKeys = this.getCheckedKeys()
const previousSelectedKey = this.getSelectedKey()
let triggerSetDataFlag = this.isChildrenChanged(node, newNodeCopy)
if (('children' in newNodeCopy) && (!!node.children.length || !!newNodeCopy.children?.length)) {
// remove all children
this.removeChildren(key, false, false)
// add new children
if (Array.isArray(newNodeCopy.children)) {
this.loadChildren(node, newNodeCopy.children, node.expand)
}
delete newNodeCopy.children
}
if ('checked' in newNodeCopy) {
this.setChecked(key, newNodeCopy.checked, false, false)
delete newNodeCopy.checked
}
if ('selected' in newNodeCopy) {
this.setSelected(key, newNodeCopy.selected, false, false)
delete newNodeCopy.selected
}
if ('expand' in newNodeCopy) {
this.setExpand(key, newNodeCopy.expand, false, false, false)
delete newNodeCopy.expand
}
Object.keys(newNodeCopy).forEach((field) => {
node[field] = newNodeCopy[field]
})
const currentCheckedKeys = this.getCheckedKeys()
const currentSelectedKey = this.getSelectedKey()
if (triggerEvent) {
if (JSON.stringify(currentCheckedKeys.sort()) !== JSON.stringify(previousCheckedKeys.sort())) {
this.triggerCheckedChange(true, false)
}
if (currentSelectedKey !== previousSelectedKey) {
this.triggerSelectedChange(true, false)
}
}
if (triggerDataChange) {
if (triggerSetDataFlag) {
this.emit('set-data')
}
this.emit('visible-data-change')
}
}
updateNodes(newNodes: ITreeNodeOptions[]) {
const validNodes = newNodes.filter((node) => node[this.options.keyField] != null)
if (!validNodes.length) return
const previousCheckedKeys = this.getCheckedKeys()
const previousSelectedKey = this.getSelectedKey()
let triggerSetDataFlag = false
validNodes.forEach((newNode) => {
const key = newNode[this.options.keyField]
const node = this.mapData[key]
if (node) {
triggerSetDataFlag = triggerSetDataFlag || this.isChildrenChanged(node, newNode)
this.updateNode(key, newNode, false, false)
}
})
const currentCheckedKeys = this.getCheckedKeys()
const currentSelectedKey = this.getSelectedKey()
if (JSON.stringify(currentCheckedKeys.sort()) !== JSON.stringify(previousCheckedKeys.sort())) {
this.triggerCheckedChange(true, false)
}
if (currentSelectedKey !== previousSelectedKey) {
this.triggerSelectedChange(true, false)
}
if (triggerSetDataFlag) {
this.emit('set-data')
}
this.emit('visible-data-change')
}
//#endregion Set api
//#region Get api
/**
* 获取多选选中节点
* @param ignoreMode 忽略模式,可选择忽略父节点或子节点,默认值是 VTree 的 ignoreMode Prop
*/
getCheckedNodes(ignoreMode = this.options.ignoreMode): TreeNode[] {
if (ignoreMode === ignoreEnum.children) {
const result: TreeNode[] = []
const traversal = (nodes: TreeNode[]) => {
nodes.forEach(node => {
if (node.checked) {
result.push(node)
} else if (!node.isLeaf && node.indeterminate) {
traversal(node.children)
}
})
}
traversal(this.data)
return result
} else {
return this.flatData.filter(node => {
if (ignoreMode === ignoreEnum.parents)
return node.checked && node.isLeaf
return node.checked
})
}
}
/**
* 获取多选选中的节点 key ,包括未加载的 key
* @param ignoreMode 忽略模式,同 `getCheckedNodes`
*/
getCheckedKeys(ignoreMode = this.options.ignoreMode): TreeNodeKeyType[] {
return this.getCheckedNodes(ignoreMode)
.map(checkedNodes => checkedNodes[this.options.keyField])
.concat(this.unloadCheckedKeys)
}
/**
* 获取多选半选状态节点
*/
getIndeterminateNodes(): TreeNode[] {
return this.flatData.filter(node => node.indeterminate)
}
/**
* 获取当前单选选中节点
*/
getSelectedNode(): TreeNode | null {
if (this.currentSelectedKey === null) return null
return this.mapData[this.currentSelectedKey] || null
}
/**
* 获取当前单选选中节点 key ,有可能是未加载的选中项
*/
getSelectedKey(): TreeNodeKeyType | null {
return this.currentSelectedKey || this.unloadSelectedKey || null
}
/**
* 获取未加载但多选选中的节点
*/
getUnloadCheckedKeys(): TreeNodeKeyType[] {
return this.unloadCheckedKeys
}
/**
* 获取展开节点
*/
getExpandNodes(): TreeNode[] {
return this.flatData.filter(node => !node.isLeaf && node.expand)
}
/**
* 获取展开节点 keys
*/
getExpandKeys(): TreeNodeKeyType[] {
return this.getExpandNodes().map(node => node[this.options.keyField])
}
/**
* 根据节点 key 获取节点
* @param key 节点 key
*/
getNode(key: TreeNodeKeyType): TreeNode | null {
return this.mapData[key] || null
}
//#endregion Get api
//#region Node transfer api
// 这边的 referenceKey 都是被执行的节点,比如 insertBefore ,在这里 referenceKey 就是指要插入到 referenceKey 节点前面
insertBefore(
insertedNode: TreeNodeKeyType | ITreeNodeOptions,
referenceKey: TreeNodeKeyType,
triggerEvent = true,
triggerDataChange = true,
): TreeNode | null {
const node = this.getInsertedNode(insertedNode, referenceKey)
if (!node) return null
this.remove(node[this.options.keyField], false, false)
const referenceNode = this.mapData[referenceKey]
const parentNode = referenceNode._parent
const childIndex = this.findIndex(
referenceKey,
parentNode && parentNode.children
)
const flatIndex = this.findIndex(referenceKey)
const dataIndex =
(parentNode && -1) || this.findIndex(referenceKey, this.data)
this.insertIntoStore(node, parentNode, childIndex, flatIndex, dataIndex, triggerEvent, triggerDataChange)
if (triggerDataChange) {
this.emit('visible-data-change')
}
return node
}
insertAfter(
insertedNode: TreeNodeKeyType | ITreeNodeOptions,
referenceKey: TreeNodeKeyType,
triggerEvent = true,
triggerDataChange = true,
): TreeNode | null {
const node = this.getInsertedNode(insertedNode, referenceKey)
if (!node) return null
this.remove(node[this.options.keyField], false, false)
const referenceNode = this.mapData[referenceKey]
const parentNode = referenceNode._parent
const childIndex =
this.findIndex(referenceKey, parentNode && parentNode.children) + 1
const length = this.flatData.length
const referenceIndex = this.findIndex(referenceKey)
// 找到待插入节点应该插入 flatData 的索引
let flatIndex = referenceIndex + 1
for (let i = referenceIndex + 1; i <= length; i++) {
if (i === length) {
flatIndex = i
break
}
if (this.flatData[i]._level <= referenceNode._level) {
flatIndex = i
break
}
}
const dataIndex =
(parentNode && -1) || this.findIndex(referenceKey, this.data) + 1
this.insertIntoStore(node, parentNode, childIndex, flatIndex, dataIndex, triggerEvent, triggerDataChange)
if (triggerDataChange) {
this.emit('visible-data-change')
}
return node
}
append(
insertedNode: TreeNodeKeyType | ITreeNodeOptions,
parentKey: TreeNodeKeyType,
triggerEvent = true,
triggerDataChange = true,
): TreeNode | null {
const parentNode = this.mapData[parentKey]
if (!parentNode.isLeaf) {
const childrenLength = parentNode.children.length
return this.insertAfter(
insertedNode,
parentNode.children[childrenLength - 1][this.options.keyField],
triggerEvent,
triggerDataChange,
)
}
const node = this.getInsertedNode(insertedNode, parentKey, true)
if (!node) return null
this.remove(node[this.options.keyField], false, false)
const flatIndex = this.findIndex(parentKey) + 1
this.insertIntoStore(node, parentNode, 0, flatIndex, undefined, triggerEvent, triggerDataChange)
if (triggerDataChange) {
this.emit('visible-data-change')
}
return node
}
prepend(
insertedNode: TreeNodeKeyType | ITreeNodeOptions,
parentKey: TreeNodeKeyType,
triggerEvent = true,
triggerDataChange = true,
): TreeNode | null {
const parentNode = this.mapData[parentKey]
if (!parentNode.isLeaf) {
return this.insertBefore(
insertedNode,
parentNode.children[0][this.options.keyField],
triggerEvent,
triggerDataChange,
)
}
const node = this.getInsertedNode(insertedNode, parentKey, true)
if (!node) return null
this.remove(node[this.options.keyField], false, false)
const flatIndex = this.findIndex(parentKey) + 1
this.insertIntoStore(node, parentNode, 0, flatIndex, undefined, triggerEvent, triggerDataChange)
if (triggerDataChange) {
this.emit('visible-data-change')
}
return node
}
/**
* 删除节点
* @param removedKey 要删除的节点 key
*/
remove(
removedKey: TreeNodeKeyType,
triggerEvent: boolean = true,
triggerDataChange: boolean = true,
): TreeNode | null {
const node = this.mapData[removedKey]
if (!node) return null
// 从 flatData 中移除
const index = this.findIndex(node)
if (index === -1) return null
let deleteCount = 1
const length = this.flatData.length
for (let i = index + 1; i < length; i++) {
if (this.flatData[i]._level > node._level) {
deleteCount++
} else break
}
this.flatData.splice(index, deleteCount)
// 从 mapData 中移除
const deleteInMap = (key: TreeNodeKeyType): void => {
const node = this.mapData[key]
delete this.mapData[key]
node.children.forEach(child => deleteInMap(child[this.options.keyField]))
}
deleteInMap(removedKey)
// 从 data 中移除
if (!node._parent) {
const index = this.findIndex(node, this.data)
if (index > -1) {
this.data.splice(index, 1)
}
}
// 从父节点 children 中移除
if (node._parent) {
const childIndex = this.findIndex(node, node._parent.children)
if (childIndex !== -1) {
node._parent.children.splice(childIndex, 1)
}
// 处理父节点 isLeaf
node._parent.isLeaf = !node._parent.children.length
// 处理父节点 expand
if (node._parent.isLeaf) {
node._parent.expand = false
node._parent.indeterminate = false
}
// 更新被移除处父节点状态
this.updateMovingNodeStatus(node, triggerEvent, triggerDataChange)
}
if (triggerDataChange) {
this.emit('visible-data-change')
}
return node
}
private removeChildren(
parentKey: TreeNodeKeyType,
triggerEvent: boolean = true,
triggerDataChange: boolean = true,
) {
const node = this.mapData[parentKey]
if (!node || !node.children.length) return null
const firstChild = node.children[0]
let movingNode = firstChild
// 从 flatData 中移除
const index = this.findIndex(node)
if (index === -1) return null
let deleteCount = 0
const length = this.flatData.length
for (let i = index + 1; i < length; i++) {
if (this.flatData[i]._level > node._level) {
// 从 mapData 中移除
delete this.mapData[this.flatData[i][this.options.keyField]]
deleteCount++
// 如果是 Selected 的节点,则记录
if (this.flatData[i].selected) {
movingNode = this.flatData[i]
}
} else break
}
this.flatData.splice(index + 1, deleteCount)
// 从父节点 children 中移除
node.children.splice(0, node.children.length)
node.isLeaf = true
node.indeterminate = false
// 更新被移除处父节点状态
this.updateMovingNodeStatus(movingNode, triggerEvent, triggerDataChange)
if (triggerDataChange) {
this.emit('visible-data-change')
}
return node
}
private loadChildren(node: TreeNode, children: any[], expand: boolean) {
const parentIndex: number = this.findIndex(node)
if (parentIndex === -1) return
node._loaded = true
node.expand = expand
node.setChildren(children)
node.isLeaf = !node.children.length
// 如果单选选中的值为空,则允许后续数据覆盖单选 value
const currentCheckedKeys = this.getCheckedKeys()
const flattenChildren = this.flattenData(
node.children,
this.getSelectedKey() === null
)
this.insertIntoFlatData(parentIndex + 1, flattenChildren)
// 如果有未加载的选中节点,判断其是否已加载
this.setUnloadCheckedKeys(currentCheckedKeys)
if (this.unloadSelectedKey !== null) {
this.setUnloadSelectedKey(this.unloadSelectedKey)
}
this.checkNodeUpward(node, true)
}
private getInsertedNode(
insertedNode: TreeNodeKeyType | ITreeNodeOptions,
referenceKey: TreeNodeKeyType,
isParent: boolean = false
): TreeNode | null {
const referenceNode = this.mapData[referenceKey]
if (!referenceNode) return null
const parentNode = isParent ? referenceNode : referenceNode._parent
if (insertedNode instanceof TreeNode) {
// 与参照节点是同一个节点
if (insertedNode[this.options.keyField] === referenceKey) return null
return insertedNode
} else if (typeof insertedNode === 'object') {
if (insertedNode[this.options.keyField] === referenceKey) return null
const mapNode = this.mapData[insertedNode[this.options.keyField]]
if (mapNode) return mapNode
return new TreeNode(
insertedNode,
parentNode,
this.options.keyField,
!!this.options.load
)
} else {
if (!this.mapData[insertedNode] || insertedNode === referenceKey)
return null
return this.mapData[insertedNode]
}
}
/**
* 将一个节点插入 store 记录中
* @param node 要插入的节点
* @param parentNode 要插入节点的父节点
* @param childIndex 如果有父节点,则需提供要插入的节点在同级节点中的顺序
* @param flatIndex 在 flatData 中的索引
* @param dataIndex 如果没有父节点,需要提供节点在 data 中的索引
*/
private insertIntoStore(
node: TreeNode,
parentNode: TreeNode | null,
childIndex: number,
flatIndex: number,
dataIndex?: number,
triggerEvent = true,
triggerDataChange = true,
): void {
if (flatIndex === -1) return
// 插入父节点 children 中
if (childIndex !== -1 && parentNode) {
parentNode.children.splice(childIndex, 0, node)
}
// 更新 _parent
node._parent = parentNode
// 更新父节点 isLeaf, expand
if (parentNode) {
parentNode.isLeaf = false
this.setExpand(
parentNode[this.options.keyField],
true,
false,
false,
false
)
} else if (typeof dataIndex === 'number' && dataIndex > -1) {
// 没有父节点,则需要插入到 this.data 中以保证数据正确
this.data.splice(dataIndex, 0, node)
}
// 插入 flatData 与 mapData
const nodes: TreeNode[] = this.flattenData([node])
// 处理自身及子节点 _level
node._level = parentNode ? parentNode._level + 1 : 0
nodes.forEach(
childNode =>
(childNode._level = childNode._parent
? childNode._parent._level + 1
: 0)
)
this.insertIntoFlatData(flatIndex, nodes)
// 更新插入节点父节点状态
this.updateMovingNodeStatus(node, triggerEvent, triggerDataChange)
}
private updateMovingNodeStatus(movingNode: TreeNode, triggerEvent = true, triggerDataChange = true): void {
// 处理多选
this.checkNodeUpward(movingNode)
this.triggerCheckedChange(triggerEvent, triggerDataChange)
// 处理单选
if (movingNode.selected) {
this.setSelected(movingNode[this.options.keyField], true, triggerEvent, triggerDataChange)
}
}
//#endregion Node transfer api
/**
* 过滤本地节点数据
* @param keyword 过滤关键词
* @param filterMethod 过滤方法
*/
filter(keyword: string, filterMethod: FilterFunctionType): void {
// 使用树形结构数据进行遍历
const filterVisibleNodes: TreeNode[] = []
this.flatData.forEach(node => {
node._filterVisible =
(node._parent && node._parent._filterVisible) ||
filterMethod(keyword, node)
node.visible = node._filterVisible
if (node._filterVisible) {
filterVisibleNodes.push(node)
}
})
// 对于临时列表中的节点,都是可见的,因此将它们的父节点都设为可见并展开
filterVisibleNodes.forEach(node => {
const stack = []
let parent = node._parent
while (parent) {
stack.unshift(parent)
parent = parent._parent
}
stack.forEach(parent => {
parent._filterVisible = true
// parent.visible = parent._filterVisible
// _filterVisible 且 无父级或者父级展开且可见,则设为可见
parent.visible =
(parent._parent === null ||
(parent._parent.expand && parent._parent.visible)) &&
parent._filterVisible
this.options.expandOnFilter &&
this.setExpand(
parent[this.options.keyField],
true,
false,
false,
false
)
})
node.visible =
node._parent === null || (node._parent.expand && node._parent.visible)
})
this.emit('visible-data-change')
}
/**
* 过滤未加载多选节点,对比最终勾选节点是否有变化并触发 checked-change 事件
* @param keys 全量选中节点 key 数组,包括加载与未加载选中节点
*/
private setUnloadCheckedKeys(keys: TreeNodeKeyType[]): void {
this.unloadCheckedKeys = keys
const checkedKeysCache = keys.concat()
const length = this.unloadCheckedKeys.length
for (let i = length - 1; i >= 0; i--) {
const key = this.unloadCheckedKeys[i]
if (this.mapData[key]) {
this.setChecked(key, true, false, false)
this.unloadCheckedKeys.splice(i, 1)
}
}
const newCheckedKeys = this.getCheckedKeys()
if (
newCheckedKeys.length === checkedKeysCache.length &&
newCheckedKeys.every(val =>
(checkedKeysCache as TreeNodeKeyType[]).some(cache => cache === val)
)
)
return
// 因为多选模式下,value 传过来的数据跟数据上标识 checked: true 的结果可能不一致,因此需要触发一个事件告诉外部最终勾选了哪些节点
this.emit('checked-change', this.getCheckedNodes(), newCheckedKeys)
}
/**
* 过滤未加载单选选中节点,对比是否有变化并触发 selected-change 事件
* @param key 节点 key
*/
private setUnloadSelectedKey(key: TreeNodeKeyType): void {
const selectedKeyCache = this.getSelectedKey()
if (this.mapData[key]) {
// 如果节点已加载
this.setSelected(key, true, false)
this.unloadSelectedKey = null
} else {
// 节点未加载
this.currentSelectedKey &&
this.setSelected(this.currentSelectedKey, false, false)
this.unloadSelectedKey = key
}
const newSelectedKey = this.getSelectedKey()
if (newSelectedKey === selectedKeyCache) return
this.emit('selected-change', this.getSelectedNode(), newSelectedKey)
}
/**
* 保存扁平化的节点数据 `flatData` 与节点数据 Map `mapData`
* @param nodes 树状节点数据
* @param overrideSelected 是否根据数据设置 `selected`
*/
private flattenData(
nodes: TreeNode[],
overrideSelected: boolean = true,
result: TreeNode[] = []
): TreeNode[] {
const length = nodes.length
for (let i = 0; i < length; i++) {
const node = nodes[i]
const key: TreeNodeKeyType = node[this.options.keyField]
result.push(node)
if (this.mapData[key]) {
throw new Error('[VTree] Duplicate tree node key.')
}
this.mapData[key] = node
// 如果数据上就是选中的,则更新节点状态
if (node.checked && this.options.cascade) {
// 向下勾选,包括自身
this.checkNodeDownward(node, true)
}
if (node.selected && overrideSelected) {
this.clearSelected(false, false)
this.currentSelectedKey = node[this.options.keyField]
this.emit('selected-change', node, this.currentSelectedKey)
}
if (
(this.options.defaultExpandAll || node.expand) &&
!this.options.load &&
!node.isLeaf
) {
node.expand = false
this.setExpand(node[this.options.keyField], true, false, false, false)
}
if (node.children.length) {
this.flattenData(node.children, overrideSelected, result)
}
}
if (this.options.cascade && !!length) {
// 向上勾选父节点直到根节点
this.checkNodeUpward(nodes[0])
}
return result
}
//#region Check nodes
/**
* 向下勾选/取消勾选节点,包括自身
* @param node 需要向下勾选的节点
* @param value 勾选或取消勾选
* @param filtering 是否正在过滤,如果是,则考虑 `filteredNodeCheckable` Prop
*/
private checkNodeDownward(
node: TreeNode,
value: boolean,
filtering: boolean = false
): void {
node.children.forEach(child => {
this.checkNodeDownward(child, value, filtering)
})
if (node.isLeaf || (this.options.load && !node.children.length)) {
if (!node.disabled) {
// 正在过滤,若被过滤节点不可勾选,且节点过滤后不可见,则直接返回
if (
filtering &&
!this.options.filteredNodeCheckable &&
!node._filterVisible
)
return
node.checked = value
node.indeterminate = false
}
} else {
this.checkParentNode(node)
}
}
/**
* 向上勾选/取消勾选父节点,不包括自身
* @param node 需要勾选的节点
* @param fromCurrentNode 是否从当前节点开始处理
*/
private checkNodeUpward(node: TreeNode, fromCurrentNode = false) {
let parent = fromCurrentNode ? node : node._parent
while (parent) {
this.checkParentNode(parent)
parent = parent._parent
}
}
/**
* 根据子节点的勾选状态更新当前父节点的勾选状态
* @param node 需要勾选的节点
*/
private checkParentNode(node: TreeNode): void {
const length = node.children.length
if (!length) return
let hasChecked = false
let hasUnchecked = false
let isInterrupted = false
for (let i = 0; i < length; i++) {
const child = node.children[i]
if (child.checked) {
hasChecked = true
} else {
hasUnchecked = true
}
if ((hasChecked && hasUnchecked) || child.indeterminate) {
isInterrupted = true
node.checked = false
node.indeterminate = true
break
}
}
if (!isInterrupted) {
node.checked = hasChecked
node.indeterminate = false
}
}
//#endregion Check nodes
//#region Utils
/**
* 搜索节点在指定数组中的位置
*/
private findIndex(
keyOrNode: TreeNode | TreeNodeKeyType,
searchList: TreeNode[] | TreeNodeKeyType[] | null = this.flatData
): number {
if (searchList !== null) {
let key: TreeNodeKeyType =
keyOrNode instanceof TreeNode
? keyOrNode[this.options.keyField]
: keyOrNode
const length = searchList.length
for (let i = 0; i < length; i++) {
if (searchList[0] instanceof TreeNode) {
if (key === (searchList as TreeNode[])[i][this.options.keyField]) {
return i
}
} else {
if (key === searchList[i]) {
return i
}
}
}
}
return -1
}
//#endregion Utils
}