refactor: tree

pull/4577/head
tangjinzhou 2021-08-20 14:29:18 +08:00
parent 99f034deac
commit ae36627763
6 changed files with 4 additions and 659 deletions

View File

@ -1,251 +0,0 @@
import type { VNode } from 'vue';
import { defineComponent, inject } from 'vue';
import omit from 'omit.js';
import debounce from 'lodash-es/debounce';
import FolderOpenOutlined from '@ant-design/icons-vue/FolderOpenOutlined';
import FolderOutlined from '@ant-design/icons-vue/FolderOutlined';
import FileOutlined from '@ant-design/icons-vue/FileOutlined';
import PropTypes from '../_util/vue-types';
import classNames from '../_util/classNames';
import { conductExpandParent, convertTreeToEntities } from '../vc-tree/src/util';
import type { CheckEvent, ExpendEvent, SelectEvent } from './Tree';
import Tree, { TreeProps } from './Tree';
import {
calcRangeKeys,
getFullKeyList,
convertDirectoryKeysToNodes,
getFullKeyListByTreeData,
} from './util';
import BaseMixin from '../_util/BaseMixin';
import { getOptionProps, getComponent, getSlot } from '../_util/props-util';
import initDefaultProps from '../_util/props-util/initDefaultProps';
import { defaultConfigProvider } from '../config-provider';
// export type ExpandAction = false | 'click' | 'dblclick'; export interface
// DirectoryTreeProps extends TreeProps { expandAction?: ExpandAction; }
// export interface DirectoryTreeState { expandedKeys?: string[];
// selectedKeys?: string[]; }
export interface DirectoryTreeState {
_expandedKeys?: (string | number)[];
_selectedKeys?: (string | number)[];
}
function getIcon(props: { isLeaf: boolean; expanded: boolean } & VNode) {
const { isLeaf, expanded } = props;
if (isLeaf) {
return <FileOutlined />;
}
return expanded ? <FolderOpenOutlined /> : <FolderOutlined />;
}
export default defineComponent({
name: 'ADirectoryTree',
mixins: [BaseMixin],
inheritAttrs: false,
props: initDefaultProps(
{
...TreeProps(),
expandAction: PropTypes.oneOf([false, 'click', 'doubleclick', 'dblclick']),
},
{
showIcon: true,
expandAction: 'click',
},
),
setup() {
return {
children: null,
onDebounceExpand: null,
tree: null,
lastSelectedKey: '',
cachedSelectedKeys: [],
configProvider: inject('configProvider', defaultConfigProvider),
};
},
data() {
const props = getOptionProps(this);
const { defaultExpandAll, defaultExpandParent, expandedKeys, defaultExpandedKeys } = props;
const children = getSlot(this);
const { keyEntities } = convertTreeToEntities(children);
const state: DirectoryTreeState = {};
// Selected keys
state._selectedKeys = props.selectedKeys || props.defaultSelectedKeys || [];
// Expanded keys
if (defaultExpandAll) {
if (props.treeData) {
state._expandedKeys = getFullKeyListByTreeData(props.treeData, props.replaceFields);
} else {
state._expandedKeys = getFullKeyList(children);
}
} else if (defaultExpandParent) {
state._expandedKeys = conductExpandParent(expandedKeys || defaultExpandedKeys, keyEntities);
} else {
state._expandedKeys = expandedKeys || defaultExpandedKeys;
}
return {
_selectedKeys: [],
_expandedKeys: [],
...state,
};
},
watch: {
expandedKeys(val) {
this.setState({ _expandedKeys: val });
},
selectedKeys(val) {
this.setState({ _selectedKeys: val });
},
},
created() {
this.onDebounceExpand = debounce(this.expandFolderNode, 200, { leading: true });
},
methods: {
handleExpand(expandedKeys: (string | number)[], info: ExpendEvent) {
this.setUncontrolledState({ _expandedKeys: expandedKeys });
this.$emit('update:expandedKeys', expandedKeys);
this.$emit('expand', expandedKeys, info);
return undefined;
},
handleClick(event: MouseEvent, node: VNode) {
const { expandAction } = this.$props;
// Expand the tree
if (expandAction === 'click') {
this.onDebounceExpand(event, node);
}
this.$emit('click', event, node);
},
handleDoubleClick(event: MouseEvent, node: VNode) {
const { expandAction } = this.$props;
// Expand the tree
if (expandAction === 'dblclick' || expandAction === 'doubleclick') {
this.onDebounceExpand(event, node);
}
this.$emit('doubleclick', event, node);
this.$emit('dblclick', event, node);
},
hanldeSelect(keys: (string | number)[], event: SelectEvent) {
const { multiple } = this.$props;
const children = this.children || [];
const { _expandedKeys: expandedKeys = [] } = this.$data;
const { node, nativeEvent } = event;
const { eventKey = '' } = node;
const newState: DirectoryTreeState = {};
// We need wrap this event since some value is not same
const newEvent = {
...event,
selected: true, // Directory selected always true
};
// Windows / Mac single pick
const ctrlPick = nativeEvent.ctrlKey || nativeEvent.metaKey;
const shiftPick = nativeEvent.shiftKey;
// Generate new selected keys
let newSelectedKeys: (string | number)[];
if (multiple && ctrlPick) {
// Control click
newSelectedKeys = keys;
this.lastSelectedKey = eventKey;
this.cachedSelectedKeys = newSelectedKeys;
newEvent.selectedNodes = convertDirectoryKeysToNodes(children, newSelectedKeys);
} else if (multiple && shiftPick) {
// Shift click
newSelectedKeys = Array.from(
new Set([
...(this.cachedSelectedKeys || []),
...calcRangeKeys(children, expandedKeys, eventKey, this.lastSelectedKey),
]),
);
newEvent.selectedNodes = convertDirectoryKeysToNodes(children, newSelectedKeys);
} else {
// Single click
newSelectedKeys = [eventKey];
this.lastSelectedKey = eventKey;
this.cachedSelectedKeys = newSelectedKeys;
newEvent.selectedNodes = [event.node];
}
newState._selectedKeys = newSelectedKeys;
this.$emit('update:selectedKeys', newSelectedKeys);
this.$emit('select', newSelectedKeys, newEvent);
this.setUncontrolledState(newState);
},
setTreeRef(node: VNode) {
this.tree = node;
},
expandFolderNode(event: MouseEvent, node: { isLeaf: boolean } & VNode) {
const { isLeaf } = node;
if (isLeaf || event.shiftKey || event.metaKey || event.ctrlKey) {
return;
}
if (this.tree.tree) {
// Get internal vc-tree
const internalTree = this.tree.tree;
// Call internal rc-tree expand function
// https://github.com/ant-design/ant-design/issues/12567
internalTree.onNodeExpand(event, node);
}
},
setUncontrolledState(state: unknown) {
const newState = omit(
state,
Object.keys(getOptionProps(this)).map(p => `_${p}`),
);
if (Object.keys(newState).length) {
this.setState(newState);
}
},
handleCheck(checkedObj: (string | number)[], eventObj: CheckEvent) {
this.$emit('update:checkedKeys', checkedObj);
this.$emit('check', checkedObj, eventObj);
},
},
render() {
this.children = getSlot(this);
const { prefixCls: customizePrefixCls, ...props } = getOptionProps(this);
const getPrefixCls = this.configProvider.getPrefixCls;
const prefixCls = getPrefixCls('tree', customizePrefixCls);
const { _expandedKeys: expandedKeys, _selectedKeys: selectedKeys } = this.$data;
const { class: className, ...restAttrs } = this.$attrs;
const connectClassName = classNames(`${prefixCls}-directory`, className);
const treeProps = {
icon: getIcon,
...restAttrs,
...omit(props, ['onUpdate:selectedKeys', 'onUpdate:checkedKeys', 'onUpdate:expandedKeys']),
prefixCls,
expandedKeys,
selectedKeys,
switcherIcon: getComponent(this, 'switcherIcon'),
ref: this.setTreeRef,
class: connectClassName,
onSelect: this.hanldeSelect,
onClick: this.handleClick,
onDblclick: this.handleDoubleClick,
onExpand: this.handleExpand,
onCheck: this.handleCheck,
};
return (
<Tree {...treeProps} v-slots={omit(this.$slots, ['default'])}>
{this.children}
</Tree>
);
},
});

View File

@ -1,289 +0,0 @@
import type { VNode, PropType, CSSProperties } from 'vue';
import { defineComponent, inject } from 'vue';
import classNames from '../_util/classNames';
import LoadingOutlined from '@ant-design/icons-vue/LoadingOutlined';
import FileOutlined from '@ant-design/icons-vue/FileOutlined';
import CaretDownFilled from '@ant-design/icons-vue/CaretDownFilled';
import MinusSquareOutlined from '@ant-design/icons-vue/MinusSquareOutlined';
import PlusSquareOutlined from '@ant-design/icons-vue/PlusSquareOutlined';
import VcTree, { TreeNode } from '../vc-tree';
import animation from '../_util/openAnimation';
import PropTypes from '../_util/vue-types';
import { getOptionProps, getComponent, getSlot } from '../_util/props-util';
import initDefaultProps from '../_util/props-util/initDefaultProps';
import { cloneElement } from '../_util/vnode';
import { defaultConfigProvider } from '../config-provider';
export interface TreeDataItem {
key?: string | number;
title?: string;
isLeaf?: boolean;
selectable?: boolean;
children?: TreeDataItem[];
disableCheckbox?: boolean;
disabled?: boolean;
class?: string;
style?: CSSProperties;
checkable?: boolean;
icon?: VNode;
slots?: Record<string, string>;
switcherIcon?: VNode;
// support custom field
[key: string]: any;
}
interface DefaultEvent {
nativeEvent: MouseEvent;
node: Record<string, any>;
}
export interface CheckEvent extends DefaultEvent {
checked: boolean;
checkedNodes: Array<Record<string, any>>;
checkedNodesPositions: { node: Record<string, any>; pos: string | number }[];
event: string;
halfCheckedKeys: (string | number)[];
}
export interface ExpendEvent extends DefaultEvent {
expanded: boolean;
}
export interface SelectEvent extends DefaultEvent {
event: string;
selected: boolean;
selectedNodes: Array<Record<string, any>>;
}
export interface TreeDragEvent {
event: DragEvent;
expandedKeys: (string | number)[];
node: Record<string, any>;
}
export interface DropEvent {
dragNode: Record<string, any>;
dragNodesKeys: (string | number)[];
dropPosition: number;
dropToGap: boolean;
event: DragEvent;
node: Record<string, any>;
}
function TreeProps() {
return {
showLine: PropTypes.looseBool,
/** 是否支持多选 */
multiple: PropTypes.looseBool,
/** 是否自动展开父节点 */
autoExpandParent: PropTypes.looseBool,
/** checkable状态下节点选择完全受控父子节点选中状态不再关联*/
checkStrictly: PropTypes.looseBool,
/** 是否支持选中 */
checkable: PropTypes.looseBool,
/** 是否禁用树 */
disabled: PropTypes.looseBool,
/** 默认展开所有树节点 */
defaultExpandAll: PropTypes.looseBool,
/** 默认展开对应树节点 */
defaultExpandParent: PropTypes.looseBool,
/** 默认展开指定的树节点 */
defaultExpandedKeys: PropTypes.arrayOf(
PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
),
/** (受控)展开指定的树节点 */
expandedKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),
/** (受控)选中复选框的树节点 */
checkedKeys: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),
PropTypes.shape({
checked: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),
halfChecked: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),
}).loose,
]),
/** 默认选中复选框的树节点 */
defaultCheckedKeys: PropTypes.arrayOf(
PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
),
/** (受控)设置选中的树节点 */
selectedKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),
/** 默认选中的树节点 */
defaultSelectedKeys: PropTypes.arrayOf(
PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
),
selectable: PropTypes.looseBool,
/** filter some AntTreeNodes as you need. it should return true */
filterAntTreeNode: PropTypes.func,
/** 异步加载数据 */
loadData: PropTypes.func,
loadedKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),
// onLoaded: (loadedKeys: string[], info: { event: 'load', node: AntTreeNode; }) => void,
/** 响应右键点击 */
// onRightClick: (options: AntTreeNodeMouseEvent) => void,
/** 设置节点可拖拽IE>8*/
draggable: PropTypes.looseBool,
// /** */
// onDragStart: (options: AntTreeNodeMouseEvent) => void,
// /** dragenter */
// onDragEnter: (options: AntTreeNodeMouseEvent) => void,
// /** dragover */
// onDragOver: (options: AntTreeNodeMouseEvent) => void,
// /** dragleave */
// onDragLeave: (options: AntTreeNodeMouseEvent) => void,
// /** drop */
// onDrop: (options: AntTreeNodeMouseEvent) => void,
showIcon: PropTypes.looseBool,
icon: PropTypes.func,
switcherIcon: PropTypes.any,
prefixCls: PropTypes.string,
filterTreeNode: PropTypes.func,
openAnimation: PropTypes.any,
treeData: {
type: Array as PropType<TreeDataItem[]>,
},
/**
* @default{title,key,children}
* 替换treeNode中 title,key,children字段为treeData中对应的字段
*/
replaceFields: PropTypes.object,
blockNode: PropTypes.looseBool,
/** 展开/收起节点时触发 */
onExpand: PropTypes.func,
/** 点击复选框触发 */
onCheck: PropTypes.func,
/** 点击树节点触发 */
onSelect: PropTypes.func,
/** 单击树节点触发 */
onClick: PropTypes.func,
/** 双击树节点触发 */
onDoubleclick: PropTypes.func,
onDblclick: PropTypes.func,
'onUpdate:selectedKeys': PropTypes.func,
'onUpdate:checkedKeys': PropTypes.func,
'onUpdate:expandedKeys': PropTypes.func,
};
}
export { TreeProps };
export default defineComponent({
name: 'ATree',
inheritAttrs: false,
props: initDefaultProps(TreeProps(), {
checkable: false,
showIcon: false,
openAnimation: {
...animation,
appear: null,
},
blockNode: false,
}),
setup() {
return {
tree: null,
configProvider: inject('configProvider', defaultConfigProvider),
};
},
TreeNode,
methods: {
renderSwitcherIcon(prefixCls: string, switcherIcon: VNode, { isLeaf, loading, expanded }) {
const { showLine } = this.$props;
if (loading) {
return <LoadingOutlined class={`${prefixCls}-switcher-loading-icon`} />;
}
if (isLeaf) {
return showLine ? <FileOutlined class={`${prefixCls}-switcher-line-icon`} /> : null;
}
const switcherCls = `${prefixCls}-switcher-icon`;
if (switcherIcon) {
return cloneElement(switcherIcon, {
class: switcherCls,
});
}
return showLine ? (
expanded ? (
<MinusSquareOutlined class={`${prefixCls}-switcher-line-icon`} />
) : (
<PlusSquareOutlined class={`${prefixCls}-switcher-line-icon`} />
)
) : (
<CaretDownFilled class={switcherCls} />
);
},
updateTreeData(treeData: TreeDataItem[]) {
const { $slots } = this;
const defaultFields = { children: 'children', title: 'title', key: 'key' };
const replaceFields = { ...defaultFields, ...this.$props.replaceFields };
return treeData.map(item => {
const key = item[replaceFields.key];
const children = item[replaceFields.children];
const { slots = {}, class: cls, style, ...restProps } = item;
const treeNodeProps = {
...restProps,
icon: $slots[slots.icon] || restProps.icon,
switcherIcon: $slots[slots.switcherIcon] || restProps.switcherIcon,
title: $slots[slots.title] || $slots.title || restProps[replaceFields.title],
dataRef: item,
key,
class: cls,
style,
};
if (children) {
return { ...treeNodeProps, children: this.updateTreeData(children) };
}
return treeNodeProps;
});
},
setTreeRef(node: VNode) {
this.tree = node;
},
handleCheck(checkedObj: (number | string)[], eventObj: CheckEvent) {
this.$emit('update:checkedKeys', checkedObj);
this.$emit('check', checkedObj, eventObj);
},
handleExpand(expandedKeys: (number | string)[], eventObj: ExpendEvent) {
this.$emit('update:expandedKeys', expandedKeys);
this.$emit('expand', expandedKeys, eventObj);
},
handleSelect(selectedKeys: (number | string)[], eventObj: SelectEvent) {
this.$emit('update:selectedKeys', selectedKeys);
this.$emit('select', selectedKeys, eventObj);
},
},
render() {
const props = getOptionProps(this);
const { prefixCls: customizePrefixCls, showIcon, treeNodes, blockNode } = props;
const getPrefixCls = this.configProvider.getPrefixCls;
const prefixCls = getPrefixCls('tree', customizePrefixCls);
const switcherIcon = getComponent(this, 'switcherIcon');
const checkable = props.checkable;
let treeData = props.treeData || treeNodes;
if (treeData) {
treeData = this.updateTreeData(treeData);
}
const { class: className, ...restAttrs } = this.$attrs;
const vcTreeProps = {
...props,
prefixCls,
checkable: checkable ? <span class={`${prefixCls}-checkbox-inner`} /> : checkable,
children: getSlot(this),
switcherIcon: nodeProps => this.renderSwitcherIcon(prefixCls, switcherIcon, nodeProps),
ref: this.setTreeRef,
...restAttrs,
class: classNames(className, {
[`${prefixCls}-icon-hide`]: !showIcon,
[`${prefixCls}-block-node`]: blockNode,
}),
onCheck: this.handleCheck,
onExpand: this.handleExpand,
onSelect: this.handleSelect,
} as Record<string, any>;
if (treeData) {
vcTreeProps.treeData = treeData;
}
return <VcTree {...vcTreeProps} />;
},
});

View File

@ -1,110 +0,0 @@
import type { VNode } from 'vue';
import { getNodeChildren, convertTreeToEntities } from '../vc-tree/src/util';
import { getSlot } from '../_util/props-util';
import type { TreeDataItem } from './Tree';
enum Record {
None,
Start,
End,
}
type TreeKey = string | number;
// TODO: Move this logic into `rc-tree`
function traverseNodesKey(rootChildren: VNode[], callback?: Function) {
const nodeList = getNodeChildren(rootChildren) || [];
function processNode(node: VNode) {
const { key } = node;
const children = getSlot(node);
if (callback(key, node) !== false) {
traverseNodesKey(children, callback);
}
}
nodeList.forEach(processNode);
}
export function getFullKeyList(children: VNode[]) {
const { keyEntities } = convertTreeToEntities(children);
return [...keyEntities.keys()];
}
/** 计算选中范围只考虑expanded情况以优化性能 */
export function calcRangeKeys(
rootChildren: VNode[],
expandedKeys: TreeKey[],
startKey: TreeKey,
endKey: TreeKey,
) {
const keys = [];
let record = Record.None;
if (startKey && startKey === endKey) {
return [startKey];
}
if (!startKey || !endKey) {
return [];
}
function matchKey(key: TreeKey) {
return key === startKey || key === endKey;
}
traverseNodesKey(rootChildren, (key: TreeKey) => {
if (record === Record.End) {
return false;
}
if (matchKey(key)) {
// Match test
keys.push(key);
if (record === Record.None) {
record = Record.Start;
} else if (record === Record.Start) {
record = Record.End;
return false;
}
} else if (record === Record.Start) {
// Append selection
keys.push(key);
}
if (expandedKeys.indexOf(key) === -1) {
return false;
}
return true;
});
return keys;
}
export function convertDirectoryKeysToNodes(rootChildren: VNode[], keys: TreeKey[]) {
const restKeys = [...keys];
const nodes = [];
traverseNodesKey(rootChildren, (key: TreeKey, node: VNode) => {
const index = restKeys.indexOf(key);
if (index !== -1) {
nodes.push(node);
restKeys.splice(index, 1);
}
return !!restKeys.length;
});
return nodes;
}
export function getFullKeyListByTreeData(treeData: TreeDataItem[], replaceFields: any = {}) {
let keys = [];
const { key = 'key', children = 'children' } = replaceFields;
(treeData || []).forEach((item: TreeDataItem) => {
keys.push(item[key]);
if (item[children]) {
keys = [...keys, ...getFullKeyListByTreeData(item[children], replaceFields)];
}
});
return keys;
}

View File

@ -3,7 +3,7 @@ import type { FlattenNode } from './interface';
import type { TreeNodeRequiredProps } from './utils/treeUtil';
import { getTreeNodeProps } from './utils/treeUtil';
import { useInjectTreeContext } from './contextTypes';
import { computed, getCurrentInstance, nextTick, PropType } from 'vue';
import { computed, nextTick, PropType } from 'vue';
import { defineComponent, onBeforeUnmount, onMounted, ref, Transition, watch } from 'vue';
import { treeNodeProps } from './props';
import { collapseMotion } from '../_util/transition';

View File

@ -1,4 +1,4 @@
import type { ComputedRef, CSSProperties, DefineComponent, Ref, VNode } from 'vue';
import type { ComputedRef, CSSProperties, Ref, VNode } from 'vue';
import { TreeNodeProps } from './props';
export type { ScrollTo } from '../vc-virtual-list/List';
@ -40,11 +40,7 @@ export type IconType = any;
export type Key = string | number;
export type NodeElement = VNode<DefineComponent<TreeNodeProps>> & {
type: {
isTreeNode: boolean;
};
};
export type NodeElement = VNode<TreeNodeProps>;
export type DragNodeEvent = {
eventData: ComputedRef<EventDataNode>;

View File

@ -42,7 +42,7 @@ export function getPosition(level: string | number, index: number) {
}
export function isTreeNode(node: NodeElement) {
return node && node.type && node.type.isTreeNode;
return node && node.type && (node.type as any).isTreeNode;
}
export function getDragChildrenKeys(dragNodeKey: Key, keyEntities: Record<Key, DataEntity>): Key[] {
@ -272,7 +272,6 @@ export function convertDataToTree(
const list = Array.isArray(treeData) ? treeData : [treeData];
return list.map(({ children, ...props }): NodeElement => {
const childrenNodes = convertDataToTree(children, processor);
return <TreeNode {...processProps(props)}>{childrenNodes}</TreeNode>;
});
}