diff --git a/site/.vitepress/code/ReloadChildren.vue b/site/.vitepress/code/ReloadChildren.vue new file mode 100644 index 0000000..2a91d8d --- /dev/null +++ b/site/.vitepress/code/ReloadChildren.vue @@ -0,0 +1,72 @@ +<template> + <button @click="handleClearChildren">Clear node-1 children</button> + <button @click="handleSetChildren">Set node-1 children</button> + <button @click="handleUpdateChildren">Update node-1 children</button> + <div :style="{ height: '300px' }"> + <VTree ref="tree" checkable selectable /> + </div> +</template> + +<script setup lang="ts"> +import { computed, onMounted, ref } from 'vue' +import VTree from '@wsfe/vue-tree' + +const tree = ref() + +const children = Array.from({ length: 100000 }).map((_, i) => { + return { + title: `node-1-${i + 1}`, + id: `node-1-${i + 1}`, + } +}) + +const data = [ + { + title: 'node-1', + id: 'node-1', + children, + }, + { + title: 'node-2', + id: 'node-2', + children: [ + { + title: 'node-2-1', + id: 'node-2-1', + }, + ], + }, +] + +onMounted(() => { + tree.value.setData(data) +}) + +const handleSetChildren = () => { + tree.value.updateNode('node-1', { children }) +} +const handleClearChildren = () => { + tree.value.updateNode('node-1', { children: [] }) +} +const handleUpdateChildren = () => { + tree.value.updateNode('node-1', { + children: children.map((child) => { + return { + ...child, + title: `${child.title} ${Date.now()}`, + checked: true, + } + }) + }) +} +</script> + +<style scoped> +button { + border: 1px solid lightgray; + border-radius: 8px; + padding-left: 10px; + padding-right: 10px; + margin-right: 20px; +} +</style> diff --git a/site/.vitepress/code/UpdateCustomField.vue b/site/.vitepress/code/UpdateCustomField.vue new file mode 100644 index 0000000..34a7a3d --- /dev/null +++ b/site/.vitepress/code/UpdateCustomField.vue @@ -0,0 +1,66 @@ +<template> + <button @click="handleUpdateCount">Update node-1 count</button> + <VTree ref="tree"> + <template #node="{ node }"> + <span>{{ node.title }}</span> + <span v-if="typeof node.count === 'number'"> + Count: {{ node.count }} + </span> + </template> + </VTree> +</template> + +<script setup lang="ts"> +import { onMounted, ref } from 'vue' +import VTree from '@wsfe/vue-tree' + +const tree = ref() + +const data = [ + { + title: 'node-1', + id: 'node-1', + count: 0, + children: [ + { + title: 'node-1-1', + id: 'node-1-1', + }, + { + title: 'node-1-2', + id: 'node-1-2', + }, + ], + }, + { + title: 'node-2', + id: 'node-2', + children: [ + { + title: 'node-2-1', + id: 'node-2-1', + }, + ], + }, +] + +onMounted(() => { + tree.value.setData(data) +}) + +const handleUpdateCount = () => { + const key = 'node-1' + const currentCount = tree.value.getNode(key).count + tree.value.updateNode(key, { count: currentCount + 1 }) +} +</script> + +<style scoped> +button { + border: 1px solid lightgray; + border-radius: 8px; + padding-left: 10px; + padding-right: 10px; + margin-right: 20px; +} +</style> diff --git a/site/.vitepress/code/UpdateNodeTitle.vue b/site/.vitepress/code/UpdateNodeTitle.vue new file mode 100644 index 0000000..9eb4e82 --- /dev/null +++ b/site/.vitepress/code/UpdateNodeTitle.vue @@ -0,0 +1,73 @@ +<template> + <button @click="handleUpdateSingleNode">Update node-1</button> + <button @click="handleUpdateMultipleNode">Update node-1 & node-2</button> + <VTree ref="tree" /> +</template> + +<script setup lang="ts"> +import { computed, onMounted, ref } from 'vue' +import VTree from '@wsfe/vue-tree' + +const tree = ref() + +const data = [ + { + title: 'node-1', + id: 'node-1', + children: [ + { + title: 'node-1-1', + id: 'node-1-1', + }, + { + title: 'node-1-2', + id: 'node-1-2', + }, + ], + }, + { + title: 'node-2', + id: 'node-2', + children: [ + { + title: 'node-2-1', + id: 'node-2-1', + }, + ], + }, +] + +onMounted(() => { + tree.value.setData(data) +}) + +const count = ref(0) + +const handleUpdateSingleNode = () => { + count.value++ + tree.value.updateNode('node-1', { title: `node-1 - ${count.value}` }) +} +const handleUpdateMultipleNode = () => { + count.value++ + tree.value.updateNodes([ + { + id: 'node-1', + title: `node-1 - ${count.value}`, + }, + { + id: 'node-2', + title: `node-2 - ${count.value}`, + }, + ]) +} +</script> + +<style scoped> +button { + border: 1px solid lightgray; + border-radius: 8px; + padding-left: 10px; + padding-right: 10px; + margin-right: 20px; +} +</style> diff --git a/site/api/vtree.md b/site/api/vtree.md index 41d6d34..fe1aafa 100644 --- a/site/api/vtree.md +++ b/site/api/vtree.md @@ -97,6 +97,8 @@ | filter | 过滤节点 | `keyword: string`: 过滤关键词<br/>`filterMethod: (keyword: string, node: TreeNode) => boolean`: 过滤方法,默认为 filterMethod Prop ,如果没有传 filterMethod Prop 则为搜索 title 字段的一个内置方法 | `void` | | showCheckedNodes | 展示已选节点 | `showUnloadCheckedNodes: boolean`: 是否显示未加载的选中节点,默认为 Prop 传入的值 | `void` | | loadRootNodes | 从远程加载根节点 | 无 | `Promise<void>` | +| updateNode `4.1.0` | 更新单个节点 | `key: string \| number`: 节点 key<br/>`newNode: object`: 新节点数据,某些字段将被忽略,例如以下划线 "_" 开头的字段,以及 key 字段和 `indeterminate`, `visible`, `isLeaf` 等 | `void` | +| updateNodes `4.1.0` | 更新多个节点 | `newNodes: object[]`: 新节点数据数组,与 `updateNode` 相同,特定的字段会被忽略,且没有 key 字段的元素将被忽略 | `void` | | scrollTo | 滚动到指定节点位置 | `key: string \| number`: 节点 key<br/>`verticalPosition: 'top' \| 'center' \| 'bottom' \| number`: 滚动的垂直位置 | `void` | ## VTree Slots diff --git a/site/en/api/vtree.md b/site/en/api/vtree.md index ef725a0..0eb7e9b 100644 --- a/site/en/api/vtree.md +++ b/site/en/api/vtree.md @@ -97,6 +97,8 @@ Note: Since `2.0.8`, the node info returned in events contains the full node inf | filter | Filter nodes | `keyword: string`: filter keyword<br/>`filterMethod: (keyword: string, node: TreeNode) => boolean`: filter method, default to filterMethod prop. if filterMethod prop is not present, it's an internal method that searches node title | `void` | | showCheckedNodes | Show checked nodes | `showUnloadCheckedNodes: boolean`: whether to show checked nodes that are not loaded, default to prop value | `void` | | loadRootNodes | Load root nodes from remote | None | `Promise<void>` | +| updateNode `4.1.0` | Update single node | `key: string \| number`: node key<br/>`newNode: object`: new node data, some fields will be ignored, like those start with underscore '_', the key field and `indeterminate`, `visible`, `isLeaf`, etc. | `void` | +| updateNodes `4.1.0` | Update multiple nodes | `newNodes: object[]`: new nodes array, some specific fields will be ignored like `updateNode`, and the elements without key field also will be ignored | `void` | | scrollTo | Scroll to specific node position | `key: string \| number`: node key<br/>`verticalPosition: 'top' \| 'center' \| 'bottom' \| number`: vertical position of scrolling | `void` | ## VTree Slots diff --git a/site/en/examples/node-manipulation.md b/site/en/examples/node-manipulation.md index 5e4f92e..216d33c 100644 --- a/site/en/examples/node-manipulation.md +++ b/site/en/examples/node-manipulation.md @@ -22,3 +22,23 @@ Enable `draggable` and `droppable` - Invoke `remove` to remove a node <CodeDemo component="NodeCreationAndRemoval" /> + +## Update Node Title {#update-node-title} + +Invoke `updateNode` method to update some fields of tree node + +Invoke `updateNodes` to update multiple nodes + +<CodeDemo component="UpdateNodeTitle" /> + +## Update Custom Field {#update-custom-field} + +Invoke `updateNode` method to update custom fields in tree node + +<CodeDemo component="UpdateCustomField" /> + +## Reload Child Nodes {#reload-children} + +Invoke `updateNode` and pass a new `children` list to reload child nodes + +<CodeDemo component="ReloadChildren" /> diff --git a/site/examples/node-manipulation.md b/site/examples/node-manipulation.md index 36a1e41..98947b0 100644 --- a/site/examples/node-manipulation.md +++ b/site/examples/node-manipulation.md @@ -22,3 +22,23 @@ - 调用树组件的 `remove` 方法,可移除节点 <CodeDemo component="NodeCreationAndRemoval" /> + +## 更新节点名称 {#update-node-title} + +调用树组件的 `updateNode` 方法可更新节点部分字段 + +调用 `updateNodes` 可批量更新 + +<CodeDemo component="UpdateNodeTitle" /> + +## 更新自定义字段 {#update-custom-field} + +调用树组件的 `updateNode` 方法更新自定义字段 + +<CodeDemo component="UpdateCustomField" /> + +## 重新加载子节点 {#reload-children} + +调用 `updateNode` 传入新的 `children` 列表可以重新加载子节点 + +<CodeDemo component="ReloadChildren" /> diff --git a/src/components/Tree.vue b/src/components/Tree.vue index e8ab382..4f70e63 100644 --- a/src/components/Tree.vue +++ b/src/components/Tree.vue @@ -405,6 +405,8 @@ const { filter, showCheckedNodes, loadRootNodes, + updateNode, + updateNodes, } = usePublicTreeAPI(nonReactive, props, { resetSpaceHeights, updateExpandedKeys, @@ -705,6 +707,8 @@ defineExpose({ filter, showCheckedNodes, loadRootNodes, + updateNode, + updateNodes, scrollTo, }) diff --git a/src/constants/index.ts b/src/constants/index.ts index b316bdf..6a5ae03 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -39,6 +39,8 @@ export const TREE_API_METHODS = [ 'filter', 'showCheckedNodes', 'loadRootNodes', + 'updateNode', + 'updateNodes', 'scrollTo' ] as const diff --git a/src/hooks/usePublicTreeAPI.ts b/src/hooks/usePublicTreeAPI.ts index 7c553a2..d22737f 100644 --- a/src/hooks/usePublicTreeAPI.ts +++ b/src/hooks/usePublicTreeAPI.ts @@ -235,6 +235,18 @@ export const usePublicTreeAPI = ( isRootLoading.value = false }) } + /** + * 更新单个节点 + */ + function updateNode(key: TreeNodeKeyType, newNode: ITreeNodeOptions) { + return nonReactive.store.updateNode(key, newNode) + } + /** + * 更新多个节点 + */ + function updateNodes(newNodes: ITreeNodeOptions[]) { + return nonReactive.store.updateNodes(newNodes) + } return { unloadCheckedNodes, @@ -269,5 +281,7 @@ export const usePublicTreeAPI = ( filter, showCheckedNodes, loadRootNodes, + updateNode, + updateNodes, } } diff --git a/src/store/tree-store.ts b/src/store/tree-store.ts index 79e5d60..15d1bd1 100644 --- a/src/store/tree-store.ts +++ b/src/store/tree-store.ts @@ -232,8 +232,8 @@ export default class TreeStore extends TreeEventTarget { * @param triggerDataChange 是否触发视图刷新 */ private triggerCheckedChange( - triggerEvent: boolean = true, - triggerDataChange: boolean = true + triggerEvent = true, + triggerDataChange = true, ) { if (triggerEvent) { this.emit('checked-change', this.getCheckedNodes(), this.getCheckedKeys()) @@ -244,6 +244,24 @@ export default class TreeStore extends TreeEventTarget { } } + /** + * 触发 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 @@ -273,18 +291,15 @@ export default class TreeStore extends TreeEventTarget { } else { // 设置的节点不是当前已选中节点,要么当前没有选中节点,要么当前有选中节点 if (value) { - if (this.currentSelectedKey === null) { - // 当前没有选中节点 - node.selected = value - this.currentSelectedKey = node[this.options.keyField] - } else { + if (this.currentSelectedKey !== null) { // 取消当前已选中,设置新的选中节点 if (this.mapData[this.currentSelectedKey]) { this.mapData[this.currentSelectedKey].selected = false } - node.selected = value - this.currentSelectedKey = node[this.options.keyField] } + node.selected = value + this.currentSelectedKey = node[this.options.keyField] + this.unloadSelectedKey = null } } @@ -294,17 +309,9 @@ export default class TreeStore extends TreeEventTarget { } else { this.emit('unselect', node) } - - this.emit( - 'selected-change', - this.getSelectedNode(), - this.getSelectedKey() - ) } - if (triggerDataChange) { - this.emit('render-data-change') - } + this.triggerSelectedChange(triggerEvent, triggerDataChange) } /** @@ -317,9 +324,7 @@ export default class TreeStore extends TreeEventTarget { triggerDataChange: boolean = true ): void { if (value) { - if (this.currentSelectedKey) { - this.setSelected(this.currentSelectedKey, false, false, false) - } + this.currentSelectedKey = null this.unloadSelectedKey = key } else { if (this.unloadSelectedKey === key) { @@ -327,17 +332,7 @@ export default class TreeStore extends TreeEventTarget { } } - if (triggerEvent) { - this.emit( - 'selected-change', - this.getSelectedNode(), - this.getSelectedKey() - ) - } - - if (triggerDataChange) { - this.emit('render-data-change') - } + this.triggerSelectedChange(triggerEvent, triggerDataChange) } /** @@ -359,17 +354,7 @@ export default class TreeStore extends TreeEventTarget { } else if (this.unloadSelectedKey !== null) { this.unloadSelectedKey = null - if (triggerEvent) { - this.emit( - 'selected-change', - this.getSelectedNode(), - this.getSelectedKey() - ) - } - - if (triggerDataChange) { - this.emit('render-data-change') - } + this.triggerSelectedChange(triggerEvent, triggerDataChange) } } @@ -407,23 +392,7 @@ export default class TreeStore extends TreeEventTarget { }) .then(children => { if (Array.isArray(children)) { - const parentIndex: number = this.findIndex(node) - if (parentIndex === -1) return - node._loaded = true - node.expand = value - node.setChildren(children) - // 如果单选选中的值为空,则允许后续数据覆盖单选 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.loadChildren(node, children, value) this.emit('set-data') } }) @@ -518,6 +487,116 @@ export default class TreeStore extends TreeEventTarget { } } + 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 @@ -618,12 +697,14 @@ export default class TreeStore extends TreeEventTarget { insertBefore( insertedNode: TreeNodeKeyType | ITreeNodeOptions, - referenceKey: TreeNodeKeyType + 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) + this.remove(node[this.options.keyField], false, false) const referenceNode = this.mapData[referenceKey] const parentNode = referenceNode._parent @@ -636,19 +717,25 @@ export default class TreeStore extends TreeEventTarget { const dataIndex = (parentNode && -1) || this.findIndex(referenceKey, this.data) - this.insertIntoStore(node, parentNode, childIndex, flatIndex, dataIndex) - this.emit('visible-data-change') + this.insertIntoStore(node, parentNode, childIndex, flatIndex, dataIndex, triggerEvent, triggerDataChange) + + if (triggerDataChange) { + this.emit('visible-data-change') + } + return node } insertAfter( insertedNode: TreeNodeKeyType | ITreeNodeOptions, - referenceKey: TreeNodeKeyType + 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) + this.remove(node[this.options.keyField], false, false) const referenceNode = this.mapData[referenceKey] const parentNode = referenceNode._parent @@ -674,57 +761,77 @@ export default class TreeStore extends TreeEventTarget { const dataIndex = (parentNode && -1) || this.findIndex(referenceKey, this.data) + 1 - this.insertIntoStore(node, parentNode, childIndex, flatIndex, dataIndex) - this.emit('visible-data-change') + this.insertIntoStore(node, parentNode, childIndex, flatIndex, dataIndex, triggerEvent, triggerDataChange) + + if (triggerDataChange) { + this.emit('visible-data-change') + } + return node } append( insertedNode: TreeNodeKeyType | ITreeNodeOptions, - parentKey: TreeNodeKeyType + 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] + 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) + this.remove(node[this.options.keyField], false, false) const flatIndex = this.findIndex(parentKey) + 1 - this.insertIntoStore(node, parentNode, 0, flatIndex) - this.emit('visible-data-change') + this.insertIntoStore(node, parentNode, 0, flatIndex, undefined, triggerEvent, triggerDataChange) + + if (triggerDataChange) { + this.emit('visible-data-change') + } + return node } prepend( insertedNode: TreeNodeKeyType | ITreeNodeOptions, - parentKey: TreeNodeKeyType + 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] + 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) + this.remove(node[this.options.keyField], false, false) const flatIndex = this.findIndex(parentKey) + 1 - this.insertIntoStore(node, parentNode, 0, flatIndex) - this.emit('visible-data-change') + this.insertIntoStore(node, parentNode, 0, flatIndex, undefined, triggerEvent, triggerDataChange) + + if (triggerDataChange) { + this.emit('visible-data-change') + } + return node } @@ -734,7 +841,8 @@ export default class TreeStore extends TreeEventTarget { */ remove( removedKey: TreeNodeKeyType, - triggerDataChange: boolean = true + triggerEvent: boolean = true, + triggerDataChange: boolean = true, ): TreeNode | null { const node = this.mapData[removedKey] if (!node) return null @@ -781,8 +889,53 @@ export default class TreeStore extends TreeEventTarget { node._parent.indeterminate = false } // 更新被移除处父节点状态 - this.updateMovingNodeStatus(node) + 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') @@ -791,6 +944,29 @@ export default class TreeStore extends TreeEventTarget { 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, @@ -833,7 +1009,9 @@ export default class TreeStore extends TreeEventTarget { parentNode: TreeNode | null, childIndex: number, flatIndex: number, - dataIndex?: number + dataIndex?: number, + triggerEvent = true, + triggerDataChange = true, ): void { if (flatIndex === -1) return @@ -873,16 +1051,16 @@ export default class TreeStore extends TreeEventTarget { this.insertIntoFlatData(flatIndex, nodes) // 更新插入节点父节点状态 - this.updateMovingNodeStatus(node) + this.updateMovingNodeStatus(node, triggerEvent, triggerDataChange) } - private updateMovingNodeStatus(movingNode: TreeNode): void { + private updateMovingNodeStatus(movingNode: TreeNode, triggerEvent = true, triggerDataChange = true): void { // 处理多选 this.checkNodeUpward(movingNode) - this.triggerCheckedChange() + this.triggerCheckedChange(triggerEvent, triggerDataChange) // 处理单选 if (movingNode.selected) { - this.setSelected(movingNode[this.options.keyField], true) + this.setSelected(movingNode[this.options.keyField], true, triggerEvent, triggerDataChange) } } @@ -1012,8 +1190,6 @@ export default class TreeStore extends TreeEventTarget { if (node.checked && this.options.cascade) { // 向下勾选,包括自身 this.checkNodeDownward(node, true) - // 向上勾选父节点直到根节点 - this.checkNodeUpward(node) } if (node.selected && overrideSelected) { @@ -1035,6 +1211,12 @@ export default class TreeStore extends TreeEventTarget { this.flattenData(node.children, overrideSelected, result) } } + + if (this.options.cascade && !!length) { + // 向上勾选父节点直到根节点 + this.checkNodeUpward(nodes[0]) + } + return result } @@ -1074,9 +1256,10 @@ export default class TreeStore extends TreeEventTarget { /** * 向上勾选/取消勾选父节点,不包括自身 * @param node 需要勾选的节点 + * @param fromCurrentNode 是否从当前节点开始处理 */ - private checkNodeUpward(node: TreeNode) { - let parent = node._parent + private checkNodeUpward(node: TreeNode, fromCurrentNode = false) { + let parent = fromCurrentNode ? node : node._parent while (parent) { this.checkParentNode(parent) parent = parent._parent diff --git a/tests/unit/tree.spec.ts b/tests/unit/tree.spec.ts index 7050458..f342c34 100644 --- a/tests/unit/tree.spec.ts +++ b/tests/unit/tree.spec.ts @@ -168,7 +168,7 @@ describe('树展示测试', () => { ).toBe(true) expect( treeNodes[1].find('.vtree-tree-node__checkbox_indeterminate').exists() - ).toBe(true) + ).toBe(false) expect( treeNodes[2].find('.vtree-tree-node__title_selected').exists() ).toBe(true)