# treemate · [![Coverage Status](https://coveralls.io/repos/github/07akioni/treemate/badge.svg)](https://coveralls.io/github/07akioni/treemate) https://treemate.vercel.app/zh-CN.html 组件开发中树形数据结构的一站式解决方案。 帮助人们操作树形数据结构(可以用于 Tree,Select,Dropdown,Table,Menu 等组件)。 1. 勾选节点,取消勾选节点 2. 展开节点,折叠节点 3. 在节点中移动 4. 获取打平为数组的树 5. 查询节点 6. 支持 group 节点(聚集了一些同层级节点的节点) 7. 支持 ignored 节点(在移动时忽略) 8. 节点的元信息 9. 获取到对应的原始数据引用 10. 不完整数据中的勾选 11. ... ## 安装 ```bash npm i -D treemate ``` ## 基本概念 在开始之前,我强烈建议阅读这一节,了解一些 treemate 的基本概念。 ### 普通节点,Group 节点和 Ignored 节点 在 treemate 中,一个树由节点(可选的 group 节点和可选的 ignored 节点)组成。 一个普通节点包括一个 key,可能还包含一个 children 属性,里面是它的子节点。 #### 普通节点 ``` Node { key, children? } ``` #### Group 节点 如果你不需要 Group 节点,可以跨过这一节。 一个 Group 节点包含一个 key,一个名为 type 的属性,值为 `group`,还有一个 children 属性包含了它的子节点(不能包含 Group 节点)。 ``` GroupNode { key, type: 'group', children } ``` 在不同节点的移动过程中,Group 节点自身会被忽略,Group 节点的子节点会被看作为和 Group 在同一层的节点。 例如在下面的树中,group node 1 是 node 1 的下一个节点,group child 2 的下一个节点是 node 2. ``` - node 1 - group node 1 - group child 1 - group child 2 - node 2 ``` #### Ignored 节点 有的时候你需要一些仅需渲染的节点。例如 ``` - node 1 - 分割线 (render only) - node 2 ``` 在数据层面,分割线节点没有实际意义。你可以把它作为一个 Ignored 节点。这个节点在节点移动的过程中会被忽略。(node 2 会被看作 node 1 的下一个节点。)同时 `getNode` 方法也不会返回 Ignored 节点。 Ignored 节点也应该包含一个 key(为现代前端框架的 diff 过程使用)。 ``` IgnoredNode { key, type: 'ignored' } ``` ## 使用方式 ### 创建一个 Treemate `createTreeMate` 方法接受一个节点的数组作为输入数据,返回一个 treemate 的实例。 在 javascript 中使用: ```js import { createTreeMate } from 'treemate' const data = [ // 非叶节点 { key: 1, children: [ { key: 2 } ] }, // 叶节点 { key: 3 }, // ignored 节点 { key: 4, type: 'ignored' }, // group 节点 { key: 5, type: 'group', children: [ { key: 6 } ] } ] const treeMate = createTreeMate(data) ``` 在 typescript 中,data 没什么区别。但是 `createTreeMate` 接受 3 个可选的泛型参数,这些参数为普通节点、Group 节点和 Ignored 节点的类型。 ```ts interface BaseNode { key: string | number children?: Array } interface GroupNode { key: string | number type: 'group' children: Array } interface IgnoredNode { key: string | number type: 'ignored' } // 1. 指名左右节点的类型 const treeMate = createTreeMate(data) // 2. 等价于 createTreeMate() // 也就是说在数据中不会出现 Ignored 节点 const treeMate = createTreeMate(data) // 3. 等价于 createTreeMate() // 也就是说 Ignored 节点和 Group 节点不会出现在数据中 const treeMate = createTreeMate(data) // 4. 不写任何泛型参数 // 它会使用一个内置的类型作为基本节点的类型 // RawNode { // key: string | number // children?: RawNode[] // disabled?: boolean // isLeaf? boolean // } // Ignored 节点和 Group 节点不会出现在数据中 const treeMate = createTreeMate(data) ``` ### 自定义 `createTreeMate` 的选项 如果希望通过其他方式来判定一个节点的 key 和 disabled、group、ignored 的状态,你可以给 create treemate 传递一个选项。 ```ts const treeMate = createTreeMate(data, { getKey: (node) => Key, getDisabled: (node) => boolean, getIsGroup: (node) => boolean, getIgnored: (node) => boolean }) ``` ### 从树中获取一个节点 假设我们有一个 treemate 的实例。 ```ts const tmNode = treeMate.getNode(key) // 如果不存在该节点会返回 null // 注意:getNode 不会返回 Group 节点和 Ignored 节点! // 如果你需要获取这些节点,你可以使用 treeNodeMap,例如: treeMate.treeNodeMap.get(key) ``` ### TreeMateNode 树节点的属性 ```js TreeMateNode { key, rawNode, // 对于原始数据节点的引用,很可能有用 level, // 从 0 开始 index, // 它在父节点(或者根数组)中的 index siblings, isFirstChild, isLastChild, parent, // 父树节点(不是数据节点) isShallowLoaded, // 在局部加载数据时会用到 isLeaf, isGroup, ignored, // boolean disabled, // disabled children?, // 它的子树节点(不是原始数据节点) getPrev(), // 方法 getNext(), // 方法 getParent(), // 方法 getChild() // 方法 } ``` ### 在树中勾选节点或取消勾选节点 #### `TreeMate.getCheckedKeys(checkedKeys, options?)` 获取树的勾选状态。 `disabled = true` 的节点会阻止关联勾选的检查传播。 参数 `checkedKeys` 有两种形式: ```ts Key[] // 1. 当前勾选的节点 // 2. 当前的综合勾选状态 interface InputMergedKeys { checkedKeys?: Key[] | null indeterminateKeys?: Key[] | null // half checked } // 也可以是 null | undefined // 会被当作空数组看待 ``` 选项 `options` 形如: ```ts interface CheckOptions { cascade?: boolean // 是否关联勾选,子级勾选会影响父级状态,默认为 true leafOnly?: boolean // 是否只允许叶节点被勾选,默认为 false checkStrategy?: string // 勾选显示策略 'all' | 'parent' | 'child' allowNodeLoaded?: boolean // 是否在允许在不加载完全相关节点的情况下进行关联勾选 } ``` 返回值形如: ```ts interface MergedKeys { checkedKeys: Key[] indeterminateKeys: Key[] // 半选 } ``` ##### 使用举例 ```ts const { checkedKeys, indeterminateKeys } = treeMate.getCheckedKeys([1]) const { checkedKeys, indeterminateKeys } = treeMate.getCheckedKeys([1], { cascade: false }) const { checkedKeys, indeterminateKeys } = treeMate.getCheckedKeys({ checkedKeys: [1], indeterminateKeys: [2] }) // ... ``` #### `TreeMate.check(keysToCheck, checkedKeys, options?)` 获取树在勾选一些节点后新的勾选状态。 `keysToCheck` 可以为 `Key | Key[] | null | undefined`。 `checkedKeys`,`options` 和返回值参考 `getCheckedKeys(checkedKeys, options?)`。 #### `TreeMate.uncheck(keysToUncheck, checkedKeys, options?)` 获取树在取消勾选一些节点后新的勾选状态。 `keysToCheck` 可以为 `Key | Key[] | null | undefined`。 `checkedKeys`, `options` 和返回值参考 `getCheckedKeys(checkedKeys, options?)`。 ### 在树中移动 #### `TreeMate.getPrev(key, options?)` 获取该 `key` 对应节点的前一个非 `disabled` 的 `TreeMateNode`,寻找过程中 `group | ignored` 节点自身会被忽略,不存在时返回 `null`。 `options` 形如 `{ loop?: boolean }`,默认 `loop` 为 `false`,不会循环寻找。 #### `TreeMate.getNext(key, options?)` 获取该 `key` 对应节点的后一个非 `disabled` 的 `TreeMateNode`,寻找过程中 `group | ignored` 节点自身会被忽略,不存在时返回 `null`。 `options` 形如 `{ loop?: boolean }`,默认 `loop` 为 `false`,不会循环寻找。 #### `TreeMate.getParent(key)` 获取该 `key` 对应节点的父级 `TreeMateNode`,寻找过程中 `group` 节点自身会被忽略,不存在时返回 `null`。 #### `TreeMate.getChild(key)` 获取该 `key` 对应节点第一个非 `disabled` 的 `TreeMateNode`,寻找过程中 `group | ignored` 节点自身会被忽略,不存在时返回 `null`。 #### `TreeMateNode.getPrev(options?)` 获取该节点的前一个非 `disabled` 的 `TreeMateNode`,寻找过程中 `group | ignored` 节点自身会被忽略,不存在时返回 `null`。 `options` 形如 `{ loop?: boolean }`,默认 `loop` 为 `false`,不会循环寻找。 #### `TreeMateNode.getNext(options?)` 获取该节点的后一个非 `disabled` 的 `TreeMateNode`,寻找过程中 `group | ignored` 节点自身会被忽略,不存在时返回 `null`。 `options` 形如 `{ loop?: boolean }`,默认 `loop` 为 `false`,不会循环寻找。 #### `TreeMateNode.getParent()` 获取该节点的父级 `TreeMateNode`,寻找过程中 `group` 节点自身会被忽略,不存在时返回 `null`。 #### `TreeMateNode.getChild()` 获取该节点第一个非 `disabled` 的 `TreeMateNode`,寻找过程中 `group | ignored` 节点自身会被忽略,不存在时返回 `null`。 ### 展开、折叠树节点 展开状态会影响树的展平状态。展平节点对于虚拟列表至关重要。 #### `TreeMate.getFlattenedNodes(expandedKeys?)` 获取树对应于 `expandedKeys` 的展平节点。如果 `expandedKeys` 没有传入,treemate 会当作所有节点全部处于展开状态。 #### `createIndexGetter(flattenedNodes)` 从展平节点创建一个索引的 getter 函数。 ```ts import { createIndexGetter } from 'treemate' const getIndex = createIndexGetter(flattenedNodes) getIndex(flattenedNodes[0].key) === 0 ``` ### 获取节点的路径 #### `TreeMate.getPath(key, options)` 获取从根到该 `key` 对应节点的路径。返回值形如 ```ts interface MergedPath { keyPath: Key[] treeNodePath: TreeMateNode[] treeNode: TreeMateNode | null } ``` 其中 `keyPath` 为路径中各个节点的 `key`。其中 `treeNodePath` 为节点路径。`treeNode` 该 `key` 对应的 `TreeMateNode`。 `options` 形如 `{ includeSelf?: boolean, includeGroup?: boolean }`,默认 `includeSelf` 为 true,`includeGroup` 为 false。 ### 获取树第一个可用的节点 可以用于获取选择菜单的默认选项。 #### `TreeMate.getFirstAvailableNode()` 获取整个树第一个非 `disabled` 的 `TreeMateNode`,寻找过程中 `group | ignored` 节点自身会被忽略,不存在时返回 `null`。 ### TreeMate 实例的其他属性 #### `TreeMate.treeNodes` 原数据对应的 `TreeMateNode` 数组,结构完全对应于原数据。 #### `TreeMate.treeNodeMap` `key` 到节点的 Map。包含全部节点,`group | ignored` 节点包含在内。