refactor: directory tree

pull/4577/head
tangjinzhou 2021-08-19 23:29:07 +08:00
parent 94aa0df8d7
commit 99f034deac
10 changed files with 197 additions and 33 deletions

View File

@ -0,0 +1,89 @@
// copy from https://github.dev/vueuse/vueuse
import type { Ref, WatchOptions, WatchStopHandle } from 'vue';
import { unref, watch } from 'vue';
type MaybeRef<T> = T | Ref<T>;
type Fn = () => void;
export type FunctionArgs<Args extends any[] = any[], Return = void> = (...args: Args) => Return;
export interface FunctionWrapperOptions<Args extends any[] = any[], This = any> {
fn: FunctionArgs<Args, This>;
args: Args;
thisArg: This;
}
export type EventFilter<Args extends any[] = any[], This = any> = (
invoke: Fn,
options: FunctionWrapperOptions<Args, This>,
) => void;
const bypassFilter: EventFilter = invoke => {
return invoke();
};
/**
* Create an EventFilter that debounce the events
*
* @param ms
*/
export function debounceFilter(ms: MaybeRef<number>) {
let timer: ReturnType<typeof setTimeout> | undefined;
const filter: EventFilter = invoke => {
const duration = unref(ms);
if (timer) clearTimeout(timer);
if (duration <= 0) return invoke();
timer = setTimeout(invoke, duration);
};
return filter;
}
export interface DebouncedWatchOptions<Immediate> extends WatchOptions<Immediate> {
debounce?: MaybeRef<number>;
}
interface ConfigurableEventFilter {
eventFilter?: EventFilter;
}
/**
* @internal
*/
function createFilterWrapper<T extends FunctionArgs>(filter: EventFilter, fn: T) {
function wrapper(this: any, ...args: any[]) {
filter(() => fn.apply(this, args), { fn, thisArg: this, args });
}
return wrapper as any as T;
}
export interface WatchWithFilterOptions<Immediate>
extends WatchOptions<Immediate>,
ConfigurableEventFilter {}
// implementation
export function watchWithFilter<Immediate extends Readonly<boolean> = false>(
source: any,
cb: any,
options: WatchWithFilterOptions<Immediate> = {},
): WatchStopHandle {
const { eventFilter = bypassFilter, ...watchOptions } = options;
return watch(source, createFilterWrapper(eventFilter, cb), watchOptions);
}
// implementation
export default function debouncedWatch<Immediate extends Readonly<boolean> = false>(
source: any,
cb: any,
options: DebouncedWatchOptions<Immediate> = {},
): WatchStopHandle {
const { debounce = 0, ...watchOptions } = options;
return watchWithFilter(source, cb, {
...watchOptions,
eventFilter: debounceFilter(debounce),
});
}

View File

@ -247,13 +247,13 @@ export default defineComponent({
},
attrs.class,
);
const { icon = slots.icon, ...otherProps } = props;
const { icon = slots.icon, blockNode = true, ...otherProps } = props;
return (
<Tree
{...attrs}
icon={icon || getIcon}
ref={treeRef}
blockNode
blockNode={blockNode}
{...otherProps}
prefixCls={prefixCls.value}
class={connectClassName}

View File

@ -3,7 +3,6 @@ import { ref } from 'vue';
import { defineComponent } from 'vue';
import classNames from '../_util/classNames';
import VcTree, { TreeNode } from '../vc-tree';
import animation from '../_util/openAnimation';
import PropTypes from '../_util/vue-types';
import { filterEmpty } from '../_util/props-util';
import initDefaultProps from '../_util/props-util/initDefaultProps';
@ -136,10 +135,6 @@ export default defineComponent({
checkable: false,
selectable: true,
showIcon: false,
openAnimation: {
...animation,
appear: false,
},
blockNode: false,
}),
slots: ['icon', 'title', 'switcherIcon'],

View File

@ -3,9 +3,10 @@ import type { FlattenNode } from './interface';
import type { TreeNodeRequiredProps } from './utils/treeUtil';
import { getTreeNodeProps } from './utils/treeUtil';
import { useInjectTreeContext } from './contextTypes';
import type { PropType } from 'vue';
import { computed, getCurrentInstance, nextTick, PropType } from 'vue';
import { defineComponent, onBeforeUnmount, onMounted, ref, Transition, watch } from 'vue';
import { treeNodeProps } from './props';
import { collapseMotion } from '../_util/transition';
export default defineComponent({
name: 'MotionTreeNode',
@ -25,7 +26,21 @@ export default defineComponent({
const visible = ref(true);
const context = useInjectTreeContext();
const motionedRef = ref(false);
const onMotionEnd = () => {
const transitionClass = ref('');
const transitionStyle = ref({});
const transitionProps = computed(() => {
if (props.motion) {
return props.motion;
} else {
return collapseMotion(transitionStyle, transitionClass);
}
});
const onMotionEnd = (type?: 'appear' | 'leave') => {
if (type === 'appear') {
transitionProps.value?.onAfterAppear?.();
} else if (type === 'leave') {
transitionProps.value?.onAfterLeave?.();
}
if (!motionedRef.value) {
props.onMotionEnd();
}
@ -36,9 +51,12 @@ export default defineComponent({
() => props.motionNodes,
() => {
if (props.motionNodes && props.motionType === 'hide' && visible.value) {
visible.value = false;
nextTick(() => {
visible.value = false;
});
}
},
{ immediate: true, flush: 'post' },
);
onMounted(() => {
props.motionNodes && props.onMotionStart();
@ -46,18 +64,23 @@ export default defineComponent({
onBeforeUnmount(() => {
props.motionNodes && onMotionEnd();
});
return () => {
const { motion, motionNodes, motionType, active, treeNodeRequiredProps, ...otherProps } =
props;
if (motionNodes) {
return (
<Transition
{...motion}
{...transitionProps.value}
appear={motionType === 'show'}
onAfterAppear={onMotionEnd}
onAfterLeave={onMotionEnd}
onAfterAppear={() => onMotionEnd('appear')}
onAfterLeave={() => onMotionEnd('leave')}
>
<div v-show={visible.value} class={`${context.value.prefixCls}-treenode-motion`}>
<div
v-show={visible.value}
class={[`${context.value.prefixCls}-treenode-motion`, transitionClass.value]}
style={transitionStyle.value}
>
{motionNodes.map((treeNode: FlattenNode) => {
const {
data: { ...restProps },

View File

@ -118,7 +118,7 @@ export default defineComponent({
props.onListChangeEnd();
}
watch(
[() => ({ ...props.expandedKeys }), () => props.data],
[() => [...props.expandedKeys], () => props.data],
([expandedKeys, data], [prevExpandedKeys, prevData]) => {
const diffExpanded = findExpandedKeys(prevExpandedKeys, expandedKeys);
@ -161,9 +161,7 @@ export default defineComponent({
transitionData.value = data;
}
},
{ immediate: true },
);
// We should clean up motion if is changed by dragging
watch(
() => props.dragging,
@ -174,7 +172,9 @@ export default defineComponent({
},
);
const mergedData = computed(() => (props.motion ? transitionData.value : props.data));
const mergedData = computed(() =>
props.motion === undefined ? transitionData.value : props.data,
);
return () => {
const {

View File

@ -84,12 +84,11 @@ export default defineComponent({
// abstract-drag-over-node is the top node
dragOverNodeKey: null,
});
warning(
!(props.treeData === undefined && props.children),
'`children` of Tree is deprecated. Please use `treeData` instead.',
);
const treeData = computed(() => {
warning(
!(props.treeData === undefined && props.children),
'`children` of Tree is deprecated. Please use `treeData` instead.',
);
return props.treeData !== undefined ? props.treeData : convertTreeToData(props.children);
});
const keyEntities = ref({});

View File

@ -2,7 +2,7 @@ import { useInjectTreeContext } from './contextTypes';
import { getDataAndAria } from './util';
import Indent from './Indent';
import { convertNodePropsToEventData } from './utils/treeUtil';
import { computed, defineComponent, getCurrentInstance, onMounted, onUpdated, ref } from 'vue';
import { computed, defineComponent, onMounted, onUpdated, ref } from 'vue';
import { treeNodeProps } from './props';
import classNames from '../_util/classNames';
import { warning } from '../vc-util/warning';
@ -18,7 +18,7 @@ export default defineComponent({
inheritAttrs: false,
props: treeNodeProps,
isTreeNode: 1,
slots: ['title', 'icon', 'switcherIcon', 'checkable'],
slots: ['title', 'icon', 'switcherIcon'],
setup(props, { attrs, slots, expose }) {
warning(
!('slots' in props.data),

View File

@ -41,7 +41,7 @@ export const treeNodeProps = {
dragOverGapTop: { type: Boolean, default: undefined },
dragOverGapBottom: { type: Boolean, default: undefined },
pos: String,
// domRef: React.Ref<HTMLDivElement>,
/** New added in Tree for easy data access */
data: { type: Object as PropType<DataNode> },
isStart: { type: Array as PropType<boolean[]> },

View File

@ -60,6 +60,20 @@ export function warningWithoutKey(treeData: DataNode[], fieldNames: FieldNames)
dig(treeData);
}
const cacheStringFunction = (fn: (s: string) => string) => {
const cache = Object.create(null);
return (str: string) => {
const hit = cache[str];
return hit || (cache[str] = fn(str));
};
};
const camelizeRE = /-(\w)/g;
const camelize = cacheStringFunction((str: string) => {
return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ''));
});
/**
* Convert `children` of Tree into `treeData` structure.
*/
@ -73,13 +87,37 @@ export function convertTreeToData(rootNodes: VNodeChild): DataNode[] {
warning(!treeNode, 'Tree/TreeNode can only accept TreeNode as children.');
return null;
}
const slots = (treeNode.children as any) || {};
const key = treeNode.key as string | number;
const { ...rest } = treeNode.props;
const children = (treeNode.children as any)?.default?.();
const props: any = {};
for (const [k, v] of Object.entries(treeNode.props)) {
props[camelize(k)] = v;
}
const { isLeaf, checkable, selectable, disabled, disableCheckbox } = props;
// 默认值为 undefined
const newProps = {
isLeaf: isLeaf || isLeaf === '' || undefined,
checkable: checkable || checkable === '' || undefined,
selectable: selectable || selectable === '' || undefined,
disabled: disabled || disabled === '' || undefined,
disableCheckbox: disableCheckbox || disableCheckbox === '' || undefined,
};
const slotsProps = { ...props, ...newProps };
const {
title = slots.title?.(slotsProps),
icon = slots.icon?.(slotsProps),
switcherIcon = slots.switcherIcon?.(slotsProps),
...rest
} = props;
const children = slots.default?.();
const dataNode: DataNode = {
...rest,
title,
icon,
switcherIcon,
key,
isLeaf,
...newProps,
};
const parsedChildren = dig(children);

View File

@ -3,9 +3,10 @@
v-model:expandedKeys="expandedKeys"
v-model:selectedKeys="selectedKeys"
multiple
@test="onTest"
:tree-data1="treeData"
>
<a-tree-node key="0-0" title="parent 0">
<a-tree-node key="0-0" title="ddd" class="test" style="color: red">
<template #title="{ title }">{{ title }}</template>
<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>
@ -16,15 +17,34 @@
</a-directory-tree>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import { defineComponent, ref, watch } from 'vue';
export default defineComponent({
setup() {
const expandedKeys = ref<string[]>(['0-0', '0-1']);
const expandedKeys = ref<string[]>([]);
const selectedKeys = ref<string[]>([]);
const treeData = [
{
title: 'parent 0',
key: '0-0',
children: [
{ title: 'leaf 0-0', key: '0-0-0', isLeaf: true },
// { title: 'leaf 0-1', key: '0-0-1', isLeaf: true },
],
},
{
title: 'parent 1',
key: '0-1',
children: [
{ title: 'leaf 1-0', key: '0-1-0', isLeaf: true },
{ title: 'leaf 1-1', key: '0-1-1', isLeaf: true },
],
},
];
return {
expandedKeys,
selectedKeys,
onTest: () => {},
treeData,
};
},
});