Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions packages/devui-vue/devui/tree/src/core/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Tree Factory

Tree 组件最核心的部分,是 UI 无关的,用于树形结构的处理。

- 获取 / 设置整棵树
- 获取子节点
- 搜索节点
- 插入 / 移除 / 编辑节点
- 点选 / 勾选 / 展开收起节点
- 点选 / 勾选 / 展开收起节点功能的禁用

## 快速开始

```ts
const treeData = [
{
label: 'Parent node 1',
children: [
{ label: 'Leaf node 1-1' }
]
},
{ label: 'Leaf node 2' }
];

const treeFactory = new TreeFactory(treeData);
treeFactory.getTree();
```

## API

| 名称 | 描述 |
| -- | -- |
| getTree() => ITreeNode[] | 获取整棵树 |
| setTree(tree: ITreeNode[]) => void | 设置整棵树 |
| getLevel(node: ITreeNode) => number | 获取节点层级 |
| getChildren(node: ITreeNode): ITreeNode[] | 获取子节点 |
| selectNode(node: ITreeNode): void | 点击选择 |
| checkNode(node: ITreeNode): void | 勾选 |
| uncheckNode(node: ITreeNode): void | 取消勾选 |
| expandNode(node: ITreeNode): void | 展开 |
| collapseNode(node: ITreeNode): void | 收起 |
| toggleNode(node: ITreeNode): void | 切换展开/收起状态 |
| disableSelectNode(node: ITreeNode): void | 禁用点击选择 |
| disableCheckNode(node: ITreeNode): void | 禁用勾选 |
| disableToggleNode(node: ITreeNode): void | 禁用展开/收起 |
| insertBefore(parentNode: ITreeNode, node: ITreeNode, referenceNode: ITreeNode, cut: boolean = false): void | 插入节点 |
| removeNode(node: ITreeNode): void | 移除节点 |
| editNode(node: ITreeNode, label: string): void | 编辑节点内容 |

## ITreeNode

```ts
interface ITreeNode {
label: string;
id?: string;
children?: ITreeNode[];

selected?: boolean;
checked?: boolean;
expanded?: boolean;

disableSelect?: boolean;
disableCheck?: boolean;
disableToggle?: boolean;
}
```
23 changes: 23 additions & 0 deletions packages/devui-vue/devui/tree/src/core/tree-factory-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// 外部数据结构先只考虑嵌套结构
export interface ITreeNode {
label: string;
id?: string;
children?: ITreeNode[];

selected?: boolean;
checked?: boolean;
expanded?: boolean;

disableSelect?: boolean;
disableCheck?: boolean;
disableToggle?: boolean;
}

// 内部数据结构使用扁平结构
export interface IInnerTreeNode extends ITreeNode {
level: number;
idType?: 'random';
parentId?: string;
}

export type valueof<T> = T[keyof T];
99 changes: 99 additions & 0 deletions packages/devui-vue/devui/tree/src/core/tree-factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import type { IInnerTreeNode, ITreeNode } from './tree-factory-types';
import { flatToNested, generateInnerTree } from './utils';

export default class TreeFactory {
private _innerTree: IInnerTreeNode[] = [];

private _getIndex(node: ITreeNode): number {
return this._innerTree.findIndex((item) => item.id === node.id);
}

private _setNodeValue(node, key, value): void {
this._innerTree[this._getIndex(node)][key] = value;
}

constructor(tree: ITreeNode[]) {
this.setTree(tree);
}

getTree(flat?: boolean = false): IInnerTreeNode[] {
if (flat) {
return this._innerTree;
} else {
// TODO: 移除内部属性(level / parentId / idType) / 内部生成的id / 空children
return flatToNested(this._innerTree);
}
}

setTree(tree: ITreeNode[]) {
this._innerTree = generateInnerTree(tree);
}

getLevel(node: ITreeNode): number {
return this._innerTree.find((item) => item.id === node.id).level;
}

getChildren(node: ITreeNode): IInnerTreeNode[] {
let result = [];
const startIndex = this._innerTree.findIndex((item) => item.id === node.id);

for (let i = startIndex + 1; i < this._innerTree.length && this.getLevel(node) < this._innerTree[i].level; i++) {
result.push(this._innerTree[i]);
}
return result;
}

selectNode(node: ITreeNode): void {
this._setNodeValue(node, 'selected', true);
}

checkNode(node: ITreeNode): void {
this._setNodeValue(node, 'checked', true);
}

uncheckNode(node: ITreeNode): void {
this._setNodeValue(node, 'checked', false);
}

expandNode(node: ITreeNode): void {
this._setNodeValue(node, 'expanded', true);
}

collapseNode(node: ITreeNode): void {
this._setNodeValue(node, 'expanded', false);
}

toggleNode(node: ITreeNode): void {
if (node.expanded) {
this._setNodeValue(node, 'expanded', false);
} else {
this._setNodeValue(node, 'expanded', true);
}
}

disableSelectNode(node: ITreeNode): void {
this._setNodeValue(node, 'disableSelect', true);
}

disableCheckNode(node: ITreeNode): void {
this._setNodeValue(node, 'disableCheck', true);
}

disableToggleNode(node: ITreeNode): void {
this._setNodeValue(node, 'disableToggle', true);
}

insertBefore(parentNode: ITreeNode, node: ITreeNode, referenceNode: ITreeNode, cut: boolean = false): void {
// TODO
}

removeNode(node: ITreeNode): void {
this._innerTree = this._innerTree.filter(item => {
return item.id !== node.id && !this.getChildren(node).map(nodeItem => nodeItem.id).includes(item.id);
})
}

editNode(node: ITreeNode, label: string): void {
this._setNodeValue(node, 'label', label);
}
}
68 changes: 68 additions & 0 deletions packages/devui-vue/devui/tree/src/core/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { randomId } from '../../../anchor/src/util';
import { IInnerTreeNode, ITreeNode } from './tree-factory-types';

export function flatToNested(flatTree: IInnerTreeNode[]): ITreeNode[] {
let treeMap = {};
return flatTree.reduce((acc: ITreeNode[], cur: IInnerTreeNode) => {
const { id, parentId } = cur;

if (!treeMap[id]) {
treeMap[id] = {
...cur,
children: [],
};
}

if (!treeMap[parentId]) {
acc.push(treeMap[id]);
} else {
treeMap[parentId].children.push(treeMap[id]);
}

return acc;
}, []);
}

export function generateInnerTree(
tree: ITreeNode[],
key = 'children',
level: number = 0,
path: ITreeNode[] = []
): IInnerTreeNode[] {
level++;

return tree.reduce((acc: ITreeNode[], item: ITreeNode) => {
const newItem = Object.assign({}, item);
if (newItem.id === undefined) {
newItem.id = randomId();
newItem.idType = 'random';
}

newItem.level = level;

if (path.length > 0 && path[path.length - 1]?.level >= level) {
while (path[path.length - 1]?.level >= level) {
path.pop();
}
}

path.push(newItem);

const parentNode = path[path.length - 2];
if (parentNode) {
newItem.parentId = parentNode.id;
}

if (!newItem[key]) {
return acc.concat(newItem);
} else {
return acc.concat(omit<ITreeNode>(newItem, 'children'), generateInnerTree(newItem[key], key, level, path));
}
}, []);
}

export function omit<T>(obj: T, ...keys: Array<keyof T>) {
return Object.entries(obj)
.filter((item) => !keys.includes(item[0]))
.reduce((acc, item) => Object.assign({}, acc, { [item[0]]: item[1] }), {});
}