refactor: tree

pull/4577/head
tangjinzhou 2021-08-17 14:36:46 +08:00
parent af0620d14e
commit 3117c2748b
25 changed files with 3553 additions and 1897 deletions

View File

@ -0,0 +1,34 @@
import { CSSProperties } from 'vue';
export default function DropIndicator({
dropPosition,
dropLevelOffset,
indent,
}: {
dropPosition: -1 | 0 | 1;
dropLevelOffset: number;
indent: number;
}) {
const style: CSSProperties = {
pointerEvents: 'none',
position: 'absolute',
right: 0,
backgroundColor: 'red',
height: `${2}px`,
};
switch (dropPosition) {
case -1:
style.top = 0;
style.left = `${-dropLevelOffset * indent}px`;
break;
case 1:
style.bottom = 0;
style.left = `${-dropLevelOffset * indent}px`;
break;
case 0:
style.bottom = 0;
style.left = `${indent}`;
break;
}
return <div style={style} />;
}

View File

@ -0,0 +1,31 @@
interface IndentProps {
prefixCls: string;
level: number;
isStart: boolean[];
isEnd: boolean[];
}
const Indent = ({ prefixCls, level, isStart, isEnd }: IndentProps) => {
const baseClassName = `${prefixCls}-indent-unit`;
const list = [];
for (let i = 0; i < level; i += 1) {
list.push(
<span
key={i}
class={{
[baseClassName]: true,
[`${baseClassName}-start`]: isStart[i],
[`${baseClassName}-end`]: isEnd[i],
}}
/>,
);
}
return (
<span aria-hidden="true" class={`${prefixCls}-indent`}>
{list}
</span>
);
};
export default Indent;

View File

@ -0,0 +1,101 @@
import TreeNode from './TreeNode';
import { FlattenNode } from './interface';
import { getTreeNodeProps, TreeNodeRequiredProps } from './utils/treeUtil';
import { useInjectTreeContext } from './contextTypes';
import { defineComponent, onBeforeUnmount, onMounted, PropType, ref, Transition, watch } from 'vue';
import { treeNodeProps } from './props';
export default defineComponent({
name: 'MotionTreeNode',
inheritAttrs: false,
props: {
...treeNodeProps,
active: Boolean,
motion: Object,
motionNodes: { type: Array as PropType<FlattenNode[]> },
onMotionStart: Function,
onMotionEnd: Function,
motionType: String,
treeNodeRequiredProps: { type: Object as PropType<TreeNodeRequiredProps> },
},
slots: ['title', 'icon', 'switcherIcon'],
setup(props, { attrs, slots }) {
const visible = ref(true);
const context = useInjectTreeContext();
const motionedRef = ref(false);
const onMotionEnd = () => {
if (!motionedRef.value) {
props.onMotionEnd();
}
motionedRef.value = true;
};
watch(
() => props.motionNodes,
() => {
if (props.motionNodes && props.motionType === 'hide' && visible.value) {
visible.value = false;
}
},
);
onMounted(() => {
props.motionNodes && props.onMotionStart();
});
onBeforeUnmount(() => {
props.motionNodes && onMotionEnd();
});
return () => {
const { motion, motionNodes, motionType, active, treeNodeRequiredProps, ...otherProps } =
props;
if (motionNodes) {
return (
<Transition
{...motion}
appear={motionType === 'show'}
onAfterAppear={onMotionEnd}
onAfterLeave={onMotionEnd}
>
<div v-show={visible.value} class={`${context.value.prefixCls}-treenode-motion`}>
{motionNodes.map((treeNode: FlattenNode) => {
const {
data: { ...restProps },
title,
key,
isStart,
isEnd,
} = treeNode;
delete restProps.children;
const treeNodeProps = getTreeNodeProps(key, treeNodeRequiredProps);
return (
<TreeNode
v-slots={slots}
{...restProps}
{...treeNodeProps}
title={title}
active={active}
data={treeNode.data}
key={key}
isStart={isStart}
isEnd={isEnd}
/>
);
})}
</div>
</Transition>
);
}
return (
<TreeNode
v-slots={slots}
domRef={ref}
class={attrs.class}
style={attrs.style}
{...otherProps}
active={active}
/>
);
};
},
});

View File

@ -0,0 +1,321 @@
/**
* Handle virtual list of the TreeNodes.
*/
import { computed, defineComponent, ref, watch } from 'vue';
import VirtualList from '../vc-virtual-list';
import { FlattenNode, DataEntity, DataNode, ScrollTo } from './interface';
import MotionTreeNode from './MotionTreeNode';
import { nodeListProps } from './props';
import { findExpandedKeys, getExpandRange } from './utils/diffUtil';
import { getTreeNodeProps, getKey } from './utils/treeUtil';
const HIDDEN_STYLE = {
width: 0,
height: 0,
display: 'flex',
overflow: 'hidden',
opacity: 0,
border: 0,
padding: 0,
margin: 0,
};
const noop = () => {};
export const MOTION_KEY = `RC_TREE_MOTION_${Math.random()}`;
const MotionNode: DataNode = {
key: MOTION_KEY,
};
export const MotionEntity: DataEntity = {
key: MOTION_KEY,
level: 0,
index: 0,
pos: '0',
node: MotionNode,
};
const MotionFlattenData: FlattenNode = {
parent: null,
children: [],
pos: MotionEntity.pos,
data: MotionNode,
title: null,
key: MOTION_KEY,
/** Hold empty list here since we do not use it */
isStart: [],
isEnd: [],
};
export interface NodeListRef {
scrollTo: ScrollTo;
getIndentWidth: () => number;
}
/**
* We only need get visible content items to play the animation.
*/
export function getMinimumRangeTransitionRange(
list: FlattenNode[],
virtual: boolean,
height: number,
itemHeight: number,
) {
if (virtual === false || !height) {
return list;
}
return list.slice(0, Math.ceil(height / itemHeight) + 1);
}
function itemKey(item: FlattenNode) {
const {
data: { key },
pos,
} = item;
return getKey(key, pos);
}
function getAccessibilityPath(item: FlattenNode): string {
let path = String(item.data.key);
let current = item;
while (current.parent) {
current = current.parent;
path = `${current.data.key} > ${path}`;
}
return path;
}
export default defineComponent({
name: 'NodeList',
inheritAttrs: false,
props: nodeListProps,
setup(props, { expose, attrs, slots }) {
// =============================== Ref ================================
const listRef = ref(null);
const indentMeasurerRef = ref(null);
expose({
scrollTo: scroll => {
listRef.value.scrollTo(scroll);
},
getIndentWidth: () => indentMeasurerRef.value.offsetWidth,
});
// ============================== Motion ==============================
const transitionData = ref<FlattenNode[]>(props.data);
const transitionRange = ref([]);
const motionType = ref<'show' | 'hide' | null>(null);
function onMotionEnd() {
transitionData.value = props.data;
transitionRange.value = [];
motionType.value = null;
props.onListChangeEnd();
}
watch(
[() => ({ ...props.expandedKeys }), () => props.data],
([expandedKeys, data], [prevExpandedKeys, prevData]) => {
const diffExpanded = findExpandedKeys(prevExpandedKeys, expandedKeys);
if (diffExpanded.key !== null) {
const { virtual, height, itemHeight } = props;
if (diffExpanded.add) {
const keyIndex = prevData.findIndex(({ data: { key } }) => key === diffExpanded.key);
const rangeNodes = getMinimumRangeTransitionRange(
getExpandRange(prevData, data, diffExpanded.key),
virtual,
height,
itemHeight,
);
const newTransitionData: FlattenNode[] = prevData.slice();
newTransitionData.splice(keyIndex + 1, 0, MotionFlattenData);
transitionData.value = newTransitionData;
transitionRange.value = rangeNodes;
motionType.value = 'show';
} else {
const keyIndex = data.findIndex(({ data: { key } }) => key === diffExpanded.key);
const rangeNodes = getMinimumRangeTransitionRange(
getExpandRange(data, prevData, diffExpanded.key),
virtual,
height,
itemHeight,
);
const newTransitionData: FlattenNode[] = data.slice();
newTransitionData.splice(keyIndex + 1, 0, MotionFlattenData);
transitionData.value = newTransitionData;
transitionRange.value = rangeNodes;
motionType.value = 'hide';
}
} else if (prevData !== data) {
transitionData.value = data;
}
},
{ immediate: true },
);
// We should clean up motion if is changed by dragging
watch(
() => props.dragging,
dragging => {
if (!dragging) {
onMotionEnd();
}
},
);
const mergedData = computed(() => (props.motion ? transitionData.value : props.data));
return () => {
const {
prefixCls,
data,
selectable,
checkable,
expandedKeys,
selectedKeys,
checkedKeys,
loadedKeys,
loadingKeys,
halfCheckedKeys,
keyEntities,
disabled,
dragging,
dragOverNodeKey,
dropPosition,
motion,
height,
itemHeight,
virtual,
focusable,
activeItem,
focused,
tabindex,
onKeydown,
onFocus,
onBlur,
onActiveChange,
onListChangeStart,
onListChangeEnd,
...domProps
} = { ...props, ...attrs };
const treeNodeRequiredProps = {
expandedKeys,
selectedKeys,
loadedKeys,
loadingKeys,
checkedKeys,
halfCheckedKeys,
dragOverNodeKey,
dropPosition,
keyEntities,
};
return (
<>
{focused && activeItem && (
<span style={HIDDEN_STYLE} aria-live="assertive">
{getAccessibilityPath(activeItem)}
</span>
)}
<div>
<input
style={HIDDEN_STYLE}
disabled={focusable === false || disabled}
tabindex={focusable !== false ? tabindex : null}
onKeydown={onKeydown}
onFocus={onFocus}
onBlur={onBlur}
value=""
onChange={noop}
aria-label="for screen reader"
/>
</div>
<div
class={`${prefixCls}-treenode`}
aria-hidden
style={{
position: 'absolute',
pointerEvents: 'none',
visibility: 'hidden',
height: 0,
overflow: 'hidden',
}}
>
<div class={`${prefixCls}-indent`}>
<div ref={indentMeasurerRef} class={`${prefixCls}-indent-unit`} />
</div>
</div>
<VirtualList
{...domProps}
data={mergedData.value}
itemKey={itemKey as any}
height={height}
fullHeight={false}
virtual={virtual}
itemHeight={itemHeight}
prefixCls={`${prefixCls}-list`}
ref={listRef}
>
{(treeNode: FlattenNode) => {
const {
pos,
data: { ...restProps },
title,
key,
isStart,
isEnd,
} = treeNode;
const mergedKey = getKey(key, pos);
delete restProps.key;
delete restProps.children;
const treeNodeProps = getTreeNodeProps(mergedKey, treeNodeRequiredProps);
return (
<MotionTreeNode
{...restProps}
{...treeNodeProps}
title={title}
active={!!activeItem && key === activeItem.data.key}
pos={pos}
data={treeNode.data}
isStart={isStart}
isEnd={isEnd}
motion={motion}
motionNodes={key === MOTION_KEY ? transitionRange.value : null}
motionType={motionType.value}
onMotionStart={onListChangeStart}
onMotionEnd={onMotionEnd}
treeNodeRequiredProps={treeNodeRequiredProps}
onMousemove={() => {
onActiveChange(null);
}}
/>
);
}}
</VirtualList>
</>
);
};
},
});

1087
components/vc-tree/Tree.tsx Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,465 @@
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 { treeNodeProps } from './props';
import classNames from '../_util/classNames';
const ICON_OPEN = 'open';
const ICON_CLOSE = 'close';
const defaultTitle = '---';
export default defineComponent({
name: 'TreeNode',
inheritAttrs: false,
props: treeNodeProps,
isTreeNode: 1,
slots: ['title', 'icon', 'switcherIcon'],
setup(props, { attrs, expose, slots }) {
const dragNodeHighlight = ref(false);
const context = useInjectTreeContext();
const instance = getCurrentInstance();
const selectHandle = ref();
const hasChildren = computed(() => {
const { eventKey } = props;
const { keyEntities } = context.value;
const { children } = keyEntities[eventKey] || {};
return !!(children || []).length;
});
const isLeaf = computed(() => {
const { isLeaf, loaded } = props;
const { loadData } = context.value;
const has = hasChildren.value;
if (isLeaf === false) {
return false;
}
return isLeaf || (!loadData && !has) || (loadData && loaded && !has);
});
const nodeState = computed(() => {
const { expanded } = props;
if (isLeaf.value) {
return null;
}
return expanded ? ICON_OPEN : ICON_CLOSE;
});
const isDisabled = computed(() => {
const { disabled } = props;
const { disabled: treeDisabled } = context.value;
return !!(treeDisabled || disabled);
});
const isCheckable = computed(() => {
const { checkable } = props;
const { checkable: treeCheckable } = context.value;
// Return false if tree or treeNode is not checkable
if (!treeCheckable || checkable === false) return false;
return treeCheckable;
});
const isSelectable = computed(() => {
const { selectable } = props;
const { selectable: treeSelectable } = context.value;
// Ignore when selectable is undefined or null
if (typeof selectable === 'boolean') {
return selectable;
}
return treeSelectable;
});
const onSelectorDoubleClick = (e: MouseEvent) => {
const { onNodeDoubleClick } = context.value;
onNodeDoubleClick(e, convertNodePropsToEventData(props));
};
const onSelect = (e: MouseEvent) => {
if (isDisabled.value) return;
const { onNodeSelect } = context.value;
e.preventDefault();
onNodeSelect(e, convertNodePropsToEventData(props));
};
const onCheck = (e: MouseEvent) => {
if (isDisabled.value) return;
const { disableCheckbox, checked } = props;
const { onNodeCheck } = context.value;
if (!isCheckable.value || disableCheckbox) return;
e.preventDefault();
const targetChecked = !checked;
onNodeCheck(e, convertNodePropsToEventData(props), targetChecked);
};
const onSelectorClick = (e: MouseEvent) => {
// Click trigger before select/check operation
const { onNodeClick } = context.value;
onNodeClick(e, convertNodePropsToEventData(props));
if (isSelectable.value) {
onSelect(e);
} else {
onCheck(e);
}
};
const onMouseEnter = (e: MouseEvent) => {
const { onNodeMouseEnter } = context.value;
onNodeMouseEnter(e, convertNodePropsToEventData(props));
};
const onMouseLeave = (e: MouseEvent) => {
const { onNodeMouseLeave } = context.value;
onNodeMouseLeave(e, convertNodePropsToEventData(props));
};
const onContextmenu = (e: MouseEvent) => {
const { onNodeContextMenu } = context.value;
onNodeContextMenu(e, convertNodePropsToEventData(props));
};
const onDragStart = (e: DragEvent) => {
const { onNodeDragStart } = context.value;
e.stopPropagation();
dragNodeHighlight.value = true;
onNodeDragStart(e, instance.vnode);
try {
// ie throw error
// firefox-need-it
e.dataTransfer.setData('text/plain', '');
} catch (error) {
// empty
}
};
const onDragEnter = (e: DragEvent) => {
const { onNodeDragEnter } = context.value;
e.preventDefault();
e.stopPropagation();
onNodeDragEnter(e, instance.vnode);
};
const onDragOver = (e: DragEvent) => {
const { onNodeDragOver } = context.value;
e.preventDefault();
e.stopPropagation();
onNodeDragOver(e, instance.vnode);
};
const onDragLeave = (e: DragEvent) => {
const { onNodeDragLeave } = context.value;
e.stopPropagation();
onNodeDragLeave(e, instance.vnode);
};
const onDragEnd = (e: DragEvent) => {
const { onNodeDragEnd } = context.value;
e.stopPropagation();
dragNodeHighlight.value = false;
onNodeDragEnd(e, instance.vnode);
};
const onDrop = (e: DragEvent) => {
const { onNodeDrop } = context.value;
e.preventDefault();
e.stopPropagation();
dragNodeHighlight.value = false;
onNodeDrop(e, instance.vnode);
};
// Disabled item still can be switch
const onExpand = e => {
const { onNodeExpand } = context.value;
if (props.loading) return;
onNodeExpand(e, convertNodePropsToEventData(props));
};
const renderSwitcherIconDom = (isLeaf: boolean) => {
const { switcherIcon: switcherIconFromProps = slots.switcherIcon } = props;
const { switcherIcon: switcherIconFromCtx } = context.value;
const switcherIcon = switcherIconFromProps || switcherIconFromCtx;
// if switcherIconDom is null, no render switcher span
if (typeof switcherIcon === 'function') {
return switcherIcon({ ...props, isLeaf });
}
return switcherIcon;
};
// Load data to avoid default expanded tree without data
const syncLoadData = () => {
const { expanded, loading, loaded } = props;
const { loadData, onNodeLoad } = context.value;
if (loading) {
return;
}
// read from state to avoid loadData at same time
if (loadData && expanded && !isLeaf.value) {
// We needn't reload data when has children in sync logic
// It's only needed in node expanded
if (!hasChildren.value && !loaded) {
onNodeLoad(convertNodePropsToEventData(props));
}
}
};
onMounted(() => {
syncLoadData();
});
onUpdated(() => {
syncLoadData();
});
// Switcher
const renderSwitcher = () => {
const { expanded } = props;
const { prefixCls } = context.value;
if (isLeaf.value) {
// if switcherIconDom is null, no render switcher span
const switcherIconDom = renderSwitcherIconDom(true);
return switcherIconDom !== false ? (
<span class={classNames(`${prefixCls}-switcher`, `${prefixCls}-switcher-noop`)}>
{switcherIconDom}
</span>
) : null;
}
const switcherCls = classNames(
`${prefixCls}-switcher`,
`${prefixCls}-switcher_${expanded ? ICON_OPEN : ICON_CLOSE}`,
);
const switcherIconDom = renderSwitcherIconDom(false);
return switcherIconDom !== false ? (
<span onClick={onExpand} class={switcherCls}>
{switcherIconDom}
</span>
) : null;
};
// Checkbox
const renderCheckbox = () => {
const { checked, halfChecked, disableCheckbox } = props;
const { prefixCls } = context.value;
const disabled = isDisabled.value;
const checkable = isCheckable.value;
if (!checkable) return null;
// [Legacy] Custom element should be separate with `checkable` in future
const $custom = typeof checkable !== 'boolean' ? checkable : null;
return (
<span
class={classNames(
`${prefixCls}-checkbox`,
checked && `${prefixCls}-checkbox-checked`,
!checked && halfChecked && `${prefixCls}-checkbox-indeterminate`,
(disabled || disableCheckbox) && `${prefixCls}-checkbox-disabled`,
)}
onClick={onCheck}
>
{$custom}
</span>
);
};
const renderIcon = () => {
const { loading } = props;
const { prefixCls } = context.value;
return (
<span
class={classNames(
`${prefixCls}-iconEle`,
`${prefixCls}-icon__${nodeState.value || 'docu'}`,
loading && `${prefixCls}-icon_loading`,
)}
/>
);
};
const renderDropIndicator = () => {
const { disabled, eventKey } = props;
const {
draggable,
dropLevelOffset,
dropPosition,
prefixCls,
indent,
dropIndicatorRender,
dragOverNodeKey,
direction,
} = context.value;
const mergedDraggable = draggable !== false;
// allowDrop is calculated in Tree.tsx, there is no need for calc it here
const showIndicator = !disabled && mergedDraggable && dragOverNodeKey === eventKey;
return showIndicator
? dropIndicatorRender({ dropPosition, dropLevelOffset, indent, prefixCls, direction })
: null;
};
// Icon + Title
const renderSelector = () => {
const { title = slots.title, selected, icon = slots.icon, loading, data } = props;
const {
prefixCls,
showIcon,
icon: treeIcon,
draggable,
loadData,
titleRender,
} = context.value;
const disabled = isDisabled.value;
const mergedDraggable = typeof draggable === 'function' ? draggable(data) : draggable;
const wrapClass = `${prefixCls}-node-content-wrapper`;
// Icon - Still show loading icon when loading without showIcon
let $icon;
if (showIcon) {
const currentIcon = icon || treeIcon;
$icon = currentIcon ? (
<span class={classNames(`${prefixCls}-iconEle`, `${prefixCls}-icon__customize`)}>
{typeof currentIcon === 'function' ? currentIcon(props) : currentIcon}
</span>
) : (
renderIcon()
);
} else if (loadData && loading) {
$icon = renderIcon();
}
// Title
let titleNode: any;
if (typeof title === 'function') {
titleNode = title(data);
} else if (titleRender) {
titleNode = titleRender(data);
} else {
titleNode = title === undefined ? defaultTitle : title;
}
const $title = <span class={`${prefixCls}-title`}>{titleNode}</span>;
return (
<span
ref={selectHandle}
title={typeof title === 'string' ? title : ''}
class={classNames(
`${wrapClass}`,
`${wrapClass}-${nodeState.value || 'normal'}`,
!disabled && (selected || dragNodeHighlight) && `${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}
{renderDropIndicator()}
</span>
);
};
return () => {
const {
eventKey,
dragOver,
dragOverGapTop,
dragOverGapBottom,
isLeaf,
isStart,
isEnd,
expanded,
selected,
checked,
halfChecked,
loading,
domRef,
active,
data,
onMousemove,
...otherProps
} = { ...props, ...attrs };
const { prefixCls, filterTreeNode, draggable, keyEntities, dropContainerKey, dropTargetKey } =
context.value;
const disabled = isDisabled.value;
const dataOrAriaAttributeProps = getDataAndAria(otherProps);
const { level } = keyEntities[eventKey] || {};
const isEndNode = isEnd[isEnd.length - 1];
const mergedDraggable = typeof draggable === 'function' ? draggable(data) : draggable;
return (
<div
ref={domRef}
class={classNames(attrs.class, `${prefixCls}-treenode`, {
[`${prefixCls}-treenode-disabled`]: disabled,
[`${prefixCls}-treenode-switcher-${expanded ? 'open' : 'close'}`]: !isLeaf,
[`${prefixCls}-treenode-checkbox-checked`]: checked,
[`${prefixCls}-treenode-checkbox-indeterminate`]: halfChecked,
[`${prefixCls}-treenode-selected`]: selected,
[`${prefixCls}-treenode-loading`]: loading,
[`${prefixCls}-treenode-active`]: active,
[`${prefixCls}-treenode-leaf-last`]: isEndNode,
'drop-target': dropTargetKey === eventKey,
'drop-container': dropContainerKey === eventKey,
'drag-over': !disabled && dragOver,
'drag-over-gap-top': !disabled && dragOverGapTop,
'drag-over-gap-bottom': !disabled && dragOverGapBottom,
'filter-node': filterTreeNode && filterTreeNode(convertNodePropsToEventData(props)),
})}
style={attrs.style}
onDragenter={mergedDraggable ? onDragEnter : undefined}
onDragover={mergedDraggable ? onDragOver : undefined}
onDragleave={mergedDraggable ? onDragLeave : undefined}
onDrop={mergedDraggable ? onDrop : undefined}
onDragend={mergedDraggable ? onDragEnd : undefined}
onMousemove={onMousemove}
{...dataOrAriaAttributeProps}
>
<Indent prefixCls={prefixCls} level={level} isStart={isStart} isEnd={isEnd} />
{renderSwitcher()}
{renderCheckbox()}
{renderSelector()}
</div>
);
};
},
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 381 B

View File

@ -0,0 +1,104 @@
/**
* Webpack has bug for import loop, which is not the same behavior as ES module.
* When util.js imports the TreeNode for tree generate will cause treeContextTypes be empty.
*/
import type { ComputedRef, InjectionKey, PropType } from 'vue';
import { inject } from 'vue';
import { computed } from 'vue';
import { defineComponent, provide } from 'vue';
import type { VueNode } from '../_util/type';
import type {
IconType,
Key,
DataEntity,
EventDataNode,
NodeInstance,
DataNode,
Direction,
} from './interface';
export type NodeMouseEventParams = {
event: MouseEvent;
node: EventDataNode;
};
export type NodeDragEventParams = {
event: MouseEvent;
node: EventDataNode;
};
export type NodeMouseEventHandler = (e: MouseEvent, node: EventDataNode) => void;
export type NodeDragEventHandler = (
e: MouseEvent,
node: NodeInstance,
outsideTree?: boolean,
) => void;
export interface TreeContextProps {
prefixCls: string;
selectable: boolean;
showIcon: boolean;
icon: IconType;
switcherIcon: IconType;
draggable: ((node: DataNode) => boolean) | boolean;
checkable: boolean | VueNode;
checkStrictly: boolean;
disabled: boolean;
keyEntities: Record<Key, DataEntity>;
// for details see comment in Tree.state (Tree.tsx)
dropLevelOffset?: number;
dropContainerKey: Key | null;
dropTargetKey: Key | null;
dropPosition: -1 | 0 | 1 | null;
indent: number | null;
dropIndicatorRender: (props: {
dropPosition: -1 | 0 | 1;
dropLevelOffset: number;
indent: number | null;
prefixCls: string;
direction: Direction;
}) => VueNode;
dragOverNodeKey: Key | null;
direction: Direction;
loadData: (treeNode: EventDataNode) => Promise<void>;
filterTreeNode: (treeNode: EventDataNode) => boolean;
titleRender?: (node: DataNode) => VueNode;
onNodeClick: NodeMouseEventHandler;
onNodeDoubleClick: NodeMouseEventHandler;
onNodeExpand: NodeMouseEventHandler;
onNodeSelect: NodeMouseEventHandler;
onNodeCheck: (e: MouseEvent, treeNode: EventDataNode, checked: boolean) => void;
onNodeLoad: (treeNode: EventDataNode) => void;
onNodeMouseEnter: NodeMouseEventHandler;
onNodeMouseLeave: NodeMouseEventHandler;
onNodeContextMenu: NodeMouseEventHandler;
onNodeDragStart: NodeDragEventHandler;
onNodeDragEnter: NodeDragEventHandler;
onNodeDragOver: NodeDragEventHandler;
onNodeDragLeave: NodeDragEventHandler;
onNodeDragEnd: NodeDragEventHandler;
onNodeDrop: NodeDragEventHandler;
}
const TreeContextKey: InjectionKey<ComputedRef<TreeContextProps>> = Symbol('TreeContextKey');
export const TreeContext = defineComponent({
props: {
value: { type: Object as PropType<TreeContextProps> },
},
setup(props, { slots }) {
provide(
TreeContextKey,
computed(() => props.value),
);
return slots.default?.();
},
});
export const useInjectTreeContext = () => {
return inject(
TreeContextKey,
computed(() => ({} as TreeContextProps)),
);
};

View File

@ -1,4 +0,0 @@
// based on rc-tree 2.1.3
import Tree from './src';
export default Tree;

View File

@ -0,0 +1,8 @@
import Tree from './Tree';
import TreeNode from './TreeNode';
import type { TreeProps } from './Tree';
import type { TreeNodeProps } from './TreeNode';
export { TreeNode };
export type { TreeProps, TreeNodeProps };
export default Tree;

View File

@ -0,0 +1,87 @@
import { VNode } from 'vue';
export type { ScrollTo } from '../vc-virtual-list/List';
export interface DataNode {
checkable?: boolean;
children?: DataNode[];
disabled?: boolean;
disableCheckbox?: boolean;
icon?: IconType;
isLeaf?: boolean;
key: string | number;
title?: any;
selectable?: boolean;
switcherIcon?: IconType;
/** Set style of TreeNode. This is not recommend if you don't have any force requirement */
// className?: string;
// style?: CSSProperties;
}
export interface EventDataNode extends DataNode {
expanded: boolean;
selected: boolean;
checked: boolean;
loaded: boolean;
loading: boolean;
halfChecked: boolean;
dragOver: boolean;
dragOverGapTop: boolean;
dragOverGapBottom: boolean;
pos: string;
active: boolean;
}
export type IconType = any;
export type Key = string | number;
export type NodeElement = VNode & {
selectHandle?: HTMLSpanElement;
type: {
isTreeNode: boolean;
};
};
export type NodeInstance = VNode & {
selectHandle?: HTMLSpanElement;
};
export interface Entity {
node: NodeElement;
index: number;
key: Key;
pos: string;
parent?: Entity;
children?: Entity[];
}
export interface DataEntity extends Omit<Entity, 'node' | 'parent' | 'children'> {
node: DataNode;
parent?: DataEntity;
children?: DataEntity[];
level: number;
}
export interface FlattenNode {
parent: FlattenNode | null;
children: FlattenNode[];
pos: string;
data: DataNode;
title: any;
key: Key;
isStart: boolean[];
isEnd: boolean[];
}
export type GetKey<RecordType> = (record: RecordType, index?: number) => Key;
export type GetCheckDisabled<RecordType> = (record: RecordType) => boolean;
export type Direction = 'ltr' | 'rtl' | undefined;
export interface FieldNames {
title?: string;
key?: string;
children?: string;
}

233
components/vc-tree/props.ts Normal file
View File

@ -0,0 +1,233 @@
import type { ExtractPropTypes, PropType } from 'vue';
import PropTypes from '../_util/vue-types';
import type {
NodeDragEventParams,
NodeMouseEventHandler,
NodeMouseEventParams,
} from './contextTypes';
import type { DataNode, Key, FlattenNode, DataEntity, EventDataNode, Direction } from './interface';
import { fillFieldNames } from './utils/treeUtil';
export interface CheckInfo {
event: 'check';
node: EventDataNode;
checked: boolean;
nativeEvent: MouseEvent;
checkedNodes: DataNode[];
checkedNodesPositions?: { node: DataNode; pos: string }[];
halfCheckedKeys?: Key[];
}
export const treeNodeProps = {
eventKey: [String, Number], // Pass by parent `cloneElement`
prefixCls: String,
// By parent
expanded: { type: Boolean, default: undefined },
selected: { type: Boolean, default: undefined },
checked: { type: Boolean, default: undefined },
loaded: { type: Boolean, default: undefined },
loading: { type: Boolean, default: undefined },
halfChecked: { type: Boolean, default: undefined },
title: PropTypes.any,
dragOver: { type: Boolean, default: undefined },
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[]> },
isEnd: { type: Array as PropType<boolean[]> },
active: { type: Boolean, default: undefined },
onMousemove: { type: Function as PropType<EventHandlerNonNull> },
// By user
isLeaf: { type: Boolean, default: undefined },
checkable: { type: Boolean, default: undefined },
selectable: { type: Boolean, default: undefined },
disabled: { type: Boolean, default: undefined },
disableCheckbox: { type: Boolean, default: undefined },
icon: PropTypes.any,
switcherIcon: PropTypes.any,
domRef: { type: Function as PropType<(arg: any) => void> },
};
export type TreeNodeProps = Partial<ExtractPropTypes<typeof treeNodeProps>>;
export const nodeListProps = {
prefixCls: { type: String as PropType<string> },
data: { type: Array as PropType<FlattenNode[]> },
motion: { type: Object as PropType<any> },
focusable: { type: Boolean as PropType<boolean> },
activeItem: { type: Object as PropType<FlattenNode> },
focused: { type: Boolean as PropType<boolean> },
tabindex: { type: Number as PropType<number> },
checkable: { type: Boolean as PropType<boolean> },
selectable: { type: Boolean as PropType<boolean> },
disabled: { type: Boolean as PropType<boolean> },
expandedKeys: { type: Array as PropType<Key[]> },
selectedKeys: { type: Array as PropType<Key[]> },
checkedKeys: { type: Array as PropType<Key[]> },
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>> },
dragging: { type: Boolean as PropType<boolean> },
dragOverNodeKey: { type: [String, Number] as PropType<Key> },
dropPosition: { type: Number as PropType<number> },
// Virtual list
height: { type: Number as PropType<number> },
itemHeight: { type: Number as PropType<number> },
virtual: { type: Boolean as PropType<boolean> },
onKeydown: { type: Function as PropType<EventHandlerNonNull> },
onFocus: { type: Function as PropType<(e: FocusEvent) => void> },
onBlur: { type: Function as PropType<(e: FocusEvent) => void> },
onActiveChange: { type: Function as PropType<(key: Key) => void> },
onContextmenu: { type: Function as PropType<EventHandlerNonNull> },
onListChangeStart: { type: Function as PropType<() => void> },
onListChangeEnd: { type: Function as PropType<() => void> },
};
export type NodeListProps = Partial<ExtractPropTypes<typeof nodeListProps>>;
export type AllowDrop = (options: { dropNode: DataNode; dropPosition: -1 | 0 | 1 }) => boolean;
export const treeProps = () => ({
prefixCls: String,
focusable: { type: Boolean, default: undefined },
tabindex: Number,
children: PropTypes.VNodeChild,
treeData: { type: Array as PropType<DataNode[]> }, // Generate treeNode by children
fieldNames: fillFieldNames,
showLine: { type: Boolean, default: undefined },
showIcon: { type: Boolean, default: undefined },
icon: PropTypes.any,
selectable: { type: Boolean, default: undefined },
disabled: { type: Boolean, default: undefined },
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> },
defaultExpandParent: { type: Boolean, default: undefined },
autoExpandParent: { type: Boolean, default: undefined },
defaultExpandAll: { type: Boolean, default: undefined },
defaultExpandedKeys: { type: Array as PropType<Key[]> },
expandedKeys: { type: Array as PropType<Key[]> },
defaultCheckedKeys: { type: Array as PropType<Key[]> },
checkedKeys: {
type: [Object, Array] as PropType<Key[] | { checked: Key[]; halfChecked: Key[] }>,
},
defaultSelectedKeys: { type: Array as PropType<Key[]> },
selectedKeys: { type: Array as PropType<Key[]> },
allowDrop: { type: Function as PropType<AllowDrop> },
titleRender: { type: Function as PropType<(node: DataNode) => any> },
dropIndicatorRender: {
type: Function as PropType<
(props: {
dropPosition: -1 | 0 | 1;
dropLevelOffset: number;
indent: number;
prefixCls: string;
direction: Direction;
}) => any
>,
},
onFocus: { type: Function as PropType<(e: FocusEvent) => void> },
onBlur: { type: Function as PropType<(e: FocusEvent) => void> },
onKeyDown: { type: Function as PropType<EventHandlerNonNull> },
onContextmenu: { type: Function as PropType<EventHandlerNonNull> },
onClick: { type: Function as PropType<NodeMouseEventHandler> },
onDblClick: { type: Function as PropType<NodeMouseEventHandler> },
onScroll: { type: Function as PropType<EventHandlerNonNull> },
onExpand: {
type: Function as PropType<
(
expandedKeys: Key[],
info: {
node: EventDataNode;
expanded: boolean;
nativeEvent: MouseEvent;
},
) => void
>,
},
onCheck: {
type: Function as PropType<
(checked: { checked: Key[]; halfChecked: Key[] } | Key[], info: CheckInfo) => void
>,
},
onSelect: {
type: Function as PropType<
(
selectedKeys: Key[],
info: {
event: 'select';
selected: boolean;
node: EventDataNode;
selectedNodes: DataNode[];
nativeEvent: MouseEvent;
},
) => void
>,
},
onLoad: {
type: Function as PropType<
(
loadedKeys: Key[],
info: {
event: 'load';
node: EventDataNode;
},
) => void
>,
},
loadData: { type: Function as PropType<(treeNode: EventDataNode) => Promise<void>> },
loadedKeys: { type: Array as PropType<Key[]> },
onMouseenter: { type: Function as PropType<(info: NodeMouseEventParams) => void> },
onMouseleave: { type: Function as PropType<(info: NodeMouseEventParams) => void> },
onRightClick: {
type: Function as PropType<(info: { event: MouseEvent; node: EventDataNode }) => void>,
},
onDragstart: { type: Function as PropType<(info: NodeDragEventParams) => void> },
onDragenter: {
type: Function as PropType<(info: NodeDragEventParams & { expandedKeys: Key[] }) => void>,
},
onDragover: { type: Function as PropType<(info: NodeDragEventParams) => void> },
onDragleave: { type: Function as PropType<(info: NodeDragEventParams) => void> },
onDragend: { type: Function as PropType<(info: NodeDragEventParams) => void> },
onDrop: {
type: Function as PropType<
(
info: NodeDragEventParams & {
dragNode: EventDataNode;
dragNodesKeys: Key[];
dropPosition: number;
dropToGap: boolean;
},
) => void
>,
},
/**
* Used for `rc-tree-select` only.
* Do not use in your production code directly since this will be refactor.
*/
onActiveChange: { type: Function as PropType<(key: Key) => void> },
filterTreeNode: { type: Function as PropType<(treeNode: EventDataNode) => boolean> },
motion: PropTypes.any,
switcherIcon: PropTypes.any,
// Virtual List
height: Number,
itemHeight: Number,
virtual: { type: Boolean, default: undefined },
// direction for drag logic
direction: { type: String as PropType<Direction> },
});
export type TreeProps = Partial<ExtractPropTypes<ReturnType<typeof treeProps>>>;

View File

@ -1,686 +0,0 @@
import PropTypes, { withUndefined } from '../../_util/vue-types';
import classNames from '../../_util/classNames';
import warning from 'warning';
import { hasProp, initDefaultProps, getOptionProps, getSlot } from '../../_util/props-util';
import { cloneElement } from '../../_util/vnode';
import BaseMixin from '../../_util/BaseMixin';
import {
convertTreeToEntities,
convertDataToTree,
getPosition,
getDragNodesKeys,
parseCheckedKeys,
conductExpandParent,
calcSelectedKeys,
calcDropPosition,
arrAdd,
arrDel,
posToArr,
mapChildren,
conductCheck,
warnOnlyTreeNode,
getDataAndAria,
} from './util';
import { defineComponent } from 'vue';
/**
* Thought we still use `cloneElement` to pass `key`,
* other props can pass with context for future refactor.
*/
function getWatch(keys = []) {
const watch = {};
keys.forEach(k => {
watch[k] = {
handler() {
this.needSyncKeys[k] = true;
},
flush: 'sync',
};
});
return watch;
}
const Tree = defineComponent({
name: 'Tree',
mixins: [BaseMixin],
provide() {
return {
vcTree: this,
};
},
inheritAttrs: false,
props: initDefaultProps(
{
prefixCls: PropTypes.string,
tabindex: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
children: PropTypes.any,
treeData: PropTypes.array, // Generate treeNode by children
showLine: PropTypes.looseBool,
showIcon: PropTypes.looseBool,
icon: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
focusable: PropTypes.looseBool,
selectable: PropTypes.looseBool,
disabled: PropTypes.looseBool,
multiple: PropTypes.looseBool,
checkable: withUndefined(PropTypes.oneOfType([PropTypes.object, PropTypes.looseBool])),
checkStrictly: PropTypes.looseBool,
draggable: PropTypes.looseBool,
defaultExpandParent: PropTypes.looseBool,
autoExpandParent: PropTypes.looseBool,
defaultExpandAll: PropTypes.looseBool,
defaultExpandedKeys: PropTypes.array,
expandedKeys: PropTypes.array,
defaultCheckedKeys: PropTypes.array,
checkedKeys: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
defaultSelectedKeys: PropTypes.array,
selectedKeys: PropTypes.array,
// onClick: PropTypes.func,
// onDoubleClick: PropTypes.func,
// onExpand: PropTypes.func,
// onCheck: PropTypes.func,
// onSelect: PropTypes.func,
loadData: PropTypes.func,
loadedKeys: PropTypes.array,
// onMouseEnter: PropTypes.func,
// onMouseLeave: PropTypes.func,
// onRightClick: PropTypes.func,
// onDragStart: PropTypes.func,
// onDragEnter: PropTypes.func,
// onDragOver: PropTypes.func,
// onDragLeave: PropTypes.func,
// onDragEnd: PropTypes.func,
// onDrop: PropTypes.func,
filterTreeNode: PropTypes.func,
openTransitionName: PropTypes.string,
openAnimation: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
switcherIcon: PropTypes.any,
__propsSymbol__: PropTypes.any,
},
{
prefixCls: 'rc-tree',
showLine: false,
showIcon: true,
selectable: true,
multiple: false,
checkable: false,
disabled: false,
checkStrictly: false,
draggable: false,
defaultExpandParent: true,
autoExpandParent: false,
defaultExpandAll: false,
defaultExpandedKeys: [],
defaultCheckedKeys: [],
defaultSelectedKeys: [],
},
),
data() {
warning(this.$props.__propsSymbol__, 'must pass __propsSymbol__');
warning(this.$props.children, 'please use children prop replace slots.default');
this.needSyncKeys = {};
this.domTreeNodes = {};
const state = {
_posEntities: new Map(),
_keyEntities: new Map(),
_expandedKeys: [],
_selectedKeys: [],
_checkedKeys: [],
_halfCheckedKeys: [],
_loadedKeys: [],
_loadingKeys: [],
_treeNode: [],
_prevProps: null,
_dragOverNodeKey: '',
_dropPosition: null,
_dragNodesKeys: [],
};
return {
...state,
...this.getDerivedState(getOptionProps(this), state),
};
},
watch: {
// watch
...getWatch([
'treeData',
'children',
'expandedKeys',
'autoExpandParent',
'selectedKeys',
'checkedKeys',
'loadedKeys',
]),
__propsSymbol__() {
this.setState(this.getDerivedState(getOptionProps(this), this.$data));
this.needSyncKeys = {};
},
},
methods: {
getDerivedState(props, prevState) {
const { _prevProps } = prevState;
const newState = {
_prevProps: { ...props },
};
const self = this;
function needSync(name) {
return (!_prevProps && name in props) || (_prevProps && self.needSyncKeys[name]);
}
// ================== Tree Node ==================
let treeNode = null;
// Check if `treeData` or `children` changed and save into the state.
if (needSync('treeData')) {
treeNode = convertDataToTree(props.treeData);
} else if (needSync('children')) {
treeNode = props.children;
}
// Tree support filter function which will break the tree structure in the vdm.
// We cache the treeNodes in state so that we can return the treeNode in event trigger.
if (treeNode) {
newState._treeNode = treeNode;
// Calculate the entities data for quick match
const entitiesMap = convertTreeToEntities(treeNode);
newState._keyEntities = entitiesMap.keyEntities;
}
const keyEntities = newState._keyEntities || prevState._keyEntities;
// ================ expandedKeys =================
if (needSync('expandedKeys') || (_prevProps && needSync('autoExpandParent'))) {
newState._expandedKeys =
props.autoExpandParent || (!_prevProps && props.defaultExpandParent)
? conductExpandParent(props.expandedKeys, keyEntities)
: props.expandedKeys;
} else if (!_prevProps && props.defaultExpandAll) {
newState._expandedKeys = [...keyEntities.keys()];
} else if (!_prevProps && props.defaultExpandedKeys) {
newState._expandedKeys =
props.autoExpandParent || props.defaultExpandParent
? conductExpandParent(props.defaultExpandedKeys, keyEntities)
: props.defaultExpandedKeys;
}
// ================ selectedKeys =================
if (props.selectable) {
if (needSync('selectedKeys')) {
newState._selectedKeys = calcSelectedKeys(props.selectedKeys, props);
} else if (!_prevProps && props.defaultSelectedKeys) {
newState._selectedKeys = calcSelectedKeys(props.defaultSelectedKeys, props);
}
}
// ================= checkedKeys =================
if (props.checkable) {
let checkedKeyEntity;
if (needSync('checkedKeys')) {
checkedKeyEntity = parseCheckedKeys(props.checkedKeys) || {};
} else if (!_prevProps && props.defaultCheckedKeys) {
checkedKeyEntity = parseCheckedKeys(props.defaultCheckedKeys) || {};
} else if (treeNode) {
// If treeNode changed, we also need check it
checkedKeyEntity = parseCheckedKeys(props.checkedKeys) || {
checkedKeys: prevState._checkedKeys,
halfCheckedKeys: prevState._halfCheckedKeys,
};
}
if (checkedKeyEntity) {
let { checkedKeys = [], halfCheckedKeys = [] } = checkedKeyEntity;
if (!props.checkStrictly) {
const conductKeys = conductCheck(checkedKeys, true, keyEntities);
({ checkedKeys, halfCheckedKeys } = conductKeys);
}
newState._checkedKeys = checkedKeys;
newState._halfCheckedKeys = halfCheckedKeys;
}
}
// ================= loadedKeys ==================
if (needSync('loadedKeys')) {
newState._loadedKeys = props.loadedKeys;
}
return newState;
},
onNodeDragStart(event, node) {
const { _expandedKeys } = this.$data;
const { eventKey } = node;
const children = getSlot(node);
this.dragNode = node;
this.setState({
_dragNodesKeys: getDragNodesKeys(
typeof children === 'function' ? children() : children,
node,
),
_expandedKeys: arrDel(_expandedKeys, eventKey),
});
this.__emit('dragstart', { event, node });
},
/**
* [Legacy] Select handler is less small than node,
* so that this will trigger when drag enter node or select handler.
* This is a little tricky if customize css without padding.
* Better for use mouse move event to refresh drag state.
* But let's just keep it to avoid event trigger logic change.
*/
onNodeDragEnter(event, node) {
const { _expandedKeys: expandedKeys } = this.$data;
const { pos, eventKey } = node;
if (!this.dragNode || !node.selectHandle) return;
const dropPosition = calcDropPosition(event, node);
// Skip if drag node is self
if (this.dragNode.eventKey === eventKey && dropPosition === 0) {
this.setState({
_dragOverNodeKey: '',
_dropPosition: null,
});
return;
}
// Ref: https://github.com/react-component/tree/issues/132
// Add timeout to let onDragLevel fire before onDragEnter,
// so that we can clean drag props for onDragLeave node.
// Macro task for this:
// https://html.spec.whatwg.org/multipage/webappapis.html#clean-up-after-running-script
setTimeout(() => {
// Update drag over node
this.setState({
_dragOverNodeKey: eventKey,
_dropPosition: dropPosition,
});
// Side effect for delay drag
if (!this.delayedDragEnterLogic) {
this.delayedDragEnterLogic = {};
}
Object.keys(this.delayedDragEnterLogic).forEach(key => {
clearTimeout(this.delayedDragEnterLogic[key]);
});
this.delayedDragEnterLogic[pos] = setTimeout(() => {
const newExpandedKeys = arrAdd(expandedKeys, eventKey);
if (!hasProp(this, 'expandedKeys')) {
this.setState({
_expandedKeys: newExpandedKeys,
});
}
this.__emit('dragenter', { event, node, expandedKeys: newExpandedKeys });
}, 400);
}, 0);
},
onNodeDragOver(event, node) {
const { eventKey } = node;
const { _dragOverNodeKey, _dropPosition } = this.$data;
// Update drag position
if (this.dragNode && eventKey === _dragOverNodeKey && node.selectHandle) {
const dropPosition = calcDropPosition(event, node);
if (dropPosition === _dropPosition) return;
this.setState({
_dropPosition: dropPosition,
});
}
this.__emit('dragover', { event, node });
},
onNodeDragLeave(event, node) {
this.setState({
_dragOverNodeKey: '',
});
this.__emit('dragleave', { event, node });
},
onNodeDragEnd(event, node) {
this.setState({
_dragOverNodeKey: '',
});
this.__emit('dragend', { event, node });
this.dragNode = null;
},
onNodeDrop(event, node) {
const { _dragNodesKeys = [], _dropPosition } = this.$data;
const { eventKey, pos } = node;
this.setState({
_dragOverNodeKey: '',
});
if (_dragNodesKeys.indexOf(eventKey) !== -1) {
warning(false, "Can not drop to dragNode(include it's children node)");
return;
}
const posArr = posToArr(pos);
const dropResult = {
event,
node,
dragNode: this.dragNode,
dragNodesKeys: _dragNodesKeys.slice(),
dropPosition: _dropPosition + Number(posArr[posArr.length - 1]),
dropToGap: false,
};
if (_dropPosition !== 0) {
dropResult.dropToGap = true;
}
this.__emit('drop', dropResult);
this.dragNode = null;
},
onNodeClick(e, treeNode) {
this.__emit('click', e, treeNode);
},
onNodeDoubleClick(e, treeNode) {
this.__emit('dblclick', e, treeNode);
},
onNodeSelect(e, treeNode) {
let { _selectedKeys: selectedKeys } = this.$data;
const { _keyEntities: keyEntities } = this.$data;
const { multiple } = this.$props;
const { selected, eventKey } = getOptionProps(treeNode);
const targetSelected = !selected;
// Update selected keys
if (!targetSelected) {
selectedKeys = arrDel(selectedKeys, eventKey);
} else if (!multiple) {
selectedKeys = [eventKey];
} else {
selectedKeys = arrAdd(selectedKeys, eventKey);
}
// [Legacy] Not found related usage in doc or upper libs
const selectedNodes = selectedKeys
.map(key => {
const entity = keyEntities.get(key);
if (!entity) return null;
return entity.node;
})
.filter(node => node);
this.setUncontrolledState({ _selectedKeys: selectedKeys });
const eventObj = {
event: 'select',
selected: targetSelected,
node: treeNode,
selectedNodes,
nativeEvent: e,
};
this.__emit('select', selectedKeys, eventObj);
},
onNodeCheck(e, treeNode, checked) {
const {
_keyEntities: keyEntities,
_checkedKeys: oriCheckedKeys,
_halfCheckedKeys: oriHalfCheckedKeys,
} = this.$data;
const { checkStrictly } = this.$props;
const { eventKey } = getOptionProps(treeNode);
// Prepare trigger arguments
let checkedObj;
const eventObj = {
event: 'check',
node: treeNode,
checked,
nativeEvent: e,
};
if (checkStrictly) {
const checkedKeys = checked
? arrAdd(oriCheckedKeys, eventKey)
: arrDel(oriCheckedKeys, eventKey);
const halfCheckedKeys = arrDel(oriHalfCheckedKeys, eventKey);
checkedObj = { checked: checkedKeys, halfChecked: halfCheckedKeys };
eventObj.checkedNodes = checkedKeys
.map(key => keyEntities.get(key))
.filter(entity => entity)
.map(entity => entity.node);
this.setUncontrolledState({ _checkedKeys: checkedKeys });
} else {
const { checkedKeys, halfCheckedKeys } = conductCheck([eventKey], checked, keyEntities, {
checkedKeys: oriCheckedKeys,
halfCheckedKeys: oriHalfCheckedKeys,
});
checkedObj = checkedKeys;
// [Legacy] This is used for `rc-tree-select`
eventObj.checkedNodes = [];
eventObj.checkedNodesPositions = [];
eventObj.halfCheckedKeys = halfCheckedKeys;
checkedKeys.forEach(key => {
const entity = keyEntities.get(key);
if (!entity) return;
const { node, pos } = entity;
eventObj.checkedNodes.push(node);
eventObj.checkedNodesPositions.push({ node, pos });
});
this.setUncontrolledState({
_checkedKeys: checkedKeys,
_halfCheckedKeys: halfCheckedKeys,
});
}
this.__emit('check', checkedObj, eventObj);
},
onNodeLoad(treeNode) {
return new Promise(resolve => {
// We need to get the latest state of loading/loaded keys
this.setState(({ _loadedKeys: loadedKeys = [], _loadingKeys: loadingKeys = [] }) => {
const { loadData } = this.$props;
const { eventKey } = getOptionProps(treeNode);
if (
!loadData ||
loadedKeys.indexOf(eventKey) !== -1 ||
loadingKeys.indexOf(eventKey) !== -1
) {
return {};
}
// Process load data
const promise = loadData(treeNode);
promise.then(() => {
const { _loadedKeys: currentLoadedKeys, _loadingKeys: currentLoadingKeys } = this.$data;
const newLoadedKeys = arrAdd(currentLoadedKeys, eventKey);
const newLoadingKeys = arrDel(currentLoadingKeys, eventKey);
// onLoad should trigger before internal setState to avoid `loadData` trigger twice.
// https://github.com/ant-design/ant-design/issues/12464
this.__emit('load', newLoadedKeys, {
event: 'load',
node: treeNode,
});
this.setUncontrolledState({
_loadedKeys: newLoadedKeys,
});
this.setState({
_loadingKeys: newLoadingKeys,
});
resolve();
});
return {
_loadingKeys: arrAdd(loadingKeys, eventKey),
};
});
});
},
onNodeExpand(e, treeNode) {
let { _expandedKeys: expandedKeys } = this.$data;
const { loadData } = this.$props;
const { eventKey, expanded } = getOptionProps(treeNode);
// Update selected keys
const index = expandedKeys.indexOf(eventKey);
const targetExpanded = !expanded;
warning(
(expanded && index !== -1) || (!expanded && index === -1),
'Expand state not sync with index check',
);
if (targetExpanded) {
expandedKeys = arrAdd(expandedKeys, eventKey);
} else {
expandedKeys = arrDel(expandedKeys, eventKey);
}
this.setUncontrolledState({ _expandedKeys: expandedKeys });
this.__emit('expand', expandedKeys, {
node: treeNode,
expanded: targetExpanded,
nativeEvent: e,
});
// Async Load data
if (targetExpanded && loadData) {
const loadPromise = this.onNodeLoad(treeNode);
return loadPromise
? loadPromise.then(() => {
// [Legacy] Refresh logic
this.setUncontrolledState({ _expandedKeys: expandedKeys });
})
: null;
}
return null;
},
onNodeMouseEnter(event, node) {
this.__emit('mouseenter', { event, node });
},
onNodeMouseLeave(event, node) {
this.__emit('mouseleave', { event, node });
},
onNodeContextMenu(event, node) {
event.preventDefault();
this.__emit('rightClick', { event, node });
},
/**
* Only update the value which is not in props
*/
setUncontrolledState(state) {
let needSync = false;
const newState = {};
const props = getOptionProps(this);
Object.keys(state).forEach(name => {
if (name.replace('_', '') in props) return;
needSync = true;
newState[name] = state[name];
});
if (needSync) {
this.setState(newState);
}
},
registerTreeNode(key, node) {
if (node) {
this.domTreeNodes[key] = node;
} else {
delete this.domTreeNodes[key];
}
},
isKeyChecked(key) {
const { _checkedKeys: checkedKeys = [] } = this.$data;
return checkedKeys.indexOf(key) !== -1;
},
/**
* [Legacy] Original logic use `key` as tracking clue.
* We have to use `cloneElement` to pass `key`.
*/
renderTreeNode(child, index, level = 0) {
const {
_keyEntities: keyEntities,
_expandedKeys: expandedKeys = [],
_selectedKeys: selectedKeys = [],
_halfCheckedKeys: halfCheckedKeys = [],
_loadedKeys: loadedKeys = [],
_loadingKeys: loadingKeys = [],
_dragOverNodeKey: dragOverNodeKey,
_dropPosition: dropPosition,
} = this.$data;
const pos = getPosition(level, index);
let key = child.key;
if (!key && (key === undefined || key === null)) {
key = pos;
}
if (!keyEntities.get(key)) {
warnOnlyTreeNode();
return null;
}
return cloneElement(child, {
eventKey: key,
expanded: expandedKeys.indexOf(key) !== -1,
selected: selectedKeys.indexOf(key) !== -1,
loaded: loadedKeys.indexOf(key) !== -1,
loading: loadingKeys.indexOf(key) !== -1,
checked: this.isKeyChecked(key),
halfChecked: halfCheckedKeys.indexOf(key) !== -1,
pos,
// [Legacy] Drag props
dragOver: dragOverNodeKey === key && dropPosition === 0,
dragOverGapTop: dragOverNodeKey === key && dropPosition === -1,
dragOverGapBottom: dragOverNodeKey === key && dropPosition === 1,
key,
});
},
},
render() {
const { _treeNode: treeNode } = this.$data;
const { prefixCls, focusable, showLine, tabindex = 0 } = this.$props;
const domProps = getDataAndAria({ ...this.$props, ...this.$attrs });
const { class: className, style } = this.$attrs;
return (
<ul
{...domProps}
class={classNames(prefixCls, className, {
[`${prefixCls}-show-line`]: showLine,
})}
style={style}
role="tree"
unselectable="on"
tabindex={focusable ? tabindex : null}
>
{mapChildren(treeNode, (node, index) => this.renderTreeNode(node, index))}
</ul>
);
},
});
export { Tree };
export default Tree;

View File

@ -1,578 +0,0 @@
import { defineComponent, inject, provide } from 'vue';
import PropTypes from '../../_util/vue-types';
import classNames from '../../_util/classNames';
import { getNodeChildren, mapChildren, warnOnlyTreeNode, getDataAndAria } from './util';
import { initDefaultProps, getComponent, getSlot } from '../../_util/props-util';
import BaseMixin from '../../_util/BaseMixin';
import { getTransitionProps, Transition } from '../../_util/transition';
function noop() {}
const ICON_OPEN = 'open';
const ICON_CLOSE = 'close';
const defaultTitle = '---';
const TreeNode = defineComponent({
name: 'TreeNode',
mixins: [BaseMixin],
inheritAttrs: false,
__ANT_TREE_NODE: true,
props: initDefaultProps(
{
eventKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), // Pass by parent `cloneElement`
prefixCls: PropTypes.string,
// className: PropTypes.string,
root: PropTypes.object,
// onSelect: PropTypes.func,
// By parent
expanded: PropTypes.looseBool,
selected: PropTypes.looseBool,
checked: PropTypes.looseBool,
loaded: PropTypes.looseBool,
loading: PropTypes.looseBool,
halfChecked: PropTypes.looseBool,
title: PropTypes.any,
pos: PropTypes.string,
dragOver: PropTypes.looseBool,
dragOverGapTop: PropTypes.looseBool,
dragOverGapBottom: PropTypes.looseBool,
// By user
isLeaf: PropTypes.looseBool,
checkable: PropTypes.looseBool,
selectable: PropTypes.looseBool,
disabled: PropTypes.looseBool,
disableCheckbox: PropTypes.looseBool,
icon: PropTypes.any,
dataRef: PropTypes.object,
switcherIcon: PropTypes.any,
label: PropTypes.any,
value: PropTypes.any,
},
{},
),
setup() {
return {
vcTree: inject('vcTree', {}),
vcTreeNode: inject('vcTreeNode', {}),
};
},
data() {
this.children = null;
return {
dragNodeHighlight: false,
};
},
created() {
provide('vcTreeNode', this);
},
// Isomorphic needn't load data in server side
mounted() {
const {
eventKey,
vcTree: { registerTreeNode },
} = this;
this.syncLoadData(this.$props);
registerTreeNode && registerTreeNode(eventKey, this);
},
updated() {
this.syncLoadData(this.$props);
},
beforeUnmount() {
const {
eventKey,
vcTree: { registerTreeNode },
} = this;
registerTreeNode && registerTreeNode(eventKey, null);
},
methods: {
onSelectorClick(e) {
// Click trigger before select/check operation
const {
vcTree: { onNodeClick },
} = this;
onNodeClick(e, this);
if (this.isSelectable()) {
this.onSelect(e);
} else {
this.onCheck(e);
}
},
onSelectorDoubleClick(e) {
const {
vcTree: { onNodeDoubleClick },
} = this;
onNodeDoubleClick(e, this);
},
onSelect(e) {
if (this.isDisabled()) return;
const {
vcTree: { onNodeSelect },
} = this;
e.preventDefault();
onNodeSelect(e, this);
},
onCheck(e) {
if (this.isDisabled()) return;
const { disableCheckbox, checked } = this;
const {
vcTree: { onNodeCheck },
} = this;
if (!this.isCheckable() || disableCheckbox) return;
e.preventDefault();
const targetChecked = !checked;
onNodeCheck(e, this, targetChecked);
},
onMouseEnter(e) {
const {
vcTree: { onNodeMouseEnter },
} = this;
onNodeMouseEnter(e, this);
},
onMouseLeave(e) {
const {
vcTree: { onNodeMouseLeave },
} = this;
onNodeMouseLeave(e, this);
},
onContextMenu(e) {
const {
vcTree: { onNodeContextMenu },
} = this;
onNodeContextMenu(e, this);
},
onDragStart(e) {
const {
vcTree: { onNodeDragStart },
} = this;
e.stopPropagation();
this.setState({
dragNodeHighlight: true,
});
onNodeDragStart(e, this);
try {
// ie throw error
// firefox-need-it
e.dataTransfer.setData('text/plain', '');
} catch (error) {
// empty
}
},
onDragEnter(e) {
const {
vcTree: { onNodeDragEnter },
} = this;
e.preventDefault();
e.stopPropagation();
onNodeDragEnter(e, this);
},
onDragOver(e) {
const {
vcTree: { onNodeDragOver },
} = this;
e.preventDefault();
e.stopPropagation();
onNodeDragOver(e, this);
},
onDragLeave(e) {
const {
vcTree: { onNodeDragLeave },
} = this;
e.stopPropagation();
onNodeDragLeave(e, this);
},
onDragEnd(e) {
const {
vcTree: { onNodeDragEnd },
} = this;
e.stopPropagation();
this.setState({
dragNodeHighlight: false,
});
onNodeDragEnd(e, this);
},
onDrop(e) {
const {
vcTree: { onNodeDrop },
} = this;
e.preventDefault();
e.stopPropagation();
this.setState({
dragNodeHighlight: false,
});
onNodeDrop(e, this);
},
// Disabled item still can be switch
onExpand(e) {
const {
vcTree: { onNodeExpand },
} = this;
onNodeExpand(e, this);
},
// Drag usage
setSelectHandle(node) {
this.selectHandle = node;
},
getNodeChildren() {
const originList = this.children;
const targetList = getNodeChildren(originList);
if (originList.length !== targetList.length) {
warnOnlyTreeNode();
}
return targetList;
},
getNodeState() {
const { expanded } = this;
if (this.isLeaf2()) {
return null;
}
return expanded ? ICON_OPEN : ICON_CLOSE;
},
isLeaf2() {
const { isLeaf, loaded } = this;
const {
vcTree: { loadData },
} = this;
const hasChildren = this.getNodeChildren().length !== 0;
if (isLeaf === false) {
return false;
}
return isLeaf || (!loadData && !hasChildren) || (loadData && loaded && !hasChildren);
},
isDisabled() {
const { disabled } = this;
const {
vcTree: { disabled: treeDisabled },
} = this;
// Follow the logic of Selectable
if (disabled === false) {
return false;
}
return !!(treeDisabled || disabled);
},
isCheckable() {
const { checkable } = this.$props;
const {
vcTree: { checkable: treeCheckable },
} = this;
// Return false if tree or treeNode is not checkable
if (!treeCheckable || checkable === false) return false;
return treeCheckable;
},
// Load data to avoid default expanded tree without data
syncLoadData(props) {
const { expanded, loading, loaded } = props;
const {
vcTree: { loadData, onNodeLoad },
} = this;
if (loading) return;
// read from state to avoid loadData at same time
if (loadData && expanded && !this.isLeaf2()) {
// We needn't reload data when has children in sync logic
// It's only needed in node expanded
const hasChildren = this.getNodeChildren().length !== 0;
if (!hasChildren && !loaded) {
onNodeLoad(this);
}
}
},
isSelectable() {
const { selectable } = this;
const {
vcTree: { selectable: treeSelectable },
} = this;
// Ignore when selectable is undefined or null
if (typeof selectable === 'boolean') {
return selectable;
}
return treeSelectable;
},
// Switcher
renderSwitcher() {
const { expanded } = this;
const {
vcTree: { prefixCls },
} = this;
const switcherIcon =
getComponent(this, 'switcherIcon', {}, false) ||
getComponent(this.vcTree, 'switcherIcon', {}, false);
if (this.isLeaf2()) {
return (
<span
key="switcher"
class={classNames(`${prefixCls}-switcher`, `${prefixCls}-switcher-noop`)}
>
{typeof switcherIcon === 'function'
? switcherIcon({ ...this.$props, ...this.$props.dataRef, isLeaf: true })
: switcherIcon}
</span>
);
}
const switcherCls = classNames(
`${prefixCls}-switcher`,
`${prefixCls}-switcher_${expanded ? ICON_OPEN : ICON_CLOSE}`,
);
return (
<span key="switcher" onClick={this.onExpand} class={switcherCls}>
{typeof switcherIcon === 'function'
? switcherIcon({ ...this.$props, ...this.$props.dataRef, isLeaf: false })
: switcherIcon}
</span>
);
},
// Checkbox
renderCheckbox() {
const { checked, halfChecked, disableCheckbox } = this;
const {
vcTree: { prefixCls },
} = this;
const disabled = this.isDisabled();
const checkable = this.isCheckable();
if (!checkable) return null;
// [Legacy] Custom element should be separate with `checkable` in future
const $custom = typeof checkable !== 'boolean' ? checkable : null;
return (
<span
key="checkbox"
class={classNames(
`${prefixCls}-checkbox`,
checked && `${prefixCls}-checkbox-checked`,
!checked && halfChecked && `${prefixCls}-checkbox-indeterminate`,
(disabled || disableCheckbox) && `${prefixCls}-checkbox-disabled`,
)}
onClick={this.onCheck}
>
{$custom}
</span>
);
},
renderIcon() {
const { loading } = this;
const {
vcTree: { prefixCls },
} = this;
return (
<span
key="icon"
class={classNames(
`${prefixCls}-iconEle`,
`${prefixCls}-icon__${this.getNodeState() || 'docu'}`,
loading && `${prefixCls}-icon_loading`,
)}
/>
);
},
// Icon + Title
renderSelector() {
const { selected, loading, dragNodeHighlight } = this;
const icon = getComponent(this, 'icon', {}, false);
const {
vcTree: { prefixCls, showIcon, icon: treeIcon, draggable, loadData },
} = this;
const disabled = this.isDisabled();
const title = getComponent(this, 'title', {}, false);
const wrapClass = `${prefixCls}-node-content-wrapper`;
// Icon - Still show loading icon when loading without showIcon
let $icon;
if (showIcon) {
const currentIcon = icon || treeIcon;
$icon = currentIcon ? (
<span class={classNames(`${prefixCls}-iconEle`, `${prefixCls}-icon__customize`)}>
{typeof currentIcon === 'function'
? currentIcon({ ...this.$props, ...this.$props.dataRef })
: currentIcon}
</span>
) : (
this.renderIcon()
);
} else if (loadData && loading) {
$icon = this.renderIcon();
}
const currentTitle = title;
let $title = currentTitle ? (
<span class={`${prefixCls}-title`}>
{typeof currentTitle === 'function'
? currentTitle({ ...this.$props, ...this.$props.dataRef })
: currentTitle}
</span>
) : (
<span class={`${prefixCls}-title`}>{defaultTitle}</span>
);
return (
<span
key="selector"
ref={this.setSelectHandle}
title={typeof title === 'string' ? title : ''}
class={classNames(
`${wrapClass}`,
`${wrapClass}-${this.getNodeState() || 'normal'}`,
!disabled && (selected || dragNodeHighlight) && `${prefixCls}-node-selected`,
!disabled && draggable && 'draggable',
)}
draggable={(!disabled && draggable) || undefined}
aria-grabbed={(!disabled && draggable) || undefined}
onMouseenter={this.onMouseEnter}
onMouseleave={this.onMouseLeave}
onContextmenu={this.onContextMenu}
onClick={this.onSelectorClick}
onDblclick={this.onSelectorDoubleClick}
onDragstart={draggable ? this.onDragStart : noop}
>
{$icon}
{$title}
</span>
);
},
// Children list wrapped with `Animation`
renderChildren() {
const { expanded, pos } = this;
const {
vcTree: { prefixCls, openTransitionName, openAnimation, renderTreeNode },
} = this;
let animProps = {};
if (openTransitionName) {
animProps = getTransitionProps(openTransitionName);
} else if (typeof openAnimation === 'object') {
animProps = { ...openAnimation, css: false, ...animProps };
}
// Children TreeNode
const nodeList = this.getNodeChildren();
if (nodeList.length === 0) {
return null;
}
let $children;
if (expanded) {
$children = (
<ul
class={classNames(
`${prefixCls}-child-tree`,
expanded && `${prefixCls}-child-tree-open`,
)}
data-expanded={expanded}
role="group"
>
{mapChildren(nodeList, (node, index) => renderTreeNode(node, index, pos))}
</ul>
);
}
return <Transition {...animProps}>{$children}</Transition>;
},
},
render() {
this.children = getSlot(this);
const {
dragOver,
dragOverGapTop,
dragOverGapBottom,
isLeaf,
expanded,
selected,
checked,
halfChecked,
loading,
} = this.$props;
const {
vcTree: { prefixCls, filterTreeNode, draggable },
} = this;
const disabled = this.isDisabled();
const dataOrAriaAttributeProps = getDataAndAria({ ...this.$props, ...this.$attrs });
const { class: className, style } = this.$attrs;
return (
<li
class={{
[className]: className,
[`${prefixCls}-treenode-disabled`]: disabled,
[`${prefixCls}-treenode-switcher-${expanded ? 'open' : 'close'}`]: !isLeaf,
[`${prefixCls}-treenode-checkbox-checked`]: checked,
[`${prefixCls}-treenode-checkbox-indeterminate`]: halfChecked,
[`${prefixCls}-treenode-selected`]: selected,
[`${prefixCls}-treenode-loading`]: loading,
'drag-over': !disabled && dragOver,
'drag-over-gap-top': !disabled && dragOverGapTop,
'drag-over-gap-bottom': !disabled && dragOverGapBottom,
'filter-node': filterTreeNode && filterTreeNode(this),
}}
style={style}
role="treeitem"
onDragenter={draggable ? this.onDragEnter : noop}
onDragover={draggable ? this.onDragOver : noop}
onDragleave={draggable ? this.onDragLeave : noop}
onDrop={draggable ? this.onDrop : noop}
onDragend={draggable ? this.onDragEnd : noop}
{...dataOrAriaAttributeProps}
>
{this.renderSwitcher()}
{this.renderCheckbox()}
{this.renderSelector()}
{this.renderChildren()}
</li>
);
},
});
TreeNode.isTreeNode = 1;
export default TreeNode;

View File

@ -1,5 +0,0 @@
import Tree from './Tree';
import TreeNode from './TreeNode';
Tree.TreeNode = TreeNode;
export default Tree;

View File

@ -1,426 +0,0 @@
/* eslint no-loop-func: 0*/
import warning from 'warning';
import TreeNode from './TreeNode';
import { getOptionProps, getSlot } from '../../_util/props-util';
const DRAG_SIDE_RANGE = 0.25;
const DRAG_MIN_GAP = 2;
let onlyTreeNodeWarned = false;
export function warnOnlyTreeNode() {
if (onlyTreeNodeWarned) return;
onlyTreeNodeWarned = true;
warning(false, 'Tree only accept TreeNode as children.');
}
export function arrDel(list, value) {
const clone = list.slice();
const index = clone.indexOf(value);
if (index >= 0) {
clone.splice(index, 1);
}
return clone;
}
export function arrAdd(list, value) {
const clone = list.slice();
if (clone.indexOf(value) === -1) {
clone.push(value);
}
return clone;
}
export function posToArr(pos) {
return pos.split('-');
}
export function getPosition(level, index) {
return `${level}-${index}`;
}
export function isTreeNode(node) {
return node.type && node.type.isTreeNode;
}
export function getNodeChildren(children = []) {
return children.filter(isTreeNode);
}
export function isCheckDisabled(node) {
const { disabled, disableCheckbox, checkable } = getOptionProps(node) || {};
return !!(disabled || disableCheckbox) || checkable === false;
}
export function traverseTreeNodes(treeNodes, callback) {
function processNode(node, index, parent) {
const children = node ? getSlot(node) : treeNodes;
const pos = node ? getPosition(parent.pos, index) : 0;
// Filter children
const childList = getNodeChildren(children);
// Process node if is not root
if (node) {
let key = node.key;
if (!key && (key === undefined || key === null)) {
key = pos;
}
const data = {
node,
index,
pos,
key,
parentPos: parent.node ? parent.pos : null,
};
callback(data);
}
// Process children node
childList.forEach((subNode, subIndex) => {
processNode(subNode, subIndex, { node, pos });
});
}
processNode(null);
}
/**
* Use `rc-util` `toArray` to get the children list which keeps the key.
* And return single node if children is only one(This can avoid `key` missing check).
*/
export function mapChildren(children = [], func) {
const list = children.map(func);
if (list.length === 1) {
return list[0];
}
return list;
}
export function getDragNodesKeys(treeNodes, node) {
const { eventKey, pos } = getOptionProps(node);
const dragNodesKeys = [];
traverseTreeNodes(treeNodes, ({ key }) => {
dragNodesKeys.push(key);
});
dragNodesKeys.push(eventKey || pos);
return dragNodesKeys;
}
export function calcDropPosition(event, treeNode) {
const { clientY } = event;
const { top, bottom, height } = treeNode.selectHandle.getBoundingClientRect();
const des = Math.max(height * DRAG_SIDE_RANGE, DRAG_MIN_GAP);
if (clientY <= top + des) {
return -1;
}
if (clientY >= bottom - des) {
return 1;
}
return 0;
}
/**
* Return selectedKeys according with multiple prop
* @param selectedKeys
* @param props
* @returns [string]
*/
export function calcSelectedKeys(selectedKeys, props) {
if (!selectedKeys) {
return undefined;
}
const { multiple } = props;
if (multiple) {
return selectedKeys.slice();
}
if (selectedKeys.length) {
return [selectedKeys[0]];
}
return selectedKeys;
}
/**
* Since React internal will convert key to string,
* we need do this to avoid `checkStrictly` use number match
*/
// function keyListToString (keyList) {
// if (!keyList) return keyList
// return keyList.map(key => String(key))
// }
const internalProcessProps = (props = {}) => {
return {
...props,
class: props.class || props.className,
style: props.style,
key: props.key,
};
};
export function convertDataToTree(treeData, processor) {
if (!treeData) return [];
const { processProps = internalProcessProps } = processor || {};
const list = Array.isArray(treeData) ? treeData : [treeData];
return list.map(({ children, ...props }) => {
const childrenNodes = convertDataToTree(children, processor);
return <TreeNode {...processProps(props)}>{childrenNodes}</TreeNode>;
});
}
// TODO: ========================= NEW LOGIC =========================
/**
* Calculate treeNodes entities. `processTreeEntity` is used for `rc-tree-select`
* @param treeNodes
* @param processTreeEntity User can customize the entity
*/
export function convertTreeToEntities(
treeNodes,
{ initWrapper, processEntity, onProcessFinished } = {},
) {
const posEntities = new Map();
const keyEntities = new Map();
let wrapper = {
posEntities,
keyEntities,
};
if (initWrapper) {
wrapper = initWrapper(wrapper) || wrapper;
}
traverseTreeNodes(treeNodes, item => {
const { node, index, pos, key, parentPos } = item;
const entity = { node, index, key, pos };
posEntities.set(pos, entity);
keyEntities.set(key, entity);
// Fill children
entity.parent = posEntities.get(parentPos);
if (entity.parent) {
entity.parent.children = entity.parent.children || [];
entity.parent.children.push(entity);
}
if (processEntity) {
processEntity(entity, wrapper);
}
});
if (onProcessFinished) {
onProcessFinished(wrapper);
}
return wrapper;
}
/**
* Parse `checkedKeys` to { checkedKeys, halfCheckedKeys } style
*/
export function parseCheckedKeys(keys) {
if (!keys) {
return null;
}
// Convert keys to object format
let keyProps;
if (Array.isArray(keys)) {
// [Legacy] Follow the api doc
keyProps = {
checkedKeys: keys,
halfCheckedKeys: undefined,
};
} else if (typeof keys === 'object') {
keyProps = {
checkedKeys: keys.checked || undefined,
halfCheckedKeys: keys.halfChecked || undefined,
};
} else {
warning(false, '`checkedKeys` is not an array or an object');
return null;
}
// keyProps.checkedKeys = keyListToString(keyProps.checkedKeys)
// keyProps.halfCheckedKeys = keyListToString(keyProps.halfCheckedKeys)
return keyProps;
}
/**
* Conduct check state by the keyList. It will conduct up & from the provided key.
* If the conduct path reach the disabled or already checked / unchecked node will stop conduct.
* @param keyList list of keys
* @param isCheck is check the node or not
* @param keyEntities parsed by `convertTreeToEntities` function in Tree
* @param checkStatus Can pass current checked status for process (usually for uncheck operation)
* @returns {{checkedKeys: [], halfCheckedKeys: []}}
*/
export function conductCheck(keyList, isCheck, keyEntities, checkStatus = {}) {
const checkedKeys = new Map();
const halfCheckedKeys = new Map(); // Record the key has some child checked (include child half checked)
(checkStatus.checkedKeys || []).forEach(key => {
checkedKeys.set(key, true);
});
(checkStatus.halfCheckedKeys || []).forEach(key => {
halfCheckedKeys.set(key, true);
});
// Conduct up
function conductUp(key) {
if (checkedKeys.get(key) === isCheck) return;
const entity = keyEntities.get(key);
if (!entity) return;
const { children, parent, node } = entity;
if (isCheckDisabled(node)) return;
// Check child node checked status
let everyChildChecked = true;
let someChildChecked = false; // Child checked or half checked
(children || [])
.filter(child => !isCheckDisabled(child.node))
.forEach(({ key: childKey }) => {
const childChecked = checkedKeys.get(childKey);
const childHalfChecked = halfCheckedKeys.get(childKey);
if (childChecked || childHalfChecked) someChildChecked = true;
if (!childChecked) everyChildChecked = false;
});
// Update checked status
if (isCheck) {
checkedKeys.set(key, everyChildChecked);
} else {
checkedKeys.set(key, false);
}
halfCheckedKeys.set(key, someChildChecked);
if (parent) {
conductUp(parent.key);
}
}
// Conduct down
function conductDown(key) {
if (checkedKeys.get(key) === isCheck) return;
const entity = keyEntities.get(key);
if (!entity) return;
const { children, node } = entity;
if (isCheckDisabled(node)) return;
checkedKeys.set(key, isCheck);
(children || []).forEach(child => {
conductDown(child.key);
});
}
function conduct(key) {
const entity = keyEntities.get(key);
if (!entity) {
warning(false, `'${key}' does not exist in the tree.`);
return;
}
const { children, parent, node } = entity;
checkedKeys.set(key, isCheck);
if (isCheckDisabled(node)) return;
// Conduct down
(children || [])
.filter(child => !isCheckDisabled(child.node))
.forEach(child => {
conductDown(child.key);
});
// Conduct up
if (parent) {
conductUp(parent.key);
}
}
(keyList || []).forEach(key => {
conduct(key);
});
const checkedKeyList = [];
const halfCheckedKeyList = [];
// Fill checked list
for (const [key, value] of checkedKeys) {
if (value) {
checkedKeyList.push(key);
}
}
// Fill half checked list
for (const [key, value] of halfCheckedKeys) {
if (!checkedKeys.get(key) && value) {
halfCheckedKeyList.push(key);
}
}
return {
checkedKeys: checkedKeyList,
halfCheckedKeys: halfCheckedKeyList,
};
}
/**
* If user use `autoExpandParent` we should get the list of parent node
* @param keyList
* @param keyEntities
*/
export function conductExpandParent(keyList, keyEntities) {
const expandedKeys = new Map();
function conductUp(key) {
if (expandedKeys.get(key)) return;
const entity = keyEntities.get(key);
if (!entity) return;
expandedKeys.set(key, true);
const { parent, node } = entity;
const props = getOptionProps(node);
if (props && props.disabled) return;
if (parent) {
conductUp(parent.key);
}
}
(keyList || []).forEach(key => {
conductUp(key);
});
return [...expandedKeys.keys()];
}
/**
* Returns only the data- and aria- key/value pairs
* @param {object} props
*/
export function getDataAndAria(props) {
return Object.keys(props).reduce((prev, key) => {
if (key.substr(0, 5) === 'data-' || key.substr(0, 5) === 'aria-') {
prev[key] = props[key];
}
return prev;
}, {});
}

353
components/vc-tree/util.tsx Normal file
View File

@ -0,0 +1,353 @@
/* eslint-disable no-lonely-if */
/**
* Legacy code. Should avoid to use if you are new to import these code.
*/
import TreeNode from './TreeNode';
import {
NodeElement,
Key,
DataNode,
DataEntity,
NodeInstance,
FlattenNode,
Direction,
} from './interface';
import { warning } from '../vc-util/warning';
import { AllowDrop, TreeNodeProps, TreeProps } from './props';
export function arrDel(list: Key[], value: Key) {
const clone = list.slice();
const index = clone.indexOf(value);
if (index >= 0) {
clone.splice(index, 1);
}
return clone;
}
export function arrAdd(list: Key[], value: Key) {
const clone = list.slice();
if (clone.indexOf(value) === -1) {
clone.push(value);
}
return clone;
}
export function posToArr(pos: string) {
return pos.split('-');
}
export function getPosition(level: string | number, index: number) {
return `${level}-${index}`;
}
export function isTreeNode(node: NodeElement) {
return node && node.type && node.type.isTreeNode;
}
export function getDragChildrenKeys(dragNodeKey: Key, keyEntities: Record<Key, DataEntity>): Key[] {
// not contains self
// self for left or right drag
const dragChildrenKeys = [];
const entity = keyEntities[dragNodeKey];
function dig(list: DataEntity[] = []) {
list.forEach(({ key, children }) => {
dragChildrenKeys.push(key);
dig(children);
});
}
dig(entity.children);
return dragChildrenKeys;
}
export function isLastChild(treeNodeEntity: DataEntity) {
if (treeNodeEntity.parent) {
const posArr = posToArr(treeNodeEntity.pos);
return Number(posArr[posArr.length - 1]) === treeNodeEntity.parent.children.length - 1;
}
return false;
}
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(
event: MouseEvent,
_dragNode: NodeInstance,
targetNode: NodeInstance,
indent: number,
startMousePosition: {
x: number;
y: number;
},
allowDrop: AllowDrop,
flattenedNodes: FlattenNode[],
keyEntities: Record<Key, DataEntity>,
expandKeys: Key[],
direction: Direction,
): {
dropPosition: -1 | 0 | 1;
dropLevelOffset: number;
dropTargetKey: Key;
dropTargetPos: string;
dropContainerKey: Key;
dragOverNodeKey: Key;
dropAllowed: boolean;
} {
const { clientX, clientY } = event;
const { top, height } = (event.target as HTMLElement).getBoundingClientRect();
// optional chain for testing
const horizontalMouseOffset =
(direction === 'rtl' ? -1 : 1) * ((startMousePosition?.x || 0) - clientX);
const rawDropLevelOffset = (horizontalMouseOffset - 12) / indent;
// find abstract drop node by horizontal offset
let abstractDropNodeEntity: DataEntity = keyEntities[targetNode.props.eventKey];
if (clientY < top + height / 2) {
// first half, set abstract drop node to previous node
const nodeIndex = flattenedNodes.findIndex(
flattenedNode => flattenedNode.data.key === abstractDropNodeEntity.key,
);
const prevNodeIndex = nodeIndex <= 0 ? 0 : nodeIndex - 1;
const prevNodeKey = flattenedNodes[prevNodeIndex].data.key;
abstractDropNodeEntity = keyEntities[prevNodeKey];
}
const initialAbstractDropNodeKey = abstractDropNodeEntity.key;
const abstractDragOverEntity = abstractDropNodeEntity;
const dragOverNodeKey = abstractDropNodeEntity.key;
let dropPosition: -1 | 0 | 1 = 0;
let dropLevelOffset = 0;
// Only allow cross level drop when dragging on a non-expanded node
if (!expandKeys.includes(initialAbstractDropNodeKey)) {
for (let i = 0; i < rawDropLevelOffset; i += 1) {
if (isLastChild(abstractDropNodeEntity)) {
abstractDropNodeEntity = abstractDropNodeEntity.parent;
dropLevelOffset += 1;
} else {
break;
}
}
}
const abstractDropDataNode = abstractDropNodeEntity.node;
let dropAllowed = true;
if (
isFirstChild(abstractDropNodeEntity) &&
abstractDropNodeEntity.level === 0 &&
clientY < top + height / 2 &&
allowDrop({
dropNode: abstractDropDataNode,
dropPosition: -1,
}) &&
abstractDropNodeEntity.key === targetNode.props.eventKey
) {
// first half of first node in first level
dropPosition = -1;
} else if (
(abstractDragOverEntity.children || []).length &&
expandKeys.includes(dragOverNodeKey)
) {
// drop on expanded node
// only allow drop inside
if (
allowDrop({
dropNode: abstractDropDataNode,
dropPosition: 0,
})
) {
dropPosition = 0;
} else {
dropAllowed = false;
}
} else if (dropLevelOffset === 0) {
if (rawDropLevelOffset > -1.5) {
// | Node | <- abstractDropNode
// | -^-===== | <- mousePosition
// 1. try drop after
// 2. do not allow drop
if (
allowDrop({
dropNode: abstractDropDataNode,
dropPosition: 1,
})
) {
dropPosition = 1;
} else {
dropAllowed = false;
}
} else {
// | Node | <- abstractDropNode
// | ---==^== | <- mousePosition
// whether it has children or doesn't has children
// always
// 1. try drop inside
// 2. try drop after
// 3. do not allow drop
if (
allowDrop({
dropNode: abstractDropDataNode,
dropPosition: 0,
})
) {
dropPosition = 0;
} else if (
allowDrop({
dropNode: abstractDropDataNode,
dropPosition: 1,
})
) {
dropPosition = 1;
} else {
dropAllowed = false;
}
}
} else {
// | Node1 | <- abstractDropNode
// | Node2 |
// --^--|----=====| <- mousePosition
// 1. try insert after Node1
// 2. do not allow drop
if (
allowDrop({
dropNode: abstractDropDataNode,
dropPosition: 1,
})
) {
dropPosition = 1;
} else {
dropAllowed = false;
}
}
return {
dropPosition,
dropLevelOffset,
dropTargetKey: abstractDropNodeEntity.key,
dropTargetPos: abstractDropNodeEntity.pos,
dragOverNodeKey,
dropContainerKey: dropPosition === 0 ? null : abstractDropNodeEntity.parent?.key || null,
dropAllowed,
};
}
/**
* Return selectedKeys according with multiple prop
* @param selectedKeys
* @param props
* @returns [string]
*/
export function calcSelectedKeys(selectedKeys: Key[], props: TreeProps) {
if (!selectedKeys) return undefined;
const { multiple } = props;
if (multiple) {
return selectedKeys.slice();
}
if (selectedKeys.length) {
return [selectedKeys[0]];
}
return selectedKeys;
}
const internalProcessProps = (props: DataNode): Partial<TreeNodeProps> => props;
export function convertDataToTree(
treeData: DataNode[],
processor?: { processProps: (prop: DataNode) => any },
): NodeElement[] {
if (!treeData) return [];
const { processProps = internalProcessProps } = processor || {};
const list = Array.isArray(treeData) ? treeData : [treeData];
return list.map(({ children, ...props }): NodeElement => {
const childrenNodes = convertDataToTree(children, processor);
return <TreeNode {...processProps(props)}>{childrenNodes}</TreeNode>;
});
}
/**
* Parse `checkedKeys` to { checkedKeys, halfCheckedKeys } style
*/
export function parseCheckedKeys(keys: Key[] | { checked: Key[]; halfChecked: Key[] }) {
if (!keys) {
return null;
}
// Convert keys to object format
let keyProps;
if (Array.isArray(keys)) {
// [Legacy] Follow the api doc
keyProps = {
checkedKeys: keys,
halfCheckedKeys: undefined,
};
} else if (typeof keys === 'object') {
keyProps = {
checkedKeys: keys.checked || undefined,
halfCheckedKeys: keys.halfChecked || undefined,
};
} else {
warning(false, '`checkedKeys` is not an array or an object');
return null;
}
return keyProps;
}
/**
* If user use `autoExpandParent` we should get the list of parent node
* @param keyList
* @param keyEntities
*/
export function conductExpandParent(keyList: Key[], keyEntities: Record<Key, DataEntity>): Key[] {
const expandedKeys = new Set<Key>();
function conductUp(key: Key) {
if (expandedKeys.has(key)) return;
const entity = keyEntities[key];
if (!entity) return;
expandedKeys.add(key);
const { parent, node } = entity;
if (node.disabled) return;
if (parent) {
conductUp(parent.key);
}
}
(keyList || []).forEach(key => {
conductUp(key);
});
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

@ -0,0 +1,252 @@
import { warning } from '../../vc-util/warning';
import type { Key, DataEntity, DataNode, GetCheckDisabled } from '../interface';
interface ConductReturnType {
checkedKeys: Key[];
halfCheckedKeys: Key[];
}
function removeFromCheckedKeys(halfCheckedKeys: Set<Key>, checkedKeys: Set<Key>) {
const filteredKeys = new Set<Key>();
halfCheckedKeys.forEach(key => {
if (!checkedKeys.has(key)) {
filteredKeys.add(key);
}
});
return filteredKeys;
}
export function isCheckDisabled(node: DataNode) {
const { disabled, disableCheckbox, checkable } = (node || {}) as DataNode;
return !!(disabled || disableCheckbox) || checkable === false;
}
// Fill miss keys
function fillConductCheck(
keys: Set<Key>,
levelEntities: Map<number, Set<DataEntity>>,
maxLevel: number,
syntheticGetCheckDisabled: GetCheckDisabled<DataNode>,
): ConductReturnType {
const checkedKeys = new Set<Key>(keys);
const halfCheckedKeys = new Set<Key>();
// Add checked keys top to bottom
for (let level = 0; level <= maxLevel; level += 1) {
const entities = levelEntities.get(level) || new Set();
entities.forEach(entity => {
const { key, node, children = [] } = entity;
if (checkedKeys.has(key) && !syntheticGetCheckDisabled(node)) {
children
.filter(childEntity => !syntheticGetCheckDisabled(childEntity.node))
.forEach(childEntity => {
checkedKeys.add(childEntity.key);
});
}
});
}
// Add checked keys from bottom to top
const visitedKeys = new Set<Key>();
for (let level = maxLevel; level >= 0; level -= 1) {
const entities = levelEntities.get(level) || new Set();
entities.forEach(entity => {
const { parent, node } = entity;
// Skip if no need to check
if (syntheticGetCheckDisabled(node) || !entity.parent || visitedKeys.has(entity.parent.key)) {
return;
}
// Skip if parent is disabled
if (syntheticGetCheckDisabled(entity.parent.node)) {
visitedKeys.add(parent.key);
return;
}
let allChecked = true;
let partialChecked = false;
(parent.children || [])
.filter(childEntity => !syntheticGetCheckDisabled(childEntity.node))
.forEach(({ key }) => {
const checked = checkedKeys.has(key);
if (allChecked && !checked) {
allChecked = false;
}
if (!partialChecked && (checked || halfCheckedKeys.has(key))) {
partialChecked = true;
}
});
if (allChecked) {
checkedKeys.add(parent.key);
}
if (partialChecked) {
halfCheckedKeys.add(parent.key);
}
visitedKeys.add(parent.key);
});
}
return {
checkedKeys: Array.from(checkedKeys),
halfCheckedKeys: Array.from(removeFromCheckedKeys(halfCheckedKeys, checkedKeys)),
};
}
// Remove useless key
function cleanConductCheck(
keys: Set<Key>,
halfKeys: Key[],
levelEntities: Map<number, Set<DataEntity>>,
maxLevel: number,
syntheticGetCheckDisabled: GetCheckDisabled<DataNode>,
): ConductReturnType {
const checkedKeys = new Set<Key>(keys);
let halfCheckedKeys = new Set<Key>(halfKeys);
// Remove checked keys from top to bottom
for (let level = 0; level <= maxLevel; level += 1) {
const entities = levelEntities.get(level) || new Set();
entities.forEach(entity => {
const { key, node, children = [] } = entity;
if (!checkedKeys.has(key) && !halfCheckedKeys.has(key) && !syntheticGetCheckDisabled(node)) {
children
.filter(childEntity => !syntheticGetCheckDisabled(childEntity.node))
.forEach(childEntity => {
checkedKeys.delete(childEntity.key);
});
}
});
}
// Remove checked keys form bottom to top
halfCheckedKeys = new Set<Key>();
const visitedKeys = new Set<Key>();
for (let level = maxLevel; level >= 0; level -= 1) {
const entities = levelEntities.get(level) || new Set();
entities.forEach(entity => {
const { parent, node } = entity;
// Skip if no need to check
if (syntheticGetCheckDisabled(node) || !entity.parent || visitedKeys.has(entity.parent.key)) {
return;
}
// Skip if parent is disabled
if (syntheticGetCheckDisabled(entity.parent.node)) {
visitedKeys.add(parent.key);
return;
}
let allChecked = true;
let partialChecked = false;
(parent.children || [])
.filter(childEntity => !syntheticGetCheckDisabled(childEntity.node))
.forEach(({ key }) => {
const checked = checkedKeys.has(key);
if (allChecked && !checked) {
allChecked = false;
}
if (!partialChecked && (checked || halfCheckedKeys.has(key))) {
partialChecked = true;
}
});
if (!allChecked) {
checkedKeys.delete(parent.key);
}
if (partialChecked) {
halfCheckedKeys.add(parent.key);
}
visitedKeys.add(parent.key);
});
}
return {
checkedKeys: Array.from(checkedKeys),
halfCheckedKeys: Array.from(removeFromCheckedKeys(halfCheckedKeys, checkedKeys)),
};
}
/**
* Conduct with keys.
* @param keyList current key list
* @param keyEntities key - dataEntity map
* @param mode `fill` to fill missing key, `clean` to remove useless key
*/
export function conductCheck(
keyList: Key[],
checked: true | { checked: false; halfCheckedKeys: Key[] },
keyEntities: Record<Key, DataEntity>,
getCheckDisabled?: GetCheckDisabled<DataNode>,
): ConductReturnType {
const warningMissKeys: Key[] = [];
let syntheticGetCheckDisabled: GetCheckDisabled<DataNode>;
if (getCheckDisabled) {
syntheticGetCheckDisabled = getCheckDisabled;
} else {
syntheticGetCheckDisabled = isCheckDisabled;
}
// We only handle exist keys
const keys = new Set<Key>(
keyList.filter(key => {
const hasEntity = !!keyEntities[key];
if (!hasEntity) {
warningMissKeys.push(key);
}
return hasEntity;
}),
);
const levelEntities = new Map<number, Set<DataEntity>>();
let maxLevel = 0;
// Convert entities by level for calculation
Object.keys(keyEntities).forEach(key => {
const entity = keyEntities[key];
const { level } = entity;
let levelSet: Set<DataEntity> = levelEntities.get(level);
if (!levelSet) {
levelSet = new Set();
levelEntities.set(level, levelSet);
}
levelSet.add(entity);
maxLevel = Math.max(maxLevel, level);
});
warning(
!warningMissKeys.length,
`Tree missing follow keys: ${warningMissKeys
.slice(0, 100)
.map(key => `'${key}'`)
.join(', ')}`,
);
let result: ConductReturnType;
if (checked === true) {
result = fillConductCheck(keys, levelEntities, maxLevel, syntheticGetCheckDisabled);
} else {
result = cleanConductCheck(
keys,
checked.halfCheckedKeys,
levelEntities,
maxLevel,
syntheticGetCheckDisabled,
);
}
return result;
}

View File

@ -0,0 +1,45 @@
import type { Key, FlattenNode } from '../interface';
export function findExpandedKeys(prev: Key[] = [], next: Key[] = []) {
const prevLen = prev.length;
const nextLen = next.length;
if (Math.abs(prevLen - nextLen) !== 1) {
return { add: false, key: null };
}
function find(shorter: Key[], longer: Key[]) {
const cache: Map<Key, boolean> = new Map();
shorter.forEach(key => {
cache.set(key, true);
});
const keys = longer.filter(key => !cache.has(key));
return keys.length === 1 ? keys[0] : null;
}
if (prevLen < nextLen) {
return {
add: true,
key: find(prev, next),
};
}
return {
add: false,
key: find(next, prev),
};
}
export function getExpandRange(shorter: FlattenNode[], longer: FlattenNode[], key: Key) {
const shorterStartIndex = shorter.findIndex(({ data }) => data.key === key);
const shorterEndNode = shorter[shorterStartIndex + 1];
const longerStartIndex = longer.findIndex(({ data }) => data.key === key);
if (shorterEndNode) {
const longerEndIndex = longer.findIndex(({ data }) => data.key === shorterEndNode.data.key);
return longer.slice(longerStartIndex + 1, longerEndIndex);
}
return longer.slice(longerStartIndex + 1);
}

View File

@ -0,0 +1,411 @@
import type {
DataNode,
FlattenNode,
NodeElement,
DataEntity,
Key,
EventDataNode,
GetKey,
FieldNames,
} from '../interface';
import { getPosition, isTreeNode } from '../util';
import { warning } from '../../vc-util/warning';
import Omit from 'omit.js';
import type { VNodeChild } from 'vue';
import type { TreeNodeProps } from '../props';
export function getKey(key: Key, pos: string) {
if (key !== null && key !== undefined) {
return key;
}
return pos;
}
export function fillFieldNames(fieldNames?: FieldNames) {
const { title, key, children } = fieldNames || {};
return {
title: title || 'title',
key: key || 'key',
children: children || 'children',
};
}
/**
* Warning if TreeNode do not provides key
*/
export function warningWithoutKey(treeData: DataNode[], fieldNames: FieldNames) {
const keys: Map<string, boolean> = new Map();
function dig(list: DataNode[], path = '') {
(list || []).forEach(treeNode => {
const key = treeNode[fieldNames.key];
const children = treeNode[fieldNames.children];
warning(
key !== null && key !== undefined,
`Tree node must have a certain key: [${path}${key}]`,
);
const recordKey = String(key);
warning(
!keys.has(recordKey) || key === null || key === undefined,
`Same 'key' exist in the Tree: ${recordKey}`,
);
keys.set(recordKey, true);
dig(children, `${path}${recordKey} > `);
});
}
dig(treeData);
}
/**
* Convert `children` of Tree into `treeData` structure.
*/
export function convertTreeToData(rootNodes: VNodeChild): DataNode[] {
function dig(node: VNodeChild): DataNode[] {
const treeNodes = node as NodeElement[];
return treeNodes
.map(treeNode => {
// Filter invalidate node
if (!isTreeNode(treeNode)) {
warning(!treeNode, 'Tree/TreeNode can only accept TreeNode as children.');
return null;
}
const key = treeNode.key as string | number;
const { children, ...rest } = treeNode.props;
const dataNode: DataNode = {
...rest,
key,
};
const parsedChildren = dig(children);
if (parsedChildren.length) {
dataNode.children = parsedChildren;
}
return dataNode;
})
.filter((dataNode: DataNode) => dataNode);
}
return dig(rootNodes);
}
/**
* Flat nest tree data into flatten list. This is used for virtual list render.
* @param treeNodeList Origin data node list
* @param expandedKeys
* need expanded keys, provides `true` means all expanded (used in `rc-tree-select`).
*/
export function flattenTreeData(
treeNodeList: DataNode[],
expandedKeys: Key[] | true,
fieldNames: FieldNames,
): FlattenNode[] {
const { title: fieldTitle, key: fieldKey, children: fieldChildren } = fillFieldNames(fieldNames);
const expandedKeySet = new Set(expandedKeys === true ? [] : expandedKeys);
const flattenList: FlattenNode[] = [];
function dig(list: DataNode[], parent: FlattenNode = null): FlattenNode[] {
return list.map((treeNode, index) => {
const pos: string = getPosition(parent ? parent.pos : '0', index);
const mergedKey = getKey(treeNode[fieldKey], pos);
// Add FlattenDataNode into list
const flattenNode: FlattenNode = {
...Omit(treeNode, [fieldTitle, fieldKey, fieldChildren] as any),
title: treeNode[fieldTitle],
key: mergedKey,
parent,
pos,
children: null,
data: treeNode,
isStart: [...(parent ? parent.isStart : []), index === 0],
isEnd: [...(parent ? parent.isEnd : []), index === list.length - 1],
};
flattenList.push(flattenNode);
// Loop treeNode children
if (expandedKeys === true || expandedKeySet.has(mergedKey)) {
flattenNode.children = dig(treeNode[fieldChildren] || [], flattenNode);
} else {
flattenNode.children = [];
}
return flattenNode;
});
}
dig(treeNodeList);
return flattenList;
}
type ExternalGetKey = GetKey<DataNode> | string;
interface TraverseDataNodesConfig {
childrenPropName?: string;
externalGetKey?: ExternalGetKey;
fieldNames?: FieldNames;
}
/**
* Traverse all the data by `treeData`.
* Please not use it out of the `rc-tree` since we may refactor this code.
*/
export function traverseDataNodes(
dataNodes: DataNode[],
callback: (data: {
node: DataNode;
index: number;
pos: string;
key: Key;
parentPos: string | number;
level: number;
}) => void,
// To avoid too many params, let use config instead of origin param
config?: TraverseDataNodesConfig | string,
) {
let mergedConfig: TraverseDataNodesConfig = {};
if (typeof config === 'object') {
mergedConfig = config;
} else {
mergedConfig = { externalGetKey: config };
}
mergedConfig = mergedConfig || {};
// Init config
const { childrenPropName, externalGetKey, fieldNames } = mergedConfig;
const { key: fieldKey, children: fieldChildren } = fillFieldNames(fieldNames);
const mergeChildrenPropName = childrenPropName || fieldChildren;
// Get keys
let syntheticGetKey: (node: DataNode, pos?: string) => Key;
if (externalGetKey) {
if (typeof externalGetKey === 'string') {
syntheticGetKey = (node: DataNode) => (node as any)[externalGetKey as string];
} else if (typeof externalGetKey === 'function') {
syntheticGetKey = (node: DataNode) => (externalGetKey as GetKey<DataNode>)(node);
}
} else {
syntheticGetKey = (node, pos) => getKey(node[fieldKey], pos);
}
// Process
function processNode(
node: DataNode,
index?: number,
parent?: { node: DataNode; pos: string; level: number },
) {
const children = node ? node[mergeChildrenPropName] : dataNodes;
const pos = node ? getPosition(parent.pos, index) : '0';
// Process node if is not root
if (node) {
const key: Key = syntheticGetKey(node, pos);
const data = {
node,
index,
pos,
key,
parentPos: parent.node ? parent.pos : null,
level: parent.level + 1,
};
callback(data);
}
// Process children node
if (children) {
children.forEach((subNode, subIndex) => {
processNode(subNode, subIndex, {
node,
pos,
level: parent ? parent.level + 1 : -1,
});
});
}
}
processNode(null);
}
interface Wrapper {
posEntities: Record<string, DataEntity>;
keyEntities: Record<Key, DataEntity>;
}
/**
* Convert `treeData` into entity records.
*/
export function convertDataToEntities(
dataNodes: DataNode[],
{
initWrapper,
processEntity,
onProcessFinished,
externalGetKey,
childrenPropName,
fieldNames,
}: {
initWrapper?: (wrapper: Wrapper) => Wrapper;
processEntity?: (entity: DataEntity, wrapper: Wrapper) => void;
onProcessFinished?: (wrapper: Wrapper) => void;
externalGetKey?: ExternalGetKey;
childrenPropName?: string;
fieldNames?: FieldNames;
} = {},
/** @deprecated Use `config.externalGetKey` instead */
legacyExternalGetKey?: ExternalGetKey,
) {
// Init config
const mergedExternalGetKey = externalGetKey || legacyExternalGetKey;
const posEntities = {};
const keyEntities = {};
let wrapper = {
posEntities,
keyEntities,
};
if (initWrapper) {
wrapper = initWrapper(wrapper) || wrapper;
}
traverseDataNodes(
dataNodes,
item => {
const { node, index, pos, key, parentPos, level } = item;
const entity: DataEntity = { node, index, key, pos, level };
const mergedKey = getKey(key, pos);
posEntities[pos] = entity;
keyEntities[mergedKey] = entity;
// Fill children
entity.parent = posEntities[parentPos];
if (entity.parent) {
entity.parent.children = entity.parent.children || [];
entity.parent.children.push(entity);
}
if (processEntity) {
processEntity(entity, wrapper);
}
},
{ externalGetKey: mergedExternalGetKey, childrenPropName, fieldNames },
);
if (onProcessFinished) {
onProcessFinished(wrapper);
}
return wrapper;
}
export interface TreeNodeRequiredProps {
expandedKeys: Key[];
selectedKeys: Key[];
loadedKeys: Key[];
loadingKeys: Key[];
checkedKeys: Key[];
halfCheckedKeys: Key[];
dragOverNodeKey: Key;
dropPosition: number;
keyEntities: Record<Key, DataEntity>;
}
/**
* Get TreeNode props with Tree props.
*/
export function getTreeNodeProps(
key: Key,
{
expandedKeys,
selectedKeys,
loadedKeys,
loadingKeys,
checkedKeys,
halfCheckedKeys,
dragOverNodeKey,
dropPosition,
keyEntities,
}: TreeNodeRequiredProps,
) {
const entity = keyEntities[key];
const treeNodeProps = {
eventKey: key,
expanded: expandedKeys.indexOf(key) !== -1,
selected: selectedKeys.indexOf(key) !== -1,
loaded: loadedKeys.indexOf(key) !== -1,
loading: loadingKeys.indexOf(key) !== -1,
checked: checkedKeys.indexOf(key) !== -1,
halfChecked: halfCheckedKeys.indexOf(key) !== -1,
pos: String(entity ? entity.pos : ''),
// [Legacy] Drag props
// Since the interaction of drag is changed, the semantic of the props are
// not accuracy, I think it should be finally removed
dragOver: dragOverNodeKey === key && dropPosition === 0,
dragOverGapTop: dragOverNodeKey === key && dropPosition === -1,
dragOverGapBottom: dragOverNodeKey === key && dropPosition === 1,
};
return treeNodeProps;
}
export function convertNodePropsToEventData(props: TreeNodeProps): EventDataNode {
const {
data,
expanded,
selected,
checked,
loaded,
loading,
halfChecked,
dragOver,
dragOverGapTop,
dragOverGapBottom,
pos,
active,
} = props;
const eventData = {
...data,
expanded,
selected,
checked,
loaded,
loading,
halfChecked,
dragOver,
dragOverGapTop,
dragOverGapBottom,
pos,
active,
};
if (!('props' in eventData)) {
Object.defineProperty(eventData, 'props', {
get() {
warning(
false,
'Second param return from event is node data instead of TreeNode instance. Please read value directly instead of reading from `props`.',
);
return props;
},
});
}
return eventData;
}

View File

@ -30,6 +30,20 @@ const ScrollStyle: CSSProperties = {
overflowAnchor: 'none',
};
export type ScrollAlign = 'top' | 'bottom' | 'auto';
export type ScrollConfig =
| {
index: number;
align?: ScrollAlign;
offset?: number;
}
| {
key: Key;
align?: ScrollAlign;
offset?: number;
};
export type ScrollTo = (arg: number | ScrollConfig) => void;
function renderChildren<T>(
list: T[],
startIndex: number,
@ -68,7 +82,7 @@ const List = defineComponent({
/** If not match virtual scroll condition, Set List still use height of container. */
fullHeight: PropTypes.looseBool,
itemKey: {
type: [String, Number, Function] as PropType<Key | ((item: object) => Key)>,
type: [String, Number, Function] as PropType<Key | ((item: Record<string, any>) => Key)>,
required: true,
},
component: {
@ -81,7 +95,7 @@ const List = defineComponent({
onMousedown: PropTypes.func,
onMouseenter: PropTypes.func,
},
setup(props) {
setup(props, { expose }) {
// ================================= MISC =================================
const useVirtual = computed(() => {
const { height, itemHeight, virtual } = props;
@ -323,6 +337,10 @@ const List = defineComponent({
},
);
expose({
scrollTo,
});
const componentStyle = computed(() => {
let cs: CSSProperties | null = null;
if (props.height) {
@ -343,7 +361,6 @@ const List = defineComponent({
state,
mergedData,
componentStyle,
scrollTo,
onFallbackScroll,
onScrollBar,
componentRef,

2
v2-doc

@ -1 +1 @@
Subproject commit 7a7b52df8b3b69d8b1a8b8dcd96e1b0f7bb3f8c9
Subproject commit d571ad4bf772cfc372511dc1dedf07981dc56ae8