diff --git a/components/vc-tree/NodeList.tsx b/components/vc-tree/NodeList.tsx index b90b1e5b8..e47ae4b8e 100644 --- a/components/vc-tree/NodeList.tsx +++ b/components/vc-tree/NodeList.tsx @@ -6,6 +6,7 @@ import { computed, defineComponent, ref, shallowRef, watch } from 'vue'; import VirtualList from '../vc-virtual-list'; import type { FlattenNode, DataEntity, DataNode, ScrollTo } from './interface'; import MotionTreeNode from './MotionTreeNode'; +import type { NodeListProps } from './props'; import { nodeListProps } from './props'; import { findExpandedKeys, getExpandRange } from './utils/diffUtil'; import { getTreeNodeProps, getKey } from './utils/treeUtil'; @@ -35,6 +36,7 @@ export const MotionEntity: DataEntity = { index: 0, pos: '0', node: MotionNode, + nodes: [MotionNode], }; const MotionFlattenData: FlattenNode = { @@ -208,7 +210,7 @@ export default defineComponent({ onListChangeEnd, ...domProps - } = { ...props, ...attrs }; + } = { ...props, ...attrs } as NodeListProps; const treeNodeRequiredProps = { expandedKeys, @@ -269,6 +271,15 @@ export default defineComponent({ itemHeight={itemHeight} prefixCls={`${prefixCls}-list`} ref={listRef} + onVisibleChange={(originList, fullList) => { + const originSet = new Set(originList); + const restList = fullList.filter(item => !originSet.has(item)); + + // Motion node is not render. Skip motion + if (restList.some(item => itemKey(item) === MOTION_KEY)) { + onMotionEnd(); + } + }} v-slots={{ default: (treeNode: FlattenNode) => { const { diff --git a/components/vc-tree/Tree.tsx b/components/vc-tree/Tree.tsx index d0f1f7017..1fceb7bee 100644 --- a/components/vc-tree/Tree.tsx +++ b/components/vc-tree/Tree.tsx @@ -1,7 +1,6 @@ import type { NodeMouseEventHandler, NodeDragEventHandler } from './contextTypes'; import { TreeContext } from './contextTypes'; import { - getDataAndAria, getDragChildrenKeys, parseCheckedKeys, conductExpandParent, @@ -34,11 +33,19 @@ import { watchEffect, } from 'vue'; import initDefaultProps from '../_util/props-util/initDefaultProps'; -import type { CheckInfo } from './props'; +import type { CheckInfo, DraggableFn } from './props'; import { treeProps } from './props'; import { warning } from '../vc-util/warning'; import KeyCode from '../_util/KeyCode'; import classNames from '../_util/classNames'; +import pickAttrs from '../_util/pickAttrs'; + +const MAX_RETRY_TIMES = 10; + +export type DraggableConfig = { + icon?: any; + nodeDraggable?: DraggableFn; +}; export default defineComponent({ name: 'Tree', @@ -74,9 +81,9 @@ export default defineComponent({ const loadedKeys = shallowRef([]); const loadingKeys = shallowRef([]); const expandedKeys = shallowRef([]); - + const loadingRetryTimes: Record = {}; const dragState = reactive({ - dragging: false, + draggingNodeKey: null, dragChildrenKeys: [], // dropTargetKey is the key of abstract-drop-node @@ -111,6 +118,8 @@ export default defineComponent({ let dragNode: DragNodeEvent = null; + let currentMouseOverDroppableNodeKey = null; + const treeNodeRequiredProps = computed(() => { return { expandedKeys: expandedKeys.value || [], @@ -219,6 +228,17 @@ export default defineComponent({ } }); + const resetDragState = () => { + Object.assign(dragState, { + dragOverNodeKey: null, + dropPosition: null, + dropLevelOffset: null, + dropTargetKey: null, + dropContainerKey: null, + dropTargetPos: null, + dropAllowed: false, + }); + }; const scrollTo: ScrollTo = scroll => { listRef.value.scrollTo(scroll); }; @@ -231,9 +251,9 @@ export default defineComponent({ }; const cleanDragState = () => { - if (dragState.dragging) { + if (dragState.draggingNodeKey !== null) { Object.assign(dragState, { - dragging: false, + draggingNodeKey: null, dropPosition: null, dropContainerKey: null, dropTargetKey: null, @@ -243,6 +263,7 @@ export default defineComponent({ }); } dragStartMousePosition = null; + currentMouseOverDroppableNodeKey = null; }; // if onNodeDragEnd is called, onWindowDragEnd won't be called since stopPropagation() is called const onNodeDragEnd: NodeDragEventHandler = (event, node, outsideTree = false) => { @@ -277,7 +298,7 @@ export default defineComponent({ const newExpandedKeys = arrDel(expandedKeys.value, eventKey); - dragState.dragging = true; + dragState.draggingNodeKey = eventKey; dragState.dragChildrenKeys = getDragChildrenKeys(eventKey, keyEntities.value); indent.value = listRef.value.getIndentWidth(); @@ -296,9 +317,18 @@ export default defineComponent({ * Better for use mouse move event to refresh drag state. * But let's just keep it to avoid event trigger logic change. */ - const onNodeDragEnter = (event: MouseEvent, node: DragNodeEvent) => { + const onNodeDragEnter = (event: DragEvent, node: DragNodeEvent) => { const { onDragenter, onExpand, allowDrop, direction } = props; + const { pos, eventKey } = node; + // record the key of node which is latest entered, used in dragleave event. + if (currentMouseOverDroppableNodeKey !== eventKey) { + currentMouseOverDroppableNodeKey = eventKey; + } + if (!dragNode) { + resetDragState(); + return; + } const { dropPosition, dropLevelOffset, @@ -321,21 +351,12 @@ export default defineComponent({ ); if ( - !dragNode || // don't allow drop inside its children dragState.dragChildrenKeys.indexOf(dropTargetKey) !== -1 || // don't allow drop when drop is not allowed caculated by calcDropPosition !dropAllowed ) { - Object.assign(dragState, { - dragOverNodeKey: null, - dropPosition: null, - dropLevelOffset: null, - dropTargetKey: null, - dropContainerKey: null, - dropTargetPos: null, - dropAllowed: false, - }); + resetDragState(); return; } @@ -352,8 +373,8 @@ export default defineComponent({ // since if logic is on the bottom // it will be blocked by abstract dragover node check // => if you dragenter from top, you mouse will still be consider as in the top node - delayedDragEnterLogic[node.pos] = window.setTimeout(() => { - if (!dragState.dragging) return; + delayedDragEnterLogic[pos] = window.setTimeout(() => { + if (dragState.draggingNodeKey === null) return; let newExpandedKeys = [...expandedKeys.value]; const entity = keyEntities.value[node.eventKey]; @@ -375,15 +396,7 @@ export default defineComponent({ // Skip if drag node is self if (dragNode.eventKey === dropTargetKey && dropLevelOffset === 0) { - Object.assign(dragState, { - dragOverNodeKey: null, - dropPosition: null, - dropLevelOffset: null, - dropTargetKey: null, - dropContainerKey: null, - dropTargetPos: null, - dropAllowed: false, - }); + resetDragState(); return; } @@ -407,9 +420,12 @@ export default defineComponent({ } }; - const onNodeDragOver = (event: MouseEvent, node: DragNodeEvent) => { + const onNodeDragOver = (event: DragEvent, node: DragNodeEvent) => { const { onDragover, allowDrop, direction } = props; + if (!dragNode) { + return; + } const { dropPosition, dropLevelOffset, @@ -431,7 +447,7 @@ export default defineComponent({ direction, ); - if (!dragNode || dragState.dragChildrenKeys.indexOf(dropTargetKey) !== -1 || !dropAllowed) { + if (dragState.dragChildrenKeys.indexOf(dropTargetKey) !== -1 || !dropAllowed) { // don't allow drop inside its children // don't allow drop when drop is not allowed caculated by calcDropPosition return; @@ -451,15 +467,7 @@ export default defineComponent({ dragState.dragOverNodeKey === null ) ) { - Object.assign(dragState, { - dropPosition: null, - dropLevelOffset: null, - dropTargetKey: null, - dropContainerKey: null, - dropTargetPos: null, - dropAllowed: false, - dragOverNodeKey: null, - }); + resetDragState(); } } else if ( !( @@ -489,13 +497,23 @@ export default defineComponent({ }; const onNodeDragLeave: NodeDragEventHandler = (event, node) => { + // if it is outside the droppable area + // currentMouseOverDroppableNodeKey will be updated in dragenter event when into another droppable receiver. + if ( + currentMouseOverDroppableNodeKey === node.eventKey && + !(event.currentTarget as any).contains(event.relatedTarget as Node) + ) { + resetDragState(); + currentMouseOverDroppableNodeKey = null; + } + const { onDragleave } = props; if (onDragleave) { onDragleave({ event, node: node.eventData }); } }; - const onNodeDrop = (event: MouseEvent, _node, outsideTree = false) => { + const onNodeDrop = (event: DragEvent, _node, outsideTree = false) => { const { dragChildrenKeys, dropPosition, dropTargetKey, dropTargetPos, dropAllowed } = dragState; @@ -666,11 +684,11 @@ export default defineComponent({ } }; - const onNodeLoad = (treeNode: EventDataNode) => - new Promise((resolve, reject) => { + const onNodeLoad = (treeNode: EventDataNode) => { + const key = treeNode[fieldNames.value.key]; + const loadPromise = new Promise((resolve, reject) => { // We need to get the latest state of loading/loaded keys const { loadData, onLoad } = props; - const key = treeNode[fieldNames.value.key]; if ( !loadData || @@ -705,12 +723,28 @@ export default defineComponent({ .catch(e => { const newLoadingKeys = arrDel(loadingKeys.value, key); loadingKeys.value = newLoadingKeys; + + // If exceed max retry times, we give up retry + loadingRetryTimes[key] = (loadingRetryTimes[key] || 0) + 1; + if (loadingRetryTimes[key] >= MAX_RETRY_TIMES) { + warning(false, 'Retry for `loadData` many times but still failed. No more retry.'); + const newLoadedKeys = arrAdd(loadedKeys.value, key); + if (props.loadedKeys === undefined) { + loadedKeys.value = newLoadedKeys; + } + resolve(); + } + reject(e); }); loadingKeys.value = arrAdd(loadingKeys.value, key); }); + // Not care warning if we ignore this + loadPromise.catch(() => {}); + return loadPromise; + }; const onNodeMouseEnter: NodeMouseEventHandler = (event, node) => { const { onMouseenter } = props; if (onMouseenter) { @@ -968,7 +1002,7 @@ export default defineComponent({ // focused, // flattenNodes, // keyEntities, - dragging, + draggingNodeKey, // activeKey, dropLevelOffset, dropContainerKey, @@ -1003,7 +1037,27 @@ export default defineComponent({ } = props; const { class: className, style } = attrs; - const domProps = getDataAndAria({ ...props, ...attrs }); + const domProps = pickAttrs( + { ...props, ...attrs }, + { + aria: true, + data: true, + }, + ); + + // It's better move to hooks but we just simply keep here + let draggableConfig: DraggableConfig; + if (draggable) { + if (typeof draggable === 'object') { + draggableConfig = draggable; + } else if (typeof draggable === 'function') { + draggableConfig = { + nodeDraggable: draggable, + }; + } else { + draggableConfig = {}; + } + } return ( { + const { data } = props; + const { draggable } = context.value; + return !!(draggable && (!draggable.nodeDraggable || draggable.nodeDraggable(data))); + }; + + // ==================== Render: Drag Handler ==================== + const renderDragHandler = () => { + const { draggable, prefixCls } = context.value; + return draggable?.icon ? ( + {draggable.icon} + ) : null; + }; + const renderSwitcherIconDom = () => { const { switcherIcon: switcherIconFromProps = slots.switcherIcon || @@ -368,9 +382,9 @@ export default defineComponent({ dragOverNodeKey, direction, } = context.value; - const mergedDraggable = draggable !== false; + const rootDraggable = draggable !== false; // allowDrop is calculated in Tree.tsx, there is no need for calc it here - const showIndicator = !disabled && mergedDraggable && dragOverNodeKey === eventKey; + const showIndicator = !disabled && rootDraggable && dragOverNodeKey === eventKey; return showIndicator ? dropIndicatorRender({ dropPosition, dropLevelOffset, indent, prefixCls, direction }) : null; @@ -396,12 +410,10 @@ export default defineComponent({ prefixCls, showIcon, icon: treeIcon, - draggable, loadData, // slots: contextSlots, } = context.value; const disabled = isDisabled.value; - const mergedDraggable = typeof draggable === 'function' ? draggable(data) : draggable; const wrapClass = `${prefixCls}-node-content-wrapper`; @@ -443,16 +455,12 @@ export default defineComponent({ `${wrapClass}`, `${wrapClass}-${nodeState.value || 'normal'}`, !disabled && (selected || dragNodeHighlight.value) && `${prefixCls}-node-selected`, - !disabled && mergedDraggable && 'draggable', )} - draggable={(!disabled && mergedDraggable) || undefined} - aria-grabbed={(!disabled && mergedDraggable) || undefined} onMouseenter={onMouseEnter} onMouseleave={onMouseLeave} onContextmenu={onContextmenu} onClick={onSelectorClick} onDblclick={onSelectorDoubleClick} - onDragstart={mergedDraggable ? onDragStart : undefined} > {$icon} {$title} @@ -478,15 +486,28 @@ export default defineComponent({ active, data, onMousemove, + selectable, ...otherProps } = { ...props, ...attrs }; - const { prefixCls, filterTreeNode, draggable, keyEntities, dropContainerKey, dropTargetKey } = - context.value; + const { + prefixCls, + filterTreeNode, + keyEntities, + dropContainerKey, + dropTargetKey, + draggingNodeKey, + } = context.value; const disabled = isDisabled.value; - const dataOrAriaAttributeProps = getDataAndAria(otherProps); + const dataOrAriaAttributeProps = pickAttrs(otherProps, { aria: true, data: true }); const { level } = keyEntities[eventKey] || {}; const isEndNode = isEnd[isEnd.length - 1]; - const mergedDraggable = typeof draggable === 'function' ? draggable(data) : draggable; + + const mergedDraggable = isDraggable(); + const draggableWithoutDisabled = !disabled && mergedDraggable; + + const dragging = draggingNodeKey === eventKey; + const ariaSelected = selectable !== undefined ? { 'aria-selected': !!selectable } : undefined; + return (
+ {renderDragHandler()} {renderSwitcher()} {renderCheckbox()} {renderSelector()} diff --git a/components/vc-tree/contextTypes.ts b/components/vc-tree/contextTypes.ts index 33dbc8802..0f5644f17 100644 --- a/components/vc-tree/contextTypes.ts +++ b/components/vc-tree/contextTypes.ts @@ -12,22 +12,23 @@ import type { DataEntity, EventDataNode, DragNodeEvent, - DataNode, Direction, } from './interface'; +import type { DraggableConfig } from './Tree'; + export type NodeMouseEventParams = { event: MouseEvent; node: EventDataNode; }; export type NodeDragEventParams = { - event: MouseEvent; + event: DragEvent; node: EventDataNode; }; export type NodeMouseEventHandler = (e: MouseEvent, node: EventDataNode) => void; export type NodeDragEventHandler = ( - e: MouseEvent, + e: DragEvent, node: DragNodeEvent, outsideTree?: boolean, ) => void; @@ -38,12 +39,13 @@ export interface TreeContextProps { showIcon: boolean; icon: IconType; switcherIcon: IconType; - draggable: ((node: DataNode) => boolean) | boolean; + draggable: DraggableConfig; + draggingNodeKey?: Key; checkable: boolean; customCheckable: () => any; checkStrictly: boolean; disabled: boolean; - keyEntities: Record; + keyEntities: Record>; // for details see comment in Tree.state (Tree.tsx) dropLevelOffset?: number; dropContainerKey: Key | null; @@ -79,8 +81,8 @@ export interface TreeContextProps { onNodeDragEnd: NodeDragEventHandler; onNodeDrop: NodeDragEventHandler; slots: { - title?: (data: DataNode) => any; - titleRender?: (data: DataNode) => any; + title?: (data: any) => any; + titleRender?: (data: any) => any; [key: string]: ((...args: any[]) => any) | undefined; }; } diff --git a/components/vc-tree/index.ts b/components/vc-tree/index.ts index e6552f422..9ad36aada 100644 --- a/components/vc-tree/index.ts +++ b/components/vc-tree/index.ts @@ -1,7 +1,8 @@ -// base rc-tree 5.0.1 +// base rc-tree 5.3.7 import type { TreeProps, TreeNodeProps } from './props'; import Tree from './Tree'; import TreeNode from './TreeNode'; +import type { BasicDataNode } from './interface'; export { TreeNode }; -export type { TreeProps, TreeNodeProps }; +export type { TreeProps, TreeNodeProps, BasicDataNode }; export default Tree; diff --git a/components/vc-tree/interface.tsx b/components/vc-tree/interface.tsx index 473a00857..a4e227361 100644 --- a/components/vc-tree/interface.tsx +++ b/components/vc-tree/interface.tsx @@ -2,15 +2,13 @@ import type { CSSProperties, VNode } from 'vue'; import type { TreeNodeProps } from './props'; export type { ScrollTo } from '../vc-virtual-list/List'; -export interface DataNode { +/** For fieldNames, we provides a abstract interface */ +export interface BasicDataNode { checkable?: boolean; - children?: DataNode[]; disabled?: boolean; disableCheckbox?: boolean; icon?: IconType; isLeaf?: boolean; - key: string | number; - title?: any; selectable?: boolean; switcherIcon?: IconType; @@ -21,6 +19,12 @@ export interface DataNode { [key: string]: any; } +export interface DataNode extends BasicDataNode { + children?: DataNode[]; + key: string | number; + title?: any; +} + export interface EventDataNode extends DataNode { expanded?: boolean; selected?: boolean; @@ -60,10 +64,12 @@ export interface Entity { children?: Entity[]; } -export interface DataEntity extends Omit { - node: DataNode; - parent?: DataEntity; - children?: DataEntity[]; +export interface DataEntity + extends Omit { + node: TreeDataType; + nodes: TreeDataType[]; + parent?: DataEntity; + children?: DataEntity[]; level: number; } @@ -86,6 +92,8 @@ export type Direction = 'ltr' | 'rtl' | undefined; export interface FieldNames { title?: string; + /** @private Internal usage for `vc-tree-select`, safe to remove if no need */ + _title?: string[]; key?: string; children?: string; } diff --git a/components/vc-tree/props.ts b/components/vc-tree/props.ts index a8fcfe2b7..13965ec75 100644 --- a/components/vc-tree/props.ts +++ b/components/vc-tree/props.ts @@ -1,4 +1,5 @@ import type { ExtractPropTypes, PropType } from 'vue'; +import type { BasicDataNode } from '.'; import type { EventHandler } from '../_util/EventInterface'; import PropTypes from '../_util/vue-types'; import type { @@ -10,10 +11,10 @@ import type { DataNode, Key, FlattenNode, - DataEntity, EventDataNode, Direction, FieldNames, + DataEntity, } from './interface'; export interface CheckInfo { @@ -83,7 +84,7 @@ export const nodeListProps = { loadedKeys: { type: Array as PropType }, loadingKeys: { type: Array as PropType }, halfCheckedKeys: { type: Array as PropType }, - keyEntities: { type: Object as PropType> }, + keyEntities: { type: Object as PropType>> }, dragging: { type: Boolean as PropType }, dragOverNodeKey: { type: [String, Number] as PropType }, @@ -106,8 +107,17 @@ export const nodeListProps = { }; export type NodeListProps = Partial>; -export type AllowDrop = (options: { dropNode: DataNode; dropPosition: -1 | 0 | 1 }) => boolean; +export interface AllowDropOptions { + dragNode: EventDataNode; + dropNode: TreeDataType; + dropPosition: -1 | 0 | 1; +} +export type AllowDrop = ( + options: AllowDropOptions, +) => boolean; + +export type DraggableFn = (node: DataNode) => boolean; export const treeProps = () => ({ prefixCls: String, focusable: { type: Boolean, default: undefined }, @@ -123,7 +133,7 @@ export const treeProps = () => ({ multiple: { type: Boolean, default: undefined }, checkable: { type: Boolean, default: undefined }, checkStrictly: { type: Boolean, default: undefined }, - draggable: { type: [Function, Boolean] as PropType<((node: DataNode) => boolean) | boolean> }, + draggable: { type: [Function, Boolean] as PropType }, defaultExpandParent: { type: Boolean, default: undefined }, autoExpandParent: { type: Boolean, default: undefined }, defaultExpandAll: { type: Boolean, default: undefined }, diff --git a/components/vc-tree/util.tsx b/components/vc-tree/util.tsx index 0512e2d4e..9fc2ffe64 100644 --- a/components/vc-tree/util.tsx +++ b/components/vc-tree/util.tsx @@ -12,11 +12,13 @@ import type { FlattenNode, Direction, DragNodeEvent, + BasicDataNode, } from './interface'; import { warning } from '../vc-util/warning'; import type { AllowDrop, TreeNodeProps, TreeProps } from './props'; export function arrDel(list: Key[], value: Key) { + if (!list) return []; const clone = list.slice(); const index = clone.indexOf(value); if (index >= 0) { @@ -26,7 +28,7 @@ export function arrDel(list: Key[], value: Key) { } export function arrAdd(list: Key[], value: Key) { - const clone = list.slice(); + const clone = (list || []).slice(); if (clone.indexOf(value) === -1) { clone.push(value); } @@ -45,13 +47,16 @@ export function isTreeNode(node: NodeElement) { return node && node.type && (node.type as any).isTreeNode; } -export function getDragChildrenKeys(dragNodeKey: Key, keyEntities: Record): Key[] { +export function getDragChildrenKeys( + dragNodeKey: Key, + keyEntities: Record>, +): Key[] { // not contains self // self for left or right drag const dragChildrenKeys = []; const entity = keyEntities[dragNodeKey]; - function dig(list: DataEntity[] = []) { + function dig(list: DataEntity[] = []) { list.forEach(({ key, children }) => { dragChildrenKeys.push(key); dig(children); @@ -63,7 +68,9 @@ export function getDragChildrenKeys(dragNodeKey: Key, keyEntities: Record( + treeNodeEntity: DataEntity, +) { if (treeNodeEntity.parent) { const posArr = posToArr(treeNodeEntity.pos); return Number(posArr[posArr.length - 1]) === treeNodeEntity.parent.children.length - 1; @@ -71,24 +78,26 @@ export function isLastChild(treeNodeEntity: DataEntity) { return false; } -export function isFirstChild(treeNodeEntity: DataEntity) { +export function isFirstChild( + treeNodeEntity: DataEntity, +) { const posArr = posToArr(treeNodeEntity.pos); return Number(posArr[posArr.length - 1]) === 0; } // Only used when drag, not affect SSR. -export function calcDropPosition( +export function calcDropPosition( event: MouseEvent, - _dragNode: DragNodeEvent, + dragNode: DragNodeEvent, targetNode: DragNodeEvent, indent: number, startMousePosition: { x: number; y: number; }, - allowDrop: AllowDrop, + allowDrop: AllowDrop, flattenedNodes: FlattenNode[], - keyEntities: Record, + keyEntities: Record>, expandKeys: Key[], direction: Direction, ): { @@ -108,7 +117,7 @@ export function calcDropPosition( const rawDropLevelOffset = (horizontalMouseOffset - 12) / indent; // find abstract drop node by horizontal offset - let abstractDropNodeEntity: DataEntity = keyEntities[targetNode.eventKey]; + let abstractDropNodeEntity: DataEntity = keyEntities[targetNode.eventKey]; if (clientY < top + height / 2) { // first half, set abstract drop node to previous node @@ -139,7 +148,7 @@ export function calcDropPosition( } } } - + const abstractDragDataNode = dragNode.eventData; const abstractDropDataNode = abstractDropNodeEntity.node; let dropAllowed = true; if ( @@ -147,6 +156,7 @@ export function calcDropPosition( abstractDropNodeEntity.level === 0 && clientY < top + height / 2 && allowDrop({ + dragNode: abstractDragDataNode, dropNode: abstractDropDataNode, dropPosition: -1, }) && @@ -162,6 +172,7 @@ export function calcDropPosition( // only allow drop inside if ( allowDrop({ + dragNode: abstractDragDataNode, dropNode: abstractDropDataNode, dropPosition: 0, }) @@ -178,6 +189,7 @@ export function calcDropPosition( // 2. do not allow drop if ( allowDrop({ + dragNode: abstractDragDataNode, dropNode: abstractDropDataNode, dropPosition: 1, }) @@ -196,6 +208,7 @@ export function calcDropPosition( // 3. do not allow drop if ( allowDrop({ + dragNode: abstractDragDataNode, dropNode: abstractDropDataNode, dropPosition: 0, }) @@ -203,6 +216,7 @@ export function calcDropPosition( dropPosition = 0; } else if ( allowDrop({ + dragNode: abstractDragDataNode, dropNode: abstractDropDataNode, dropPosition: 1, }) @@ -220,6 +234,7 @@ export function calcDropPosition( // 2. do not allow drop if ( allowDrop({ + dragNode: abstractDragDataNode, dropNode: abstractDropDataNode, dropPosition: 1, }) @@ -336,17 +351,3 @@ export function conductExpandParent(keyList: Key[], keyEntities: Record) { - const omitProps: Record = {}; - Object.keys(props).forEach(key => { - if (key.startsWith('data-') || key.startsWith('aria-')) { - omitProps[key] = props[key]; - } - }); - - return omitProps; -} diff --git a/components/vc-tree/utils/conductUtil.ts b/components/vc-tree/utils/conductUtil.ts index 1eff1685e..ed751be19 100644 --- a/components/vc-tree/utils/conductUtil.ts +++ b/components/vc-tree/utils/conductUtil.ts @@ -1,5 +1,5 @@ import { warning } from '../../vc-util/warning'; -import type { Key, DataEntity, DataNode, GetCheckDisabled } from '../interface'; +import type { Key, DataEntity, DataNode, GetCheckDisabled, BasicDataNode } from '../interface'; interface ConductReturnType { checkedKeys: Key[]; @@ -16,17 +16,17 @@ function removeFromCheckedKeys(halfCheckedKeys: Set, checkedKeys: Set) return filteredKeys; } -export function isCheckDisabled(node: DataNode) { +export function isCheckDisabled(node: TreeDataType) { const { disabled, disableCheckbox, checkable } = (node || {}) as DataNode; return !!(disabled || disableCheckbox) || checkable === false; } // Fill miss keys -function fillConductCheck( +function fillConductCheck( keys: Set, - levelEntities: Map>, + levelEntities: Map>>, maxLevel: number, - syntheticGetCheckDisabled: GetCheckDisabled, + syntheticGetCheckDisabled: GetCheckDisabled, ): ConductReturnType { const checkedKeys = new Set(keys); const halfCheckedKeys = new Set(); @@ -98,12 +98,12 @@ function fillConductCheck( } // Remove useless key -function cleanConductCheck( +function cleanConductCheck( keys: Set, halfKeys: Key[], - levelEntities: Map>, + levelEntities: Map>>, maxLevel: number, - syntheticGetCheckDisabled: GetCheckDisabled, + syntheticGetCheckDisabled: GetCheckDisabled, ): ConductReturnType { const checkedKeys = new Set(keys); let halfCheckedKeys = new Set(halfKeys); @@ -182,15 +182,15 @@ function cleanConductCheck( * @param keyEntities key - dataEntity map * @param mode `fill` to fill missing key, `clean` to remove useless key */ -export function conductCheck( +export function conductCheck( keyList: Key[], checked: true | { checked: false; halfCheckedKeys: Key[] }, - keyEntities: Record, - getCheckDisabled?: GetCheckDisabled, + keyEntities: Record>, + getCheckDisabled?: GetCheckDisabled, ): ConductReturnType { const warningMissKeys: Key[] = []; - let syntheticGetCheckDisabled: GetCheckDisabled; + let syntheticGetCheckDisabled: GetCheckDisabled; if (getCheckDisabled) { syntheticGetCheckDisabled = getCheckDisabled; } else { @@ -208,7 +208,7 @@ export function conductCheck( return hasEntity; }), ); - const levelEntities = new Map>(); + const levelEntities = new Map>>(); let maxLevel = 0; // Convert entities by level for calculation @@ -216,7 +216,7 @@ export function conductCheck( const entity = keyEntities[key]; const { level } = entity; - let levelSet: Set = levelEntities.get(level); + let levelSet: Set> = levelEntities.get(level); if (!levelSet) { levelSet = new Set(); levelEntities.set(level, levelSet); @@ -237,7 +237,12 @@ export function conductCheck( let result: ConductReturnType; if (checked === true) { - result = fillConductCheck(keys, levelEntities, maxLevel, syntheticGetCheckDisabled); + result = fillConductCheck( + keys, + levelEntities, + maxLevel, + syntheticGetCheckDisabled, + ); } else { result = cleanConductCheck( keys, diff --git a/components/vc-tree/utils/treeUtil.ts b/components/vc-tree/utils/treeUtil.ts index 68e2f0687..f4786289f 100644 --- a/components/vc-tree/utils/treeUtil.ts +++ b/components/vc-tree/utils/treeUtil.ts @@ -7,6 +7,7 @@ import type { EventDataNode, GetKey, FieldNames, + BasicDataNode, } from '../interface'; import { getPosition, isTreeNode } from '../util'; import { warning } from '../../vc-util/warning'; @@ -23,11 +24,13 @@ export function getKey(key: Key, pos: string) { return pos; } -export function fillFieldNames(fieldNames?: FieldNames) { - const { title, key, children } = fieldNames || {}; +export function fillFieldNames(fieldNames?: FieldNames): Required { + const { title, _title, key, children } = fieldNames || {}; + const mergedTitle = title || 'title'; return { - title: title || 'title', + title: mergedTitle, + _title: _title || [mergedTitle], key: key || 'key', children: children || 'children', }; @@ -129,7 +132,11 @@ export function flattenTreeData( expandedKeys: Key[] | true, fieldNames: FieldNames, ): FlattenNode[] { - const { title: fieldTitle, key: fieldKey, children: fieldChildren } = fillFieldNames(fieldNames); + const { + _title: fieldTitles, + key: fieldKey, + children: fieldChildren, + } = fillFieldNames(fieldNames); const expandedKeySet = new Set(expandedKeys === true ? [] : expandedKeys); const flattenList: FlattenNode[] = []; @@ -139,10 +146,19 @@ export function flattenTreeData( const pos: string = getPosition(parent ? parent.pos : '0', index); const mergedKey = getKey(treeNode[fieldKey], pos); + // Pick matched title in field title list + let mergedTitle: any; + for (let i = 0; i < fieldTitles.length; i += 1) { + const fieldTitle = fieldTitles[i]; + if (treeNode[fieldTitle] !== undefined) { + mergedTitle = treeNode[fieldTitle]; + break; + } + } // Add FlattenDataNode into list const flattenNode: FlattenNode = { - ...omit(treeNode, [fieldTitle, fieldKey, fieldChildren] as any), - title: treeNode[fieldTitle], + ...omit(treeNode, [...fieldTitles, fieldKey, fieldChildren] as any), + title: mergedTitle, key: mergedKey, parent, pos, @@ -191,6 +207,7 @@ export function traverseDataNodes( key: Key; parentPos: string | number; level: number; + nodes: DataNode[]; }) => void, // To avoid too many params, let use config instead of origin param config?: TraverseDataNodesConfig | string, @@ -227,9 +244,11 @@ export function traverseDataNodes( node: DataNode, index?: number, parent?: { node: DataNode; pos: string; level: number }, + pathNodes?: DataNode[], ) { const children = node ? node[mergeChildrenPropName] : dataNodes; const pos = node ? getPosition(parent.pos, index) : '0'; + const connectNodes = node ? [...pathNodes, node] : []; // Process node if is not root if (node) { @@ -241,6 +260,7 @@ export function traverseDataNodes( key, parentPos: parent.node ? parent.pos : null, level: parent.level + 1, + nodes: connectNodes, }; callback(data); @@ -249,11 +269,16 @@ export function traverseDataNodes( // Process children node if (children) { children.forEach((subNode, subIndex) => { - processNode(subNode, subIndex, { - node, - pos, - level: parent ? parent.level + 1 : -1, - }); + processNode( + subNode, + subIndex, + { + node, + pos, + level: parent ? parent.level + 1 : -1, + }, + connectNodes, + ); }); } } @@ -306,8 +331,8 @@ export function convertDataToEntities( traverseDataNodes( dataNodes, item => { - const { node, index, pos, key, parentPos, level } = item; - const entity: DataEntity = { node, index, key, pos, level }; + const { node, index, pos, key, parentPos, level, nodes } = item; + const entity: DataEntity = { node, nodes, index, key, pos, level }; const mergedKey = getKey(key, pos); @@ -335,7 +360,7 @@ export function convertDataToEntities( return wrapper; } -export interface TreeNodeRequiredProps { +export interface TreeNodeRequiredProps { expandedKeys: Key[]; selectedKeys: Key[]; loadedKeys: Key[]; @@ -344,13 +369,13 @@ export interface TreeNodeRequiredProps { halfCheckedKeys: Key[]; dragOverNodeKey: Key; dropPosition: number; - keyEntities: Record; + keyEntities: Record>; } /** * Get TreeNode props with Tree props. */ -export function getTreeNodeProps( +export function getTreeNodeProps( key: Key, { expandedKeys, @@ -362,7 +387,7 @@ export function getTreeNodeProps( dragOverNodeKey, dropPosition, keyEntities, - }: TreeNodeRequiredProps, + }: TreeNodeRequiredProps, ) { const entity = keyEntities[key]; @@ -418,6 +443,7 @@ export function convertNodePropsToEventData(props: TreeNodeProps): EventDataNode pos, active, eventKey, + key: eventKey, }; if (!('props' in eventData)) { Object.defineProperty(eventData, 'props', { diff --git a/components/vc-virtual-list/List.tsx b/components/vc-virtual-list/List.tsx index c55017c94..fbf883ea3 100644 --- a/components/vc-virtual-list/List.tsx +++ b/components/vc-virtual-list/List.tsx @@ -96,6 +96,7 @@ const List = defineComponent({ onScroll: PropTypes.func, onMousedown: PropTypes.func, onMouseenter: PropTypes.func, + onVisibleChange: Function as PropType<(visibleList: any[], fullList: any[]) => void>, }, setup(props, { expose }) { // ================================= MISC ================================= @@ -400,6 +401,20 @@ const List = defineComponent({ return cs; }); + // ================================ Effect ================================ + /** We need told outside that some list not rendered */ + watch( + [() => calRes.start, () => calRes.end, mergedData], + () => { + if (props.onVisibleChange) { + const renderList = mergedData.value.slice(calRes.start, calRes.end + 1); + + props.onVisibleChange(renderList, mergedData.value); + } + }, + { flush: 'post' }, + ); + return { state, mergedData, diff --git a/components/vc-virtual-list/index.ts b/components/vc-virtual-list/index.ts index 1e2ddb439..5bc1fd207 100644 --- a/components/vc-virtual-list/index.ts +++ b/components/vc-virtual-list/index.ts @@ -1,3 +1,4 @@ +// base rc-virtual-list 3.4.2 import List from './List'; export default List;