feat: tree

refactor-cascader
tangjinzhou 2022-01-06 22:10:23 +08:00
parent 98755f332c
commit 54cdc3ff40
12 changed files with 305 additions and 140 deletions

View File

@ -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 {

View File

@ -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<Key, number> = {};
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<void>((resolve, reject) => {
const onNodeLoad = (treeNode: EventDataNode) => {
const key = treeNode[fieldNames.value.key];
const loadPromise = new Promise<void>((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 (
<TreeContext
value={{
@ -1012,7 +1066,8 @@ export default defineComponent({
showIcon,
icon,
switcherIcon,
draggable,
draggable: draggableConfig,
draggingNodeKey,
checkable,
customCheckable: slots.checkable,
checkStrictly,
@ -1065,7 +1120,7 @@ export default defineComponent({
selectable={selectable}
checkable={!!checkable}
motion={motion}
dragging={dragging}
dragging={draggingNodeKey !== null}
height={height}
itemHeight={itemHeight}
virtual={virtual}

View File

@ -1,5 +1,4 @@
import { useInjectTreeContext } from './contextTypes';
import { getDataAndAria } from './util';
import Indent from './Indent';
import { convertNodePropsToEventData } from './utils/treeUtil';
import {
@ -16,6 +15,7 @@ import classNames from '../_util/classNames';
import { warning } from '../vc-util/warning';
import type { DragNodeEvent, Key } from './interface';
import pick from 'lodash-es/pick';
import pickAttrs from '../_util/pickAttrs';
const ICON_OPEN = 'open';
const ICON_CLOSE = 'close';
@ -248,6 +248,20 @@ export default defineComponent({
onNodeExpand(e, eventData.value);
};
const isDraggable = () => {
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 ? (
<span class={`${prefixCls}-draggable-icon`}>{draggable.icon}</span>
) : 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 (
<div
ref={domRef}
@ -499,7 +520,9 @@ export default defineComponent({
[`${prefixCls}-treenode-loading`]: loading,
[`${prefixCls}-treenode-active`]: active,
[`${prefixCls}-treenode-leaf-last`]: isEndNode,
[`${prefixCls}-treenode-draggable`]: draggableWithoutDisabled,
dragging,
'drop-target': dropTargetKey === eventKey,
'drop-container': dropContainerKey === eventKey,
'drag-over': !disabled && dragOver,
@ -508,15 +531,22 @@ export default defineComponent({
'filter-node': filterTreeNode && filterTreeNode(eventData.value),
})}
style={attrs.style}
// Draggable config
draggable={draggableWithoutDisabled}
aria-grabbed={dragging}
onDragstart={draggableWithoutDisabled ? onDragStart : undefined}
// Drop config
onDragenter={mergedDraggable ? onDragEnter : undefined}
onDragover={mergedDraggable ? onDragOver : undefined}
onDragleave={mergedDraggable ? onDragLeave : undefined}
onDrop={mergedDraggable ? onDrop : undefined}
onDragend={mergedDraggable ? onDragEnd : undefined}
onMousemove={onMousemove}
{...ariaSelected}
{...dataOrAriaAttributeProps}
>
<Indent prefixCls={prefixCls} level={level} isStart={isStart} isEnd={isEnd} />
{renderDragHandler()}
{renderSwitcher()}
{renderCheckbox()}
{renderSelector()}

View File

@ -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<Key, DataEntity>;
keyEntities: Record<Key, DataEntity<any>>;
// 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;
};
}

View File

@ -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;

View File

@ -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<Entity, 'node' | 'parent' | 'children'> {
node: DataNode;
parent?: DataEntity;
children?: DataEntity[];
export interface DataEntity<TreeDataType extends BasicDataNode = DataNode>
extends Omit<Entity, 'node' | 'parent' | 'children'> {
node: TreeDataType;
nodes: TreeDataType[];
parent?: DataEntity<TreeDataType>;
children?: DataEntity<TreeDataType>[];
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;
}

View File

@ -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<Key[]> },
loadingKeys: { type: Array as PropType<Key[]> },
halfCheckedKeys: { type: Array as PropType<Key[]> },
keyEntities: { type: Object as PropType<Record<Key, DataEntity>> },
keyEntities: { type: Object as PropType<Record<Key, DataEntity<DataNode>>> },
dragging: { type: Boolean as PropType<boolean> },
dragOverNodeKey: { type: [String, Number] as PropType<Key> },
@ -106,8 +107,17 @@ export const nodeListProps = {
};
export type NodeListProps = Partial<ExtractPropTypes<typeof nodeListProps>>;
export type AllowDrop = (options: { dropNode: DataNode; dropPosition: -1 | 0 | 1 }) => boolean;
export interface AllowDropOptions<TreeDataType extends BasicDataNode = DataNode> {
dragNode: EventDataNode;
dropNode: TreeDataType;
dropPosition: -1 | 0 | 1;
}
export type AllowDrop<TreeDataType extends BasicDataNode = DataNode> = (
options: AllowDropOptions<TreeDataType>,
) => 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<DraggableFn | boolean> },
defaultExpandParent: { type: Boolean, default: undefined },
autoExpandParent: { type: Boolean, default: undefined },
defaultExpandAll: { type: Boolean, default: undefined },

View File

@ -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, DataEntity>): Key[] {
export function getDragChildrenKeys<TreeDataType extends BasicDataNode = DataNode>(
dragNodeKey: Key,
keyEntities: Record<Key, DataEntity<TreeDataType>>,
): Key[] {
// not contains self
// self for left or right drag
const dragChildrenKeys = [];
const entity = keyEntities[dragNodeKey];
function dig(list: DataEntity[] = []) {
function dig(list: DataEntity<TreeDataType>[] = []) {
list.forEach(({ key, children }) => {
dragChildrenKeys.push(key);
dig(children);
@ -63,7 +68,9 @@ export function getDragChildrenKeys(dragNodeKey: Key, keyEntities: Record<Key, D
return dragChildrenKeys;
}
export function isLastChild(treeNodeEntity: DataEntity) {
export function isLastChild<TreeDataType extends BasicDataNode = DataNode>(
treeNodeEntity: DataEntity<TreeDataType>,
) {
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<TreeDataType extends BasicDataNode = DataNode>(
treeNodeEntity: DataEntity<TreeDataType>,
) {
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<TreeDataType extends BasicDataNode = DataNode>(
event: MouseEvent,
_dragNode: DragNodeEvent,
dragNode: DragNodeEvent,
targetNode: DragNodeEvent,
indent: number,
startMousePosition: {
x: number;
y: number;
},
allowDrop: AllowDrop,
allowDrop: AllowDrop<TreeDataType>,
flattenedNodes: FlattenNode[],
keyEntities: Record<Key, DataEntity>,
keyEntities: Record<Key, DataEntity<TreeDataType>>,
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<TreeDataType> = 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<Key, Dat
return [...expandedKeys];
}
/**
* Returns only the data- and aria- key/value pairs
*/
export function getDataAndAria(props: Partial<TreeProps | TreeNodeProps>) {
const omitProps: Record<string, string> = {};
Object.keys(props).forEach(key => {
if (key.startsWith('data-') || key.startsWith('aria-')) {
omitProps[key] = props[key];
}
});
return omitProps;
}

View File

@ -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<Key>, checkedKeys: Set<Key>)
return filteredKeys;
}
export function isCheckDisabled(node: DataNode) {
export function isCheckDisabled<TreeDataType>(node: TreeDataType) {
const { disabled, disableCheckbox, checkable } = (node || {}) as DataNode;
return !!(disabled || disableCheckbox) || checkable === false;
}
// Fill miss keys
function fillConductCheck(
function fillConductCheck<TreeDataType extends BasicDataNode = DataNode>(
keys: Set<Key>,
levelEntities: Map<number, Set<DataEntity>>,
levelEntities: Map<number, Set<DataEntity<TreeDataType>>>,
maxLevel: number,
syntheticGetCheckDisabled: GetCheckDisabled<DataNode>,
syntheticGetCheckDisabled: GetCheckDisabled<TreeDataType>,
): ConductReturnType {
const checkedKeys = new Set<Key>(keys);
const halfCheckedKeys = new Set<Key>();
@ -98,12 +98,12 @@ function fillConductCheck(
}
// Remove useless key
function cleanConductCheck(
function cleanConductCheck<TreeDataType extends BasicDataNode = DataNode>(
keys: Set<Key>,
halfKeys: Key[],
levelEntities: Map<number, Set<DataEntity>>,
levelEntities: Map<number, Set<DataEntity<TreeDataType>>>,
maxLevel: number,
syntheticGetCheckDisabled: GetCheckDisabled<DataNode>,
syntheticGetCheckDisabled: GetCheckDisabled<TreeDataType>,
): ConductReturnType {
const checkedKeys = new Set<Key>(keys);
let halfCheckedKeys = new Set<Key>(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<TreeDataType extends BasicDataNode = DataNode>(
keyList: Key[],
checked: true | { checked: false; halfCheckedKeys: Key[] },
keyEntities: Record<Key, DataEntity>,
getCheckDisabled?: GetCheckDisabled<DataNode>,
keyEntities: Record<Key, DataEntity<TreeDataType>>,
getCheckDisabled?: GetCheckDisabled<TreeDataType>,
): ConductReturnType {
const warningMissKeys: Key[] = [];
let syntheticGetCheckDisabled: GetCheckDisabled<DataNode>;
let syntheticGetCheckDisabled: GetCheckDisabled<TreeDataType>;
if (getCheckDisabled) {
syntheticGetCheckDisabled = getCheckDisabled;
} else {
@ -208,7 +208,7 @@ export function conductCheck(
return hasEntity;
}),
);
const levelEntities = new Map<number, Set<DataEntity>>();
const levelEntities = new Map<number, Set<DataEntity<TreeDataType>>>();
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<DataEntity> = levelEntities.get(level);
let levelSet: Set<DataEntity<TreeDataType>> = 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<TreeDataType>(
keys,
levelEntities,
maxLevel,
syntheticGetCheckDisabled,
);
} else {
result = cleanConductCheck(
keys,

View File

@ -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<FieldNames> {
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<TreeDataType extends BasicDataNode = DataNode> {
expandedKeys: Key[];
selectedKeys: Key[];
loadedKeys: Key[];
@ -344,13 +369,13 @@ export interface TreeNodeRequiredProps {
halfCheckedKeys: Key[];
dragOverNodeKey: Key;
dropPosition: number;
keyEntities: Record<Key, DataEntity>;
keyEntities: Record<Key, DataEntity<TreeDataType>>;
}
/**
* Get TreeNode props with Tree props.
*/
export function getTreeNodeProps(
export function getTreeNodeProps<TreeDataType extends BasicDataNode = DataNode>(
key: Key,
{
expandedKeys,
@ -362,7 +387,7 @@ export function getTreeNodeProps(
dragOverNodeKey,
dropPosition,
keyEntities,
}: TreeNodeRequiredProps,
}: TreeNodeRequiredProps<TreeDataType>,
) {
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', {

View File

@ -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,

View File

@ -1,3 +1,4 @@
// base rc-virtual-list 3.4.2
import List from './List';
export default List;