refactor: tree

pull/4577/head
tangjinzhou 2021-08-19 10:56:21 +08:00
parent 5f189f534e
commit 94aa0df8d7
7 changed files with 282 additions and 68 deletions

View File

@ -1,33 +1,21 @@
import type { ExtractPropTypes, PropType, VNode } from 'vue';
import { defineComponent, inject } from 'vue';
import omit from 'omit.js';
import { ExtractPropTypes, nextTick, onUpdated, PropType, ref, watch } from 'vue';
import { defineComponent } from 'vue';
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 { treeProps } from './Tree';
import { AntdTreeNodeAttribute, treeProps } from './Tree';
import Tree, { TreeProps } from './Tree';
import {
calcRangeKeys,
getFullKeyList,
convertDirectoryKeysToNodes,
getFullKeyListByTreeData,
} from './util';
import { getOptionProps, getComponent, getSlot } from '../_util/props-util';
import initDefaultProps from '../_util/props-util/initDefaultProps';
import { defaultConfigProvider } from '../config-provider';
import { convertDataToEntities, convertTreeToData } from '../vc-tree/utils/treeUtil';
import { DataNode, EventDataNode, Key } from '../vc-tree/interface';
import { conductExpandParent } from '../vc-tree/util';
import { calcRangeKeys, convertDirectoryKeysToNodes } from './utils/dictUtil';
import useConfigInject from '../_util/hooks/useConfigInject';
import { filterEmpty } from '../_util/props-util';
export type ExpandAction = false | 'click' | 'doubleClick' | 'dblclick';
function getIcon(props: { isLeaf: boolean; expanded: boolean } & VNode) {
const { isLeaf, expanded } = props;
if (isLeaf) {
return <FileOutlined />;
}
return expanded ? <FolderOpenOutlined /> : <FolderOutlined />;
}
export type ExpandAction = false | 'click' | 'doubleclick' | 'dblclick';
const directoryTreeProps = {
...treeProps(),
@ -36,6 +24,14 @@ const directoryTreeProps = {
export type DirectoryTreeProps = Partial<ExtractPropTypes<typeof directoryTreeProps>>;
function getIcon(props: AntdTreeNodeAttribute) {
const { isLeaf, expanded } = props;
if (isLeaf) {
return <FileOutlined />;
}
return expanded ? <FolderOpenOutlined /> : <FolderOutlined />;
}
export default defineComponent({
name: 'ADirectoryTree',
inheritAttrs: false,
@ -43,9 +39,234 @@ export default defineComponent({
showIcon: true,
expandAction: 'click',
}),
setup() {
slots: ['icon', 'title', 'switcherIcon'],
emits: [
'update:selectedKeys',
'update:checkedKeys',
'update:expandedKeys',
'expand',
'select',
'check',
'doubleclick',
'dblclick',
'click',
],
setup(props, { attrs, slots, emit }) {
// convertTreeToData a-tree-node a-tree-noderender treeData
const treeData = ref<DataNode[]>(
props.treeData || convertTreeToData(filterEmpty(slots.default?.())),
);
watch(
() => props.treeData,
() => {
treeData.value = props.treeData;
},
);
onUpdated(() => {
nextTick(() => {
if (props.treeData === undefined && slots.default) {
treeData.value = convertTreeToData(filterEmpty(slots.default?.()));
}
});
});
// Shift click usage
const lastSelectedKey = ref<Key>();
const cachedSelectedKeys = ref<Key[]>();
const treeRef = ref();
const getInitExpandedKeys = () => {
const { keyEntities } = convertDataToEntities(treeData.value);
let initExpandedKeys: any;
// Expanded keys
if (props.defaultExpandAll) {
initExpandedKeys = Object.keys(keyEntities);
} else if (props.defaultExpandParent) {
initExpandedKeys = conductExpandParent(
props.expandedKeys || props.defaultExpandedKeys,
keyEntities,
);
} else {
initExpandedKeys = props.expandedKeys || props.defaultExpandedKeys;
}
return initExpandedKeys;
};
const selectedKeys = ref(props.selectedKeys || props.defaultSelectedKeys || []);
const expandedKeys = ref<Key[]>(getInitExpandedKeys());
watch(
() => props.selectedKeys,
() => {
if (props.selectedKeys !== undefined) {
selectedKeys.value = props.selectedKeys;
}
},
{ immediate: true },
);
watch(
() => props.expandedKeys,
() => {
if (props.expandedKeys !== undefined) {
expandedKeys.value = props.expandedKeys;
}
},
{ immediate: true },
);
const expandFolderNode = (event: MouseEvent, node: any) => {
const { isLeaf } = node;
if (isLeaf || event.shiftKey || event.metaKey || event.ctrlKey) {
return;
}
// Call internal rc-tree expand function
// https://github.com/ant-design/ant-design/issues/12567
treeRef.value!.onNodeExpand(event as any, node);
};
const onDebounceExpand = debounce(expandFolderNode, 200, {
leading: true,
});
const onExpand = (
keys: Key[],
info: {
node: EventDataNode;
expanded: boolean;
nativeEvent: MouseEvent;
},
) => {
if (props.expandedKeys === undefined) {
expandedKeys.value = keys;
}
// Call origin function
emit('update:expandedKeys', keys);
emit('expand', keys, info);
};
const onClick = (event: MouseEvent, node: EventDataNode) => {
const { expandAction } = props;
// Expand the tree
if (expandAction === 'click') {
onDebounceExpand(event, node);
}
emit('click', event, node);
};
const onDoubleClick = (event: MouseEvent, node: EventDataNode) => {
const { expandAction } = props;
// Expand the tree
if (expandAction === 'dblclick' || expandAction === 'doubleclick') {
onDebounceExpand(event, node);
}
emit('doubleclick', event, node);
emit('dblclick', event, node);
};
const onSelect = (
keys: Key[],
event: {
event: 'select';
selected: boolean;
node: any;
selectedNodes: DataNode[];
nativeEvent: MouseEvent;
},
) => {
const { multiple } = props;
const { node, nativeEvent } = event;
const { key = '' } = node;
// const newState: DirectoryTreeState = {};
// We need wrap this event since some value is not same
const newEvent: any = {
...event,
selected: true, // Directory selected always true
};
// Windows / Mac single pick
const ctrlPick: boolean = nativeEvent.ctrlKey || nativeEvent.metaKey;
const shiftPick: boolean = nativeEvent.shiftKey;
// Generate new selected keys
let newSelectedKeys: Key[];
if (multiple && ctrlPick) {
// Control click
newSelectedKeys = keys;
lastSelectedKey.value = key;
cachedSelectedKeys.value = newSelectedKeys;
newEvent.selectedNodes = convertDirectoryKeysToNodes(treeData.value, newSelectedKeys);
} else if (multiple && shiftPick) {
// Shift click
newSelectedKeys = Array.from(
new Set([
...(cachedSelectedKeys.value || []),
...calcRangeKeys({
treeData: treeData.value,
expandedKeys: expandedKeys.value,
startKey: key,
endKey: lastSelectedKey.value,
}),
]),
);
newEvent.selectedNodes = convertDirectoryKeysToNodes(treeData.value, newSelectedKeys);
} else {
// Single click
newSelectedKeys = [key];
lastSelectedKey.value = key;
cachedSelectedKeys.value = newSelectedKeys;
newEvent.selectedNodes = convertDirectoryKeysToNodes(treeData.value, newSelectedKeys);
}
emit('update:selectedKeys', newSelectedKeys);
emit('select', newSelectedKeys, newEvent);
if (props.selectedKeys === undefined) {
selectedKeys.value = newSelectedKeys;
}
};
const onCheck: TreeProps['onCheck'] = (checkedObjOrKeys, eventObj) => {
emit('update:checkedKeys', checkedObjOrKeys);
emit('check', checkedObjOrKeys, eventObj);
};
const { prefixCls, direction } = useConfigInject('tree', props);
return () => {
return null;
const connectClassName = classNames(
`${prefixCls.value}-directory`,
{
[`${prefixCls.value}-directory-rtl`]: direction.value === 'rtl',
},
attrs.class,
);
const { icon = slots.icon, ...otherProps } = props;
return (
<Tree
{...attrs}
icon={icon || getIcon}
ref={treeRef}
blockNode
{...otherProps}
prefixCls={prefixCls.value}
class={connectClassName}
expandedKeys={expandedKeys.value}
selectedKeys={selectedKeys.value}
onSelect={onSelect}
onClick={onClick}
onDblclick={onDoubleClick}
onExpand={onExpand}
onCheck={onCheck}
v-slots={slots}
/>
);
};
},
});

View File

@ -150,13 +150,18 @@ export default defineComponent({
'expand',
'select',
'check',
'doubleclick',
'dblclick',
],
TreeNode,
setup(props, { attrs, expose, emit, slots }) {
const { prefixCls, direction, virtual } = useConfigInject('tree', props);
const tree = ref();
const treeRef = ref();
expose({
tree,
treeRef,
onNodeExpand: (...args) => {
treeRef.value?.onNodeExpand(...args);
},
});
const handleCheck: TreeProps['onCheck'] = (checkedObjOrKeys, eventObj) => {
@ -197,7 +202,7 @@ export default defineComponent({
itemHeight={20}
virtual={virtual.value}
{...newProps}
ref={tree}
ref={treeRef}
prefixCls={prefixCls.value}
class={classNames(
{

View File

@ -55,7 +55,7 @@ export default defineComponent({
allowDrop: () => true,
}),
setup(props, { attrs, slots }) {
setup(props, { attrs, slots, expose }) {
const destroyed = ref(false);
let delayedDragEnterLogic: Record<Key, number> = {};
const indent = ref();
@ -543,9 +543,9 @@ export default defineComponent({
};
const onNodeDoubleClick: NodeMouseEventHandler = (e, treeNode) => {
const { onDblClick } = props;
if (onDblClick) {
onDblClick(e, treeNode);
const { onDblclick } = props;
if (onDblclick) {
onDblclick(e, treeNode);
}
};
@ -953,7 +953,9 @@ export default defineComponent({
onKeyDown(event);
}
};
expose({
onNodeExpand,
});
onUnmounted(() => {
window.removeEventListener('dragend', onWindowDragEnd);
destroyed.value = true;

View File

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

View File

@ -149,7 +149,7 @@ export const treeProps = () => ({
onKeyDown: { type: Function as PropType<EventHandlerNonNull> },
onContextmenu: { type: Function as PropType<EventHandlerNonNull> },
onClick: { type: Function as PropType<NodeMouseEventHandler> },
onDblClick: { type: Function as PropType<NodeMouseEventHandler> },
onDblclick: { type: Function as PropType<NodeMouseEventHandler> },
onScroll: { type: Function as PropType<EventHandlerNonNull> },
onExpand: {
type: Function as PropType<

View File

@ -64,7 +64,7 @@ export function warningWithoutKey(treeData: DataNode[], fieldNames: FieldNames)
* Convert `children` of Tree into `treeData` structure.
*/
export function convertTreeToData(rootNodes: VNodeChild): DataNode[] {
function dig(node: VNodeChild): DataNode[] {
function dig(node: VNodeChild = []): DataNode[] {
const treeNodes = node as NodeElement[];
return treeNodes
.map(treeNode => {
@ -75,8 +75,8 @@ export function convertTreeToData(rootNodes: VNodeChild): DataNode[] {
}
const key = treeNode.key as string | number;
const { children, ...rest } = treeNode.props;
const { ...rest } = treeNode.props;
const children = (treeNode.children as any)?.default?.();
const dataNode: DataNode = {
...rest,
key,

View File

@ -1,45 +1,30 @@
<template>
<a-tree
<a-directory-tree
v-model:expandedKeys="expandedKeys"
v-model:selectedKeys="selectedKeys"
:load-data="onLoadData"
:tree-data="treeData"
/>
multiple
@test="onTest"
>
<a-tree-node key="0-0" title="parent 0">
<a-tree-node key="0-0-0" title="leaf 0-0" is-leaf />
<a-tree-node key="0-0-1" title="leaf 0-1" is-leaf />
</a-tree-node>
<a-tree-node key="0-1" title="parent 1">
<a-tree-node key="0-1-0" title="leaf 1-0" is-leaf />
<a-tree-node key="0-1-1" title="leaf 1-1" is-leaf />
</a-tree-node>
</a-directory-tree>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import { TreeDataItem } from 'ant-design-vue/es/tree/Tree';
export default defineComponent({
setup() {
const expandedKeys = ref<string[]>([]);
const expandedKeys = ref<string[]>(['0-0', '0-1']);
const selectedKeys = ref<string[]>([]);
const treeData = ref<TreeDataItem[]>([
{ title: 'Expand to load', key: '0' },
{ title: 'Expand to load', key: '1' },
{ title: 'Tree Node', key: '2', isLeaf: true },
]);
const onLoadData = (treeNode: any) => {
return new Promise((resolve: (value?: unknown) => void) => {
if (treeNode.dataRef.children) {
resolve();
return;
}
setTimeout(() => {
treeNode.dataRef.children = [
{ title: 'Child Node', key: `${treeNode.eventKey}-0` },
{ title: 'Child Node', key: `${treeNode.eventKey}-1` },
];
treeData.value = [...treeData.value];
resolve();
}, 1000);
});
};
return {
expandedKeys,
selectedKeys,
treeData,
onLoadData,
onTest: () => {},
};
},
});