From 08977634d45aab9aa89e4ac5d865c5a7002d5f7a Mon Sep 17 00:00:00 2001
From: ChuChencheng <azureternite@live.com>
Date: Sun, 11 Aug 2024 02:25:01 +0800
Subject: [PATCH] feat: optimize loadChildren and update doc

---
 site/.vitepress/code/ReloadChildren.vue    | 16 ++++-
 site/.vitepress/code/UpdateCustomField.vue | 66 ++++++++++++++++++++
 site/api/vtree.md                          |  2 +
 site/en/api/vtree.md                       |  2 +
 site/en/examples/node-manipulation.md      | 20 ++++++
 site/examples/node-manipulation.md         |  6 ++
 src/store/tree-store.ts                    | 71 +++++++++++++++-------
 tests/unit/tree.spec.ts                    |  2 +-
 8 files changed, 160 insertions(+), 25 deletions(-)
 create mode 100644 site/.vitepress/code/UpdateCustomField.vue

diff --git a/site/.vitepress/code/ReloadChildren.vue b/site/.vitepress/code/ReloadChildren.vue
index 47b2908..2a91d8d 100644
--- a/site/.vitepress/code/ReloadChildren.vue
+++ b/site/.vitepress/code/ReloadChildren.vue
@@ -1,8 +1,9 @@
 <template>
-  <button @click="handleSetChildren">Set node-1 children</button>
   <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" />
+    <VTree ref="tree" checkable selectable />
   </div>
 </template>
 
@@ -47,6 +48,17 @@ const handleSetChildren = () => {
 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>
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/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 a07ed86..98947b0 100644
--- a/site/examples/node-manipulation.md
+++ b/site/examples/node-manipulation.md
@@ -31,6 +31,12 @@
 
 <CodeDemo component="UpdateNodeTitle" />
 
+## 更新自定义字段 {#update-custom-field}
+
+调用树组件的 `updateNode` 方法更新自定义字段
+
+<CodeDemo component="UpdateCustomField" />
+
 ## 重新加载子节点 {#reload-children}
 
 调用 `updateNode` 传入新的 `children` 列表可以重新加载子节点
diff --git a/src/store/tree-store.ts b/src/store/tree-store.ts
index 631615e..15d1bd1 100644
--- a/src/store/tree-store.ts
+++ b/src/store/tree-store.ts
@@ -291,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
       }
     }
 
@@ -327,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) {
@@ -492,8 +487,13 @@ 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) {
-    if (!this.mapData[key]) return
+    const node = this.mapData[key]
+    if (!node) return
 
     const newNodeCopy: ITreeNodeOptions = {}
     const notAllowedFields = [
@@ -512,14 +512,15 @@ export default class TreeStore extends TreeEventTarget {
 
     const previousCheckedKeys = this.getCheckedKeys()
     const previousSelectedKey = this.getSelectedKey()
+    let triggerSetDataFlag = this.isChildrenChanged(node, newNodeCopy)
 
-    if ('children' in 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(this.mapData[key], newNodeCopy.children, this.mapData[key].expand)
+        this.loadChildren(node, newNodeCopy.children, node.expand)
       }
 
       delete newNodeCopy.children
@@ -537,7 +538,7 @@ export default class TreeStore extends TreeEventTarget {
       delete newNodeCopy.expand
     }
     Object.keys(newNodeCopy).forEach((field) => {
-      this.mapData[key][field] = newNodeCopy[field]
+      node[field] = newNodeCopy[field]
     })
 
     const currentCheckedKeys = this.getCheckedKeys()
@@ -554,6 +555,9 @@ export default class TreeStore extends TreeEventTarget {
     }
 
     if (triggerDataChange) {
+      if (triggerSetDataFlag) {
+        this.emit('set-data')
+      }
       this.emit('visible-data-change')
     }
   }
@@ -564,9 +568,15 @@ export default class TreeStore extends TreeEventTarget {
 
     const previousCheckedKeys = this.getCheckedKeys()
     const previousSelectedKey = this.getSelectedKey()
+    let triggerSetDataFlag = false
 
-    validNodes.forEach((node) => {
-      this.updateNode(node[this.options.keyField], node, false, 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()
@@ -580,6 +590,10 @@ export default class TreeStore extends TreeEventTarget {
       this.triggerSelectedChange(true, false)
     }
 
+    if (triggerSetDataFlag) {
+      this.emit('set-data')
+    }
+
     this.emit('visible-data-change')
   }
 
@@ -894,6 +908,7 @@ export default class TreeStore extends TreeEventTarget {
     if (!node || !node.children.length) return null
 
     const firstChild = node.children[0]
+    let movingNode = firstChild
 
     // 从 flatData 中移除
     const index = this.findIndex(node)
@@ -905,6 +920,11 @@ export default class TreeStore extends TreeEventTarget {
         // 从 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)
@@ -915,7 +935,7 @@ export default class TreeStore extends TreeEventTarget {
     node.indeterminate = false
 
     // 更新被移除处父节点状态
-    this.updateMovingNodeStatus(firstChild, triggerEvent, triggerDataChange)
+    this.updateMovingNodeStatus(movingNode, triggerEvent, triggerDataChange)
 
     if (triggerDataChange) {
       this.emit('visible-data-change')
@@ -935,7 +955,7 @@ export default class TreeStore extends TreeEventTarget {
     const currentCheckedKeys = this.getCheckedKeys()
     const flattenChildren = this.flattenData(
       node.children,
-      this.getSelectedKey === null
+      this.getSelectedKey() === null
     )
     this.insertIntoFlatData(parentIndex + 1, flattenChildren)
     // 如果有未加载的选中节点,判断其是否已加载
@@ -943,6 +963,8 @@ export default class TreeStore extends TreeEventTarget {
     if (this.unloadSelectedKey !== null) {
       this.setUnloadSelectedKey(this.unloadSelectedKey)
     }
+
+    this.checkNodeUpward(node, true)
   }
 
   private getInsertedNode(
@@ -1168,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) {
@@ -1191,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
   }
 
@@ -1230,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)