diff --git a/components/vc-tree/DropIndicator.tsx b/components/vc-tree/DropIndicator.tsx
new file mode 100644
index 000000000..e3d836243
--- /dev/null
+++ b/components/vc-tree/DropIndicator.tsx
@@ -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
;
+}
diff --git a/components/vc-tree/Indent.tsx b/components/vc-tree/Indent.tsx
new file mode 100644
index 000000000..de353cd89
--- /dev/null
+++ b/components/vc-tree/Indent.tsx
@@ -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(
+ ,
+ );
+ }
+
+ return (
+
+ {list}
+
+ );
+};
+
+export default Indent;
diff --git a/components/vc-tree/MotionTreeNode.tsx b/components/vc-tree/MotionTreeNode.tsx
new file mode 100644
index 000000000..568cdc9d9
--- /dev/null
+++ b/components/vc-tree/MotionTreeNode.tsx
@@ -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 },
+ onMotionStart: Function,
+ onMotionEnd: Function,
+ motionType: String,
+ treeNodeRequiredProps: { type: Object as PropType },
+ },
+ 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 (
+
+
+ {motionNodes.map((treeNode: FlattenNode) => {
+ const {
+ data: { ...restProps },
+ title,
+ key,
+ isStart,
+ isEnd,
+ } = treeNode;
+ delete restProps.children;
+
+ const treeNodeProps = getTreeNodeProps(key, treeNodeRequiredProps);
+
+ return (
+
+ );
+ })}
+
+
+ );
+ }
+ return (
+
+ );
+ };
+ },
+});
diff --git a/components/vc-tree/NodeList.tsx b/components/vc-tree/NodeList.tsx
new file mode 100644
index 000000000..01c3a2cce
--- /dev/null
+++ b/components/vc-tree/NodeList.tsx
@@ -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(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 && (
+
+ {getAccessibilityPath(activeItem)}
+
+ )}
+
+
+
+
+
+
+
+
+ {(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 (
+ {
+ onActiveChange(null);
+ }}
+ />
+ );
+ }}
+
+ >
+ );
+ };
+ },
+});
diff --git a/components/vc-tree/Tree.tsx b/components/vc-tree/Tree.tsx
new file mode 100644
index 000000000..654c59d7f
--- /dev/null
+++ b/components/vc-tree/Tree.tsx
@@ -0,0 +1,1087 @@
+import { TreeContext, NodeMouseEventHandler, NodeDragEventHandler } from './contextTypes';
+import {
+ getDataAndAria,
+ getDragChildrenKeys,
+ parseCheckedKeys,
+ conductExpandParent,
+ calcSelectedKeys,
+ calcDropPosition,
+ arrAdd,
+ arrDel,
+ posToArr,
+} from './util';
+import { Key, FlattenNode, EventDataNode, NodeInstance, ScrollTo } from './interface';
+import {
+ flattenTreeData,
+ convertTreeToData,
+ convertDataToEntities,
+ convertNodePropsToEventData,
+ getTreeNodeProps,
+ fillFieldNames,
+} from './utils/treeUtil';
+import NodeList, { MOTION_KEY, MotionEntity } from './NodeList';
+import { conductCheck } from './utils/conductUtil';
+import DropIndicator from './DropIndicator';
+import { computed, defineComponent, onMounted, onUnmounted, reactive, ref, watchEffect } from 'vue';
+import initDefaultProps from '../_util/props-util/initDefaultProps';
+import { CheckInfo, treeProps } from './props';
+import { warning } from '../vc-util/warning';
+import KeyCode from '../_util/KeyCode';
+import classNames from '../_util/classNames';
+
+export default defineComponent({
+ name: 'Tree',
+ inheritAttrs: false,
+ props: initDefaultProps(treeProps(), {
+ prefixCls: 'vc-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: [],
+ dropIndicatorRender: DropIndicator,
+ allowDrop: () => true,
+ }),
+
+ setup(props, { attrs }) {
+ const destroyed = ref(false);
+ let delayedDragEnterLogic: Record = {};
+
+ const indent = ref();
+ const selectedKeys = ref([]);
+ const checkedKeys = ref([]);
+ const halfCheckedKeys = ref([]);
+ const loadedKeys = ref([]);
+ const loadingKeys = ref([]);
+ const expandedKeys = ref([]);
+
+ const dragState = reactive({
+ dragging: false,
+ dragChildrenKeys: [],
+
+ // dropTargetKey is the key of abstract-drop-node
+ // the abstract-drop-node is the real drop node when drag and drop
+ // not the DOM drag over node
+ dropTargetKey: null,
+ dropPosition: null, // the drop position of abstract-drop-node, inside 0, top -1, bottom 1
+ dropContainerKey: null, // the container key of abstract-drop-node if dropPosition is -1 or 1
+ dropLevelOffset: null, // the drop level offset of abstract-drag-over-node
+ dropTargetPos: null, // the pos of abstract-drop-node
+ dropAllowed: true, // if drop to abstract-drop-node is allowed
+ // the abstract-drag-over-node
+ // if mouse is on the bottom of top dom node or no the top of the bottom dom node
+ // abstract-drag-over-node is the top node
+ dragOverNodeKey: null,
+ });
+
+ const treeData = computed(() => {
+ warning(
+ !(props.treeData === undefined && props.children),
+ '`children` of Tree is deprecated. Please use `treeData` instead.',
+ );
+ return props.treeData !== undefined ? props.treeData : convertTreeToData(props.children);
+ });
+ const keyEntities = ref({});
+ watchEffect(() => {
+ if (treeData.value) {
+ const entitiesMap = convertDataToEntities(treeData.value, { fieldNames: fieldNames.value });
+ keyEntities.value = {
+ [MOTION_KEY]: MotionEntity,
+ ...entitiesMap.keyEntities,
+ };
+ }
+ });
+ let init = false; // 处理 defaultXxxx api, 仅仅首次有效
+
+ onMounted(() => {
+ init = true;
+ });
+
+ // ================ expandedKeys =================
+ watchEffect(() => {
+ let keys = expandedKeys.value;
+ // ================ expandedKeys =================
+ if (props.expandedKeys !== undefined || (init && props.autoExpandParent)) {
+ keys =
+ props.autoExpandParent || (!init && props.defaultExpandParent)
+ ? conductExpandParent(props.expandedKeys, keyEntities.value)
+ : props.expandedKeys;
+ } else if (!init && props.defaultExpandAll) {
+ const cloneKeyEntities = { ...keyEntities };
+ delete cloneKeyEntities[MOTION_KEY];
+ keys = Object.keys(cloneKeyEntities).map(key => cloneKeyEntities[key].key);
+ } else if (!init && props.defaultExpandedKeys) {
+ keys =
+ props.autoExpandParent || props.defaultExpandParent
+ ? conductExpandParent(props.defaultExpandedKeys, keyEntities.value)
+ : props.defaultExpandedKeys;
+ }
+
+ if (keys) {
+ expandedKeys.value = keys;
+ }
+ });
+
+ // ================ flattenNodes =================
+ const flattenNodes = computed(() => {
+ return flattenTreeData(treeData.value, expandedKeys.value, fieldNames.value);
+ });
+
+ // ================ selectedKeys =================
+ watchEffect(() => {
+ if (props.selectable) {
+ if (props.selectedKeys !== undefined) {
+ selectedKeys.value = calcSelectedKeys(props.selectedKeys, props);
+ } else if (!init && props.defaultSelectedKeys) {
+ selectedKeys.value = calcSelectedKeys(props.defaultSelectedKeys, props);
+ }
+ }
+ });
+
+ // ================= checkedKeys =================
+ watchEffect(() => {
+ if (props.checkable) {
+ let checkedKeyEntity;
+
+ if (props.checkedKeys !== undefined) {
+ checkedKeyEntity = parseCheckedKeys(props.checkedKeys) || {};
+ } else if (!init && props.defaultCheckedKeys) {
+ checkedKeyEntity = parseCheckedKeys(props.defaultCheckedKeys) || {};
+ } else if (treeData) {
+ // If `treeData` changed, we also need check it
+ checkedKeyEntity = parseCheckedKeys(props.checkedKeys) || {
+ checkedKeys: checkedKeys.value,
+ halfCheckedKeys: halfCheckedKeys.value,
+ };
+ }
+
+ if (checkedKeyEntity) {
+ let { checkedKeys: newCheckedKeys = [], halfCheckedKeys: newHalfCheckedKeys = [] } =
+ checkedKeyEntity;
+
+ if (!props.checkStrictly) {
+ const conductKeys = conductCheck(newCheckedKeys, true, keyEntities.value);
+ ({ checkedKeys: newCheckedKeys, halfCheckedKeys: newHalfCheckedKeys } = conductKeys);
+ }
+
+ checkedKeys.value = newCheckedKeys;
+ halfCheckedKeys.value = newHalfCheckedKeys;
+ }
+ }
+ });
+
+ // ================= loadedKeys ==================
+ watchEffect(() => {
+ if (props.loadedKeys) {
+ loadedKeys.value = props.loadedKeys;
+ }
+ });
+
+ const focused = ref(false);
+ const activeKey = ref(null);
+
+ const listChanging = ref(false);
+
+ const fieldNames = computed(() => fillFieldNames(props.fieldNames));
+
+ const listRef = ref();
+
+ let dragStartMousePosition = null;
+
+ let dragNode = null;
+
+ const treeNodeRequiredProps = computed(() => {
+ return {
+ expandedKeys: expandedKeys.value || [],
+ selectedKeys: selectedKeys.value || [],
+ loadedKeys: loadedKeys.value || [],
+ loadingKeys: loadingKeys.value || [],
+ checkedKeys: checkedKeys.value || [],
+ halfCheckedKeys: halfCheckedKeys.value || [],
+ dragOverNodeKey: dragState.dragOverNodeKey,
+ dropPosition: dragState.dropPosition,
+ keyEntities: keyEntities.value,
+ };
+ });
+ const scrollTo: ScrollTo = scroll => {
+ listRef.value.scrollTo(scroll);
+ };
+ // =========================== Expanded ===========================
+ /** Set uncontrolled `expandedKeys`. This will also auto update `flattenNodes`. */
+ const setExpandedKeys = (keys: Key[]) => {
+ if (props.expandedKeys !== undefined) {
+ expandedKeys.value = keys;
+ }
+ };
+
+ const cleanDragState = () => {
+ if (dragState.dragging) {
+ Object.assign(dragState, {
+ dragging: false,
+ dropPosition: null,
+ dropContainerKey: null,
+ dropTargetKey: null,
+ dropLevelOffset: null,
+ dropAllowed: true,
+ dragOverNodeKey: null,
+ });
+ }
+ dragStartMousePosition = null;
+ };
+ // if onNodeDragEnd is called, onWindowDragEnd won't be called since stopPropagation() is called
+ const onNodeDragEnd: NodeDragEventHandler = (event, node, outsideTree = false) => {
+ const { onDragend } = props;
+
+ dragState.dragOverNodeKey = null;
+
+ cleanDragState();
+
+ if (onDragend && !outsideTree) {
+ onDragend({ event, node: convertNodePropsToEventData(node.props) });
+ }
+
+ dragNode = null;
+ };
+
+ // since stopPropagation() is called in treeNode
+ // if onWindowDrag is called, whice means state is keeped, drag state should be cleared
+ const onWindowDragEnd = event => {
+ onNodeDragEnd(event, null, true);
+ window.removeEventListener('dragend', onWindowDragEnd);
+ };
+
+ const onNodeDragStart: NodeDragEventHandler = (event, node) => {
+ const { onDragstart } = props;
+ const { eventKey } = node.props;
+
+ dragNode = node;
+ dragStartMousePosition = {
+ x: event.clientX,
+ y: event.clientY,
+ };
+
+ const newExpandedKeys = arrDel(expandedKeys.value, eventKey);
+
+ dragState.dragging = true;
+ dragState.dragChildrenKeys = getDragChildrenKeys(eventKey, keyEntities.value);
+ indent.value = listRef.value.getIndentWidth();
+
+ setExpandedKeys(newExpandedKeys);
+ window.addEventListener('dragend', onWindowDragEnd);
+
+ if (onDragstart) {
+ onDragstart({ event, node: convertNodePropsToEventData(node.props) });
+ }
+ };
+
+ /**
+ * [Legacy] Select handler is smaller 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.
+ */
+ const onNodeDragEnter = (event: MouseEvent, node: NodeInstance) => {
+ const { onDragenter, onExpand, allowDrop, direction } = props;
+ const { pos } = node.props;
+
+ const {
+ dropPosition,
+ dropLevelOffset,
+ dropTargetKey,
+ dropContainerKey,
+ dropTargetPos,
+ dropAllowed,
+ dragOverNodeKey,
+ } = calcDropPosition(
+ event,
+ dragNode,
+ node,
+ indent.value,
+ dragStartMousePosition,
+ allowDrop,
+ flattenNodes.value,
+ keyEntities.value,
+ expandedKeys.value,
+ direction,
+ );
+
+ if (
+ !dragNode ||
+ // don't allow drop inside its children
+ dragState.dragChildrenKeys.indexOf(dropTargetKey) !== -1 ||
+ // don't allow drop when drop is not allowed caculated by calcDropPosition
+ !dropAllowed
+ ) {
+ Object.assign(dragState, {
+ dragOverNodeKey: null,
+ dropPosition: null,
+ dropLevelOffset: null,
+ dropTargetKey: null,
+ dropContainerKey: null,
+ dropTargetPos: null,
+ dropAllowed: false,
+ });
+ return;
+ }
+
+ // Side effect for delay drag
+ if (!delayedDragEnterLogic) {
+ delayedDragEnterLogic = {};
+ }
+ Object.keys(delayedDragEnterLogic).forEach(key => {
+ clearTimeout(delayedDragEnterLogic[key]);
+ });
+
+ if (dragNode.props.eventKey !== node.props.eventKey) {
+ // hoist expand logic here
+ // since if logic is on the bottom
+ // it will be blocked by abstract dragover node check
+ // => if you dragenter from top, you mouse will still be consider as in the top node
+ (event as any).persist();
+ delayedDragEnterLogic[pos] = window.setTimeout(() => {
+ if (!dragState.dragging) return;
+
+ let newExpandedKeys = [...expandedKeys.value];
+ const entity = keyEntities[node.props.eventKey];
+
+ if (entity && (entity.children || []).length) {
+ newExpandedKeys = arrAdd(expandedKeys.value, node.props.eventKey);
+ }
+
+ setExpandedKeys(newExpandedKeys);
+
+ if (onExpand) {
+ onExpand(newExpandedKeys, {
+ node: convertNodePropsToEventData(node.props),
+ expanded: true,
+ nativeEvent: (event as any).nativeEvent,
+ });
+ }
+ }, 800);
+ }
+
+ // Skip if drag node is self
+ if (dragNode.props.eventKey === dropTargetKey && dropLevelOffset === 0) {
+ Object.assign(dragState, {
+ dragOverNodeKey: null,
+ dropPosition: null,
+ dropLevelOffset: null,
+ dropTargetKey: null,
+ dropContainerKey: null,
+ dropTargetPos: null,
+ dropAllowed: false,
+ });
+ return;
+ }
+
+ // Update drag over node and drag state
+ Object.assign(dragState, {
+ dragOverNodeKey,
+ dropPosition,
+ dropLevelOffset,
+ dropTargetKey,
+ dropContainerKey,
+ dropTargetPos,
+ dropAllowed,
+ });
+
+ if (onDragenter) {
+ onDragenter({
+ event,
+ node: convertNodePropsToEventData(node.props),
+ expandedKeys: expandedKeys.value,
+ });
+ }
+ };
+
+ const onNodeDragOver = (event: MouseEvent, node: NodeInstance) => {
+ const { onDragover, allowDrop, direction } = props;
+
+ const {
+ dropPosition,
+ dropLevelOffset,
+ dropTargetKey,
+ dropContainerKey,
+ dropAllowed,
+ dropTargetPos,
+ dragOverNodeKey,
+ } = calcDropPosition(
+ event,
+ dragNode,
+ node,
+ indent.value,
+ dragStartMousePosition,
+ allowDrop,
+ flattenNodes.value,
+ keyEntities.value,
+ expandedKeys.value,
+ direction,
+ );
+
+ if (!dragNode || dragState.dragChildrenKeys.indexOf(dropTargetKey) !== -1 || !dropAllowed) {
+ // don't allow drop inside its children
+ // don't allow drop when drop is not allowed caculated by calcDropPosition
+ return;
+ }
+
+ // Update drag position
+
+ if (dragNode.props.eventKey === dropTargetKey && dropLevelOffset === 0) {
+ if (
+ !(
+ dragState.dropPosition === null &&
+ dragState.dropLevelOffset === null &&
+ dragState.dropTargetKey === null &&
+ dragState.dropContainerKey === null &&
+ dragState.dropTargetPos === null &&
+ dragState.dropAllowed === false &&
+ dragState.dragOverNodeKey === null
+ )
+ ) {
+ Object.assign(dragState, {
+ dropPosition: null,
+ dropLevelOffset: null,
+ dropTargetKey: null,
+ dropContainerKey: null,
+ dropTargetPos: null,
+ dropAllowed: false,
+ dragOverNodeKey: null,
+ });
+ }
+ } else if (
+ !(
+ dropPosition === dragState.dropPosition &&
+ dropLevelOffset === dragState.dropLevelOffset &&
+ dropTargetKey === dragState.dropTargetKey &&
+ dropContainerKey === dragState.dropContainerKey &&
+ dropTargetPos === dragState.dropTargetPos &&
+ dropAllowed === dragState.dropAllowed &&
+ dragOverNodeKey === dragState.dragOverNodeKey
+ )
+ ) {
+ Object.assign(dragState, {
+ dropPosition,
+ dropLevelOffset,
+ dropTargetKey,
+ dropContainerKey,
+ dropTargetPos,
+ dropAllowed,
+ dragOverNodeKey,
+ });
+ }
+
+ if (onDragover) {
+ onDragover({ event, node: convertNodePropsToEventData(node.props) });
+ }
+ };
+
+ const onNodeDragLeave: NodeDragEventHandler = (event, node) => {
+ const { onDragleave } = props;
+
+ if (onDragleave) {
+ onDragleave({ event, node: convertNodePropsToEventData(node.props) });
+ }
+ };
+ const onNodeDrop = (event: MouseEvent, _node, outsideTree: boolean = false) => {
+ const { dragChildrenKeys, dropPosition, dropTargetKey, dropTargetPos, dropAllowed } =
+ dragState;
+
+ if (!dropAllowed) return;
+
+ const { onDrop } = props;
+
+ dragState.dragOverNodeKey = null;
+ cleanDragState();
+
+ if (dropTargetKey === null) return;
+
+ const abstractDropNodeProps = {
+ ...getTreeNodeProps(dropTargetKey, treeNodeRequiredProps.value),
+ active: activeItem.value?.data.key === dropTargetKey,
+ data: keyEntities.value[dropTargetKey].node,
+ };
+ const dropToChild = dragChildrenKeys.indexOf(dropTargetKey) !== -1;
+
+ warning(
+ !dropToChild,
+ "Can not drop to dragNode's children node. Maybe this is a bug of ant-design-vue. Please report an issue.",
+ );
+
+ const posArr = posToArr(dropTargetPos);
+
+ const dropResult = {
+ event,
+ node: convertNodePropsToEventData(abstractDropNodeProps),
+ dragNode: dragNode ? convertNodePropsToEventData(dragNode.props) : null,
+ dragNodesKeys: [dragNode.props.eventKey].concat(dragChildrenKeys),
+ dropToGap: dropPosition !== 0,
+ dropPosition: dropPosition + Number(posArr[posArr.length - 1]),
+ };
+
+ if (onDrop && !outsideTree) {
+ onDrop(dropResult);
+ }
+
+ dragNode = null;
+ };
+
+ const onNodeClick: NodeMouseEventHandler = (e, treeNode) => {
+ const { onClick } = props;
+ if (onClick) {
+ onClick(e, treeNode);
+ }
+ };
+
+ const onNodeDoubleClick: NodeMouseEventHandler = (e, treeNode) => {
+ const { onDblClick } = props;
+ if (onDblClick) {
+ onDblClick(e, treeNode);
+ }
+ };
+
+ const onNodeSelect: NodeMouseEventHandler = (e, treeNode) => {
+ let newSelectedKeys = selectedKeys.value;
+ const { onSelect, multiple } = props;
+ const { selected } = treeNode;
+ const key = treeNode[fieldNames.value.key];
+ const targetSelected = !selected;
+
+ // Update selected keys
+ if (!targetSelected) {
+ newSelectedKeys = arrDel(newSelectedKeys, key);
+ } else if (!multiple) {
+ newSelectedKeys = [key];
+ } else {
+ newSelectedKeys = arrAdd(newSelectedKeys, key);
+ }
+
+ // [Legacy] Not found related usage in doc or upper libs
+ const selectedNodes = newSelectedKeys
+ .map(selectedKey => {
+ const entity = keyEntities.value[selectedKey];
+ if (!entity) return null;
+
+ return entity.node;
+ })
+ .filter(node => node);
+
+ if (props.selectedKeys !== undefined) {
+ selectedKeys.value = newSelectedKeys;
+ }
+
+ if (onSelect) {
+ onSelect(newSelectedKeys, {
+ event: 'select',
+ selected: targetSelected,
+ node: treeNode,
+ selectedNodes,
+ nativeEvent: (e as any).nativeEvent,
+ });
+ }
+ };
+
+ const onNodeCheck = (e: MouseEvent, treeNode: EventDataNode, checked: boolean) => {
+ const { checkStrictly, onCheck } = props;
+ const { key } = treeNode;
+
+ // Prepare trigger arguments
+ let checkedObj;
+ const eventObj: Partial = {
+ event: 'check',
+ node: treeNode,
+ checked,
+ nativeEvent: (e as any).nativeEvent,
+ };
+
+ if (checkStrictly) {
+ const newCheckedKeys = checked
+ ? arrAdd(checkedKeys.value, key)
+ : arrDel(checkedKeys.value, key);
+ const newHalfCheckedKeys = arrDel(halfCheckedKeys.value, key);
+ checkedObj = { checked: newCheckedKeys, halfChecked: newHalfCheckedKeys };
+
+ eventObj.checkedNodes = newCheckedKeys
+ .map(checkedKey => keyEntities[checkedKey])
+ .filter(entity => entity)
+ .map(entity => entity.node);
+
+ if (props.checkedKeys !== undefined) {
+ checkedKeys.value = newCheckedKeys;
+ }
+ } else {
+ // Always fill first
+ let { checkedKeys: newCheckedKeys, halfCheckedKeys: newHalfCheckedKeys } = conductCheck(
+ [...checkedKeys.value, key],
+ true,
+ keyEntities.value,
+ );
+
+ // If remove, we do it again to correction
+ if (!checked) {
+ const keySet = new Set(newCheckedKeys);
+ keySet.delete(key);
+ ({ checkedKeys: newCheckedKeys, halfCheckedKeys: newHalfCheckedKeys } = conductCheck(
+ Array.from(keySet),
+ { checked: false, halfCheckedKeys: newHalfCheckedKeys },
+ keyEntities.value,
+ ));
+ }
+
+ checkedObj = newCheckedKeys;
+
+ // [Legacy] This is used for `rc-tree-select`
+ eventObj.checkedNodes = [];
+ eventObj.checkedNodesPositions = [];
+ eventObj.halfCheckedKeys = newHalfCheckedKeys;
+
+ newCheckedKeys.forEach(checkedKey => {
+ const entity = keyEntities.value[checkedKey];
+ if (!entity) return;
+
+ const { node, pos } = entity;
+
+ eventObj.checkedNodes.push(node);
+ eventObj.checkedNodesPositions.push({ node, pos });
+ });
+ if (props.checkedKeys !== undefined) {
+ checkedKeys.value = newCheckedKeys;
+ halfCheckedKeys.value = newHalfCheckedKeys;
+ }
+ }
+
+ if (onCheck) {
+ onCheck(checkedObj, eventObj as CheckInfo);
+ }
+ };
+
+ const onNodeLoad = (treeNode: EventDataNode) =>
+ new Promise((resolve, reject) => {
+ // We need to get the latest state of loading/loaded keys
+ const { loadData, onLoad } = props;
+ const { key } = treeNode;
+
+ if (
+ !loadData ||
+ loadedKeys.value.indexOf(key) !== -1 ||
+ loadingKeys.value.indexOf(key) !== -1
+ ) {
+ return null;
+ }
+
+ // Process load data
+ const promise = loadData(treeNode);
+ promise
+ .then(() => {
+ const newLoadedKeys = arrAdd(loadedKeys.value, key);
+ const newLoadingKeys = arrDel(loadingKeys.value, key);
+
+ // onLoad should trigger before internal setState to avoid `loadData` trigger twice.
+ // https://github.com/ant-design/ant-design/issues/12464
+ if (onLoad) {
+ onLoad(newLoadedKeys, {
+ event: 'load',
+ node: treeNode,
+ });
+ }
+
+ if (props.loadedKeys !== undefined) {
+ loadedKeys.value = newLoadedKeys;
+ }
+ loadingKeys.value = newLoadingKeys;
+ resolve();
+ })
+ .catch(e => {
+ const newLoadingKeys = arrDel(loadingKeys.value, key);
+ loadingKeys.value = newLoadingKeys;
+ reject(e);
+ });
+
+ loadingKeys.value = arrAdd(loadingKeys.value, key);
+ });
+
+ const onNodeMouseEnter: NodeMouseEventHandler = (event, node) => {
+ const { onMouseenter } = props;
+ if (onMouseenter) {
+ onMouseenter({ event, node });
+ }
+ };
+
+ const onNodeMouseLeave: NodeMouseEventHandler = (event, node) => {
+ const { onMouseleave } = props;
+ if (onMouseleave) {
+ onMouseleave({ event, node });
+ }
+ };
+
+ const onNodeContextMenu: NodeMouseEventHandler = (event, node) => {
+ const { onRightClick } = props;
+ if (onRightClick) {
+ event.preventDefault();
+ onRightClick({ event, node });
+ }
+ };
+
+ const onFocus = (e: FocusEvent) => {
+ const { onFocus } = props;
+ focused.value = true;
+ if (onFocus) {
+ onFocus(e);
+ }
+ };
+
+ const onBlur = (e: FocusEvent) => {
+ const { onBlur } = props;
+ focused.value = false;
+ onActiveChange(null);
+
+ if (onBlur) {
+ onBlur(e);
+ }
+ };
+
+ const onNodeExpand = (e: MouseEvent, treeNode: EventDataNode) => {
+ let newExpandedKeys = expandedKeys.value;
+ const { onExpand, loadData } = props;
+ const { expanded } = treeNode;
+ const key = treeNode[fieldNames.value.key];
+
+ // Do nothing when motion is in progress
+ if (listChanging.value) {
+ return;
+ }
+
+ // Update selected keys
+ const index = newExpandedKeys.indexOf(key);
+ const targetExpanded = !expanded;
+
+ warning(
+ (expanded && index !== -1) || (!expanded && index === -1),
+ 'Expand state not sync with index check',
+ );
+
+ if (targetExpanded) {
+ newExpandedKeys = arrAdd(newExpandedKeys, key);
+ } else {
+ newExpandedKeys = arrDel(newExpandedKeys, key);
+ }
+
+ setExpandedKeys(newExpandedKeys);
+
+ if (onExpand) {
+ onExpand(newExpandedKeys, {
+ node: treeNode,
+ expanded: targetExpanded,
+ nativeEvent: (e as any).nativeEvent,
+ });
+ }
+
+ // Async Load data
+ if (targetExpanded && loadData) {
+ const loadPromise = onNodeLoad(treeNode);
+ if (loadPromise) {
+ loadPromise
+ .then(() => {
+ // [Legacy] Refresh logic
+ // const newFlattenTreeData = flattenTreeData(
+ // treeData.value,
+ // newExpandedKeys,
+ // fieldNames.value,
+ // );
+ // flattenNodes.value = newFlattenTreeData;
+ })
+ .catch(() => {
+ const expandedKeysToRestore = arrDel(expandedKeys.value, key);
+ setExpandedKeys(expandedKeysToRestore);
+ });
+ }
+ }
+ };
+
+ const onListChangeStart = () => {
+ listChanging.value = true;
+ };
+
+ const onListChangeEnd = () => {
+ setTimeout(() => {
+ listChanging.value = false;
+ });
+ };
+
+ // =========================== Keyboard ===========================
+ const onActiveChange = (newActiveKey: Key) => {
+ const { onActiveChange } = props;
+
+ if (activeKey.value === newActiveKey) {
+ return;
+ }
+
+ activeKey.value = newActiveKey;
+ if (newActiveKey !== null) {
+ scrollTo({ key: newActiveKey });
+ }
+
+ if (onActiveChange) {
+ onActiveChange(newActiveKey);
+ }
+ };
+
+ // const getActiveItem = () => {
+ // if (activeKey.value === null) {
+ // return null;
+ // }
+
+ // return flattenNodes.value.find(({ data: { key } }) => key === activeKey.value) || null;
+ // };
+
+ const activeItem = computed(() => {
+ if (activeKey.value === null) {
+ return null;
+ }
+
+ return flattenNodes.value.find(({ data: { key } }) => key === activeKey.value) || null;
+ });
+
+ const offsetActiveKey = (offset: number) => {
+ let index = flattenNodes.value.findIndex(({ data: { key } }) => key === activeKey.value);
+
+ // Align with index
+ if (index === -1 && offset < 0) {
+ index = flattenNodes.value.length;
+ }
+
+ index = (index + offset + flattenNodes.value.length) % flattenNodes.value.length;
+
+ const item = flattenNodes.value[index];
+ if (item) {
+ const { key } = item.data;
+ onActiveChange(key);
+ } else {
+ onActiveChange(null);
+ }
+ };
+
+ const onKeyDown = event => {
+ const { onKeyDown, checkable, selectable } = props;
+
+ // >>>>>>>>>> Direction
+ switch (event.which) {
+ case KeyCode.UP: {
+ offsetActiveKey(-1);
+ event.preventDefault();
+ break;
+ }
+ case KeyCode.DOWN: {
+ offsetActiveKey(1);
+ event.preventDefault();
+ break;
+ }
+ }
+
+ // >>>>>>>>>> Expand & Selection
+ const item = activeItem.value;
+ if (item && item.data) {
+ const expandable = item.data.isLeaf === false || !!(item.data.children || []).length;
+ const eventNode = convertNodePropsToEventData({
+ ...getTreeNodeProps(activeKey.value, treeNodeRequiredProps.value),
+ data: item.data,
+ active: true,
+ });
+
+ switch (event.which) {
+ // >>> Expand
+ case KeyCode.LEFT: {
+ // Collapse if possible
+ if (expandable && expandedKeys.value.includes(activeKey.value)) {
+ onNodeExpand({} as MouseEvent, eventNode);
+ } else if (item.parent) {
+ onActiveChange(item.parent.data.key);
+ }
+ event.preventDefault();
+ break;
+ }
+ case KeyCode.RIGHT: {
+ // Expand if possible
+ if (expandable && !expandedKeys.value.includes(activeKey.value)) {
+ onNodeExpand({} as MouseEvent, eventNode);
+ } else if (item.children && item.children.length) {
+ onActiveChange(item.children[0].data.key);
+ }
+ event.preventDefault();
+ break;
+ }
+
+ // Selection
+ case KeyCode.ENTER:
+ case KeyCode.SPACE: {
+ if (
+ checkable &&
+ !eventNode.disabled &&
+ eventNode.checkable !== false &&
+ !eventNode.disableCheckbox
+ ) {
+ onNodeCheck(
+ {} as MouseEvent,
+ eventNode,
+ !checkedKeys.value.includes(activeKey.value),
+ );
+ } else if (
+ !checkable &&
+ selectable &&
+ !eventNode.disabled &&
+ eventNode.selectable !== false
+ ) {
+ onNodeSelect({} as MouseEvent, eventNode);
+ }
+ break;
+ }
+ }
+ }
+
+ if (onKeyDown) {
+ onKeyDown(event);
+ }
+ };
+
+ onUnmounted(() => {
+ window.removeEventListener('dragend', onWindowDragEnd);
+ destroyed.value = true;
+ });
+ return () => {
+ const {
+ // focused,
+ // flattenNodes,
+ // keyEntities,
+ dragging,
+ // activeKey,
+ dropLevelOffset,
+ dropContainerKey,
+ dropTargetKey,
+ dropPosition,
+ dragOverNodeKey,
+ // indent,
+ } = dragState;
+ const {
+ prefixCls,
+ showLine,
+ focusable,
+ tabindex = 0,
+ selectable,
+ showIcon,
+ icon,
+ switcherIcon,
+ draggable,
+ checkable,
+ checkStrictly,
+ disabled,
+ motion,
+ loadData,
+ filterTreeNode,
+ height,
+ itemHeight,
+ virtual,
+ titleRender,
+ dropIndicatorRender,
+ onContextmenu,
+ onScroll,
+ direction,
+ } = props;
+
+ const { class: className, style } = attrs;
+ const domProps = getDataAndAria({ ...props, ...attrs });
+
+ return (
+
+
+
+
+
+ );
+ };
+ },
+});
diff --git a/components/vc-tree/TreeNode.tsx b/components/vc-tree/TreeNode.tsx
new file mode 100644
index 000000000..da4650072
--- /dev/null
+++ b/components/vc-tree/TreeNode.tsx
@@ -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 ? (
+
+ {switcherIconDom}
+
+ ) : null;
+ }
+
+ const switcherCls = classNames(
+ `${prefixCls}-switcher`,
+ `${prefixCls}-switcher_${expanded ? ICON_OPEN : ICON_CLOSE}`,
+ );
+
+ const switcherIconDom = renderSwitcherIconDom(false);
+
+ return switcherIconDom !== false ? (
+
+ {switcherIconDom}
+
+ ) : 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 (
+
+ {$custom}
+
+ );
+ };
+
+ const renderIcon = () => {
+ const { loading } = props;
+ const { prefixCls } = context.value;
+
+ return (
+
+ );
+ };
+
+ 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 ? (
+
+ {typeof currentIcon === 'function' ? currentIcon(props) : currentIcon}
+
+ ) : (
+ 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 = {titleNode};
+
+ return (
+
+ {$icon}
+ {$title}
+ {renderDropIndicator()}
+
+ );
+ };
+ 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 (
+
+
+ {renderSwitcher()}
+ {renderCheckbox()}
+ {renderSelector()}
+
+ );
+ };
+ },
+});
diff --git a/components/vc-tree/assets/icons.png b/components/vc-tree/assets/icons.png
deleted file mode 100644
index ffda01ef1..000000000
Binary files a/components/vc-tree/assets/icons.png and /dev/null differ
diff --git a/components/vc-tree/assets/index.less b/components/vc-tree/assets/index.less
deleted file mode 100644
index 4dcb641db..000000000
--- a/components/vc-tree/assets/index.less
+++ /dev/null
@@ -1,194 +0,0 @@
-@treePrefixCls: rc-tree;
-.@{treePrefixCls} {
- margin: 0;
- padding: 5px;
- li {
- padding: 0;
- margin: 0;
- list-style: none;
- white-space: nowrap;
- outline: 0;
- .draggable {
- color: #333;
- -moz-user-select: none;
- -khtml-user-select: none;
- -webkit-user-select: none;
- user-select: none;
- /* Required to make elements draggable in old WebKit */
- -khtml-user-drag: element;
- -webkit-user-drag: element;
- }
- &.drag-over {
- > .draggable {
- background-color: #316ac5;
- color: white;
- border: 1px #316ac5 solid;
- opacity: 0.8;
- }
- }
- &.drag-over-gap-top {
- > .draggable {
- border-top: 2px blue solid;
- }
- }
- &.drag-over-gap-bottom {
- > .draggable {
- border-bottom: 2px blue solid;
- }
- }
- &.filter-node {
- > .@{treePrefixCls}-node-content-wrapper {
- color: #a60000 !important;
- font-weight: bold !important;
- }
- }
- ul {
- margin: 0;
- padding: 0 0 0 18px;
- }
- .@{treePrefixCls}-node-content-wrapper {
- display: inline-block;
- padding: 1px 3px 0 0;
- margin: 0;
- cursor: pointer;
- height: 17px;
- text-decoration: none;
- vertical-align: top;
- }
- span {
- &.@{treePrefixCls}-switcher,
- &.@{treePrefixCls}-checkbox,
- &.@{treePrefixCls}-iconEle {
- line-height: 16px;
- margin-right: 2px;
- width: 16px;
- height: 16px;
- display: inline-block;
- vertical-align: middle;
- border: 0 none;
- cursor: pointer;
- outline: none;
- background-color: transparent;
- background-repeat: no-repeat;
- background-attachment: scroll;
- background-image: url('data:image/png;base64,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');
-
- &.@{treePrefixCls}-icon__customize {
- background-image: none;
- }
- }
- &.@{treePrefixCls}-icon_loading {
- margin-right: 2px;
- vertical-align: top;
- background: url('data:image/gif;base64,R0lGODlhEAAQAKIGAMLY8YSx5HOm4Mjc88/g9Ofw+v///wAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQFCgAGACwAAAAAEAAQAAADMGi6RbUwGjKIXCAA016PgRBElAVlG/RdLOO0X9nK61W39qvqiwz5Ls/rRqrggsdkAgAh+QQFCgAGACwCAAAABwAFAAADD2hqELAmiFBIYY4MAutdCQAh+QQFCgAGACwGAAAABwAFAAADD1hU1kaDOKMYCGAGEeYFCQAh+QQFCgAGACwKAAIABQAHAAADEFhUZjSkKdZqBQG0IELDQAIAIfkEBQoABgAsCgAGAAUABwAAAxBoVlRKgyjmlAIBqCDCzUoCACH5BAUKAAYALAYACgAHAAUAAAMPaGpFtYYMAgJgLogA610JACH5BAUKAAYALAIACgAHAAUAAAMPCAHWFiI4o1ghZZJB5i0JACH5BAUKAAYALAAABgAFAAcAAAMQCAFmIaEp1motpDQySMNFAgA7')
- no-repeat scroll 0 0 transparent;
- }
- &.@{treePrefixCls}-switcher {
- &.@{treePrefixCls}-switcher-noop {
- cursor: auto;
- }
- &.@{treePrefixCls}-switcher_open {
- background-position: -93px -56px;
- }
- &.@{treePrefixCls}-switcher_close {
- background-position: -75px -56px;
- }
- }
- &.@{treePrefixCls}-checkbox {
- width: 13px;
- height: 13px;
- margin: 0 3px;
- background-position: 0 0;
- &-checked {
- background-position: -14px 0;
- }
- &-indeterminate {
- background-position: -14px -28px;
- }
- &-disabled {
- background-position: 0 -56px;
- }
- &.@{treePrefixCls}-checkbox-checked.@{treePrefixCls}-checkbox-disabled {
- background-position: -14px -56px;
- }
- &.@{treePrefixCls}-checkbox-indeterminate.@{treePrefixCls}-checkbox-disabled {
- position: relative;
- background: #ccc;
- border-radius: 3px;
- &::after {
- content: ' ';
- -webkit-transform: scale(1);
- transform: scale(1);
- position: absolute;
- left: 3px;
- top: 5px;
- width: 5px;
- height: 0;
- border: 2px solid #fff;
- border-top: 0;
- border-left: 0;
- }
- }
- }
- }
- }
- &:not(.@{treePrefixCls}-show-line) {
- .@{treePrefixCls}-switcher-noop {
- background: none;
- }
- }
- &.@{treePrefixCls}-show-line {
- li:not(:last-child) {
- > ul {
- background: url('data:image/gif;base64,R0lGODlhCQACAIAAAMzMzP///yH5BAEAAAEALAAAAAAJAAIAAAIEjI9pUAA7')
- 0 0 repeat-y;
- }
- > .@{treePrefixCls}-switcher-noop {
- background-position: -56px -18px;
- }
- }
- li:last-child {
- > .@{treePrefixCls}-switcher-noop {
- background-position: -56px -36px;
- }
- }
- }
- &-child-tree {
- display: none;
- &-open {
- display: block;
- }
- }
- &-treenode-disabled {
- > span:not(.@{treePrefixCls}-switcher),
- > a,
- > a span {
- color: #767676;
- cursor: not-allowed;
- }
- }
- &-node-selected {
- background-color: #ffe6b0;
- border: 1px #ffb951 solid;
- opacity: 0.8;
- }
- &-icon__open {
- margin-right: 2px;
- background-position: -110px -16px;
- vertical-align: top;
- }
- &-icon__close {
- margin-right: 2px;
- background-position: -110px 0;
- vertical-align: top;
- }
- &-icon__docu {
- margin-right: 2px;
- background-position: -110px -32px;
- vertical-align: top;
- }
- &-icon__customize {
- margin-right: 2px;
- vertical-align: top;
- }
-}
diff --git a/components/vc-tree/assets/line.gif b/components/vc-tree/assets/line.gif
deleted file mode 100644
index d561d36a9..000000000
Binary files a/components/vc-tree/assets/line.gif and /dev/null differ
diff --git a/components/vc-tree/assets/loading.gif b/components/vc-tree/assets/loading.gif
deleted file mode 100644
index e8c289293..000000000
Binary files a/components/vc-tree/assets/loading.gif and /dev/null differ
diff --git a/components/vc-tree/contextTypes.ts b/components/vc-tree/contextTypes.ts
new file mode 100644
index 000000000..36d61e174
--- /dev/null
+++ b/components/vc-tree/contextTypes.ts
@@ -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;
+ // 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;
+ 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> = Symbol('TreeContextKey');
+
+export const TreeContext = defineComponent({
+ props: {
+ value: { type: Object as PropType },
+ },
+ setup(props, { slots }) {
+ provide(
+ TreeContextKey,
+ computed(() => props.value),
+ );
+ return slots.default?.();
+ },
+});
+
+export const useInjectTreeContext = () => {
+ return inject(
+ TreeContextKey,
+ computed(() => ({} as TreeContextProps)),
+ );
+};
diff --git a/components/vc-tree/index.js b/components/vc-tree/index.js
deleted file mode 100644
index 384396194..000000000
--- a/components/vc-tree/index.js
+++ /dev/null
@@ -1,4 +0,0 @@
-// based on rc-tree 2.1.3
-import Tree from './src';
-
-export default Tree;
diff --git a/components/vc-tree/index.ts b/components/vc-tree/index.ts
new file mode 100644
index 000000000..6df172093
--- /dev/null
+++ b/components/vc-tree/index.ts
@@ -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;
diff --git a/components/vc-tree/interface.tsx b/components/vc-tree/interface.tsx
new file mode 100644
index 000000000..1d277f9d0
--- /dev/null
+++ b/components/vc-tree/interface.tsx
@@ -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 {
+ 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 = (record: RecordType, index?: number) => Key;
+
+export type GetCheckDisabled = (record: RecordType) => boolean;
+
+export type Direction = 'ltr' | 'rtl' | undefined;
+
+export interface FieldNames {
+ title?: string;
+ key?: string;
+ children?: string;
+}
diff --git a/components/vc-tree/props.ts b/components/vc-tree/props.ts
new file mode 100644
index 000000000..32513b4d0
--- /dev/null
+++ b/components/vc-tree/props.ts
@@ -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,
+ /** New added in Tree for easy data access */
+ data: { type: Object as PropType },
+ isStart: { type: Array as PropType },
+ isEnd: { type: Array as PropType },
+ active: { type: Boolean, default: undefined },
+ onMousemove: { type: Function as PropType },
+
+ // 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>;
+
+export const nodeListProps = {
+ prefixCls: { type: String as PropType },
+ data: { type: Array as PropType },
+ motion: { type: Object as PropType },
+ focusable: { type: Boolean as PropType },
+ activeItem: { type: Object as PropType },
+ focused: { type: Boolean as PropType },
+ tabindex: { type: Number as PropType },
+ checkable: { type: Boolean as PropType },
+ selectable: { type: Boolean as PropType },
+ disabled: { type: Boolean as PropType },
+
+ expandedKeys: { type: Array as PropType },
+ selectedKeys: { type: Array as PropType },
+ checkedKeys: { type: Array as PropType },
+ loadedKeys: { type: Array as PropType },
+ loadingKeys: { type: Array as PropType },
+ halfCheckedKeys: { type: Array as PropType },
+ keyEntities: { type: Object as PropType> },
+
+ dragging: { type: Boolean as PropType },
+ dragOverNodeKey: { type: [String, Number] as PropType },
+ dropPosition: { type: Number as PropType },
+
+ // Virtual list
+ height: { type: Number as PropType },
+ itemHeight: { type: Number as PropType },
+ virtual: { type: Boolean as PropType },
+
+ onKeydown: { type: Function as PropType },
+ 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 },
+
+ onListChangeStart: { type: Function as PropType<() => void> },
+ onListChangeEnd: { type: Function as PropType<() => void> },
+};
+
+export type NodeListProps = Partial>;
+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 }, // 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 },
+ expandedKeys: { type: Array as PropType },
+ defaultCheckedKeys: { type: Array as PropType },
+ checkedKeys: {
+ type: [Object, Array] as PropType,
+ },
+ defaultSelectedKeys: { type: Array as PropType },
+ selectedKeys: { type: Array as PropType },
+ allowDrop: { type: Function as PropType },
+ 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 },
+ onContextmenu: { type: Function as PropType },
+ onClick: { type: Function as PropType },
+ onDblClick: { type: Function as PropType },
+ onScroll: { type: Function as PropType },
+ 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> },
+ loadedKeys: { type: Array as PropType },
+ 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 },
+});
+
+export type TreeProps = Partial>>;
diff --git a/components/vc-tree/src/Tree.jsx b/components/vc-tree/src/Tree.jsx
deleted file mode 100644
index 2085c138a..000000000
--- a/components/vc-tree/src/Tree.jsx
+++ /dev/null
@@ -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 (
-
- {mapChildren(treeNode, (node, index) => this.renderTreeNode(node, index))}
-
- );
- },
-});
-
-export { Tree };
-
-export default Tree;
diff --git a/components/vc-tree/src/TreeNode.jsx b/components/vc-tree/src/TreeNode.jsx
deleted file mode 100644
index aee09368e..000000000
--- a/components/vc-tree/src/TreeNode.jsx
+++ /dev/null
@@ -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 (
-
- {typeof switcherIcon === 'function'
- ? switcherIcon({ ...this.$props, ...this.$props.dataRef, isLeaf: true })
- : switcherIcon}
-
- );
- }
-
- const switcherCls = classNames(
- `${prefixCls}-switcher`,
- `${prefixCls}-switcher_${expanded ? ICON_OPEN : ICON_CLOSE}`,
- );
- return (
-
- {typeof switcherIcon === 'function'
- ? switcherIcon({ ...this.$props, ...this.$props.dataRef, isLeaf: false })
- : switcherIcon}
-
- );
- },
-
- // 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 (
-
- {$custom}
-
- );
- },
-
- renderIcon() {
- const { loading } = this;
- const {
- vcTree: { prefixCls },
- } = this;
-
- return (
-
- );
- },
-
- // 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 ? (
-
- {typeof currentIcon === 'function'
- ? currentIcon({ ...this.$props, ...this.$props.dataRef })
- : currentIcon}
-
- ) : (
- this.renderIcon()
- );
- } else if (loadData && loading) {
- $icon = this.renderIcon();
- }
-
- const currentTitle = title;
- let $title = currentTitle ? (
-
- {typeof currentTitle === 'function'
- ? currentTitle({ ...this.$props, ...this.$props.dataRef })
- : currentTitle}
-
- ) : (
- {defaultTitle}
- );
- return (
-
- {$icon}
- {$title}
-
- );
- },
-
- // 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 = (
-
- {mapChildren(nodeList, (node, index) => renderTreeNode(node, index, pos))}
-
- );
- }
-
- return {$children};
- },
- },
-
- 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 (
-
- {this.renderSwitcher()}
- {this.renderCheckbox()}
- {this.renderSelector()}
- {this.renderChildren()}
-
- );
- },
-});
-
-TreeNode.isTreeNode = 1;
-
-export default TreeNode;
diff --git a/components/vc-tree/src/index.js b/components/vc-tree/src/index.js
deleted file mode 100644
index a37c0d201..000000000
--- a/components/vc-tree/src/index.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import Tree from './Tree';
-import TreeNode from './TreeNode';
-Tree.TreeNode = TreeNode;
-
-export default Tree;
diff --git a/components/vc-tree/src/util.js b/components/vc-tree/src/util.js
deleted file mode 100644
index ed5a5e307..000000000
--- a/components/vc-tree/src/util.js
+++ /dev/null
@@ -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 {childrenNodes};
- });
-}
-
-// 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;
- }, {});
-}
diff --git a/components/vc-tree/util.tsx b/components/vc-tree/util.tsx
new file mode 100644
index 000000000..5fb761803
--- /dev/null
+++ b/components/vc-tree/util.tsx
@@ -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[] {
+ // 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,
+ 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 => 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 {childrenNodes};
+ });
+}
+
+/**
+ * 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[] {
+ const expandedKeys = new Set();
+
+ 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) {
+ const omitProps: Record = {};
+ Object.keys(props).forEach(key => {
+ if (key.startsWith('data-') || key.startsWith('aria-')) {
+ omitProps[key] = props[key];
+ }
+ });
+
+ return omitProps;
+}
diff --git a/components/vc-tree/utils/conductUtil.ts b/components/vc-tree/utils/conductUtil.ts
new file mode 100644
index 000000000..1eff1685e
--- /dev/null
+++ b/components/vc-tree/utils/conductUtil.ts
@@ -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, checkedKeys: Set) {
+ const filteredKeys = new Set();
+ 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,
+ levelEntities: Map>,
+ maxLevel: number,
+ syntheticGetCheckDisabled: GetCheckDisabled,
+): ConductReturnType {
+ const checkedKeys = new Set(keys);
+ const halfCheckedKeys = new Set();
+
+ // 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();
+ 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,
+ halfKeys: Key[],
+ levelEntities: Map>,
+ maxLevel: number,
+ syntheticGetCheckDisabled: GetCheckDisabled,
+): ConductReturnType {
+ const checkedKeys = new Set(keys);
+ let halfCheckedKeys = new Set(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();
+ const visitedKeys = new Set();
+ 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,
+ getCheckDisabled?: GetCheckDisabled,
+): ConductReturnType {
+ const warningMissKeys: Key[] = [];
+
+ let syntheticGetCheckDisabled: GetCheckDisabled;
+ if (getCheckDisabled) {
+ syntheticGetCheckDisabled = getCheckDisabled;
+ } else {
+ syntheticGetCheckDisabled = isCheckDisabled;
+ }
+
+ // We only handle exist keys
+ const keys = new Set(
+ keyList.filter(key => {
+ const hasEntity = !!keyEntities[key];
+ if (!hasEntity) {
+ warningMissKeys.push(key);
+ }
+
+ return hasEntity;
+ }),
+ );
+ const levelEntities = new Map>();
+ let maxLevel = 0;
+
+ // Convert entities by level for calculation
+ Object.keys(keyEntities).forEach(key => {
+ const entity = keyEntities[key];
+ const { level } = entity;
+
+ let levelSet: Set = 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;
+}
diff --git a/components/vc-tree/utils/diffUtil.ts b/components/vc-tree/utils/diffUtil.ts
new file mode 100644
index 000000000..1c4f5e891
--- /dev/null
+++ b/components/vc-tree/utils/diffUtil.ts
@@ -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 = 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);
+}
diff --git a/components/vc-tree/utils/treeUtil.ts b/components/vc-tree/utils/treeUtil.ts
new file mode 100644
index 000000000..a077d2a59
--- /dev/null
+++ b/components/vc-tree/utils/treeUtil.ts
@@ -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 = 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 | 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)(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;
+ keyEntities: Record;
+}
+
+/**
+ * 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;
+}
+
+/**
+ * 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;
+}
diff --git a/components/vc-virtual-list/List.tsx b/components/vc-virtual-list/List.tsx
index ea9d788cf..0644aba21 100644
--- a/components/vc-virtual-list/List.tsx
+++ b/components/vc-virtual-list/List.tsx
@@ -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(
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)>,
+ type: [String, Number, Function] as PropType) => 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,
diff --git a/v2-doc b/v2-doc
index 7a7b52df8..d571ad4bf 160000
--- a/v2-doc
+++ b/v2-doc
@@ -1 +1 @@
-Subproject commit 7a7b52df8b3b69d8b1a8b8dcd96e1b0f7bb3f8c9
+Subproject commit d571ad4bf772cfc372511dc1dedf07981dc56ae8