vue-rc-tree
2023年11月11日
一、认识
二、实现
- App.vue
- Tree.tsx
- NodeList.tsx
- TreeNode.tsx
- Indent.tsx
- treeUtil.ts
- treeProps.ts
- contextTree.ts
<template>
<div>
<Tree :treeData="treeData" />
</div>
</template>
<script lang="ts" setup>
import { shallowRef } from 'vue';
import Tree from './components/RcTree/Tree';
const treeData = shallowRef([
{
title: '0-0-title',
key: '001',
children: [
{
title: '0-0-0-title',
key: '002',
children: [
{ title: '0-0-0-0-title', key: '003' },
{ title: '0-0-0-1-title', key: '004' },
{ title: '0-0-0-2-title', key: '005' }
]
},
{
title: '0-0-1-title',
key: '006',
children: [
{ title: '0-0-1-0-title', key: '007' },
{ title: '0-0-1-1-title', key: '008' },
{ title: '0-0-1-2-title', key: '009' }
]
},
{
title: '0-0-2-title',
key: '010'
}
]
},
{
title: '0-1-title',
key: '011',
children: [
{ title: '0-1-0-0', key: '012' },
{ title: '0-1-0-1', key: '013' },
{ title: '0-1-0-2', key: '014' }
]
},
{
title: '0-2-title',
key: '015'
}
]);
</script>
import './tree.scss';
import NodeList from './NodeList';
import { TreeProps } from './treeProps';
import { useProvideKeysState } from './contextTree';
import {
fillFieldNames,
flattenTreeData,
initDefaultProps,
convertDataToEntities,
arrAdd,
arrDel
} from './treeUtil';
import {
watch,
toRaw,
computed,
watchEffect,
shallowRef,
defineComponent
} from 'vue';
export default defineComponent({
name: 'Tree',
props: initDefaultProps(TreeProps(), {
treeData: [],
children: [],
fieldNames: { key: 'key', title: 'title', children: 'children' }
}),
setup(props) {
const treeData = shallowRef([]);
const keyEntities = shallowRef({});
const flattenNodes = shallowRef([]);
const expandedKeys = shallowRef([]);
const fieldNames = computed(() => fillFieldNames(props.fieldNames));
const setExpandedKeys = newExpandedKeys => {
expandedKeys.value = newExpandedKeys;
};
const onNodeExpand = (e, treeNode) => {
const { expanded } = treeNode;
const key = treeNode[fieldNames.value.key];
const targetExpanded = !expanded;
if (targetExpanded) {
setExpandedKeys(arrAdd(expandedKeys.value, key));
} else {
setExpandedKeys(arrDel(expandedKeys.value, key));
}
};
watch(
() => props.treeData,
() => {
treeData.value = toRaw(props.treeData);
},
{
deep: true,
immediate: true
}
);
watchEffect(() => {
if (treeData.value) {
const entitiesMap = convertDataToEntities(treeData.value, {
fieldNames: fieldNames.value
});
keyEntities.value = entitiesMap.keyEntities;
}
});
watchEffect(() => {
flattenNodes.value = flattenTreeData(
treeData.value,
expandedKeys.value,
fieldNames.value
);
});
useProvideKeysState({
expandedKeys,
flattenNodes,
keyEntities,
onNodeExpand
});
return () => {
return (
<div class="tree">
<NodeList />
</div>
);
};
}
});
import TreeNode from './TreeNode';
import { defineComponent } from 'vue';
import { useInjectKeysState } from './contextTree';
import { getKey, getTreeNodeProps } from './treeUtil';
const nodeListProps = {};
export default defineComponent({
name: 'NodeList',
props: nodeListProps,
setup() {
const { flattenNodes, expandedKeys, keyEntities } = useInjectKeysState();
return () => {
return (
<div class="tree-node-list">
{flattenNodes.value.map(treeNode => {
const { key, pos } = treeNode;
const mergedKey = getKey(key, pos);
const treeNodeProps = getTreeNodeProps(mergedKey, {
keyEntities: keyEntities.value,
expandedKeys: expandedKeys.value
});
return <TreeNode key={key} {...treeNode} {...treeNodeProps} />;
})}
</div>
);
};
}
});
import Indent from './Indent';
import { TreeNodeProps } from './treeProps';
import { computed, defineComponent } from 'vue';
import { useInjectKeysState } from './contextTree';
import { convertNodePropsToEventData, getEntity } from './treeUtil';
export default defineComponent({
name: 'TreeNode',
props: TreeNodeProps(),
setup(props) {
const { isEnd, level, title, isStart, eventKey, expanded } = props;
const { keyEntities, onNodeExpand } = useInjectKeysState();
const { children } = getEntity(keyEntities.value, eventKey) || {};
const eventData = computed(() => convertNodePropsToEventData(props));
const hasChildren = computed(() => !!(children || []).length);
const isLeaf = computed(() => !hasChildren.value);
const onExpand = e => {
onNodeExpand(e, eventData.value);
};
const renderSwitcher = () => {
if (isLeaf.value) {
return <span className={`switcher switcher-noop`}></span>;
}
const switcherIconDom = expanded ? '-' : '+';
return (
<span
className={`switcher switcher-${expanded ? 'open' : 'close'}`}
onClick={onExpand}
>
{switcherIconDom}
</span>
);
};
const renderCheckbox = () => {
return null;
};
const renderSelector = () => {
return title;
};
return () => {
return (
<div class="tree-node">
<Indent isEnd={isEnd} level={level} isStart={isStart} />
{renderSwitcher()}
{renderCheckbox()}
{renderSelector()}
</div>
);
};
}
});
import { defineComponent } from 'vue';
import { indentProps } from './treeProps';
export default defineComponent({
name: 'Indent',
props: indentProps(),
setup(props) {
const list = [];
const { level, isEnd, isStart } = props;
for (let i = 0; i < Number(level); i++) {
let className = '';
if (isStart[i]) {
className += `indent-unit-start`;
}
if (isEnd[i]) {
className += `indent-unit-end`;
}
list.push(<span key={i} className={`indent-unit ${className}`}></span>);
}
return () => {
return <div className={`indent`}>{list}</div>;
};
}
});
export function arrAdd(list, value) {
const clone = (list || []).slice();
if (clone.indexOf(value) === -1) {
clone.push(value);
}
return clone;
}
export function arrDel(list, key) {
if (!list) {
return [];
}
const clone = list.slice();
const index = clone.indexOf(key);
if (index >= 0) {
clone.splice(index, 1);
}
return clone;
}
export function getKey(key, pos) {
if (key !== null || key !== undefined) {
return key;
}
return pos;
}
export function getEntity(keyEntities, key) {
return keyEntities[key];
}
export function getPosition(level, index) {
return `${level}-${index}`;
}
export function fillFieldNames(fieldNames) {
const { key, title, children } = fieldNames || {};
return {
key: key || 'key',
title: title || 'title',
children: children || 'children'
};
}
export function initDefaultProps(types, defaultProps) {
const propTypes = { ...types };
Object.keys(defaultProps).forEach(k => {
const prop = propTypes[k];
if (prop) {
if (prop.type || prop.default) {
prop.default = defaultProps[k];
} else if (prop.def) {
prop.def(defaultProps[k]);
} else {
propTypes[k] = { type: prop, default: defaultProps[k] };
}
}
});
return propTypes;
}
function traverseDataNodes(treeData, callback, config) {
let mergedConfig = {};
if (typeof config === 'object') {
mergedConfig = config;
}
const { fieldNames, childrenPropName } = mergedConfig;
const { key: fieldKey, children: fieldChildren } = fillFieldNames(fieldNames);
const mergedChildrenPropName = childrenPropName || fieldChildren;
const processNode = (node, index, parent, pathNodes) => {
const connectNodes = node ? [...pathNodes, node] : [];
const pos = node ? getPosition(parent.pos, index) : '0';
const mergedKey = node ? getKey(node[fieldKey], pos) : '0';
const children = node ? node[mergedChildrenPropName] : treeData;
if (node) {
const data = {
node,
index,
pos,
key: mergedKey,
nodes: connectNodes,
level: parent.level + 1,
parentPos: parent.node ? parent.pos : null
};
callback(data);
}
if (children) {
children.forEach((subNode, subIndex) => {
processNode(
subNode,
subIndex,
{ pos, node, level: parent ? parent.level + 1 : -1 },
connectNodes
);
});
}
};
processNode(null);
}
export function convertDataToEntities(
dataNodes,
{ fieldNames, childrenPropName }
) {
const posEntities = {};
const keyEntities = {};
const wrapper = { posEntities, keyEntities };
traverseDataNodes(
dataNodes,
item => {
const { node, index, pos, key, parentPos, level, nodes } = item;
const entity = {
node,
nodes,
index,
key,
pos,
level
};
posEntities[pos] = entity;
keyEntities[key] = entity;
entity.parent = posEntities[parentPos];
if (entity.parent) {
entity.parent.children = entity.parent.children || [];
entity.parent.children.push(entity);
}
},
{ fieldNames, childrenPropName }
);
return wrapper;
}
export function flattenTreeData(treeNodeList, expandedKeys, fieldNames) {
const {
key: fieldKey,
title: fieldTitle,
children: fieldChildren
} = fillFieldNames(fieldNames);
const flattenList = [];
const expandedKeySet = new Set(expandedKeys === true ? [] : expandedKeys);
const recursion = (list, parent = null) => {
return list.map((treeNode, index) => {
const pos = getPosition(treeNode[fieldKey], index);
const mergedKey = getKey(treeNode[fieldKey], pos);
const flattenNode = {
pos,
parent,
data: treeNode,
key: mergedKey,
children: null,
title: treeNode[fieldTitle],
isStart: [...(parent ? parent.isStart : []), index === 0],
isEnd: [...(parent ? parent.isEnd : []), index === list.length - 1]
};
flattenList.push(flattenNode);
if (expandedKeys === true || expandedKeySet.has(mergedKey)) {
flattenNode[fieldChildren] = recursion(
treeNode[fieldChildren] || [],
flattenNode
);
} else {
flattenNode[fieldChildren] = [];
}
return flattenNode;
});
};
recursion(treeNodeList);
return flattenList;
}
export function getTreeNodeProps(key, { keyEntities, expandedKeys }) {
const entity = getEntity(keyEntities, key);
const treeNodeProps = {
eventKey: key,
level: entity ? entity.level : 0,
pos: String(entity ? entity.pos : ''),
expanded: expandedKeys.indexOf(key) !== -1
};
return treeNodeProps;
}
export function convertNodePropsToEventData(props) {
const { pos, active, eventKey, data, expanded, selected, checked } = props;
const eventData = {
...data,
pos,
data,
active,
eventKey,
checked,
expanded,
selected
};
return eventData;
}
export const TreeProps = () => ({
treeData: {
type: Array
},
children: {
type: Array
},
fieldName: {
type: Object
}
});
export const TreeNodeProps = () => ({
title: String,
level: Number,
expanded: Boolean,
eventKey: [String, Number],
data: {
type: Object,
default: undefined
},
isStart: {
type: Array
},
isEnd: {
type: Array
},
children: Array
});
export const indentProps = () => ({
level: Number,
isEnd: Array,
isStart: Array
});
import { inject, provide, shallowRef } from 'vue';
const KeysStateKey = Symbol('keysStateKey');
export const useProvideKeysState = state => {
provide(KeysStateKey, state);
};
export const useInjectKeysState = () => {
return inject(KeysStateKey, {
keyEntities: shallowRef({}),
flattenNodes: shallowRef([]),
expandedKeys: shallowRef([]),
onNodeExpand: () => {}
});
};