From 99f034deac30c19357c2ef3f70497eda2d00c692 Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Thu, 19 Aug 2021 23:29:07 +0800 Subject: [PATCH] refactor: directory tree --- components/_util/debouncedWatch.ts | 89 +++++++++++++++++++++++++++ components/tree/DirectoryTree.tsx | 4 +- components/tree/Tree.tsx | 5 -- components/vc-tree/MotionTreeNode.tsx | 37 ++++++++--- components/vc-tree/NodeList.tsx | 8 +-- components/vc-tree/Tree.tsx | 9 ++- components/vc-tree/TreeNode.tsx | 4 +- components/vc-tree/props.ts | 2 +- components/vc-tree/utils/treeUtil.ts | 44 ++++++++++++- examples/App.vue | 28 +++++++-- 10 files changed, 197 insertions(+), 33 deletions(-) create mode 100644 components/_util/debouncedWatch.ts diff --git a/components/_util/debouncedWatch.ts b/components/_util/debouncedWatch.ts new file mode 100644 index 000000000..c822ab5ca --- /dev/null +++ b/components/_util/debouncedWatch.ts @@ -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 | Ref; + +type Fn = () => void; + +export type FunctionArgs = (...args: Args) => Return; + +export interface FunctionWrapperOptions { + fn: FunctionArgs; + args: Args; + thisArg: This; +} + +export type EventFilter = ( + invoke: Fn, + options: FunctionWrapperOptions, +) => void; + +const bypassFilter: EventFilter = invoke => { + return invoke(); +}; +/** + * Create an EventFilter that debounce the events + * + * @param ms + */ +export function debounceFilter(ms: MaybeRef) { + let timer: ReturnType | 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 extends WatchOptions { + debounce?: MaybeRef; +} + +interface ConfigurableEventFilter { + eventFilter?: EventFilter; +} +/** + * @internal + */ +function createFilterWrapper(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 + extends WatchOptions, + ConfigurableEventFilter {} +// implementation +export function watchWithFilter = false>( + source: any, + cb: any, + options: WatchWithFilterOptions = {}, +): WatchStopHandle { + const { eventFilter = bypassFilter, ...watchOptions } = options; + + return watch(source, createFilterWrapper(eventFilter, cb), watchOptions); +} + +// implementation +export default function debouncedWatch = false>( + source: any, + cb: any, + options: DebouncedWatchOptions = {}, +): WatchStopHandle { + const { debounce = 0, ...watchOptions } = options; + + return watchWithFilter(source, cb, { + ...watchOptions, + eventFilter: debounceFilter(debounce), + }); +} diff --git a/components/tree/DirectoryTree.tsx b/components/tree/DirectoryTree.tsx index c84c894f3..f3d45f0bc 100644 --- a/components/tree/DirectoryTree.tsx +++ b/components/tree/DirectoryTree.tsx @@ -247,13 +247,13 @@ export default defineComponent({ }, attrs.class, ); - const { icon = slots.icon, ...otherProps } = props; + const { icon = slots.icon, blockNode = true, ...otherProps } = props; return ( { + 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 ( onMotionEnd('appear')} + onAfterLeave={() => onMotionEnd('leave')} > -
+
{motionNodes.map((treeNode: FlattenNode) => { const { data: { ...restProps }, diff --git a/components/vc-tree/NodeList.tsx b/components/vc-tree/NodeList.tsx index 6dbe01dd8..2f7d0774e 100644 --- a/components/vc-tree/NodeList.tsx +++ b/components/vc-tree/NodeList.tsx @@ -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 { diff --git a/components/vc-tree/Tree.tsx b/components/vc-tree/Tree.tsx index aee685671..af5ccfc5e 100644 --- a/components/vc-tree/Tree.tsx +++ b/components/vc-tree/Tree.tsx @@ -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({}); diff --git a/components/vc-tree/TreeNode.tsx b/components/vc-tree/TreeNode.tsx index 551d91406..f0e5215e3 100644 --- a/components/vc-tree/TreeNode.tsx +++ b/components/vc-tree/TreeNode.tsx @@ -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), diff --git a/components/vc-tree/props.ts b/components/vc-tree/props.ts index 2e0a8fa9c..42d293f50 100644 --- a/components/vc-tree/props.ts +++ b/components/vc-tree/props.ts @@ -41,7 +41,7 @@ export const treeNodeProps = { dragOverGapTop: { type: Boolean, default: undefined }, dragOverGapBottom: { type: Boolean, default: undefined }, pos: String, - // domRef: React.Ref, + /** New added in Tree for easy data access */ data: { type: Object as PropType }, isStart: { type: Array as PropType }, diff --git a/components/vc-tree/utils/treeUtil.ts b/components/vc-tree/utils/treeUtil.ts index 77eb1f097..42409dc75 100644 --- a/components/vc-tree/utils/treeUtil.ts +++ b/components/vc-tree/utils/treeUtil.ts @@ -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); diff --git a/examples/App.vue b/examples/App.vue index 927ccfd1f..d9e64b8a7 100644 --- a/examples/App.vue +++ b/examples/App.vue @@ -3,9 +3,10 @@ v-model:expandedKeys="expandedKeys" v-model:selectedKeys="selectedKeys" multiple - @test="onTest" + :tree-data1="treeData" > - + + @@ -16,15 +17,34 @@