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 ( + + ); +}; + +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,iVBORw0KGgoAAAANSUhEUgAAAKAAAABhCAYAAABRe6o8AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAK0dJREFUeNrsfQl8VNX1/5l9ksm+ELJB2ANECGtYVEAQaZBSFdAW0dpaKbi0WhX9Va1/S/+K2k+1iCztT4sFW6lKkUV2RLZAQHaSQBJCMllJJtvsM2/e75775k3evHkzTCZEAubweczMu/d7ZzLznXPvOff7zsjS7nudhXZaxZd/kKXf//9Cwgkf1xha2QOnS2DzofNw5FwZjM/KgFkTh8Idw/tBz7hImb9xQsV1W9czJf73zTsPek7I5XL3oQCFQkkOBSiV3C2eG/rz9z19Q8Wh7T5+kX3i7c9g6ojekDs6A1796Vg4XVoPe/ILYMnKzbDmxQfZaaMH+pApVFy3Sdupp8cKH6rJ8QQ55pBjvPvcEXJ8To415LDzHbOXH/OAZLK2t/vBbbcFHOOz3LOeMViW5QgYLImwTcrai0MSrdm4H/708ztgwtA0D+6OYb1hysh+kDtuEPxjWx59jUIyhYq7lc2k38HaGk5KtmniR4Au7Z5g34cnZHLF6vTRkyCuzyCAuATurKF+kuFy0aSK4/uXsy5moZuIkkbI94RCplidlZYDvZP7QUx8LD3f1NA46Up1yaRz+qPLSZ+FhIRrvDxgsCTC22DIp1Kp6OORX42GM/ef8sLh9IkeTEwi4fNNyu5Lb7Hf4VW/ZXFaDRV3qxPQcjUfEoaNkWxrLi0CW1MvVhMzOOD74GJci8Nj4lZkzn6UfKAMgLkZdv7+JU/79P95B+IG3gaFm9auNjcZlHKF/EPxGPO2ZC2O0EStmD6aOL4oBixghGpo5EgWr4F+8QOgX69M2Hn889Wkr3LDvefoGPL2kE/syXgcYpRKlQ/5uD7eOFy74fTpj0R8/8kj+sOsCUNofykcThYHLQfhVwW/gi1VW8HG2iVxt7q5GCewLukjLCERmos/g7rjr7PCo/XKVuH6Xa1QqTjyWQwAVytg53tLYfrGWs+x8/+/QNuwD/Z1T9Ve065SoVxx94g5YNY1Q6O9Giz2Vjhy7AA98D6ewzbsg33dUzXnAYMlnzQBFXDn3rsgb8YhihOST0hS3jBwwLVbMM83c/xgWLfrJMydku2DO2g8CJ/b/gNmpQmWXXgL7HY7zB/8sA+us2zTgXNs3oVyv+3jhvSC2XdkyTp7HMZpB5axSy/ww7SQkDXc53ztqUMQ2XsmvW93Mov6jL2TEKwFoPEqrl4o6ahtfBXgvj9yjze+RumSkj0RLh/bt4g88CzqnXbXotv65IBN2wqt5gYyAsfvv489QG//2vo091zkn1wrhyEpo+Hk5SN0DCXvpYIhny8BORx9o7ZPhO9+fNyLfBfmnffBYdSKgUMwz4fR7ZN/2SiJW1exDkyEfGazGaw2B7x77B1YMPQRH1xnGZLmzYW5wBAPxDid4CREcNht4HTYyJfBBn/dWoTE6fRxGKcNXE5ru147YgQBxEOxaX0AWuoAHBbvjg7BuNhG+mDfsvxvHhISUE7G6BmXDk3WBrC5rFBUUsA1uOObMwWn6O2gfoOBdTYA9pWX5T3kIWCw5BMTkMfx5o98QhySA6NWDByu9XzHCrgUixTugfg58PaFZWAlH1JLcxP8aeybkrjONCFpdBHRUF9bQUnjsFlDHkdIvmDGwb7tJSBiPF5SIR+lJMsmV10Tmc+d4FmX4fSOz//PpwUkdIIyNoVihOPJlLJRKo0SjOYWcAHj8Xy88Y+XVj4KDnBCTFgSxXieK1jyyWRiAnI49HxCE5NPiMN83Z6TZUE935bDBbS/FG5G2gz4bf9nQW5Uwp9y3oR5Q+dJ4jqVgALS0CnGTRr+cSjjCMkXzDg8AdtzCAlIUwYOO9isZrBZuIM3vL/7yw30wPsO0sdlsZIp3+UQvw4H+RtsNguZjSx+Xyu22YgntVvtmINxeAgYLPmE+R5vnJxGu/7IJ8RhsnjH8WI4fF4f8Pn2nSyBTQfP0v5SOJ1KR9d8Zx87A49lPwaR2khJ3LXsxIkTbDC3kh++2/PFxPWgj1PS+0Pv/lmUQP7Gv9Y4CUnp7RoHp1PWaWnXIZyCzXbnebPJRDwXruUs9Ghb21k8gQhtw6ibLHksjOuiF/ksDDcGGcRKyP180Wx68MY/ttIvCxmDkpkbQ8l7svaSTwp3LfKhYWoEk8WYr0M8Rq1S5Fu34wQmlT07G6HirmWjRo2SBXMrZeih+GkXSVN84QS9L/Qw7R2H93zBjtPRKbimyby5qUafHR0RAbbmBuKZXBDJr9f37IHpT7m9IQnytDER0FyjpxivXGSdeXN9Y022JloHLfYmEoK4vJ7Pbuden4z4uxhNItQ311CMIA3TfvJ1BIdJ4p/njoOn3v8KXl6zHb49fZm4Zgb2nyqF332wGX617DOYP30UiJPJoeKC8YChmHitxpOmvVOweNptzzh8ENKeQ+gBF28oWllfkA9MeAKARgcOhwOq3+QiZD4arn5rFm3DPtgXMcLXsPP3ZSsvNpyCSCYW1BBGXreDEnbhiSn0wPt4DtuwD/ZFjMcDirfJgrVQcTyZMFmM+TpMmWDUyu/pLnl4ql8PFiruWh4wFBOS5sKpwx7S4JRK5oeQxhGSL5hxAqVhAmF4I7Fvw5kKwxvKo7teSx07BViVHhxNdaBfeg/nZNThoIojgUd8GuiP7gLsixivARuhofZC0xunlAdfy0qZAA2qKmiy14PdxX0x1XItxKgTIF6RAqcqDwL2RQz1irgf90M29IChkLCr5AHL85ezVy9tbtdrTxwwC3qNeVrG7wWP+CA/YtXMjFfG9UtaEjcgGzTRsWR9L6M5QScjA1uTAQyXTkFeSe2yX28tW3ryqTFGib3giIlLU19JHxW/pG/MUNBpogFUMpoTlDtkYLQ1QWnTeag40bDs0CuVS0l/I3JPdqPUMOvX/VM+NfcnDHqyLahqOV8G44dmwL1uVcuebf/VzH94geRXu1sNc33FCISA+J7pyNH3rbtSnxmSHD0pPVbXH9v1jabS89XN+17aW/lX8rAUl3yEgKwEAT1jjHqxxzOJAyInRaeG0zFaqsyldRdb9514u84zBqdFcIsRKj4mEQtDoh+nkYTkLWRVTBaSZDEJDIbcVu7Wie1W6LMsvY1QIeLQkjJzmAm/fg9mj4qCR0Yp4cP7tJB36TJsPnAJlqxUYCBhc/9RPkIG3OtF3KMEt9IXx7Z3DdiRabirjtMeQ0KhRyJELCREexGgkrgvsmBzbzfjtjK2k36B5no6BjkKCdHIGHWSY4BAUdMmRgiSRCwjyvGEiEMSrd+8Hf72eDrcNZDx4Cb3t8HkPlaYOYiBf372Een5Cx81TCi4zloDduVxgjWhJ2OXU3IY3EfQJlrGtWsMjoBuEpU7h4NcoQBFhO/OSNi5J8mHLfoC+MEJBQlF/cd74XhVC08i3AVwhg8CB/HWytbzoGw+CVMyagih5ZJqmPbiuj1gYBu7+pTwYdB6wGMLs6/LGEouE855MEoif3o+JJHLLsqgczgF7auk/cRqGDEO1244ffIkssTdBaxMxeXDokeBMzILNKUrYHLvavjxAC3tj6ICMa46YjocMebBuuLf0W25GelPQmzJmz64W90DXk89oEIuWz0pMx0GpcVBAiflg/pGmFSkN0zaX1ixnHGxAfWAoYzB7ZG5p8+AOkCXRLjvxqEaRkqKxW0oeuMwcLh3mJLinJpUD/k8pJZrwBk1nOJy+1+l/aVwSD6hGuar0q8kcZ2ZB+wK46AeMC5rhOThtKAesOCa47lY1+KYcO3qp340HIYMjAMj+Ug++FpPj3/n6ek5bMM+2DfYMYqauQPv+xuDEpBfSwXaE6YkEm0B8jiaLtg+0Yd8uDMixmHUOq4Xt0Z0cEGSb54qbhzF5SQ30P5SOFTDNBgMYBKoYaRwt7oHvB56QJVCseLROzPBwJDAshVgywE97PhpmudYv1dP27AP9gWRHtDfGLjli0czCQH8jcF5QHfgEFAHiCQS70HzAYfbpNQwYhymTPIuWbjna5X2Uor6AxRzVB/hpYYR4nDaramsgbraq9DS3AjPjXxeEnere0A+ES118HpA8WGsPtSGd9gXTRyQAmQxBVctHGGQdGivFXJ98DG2YR/sixiv1yAaw+bkMHZCODwOHNf7HYPzgO6oNaAOkBLJ6e0B3bhAahgxDvN1m884KQ4DB5nL5kNqxdVvKW5rcaKXGkaIk1LDSOFudQ/Y0a041AP26RELda0oEkDFimB6t3jfxz7YFzHC1yAeg8fh7dGTeg+hpcZQejyZ0xJwb9eFbp11+npAiuPUMMO+zPYRJIhxmCzGfB2mTDBqxYAD1244faIHQxLJLJXwTVkMbC5Ng5cFahghDgOO+QT30Nz/criTT0nibtWdEJvhNGurPwnhkYnQUnIlqNesigwDTVyUlxhBrlCOUqmV0NTgAifrHRpYbS54Ok+Q9CDeMSVeSTHCcf2NgXiefPx44jG4KNidr/OkWvjAgXgTFz3cJHIx3h5QhCvqfRuwh+8PiONVLTRf55DTqFVlugJK/eee6RpJtP5CmqQapr24zvJcN1oRba49CpFpCaAMTw76NTdePAtys9FHD2gnrDET19dGHi5/jOf01dy2b1pyPApRyRStAhewPnpAqTHM1J2Gtb1m8lg8hjsP6E4Wi8jHT58eErGMKA8YGo5LEv+C5vUwZYJRa06yhazdouj0iR4MSSSlhgkF11l5txupiNbE4VruIET16hv086giI8FqqPaagp1W83kSyGWjgspi95ZRWchijvdgP9vRCpFqOSGRE1xWy0VvGkiPgXjEfXpPpOexeAxKQPE2WbAWKo4nk0fVcug8PLnDvad7z1A6fYo92Pp1//QsOXjcFwT3wrdlkNMvA+524/Zs+69sfeFR2nH+wws6de12IxXR2oRsuFq4jkS6MSDzc722DwHDldBQ0uClhjEbajbr65uyI8KiocFI1pPUg3GEaTA0e+7ja4oI14K+vplivLyxaAzOIj2C2jmbbfD5rATJMbrVMG4PeK1bMe7l1dvYVx++nXo+saE065O8RpxaO3Wc2nMfs3IohoiE+KD/XkO5Hpqq9TB09gZOQRCelJzz3s6q2dkZUFjvAIPFQZXNW+e2Te2zvqiGuDAVZCaoYNOpMjj62+kprLm22uMR/IzhtU4k3xGpMZShqlpCxQk8GUzN/Qn1ZLuJJ8srcXuyNjUMCuFcUp7seqphbmZFdFTanVB+dA9oI4LXHmJfhhEs4Sx1DYaSM2/sUitfmzIwFfRyFupMDrjnX3raHE6mzBSdCtKilLDrgh6wL2K852rpMczu6RjH6OFnDDoFv56bLIypgf6TiQ65jEqqX95Y6ukaCKeOwTwj4sgU0+LywqElZeawuc9+AFNHpMKUoT3gsbv7gr7GCPlnC2DZ2m3w1lNzmNrCozLxFIy4F5d/QXG5BLfYF8fyuGCm4I6sAW+0Ijospp+MYXTspbz89kgHIDJxmOfRmFUn7fm/HvGO4+lVGrN93JLstDjIjNeQz1AJODnKwAkGsxW2nqsiHjdvWdnyX7+DGOGIHRnDqzbMtcgn8/cxSZAvPae3uw2g6pjeh3z/+no/vPDj4dAzVkXCczvU110FnUoBM4cnw9j+PeCLvXnwwF3jWCEJQ8V11hqwKyiih+Suvh75RxMhxdIygE/1j731THTGkEm6pHS6TWWq05c2Xz6/r/Ljl4Ravus2hrJd5JNgoCZBS75UMircczQ5vMj36O5HYe3da0mzzGvanfncB/D8rOEQHyGDxsYm8qY7qKQHnw8vNI8k0drdWanw6qovYOPbT+FULxPjHLEuiEiKapsFagjOyvrgOssDYn4OUyTSpqDt3+c4HTHijaiWj3ixQkKSFysBJLV8Ys93PcZQtod8MtHnieTrPTrD4+kqjldA+pheHvJ5uC1YLdIaL9mpkBSrhEZDE9iIFxMGQi6yesUjITERZowaQPoXwdwpo71wzhgWwpLCodqip3vCuC3Xt2d/MLMmiG2ReeE6ywNicjiYPN/3NU6oJpRVwUI2JD1gR8ZQctwJjnw+V7mx3ONH9/4c1k5dK0k+fnze9pDAYfKQHmCxWD2ez2tI8hivzDKZTDAsIx6253FEEuKiMmMp+YRqmGf7PweZyUOgubrJC9eZa8CuMM6Kb1rZ1ro6v+0NBRfg97+5A2JjY2X8+yvaRvPcb29tP946rAcMmnyit8VzJQCSbg+Zbqet9SIfTr+0XYDLLy2DBVMzoIG8aYFSQE5CwrSkCDhbWuWDQ5OqDfP32R/74G71vWAXw8BL8/p5Zg7+YBgXVDZY4W8F5L3aVUGWOo0sT0IpC6W2n4S1Ww/oS8AA5JP5MNCbXVLkqz5WBS5TW1JoTL8MqK4zgVbOXTfsj4TYVtXQCtkDUnxwaFK1YaRwt7oHZJ3cLCKswcPSrTG8pJJ7/C2TCsyWYkpCqXWxuLbfpu3rvNrDlTEwe8KjPrX9vL4IrGtxnC58xaNTMoFRkQWfg3jfZvdSza0HvK1PHKzdV7jaYDIr5TJ5W33AoMknmoJl7j8HPZ/QfMgnDEImZMLpigbQasNAofC9eJ1/LVqtFs5fMcAUsp4T48zVRugb399LDTMkfSgYq4w+uFveAzq8lzE8+Rhyh+G2NaB30SHQl1RDQUGBlOfzqe23fsZJr+Nv0/ZJ1vYTTrsd0gMGSz7xO+NscYKeBB6UhHev9Us+IW5CVj/49lwVNFoZCA/XuasoeC8BwsLCwOiUwb4z5TBh2EAfnKOKrBEJ2XDN99Hsj2BIGkc+W4XFBxeMx7leOyo3YhzGYfd4PtThIflMxPsYyREbEwY/e2AW3Dt5FrBkWm5ubvZd6thdi7BeH1/bz2Zryz1iXT/+oG2kD/ZFjOg1SOoBUQfIawID6gFDIR+PY5oZT57vWuRD+2bHZuWrj98Dh4uugkWmhuiYGEo4lPNrNBqIjo4mLjwMjpc2wgsL7sb+Gikce5WF+rw6qDlYBXWHa4CtZSRxt7wHtNuJp+M+dCQeHrwipcUKEElWIj2HAiWglAlr+1mxhouzLe949NBBepw8eoq2YR9a2y9IPSCSDvWAQn2gWA/IETAE8glxTiOSsJISLxD5+C9MbeFJ5cw7RsCqbefhVIURXJoI6NkzBeThUXCuygJ/21EAU8ZkwdXiUzpB1BQq7tb2gMRjoYdxuPmF5LM6uIO2IzldeCtNQGFtP5uVrKfNjZ42fgr+eNoB2oZ9VGEqT20/D4l5PSD53FHzhwdvSEL+Md5iH7VapAcUb5MFa6HiKJkunVKsX/oErYzwlagywj8emEErI0iQKFTcLesBGeKZcL2HJOTJR3dX3Ao4/OydDHftiN+9aHdtPzKHgEKw8/KH0p+K3CVXZpev7ee1m+NHU4jG6wIl9YDiH48J1kLF8Tb/4QX4tZDhpZNSl0/iPq5QuCDY170m7vuIXrtMjWi7DcxubonJh+f5c5iukSQfV9svG99UK+O992xymL0ehynCweJsq+3nWUcG0BSiHtCzWyWlB/y+1TACcgVVG0ZIQt46Qw3TXusqNaJd7qAhEPnwnMspTcBAtf2qL7d9MRJSe/rU9vN4OD96wDmb6wW9IiX1gJ1WG6YRVPju4CIFoi01XjgkFdaGmbiIqw2zYKQSls8Og2MlZbDtYDG8vEoBq16YZyP9JNUwC9/hasM8QnAf+OK+NzVMV6gR7SJRsMPpSz7P1Mhw60B/UzDW6Yv7NOrVcRHToRkMYMTPT7AG5O2Fs/fT2n55DTu52n6COLjo3cUrY9J2vjo7OwLqyQyOesCZ/6n2eh5eU5igYWBTQT3FwBsPdE5tGCTfhejxnu2SwZX/8YIhiT7dvB1W/yId7uzHgNPWQr6hdsjp7YTx6VaYMdAJ6zd8DPPnPeajhgkF11lrt65QI5rBKJj1Jh8SzsG0BSH2AASUqu23+PjdPrX9eir7+NT2a5tbO6gH5En08fZGdy4u1ic5/WC/7ZK1YertRtiebyZ91ISDsZJqGJngumBUtdxOPN8qQqLbCYlMNgYssj5gDUsBhaUMtLaLMDa1hoZ1i9/dAPtXPONRwwhxlxSJYIhty/XFGKsI7oAPLlgP2F5FNP3z3Z6PtxROfUSlWf7GD2Yc3oIZx2FqhQ/eWndNomKR8fDwcKkm+77flb8zcSmjsY7aTWv7pWnI36EV1PYzN8Hxpt18bb93xEFeh/WAvAcLuCcsURsGyVcA8dB7THxANYy4NsyPyfR5ByGRmZCvUT0STGYH2IzkGyfrCVpCxNjmrwmZ9DBrQAMcPIM1XkZ44YqRfJpYbzVMfH/yLR8PYx07vXDBesCbtUb0b56aAiUlJVS8Ech0ul7Qr5/fS1VNXNHIyk9HvVgTTG0/yTFC1wO6p08pz+fRAUrVhmGMAIr4a6phQCABx4AD13wMmT7R8yH5mpqN5A20YIKTvFFhoFT2B5WtEu7ua4B/H75AiSTEoefzp4ax62VeuM60rlAjOjU1VUaOjv4pIdX2E3nB0PWA/Not0J6wVG0YcBg9ktaAahhhbRgS7WLAgWs3nHbR85lNVjAaLfT58LnDY3uDkyxsRiY1wbO7rvjg0PyqYUS4zrSuoIjuMPM6UNuPtw7rAfmAI+CesFRtGDq1BlbDDLn0IURaUBqVSc9jqgWjVgwccM2H067MrXPgvwBy02V6XfF31ToYN7S3Dw7NnxpGjOss6yqK6GXLlmE8mivVRqbce+fMmRNwHdw16gO6o92AOkCJ2jAyTFy61TD+pFg52iovHOb5MGWCUSsGHGHEC+K0yz03mYJJqB5mLCQvzAK7SlMgd+oQHxwGHLwa5u1j73JqmLShENZQ5oPrLOtCiujcJUuW3CvV8Pnnn+PBXouEbruB9QHdqZaAe8IStWFi7FdhcP3OwGoYidowm88r4FCxEzTOGoghAUecvIK82HBIVNdAgnEnRDDlcKJSA9suJ8PtgtowPC697gBENZd7qWHCGy5DSvkWH9wP3Qj5KAkD5hJDrO13Pcbwqg3jSbUEKrMhXD8QXIyzkeb5ClLnek271POpfXFYuWDl8/NYzNexDhfkkGgXAw5HK0vTNUqwwokqDXxe2AP++uwc2Pv1JjkmlH1wJNrFgMPBBMZ1WxsJ/XhCLy0fKmj4ZSHKqe4YnUbPRak4Ld8HO0+vIF7s76KAJOQx5O7NvA7Vhom2VMOQK/+AIaV/a1vzBcBhknj+vJ/D01tS4I974+A7PQtKVxOcqSZrmkMp8Ny+LHjoocVQV3RM4Y7QOoT7IZt7Gubv+7wnUvUBSUxHD17Th+faWx9QWBcQ7+M5qTE6qTZM5jWxtYXHZJgsxnwdpkwwas0hgcNMsnZ7nkyfxIN5KiOIcd9++Bu6F7zx0HlYwteGmTYUXhBVVOj2fHPEAcsWcR8vLR8h3ZlCwTXcQ7gKqVglYVhmGtQ5OS3fN7Iyr98LFo+BhuMI6wLyJh7je1fDDByQDGNypnleO+bqpPJ1/PSZf3Q3SOzrXjc1zK1ieCESf3kDf421MNVyZdNKmGTYf2/ekv3oBVeOW7aNrsPEtf2E9fx4w3NP57naVR9QXBfQM2mK6wOSD7jdUxUhkCxUnJBUST0zWLO5FaxWE819KVUa0Gp1EB4eCbU1ZV4E5zHtwQmI/oMgoERejz4u/2oV1Odvh3ELngWXTAHHPnkXpz9PIOCt5QuTHF9Ky+eVQLymHtAddEjVB4xLaGNrW3VT6Z9sKCpoK8cbKi6t1+AjrS0N45qb60Gni4aIyDhXz56p8pqaSpfdZpbj+eiYHmxkVHyevrxgfEdxPyQC8rf8FYdIPsOJnTDup08CU1cGNWabaBnvreUT6vf4un78ufbUBxTXBeRNsj5gsCSS+6lDJ4XjZgDWc8mg0JBEKEGKjU12pqX3VvLpoLS03vRWX1HubG2tV2K/64H7oRAQ32uGYTzk029ZA00nd3PkM1RBpcEAVfn7odFsX+/xTpL1AT10gfu/4jR9cvJ5tq8+oHddQN4k9YDBko/+XkgQ5JOTV4uPS4vPwMDMkV44nD7RUwlI5GNp6b2Uej04Gw1VSuyPX+hQcZ31gXcVRTQ/zSLxuAvSuduaHR9By6m9PuSrbDJ/OWfN/oXscg4rpeXjLx/hNX18bT+xlo+3joyhbA/5xJ6M/n4I66KOCL91YvJxfbxxuHbD6dMfiTxkSuultNtMtL8UDn+awWhsBZOphawDLZCQmAKJPVJ9cJ1lXUURzXs/JB6WNMHLKivOvwEG6wbodddMYFobPOQrtmlrFqz5+hEQKlo6oOW7HmMICHht8kkTUAZ1NWVkfTbIh3xCcnsiIhI44NrNswsTwNSacFdLS4NcCmc0tpB2Hfmg7GCzGqG6uowSUIzrTOsKimg0/Kzw0la1Wk01f6f1G+BHD34KX3/2M7BEtYIzn4SefUZDSa3iJMBGLzlVl6gPGCz5fAnYNrXqy4ugb/9hXuQbkpXjg8M3FwOHYN5YGmBUFUvizKZW8o13ksNKK34K1xlCXKcSsAsooo1G4zfLli3zOjesB9C94WG3vwJnDi6FBtvkGiSf0+nc42eYG1sfMFjyiQmIOOGGgxT5VCq1Fw5TJhi18oFDIMN+pL9cCofEsxDPh+TDD0qjDZPEdaZ1BUX00qVLscwFBhVa/tyHr2udxPv9BO9fLrdtfvL9jS8Rz4fyqCbJ9NiNrg8YLPlkMrmP68do15/n48knxGG+DlMmwXzA2A/7S+ESEpPptMuTLzk5QxLXmXajFNEFTw6HwStO8wEIztM1oiHvEz5Y/Afp5z2/Vw7rhqqAcdkBLxmxbwU7+TyRqK3k7RtLlz4muIQvEadStXYEoM9RyNUE64Chd3FrvA7rAYMln7iQEI/DKAyj3YuF30mST4jDZDFGs5gywajV3wur1Jc7TaZmZXR0giQO13v8mi8QrlM94A1URCMJ3Qk/uvMvV2t/YW+8mnbbP0rfEPa7+MLtH9gbagsUYeErhOd5AnMsBvJ5AUdCGyaLFSN1UWn/pgQ06uc4GeaoWsP1kSqw0GE9YCjkE+OQhNciH93LrSmTYbIY83WYMsGoVYpELS31So0mnPbv1bt/yLjOtBuliHZzjouA7fZ0xmb+feyI4Y9oe6SEnX2sX8/bPi6huxyXXph4OPXBpwdXf7k6xlJdEaEM1y0L+EJYemjkSuXc2KQH6be7se79ueBkTpHzwXyrQqsPGAr5OoLDnQpMFmO+DlMmGLUKdzTQgyGJsF9zU12HcZ1hN1IRjcliBXlvXYSFrItZGNM/a2Hi8DGgTeoFFV+tXXRyflqkKkx3T8qMuYm6qHDIePAJKP/io7dMZRcjlZExr0jnEnFGkxHis1qNWjU9PDqHfnh432Gz/ZG02QIVFA21PiAloHCbrD0WKo7fJuP3dDFlglErBg64dsPpEz2YmESh4jrDbqQimpbZUCh0MmCfiUzNeDx13F2gwKXglTOQPu0nwNrMD0cNGgYxWSPJlEPen6gEyJj3K6jY8eXvLZeLFCzretntSbWEwoPJbSznT1gzmbz6RsUPSpYrjPS58L7NdmIWacPoNZzyHthGcovFBvk8kaQekNcCYid/esAf/C8l3Yz2wOA42Su3J8+K0Cg39X7gCVBXFQJgVSvCHohPRdZw921mEj6Ygf5YS+YYEpemwvkX5trlSnU6WQPWnd8jGx4eHb9RE5auZom3ZZytjFyh08T0mJyg1XG/fmM1GZmmum/qXYzJplBGKmTAgM1SYTc3N9w3dCpLF5KjPjj2mylZfd7r1ycRqgXSqzcygUq5cka0aQaSSVxccvkq7Dt3+bcnnhr7vrL747z57MvCRjA5mJo19/YFFaafYhKANRroJRXQWEtIZ+MWdCzNygPoIsBRrYeGvV8DYzbukkfFUXLlnwDn+Amy2KSMB2M0ukHEtVUC66zFbAkwjhLOtWl7KHr0mpkkUyaBXJYKNlMRVBT+uQmxQ6fya1JfPSBvQj0hmlgPKO/+OG9KY3eUtJx5YsvlJaUbPoRWQyPIIuOAddi5MNWMhQYc3E44kjAsBhrPnYKGA9s+VIZHPk/O0A3al96G4l07DM8e27M8z1C9lZWzRmCZCkK+88Qb1nEHuY/nsA37YF/EINYTC0jUB5SqEei3PmC33XxGok3rjpLmtxd/flb2bmvrW7fNnAtMSyOZSO14Fbe7Lje5lWPiTg21B7aBXKVaK1NpCoHlyFHbAPZn33T9KzG2quS3j3yy5LHHh98TlTxM6cLC5wy3ly5TRIJcowBD+RfOj/9+esd7nziWXW2EY07G+yJ1Xz0ggJQmUKwH7PaAN6E9MTIRsnvqIE6riOyXGJGYkZWNmjwy81ro3jhrxws7rJz8GNeBhJg9J9xDSMVsIeQTRjwsIZKtzgAHNu93vH7hfGmpSmEFp9PEJafJgffxHLZhH+yLGBBsgbn1gNT7ovaPP3hDbaDnnNNJyGiR1gN2281hU3pHwsS0yORkjfPtuyeOfJiJiQVTTSklm8tBQk2tjn6wMpZEBFgvtr4cEsdMhLDBoxIr/vXXveTMIEzx4Vg5I8iDPgC/ewI00Yk6tdFE/KcslkyTHL/sWJyInMvoq1Ov+JNB8+c1AEWXAY62VW7zqwf0rRHoqwfs9oA3oT2+pQylvrGT+8U9DGNng8liAauhhu6L4+/yyXQxQEILLlmNsjRTE0BFAYQlpQKZXhPJWbp39uv5AB+9A/Dko6B2srrJkfFjeqq1yYQkPaCp+rITD7yP57AN+2BfxCDWk457d/HK/LJ6qvXTkfDGZneAxcrVCMRbPPActmEf7Ev1gN0EvDnN5HDBL7eU1fzv2eZv2ILDINfFgiw8FhjycWrTB4PVwQJTdRlkvQbT9R/EJ4NLGwtV/1lpIfTED/4cjvPWyyRAJsu0pARI6ZEYkasN76O1m2ohf//emvf/XLIWD7yP57AN+2BfxLz1suAF8XrAC3roH6MkHZSglrNktmXogffxHLZJ1wfstg7ZjVBHMy62edHWy4vMrV+uXJw7drI2dSCZL00gNzZB6cmjrrPl9ed+Fh45TJZ1OzhbGqDuzHFoLS9ZJVMqn+PHK6twLwQB1Ep1i9pS/N+WndsNez78pPGTcAUcxLYt31ZtWfzIlkemz4ibarO0qMmyUo0voIkE2sOHcvjr93vB3RaS3SB1NF7tf+l33zb80gbfLX8uF3Ihawprzd9y4Zktxa8eqbaesjI7P1sgU4ypb7VC/ZkjW+UqzUrcv+ft/oWeu2VapeWxIRklg04WwemSSii+8zau4fhZ+O9f/rfx3DcHG4dfKIMiqxPKeFCJdwGyDv5ecLd1yG6QOhpJeOV/vq193Ow4/qdfGh2x4S31G/brLRvpWnFH9cNNlk1v3De6f6E6Ivpt4pLMwp2v0jZni97oXEEpFJJWGr7mFbY9CRKytBLK+DYp69jvBXdbxwl4g9TRhFCMO7H8C885T80CwFTHQ/6ea/HixfQXqpzkOd3XlTjdAhKVUqmkekDSdgyoHpB1cuonOZXh4fUnvHW8PmC3ddiCUUeHMg5vwnE6Y/+e13XixU3k/sjExESqB6ypqZlDzh3Fdr7P9bRuAl4nC0Yd3d5x/KmjPUHJx4X+hkGpE1Y/wIjXq5xa3mPXrNujIUSbO3r0aKoH/Prrr+cSAqLi1NYZ71t3GuZ6ecAuUC9aYIs+4Yi2yE3Ga5qggIBWrVZPz8jIkOGB9/EcLzruJmAXtcDq6NDG8VVHS3o6VuKAQjPAH+cHJiFZ72kJqbAy1F3kmEYeTyDeb1ZqamoyrvHwwPt4DtuwD/ZFDGK7p+AuYjdQHb3ovQWZoBddKGkm8UGJOwR4dV4m/HFDIV/Pb7HI6w0KDw//Ii4uTo3Bh9VqZTTEBg4cGNvQwF17jvdJgPKujZhWq1WgFzQYDPaWlha88Ol0NwG7gN1IdXQx4cmFAPGmiawIXpydCW9v8iVhZWWlMyIiIpas92KSkpLoD1objUbiee3AE1Cn0ymys7OTSD/6W861tbWwffv2JsR2e8BuAzMhWKvZfzsVVRGP+JcHM+HZzwq9yrLt3r27mEyzz5rN5oUTJkzIwd8cQRIS7+ZZ7yEho6Ki6I+Jnz59mj18+PDR0tLS1fv37y/uJmC3gYXEJiYz47ddp1ZAShgg+cBhbvmHl3c0mezEm/2LTMMlly5dWjJjxox7evXqpcRUjM39K5xIPAxAvvvuOyfpu+PQoUPLCGGPkWnZ3k3AboM0HSFhtPelm612BqpbuURxZqIC1uwrhNbK0i8vvDrzKXjSK5JlCZFshIgHCgoKLH379h2QlpY2kKwFaXKaj44xSX3x4sVS0ud10vf49YyGuwl4E5u16er6d3bCfKm2H93WDyI0cvjnEQ/5Hsn5qMCnrgv+zFdCQgKMHz9ek5iYqMbIlwQbwO8Z81W3sC03N1dz5MgRqK+vx/VjNwF/6Hb6uTtRTvAazrTC84RoZ7J7quDNXYHJR4IPGDt2LAYdaqVSOblPnz49MdDA7bmioiLqAgcNGqTEilvYRqLfyWPGjMlXq9X2Y8eOdRPwh25uUpVKecY3d8H8QORDmzZtGqZesKxbSmRkZC7xcloMQI4ePVqTn5+/FfsQbzczJyenJ7bFxsbmtra2YiGkMsR2E7DbAnlG1P2Z/JEPrampiV/nqck6T028Wsu5c+f2HDhw4BPiBakekKz9tpSXlz+SlZU1lUTIahKc8DnD6/Jauy9M/wFbXFwcfxen4IHEyw2qrq4+3djYWNy7N/djj1euXAHi+fonJycPv3r1ahEJTlBhQyNgMiV3E7DbOvDh+9buwRmRrv2EQYi4zRNCXwfudBOw226o/Z8AAwBphnYirXZBiwAAAABJRU5ErkJggg=='); - - &.@{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