refactor: tree
parent
af0620d14e
commit
3117c2748b
|
@ -0,0 +1,34 @@
|
|||
import { CSSProperties } from 'vue';
|
||||
|
||||
export default function DropIndicator({
|
||||
dropPosition,
|
||||
dropLevelOffset,
|
||||
indent,
|
||||
}: {
|
||||
dropPosition: -1 | 0 | 1;
|
||||
dropLevelOffset: number;
|
||||
indent: number;
|
||||
}) {
|
||||
const style: CSSProperties = {
|
||||
pointerEvents: 'none',
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
backgroundColor: 'red',
|
||||
height: `${2}px`,
|
||||
};
|
||||
switch (dropPosition) {
|
||||
case -1:
|
||||
style.top = 0;
|
||||
style.left = `${-dropLevelOffset * indent}px`;
|
||||
break;
|
||||
case 1:
|
||||
style.bottom = 0;
|
||||
style.left = `${-dropLevelOffset * indent}px`;
|
||||
break;
|
||||
case 0:
|
||||
style.bottom = 0;
|
||||
style.left = `${indent}`;
|
||||
break;
|
||||
}
|
||||
return <div style={style} />;
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
interface IndentProps {
|
||||
prefixCls: string;
|
||||
level: number;
|
||||
isStart: boolean[];
|
||||
isEnd: boolean[];
|
||||
}
|
||||
|
||||
const Indent = ({ prefixCls, level, isStart, isEnd }: IndentProps) => {
|
||||
const baseClassName = `${prefixCls}-indent-unit`;
|
||||
const list = [];
|
||||
for (let i = 0; i < level; i += 1) {
|
||||
list.push(
|
||||
<span
|
||||
key={i}
|
||||
class={{
|
||||
[baseClassName]: true,
|
||||
[`${baseClassName}-start`]: isStart[i],
|
||||
[`${baseClassName}-end`]: isEnd[i],
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span aria-hidden="true" class={`${prefixCls}-indent`}>
|
||||
{list}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default Indent;
|
|
@ -0,0 +1,101 @@
|
|||
import TreeNode from './TreeNode';
|
||||
import { FlattenNode } from './interface';
|
||||
import { getTreeNodeProps, TreeNodeRequiredProps } from './utils/treeUtil';
|
||||
import { useInjectTreeContext } from './contextTypes';
|
||||
import { defineComponent, onBeforeUnmount, onMounted, PropType, ref, Transition, watch } from 'vue';
|
||||
import { treeNodeProps } from './props';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'MotionTreeNode',
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
...treeNodeProps,
|
||||
active: Boolean,
|
||||
motion: Object,
|
||||
motionNodes: { type: Array as PropType<FlattenNode[]> },
|
||||
onMotionStart: Function,
|
||||
onMotionEnd: Function,
|
||||
motionType: String,
|
||||
treeNodeRequiredProps: { type: Object as PropType<TreeNodeRequiredProps> },
|
||||
},
|
||||
slots: ['title', 'icon', 'switcherIcon'],
|
||||
setup(props, { attrs, slots }) {
|
||||
const visible = ref(true);
|
||||
const context = useInjectTreeContext();
|
||||
const motionedRef = ref(false);
|
||||
const onMotionEnd = () => {
|
||||
if (!motionedRef.value) {
|
||||
props.onMotionEnd();
|
||||
}
|
||||
motionedRef.value = true;
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.motionNodes,
|
||||
() => {
|
||||
if (props.motionNodes && props.motionType === 'hide' && visible.value) {
|
||||
visible.value = false;
|
||||
}
|
||||
},
|
||||
);
|
||||
onMounted(() => {
|
||||
props.motionNodes && props.onMotionStart();
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
props.motionNodes && onMotionEnd();
|
||||
});
|
||||
return () => {
|
||||
const { motion, motionNodes, motionType, active, treeNodeRequiredProps, ...otherProps } =
|
||||
props;
|
||||
if (motionNodes) {
|
||||
return (
|
||||
<Transition
|
||||
{...motion}
|
||||
appear={motionType === 'show'}
|
||||
onAfterAppear={onMotionEnd}
|
||||
onAfterLeave={onMotionEnd}
|
||||
>
|
||||
<div v-show={visible.value} class={`${context.value.prefixCls}-treenode-motion`}>
|
||||
{motionNodes.map((treeNode: FlattenNode) => {
|
||||
const {
|
||||
data: { ...restProps },
|
||||
title,
|
||||
key,
|
||||
isStart,
|
||||
isEnd,
|
||||
} = treeNode;
|
||||
delete restProps.children;
|
||||
|
||||
const treeNodeProps = getTreeNodeProps(key, treeNodeRequiredProps);
|
||||
|
||||
return (
|
||||
<TreeNode
|
||||
v-slots={slots}
|
||||
{...restProps}
|
||||
{...treeNodeProps}
|
||||
title={title}
|
||||
active={active}
|
||||
data={treeNode.data}
|
||||
key={key}
|
||||
isStart={isStart}
|
||||
isEnd={isEnd}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<TreeNode
|
||||
v-slots={slots}
|
||||
domRef={ref}
|
||||
class={attrs.class}
|
||||
style={attrs.style}
|
||||
{...otherProps}
|
||||
active={active}
|
||||
/>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
|
@ -0,0 +1,321 @@
|
|||
/**
|
||||
* Handle virtual list of the TreeNodes.
|
||||
*/
|
||||
|
||||
import { computed, defineComponent, ref, watch } from 'vue';
|
||||
import VirtualList from '../vc-virtual-list';
|
||||
import { FlattenNode, DataEntity, DataNode, ScrollTo } from './interface';
|
||||
import MotionTreeNode from './MotionTreeNode';
|
||||
import { nodeListProps } from './props';
|
||||
import { findExpandedKeys, getExpandRange } from './utils/diffUtil';
|
||||
import { getTreeNodeProps, getKey } from './utils/treeUtil';
|
||||
|
||||
const HIDDEN_STYLE = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
display: 'flex',
|
||||
overflow: 'hidden',
|
||||
opacity: 0,
|
||||
border: 0,
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
};
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
export const MOTION_KEY = `RC_TREE_MOTION_${Math.random()}`;
|
||||
|
||||
const MotionNode: DataNode = {
|
||||
key: MOTION_KEY,
|
||||
};
|
||||
|
||||
export const MotionEntity: DataEntity = {
|
||||
key: MOTION_KEY,
|
||||
level: 0,
|
||||
index: 0,
|
||||
pos: '0',
|
||||
node: MotionNode,
|
||||
};
|
||||
|
||||
const MotionFlattenData: FlattenNode = {
|
||||
parent: null,
|
||||
children: [],
|
||||
pos: MotionEntity.pos,
|
||||
data: MotionNode,
|
||||
title: null,
|
||||
key: MOTION_KEY,
|
||||
/** Hold empty list here since we do not use it */
|
||||
isStart: [],
|
||||
isEnd: [],
|
||||
};
|
||||
|
||||
export interface NodeListRef {
|
||||
scrollTo: ScrollTo;
|
||||
getIndentWidth: () => number;
|
||||
}
|
||||
|
||||
/**
|
||||
* We only need get visible content items to play the animation.
|
||||
*/
|
||||
export function getMinimumRangeTransitionRange(
|
||||
list: FlattenNode[],
|
||||
virtual: boolean,
|
||||
height: number,
|
||||
itemHeight: number,
|
||||
) {
|
||||
if (virtual === false || !height) {
|
||||
return list;
|
||||
}
|
||||
|
||||
return list.slice(0, Math.ceil(height / itemHeight) + 1);
|
||||
}
|
||||
|
||||
function itemKey(item: FlattenNode) {
|
||||
const {
|
||||
data: { key },
|
||||
pos,
|
||||
} = item;
|
||||
return getKey(key, pos);
|
||||
}
|
||||
|
||||
function getAccessibilityPath(item: FlattenNode): string {
|
||||
let path = String(item.data.key);
|
||||
let current = item;
|
||||
|
||||
while (current.parent) {
|
||||
current = current.parent;
|
||||
path = `${current.data.key} > ${path}`;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NodeList',
|
||||
inheritAttrs: false,
|
||||
props: nodeListProps,
|
||||
setup(props, { expose, attrs, slots }) {
|
||||
// =============================== Ref ================================
|
||||
const listRef = ref(null);
|
||||
const indentMeasurerRef = ref(null);
|
||||
expose({
|
||||
scrollTo: scroll => {
|
||||
listRef.value.scrollTo(scroll);
|
||||
},
|
||||
getIndentWidth: () => indentMeasurerRef.value.offsetWidth,
|
||||
});
|
||||
|
||||
// ============================== Motion ==============================
|
||||
const transitionData = ref<FlattenNode[]>(props.data);
|
||||
const transitionRange = ref([]);
|
||||
const motionType = ref<'show' | 'hide' | null>(null);
|
||||
|
||||
function onMotionEnd() {
|
||||
transitionData.value = props.data;
|
||||
transitionRange.value = [];
|
||||
motionType.value = null;
|
||||
|
||||
props.onListChangeEnd();
|
||||
}
|
||||
watch(
|
||||
[() => ({ ...props.expandedKeys }), () => props.data],
|
||||
([expandedKeys, data], [prevExpandedKeys, prevData]) => {
|
||||
const diffExpanded = findExpandedKeys(prevExpandedKeys, expandedKeys);
|
||||
|
||||
if (diffExpanded.key !== null) {
|
||||
const { virtual, height, itemHeight } = props;
|
||||
if (diffExpanded.add) {
|
||||
const keyIndex = prevData.findIndex(({ data: { key } }) => key === diffExpanded.key);
|
||||
|
||||
const rangeNodes = getMinimumRangeTransitionRange(
|
||||
getExpandRange(prevData, data, diffExpanded.key),
|
||||
virtual,
|
||||
height,
|
||||
itemHeight,
|
||||
);
|
||||
|
||||
const newTransitionData: FlattenNode[] = prevData.slice();
|
||||
newTransitionData.splice(keyIndex + 1, 0, MotionFlattenData);
|
||||
|
||||
transitionData.value = newTransitionData;
|
||||
transitionRange.value = rangeNodes;
|
||||
motionType.value = 'show';
|
||||
} else {
|
||||
const keyIndex = data.findIndex(({ data: { key } }) => key === diffExpanded.key);
|
||||
|
||||
const rangeNodes = getMinimumRangeTransitionRange(
|
||||
getExpandRange(data, prevData, diffExpanded.key),
|
||||
virtual,
|
||||
height,
|
||||
itemHeight,
|
||||
);
|
||||
|
||||
const newTransitionData: FlattenNode[] = data.slice();
|
||||
newTransitionData.splice(keyIndex + 1, 0, MotionFlattenData);
|
||||
|
||||
transitionData.value = newTransitionData;
|
||||
transitionRange.value = rangeNodes;
|
||||
motionType.value = 'hide';
|
||||
}
|
||||
} else if (prevData !== data) {
|
||||
transitionData.value = data;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// We should clean up motion if is changed by dragging
|
||||
watch(
|
||||
() => props.dragging,
|
||||
dragging => {
|
||||
if (!dragging) {
|
||||
onMotionEnd();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const mergedData = computed(() => (props.motion ? transitionData.value : props.data));
|
||||
|
||||
return () => {
|
||||
const {
|
||||
prefixCls,
|
||||
data,
|
||||
selectable,
|
||||
checkable,
|
||||
expandedKeys,
|
||||
selectedKeys,
|
||||
checkedKeys,
|
||||
loadedKeys,
|
||||
loadingKeys,
|
||||
halfCheckedKeys,
|
||||
keyEntities,
|
||||
disabled,
|
||||
|
||||
dragging,
|
||||
dragOverNodeKey,
|
||||
dropPosition,
|
||||
motion,
|
||||
|
||||
height,
|
||||
itemHeight,
|
||||
virtual,
|
||||
|
||||
focusable,
|
||||
activeItem,
|
||||
focused,
|
||||
tabindex,
|
||||
|
||||
onKeydown,
|
||||
onFocus,
|
||||
onBlur,
|
||||
onActiveChange,
|
||||
|
||||
onListChangeStart,
|
||||
onListChangeEnd,
|
||||
|
||||
...domProps
|
||||
} = { ...props, ...attrs };
|
||||
|
||||
const treeNodeRequiredProps = {
|
||||
expandedKeys,
|
||||
selectedKeys,
|
||||
loadedKeys,
|
||||
loadingKeys,
|
||||
checkedKeys,
|
||||
halfCheckedKeys,
|
||||
dragOverNodeKey,
|
||||
dropPosition,
|
||||
keyEntities,
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{focused && activeItem && (
|
||||
<span style={HIDDEN_STYLE} aria-live="assertive">
|
||||
{getAccessibilityPath(activeItem)}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<input
|
||||
style={HIDDEN_STYLE}
|
||||
disabled={focusable === false || disabled}
|
||||
tabindex={focusable !== false ? tabindex : null}
|
||||
onKeydown={onKeydown}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
value=""
|
||||
onChange={noop}
|
||||
aria-label="for screen reader"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class={`${prefixCls}-treenode`}
|
||||
aria-hidden
|
||||
style={{
|
||||
position: 'absolute',
|
||||
pointerEvents: 'none',
|
||||
visibility: 'hidden',
|
||||
height: 0,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div class={`${prefixCls}-indent`}>
|
||||
<div ref={indentMeasurerRef} class={`${prefixCls}-indent-unit`} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VirtualList
|
||||
{...domProps}
|
||||
data={mergedData.value}
|
||||
itemKey={itemKey as any}
|
||||
height={height}
|
||||
fullHeight={false}
|
||||
virtual={virtual}
|
||||
itemHeight={itemHeight}
|
||||
prefixCls={`${prefixCls}-list`}
|
||||
ref={listRef}
|
||||
>
|
||||
{(treeNode: FlattenNode) => {
|
||||
const {
|
||||
pos,
|
||||
data: { ...restProps },
|
||||
title,
|
||||
key,
|
||||
isStart,
|
||||
isEnd,
|
||||
} = treeNode;
|
||||
const mergedKey = getKey(key, pos);
|
||||
delete restProps.key;
|
||||
delete restProps.children;
|
||||
|
||||
const treeNodeProps = getTreeNodeProps(mergedKey, treeNodeRequiredProps);
|
||||
|
||||
return (
|
||||
<MotionTreeNode
|
||||
{...restProps}
|
||||
{...treeNodeProps}
|
||||
title={title}
|
||||
active={!!activeItem && key === activeItem.data.key}
|
||||
pos={pos}
|
||||
data={treeNode.data}
|
||||
isStart={isStart}
|
||||
isEnd={isEnd}
|
||||
motion={motion}
|
||||
motionNodes={key === MOTION_KEY ? transitionRange.value : null}
|
||||
motionType={motionType.value}
|
||||
onMotionStart={onListChangeStart}
|
||||
onMotionEnd={onMotionEnd}
|
||||
treeNodeRequiredProps={treeNodeRequiredProps}
|
||||
onMousemove={() => {
|
||||
onActiveChange(null);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</VirtualList>
|
||||
</>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,465 @@
|
|||
import { useInjectTreeContext } from './contextTypes';
|
||||
import { getDataAndAria } from './util';
|
||||
import Indent from './Indent';
|
||||
import { convertNodePropsToEventData } from './utils/treeUtil';
|
||||
import { computed, defineComponent, getCurrentInstance, onMounted, onUpdated, ref } from 'vue';
|
||||
import { treeNodeProps } from './props';
|
||||
import classNames from '../_util/classNames';
|
||||
|
||||
const ICON_OPEN = 'open';
|
||||
const ICON_CLOSE = 'close';
|
||||
|
||||
const defaultTitle = '---';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TreeNode',
|
||||
inheritAttrs: false,
|
||||
props: treeNodeProps,
|
||||
isTreeNode: 1,
|
||||
slots: ['title', 'icon', 'switcherIcon'],
|
||||
setup(props, { attrs, expose, slots }) {
|
||||
const dragNodeHighlight = ref(false);
|
||||
const context = useInjectTreeContext();
|
||||
const instance = getCurrentInstance();
|
||||
const selectHandle = ref();
|
||||
|
||||
const hasChildren = computed(() => {
|
||||
const { eventKey } = props;
|
||||
const { keyEntities } = context.value;
|
||||
const { children } = keyEntities[eventKey] || {};
|
||||
|
||||
return !!(children || []).length;
|
||||
});
|
||||
|
||||
const isLeaf = computed(() => {
|
||||
const { isLeaf, loaded } = props;
|
||||
const { loadData } = context.value;
|
||||
|
||||
const has = hasChildren.value;
|
||||
|
||||
if (isLeaf === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isLeaf || (!loadData && !has) || (loadData && loaded && !has);
|
||||
});
|
||||
const nodeState = computed(() => {
|
||||
const { expanded } = props;
|
||||
|
||||
if (isLeaf.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return expanded ? ICON_OPEN : ICON_CLOSE;
|
||||
});
|
||||
|
||||
const isDisabled = computed(() => {
|
||||
const { disabled } = props;
|
||||
const { disabled: treeDisabled } = context.value;
|
||||
|
||||
return !!(treeDisabled || disabled);
|
||||
});
|
||||
|
||||
const isCheckable = computed(() => {
|
||||
const { checkable } = props;
|
||||
const { checkable: treeCheckable } = context.value;
|
||||
|
||||
// Return false if tree or treeNode is not checkable
|
||||
if (!treeCheckable || checkable === false) return false;
|
||||
return treeCheckable;
|
||||
});
|
||||
|
||||
const isSelectable = computed(() => {
|
||||
const { selectable } = props;
|
||||
const { selectable: treeSelectable } = context.value;
|
||||
|
||||
// Ignore when selectable is undefined or null
|
||||
if (typeof selectable === 'boolean') {
|
||||
return selectable;
|
||||
}
|
||||
|
||||
return treeSelectable;
|
||||
});
|
||||
|
||||
const onSelectorDoubleClick = (e: MouseEvent) => {
|
||||
const { onNodeDoubleClick } = context.value;
|
||||
onNodeDoubleClick(e, convertNodePropsToEventData(props));
|
||||
};
|
||||
|
||||
const onSelect = (e: MouseEvent) => {
|
||||
if (isDisabled.value) return;
|
||||
|
||||
const { onNodeSelect } = context.value;
|
||||
e.preventDefault();
|
||||
onNodeSelect(e, convertNodePropsToEventData(props));
|
||||
};
|
||||
|
||||
const onCheck = (e: MouseEvent) => {
|
||||
if (isDisabled.value) return;
|
||||
|
||||
const { disableCheckbox, checked } = props;
|
||||
const { onNodeCheck } = context.value;
|
||||
|
||||
if (!isCheckable.value || disableCheckbox) return;
|
||||
|
||||
e.preventDefault();
|
||||
const targetChecked = !checked;
|
||||
onNodeCheck(e, convertNodePropsToEventData(props), targetChecked);
|
||||
};
|
||||
|
||||
const onSelectorClick = (e: MouseEvent) => {
|
||||
// Click trigger before select/check operation
|
||||
const { onNodeClick } = context.value;
|
||||
onNodeClick(e, convertNodePropsToEventData(props));
|
||||
|
||||
if (isSelectable.value) {
|
||||
onSelect(e);
|
||||
} else {
|
||||
onCheck(e);
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseEnter = (e: MouseEvent) => {
|
||||
const { onNodeMouseEnter } = context.value;
|
||||
onNodeMouseEnter(e, convertNodePropsToEventData(props));
|
||||
};
|
||||
|
||||
const onMouseLeave = (e: MouseEvent) => {
|
||||
const { onNodeMouseLeave } = context.value;
|
||||
onNodeMouseLeave(e, convertNodePropsToEventData(props));
|
||||
};
|
||||
|
||||
const onContextmenu = (e: MouseEvent) => {
|
||||
const { onNodeContextMenu } = context.value;
|
||||
onNodeContextMenu(e, convertNodePropsToEventData(props));
|
||||
};
|
||||
|
||||
const onDragStart = (e: DragEvent) => {
|
||||
const { onNodeDragStart } = context.value;
|
||||
|
||||
e.stopPropagation();
|
||||
dragNodeHighlight.value = true;
|
||||
onNodeDragStart(e, instance.vnode);
|
||||
|
||||
try {
|
||||
// ie throw error
|
||||
// firefox-need-it
|
||||
e.dataTransfer.setData('text/plain', '');
|
||||
} catch (error) {
|
||||
// empty
|
||||
}
|
||||
};
|
||||
|
||||
const onDragEnter = (e: DragEvent) => {
|
||||
const { onNodeDragEnter } = context.value;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onNodeDragEnter(e, instance.vnode);
|
||||
};
|
||||
|
||||
const onDragOver = (e: DragEvent) => {
|
||||
const { onNodeDragOver } = context.value;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onNodeDragOver(e, instance.vnode);
|
||||
};
|
||||
|
||||
const onDragLeave = (e: DragEvent) => {
|
||||
const { onNodeDragLeave } = context.value;
|
||||
|
||||
e.stopPropagation();
|
||||
onNodeDragLeave(e, instance.vnode);
|
||||
};
|
||||
|
||||
const onDragEnd = (e: DragEvent) => {
|
||||
const { onNodeDragEnd } = context.value;
|
||||
|
||||
e.stopPropagation();
|
||||
dragNodeHighlight.value = false;
|
||||
onNodeDragEnd(e, instance.vnode);
|
||||
};
|
||||
|
||||
const onDrop = (e: DragEvent) => {
|
||||
const { onNodeDrop } = context.value;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dragNodeHighlight.value = false;
|
||||
onNodeDrop(e, instance.vnode);
|
||||
};
|
||||
|
||||
// Disabled item still can be switch
|
||||
const onExpand = e => {
|
||||
const { onNodeExpand } = context.value;
|
||||
if (props.loading) return;
|
||||
onNodeExpand(e, convertNodePropsToEventData(props));
|
||||
};
|
||||
|
||||
const renderSwitcherIconDom = (isLeaf: boolean) => {
|
||||
const { switcherIcon: switcherIconFromProps = slots.switcherIcon } = props;
|
||||
const { switcherIcon: switcherIconFromCtx } = context.value;
|
||||
|
||||
const switcherIcon = switcherIconFromProps || switcherIconFromCtx;
|
||||
// if switcherIconDom is null, no render switcher span
|
||||
if (typeof switcherIcon === 'function') {
|
||||
return switcherIcon({ ...props, isLeaf });
|
||||
}
|
||||
return switcherIcon;
|
||||
};
|
||||
|
||||
// Load data to avoid default expanded tree without data
|
||||
const syncLoadData = () => {
|
||||
const { expanded, loading, loaded } = props;
|
||||
const { loadData, onNodeLoad } = context.value;
|
||||
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
// read from state to avoid loadData at same time
|
||||
if (loadData && expanded && !isLeaf.value) {
|
||||
// We needn't reload data when has children in sync logic
|
||||
// It's only needed in node expanded
|
||||
if (!hasChildren.value && !loaded) {
|
||||
onNodeLoad(convertNodePropsToEventData(props));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
syncLoadData();
|
||||
});
|
||||
onUpdated(() => {
|
||||
syncLoadData();
|
||||
});
|
||||
|
||||
// Switcher
|
||||
const renderSwitcher = () => {
|
||||
const { expanded } = props;
|
||||
const { prefixCls } = context.value;
|
||||
|
||||
if (isLeaf.value) {
|
||||
// if switcherIconDom is null, no render switcher span
|
||||
const switcherIconDom = renderSwitcherIconDom(true);
|
||||
|
||||
return switcherIconDom !== false ? (
|
||||
<span class={classNames(`${prefixCls}-switcher`, `${prefixCls}-switcher-noop`)}>
|
||||
{switcherIconDom}
|
||||
</span>
|
||||
) : null;
|
||||
}
|
||||
|
||||
const switcherCls = classNames(
|
||||
`${prefixCls}-switcher`,
|
||||
`${prefixCls}-switcher_${expanded ? ICON_OPEN : ICON_CLOSE}`,
|
||||
);
|
||||
|
||||
const switcherIconDom = renderSwitcherIconDom(false);
|
||||
|
||||
return switcherIconDom !== false ? (
|
||||
<span onClick={onExpand} class={switcherCls}>
|
||||
{switcherIconDom}
|
||||
</span>
|
||||
) : null;
|
||||
};
|
||||
|
||||
// Checkbox
|
||||
const renderCheckbox = () => {
|
||||
const { checked, halfChecked, disableCheckbox } = props;
|
||||
const { prefixCls } = context.value;
|
||||
|
||||
const disabled = isDisabled.value;
|
||||
const checkable = isCheckable.value;
|
||||
|
||||
if (!checkable) return null;
|
||||
|
||||
// [Legacy] Custom element should be separate with `checkable` in future
|
||||
const $custom = typeof checkable !== 'boolean' ? checkable : null;
|
||||
|
||||
return (
|
||||
<span
|
||||
class={classNames(
|
||||
`${prefixCls}-checkbox`,
|
||||
checked && `${prefixCls}-checkbox-checked`,
|
||||
!checked && halfChecked && `${prefixCls}-checkbox-indeterminate`,
|
||||
(disabled || disableCheckbox) && `${prefixCls}-checkbox-disabled`,
|
||||
)}
|
||||
onClick={onCheck}
|
||||
>
|
||||
{$custom}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const renderIcon = () => {
|
||||
const { loading } = props;
|
||||
const { prefixCls } = context.value;
|
||||
|
||||
return (
|
||||
<span
|
||||
class={classNames(
|
||||
`${prefixCls}-iconEle`,
|
||||
`${prefixCls}-icon__${nodeState.value || 'docu'}`,
|
||||
loading && `${prefixCls}-icon_loading`,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderDropIndicator = () => {
|
||||
const { disabled, eventKey } = props;
|
||||
const {
|
||||
draggable,
|
||||
dropLevelOffset,
|
||||
dropPosition,
|
||||
prefixCls,
|
||||
indent,
|
||||
dropIndicatorRender,
|
||||
dragOverNodeKey,
|
||||
direction,
|
||||
} = context.value;
|
||||
const mergedDraggable = draggable !== false;
|
||||
// allowDrop is calculated in Tree.tsx, there is no need for calc it here
|
||||
const showIndicator = !disabled && mergedDraggable && dragOverNodeKey === eventKey;
|
||||
return showIndicator
|
||||
? dropIndicatorRender({ dropPosition, dropLevelOffset, indent, prefixCls, direction })
|
||||
: null;
|
||||
};
|
||||
|
||||
// Icon + Title
|
||||
const renderSelector = () => {
|
||||
const { title = slots.title, selected, icon = slots.icon, loading, data } = props;
|
||||
const {
|
||||
prefixCls,
|
||||
showIcon,
|
||||
icon: treeIcon,
|
||||
draggable,
|
||||
loadData,
|
||||
titleRender,
|
||||
} = context.value;
|
||||
const disabled = isDisabled.value;
|
||||
const mergedDraggable = typeof draggable === 'function' ? draggable(data) : draggable;
|
||||
|
||||
const wrapClass = `${prefixCls}-node-content-wrapper`;
|
||||
|
||||
// Icon - Still show loading icon when loading without showIcon
|
||||
let $icon;
|
||||
|
||||
if (showIcon) {
|
||||
const currentIcon = icon || treeIcon;
|
||||
|
||||
$icon = currentIcon ? (
|
||||
<span class={classNames(`${prefixCls}-iconEle`, `${prefixCls}-icon__customize`)}>
|
||||
{typeof currentIcon === 'function' ? currentIcon(props) : currentIcon}
|
||||
</span>
|
||||
) : (
|
||||
renderIcon()
|
||||
);
|
||||
} else if (loadData && loading) {
|
||||
$icon = renderIcon();
|
||||
}
|
||||
|
||||
// Title
|
||||
let titleNode: any;
|
||||
if (typeof title === 'function') {
|
||||
titleNode = title(data);
|
||||
} else if (titleRender) {
|
||||
titleNode = titleRender(data);
|
||||
} else {
|
||||
titleNode = title === undefined ? defaultTitle : title;
|
||||
}
|
||||
|
||||
const $title = <span class={`${prefixCls}-title`}>{titleNode}</span>;
|
||||
|
||||
return (
|
||||
<span
|
||||
ref={selectHandle}
|
||||
title={typeof title === 'string' ? title : ''}
|
||||
class={classNames(
|
||||
`${wrapClass}`,
|
||||
`${wrapClass}-${nodeState.value || 'normal'}`,
|
||||
!disabled && (selected || dragNodeHighlight) && `${prefixCls}-node-selected`,
|
||||
!disabled && mergedDraggable && 'draggable',
|
||||
)}
|
||||
draggable={(!disabled && mergedDraggable) || undefined}
|
||||
aria-grabbed={(!disabled && mergedDraggable) || undefined}
|
||||
onMouseenter={onMouseEnter}
|
||||
onMouseleave={onMouseLeave}
|
||||
onContextmenu={onContextmenu}
|
||||
onClick={onSelectorClick}
|
||||
onDblclick={onSelectorDoubleClick}
|
||||
onDragstart={mergedDraggable ? onDragStart : undefined}
|
||||
>
|
||||
{$icon}
|
||||
{$title}
|
||||
{renderDropIndicator()}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
return () => {
|
||||
const {
|
||||
eventKey,
|
||||
dragOver,
|
||||
dragOverGapTop,
|
||||
dragOverGapBottom,
|
||||
isLeaf,
|
||||
isStart,
|
||||
isEnd,
|
||||
expanded,
|
||||
selected,
|
||||
checked,
|
||||
halfChecked,
|
||||
loading,
|
||||
domRef,
|
||||
active,
|
||||
data,
|
||||
onMousemove,
|
||||
...otherProps
|
||||
} = { ...props, ...attrs };
|
||||
const { prefixCls, filterTreeNode, draggable, keyEntities, dropContainerKey, dropTargetKey } =
|
||||
context.value;
|
||||
const disabled = isDisabled.value;
|
||||
const dataOrAriaAttributeProps = getDataAndAria(otherProps);
|
||||
const { level } = keyEntities[eventKey] || {};
|
||||
const isEndNode = isEnd[isEnd.length - 1];
|
||||
const mergedDraggable = typeof draggable === 'function' ? draggable(data) : draggable;
|
||||
return (
|
||||
<div
|
||||
ref={domRef}
|
||||
class={classNames(attrs.class, `${prefixCls}-treenode`, {
|
||||
[`${prefixCls}-treenode-disabled`]: disabled,
|
||||
[`${prefixCls}-treenode-switcher-${expanded ? 'open' : 'close'}`]: !isLeaf,
|
||||
[`${prefixCls}-treenode-checkbox-checked`]: checked,
|
||||
[`${prefixCls}-treenode-checkbox-indeterminate`]: halfChecked,
|
||||
[`${prefixCls}-treenode-selected`]: selected,
|
||||
[`${prefixCls}-treenode-loading`]: loading,
|
||||
[`${prefixCls}-treenode-active`]: active,
|
||||
[`${prefixCls}-treenode-leaf-last`]: isEndNode,
|
||||
|
||||
'drop-target': dropTargetKey === eventKey,
|
||||
'drop-container': dropContainerKey === eventKey,
|
||||
'drag-over': !disabled && dragOver,
|
||||
'drag-over-gap-top': !disabled && dragOverGapTop,
|
||||
'drag-over-gap-bottom': !disabled && dragOverGapBottom,
|
||||
'filter-node': filterTreeNode && filterTreeNode(convertNodePropsToEventData(props)),
|
||||
})}
|
||||
style={attrs.style}
|
||||
onDragenter={mergedDraggable ? onDragEnter : undefined}
|
||||
onDragover={mergedDraggable ? onDragOver : undefined}
|
||||
onDragleave={mergedDraggable ? onDragLeave : undefined}
|
||||
onDrop={mergedDraggable ? onDrop : undefined}
|
||||
onDragend={mergedDraggable ? onDragEnd : undefined}
|
||||
onMousemove={onMousemove}
|
||||
{...dataOrAriaAttributeProps}
|
||||
>
|
||||
<Indent prefixCls={prefixCls} level={level} isStart={isStart} isEnd={isEnd} />
|
||||
{renderSwitcher()}
|
||||
{renderCheckbox()}
|
||||
{renderSelector()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
Binary file not shown.
Before Width: | Height: | Size: 11 KiB |
File diff suppressed because one or more lines are too long
Binary file not shown.
Before Width: | Height: | Size: 45 B |
Binary file not shown.
Before Width: | Height: | Size: 381 B |
|
@ -0,0 +1,104 @@
|
|||
/**
|
||||
* Webpack has bug for import loop, which is not the same behavior as ES module.
|
||||
* When util.js imports the TreeNode for tree generate will cause treeContextTypes be empty.
|
||||
*/
|
||||
|
||||
import type { ComputedRef, InjectionKey, PropType } from 'vue';
|
||||
import { inject } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import { defineComponent, provide } from 'vue';
|
||||
import type { VueNode } from '../_util/type';
|
||||
import type {
|
||||
IconType,
|
||||
Key,
|
||||
DataEntity,
|
||||
EventDataNode,
|
||||
NodeInstance,
|
||||
DataNode,
|
||||
Direction,
|
||||
} from './interface';
|
||||
|
||||
export type NodeMouseEventParams = {
|
||||
event: MouseEvent;
|
||||
node: EventDataNode;
|
||||
};
|
||||
export type NodeDragEventParams = {
|
||||
event: MouseEvent;
|
||||
node: EventDataNode;
|
||||
};
|
||||
|
||||
export type NodeMouseEventHandler = (e: MouseEvent, node: EventDataNode) => void;
|
||||
export type NodeDragEventHandler = (
|
||||
e: MouseEvent,
|
||||
node: NodeInstance,
|
||||
outsideTree?: boolean,
|
||||
) => void;
|
||||
|
||||
export interface TreeContextProps {
|
||||
prefixCls: string;
|
||||
selectable: boolean;
|
||||
showIcon: boolean;
|
||||
icon: IconType;
|
||||
switcherIcon: IconType;
|
||||
draggable: ((node: DataNode) => boolean) | boolean;
|
||||
checkable: boolean | VueNode;
|
||||
checkStrictly: boolean;
|
||||
disabled: boolean;
|
||||
keyEntities: Record<Key, DataEntity>;
|
||||
// for details see comment in Tree.state (Tree.tsx)
|
||||
dropLevelOffset?: number;
|
||||
dropContainerKey: Key | null;
|
||||
dropTargetKey: Key | null;
|
||||
dropPosition: -1 | 0 | 1 | null;
|
||||
indent: number | null;
|
||||
dropIndicatorRender: (props: {
|
||||
dropPosition: -1 | 0 | 1;
|
||||
dropLevelOffset: number;
|
||||
indent: number | null;
|
||||
prefixCls: string;
|
||||
direction: Direction;
|
||||
}) => VueNode;
|
||||
dragOverNodeKey: Key | null;
|
||||
direction: Direction;
|
||||
|
||||
loadData: (treeNode: EventDataNode) => Promise<void>;
|
||||
filterTreeNode: (treeNode: EventDataNode) => boolean;
|
||||
titleRender?: (node: DataNode) => VueNode;
|
||||
|
||||
onNodeClick: NodeMouseEventHandler;
|
||||
onNodeDoubleClick: NodeMouseEventHandler;
|
||||
onNodeExpand: NodeMouseEventHandler;
|
||||
onNodeSelect: NodeMouseEventHandler;
|
||||
onNodeCheck: (e: MouseEvent, treeNode: EventDataNode, checked: boolean) => void;
|
||||
onNodeLoad: (treeNode: EventDataNode) => void;
|
||||
onNodeMouseEnter: NodeMouseEventHandler;
|
||||
onNodeMouseLeave: NodeMouseEventHandler;
|
||||
onNodeContextMenu: NodeMouseEventHandler;
|
||||
onNodeDragStart: NodeDragEventHandler;
|
||||
onNodeDragEnter: NodeDragEventHandler;
|
||||
onNodeDragOver: NodeDragEventHandler;
|
||||
onNodeDragLeave: NodeDragEventHandler;
|
||||
onNodeDragEnd: NodeDragEventHandler;
|
||||
onNodeDrop: NodeDragEventHandler;
|
||||
}
|
||||
const TreeContextKey: InjectionKey<ComputedRef<TreeContextProps>> = Symbol('TreeContextKey');
|
||||
|
||||
export const TreeContext = defineComponent({
|
||||
props: {
|
||||
value: { type: Object as PropType<TreeContextProps> },
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
provide(
|
||||
TreeContextKey,
|
||||
computed(() => props.value),
|
||||
);
|
||||
return slots.default?.();
|
||||
},
|
||||
});
|
||||
|
||||
export const useInjectTreeContext = () => {
|
||||
return inject(
|
||||
TreeContextKey,
|
||||
computed(() => ({} as TreeContextProps)),
|
||||
);
|
||||
};
|
|
@ -1,4 +0,0 @@
|
|||
// based on rc-tree 2.1.3
|
||||
import Tree from './src';
|
||||
|
||||
export default Tree;
|
|
@ -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;
|
|
@ -0,0 +1,87 @@
|
|||
import { VNode } from 'vue';
|
||||
export type { ScrollTo } from '../vc-virtual-list/List';
|
||||
|
||||
export interface DataNode {
|
||||
checkable?: boolean;
|
||||
children?: DataNode[];
|
||||
disabled?: boolean;
|
||||
disableCheckbox?: boolean;
|
||||
icon?: IconType;
|
||||
isLeaf?: boolean;
|
||||
key: string | number;
|
||||
title?: any;
|
||||
selectable?: boolean;
|
||||
switcherIcon?: IconType;
|
||||
|
||||
/** Set style of TreeNode. This is not recommend if you don't have any force requirement */
|
||||
// className?: string;
|
||||
// style?: CSSProperties;
|
||||
}
|
||||
|
||||
export interface EventDataNode extends DataNode {
|
||||
expanded: boolean;
|
||||
selected: boolean;
|
||||
checked: boolean;
|
||||
loaded: boolean;
|
||||
loading: boolean;
|
||||
halfChecked: boolean;
|
||||
dragOver: boolean;
|
||||
dragOverGapTop: boolean;
|
||||
dragOverGapBottom: boolean;
|
||||
pos: string;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
export type IconType = any;
|
||||
|
||||
export type Key = string | number;
|
||||
|
||||
export type NodeElement = VNode & {
|
||||
selectHandle?: HTMLSpanElement;
|
||||
type: {
|
||||
isTreeNode: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type NodeInstance = VNode & {
|
||||
selectHandle?: HTMLSpanElement;
|
||||
};
|
||||
|
||||
export interface Entity {
|
||||
node: NodeElement;
|
||||
index: number;
|
||||
key: Key;
|
||||
pos: string;
|
||||
parent?: Entity;
|
||||
children?: Entity[];
|
||||
}
|
||||
|
||||
export interface DataEntity extends Omit<Entity, 'node' | 'parent' | 'children'> {
|
||||
node: DataNode;
|
||||
parent?: DataEntity;
|
||||
children?: DataEntity[];
|
||||
level: number;
|
||||
}
|
||||
|
||||
export interface FlattenNode {
|
||||
parent: FlattenNode | null;
|
||||
children: FlattenNode[];
|
||||
pos: string;
|
||||
data: DataNode;
|
||||
title: any;
|
||||
key: Key;
|
||||
isStart: boolean[];
|
||||
isEnd: boolean[];
|
||||
}
|
||||
|
||||
export type GetKey<RecordType> = (record: RecordType, index?: number) => Key;
|
||||
|
||||
export type GetCheckDisabled<RecordType> = (record: RecordType) => boolean;
|
||||
|
||||
export type Direction = 'ltr' | 'rtl' | undefined;
|
||||
|
||||
export interface FieldNames {
|
||||
title?: string;
|
||||
key?: string;
|
||||
children?: string;
|
||||
}
|
|
@ -0,0 +1,233 @@
|
|||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
import PropTypes from '../_util/vue-types';
|
||||
import type {
|
||||
NodeDragEventParams,
|
||||
NodeMouseEventHandler,
|
||||
NodeMouseEventParams,
|
||||
} from './contextTypes';
|
||||
import type { DataNode, Key, FlattenNode, DataEntity, EventDataNode, Direction } from './interface';
|
||||
import { fillFieldNames } from './utils/treeUtil';
|
||||
|
||||
export interface CheckInfo {
|
||||
event: 'check';
|
||||
node: EventDataNode;
|
||||
checked: boolean;
|
||||
nativeEvent: MouseEvent;
|
||||
checkedNodes: DataNode[];
|
||||
checkedNodesPositions?: { node: DataNode; pos: string }[];
|
||||
halfCheckedKeys?: Key[];
|
||||
}
|
||||
|
||||
export const treeNodeProps = {
|
||||
eventKey: [String, Number], // Pass by parent `cloneElement`
|
||||
prefixCls: String,
|
||||
|
||||
// By parent
|
||||
expanded: { type: Boolean, default: undefined },
|
||||
selected: { type: Boolean, default: undefined },
|
||||
checked: { type: Boolean, default: undefined },
|
||||
loaded: { type: Boolean, default: undefined },
|
||||
loading: { type: Boolean, default: undefined },
|
||||
halfChecked: { type: Boolean, default: undefined },
|
||||
title: PropTypes.any,
|
||||
dragOver: { type: Boolean, default: undefined },
|
||||
dragOverGapTop: { type: Boolean, default: undefined },
|
||||
dragOverGapBottom: { type: Boolean, default: undefined },
|
||||
pos: String,
|
||||
// domRef: React.Ref<HTMLDivElement>,
|
||||
/** New added in Tree for easy data access */
|
||||
data: { type: Object as PropType<DataNode> },
|
||||
isStart: { type: Array as PropType<boolean[]> },
|
||||
isEnd: { type: Array as PropType<boolean[]> },
|
||||
active: { type: Boolean, default: undefined },
|
||||
onMousemove: { type: Function as PropType<EventHandlerNonNull> },
|
||||
|
||||
// By user
|
||||
isLeaf: { type: Boolean, default: undefined },
|
||||
checkable: { type: Boolean, default: undefined },
|
||||
selectable: { type: Boolean, default: undefined },
|
||||
disabled: { type: Boolean, default: undefined },
|
||||
disableCheckbox: { type: Boolean, default: undefined },
|
||||
icon: PropTypes.any,
|
||||
switcherIcon: PropTypes.any,
|
||||
domRef: { type: Function as PropType<(arg: any) => void> },
|
||||
};
|
||||
|
||||
export type TreeNodeProps = Partial<ExtractPropTypes<typeof treeNodeProps>>;
|
||||
|
||||
export const nodeListProps = {
|
||||
prefixCls: { type: String as PropType<string> },
|
||||
data: { type: Array as PropType<FlattenNode[]> },
|
||||
motion: { type: Object as PropType<any> },
|
||||
focusable: { type: Boolean as PropType<boolean> },
|
||||
activeItem: { type: Object as PropType<FlattenNode> },
|
||||
focused: { type: Boolean as PropType<boolean> },
|
||||
tabindex: { type: Number as PropType<number> },
|
||||
checkable: { type: Boolean as PropType<boolean> },
|
||||
selectable: { type: Boolean as PropType<boolean> },
|
||||
disabled: { type: Boolean as PropType<boolean> },
|
||||
|
||||
expandedKeys: { type: Array as PropType<Key[]> },
|
||||
selectedKeys: { type: Array as PropType<Key[]> },
|
||||
checkedKeys: { type: Array as PropType<Key[]> },
|
||||
loadedKeys: { type: Array as PropType<Key[]> },
|
||||
loadingKeys: { type: Array as PropType<Key[]> },
|
||||
halfCheckedKeys: { type: Array as PropType<Key[]> },
|
||||
keyEntities: { type: Object as PropType<Record<Key, DataEntity>> },
|
||||
|
||||
dragging: { type: Boolean as PropType<boolean> },
|
||||
dragOverNodeKey: { type: [String, Number] as PropType<Key> },
|
||||
dropPosition: { type: Number as PropType<number> },
|
||||
|
||||
// Virtual list
|
||||
height: { type: Number as PropType<number> },
|
||||
itemHeight: { type: Number as PropType<number> },
|
||||
virtual: { type: Boolean as PropType<boolean> },
|
||||
|
||||
onKeydown: { type: Function as PropType<EventHandlerNonNull> },
|
||||
onFocus: { type: Function as PropType<(e: FocusEvent) => void> },
|
||||
onBlur: { type: Function as PropType<(e: FocusEvent) => void> },
|
||||
onActiveChange: { type: Function as PropType<(key: Key) => void> },
|
||||
onContextmenu: { type: Function as PropType<EventHandlerNonNull> },
|
||||
|
||||
onListChangeStart: { type: Function as PropType<() => void> },
|
||||
onListChangeEnd: { type: Function as PropType<() => void> },
|
||||
};
|
||||
|
||||
export type NodeListProps = Partial<ExtractPropTypes<typeof nodeListProps>>;
|
||||
export type AllowDrop = (options: { dropNode: DataNode; dropPosition: -1 | 0 | 1 }) => boolean;
|
||||
|
||||
export const treeProps = () => ({
|
||||
prefixCls: String,
|
||||
focusable: { type: Boolean, default: undefined },
|
||||
tabindex: Number,
|
||||
children: PropTypes.VNodeChild,
|
||||
treeData: { type: Array as PropType<DataNode[]> }, // Generate treeNode by children
|
||||
fieldNames: fillFieldNames,
|
||||
showLine: { type: Boolean, default: undefined },
|
||||
showIcon: { type: Boolean, default: undefined },
|
||||
icon: PropTypes.any,
|
||||
selectable: { type: Boolean, default: undefined },
|
||||
disabled: { type: Boolean, default: undefined },
|
||||
multiple: { type: Boolean, default: undefined },
|
||||
checkable: { type: Boolean, default: undefined },
|
||||
checkStrictly: { type: Boolean, default: undefined },
|
||||
draggable: { type: [Function, Boolean] as PropType<((node: DataNode) => boolean) | boolean> },
|
||||
defaultExpandParent: { type: Boolean, default: undefined },
|
||||
autoExpandParent: { type: Boolean, default: undefined },
|
||||
defaultExpandAll: { type: Boolean, default: undefined },
|
||||
defaultExpandedKeys: { type: Array as PropType<Key[]> },
|
||||
expandedKeys: { type: Array as PropType<Key[]> },
|
||||
defaultCheckedKeys: { type: Array as PropType<Key[]> },
|
||||
checkedKeys: {
|
||||
type: [Object, Array] as PropType<Key[] | { checked: Key[]; halfChecked: Key[] }>,
|
||||
},
|
||||
defaultSelectedKeys: { type: Array as PropType<Key[]> },
|
||||
selectedKeys: { type: Array as PropType<Key[]> },
|
||||
allowDrop: { type: Function as PropType<AllowDrop> },
|
||||
titleRender: { type: Function as PropType<(node: DataNode) => any> },
|
||||
dropIndicatorRender: {
|
||||
type: Function as PropType<
|
||||
(props: {
|
||||
dropPosition: -1 | 0 | 1;
|
||||
dropLevelOffset: number;
|
||||
indent: number;
|
||||
prefixCls: string;
|
||||
direction: Direction;
|
||||
}) => any
|
||||
>,
|
||||
},
|
||||
onFocus: { type: Function as PropType<(e: FocusEvent) => void> },
|
||||
onBlur: { type: Function as PropType<(e: FocusEvent) => void> },
|
||||
onKeyDown: { type: Function as PropType<EventHandlerNonNull> },
|
||||
onContextmenu: { type: Function as PropType<EventHandlerNonNull> },
|
||||
onClick: { type: Function as PropType<NodeMouseEventHandler> },
|
||||
onDblClick: { type: Function as PropType<NodeMouseEventHandler> },
|
||||
onScroll: { type: Function as PropType<EventHandlerNonNull> },
|
||||
onExpand: {
|
||||
type: Function as PropType<
|
||||
(
|
||||
expandedKeys: Key[],
|
||||
info: {
|
||||
node: EventDataNode;
|
||||
expanded: boolean;
|
||||
nativeEvent: MouseEvent;
|
||||
},
|
||||
) => void
|
||||
>,
|
||||
},
|
||||
onCheck: {
|
||||
type: Function as PropType<
|
||||
(checked: { checked: Key[]; halfChecked: Key[] } | Key[], info: CheckInfo) => void
|
||||
>,
|
||||
},
|
||||
onSelect: {
|
||||
type: Function as PropType<
|
||||
(
|
||||
selectedKeys: Key[],
|
||||
info: {
|
||||
event: 'select';
|
||||
selected: boolean;
|
||||
node: EventDataNode;
|
||||
selectedNodes: DataNode[];
|
||||
nativeEvent: MouseEvent;
|
||||
},
|
||||
) => void
|
||||
>,
|
||||
},
|
||||
onLoad: {
|
||||
type: Function as PropType<
|
||||
(
|
||||
loadedKeys: Key[],
|
||||
info: {
|
||||
event: 'load';
|
||||
node: EventDataNode;
|
||||
},
|
||||
) => void
|
||||
>,
|
||||
},
|
||||
loadData: { type: Function as PropType<(treeNode: EventDataNode) => Promise<void>> },
|
||||
loadedKeys: { type: Array as PropType<Key[]> },
|
||||
onMouseenter: { type: Function as PropType<(info: NodeMouseEventParams) => void> },
|
||||
onMouseleave: { type: Function as PropType<(info: NodeMouseEventParams) => void> },
|
||||
onRightClick: {
|
||||
type: Function as PropType<(info: { event: MouseEvent; node: EventDataNode }) => void>,
|
||||
},
|
||||
onDragstart: { type: Function as PropType<(info: NodeDragEventParams) => void> },
|
||||
onDragenter: {
|
||||
type: Function as PropType<(info: NodeDragEventParams & { expandedKeys: Key[] }) => void>,
|
||||
},
|
||||
onDragover: { type: Function as PropType<(info: NodeDragEventParams) => void> },
|
||||
onDragleave: { type: Function as PropType<(info: NodeDragEventParams) => void> },
|
||||
onDragend: { type: Function as PropType<(info: NodeDragEventParams) => void> },
|
||||
onDrop: {
|
||||
type: Function as PropType<
|
||||
(
|
||||
info: NodeDragEventParams & {
|
||||
dragNode: EventDataNode;
|
||||
dragNodesKeys: Key[];
|
||||
dropPosition: number;
|
||||
dropToGap: boolean;
|
||||
},
|
||||
) => void
|
||||
>,
|
||||
},
|
||||
/**
|
||||
* Used for `rc-tree-select` only.
|
||||
* Do not use in your production code directly since this will be refactor.
|
||||
*/
|
||||
onActiveChange: { type: Function as PropType<(key: Key) => void> },
|
||||
filterTreeNode: { type: Function as PropType<(treeNode: EventDataNode) => boolean> },
|
||||
motion: PropTypes.any,
|
||||
switcherIcon: PropTypes.any,
|
||||
|
||||
// Virtual List
|
||||
height: Number,
|
||||
itemHeight: Number,
|
||||
virtual: { type: Boolean, default: undefined },
|
||||
|
||||
// direction for drag logic
|
||||
direction: { type: String as PropType<Direction> },
|
||||
});
|
||||
|
||||
export type TreeProps = Partial<ExtractPropTypes<ReturnType<typeof treeProps>>>;
|
|
@ -1,686 +0,0 @@
|
|||
import PropTypes, { withUndefined } from '../../_util/vue-types';
|
||||
import classNames from '../../_util/classNames';
|
||||
import warning from 'warning';
|
||||
import { hasProp, initDefaultProps, getOptionProps, getSlot } from '../../_util/props-util';
|
||||
import { cloneElement } from '../../_util/vnode';
|
||||
import BaseMixin from '../../_util/BaseMixin';
|
||||
import {
|
||||
convertTreeToEntities,
|
||||
convertDataToTree,
|
||||
getPosition,
|
||||
getDragNodesKeys,
|
||||
parseCheckedKeys,
|
||||
conductExpandParent,
|
||||
calcSelectedKeys,
|
||||
calcDropPosition,
|
||||
arrAdd,
|
||||
arrDel,
|
||||
posToArr,
|
||||
mapChildren,
|
||||
conductCheck,
|
||||
warnOnlyTreeNode,
|
||||
getDataAndAria,
|
||||
} from './util';
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
/**
|
||||
* Thought we still use `cloneElement` to pass `key`,
|
||||
* other props can pass with context for future refactor.
|
||||
*/
|
||||
|
||||
function getWatch(keys = []) {
|
||||
const watch = {};
|
||||
keys.forEach(k => {
|
||||
watch[k] = {
|
||||
handler() {
|
||||
this.needSyncKeys[k] = true;
|
||||
},
|
||||
flush: 'sync',
|
||||
};
|
||||
});
|
||||
return watch;
|
||||
}
|
||||
|
||||
const Tree = defineComponent({
|
||||
name: 'Tree',
|
||||
mixins: [BaseMixin],
|
||||
provide() {
|
||||
return {
|
||||
vcTree: this,
|
||||
};
|
||||
},
|
||||
inheritAttrs: false,
|
||||
props: initDefaultProps(
|
||||
{
|
||||
prefixCls: PropTypes.string,
|
||||
tabindex: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
children: PropTypes.any,
|
||||
treeData: PropTypes.array, // Generate treeNode by children
|
||||
showLine: PropTypes.looseBool,
|
||||
showIcon: PropTypes.looseBool,
|
||||
icon: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
|
||||
focusable: PropTypes.looseBool,
|
||||
selectable: PropTypes.looseBool,
|
||||
disabled: PropTypes.looseBool,
|
||||
multiple: PropTypes.looseBool,
|
||||
checkable: withUndefined(PropTypes.oneOfType([PropTypes.object, PropTypes.looseBool])),
|
||||
checkStrictly: PropTypes.looseBool,
|
||||
draggable: PropTypes.looseBool,
|
||||
defaultExpandParent: PropTypes.looseBool,
|
||||
autoExpandParent: PropTypes.looseBool,
|
||||
defaultExpandAll: PropTypes.looseBool,
|
||||
defaultExpandedKeys: PropTypes.array,
|
||||
expandedKeys: PropTypes.array,
|
||||
defaultCheckedKeys: PropTypes.array,
|
||||
checkedKeys: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
||||
defaultSelectedKeys: PropTypes.array,
|
||||
selectedKeys: PropTypes.array,
|
||||
// onClick: PropTypes.func,
|
||||
// onDoubleClick: PropTypes.func,
|
||||
// onExpand: PropTypes.func,
|
||||
// onCheck: PropTypes.func,
|
||||
// onSelect: PropTypes.func,
|
||||
loadData: PropTypes.func,
|
||||
loadedKeys: PropTypes.array,
|
||||
// onMouseEnter: PropTypes.func,
|
||||
// onMouseLeave: PropTypes.func,
|
||||
// onRightClick: PropTypes.func,
|
||||
// onDragStart: PropTypes.func,
|
||||
// onDragEnter: PropTypes.func,
|
||||
// onDragOver: PropTypes.func,
|
||||
// onDragLeave: PropTypes.func,
|
||||
// onDragEnd: PropTypes.func,
|
||||
// onDrop: PropTypes.func,
|
||||
filterTreeNode: PropTypes.func,
|
||||
openTransitionName: PropTypes.string,
|
||||
openAnimation: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
switcherIcon: PropTypes.any,
|
||||
__propsSymbol__: PropTypes.any,
|
||||
},
|
||||
{
|
||||
prefixCls: 'rc-tree',
|
||||
showLine: false,
|
||||
showIcon: true,
|
||||
selectable: true,
|
||||
multiple: false,
|
||||
checkable: false,
|
||||
disabled: false,
|
||||
checkStrictly: false,
|
||||
draggable: false,
|
||||
defaultExpandParent: true,
|
||||
autoExpandParent: false,
|
||||
defaultExpandAll: false,
|
||||
defaultExpandedKeys: [],
|
||||
defaultCheckedKeys: [],
|
||||
defaultSelectedKeys: [],
|
||||
},
|
||||
),
|
||||
|
||||
data() {
|
||||
warning(this.$props.__propsSymbol__, 'must pass __propsSymbol__');
|
||||
warning(this.$props.children, 'please use children prop replace slots.default');
|
||||
this.needSyncKeys = {};
|
||||
this.domTreeNodes = {};
|
||||
const state = {
|
||||
_posEntities: new Map(),
|
||||
_keyEntities: new Map(),
|
||||
_expandedKeys: [],
|
||||
_selectedKeys: [],
|
||||
_checkedKeys: [],
|
||||
_halfCheckedKeys: [],
|
||||
_loadedKeys: [],
|
||||
_loadingKeys: [],
|
||||
_treeNode: [],
|
||||
_prevProps: null,
|
||||
_dragOverNodeKey: '',
|
||||
_dropPosition: null,
|
||||
_dragNodesKeys: [],
|
||||
};
|
||||
return {
|
||||
...state,
|
||||
...this.getDerivedState(getOptionProps(this), state),
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
// watch 引用类型的改变
|
||||
...getWatch([
|
||||
'treeData',
|
||||
'children',
|
||||
'expandedKeys',
|
||||
'autoExpandParent',
|
||||
'selectedKeys',
|
||||
'checkedKeys',
|
||||
'loadedKeys',
|
||||
]),
|
||||
__propsSymbol__() {
|
||||
this.setState(this.getDerivedState(getOptionProps(this), this.$data));
|
||||
this.needSyncKeys = {};
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
getDerivedState(props, prevState) {
|
||||
const { _prevProps } = prevState;
|
||||
const newState = {
|
||||
_prevProps: { ...props },
|
||||
};
|
||||
const self = this;
|
||||
function needSync(name) {
|
||||
return (!_prevProps && name in props) || (_prevProps && self.needSyncKeys[name]);
|
||||
}
|
||||
|
||||
// ================== Tree Node ==================
|
||||
let treeNode = null;
|
||||
|
||||
// Check if `treeData` or `children` changed and save into the state.
|
||||
if (needSync('treeData')) {
|
||||
treeNode = convertDataToTree(props.treeData);
|
||||
} else if (needSync('children')) {
|
||||
treeNode = props.children;
|
||||
}
|
||||
|
||||
// Tree support filter function which will break the tree structure in the vdm.
|
||||
// We cache the treeNodes in state so that we can return the treeNode in event trigger.
|
||||
if (treeNode) {
|
||||
newState._treeNode = treeNode;
|
||||
|
||||
// Calculate the entities data for quick match
|
||||
const entitiesMap = convertTreeToEntities(treeNode);
|
||||
newState._keyEntities = entitiesMap.keyEntities;
|
||||
}
|
||||
|
||||
const keyEntities = newState._keyEntities || prevState._keyEntities;
|
||||
|
||||
// ================ expandedKeys =================
|
||||
if (needSync('expandedKeys') || (_prevProps && needSync('autoExpandParent'))) {
|
||||
newState._expandedKeys =
|
||||
props.autoExpandParent || (!_prevProps && props.defaultExpandParent)
|
||||
? conductExpandParent(props.expandedKeys, keyEntities)
|
||||
: props.expandedKeys;
|
||||
} else if (!_prevProps && props.defaultExpandAll) {
|
||||
newState._expandedKeys = [...keyEntities.keys()];
|
||||
} else if (!_prevProps && props.defaultExpandedKeys) {
|
||||
newState._expandedKeys =
|
||||
props.autoExpandParent || props.defaultExpandParent
|
||||
? conductExpandParent(props.defaultExpandedKeys, keyEntities)
|
||||
: props.defaultExpandedKeys;
|
||||
}
|
||||
|
||||
// ================ selectedKeys =================
|
||||
if (props.selectable) {
|
||||
if (needSync('selectedKeys')) {
|
||||
newState._selectedKeys = calcSelectedKeys(props.selectedKeys, props);
|
||||
} else if (!_prevProps && props.defaultSelectedKeys) {
|
||||
newState._selectedKeys = calcSelectedKeys(props.defaultSelectedKeys, props);
|
||||
}
|
||||
}
|
||||
|
||||
// ================= checkedKeys =================
|
||||
if (props.checkable) {
|
||||
let checkedKeyEntity;
|
||||
|
||||
if (needSync('checkedKeys')) {
|
||||
checkedKeyEntity = parseCheckedKeys(props.checkedKeys) || {};
|
||||
} else if (!_prevProps && props.defaultCheckedKeys) {
|
||||
checkedKeyEntity = parseCheckedKeys(props.defaultCheckedKeys) || {};
|
||||
} else if (treeNode) {
|
||||
// If treeNode changed, we also need check it
|
||||
checkedKeyEntity = parseCheckedKeys(props.checkedKeys) || {
|
||||
checkedKeys: prevState._checkedKeys,
|
||||
halfCheckedKeys: prevState._halfCheckedKeys,
|
||||
};
|
||||
}
|
||||
|
||||
if (checkedKeyEntity) {
|
||||
let { checkedKeys = [], halfCheckedKeys = [] } = checkedKeyEntity;
|
||||
|
||||
if (!props.checkStrictly) {
|
||||
const conductKeys = conductCheck(checkedKeys, true, keyEntities);
|
||||
({ checkedKeys, halfCheckedKeys } = conductKeys);
|
||||
}
|
||||
|
||||
newState._checkedKeys = checkedKeys;
|
||||
newState._halfCheckedKeys = halfCheckedKeys;
|
||||
}
|
||||
}
|
||||
// ================= loadedKeys ==================
|
||||
if (needSync('loadedKeys')) {
|
||||
newState._loadedKeys = props.loadedKeys;
|
||||
}
|
||||
|
||||
return newState;
|
||||
},
|
||||
onNodeDragStart(event, node) {
|
||||
const { _expandedKeys } = this.$data;
|
||||
const { eventKey } = node;
|
||||
const children = getSlot(node);
|
||||
this.dragNode = node;
|
||||
|
||||
this.setState({
|
||||
_dragNodesKeys: getDragNodesKeys(
|
||||
typeof children === 'function' ? children() : children,
|
||||
node,
|
||||
),
|
||||
_expandedKeys: arrDel(_expandedKeys, eventKey),
|
||||
});
|
||||
this.__emit('dragstart', { event, node });
|
||||
},
|
||||
|
||||
/**
|
||||
* [Legacy] Select handler is less small than node,
|
||||
* so that this will trigger when drag enter node or select handler.
|
||||
* This is a little tricky if customize css without padding.
|
||||
* Better for use mouse move event to refresh drag state.
|
||||
* But let's just keep it to avoid event trigger logic change.
|
||||
*/
|
||||
onNodeDragEnter(event, node) {
|
||||
const { _expandedKeys: expandedKeys } = this.$data;
|
||||
const { pos, eventKey } = node;
|
||||
|
||||
if (!this.dragNode || !node.selectHandle) return;
|
||||
|
||||
const dropPosition = calcDropPosition(event, node);
|
||||
|
||||
// Skip if drag node is self
|
||||
if (this.dragNode.eventKey === eventKey && dropPosition === 0) {
|
||||
this.setState({
|
||||
_dragOverNodeKey: '',
|
||||
_dropPosition: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Ref: https://github.com/react-component/tree/issues/132
|
||||
// Add timeout to let onDragLevel fire before onDragEnter,
|
||||
// so that we can clean drag props for onDragLeave node.
|
||||
// Macro task for this:
|
||||
// https://html.spec.whatwg.org/multipage/webappapis.html#clean-up-after-running-script
|
||||
setTimeout(() => {
|
||||
// Update drag over node
|
||||
this.setState({
|
||||
_dragOverNodeKey: eventKey,
|
||||
_dropPosition: dropPosition,
|
||||
});
|
||||
|
||||
// Side effect for delay drag
|
||||
if (!this.delayedDragEnterLogic) {
|
||||
this.delayedDragEnterLogic = {};
|
||||
}
|
||||
Object.keys(this.delayedDragEnterLogic).forEach(key => {
|
||||
clearTimeout(this.delayedDragEnterLogic[key]);
|
||||
});
|
||||
this.delayedDragEnterLogic[pos] = setTimeout(() => {
|
||||
const newExpandedKeys = arrAdd(expandedKeys, eventKey);
|
||||
if (!hasProp(this, 'expandedKeys')) {
|
||||
this.setState({
|
||||
_expandedKeys: newExpandedKeys,
|
||||
});
|
||||
}
|
||||
this.__emit('dragenter', { event, node, expandedKeys: newExpandedKeys });
|
||||
}, 400);
|
||||
}, 0);
|
||||
},
|
||||
onNodeDragOver(event, node) {
|
||||
const { eventKey } = node;
|
||||
const { _dragOverNodeKey, _dropPosition } = this.$data;
|
||||
// Update drag position
|
||||
if (this.dragNode && eventKey === _dragOverNodeKey && node.selectHandle) {
|
||||
const dropPosition = calcDropPosition(event, node);
|
||||
|
||||
if (dropPosition === _dropPosition) return;
|
||||
|
||||
this.setState({
|
||||
_dropPosition: dropPosition,
|
||||
});
|
||||
}
|
||||
this.__emit('dragover', { event, node });
|
||||
},
|
||||
onNodeDragLeave(event, node) {
|
||||
this.setState({
|
||||
_dragOverNodeKey: '',
|
||||
});
|
||||
this.__emit('dragleave', { event, node });
|
||||
},
|
||||
onNodeDragEnd(event, node) {
|
||||
this.setState({
|
||||
_dragOverNodeKey: '',
|
||||
});
|
||||
this.__emit('dragend', { event, node });
|
||||
this.dragNode = null;
|
||||
},
|
||||
onNodeDrop(event, node) {
|
||||
const { _dragNodesKeys = [], _dropPosition } = this.$data;
|
||||
|
||||
const { eventKey, pos } = node;
|
||||
|
||||
this.setState({
|
||||
_dragOverNodeKey: '',
|
||||
});
|
||||
|
||||
if (_dragNodesKeys.indexOf(eventKey) !== -1) {
|
||||
warning(false, "Can not drop to dragNode(include it's children node)");
|
||||
return;
|
||||
}
|
||||
|
||||
const posArr = posToArr(pos);
|
||||
|
||||
const dropResult = {
|
||||
event,
|
||||
node,
|
||||
dragNode: this.dragNode,
|
||||
dragNodesKeys: _dragNodesKeys.slice(),
|
||||
dropPosition: _dropPosition + Number(posArr[posArr.length - 1]),
|
||||
dropToGap: false,
|
||||
};
|
||||
|
||||
if (_dropPosition !== 0) {
|
||||
dropResult.dropToGap = true;
|
||||
}
|
||||
this.__emit('drop', dropResult);
|
||||
this.dragNode = null;
|
||||
},
|
||||
|
||||
onNodeClick(e, treeNode) {
|
||||
this.__emit('click', e, treeNode);
|
||||
},
|
||||
|
||||
onNodeDoubleClick(e, treeNode) {
|
||||
this.__emit('dblclick', e, treeNode);
|
||||
},
|
||||
|
||||
onNodeSelect(e, treeNode) {
|
||||
let { _selectedKeys: selectedKeys } = this.$data;
|
||||
const { _keyEntities: keyEntities } = this.$data;
|
||||
const { multiple } = this.$props;
|
||||
const { selected, eventKey } = getOptionProps(treeNode);
|
||||
const targetSelected = !selected;
|
||||
// Update selected keys
|
||||
if (!targetSelected) {
|
||||
selectedKeys = arrDel(selectedKeys, eventKey);
|
||||
} else if (!multiple) {
|
||||
selectedKeys = [eventKey];
|
||||
} else {
|
||||
selectedKeys = arrAdd(selectedKeys, eventKey);
|
||||
}
|
||||
|
||||
// [Legacy] Not found related usage in doc or upper libs
|
||||
const selectedNodes = selectedKeys
|
||||
.map(key => {
|
||||
const entity = keyEntities.get(key);
|
||||
if (!entity) return null;
|
||||
|
||||
return entity.node;
|
||||
})
|
||||
.filter(node => node);
|
||||
|
||||
this.setUncontrolledState({ _selectedKeys: selectedKeys });
|
||||
|
||||
const eventObj = {
|
||||
event: 'select',
|
||||
selected: targetSelected,
|
||||
node: treeNode,
|
||||
selectedNodes,
|
||||
nativeEvent: e,
|
||||
};
|
||||
this.__emit('select', selectedKeys, eventObj);
|
||||
},
|
||||
onNodeCheck(e, treeNode, checked) {
|
||||
const {
|
||||
_keyEntities: keyEntities,
|
||||
_checkedKeys: oriCheckedKeys,
|
||||
_halfCheckedKeys: oriHalfCheckedKeys,
|
||||
} = this.$data;
|
||||
const { checkStrictly } = this.$props;
|
||||
const { eventKey } = getOptionProps(treeNode);
|
||||
|
||||
// Prepare trigger arguments
|
||||
let checkedObj;
|
||||
const eventObj = {
|
||||
event: 'check',
|
||||
node: treeNode,
|
||||
checked,
|
||||
nativeEvent: e,
|
||||
};
|
||||
|
||||
if (checkStrictly) {
|
||||
const checkedKeys = checked
|
||||
? arrAdd(oriCheckedKeys, eventKey)
|
||||
: arrDel(oriCheckedKeys, eventKey);
|
||||
const halfCheckedKeys = arrDel(oriHalfCheckedKeys, eventKey);
|
||||
checkedObj = { checked: checkedKeys, halfChecked: halfCheckedKeys };
|
||||
|
||||
eventObj.checkedNodes = checkedKeys
|
||||
.map(key => keyEntities.get(key))
|
||||
.filter(entity => entity)
|
||||
.map(entity => entity.node);
|
||||
|
||||
this.setUncontrolledState({ _checkedKeys: checkedKeys });
|
||||
} else {
|
||||
const { checkedKeys, halfCheckedKeys } = conductCheck([eventKey], checked, keyEntities, {
|
||||
checkedKeys: oriCheckedKeys,
|
||||
halfCheckedKeys: oriHalfCheckedKeys,
|
||||
});
|
||||
|
||||
checkedObj = checkedKeys;
|
||||
|
||||
// [Legacy] This is used for `rc-tree-select`
|
||||
eventObj.checkedNodes = [];
|
||||
eventObj.checkedNodesPositions = [];
|
||||
eventObj.halfCheckedKeys = halfCheckedKeys;
|
||||
|
||||
checkedKeys.forEach(key => {
|
||||
const entity = keyEntities.get(key);
|
||||
if (!entity) return;
|
||||
|
||||
const { node, pos } = entity;
|
||||
|
||||
eventObj.checkedNodes.push(node);
|
||||
eventObj.checkedNodesPositions.push({ node, pos });
|
||||
});
|
||||
|
||||
this.setUncontrolledState({
|
||||
_checkedKeys: checkedKeys,
|
||||
_halfCheckedKeys: halfCheckedKeys,
|
||||
});
|
||||
}
|
||||
this.__emit('check', checkedObj, eventObj);
|
||||
},
|
||||
onNodeLoad(treeNode) {
|
||||
return new Promise(resolve => {
|
||||
// We need to get the latest state of loading/loaded keys
|
||||
this.setState(({ _loadedKeys: loadedKeys = [], _loadingKeys: loadingKeys = [] }) => {
|
||||
const { loadData } = this.$props;
|
||||
const { eventKey } = getOptionProps(treeNode);
|
||||
|
||||
if (
|
||||
!loadData ||
|
||||
loadedKeys.indexOf(eventKey) !== -1 ||
|
||||
loadingKeys.indexOf(eventKey) !== -1
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Process load data
|
||||
const promise = loadData(treeNode);
|
||||
promise.then(() => {
|
||||
const { _loadedKeys: currentLoadedKeys, _loadingKeys: currentLoadingKeys } = this.$data;
|
||||
const newLoadedKeys = arrAdd(currentLoadedKeys, eventKey);
|
||||
const newLoadingKeys = arrDel(currentLoadingKeys, eventKey);
|
||||
|
||||
// onLoad should trigger before internal setState to avoid `loadData` trigger twice.
|
||||
// https://github.com/ant-design/ant-design/issues/12464
|
||||
this.__emit('load', newLoadedKeys, {
|
||||
event: 'load',
|
||||
node: treeNode,
|
||||
});
|
||||
this.setUncontrolledState({
|
||||
_loadedKeys: newLoadedKeys,
|
||||
});
|
||||
this.setState({
|
||||
_loadingKeys: newLoadingKeys,
|
||||
});
|
||||
resolve();
|
||||
});
|
||||
|
||||
return {
|
||||
_loadingKeys: arrAdd(loadingKeys, eventKey),
|
||||
};
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
onNodeExpand(e, treeNode) {
|
||||
let { _expandedKeys: expandedKeys } = this.$data;
|
||||
const { loadData } = this.$props;
|
||||
const { eventKey, expanded } = getOptionProps(treeNode);
|
||||
|
||||
// Update selected keys
|
||||
const index = expandedKeys.indexOf(eventKey);
|
||||
const targetExpanded = !expanded;
|
||||
|
||||
warning(
|
||||
(expanded && index !== -1) || (!expanded && index === -1),
|
||||
'Expand state not sync with index check',
|
||||
);
|
||||
|
||||
if (targetExpanded) {
|
||||
expandedKeys = arrAdd(expandedKeys, eventKey);
|
||||
} else {
|
||||
expandedKeys = arrDel(expandedKeys, eventKey);
|
||||
}
|
||||
|
||||
this.setUncontrolledState({ _expandedKeys: expandedKeys });
|
||||
this.__emit('expand', expandedKeys, {
|
||||
node: treeNode,
|
||||
expanded: targetExpanded,
|
||||
nativeEvent: e,
|
||||
});
|
||||
|
||||
// Async Load data
|
||||
if (targetExpanded && loadData) {
|
||||
const loadPromise = this.onNodeLoad(treeNode);
|
||||
return loadPromise
|
||||
? loadPromise.then(() => {
|
||||
// [Legacy] Refresh logic
|
||||
this.setUncontrolledState({ _expandedKeys: expandedKeys });
|
||||
})
|
||||
: null;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
onNodeMouseEnter(event, node) {
|
||||
this.__emit('mouseenter', { event, node });
|
||||
},
|
||||
|
||||
onNodeMouseLeave(event, node) {
|
||||
this.__emit('mouseleave', { event, node });
|
||||
},
|
||||
|
||||
onNodeContextMenu(event, node) {
|
||||
event.preventDefault();
|
||||
this.__emit('rightClick', { event, node });
|
||||
},
|
||||
|
||||
/**
|
||||
* Only update the value which is not in props
|
||||
*/
|
||||
setUncontrolledState(state) {
|
||||
let needSync = false;
|
||||
const newState = {};
|
||||
const props = getOptionProps(this);
|
||||
Object.keys(state).forEach(name => {
|
||||
if (name.replace('_', '') in props) return;
|
||||
needSync = true;
|
||||
newState[name] = state[name];
|
||||
});
|
||||
|
||||
if (needSync) {
|
||||
this.setState(newState);
|
||||
}
|
||||
},
|
||||
|
||||
registerTreeNode(key, node) {
|
||||
if (node) {
|
||||
this.domTreeNodes[key] = node;
|
||||
} else {
|
||||
delete this.domTreeNodes[key];
|
||||
}
|
||||
},
|
||||
|
||||
isKeyChecked(key) {
|
||||
const { _checkedKeys: checkedKeys = [] } = this.$data;
|
||||
return checkedKeys.indexOf(key) !== -1;
|
||||
},
|
||||
|
||||
/**
|
||||
* [Legacy] Original logic use `key` as tracking clue.
|
||||
* We have to use `cloneElement` to pass `key`.
|
||||
*/
|
||||
renderTreeNode(child, index, level = 0) {
|
||||
const {
|
||||
_keyEntities: keyEntities,
|
||||
_expandedKeys: expandedKeys = [],
|
||||
_selectedKeys: selectedKeys = [],
|
||||
_halfCheckedKeys: halfCheckedKeys = [],
|
||||
_loadedKeys: loadedKeys = [],
|
||||
_loadingKeys: loadingKeys = [],
|
||||
_dragOverNodeKey: dragOverNodeKey,
|
||||
_dropPosition: dropPosition,
|
||||
} = this.$data;
|
||||
const pos = getPosition(level, index);
|
||||
let key = child.key;
|
||||
if (!key && (key === undefined || key === null)) {
|
||||
key = pos;
|
||||
}
|
||||
if (!keyEntities.get(key)) {
|
||||
warnOnlyTreeNode();
|
||||
return null;
|
||||
}
|
||||
|
||||
return cloneElement(child, {
|
||||
eventKey: key,
|
||||
expanded: expandedKeys.indexOf(key) !== -1,
|
||||
selected: selectedKeys.indexOf(key) !== -1,
|
||||
loaded: loadedKeys.indexOf(key) !== -1,
|
||||
loading: loadingKeys.indexOf(key) !== -1,
|
||||
checked: this.isKeyChecked(key),
|
||||
halfChecked: halfCheckedKeys.indexOf(key) !== -1,
|
||||
pos,
|
||||
|
||||
// [Legacy] Drag props
|
||||
dragOver: dragOverNodeKey === key && dropPosition === 0,
|
||||
dragOverGapTop: dragOverNodeKey === key && dropPosition === -1,
|
||||
dragOverGapBottom: dragOverNodeKey === key && dropPosition === 1,
|
||||
key,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
render() {
|
||||
const { _treeNode: treeNode } = this.$data;
|
||||
const { prefixCls, focusable, showLine, tabindex = 0 } = this.$props;
|
||||
const domProps = getDataAndAria({ ...this.$props, ...this.$attrs });
|
||||
const { class: className, style } = this.$attrs;
|
||||
return (
|
||||
<ul
|
||||
{...domProps}
|
||||
class={classNames(prefixCls, className, {
|
||||
[`${prefixCls}-show-line`]: showLine,
|
||||
})}
|
||||
style={style}
|
||||
role="tree"
|
||||
unselectable="on"
|
||||
tabindex={focusable ? tabindex : null}
|
||||
>
|
||||
{mapChildren(treeNode, (node, index) => this.renderTreeNode(node, index))}
|
||||
</ul>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export { Tree };
|
||||
|
||||
export default Tree;
|
|
@ -1,578 +0,0 @@
|
|||
import { defineComponent, inject, provide } from 'vue';
|
||||
import PropTypes from '../../_util/vue-types';
|
||||
import classNames from '../../_util/classNames';
|
||||
import { getNodeChildren, mapChildren, warnOnlyTreeNode, getDataAndAria } from './util';
|
||||
import { initDefaultProps, getComponent, getSlot } from '../../_util/props-util';
|
||||
import BaseMixin from '../../_util/BaseMixin';
|
||||
import { getTransitionProps, Transition } from '../../_util/transition';
|
||||
|
||||
function noop() {}
|
||||
const ICON_OPEN = 'open';
|
||||
const ICON_CLOSE = 'close';
|
||||
|
||||
const defaultTitle = '---';
|
||||
|
||||
const TreeNode = defineComponent({
|
||||
name: 'TreeNode',
|
||||
mixins: [BaseMixin],
|
||||
inheritAttrs: false,
|
||||
__ANT_TREE_NODE: true,
|
||||
props: initDefaultProps(
|
||||
{
|
||||
eventKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), // Pass by parent `cloneElement`
|
||||
prefixCls: PropTypes.string,
|
||||
// className: PropTypes.string,
|
||||
root: PropTypes.object,
|
||||
// onSelect: PropTypes.func,
|
||||
|
||||
// By parent
|
||||
expanded: PropTypes.looseBool,
|
||||
selected: PropTypes.looseBool,
|
||||
checked: PropTypes.looseBool,
|
||||
loaded: PropTypes.looseBool,
|
||||
loading: PropTypes.looseBool,
|
||||
halfChecked: PropTypes.looseBool,
|
||||
title: PropTypes.any,
|
||||
pos: PropTypes.string,
|
||||
dragOver: PropTypes.looseBool,
|
||||
dragOverGapTop: PropTypes.looseBool,
|
||||
dragOverGapBottom: PropTypes.looseBool,
|
||||
|
||||
// By user
|
||||
isLeaf: PropTypes.looseBool,
|
||||
checkable: PropTypes.looseBool,
|
||||
selectable: PropTypes.looseBool,
|
||||
disabled: PropTypes.looseBool,
|
||||
disableCheckbox: PropTypes.looseBool,
|
||||
icon: PropTypes.any,
|
||||
dataRef: PropTypes.object,
|
||||
switcherIcon: PropTypes.any,
|
||||
label: PropTypes.any,
|
||||
value: PropTypes.any,
|
||||
},
|
||||
{},
|
||||
),
|
||||
setup() {
|
||||
return {
|
||||
vcTree: inject('vcTree', {}),
|
||||
vcTreeNode: inject('vcTreeNode', {}),
|
||||
};
|
||||
},
|
||||
|
||||
data() {
|
||||
this.children = null;
|
||||
return {
|
||||
dragNodeHighlight: false,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
provide('vcTreeNode', this);
|
||||
},
|
||||
// Isomorphic needn't load data in server side
|
||||
mounted() {
|
||||
const {
|
||||
eventKey,
|
||||
vcTree: { registerTreeNode },
|
||||
} = this;
|
||||
this.syncLoadData(this.$props);
|
||||
registerTreeNode && registerTreeNode(eventKey, this);
|
||||
},
|
||||
updated() {
|
||||
this.syncLoadData(this.$props);
|
||||
},
|
||||
beforeUnmount() {
|
||||
const {
|
||||
eventKey,
|
||||
vcTree: { registerTreeNode },
|
||||
} = this;
|
||||
registerTreeNode && registerTreeNode(eventKey, null);
|
||||
},
|
||||
|
||||
methods: {
|
||||
onSelectorClick(e) {
|
||||
// Click trigger before select/check operation
|
||||
const {
|
||||
vcTree: { onNodeClick },
|
||||
} = this;
|
||||
onNodeClick(e, this);
|
||||
if (this.isSelectable()) {
|
||||
this.onSelect(e);
|
||||
} else {
|
||||
this.onCheck(e);
|
||||
}
|
||||
},
|
||||
|
||||
onSelectorDoubleClick(e) {
|
||||
const {
|
||||
vcTree: { onNodeDoubleClick },
|
||||
} = this;
|
||||
onNodeDoubleClick(e, this);
|
||||
},
|
||||
|
||||
onSelect(e) {
|
||||
if (this.isDisabled()) return;
|
||||
|
||||
const {
|
||||
vcTree: { onNodeSelect },
|
||||
} = this;
|
||||
e.preventDefault();
|
||||
onNodeSelect(e, this);
|
||||
},
|
||||
|
||||
onCheck(e) {
|
||||
if (this.isDisabled()) return;
|
||||
|
||||
const { disableCheckbox, checked } = this;
|
||||
const {
|
||||
vcTree: { onNodeCheck },
|
||||
} = this;
|
||||
|
||||
if (!this.isCheckable() || disableCheckbox) return;
|
||||
|
||||
e.preventDefault();
|
||||
const targetChecked = !checked;
|
||||
onNodeCheck(e, this, targetChecked);
|
||||
},
|
||||
|
||||
onMouseEnter(e) {
|
||||
const {
|
||||
vcTree: { onNodeMouseEnter },
|
||||
} = this;
|
||||
onNodeMouseEnter(e, this);
|
||||
},
|
||||
|
||||
onMouseLeave(e) {
|
||||
const {
|
||||
vcTree: { onNodeMouseLeave },
|
||||
} = this;
|
||||
onNodeMouseLeave(e, this);
|
||||
},
|
||||
|
||||
onContextMenu(e) {
|
||||
const {
|
||||
vcTree: { onNodeContextMenu },
|
||||
} = this;
|
||||
onNodeContextMenu(e, this);
|
||||
},
|
||||
|
||||
onDragStart(e) {
|
||||
const {
|
||||
vcTree: { onNodeDragStart },
|
||||
} = this;
|
||||
|
||||
e.stopPropagation();
|
||||
this.setState({
|
||||
dragNodeHighlight: true,
|
||||
});
|
||||
onNodeDragStart(e, this);
|
||||
|
||||
try {
|
||||
// ie throw error
|
||||
// firefox-need-it
|
||||
e.dataTransfer.setData('text/plain', '');
|
||||
} catch (error) {
|
||||
// empty
|
||||
}
|
||||
},
|
||||
|
||||
onDragEnter(e) {
|
||||
const {
|
||||
vcTree: { onNodeDragEnter },
|
||||
} = this;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onNodeDragEnter(e, this);
|
||||
},
|
||||
|
||||
onDragOver(e) {
|
||||
const {
|
||||
vcTree: { onNodeDragOver },
|
||||
} = this;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onNodeDragOver(e, this);
|
||||
},
|
||||
|
||||
onDragLeave(e) {
|
||||
const {
|
||||
vcTree: { onNodeDragLeave },
|
||||
} = this;
|
||||
|
||||
e.stopPropagation();
|
||||
onNodeDragLeave(e, this);
|
||||
},
|
||||
|
||||
onDragEnd(e) {
|
||||
const {
|
||||
vcTree: { onNodeDragEnd },
|
||||
} = this;
|
||||
|
||||
e.stopPropagation();
|
||||
this.setState({
|
||||
dragNodeHighlight: false,
|
||||
});
|
||||
onNodeDragEnd(e, this);
|
||||
},
|
||||
|
||||
onDrop(e) {
|
||||
const {
|
||||
vcTree: { onNodeDrop },
|
||||
} = this;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.setState({
|
||||
dragNodeHighlight: false,
|
||||
});
|
||||
onNodeDrop(e, this);
|
||||
},
|
||||
|
||||
// Disabled item still can be switch
|
||||
onExpand(e) {
|
||||
const {
|
||||
vcTree: { onNodeExpand },
|
||||
} = this;
|
||||
onNodeExpand(e, this);
|
||||
},
|
||||
// Drag usage
|
||||
setSelectHandle(node) {
|
||||
this.selectHandle = node;
|
||||
},
|
||||
|
||||
getNodeChildren() {
|
||||
const originList = this.children;
|
||||
const targetList = getNodeChildren(originList);
|
||||
|
||||
if (originList.length !== targetList.length) {
|
||||
warnOnlyTreeNode();
|
||||
}
|
||||
|
||||
return targetList;
|
||||
},
|
||||
|
||||
getNodeState() {
|
||||
const { expanded } = this;
|
||||
|
||||
if (this.isLeaf2()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return expanded ? ICON_OPEN : ICON_CLOSE;
|
||||
},
|
||||
|
||||
isLeaf2() {
|
||||
const { isLeaf, loaded } = this;
|
||||
const {
|
||||
vcTree: { loadData },
|
||||
} = this;
|
||||
|
||||
const hasChildren = this.getNodeChildren().length !== 0;
|
||||
if (isLeaf === false) {
|
||||
return false;
|
||||
}
|
||||
return isLeaf || (!loadData && !hasChildren) || (loadData && loaded && !hasChildren);
|
||||
},
|
||||
|
||||
isDisabled() {
|
||||
const { disabled } = this;
|
||||
const {
|
||||
vcTree: { disabled: treeDisabled },
|
||||
} = this;
|
||||
|
||||
// Follow the logic of Selectable
|
||||
if (disabled === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !!(treeDisabled || disabled);
|
||||
},
|
||||
|
||||
isCheckable() {
|
||||
const { checkable } = this.$props;
|
||||
const {
|
||||
vcTree: { checkable: treeCheckable },
|
||||
} = this;
|
||||
|
||||
// Return false if tree or treeNode is not checkable
|
||||
if (!treeCheckable || checkable === false) return false;
|
||||
return treeCheckable;
|
||||
},
|
||||
|
||||
// Load data to avoid default expanded tree without data
|
||||
syncLoadData(props) {
|
||||
const { expanded, loading, loaded } = props;
|
||||
const {
|
||||
vcTree: { loadData, onNodeLoad },
|
||||
} = this;
|
||||
if (loading) return;
|
||||
// read from state to avoid loadData at same time
|
||||
if (loadData && expanded && !this.isLeaf2()) {
|
||||
// We needn't reload data when has children in sync logic
|
||||
// It's only needed in node expanded
|
||||
const hasChildren = this.getNodeChildren().length !== 0;
|
||||
if (!hasChildren && !loaded) {
|
||||
onNodeLoad(this);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
isSelectable() {
|
||||
const { selectable } = this;
|
||||
const {
|
||||
vcTree: { selectable: treeSelectable },
|
||||
} = this;
|
||||
|
||||
// Ignore when selectable is undefined or null
|
||||
if (typeof selectable === 'boolean') {
|
||||
return selectable;
|
||||
}
|
||||
|
||||
return treeSelectable;
|
||||
},
|
||||
|
||||
// Switcher
|
||||
renderSwitcher() {
|
||||
const { expanded } = this;
|
||||
const {
|
||||
vcTree: { prefixCls },
|
||||
} = this;
|
||||
const switcherIcon =
|
||||
getComponent(this, 'switcherIcon', {}, false) ||
|
||||
getComponent(this.vcTree, 'switcherIcon', {}, false);
|
||||
if (this.isLeaf2()) {
|
||||
return (
|
||||
<span
|
||||
key="switcher"
|
||||
class={classNames(`${prefixCls}-switcher`, `${prefixCls}-switcher-noop`)}
|
||||
>
|
||||
{typeof switcherIcon === 'function'
|
||||
? switcherIcon({ ...this.$props, ...this.$props.dataRef, isLeaf: true })
|
||||
: switcherIcon}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const switcherCls = classNames(
|
||||
`${prefixCls}-switcher`,
|
||||
`${prefixCls}-switcher_${expanded ? ICON_OPEN : ICON_CLOSE}`,
|
||||
);
|
||||
return (
|
||||
<span key="switcher" onClick={this.onExpand} class={switcherCls}>
|
||||
{typeof switcherIcon === 'function'
|
||||
? switcherIcon({ ...this.$props, ...this.$props.dataRef, isLeaf: false })
|
||||
: switcherIcon}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
|
||||
// Checkbox
|
||||
renderCheckbox() {
|
||||
const { checked, halfChecked, disableCheckbox } = this;
|
||||
const {
|
||||
vcTree: { prefixCls },
|
||||
} = this;
|
||||
const disabled = this.isDisabled();
|
||||
const checkable = this.isCheckable();
|
||||
|
||||
if (!checkable) return null;
|
||||
|
||||
// [Legacy] Custom element should be separate with `checkable` in future
|
||||
const $custom = typeof checkable !== 'boolean' ? checkable : null;
|
||||
|
||||
return (
|
||||
<span
|
||||
key="checkbox"
|
||||
class={classNames(
|
||||
`${prefixCls}-checkbox`,
|
||||
checked && `${prefixCls}-checkbox-checked`,
|
||||
!checked && halfChecked && `${prefixCls}-checkbox-indeterminate`,
|
||||
(disabled || disableCheckbox) && `${prefixCls}-checkbox-disabled`,
|
||||
)}
|
||||
onClick={this.onCheck}
|
||||
>
|
||||
{$custom}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
|
||||
renderIcon() {
|
||||
const { loading } = this;
|
||||
const {
|
||||
vcTree: { prefixCls },
|
||||
} = this;
|
||||
|
||||
return (
|
||||
<span
|
||||
key="icon"
|
||||
class={classNames(
|
||||
`${prefixCls}-iconEle`,
|
||||
`${prefixCls}-icon__${this.getNodeState() || 'docu'}`,
|
||||
loading && `${prefixCls}-icon_loading`,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
// Icon + Title
|
||||
renderSelector() {
|
||||
const { selected, loading, dragNodeHighlight } = this;
|
||||
const icon = getComponent(this, 'icon', {}, false);
|
||||
const {
|
||||
vcTree: { prefixCls, showIcon, icon: treeIcon, draggable, loadData },
|
||||
} = this;
|
||||
const disabled = this.isDisabled();
|
||||
const title = getComponent(this, 'title', {}, false);
|
||||
const wrapClass = `${prefixCls}-node-content-wrapper`;
|
||||
|
||||
// Icon - Still show loading icon when loading without showIcon
|
||||
let $icon;
|
||||
|
||||
if (showIcon) {
|
||||
const currentIcon = icon || treeIcon;
|
||||
$icon = currentIcon ? (
|
||||
<span class={classNames(`${prefixCls}-iconEle`, `${prefixCls}-icon__customize`)}>
|
||||
{typeof currentIcon === 'function'
|
||||
? currentIcon({ ...this.$props, ...this.$props.dataRef })
|
||||
: currentIcon}
|
||||
</span>
|
||||
) : (
|
||||
this.renderIcon()
|
||||
);
|
||||
} else if (loadData && loading) {
|
||||
$icon = this.renderIcon();
|
||||
}
|
||||
|
||||
const currentTitle = title;
|
||||
let $title = currentTitle ? (
|
||||
<span class={`${prefixCls}-title`}>
|
||||
{typeof currentTitle === 'function'
|
||||
? currentTitle({ ...this.$props, ...this.$props.dataRef })
|
||||
: currentTitle}
|
||||
</span>
|
||||
) : (
|
||||
<span class={`${prefixCls}-title`}>{defaultTitle}</span>
|
||||
);
|
||||
return (
|
||||
<span
|
||||
key="selector"
|
||||
ref={this.setSelectHandle}
|
||||
title={typeof title === 'string' ? title : ''}
|
||||
class={classNames(
|
||||
`${wrapClass}`,
|
||||
`${wrapClass}-${this.getNodeState() || 'normal'}`,
|
||||
!disabled && (selected || dragNodeHighlight) && `${prefixCls}-node-selected`,
|
||||
!disabled && draggable && 'draggable',
|
||||
)}
|
||||
draggable={(!disabled && draggable) || undefined}
|
||||
aria-grabbed={(!disabled && draggable) || undefined}
|
||||
onMouseenter={this.onMouseEnter}
|
||||
onMouseleave={this.onMouseLeave}
|
||||
onContextmenu={this.onContextMenu}
|
||||
onClick={this.onSelectorClick}
|
||||
onDblclick={this.onSelectorDoubleClick}
|
||||
onDragstart={draggable ? this.onDragStart : noop}
|
||||
>
|
||||
{$icon}
|
||||
{$title}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
|
||||
// Children list wrapped with `Animation`
|
||||
renderChildren() {
|
||||
const { expanded, pos } = this;
|
||||
const {
|
||||
vcTree: { prefixCls, openTransitionName, openAnimation, renderTreeNode },
|
||||
} = this;
|
||||
|
||||
let animProps = {};
|
||||
if (openTransitionName) {
|
||||
animProps = getTransitionProps(openTransitionName);
|
||||
} else if (typeof openAnimation === 'object') {
|
||||
animProps = { ...openAnimation, css: false, ...animProps };
|
||||
}
|
||||
|
||||
// Children TreeNode
|
||||
const nodeList = this.getNodeChildren();
|
||||
|
||||
if (nodeList.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let $children;
|
||||
if (expanded) {
|
||||
$children = (
|
||||
<ul
|
||||
class={classNames(
|
||||
`${prefixCls}-child-tree`,
|
||||
expanded && `${prefixCls}-child-tree-open`,
|
||||
)}
|
||||
data-expanded={expanded}
|
||||
role="group"
|
||||
>
|
||||
{mapChildren(nodeList, (node, index) => renderTreeNode(node, index, pos))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
return <Transition {...animProps}>{$children}</Transition>;
|
||||
},
|
||||
},
|
||||
|
||||
render() {
|
||||
this.children = getSlot(this);
|
||||
const {
|
||||
dragOver,
|
||||
dragOverGapTop,
|
||||
dragOverGapBottom,
|
||||
isLeaf,
|
||||
expanded,
|
||||
selected,
|
||||
checked,
|
||||
halfChecked,
|
||||
loading,
|
||||
} = this.$props;
|
||||
const {
|
||||
vcTree: { prefixCls, filterTreeNode, draggable },
|
||||
} = this;
|
||||
const disabled = this.isDisabled();
|
||||
const dataOrAriaAttributeProps = getDataAndAria({ ...this.$props, ...this.$attrs });
|
||||
const { class: className, style } = this.$attrs;
|
||||
return (
|
||||
<li
|
||||
class={{
|
||||
[className]: className,
|
||||
[`${prefixCls}-treenode-disabled`]: disabled,
|
||||
[`${prefixCls}-treenode-switcher-${expanded ? 'open' : 'close'}`]: !isLeaf,
|
||||
[`${prefixCls}-treenode-checkbox-checked`]: checked,
|
||||
[`${prefixCls}-treenode-checkbox-indeterminate`]: halfChecked,
|
||||
[`${prefixCls}-treenode-selected`]: selected,
|
||||
[`${prefixCls}-treenode-loading`]: loading,
|
||||
'drag-over': !disabled && dragOver,
|
||||
'drag-over-gap-top': !disabled && dragOverGapTop,
|
||||
'drag-over-gap-bottom': !disabled && dragOverGapBottom,
|
||||
'filter-node': filterTreeNode && filterTreeNode(this),
|
||||
}}
|
||||
style={style}
|
||||
role="treeitem"
|
||||
onDragenter={draggable ? this.onDragEnter : noop}
|
||||
onDragover={draggable ? this.onDragOver : noop}
|
||||
onDragleave={draggable ? this.onDragLeave : noop}
|
||||
onDrop={draggable ? this.onDrop : noop}
|
||||
onDragend={draggable ? this.onDragEnd : noop}
|
||||
{...dataOrAriaAttributeProps}
|
||||
>
|
||||
{this.renderSwitcher()}
|
||||
{this.renderCheckbox()}
|
||||
{this.renderSelector()}
|
||||
{this.renderChildren()}
|
||||
</li>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
TreeNode.isTreeNode = 1;
|
||||
|
||||
export default TreeNode;
|
|
@ -1,5 +0,0 @@
|
|||
import Tree from './Tree';
|
||||
import TreeNode from './TreeNode';
|
||||
Tree.TreeNode = TreeNode;
|
||||
|
||||
export default Tree;
|
|
@ -1,426 +0,0 @@
|
|||
/* eslint no-loop-func: 0*/
|
||||
import warning from 'warning';
|
||||
import TreeNode from './TreeNode';
|
||||
import { getOptionProps, getSlot } from '../../_util/props-util';
|
||||
const DRAG_SIDE_RANGE = 0.25;
|
||||
const DRAG_MIN_GAP = 2;
|
||||
|
||||
let onlyTreeNodeWarned = false;
|
||||
|
||||
export function warnOnlyTreeNode() {
|
||||
if (onlyTreeNodeWarned) return;
|
||||
|
||||
onlyTreeNodeWarned = true;
|
||||
warning(false, 'Tree only accept TreeNode as children.');
|
||||
}
|
||||
|
||||
export function arrDel(list, value) {
|
||||
const clone = list.slice();
|
||||
const index = clone.indexOf(value);
|
||||
if (index >= 0) {
|
||||
clone.splice(index, 1);
|
||||
}
|
||||
return clone;
|
||||
}
|
||||
|
||||
export function arrAdd(list, value) {
|
||||
const clone = list.slice();
|
||||
if (clone.indexOf(value) === -1) {
|
||||
clone.push(value);
|
||||
}
|
||||
return clone;
|
||||
}
|
||||
|
||||
export function posToArr(pos) {
|
||||
return pos.split('-');
|
||||
}
|
||||
|
||||
export function getPosition(level, index) {
|
||||
return `${level}-${index}`;
|
||||
}
|
||||
|
||||
export function isTreeNode(node) {
|
||||
return node.type && node.type.isTreeNode;
|
||||
}
|
||||
|
||||
export function getNodeChildren(children = []) {
|
||||
return children.filter(isTreeNode);
|
||||
}
|
||||
|
||||
export function isCheckDisabled(node) {
|
||||
const { disabled, disableCheckbox, checkable } = getOptionProps(node) || {};
|
||||
return !!(disabled || disableCheckbox) || checkable === false;
|
||||
}
|
||||
|
||||
export function traverseTreeNodes(treeNodes, callback) {
|
||||
function processNode(node, index, parent) {
|
||||
const children = node ? getSlot(node) : treeNodes;
|
||||
const pos = node ? getPosition(parent.pos, index) : 0;
|
||||
|
||||
// Filter children
|
||||
const childList = getNodeChildren(children);
|
||||
|
||||
// Process node if is not root
|
||||
if (node) {
|
||||
let key = node.key;
|
||||
if (!key && (key === undefined || key === null)) {
|
||||
key = pos;
|
||||
}
|
||||
const data = {
|
||||
node,
|
||||
index,
|
||||
pos,
|
||||
key,
|
||||
parentPos: parent.node ? parent.pos : null,
|
||||
};
|
||||
callback(data);
|
||||
}
|
||||
|
||||
// Process children node
|
||||
childList.forEach((subNode, subIndex) => {
|
||||
processNode(subNode, subIndex, { node, pos });
|
||||
});
|
||||
}
|
||||
|
||||
processNode(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use `rc-util` `toArray` to get the children list which keeps the key.
|
||||
* And return single node if children is only one(This can avoid `key` missing check).
|
||||
*/
|
||||
export function mapChildren(children = [], func) {
|
||||
const list = children.map(func);
|
||||
if (list.length === 1) {
|
||||
return list[0];
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
export function getDragNodesKeys(treeNodes, node) {
|
||||
const { eventKey, pos } = getOptionProps(node);
|
||||
const dragNodesKeys = [];
|
||||
|
||||
traverseTreeNodes(treeNodes, ({ key }) => {
|
||||
dragNodesKeys.push(key);
|
||||
});
|
||||
dragNodesKeys.push(eventKey || pos);
|
||||
return dragNodesKeys;
|
||||
}
|
||||
|
||||
export function calcDropPosition(event, treeNode) {
|
||||
const { clientY } = event;
|
||||
const { top, bottom, height } = treeNode.selectHandle.getBoundingClientRect();
|
||||
const des = Math.max(height * DRAG_SIDE_RANGE, DRAG_MIN_GAP);
|
||||
|
||||
if (clientY <= top + des) {
|
||||
return -1;
|
||||
}
|
||||
if (clientY >= bottom - des) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return selectedKeys according with multiple prop
|
||||
* @param selectedKeys
|
||||
* @param props
|
||||
* @returns [string]
|
||||
*/
|
||||
export function calcSelectedKeys(selectedKeys, props) {
|
||||
if (!selectedKeys) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { multiple } = props;
|
||||
if (multiple) {
|
||||
return selectedKeys.slice();
|
||||
}
|
||||
|
||||
if (selectedKeys.length) {
|
||||
return [selectedKeys[0]];
|
||||
}
|
||||
return selectedKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Since React internal will convert key to string,
|
||||
* we need do this to avoid `checkStrictly` use number match
|
||||
*/
|
||||
// function keyListToString (keyList) {
|
||||
// if (!keyList) return keyList
|
||||
// return keyList.map(key => String(key))
|
||||
// }
|
||||
|
||||
const internalProcessProps = (props = {}) => {
|
||||
return {
|
||||
...props,
|
||||
class: props.class || props.className,
|
||||
style: props.style,
|
||||
key: props.key,
|
||||
};
|
||||
};
|
||||
export function convertDataToTree(treeData, processor) {
|
||||
if (!treeData) return [];
|
||||
|
||||
const { processProps = internalProcessProps } = processor || {};
|
||||
const list = Array.isArray(treeData) ? treeData : [treeData];
|
||||
return list.map(({ children, ...props }) => {
|
||||
const childrenNodes = convertDataToTree(children, processor);
|
||||
return <TreeNode {...processProps(props)}>{childrenNodes}</TreeNode>;
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: ========================= NEW LOGIC =========================
|
||||
/**
|
||||
* Calculate treeNodes entities. `processTreeEntity` is used for `rc-tree-select`
|
||||
* @param treeNodes
|
||||
* @param processTreeEntity User can customize the entity
|
||||
*/
|
||||
export function convertTreeToEntities(
|
||||
treeNodes,
|
||||
{ initWrapper, processEntity, onProcessFinished } = {},
|
||||
) {
|
||||
const posEntities = new Map();
|
||||
const keyEntities = new Map();
|
||||
let wrapper = {
|
||||
posEntities,
|
||||
keyEntities,
|
||||
};
|
||||
|
||||
if (initWrapper) {
|
||||
wrapper = initWrapper(wrapper) || wrapper;
|
||||
}
|
||||
|
||||
traverseTreeNodes(treeNodes, item => {
|
||||
const { node, index, pos, key, parentPos } = item;
|
||||
const entity = { node, index, key, pos };
|
||||
|
||||
posEntities.set(pos, entity);
|
||||
keyEntities.set(key, entity);
|
||||
|
||||
// Fill children
|
||||
entity.parent = posEntities.get(parentPos);
|
||||
if (entity.parent) {
|
||||
entity.parent.children = entity.parent.children || [];
|
||||
entity.parent.children.push(entity);
|
||||
}
|
||||
|
||||
if (processEntity) {
|
||||
processEntity(entity, wrapper);
|
||||
}
|
||||
});
|
||||
|
||||
if (onProcessFinished) {
|
||||
onProcessFinished(wrapper);
|
||||
}
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse `checkedKeys` to { checkedKeys, halfCheckedKeys } style
|
||||
*/
|
||||
export function parseCheckedKeys(keys) {
|
||||
if (!keys) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Convert keys to object format
|
||||
let keyProps;
|
||||
if (Array.isArray(keys)) {
|
||||
// [Legacy] Follow the api doc
|
||||
keyProps = {
|
||||
checkedKeys: keys,
|
||||
halfCheckedKeys: undefined,
|
||||
};
|
||||
} else if (typeof keys === 'object') {
|
||||
keyProps = {
|
||||
checkedKeys: keys.checked || undefined,
|
||||
halfCheckedKeys: keys.halfChecked || undefined,
|
||||
};
|
||||
} else {
|
||||
warning(false, '`checkedKeys` is not an array or an object');
|
||||
return null;
|
||||
}
|
||||
|
||||
// keyProps.checkedKeys = keyListToString(keyProps.checkedKeys)
|
||||
// keyProps.halfCheckedKeys = keyListToString(keyProps.halfCheckedKeys)
|
||||
|
||||
return keyProps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Conduct check state by the keyList. It will conduct up & from the provided key.
|
||||
* If the conduct path reach the disabled or already checked / unchecked node will stop conduct.
|
||||
* @param keyList list of keys
|
||||
* @param isCheck is check the node or not
|
||||
* @param keyEntities parsed by `convertTreeToEntities` function in Tree
|
||||
* @param checkStatus Can pass current checked status for process (usually for uncheck operation)
|
||||
* @returns {{checkedKeys: [], halfCheckedKeys: []}}
|
||||
*/
|
||||
export function conductCheck(keyList, isCheck, keyEntities, checkStatus = {}) {
|
||||
const checkedKeys = new Map();
|
||||
const halfCheckedKeys = new Map(); // Record the key has some child checked (include child half checked)
|
||||
|
||||
(checkStatus.checkedKeys || []).forEach(key => {
|
||||
checkedKeys.set(key, true);
|
||||
});
|
||||
|
||||
(checkStatus.halfCheckedKeys || []).forEach(key => {
|
||||
halfCheckedKeys.set(key, true);
|
||||
});
|
||||
|
||||
// Conduct up
|
||||
function conductUp(key) {
|
||||
if (checkedKeys.get(key) === isCheck) return;
|
||||
|
||||
const entity = keyEntities.get(key);
|
||||
if (!entity) return;
|
||||
|
||||
const { children, parent, node } = entity;
|
||||
|
||||
if (isCheckDisabled(node)) return;
|
||||
|
||||
// Check child node checked status
|
||||
let everyChildChecked = true;
|
||||
let someChildChecked = false; // Child checked or half checked
|
||||
|
||||
(children || [])
|
||||
.filter(child => !isCheckDisabled(child.node))
|
||||
.forEach(({ key: childKey }) => {
|
||||
const childChecked = checkedKeys.get(childKey);
|
||||
const childHalfChecked = halfCheckedKeys.get(childKey);
|
||||
|
||||
if (childChecked || childHalfChecked) someChildChecked = true;
|
||||
if (!childChecked) everyChildChecked = false;
|
||||
});
|
||||
|
||||
// Update checked status
|
||||
if (isCheck) {
|
||||
checkedKeys.set(key, everyChildChecked);
|
||||
} else {
|
||||
checkedKeys.set(key, false);
|
||||
}
|
||||
halfCheckedKeys.set(key, someChildChecked);
|
||||
|
||||
if (parent) {
|
||||
conductUp(parent.key);
|
||||
}
|
||||
}
|
||||
|
||||
// Conduct down
|
||||
function conductDown(key) {
|
||||
if (checkedKeys.get(key) === isCheck) return;
|
||||
|
||||
const entity = keyEntities.get(key);
|
||||
if (!entity) return;
|
||||
|
||||
const { children, node } = entity;
|
||||
|
||||
if (isCheckDisabled(node)) return;
|
||||
|
||||
checkedKeys.set(key, isCheck);
|
||||
|
||||
(children || []).forEach(child => {
|
||||
conductDown(child.key);
|
||||
});
|
||||
}
|
||||
|
||||
function conduct(key) {
|
||||
const entity = keyEntities.get(key);
|
||||
|
||||
if (!entity) {
|
||||
warning(false, `'${key}' does not exist in the tree.`);
|
||||
return;
|
||||
}
|
||||
const { children, parent, node } = entity;
|
||||
checkedKeys.set(key, isCheck);
|
||||
|
||||
if (isCheckDisabled(node)) return;
|
||||
|
||||
// Conduct down
|
||||
(children || [])
|
||||
.filter(child => !isCheckDisabled(child.node))
|
||||
.forEach(child => {
|
||||
conductDown(child.key);
|
||||
});
|
||||
|
||||
// Conduct up
|
||||
if (parent) {
|
||||
conductUp(parent.key);
|
||||
}
|
||||
}
|
||||
|
||||
(keyList || []).forEach(key => {
|
||||
conduct(key);
|
||||
});
|
||||
|
||||
const checkedKeyList = [];
|
||||
const halfCheckedKeyList = [];
|
||||
|
||||
// Fill checked list
|
||||
for (const [key, value] of checkedKeys) {
|
||||
if (value) {
|
||||
checkedKeyList.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Fill half checked list
|
||||
for (const [key, value] of halfCheckedKeys) {
|
||||
if (!checkedKeys.get(key) && value) {
|
||||
halfCheckedKeyList.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
checkedKeys: checkedKeyList,
|
||||
halfCheckedKeys: halfCheckedKeyList,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* If user use `autoExpandParent` we should get the list of parent node
|
||||
* @param keyList
|
||||
* @param keyEntities
|
||||
*/
|
||||
export function conductExpandParent(keyList, keyEntities) {
|
||||
const expandedKeys = new Map();
|
||||
|
||||
function conductUp(key) {
|
||||
if (expandedKeys.get(key)) return;
|
||||
|
||||
const entity = keyEntities.get(key);
|
||||
if (!entity) return;
|
||||
|
||||
expandedKeys.set(key, true);
|
||||
|
||||
const { parent, node } = entity;
|
||||
const props = getOptionProps(node);
|
||||
if (props && props.disabled) return;
|
||||
|
||||
if (parent) {
|
||||
conductUp(parent.key);
|
||||
}
|
||||
}
|
||||
|
||||
(keyList || []).forEach(key => {
|
||||
conductUp(key);
|
||||
});
|
||||
|
||||
return [...expandedKeys.keys()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns only the data- and aria- key/value pairs
|
||||
* @param {object} props
|
||||
*/
|
||||
export function getDataAndAria(props) {
|
||||
return Object.keys(props).reduce((prev, key) => {
|
||||
if (key.substr(0, 5) === 'data-' || key.substr(0, 5) === 'aria-') {
|
||||
prev[key] = props[key];
|
||||
}
|
||||
return prev;
|
||||
}, {});
|
||||
}
|
|
@ -0,0 +1,353 @@
|
|||
/* eslint-disable no-lonely-if */
|
||||
/**
|
||||
* Legacy code. Should avoid to use if you are new to import these code.
|
||||
*/
|
||||
|
||||
import TreeNode from './TreeNode';
|
||||
import {
|
||||
NodeElement,
|
||||
Key,
|
||||
DataNode,
|
||||
DataEntity,
|
||||
NodeInstance,
|
||||
FlattenNode,
|
||||
Direction,
|
||||
} from './interface';
|
||||
import { warning } from '../vc-util/warning';
|
||||
import { AllowDrop, TreeNodeProps, TreeProps } from './props';
|
||||
|
||||
export function arrDel(list: Key[], value: Key) {
|
||||
const clone = list.slice();
|
||||
const index = clone.indexOf(value);
|
||||
if (index >= 0) {
|
||||
clone.splice(index, 1);
|
||||
}
|
||||
return clone;
|
||||
}
|
||||
|
||||
export function arrAdd(list: Key[], value: Key) {
|
||||
const clone = list.slice();
|
||||
if (clone.indexOf(value) === -1) {
|
||||
clone.push(value);
|
||||
}
|
||||
return clone;
|
||||
}
|
||||
|
||||
export function posToArr(pos: string) {
|
||||
return pos.split('-');
|
||||
}
|
||||
|
||||
export function getPosition(level: string | number, index: number) {
|
||||
return `${level}-${index}`;
|
||||
}
|
||||
|
||||
export function isTreeNode(node: NodeElement) {
|
||||
return node && node.type && node.type.isTreeNode;
|
||||
}
|
||||
|
||||
export function getDragChildrenKeys(dragNodeKey: Key, keyEntities: Record<Key, DataEntity>): Key[] {
|
||||
// not contains self
|
||||
// self for left or right drag
|
||||
const dragChildrenKeys = [];
|
||||
|
||||
const entity = keyEntities[dragNodeKey];
|
||||
function dig(list: DataEntity[] = []) {
|
||||
list.forEach(({ key, children }) => {
|
||||
dragChildrenKeys.push(key);
|
||||
dig(children);
|
||||
});
|
||||
}
|
||||
|
||||
dig(entity.children);
|
||||
|
||||
return dragChildrenKeys;
|
||||
}
|
||||
|
||||
export function isLastChild(treeNodeEntity: DataEntity) {
|
||||
if (treeNodeEntity.parent) {
|
||||
const posArr = posToArr(treeNodeEntity.pos);
|
||||
return Number(posArr[posArr.length - 1]) === treeNodeEntity.parent.children.length - 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isFirstChild(treeNodeEntity: DataEntity) {
|
||||
const posArr = posToArr(treeNodeEntity.pos);
|
||||
return Number(posArr[posArr.length - 1]) === 0;
|
||||
}
|
||||
|
||||
// Only used when drag, not affect SSR.
|
||||
export function calcDropPosition(
|
||||
event: MouseEvent,
|
||||
_dragNode: NodeInstance,
|
||||
targetNode: NodeInstance,
|
||||
indent: number,
|
||||
startMousePosition: {
|
||||
x: number;
|
||||
y: number;
|
||||
},
|
||||
allowDrop: AllowDrop,
|
||||
flattenedNodes: FlattenNode[],
|
||||
keyEntities: Record<Key, DataEntity>,
|
||||
expandKeys: Key[],
|
||||
direction: Direction,
|
||||
): {
|
||||
dropPosition: -1 | 0 | 1;
|
||||
dropLevelOffset: number;
|
||||
dropTargetKey: Key;
|
||||
dropTargetPos: string;
|
||||
dropContainerKey: Key;
|
||||
dragOverNodeKey: Key;
|
||||
dropAllowed: boolean;
|
||||
} {
|
||||
const { clientX, clientY } = event;
|
||||
const { top, height } = (event.target as HTMLElement).getBoundingClientRect();
|
||||
// optional chain for testing
|
||||
const horizontalMouseOffset =
|
||||
(direction === 'rtl' ? -1 : 1) * ((startMousePosition?.x || 0) - clientX);
|
||||
const rawDropLevelOffset = (horizontalMouseOffset - 12) / indent;
|
||||
|
||||
// find abstract drop node by horizontal offset
|
||||
let abstractDropNodeEntity: DataEntity = keyEntities[targetNode.props.eventKey];
|
||||
|
||||
if (clientY < top + height / 2) {
|
||||
// first half, set abstract drop node to previous node
|
||||
const nodeIndex = flattenedNodes.findIndex(
|
||||
flattenedNode => flattenedNode.data.key === abstractDropNodeEntity.key,
|
||||
);
|
||||
const prevNodeIndex = nodeIndex <= 0 ? 0 : nodeIndex - 1;
|
||||
const prevNodeKey = flattenedNodes[prevNodeIndex].data.key;
|
||||
abstractDropNodeEntity = keyEntities[prevNodeKey];
|
||||
}
|
||||
|
||||
const initialAbstractDropNodeKey = abstractDropNodeEntity.key;
|
||||
|
||||
const abstractDragOverEntity = abstractDropNodeEntity;
|
||||
const dragOverNodeKey = abstractDropNodeEntity.key;
|
||||
|
||||
let dropPosition: -1 | 0 | 1 = 0;
|
||||
let dropLevelOffset = 0;
|
||||
|
||||
// Only allow cross level drop when dragging on a non-expanded node
|
||||
if (!expandKeys.includes(initialAbstractDropNodeKey)) {
|
||||
for (let i = 0; i < rawDropLevelOffset; i += 1) {
|
||||
if (isLastChild(abstractDropNodeEntity)) {
|
||||
abstractDropNodeEntity = abstractDropNodeEntity.parent;
|
||||
dropLevelOffset += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const abstractDropDataNode = abstractDropNodeEntity.node;
|
||||
let dropAllowed = true;
|
||||
if (
|
||||
isFirstChild(abstractDropNodeEntity) &&
|
||||
abstractDropNodeEntity.level === 0 &&
|
||||
clientY < top + height / 2 &&
|
||||
allowDrop({
|
||||
dropNode: abstractDropDataNode,
|
||||
dropPosition: -1,
|
||||
}) &&
|
||||
abstractDropNodeEntity.key === targetNode.props.eventKey
|
||||
) {
|
||||
// first half of first node in first level
|
||||
dropPosition = -1;
|
||||
} else if (
|
||||
(abstractDragOverEntity.children || []).length &&
|
||||
expandKeys.includes(dragOverNodeKey)
|
||||
) {
|
||||
// drop on expanded node
|
||||
// only allow drop inside
|
||||
if (
|
||||
allowDrop({
|
||||
dropNode: abstractDropDataNode,
|
||||
dropPosition: 0,
|
||||
})
|
||||
) {
|
||||
dropPosition = 0;
|
||||
} else {
|
||||
dropAllowed = false;
|
||||
}
|
||||
} else if (dropLevelOffset === 0) {
|
||||
if (rawDropLevelOffset > -1.5) {
|
||||
// | Node | <- abstractDropNode
|
||||
// | -^-===== | <- mousePosition
|
||||
// 1. try drop after
|
||||
// 2. do not allow drop
|
||||
if (
|
||||
allowDrop({
|
||||
dropNode: abstractDropDataNode,
|
||||
dropPosition: 1,
|
||||
})
|
||||
) {
|
||||
dropPosition = 1;
|
||||
} else {
|
||||
dropAllowed = false;
|
||||
}
|
||||
} else {
|
||||
// | Node | <- abstractDropNode
|
||||
// | ---==^== | <- mousePosition
|
||||
// whether it has children or doesn't has children
|
||||
// always
|
||||
// 1. try drop inside
|
||||
// 2. try drop after
|
||||
// 3. do not allow drop
|
||||
if (
|
||||
allowDrop({
|
||||
dropNode: abstractDropDataNode,
|
||||
dropPosition: 0,
|
||||
})
|
||||
) {
|
||||
dropPosition = 0;
|
||||
} else if (
|
||||
allowDrop({
|
||||
dropNode: abstractDropDataNode,
|
||||
dropPosition: 1,
|
||||
})
|
||||
) {
|
||||
dropPosition = 1;
|
||||
} else {
|
||||
dropAllowed = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// | Node1 | <- abstractDropNode
|
||||
// | Node2 |
|
||||
// --^--|----=====| <- mousePosition
|
||||
// 1. try insert after Node1
|
||||
// 2. do not allow drop
|
||||
if (
|
||||
allowDrop({
|
||||
dropNode: abstractDropDataNode,
|
||||
dropPosition: 1,
|
||||
})
|
||||
) {
|
||||
dropPosition = 1;
|
||||
} else {
|
||||
dropAllowed = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
dropPosition,
|
||||
dropLevelOffset,
|
||||
dropTargetKey: abstractDropNodeEntity.key,
|
||||
dropTargetPos: abstractDropNodeEntity.pos,
|
||||
dragOverNodeKey,
|
||||
dropContainerKey: dropPosition === 0 ? null : abstractDropNodeEntity.parent?.key || null,
|
||||
dropAllowed,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return selectedKeys according with multiple prop
|
||||
* @param selectedKeys
|
||||
* @param props
|
||||
* @returns [string]
|
||||
*/
|
||||
export function calcSelectedKeys(selectedKeys: Key[], props: TreeProps) {
|
||||
if (!selectedKeys) return undefined;
|
||||
|
||||
const { multiple } = props;
|
||||
if (multiple) {
|
||||
return selectedKeys.slice();
|
||||
}
|
||||
|
||||
if (selectedKeys.length) {
|
||||
return [selectedKeys[0]];
|
||||
}
|
||||
return selectedKeys;
|
||||
}
|
||||
|
||||
const internalProcessProps = (props: DataNode): Partial<TreeNodeProps> => props;
|
||||
export function convertDataToTree(
|
||||
treeData: DataNode[],
|
||||
processor?: { processProps: (prop: DataNode) => any },
|
||||
): NodeElement[] {
|
||||
if (!treeData) return [];
|
||||
|
||||
const { processProps = internalProcessProps } = processor || {};
|
||||
const list = Array.isArray(treeData) ? treeData : [treeData];
|
||||
return list.map(({ children, ...props }): NodeElement => {
|
||||
const childrenNodes = convertDataToTree(children, processor);
|
||||
|
||||
return <TreeNode {...processProps(props)}>{childrenNodes}</TreeNode>;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse `checkedKeys` to { checkedKeys, halfCheckedKeys } style
|
||||
*/
|
||||
export function parseCheckedKeys(keys: Key[] | { checked: Key[]; halfChecked: Key[] }) {
|
||||
if (!keys) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Convert keys to object format
|
||||
let keyProps;
|
||||
if (Array.isArray(keys)) {
|
||||
// [Legacy] Follow the api doc
|
||||
keyProps = {
|
||||
checkedKeys: keys,
|
||||
halfCheckedKeys: undefined,
|
||||
};
|
||||
} else if (typeof keys === 'object') {
|
||||
keyProps = {
|
||||
checkedKeys: keys.checked || undefined,
|
||||
halfCheckedKeys: keys.halfChecked || undefined,
|
||||
};
|
||||
} else {
|
||||
warning(false, '`checkedKeys` is not an array or an object');
|
||||
return null;
|
||||
}
|
||||
|
||||
return keyProps;
|
||||
}
|
||||
|
||||
/**
|
||||
* If user use `autoExpandParent` we should get the list of parent node
|
||||
* @param keyList
|
||||
* @param keyEntities
|
||||
*/
|
||||
export function conductExpandParent(keyList: Key[], keyEntities: Record<Key, DataEntity>): Key[] {
|
||||
const expandedKeys = new Set<Key>();
|
||||
|
||||
function conductUp(key: Key) {
|
||||
if (expandedKeys.has(key)) return;
|
||||
|
||||
const entity = keyEntities[key];
|
||||
if (!entity) return;
|
||||
|
||||
expandedKeys.add(key);
|
||||
|
||||
const { parent, node } = entity;
|
||||
|
||||
if (node.disabled) return;
|
||||
|
||||
if (parent) {
|
||||
conductUp(parent.key);
|
||||
}
|
||||
}
|
||||
|
||||
(keyList || []).forEach(key => {
|
||||
conductUp(key);
|
||||
});
|
||||
|
||||
return [...expandedKeys];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns only the data- and aria- key/value pairs
|
||||
*/
|
||||
export function getDataAndAria(props: Partial<TreeProps | TreeNodeProps>) {
|
||||
const omitProps: Record<string, string> = {};
|
||||
Object.keys(props).forEach(key => {
|
||||
if (key.startsWith('data-') || key.startsWith('aria-')) {
|
||||
omitProps[key] = props[key];
|
||||
}
|
||||
});
|
||||
|
||||
return omitProps;
|
||||
}
|
|
@ -0,0 +1,252 @@
|
|||
import { warning } from '../../vc-util/warning';
|
||||
import type { Key, DataEntity, DataNode, GetCheckDisabled } from '../interface';
|
||||
|
||||
interface ConductReturnType {
|
||||
checkedKeys: Key[];
|
||||
halfCheckedKeys: Key[];
|
||||
}
|
||||
|
||||
function removeFromCheckedKeys(halfCheckedKeys: Set<Key>, checkedKeys: Set<Key>) {
|
||||
const filteredKeys = new Set<Key>();
|
||||
halfCheckedKeys.forEach(key => {
|
||||
if (!checkedKeys.has(key)) {
|
||||
filteredKeys.add(key);
|
||||
}
|
||||
});
|
||||
return filteredKeys;
|
||||
}
|
||||
|
||||
export function isCheckDisabled(node: DataNode) {
|
||||
const { disabled, disableCheckbox, checkable } = (node || {}) as DataNode;
|
||||
return !!(disabled || disableCheckbox) || checkable === false;
|
||||
}
|
||||
|
||||
// Fill miss keys
|
||||
function fillConductCheck(
|
||||
keys: Set<Key>,
|
||||
levelEntities: Map<number, Set<DataEntity>>,
|
||||
maxLevel: number,
|
||||
syntheticGetCheckDisabled: GetCheckDisabled<DataNode>,
|
||||
): ConductReturnType {
|
||||
const checkedKeys = new Set<Key>(keys);
|
||||
const halfCheckedKeys = new Set<Key>();
|
||||
|
||||
// Add checked keys top to bottom
|
||||
for (let level = 0; level <= maxLevel; level += 1) {
|
||||
const entities = levelEntities.get(level) || new Set();
|
||||
entities.forEach(entity => {
|
||||
const { key, node, children = [] } = entity;
|
||||
|
||||
if (checkedKeys.has(key) && !syntheticGetCheckDisabled(node)) {
|
||||
children
|
||||
.filter(childEntity => !syntheticGetCheckDisabled(childEntity.node))
|
||||
.forEach(childEntity => {
|
||||
checkedKeys.add(childEntity.key);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add checked keys from bottom to top
|
||||
const visitedKeys = new Set<Key>();
|
||||
for (let level = maxLevel; level >= 0; level -= 1) {
|
||||
const entities = levelEntities.get(level) || new Set();
|
||||
entities.forEach(entity => {
|
||||
const { parent, node } = entity;
|
||||
|
||||
// Skip if no need to check
|
||||
if (syntheticGetCheckDisabled(node) || !entity.parent || visitedKeys.has(entity.parent.key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if parent is disabled
|
||||
if (syntheticGetCheckDisabled(entity.parent.node)) {
|
||||
visitedKeys.add(parent.key);
|
||||
return;
|
||||
}
|
||||
|
||||
let allChecked = true;
|
||||
let partialChecked = false;
|
||||
|
||||
(parent.children || [])
|
||||
.filter(childEntity => !syntheticGetCheckDisabled(childEntity.node))
|
||||
.forEach(({ key }) => {
|
||||
const checked = checkedKeys.has(key);
|
||||
if (allChecked && !checked) {
|
||||
allChecked = false;
|
||||
}
|
||||
if (!partialChecked && (checked || halfCheckedKeys.has(key))) {
|
||||
partialChecked = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (allChecked) {
|
||||
checkedKeys.add(parent.key);
|
||||
}
|
||||
if (partialChecked) {
|
||||
halfCheckedKeys.add(parent.key);
|
||||
}
|
||||
|
||||
visitedKeys.add(parent.key);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
checkedKeys: Array.from(checkedKeys),
|
||||
halfCheckedKeys: Array.from(removeFromCheckedKeys(halfCheckedKeys, checkedKeys)),
|
||||
};
|
||||
}
|
||||
|
||||
// Remove useless key
|
||||
function cleanConductCheck(
|
||||
keys: Set<Key>,
|
||||
halfKeys: Key[],
|
||||
levelEntities: Map<number, Set<DataEntity>>,
|
||||
maxLevel: number,
|
||||
syntheticGetCheckDisabled: GetCheckDisabled<DataNode>,
|
||||
): ConductReturnType {
|
||||
const checkedKeys = new Set<Key>(keys);
|
||||
let halfCheckedKeys = new Set<Key>(halfKeys);
|
||||
|
||||
// Remove checked keys from top to bottom
|
||||
for (let level = 0; level <= maxLevel; level += 1) {
|
||||
const entities = levelEntities.get(level) || new Set();
|
||||
entities.forEach(entity => {
|
||||
const { key, node, children = [] } = entity;
|
||||
|
||||
if (!checkedKeys.has(key) && !halfCheckedKeys.has(key) && !syntheticGetCheckDisabled(node)) {
|
||||
children
|
||||
.filter(childEntity => !syntheticGetCheckDisabled(childEntity.node))
|
||||
.forEach(childEntity => {
|
||||
checkedKeys.delete(childEntity.key);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Remove checked keys form bottom to top
|
||||
halfCheckedKeys = new Set<Key>();
|
||||
const visitedKeys = new Set<Key>();
|
||||
for (let level = maxLevel; level >= 0; level -= 1) {
|
||||
const entities = levelEntities.get(level) || new Set();
|
||||
|
||||
entities.forEach(entity => {
|
||||
const { parent, node } = entity;
|
||||
|
||||
// Skip if no need to check
|
||||
if (syntheticGetCheckDisabled(node) || !entity.parent || visitedKeys.has(entity.parent.key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if parent is disabled
|
||||
if (syntheticGetCheckDisabled(entity.parent.node)) {
|
||||
visitedKeys.add(parent.key);
|
||||
return;
|
||||
}
|
||||
|
||||
let allChecked = true;
|
||||
let partialChecked = false;
|
||||
|
||||
(parent.children || [])
|
||||
.filter(childEntity => !syntheticGetCheckDisabled(childEntity.node))
|
||||
.forEach(({ key }) => {
|
||||
const checked = checkedKeys.has(key);
|
||||
if (allChecked && !checked) {
|
||||
allChecked = false;
|
||||
}
|
||||
if (!partialChecked && (checked || halfCheckedKeys.has(key))) {
|
||||
partialChecked = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!allChecked) {
|
||||
checkedKeys.delete(parent.key);
|
||||
}
|
||||
if (partialChecked) {
|
||||
halfCheckedKeys.add(parent.key);
|
||||
}
|
||||
|
||||
visitedKeys.add(parent.key);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
checkedKeys: Array.from(checkedKeys),
|
||||
halfCheckedKeys: Array.from(removeFromCheckedKeys(halfCheckedKeys, checkedKeys)),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Conduct with keys.
|
||||
* @param keyList current key list
|
||||
* @param keyEntities key - dataEntity map
|
||||
* @param mode `fill` to fill missing key, `clean` to remove useless key
|
||||
*/
|
||||
export function conductCheck(
|
||||
keyList: Key[],
|
||||
checked: true | { checked: false; halfCheckedKeys: Key[] },
|
||||
keyEntities: Record<Key, DataEntity>,
|
||||
getCheckDisabled?: GetCheckDisabled<DataNode>,
|
||||
): ConductReturnType {
|
||||
const warningMissKeys: Key[] = [];
|
||||
|
||||
let syntheticGetCheckDisabled: GetCheckDisabled<DataNode>;
|
||||
if (getCheckDisabled) {
|
||||
syntheticGetCheckDisabled = getCheckDisabled;
|
||||
} else {
|
||||
syntheticGetCheckDisabled = isCheckDisabled;
|
||||
}
|
||||
|
||||
// We only handle exist keys
|
||||
const keys = new Set<Key>(
|
||||
keyList.filter(key => {
|
||||
const hasEntity = !!keyEntities[key];
|
||||
if (!hasEntity) {
|
||||
warningMissKeys.push(key);
|
||||
}
|
||||
|
||||
return hasEntity;
|
||||
}),
|
||||
);
|
||||
const levelEntities = new Map<number, Set<DataEntity>>();
|
||||
let maxLevel = 0;
|
||||
|
||||
// Convert entities by level for calculation
|
||||
Object.keys(keyEntities).forEach(key => {
|
||||
const entity = keyEntities[key];
|
||||
const { level } = entity;
|
||||
|
||||
let levelSet: Set<DataEntity> = levelEntities.get(level);
|
||||
if (!levelSet) {
|
||||
levelSet = new Set();
|
||||
levelEntities.set(level, levelSet);
|
||||
}
|
||||
|
||||
levelSet.add(entity);
|
||||
|
||||
maxLevel = Math.max(maxLevel, level);
|
||||
});
|
||||
|
||||
warning(
|
||||
!warningMissKeys.length,
|
||||
`Tree missing follow keys: ${warningMissKeys
|
||||
.slice(0, 100)
|
||||
.map(key => `'${key}'`)
|
||||
.join(', ')}`,
|
||||
);
|
||||
|
||||
let result: ConductReturnType;
|
||||
if (checked === true) {
|
||||
result = fillConductCheck(keys, levelEntities, maxLevel, syntheticGetCheckDisabled);
|
||||
} else {
|
||||
result = cleanConductCheck(
|
||||
keys,
|
||||
checked.halfCheckedKeys,
|
||||
levelEntities,
|
||||
maxLevel,
|
||||
syntheticGetCheckDisabled,
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
import type { Key, FlattenNode } from '../interface';
|
||||
|
||||
export function findExpandedKeys(prev: Key[] = [], next: Key[] = []) {
|
||||
const prevLen = prev.length;
|
||||
const nextLen = next.length;
|
||||
|
||||
if (Math.abs(prevLen - nextLen) !== 1) {
|
||||
return { add: false, key: null };
|
||||
}
|
||||
|
||||
function find(shorter: Key[], longer: Key[]) {
|
||||
const cache: Map<Key, boolean> = new Map();
|
||||
shorter.forEach(key => {
|
||||
cache.set(key, true);
|
||||
});
|
||||
|
||||
const keys = longer.filter(key => !cache.has(key));
|
||||
|
||||
return keys.length === 1 ? keys[0] : null;
|
||||
}
|
||||
|
||||
if (prevLen < nextLen) {
|
||||
return {
|
||||
add: true,
|
||||
key: find(prev, next),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
add: false,
|
||||
key: find(next, prev),
|
||||
};
|
||||
}
|
||||
|
||||
export function getExpandRange(shorter: FlattenNode[], longer: FlattenNode[], key: Key) {
|
||||
const shorterStartIndex = shorter.findIndex(({ data }) => data.key === key);
|
||||
const shorterEndNode = shorter[shorterStartIndex + 1];
|
||||
const longerStartIndex = longer.findIndex(({ data }) => data.key === key);
|
||||
|
||||
if (shorterEndNode) {
|
||||
const longerEndIndex = longer.findIndex(({ data }) => data.key === shorterEndNode.data.key);
|
||||
return longer.slice(longerStartIndex + 1, longerEndIndex);
|
||||
}
|
||||
return longer.slice(longerStartIndex + 1);
|
||||
}
|
|
@ -0,0 +1,411 @@
|
|||
import type {
|
||||
DataNode,
|
||||
FlattenNode,
|
||||
NodeElement,
|
||||
DataEntity,
|
||||
Key,
|
||||
EventDataNode,
|
||||
GetKey,
|
||||
FieldNames,
|
||||
} from '../interface';
|
||||
import { getPosition, isTreeNode } from '../util';
|
||||
import { warning } from '../../vc-util/warning';
|
||||
import Omit from 'omit.js';
|
||||
import type { VNodeChild } from 'vue';
|
||||
import type { TreeNodeProps } from '../props';
|
||||
|
||||
export function getKey(key: Key, pos: string) {
|
||||
if (key !== null && key !== undefined) {
|
||||
return key;
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
export function fillFieldNames(fieldNames?: FieldNames) {
|
||||
const { title, key, children } = fieldNames || {};
|
||||
|
||||
return {
|
||||
title: title || 'title',
|
||||
key: key || 'key',
|
||||
children: children || 'children',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Warning if TreeNode do not provides key
|
||||
*/
|
||||
export function warningWithoutKey(treeData: DataNode[], fieldNames: FieldNames) {
|
||||
const keys: Map<string, boolean> = new Map();
|
||||
|
||||
function dig(list: DataNode[], path = '') {
|
||||
(list || []).forEach(treeNode => {
|
||||
const key = treeNode[fieldNames.key];
|
||||
const children = treeNode[fieldNames.children];
|
||||
warning(
|
||||
key !== null && key !== undefined,
|
||||
`Tree node must have a certain key: [${path}${key}]`,
|
||||
);
|
||||
|
||||
const recordKey = String(key);
|
||||
warning(
|
||||
!keys.has(recordKey) || key === null || key === undefined,
|
||||
`Same 'key' exist in the Tree: ${recordKey}`,
|
||||
);
|
||||
keys.set(recordKey, true);
|
||||
|
||||
dig(children, `${path}${recordKey} > `);
|
||||
});
|
||||
}
|
||||
|
||||
dig(treeData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert `children` of Tree into `treeData` structure.
|
||||
*/
|
||||
export function convertTreeToData(rootNodes: VNodeChild): DataNode[] {
|
||||
function dig(node: VNodeChild): DataNode[] {
|
||||
const treeNodes = node as NodeElement[];
|
||||
return treeNodes
|
||||
.map(treeNode => {
|
||||
// Filter invalidate node
|
||||
if (!isTreeNode(treeNode)) {
|
||||
warning(!treeNode, 'Tree/TreeNode can only accept TreeNode as children.');
|
||||
return null;
|
||||
}
|
||||
|
||||
const key = treeNode.key as string | number;
|
||||
const { children, ...rest } = treeNode.props;
|
||||
|
||||
const dataNode: DataNode = {
|
||||
...rest,
|
||||
key,
|
||||
};
|
||||
|
||||
const parsedChildren = dig(children);
|
||||
if (parsedChildren.length) {
|
||||
dataNode.children = parsedChildren;
|
||||
}
|
||||
|
||||
return dataNode;
|
||||
})
|
||||
.filter((dataNode: DataNode) => dataNode);
|
||||
}
|
||||
|
||||
return dig(rootNodes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flat nest tree data into flatten list. This is used for virtual list render.
|
||||
* @param treeNodeList Origin data node list
|
||||
* @param expandedKeys
|
||||
* need expanded keys, provides `true` means all expanded (used in `rc-tree-select`).
|
||||
*/
|
||||
export function flattenTreeData(
|
||||
treeNodeList: DataNode[],
|
||||
expandedKeys: Key[] | true,
|
||||
fieldNames: FieldNames,
|
||||
): FlattenNode[] {
|
||||
const { title: fieldTitle, key: fieldKey, children: fieldChildren } = fillFieldNames(fieldNames);
|
||||
|
||||
const expandedKeySet = new Set(expandedKeys === true ? [] : expandedKeys);
|
||||
const flattenList: FlattenNode[] = [];
|
||||
|
||||
function dig(list: DataNode[], parent: FlattenNode = null): FlattenNode[] {
|
||||
return list.map((treeNode, index) => {
|
||||
const pos: string = getPosition(parent ? parent.pos : '0', index);
|
||||
const mergedKey = getKey(treeNode[fieldKey], pos);
|
||||
|
||||
// Add FlattenDataNode into list
|
||||
const flattenNode: FlattenNode = {
|
||||
...Omit(treeNode, [fieldTitle, fieldKey, fieldChildren] as any),
|
||||
title: treeNode[fieldTitle],
|
||||
key: mergedKey,
|
||||
parent,
|
||||
pos,
|
||||
children: null,
|
||||
data: treeNode,
|
||||
isStart: [...(parent ? parent.isStart : []), index === 0],
|
||||
isEnd: [...(parent ? parent.isEnd : []), index === list.length - 1],
|
||||
};
|
||||
|
||||
flattenList.push(flattenNode);
|
||||
|
||||
// Loop treeNode children
|
||||
if (expandedKeys === true || expandedKeySet.has(mergedKey)) {
|
||||
flattenNode.children = dig(treeNode[fieldChildren] || [], flattenNode);
|
||||
} else {
|
||||
flattenNode.children = [];
|
||||
}
|
||||
|
||||
return flattenNode;
|
||||
});
|
||||
}
|
||||
|
||||
dig(treeNodeList);
|
||||
|
||||
return flattenList;
|
||||
}
|
||||
|
||||
type ExternalGetKey = GetKey<DataNode> | string;
|
||||
|
||||
interface TraverseDataNodesConfig {
|
||||
childrenPropName?: string;
|
||||
externalGetKey?: ExternalGetKey;
|
||||
fieldNames?: FieldNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverse all the data by `treeData`.
|
||||
* Please not use it out of the `rc-tree` since we may refactor this code.
|
||||
*/
|
||||
export function traverseDataNodes(
|
||||
dataNodes: DataNode[],
|
||||
callback: (data: {
|
||||
node: DataNode;
|
||||
index: number;
|
||||
pos: string;
|
||||
key: Key;
|
||||
parentPos: string | number;
|
||||
level: number;
|
||||
}) => void,
|
||||
// To avoid too many params, let use config instead of origin param
|
||||
config?: TraverseDataNodesConfig | string,
|
||||
) {
|
||||
let mergedConfig: TraverseDataNodesConfig = {};
|
||||
if (typeof config === 'object') {
|
||||
mergedConfig = config;
|
||||
} else {
|
||||
mergedConfig = { externalGetKey: config };
|
||||
}
|
||||
mergedConfig = mergedConfig || {};
|
||||
|
||||
// Init config
|
||||
const { childrenPropName, externalGetKey, fieldNames } = mergedConfig;
|
||||
|
||||
const { key: fieldKey, children: fieldChildren } = fillFieldNames(fieldNames);
|
||||
|
||||
const mergeChildrenPropName = childrenPropName || fieldChildren;
|
||||
|
||||
// Get keys
|
||||
let syntheticGetKey: (node: DataNode, pos?: string) => Key;
|
||||
if (externalGetKey) {
|
||||
if (typeof externalGetKey === 'string') {
|
||||
syntheticGetKey = (node: DataNode) => (node as any)[externalGetKey as string];
|
||||
} else if (typeof externalGetKey === 'function') {
|
||||
syntheticGetKey = (node: DataNode) => (externalGetKey as GetKey<DataNode>)(node);
|
||||
}
|
||||
} else {
|
||||
syntheticGetKey = (node, pos) => getKey(node[fieldKey], pos);
|
||||
}
|
||||
|
||||
// Process
|
||||
function processNode(
|
||||
node: DataNode,
|
||||
index?: number,
|
||||
parent?: { node: DataNode; pos: string; level: number },
|
||||
) {
|
||||
const children = node ? node[mergeChildrenPropName] : dataNodes;
|
||||
const pos = node ? getPosition(parent.pos, index) : '0';
|
||||
|
||||
// Process node if is not root
|
||||
if (node) {
|
||||
const key: Key = syntheticGetKey(node, pos);
|
||||
const data = {
|
||||
node,
|
||||
index,
|
||||
pos,
|
||||
key,
|
||||
parentPos: parent.node ? parent.pos : null,
|
||||
level: parent.level + 1,
|
||||
};
|
||||
|
||||
callback(data);
|
||||
}
|
||||
|
||||
// Process children node
|
||||
if (children) {
|
||||
children.forEach((subNode, subIndex) => {
|
||||
processNode(subNode, subIndex, {
|
||||
node,
|
||||
pos,
|
||||
level: parent ? parent.level + 1 : -1,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
processNode(null);
|
||||
}
|
||||
|
||||
interface Wrapper {
|
||||
posEntities: Record<string, DataEntity>;
|
||||
keyEntities: Record<Key, DataEntity>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert `treeData` into entity records.
|
||||
*/
|
||||
export function convertDataToEntities(
|
||||
dataNodes: DataNode[],
|
||||
{
|
||||
initWrapper,
|
||||
processEntity,
|
||||
onProcessFinished,
|
||||
externalGetKey,
|
||||
childrenPropName,
|
||||
fieldNames,
|
||||
}: {
|
||||
initWrapper?: (wrapper: Wrapper) => Wrapper;
|
||||
processEntity?: (entity: DataEntity, wrapper: Wrapper) => void;
|
||||
onProcessFinished?: (wrapper: Wrapper) => void;
|
||||
externalGetKey?: ExternalGetKey;
|
||||
childrenPropName?: string;
|
||||
fieldNames?: FieldNames;
|
||||
} = {},
|
||||
/** @deprecated Use `config.externalGetKey` instead */
|
||||
legacyExternalGetKey?: ExternalGetKey,
|
||||
) {
|
||||
// Init config
|
||||
const mergedExternalGetKey = externalGetKey || legacyExternalGetKey;
|
||||
|
||||
const posEntities = {};
|
||||
const keyEntities = {};
|
||||
let wrapper = {
|
||||
posEntities,
|
||||
keyEntities,
|
||||
};
|
||||
|
||||
if (initWrapper) {
|
||||
wrapper = initWrapper(wrapper) || wrapper;
|
||||
}
|
||||
|
||||
traverseDataNodes(
|
||||
dataNodes,
|
||||
item => {
|
||||
const { node, index, pos, key, parentPos, level } = item;
|
||||
const entity: DataEntity = { node, index, key, pos, level };
|
||||
|
||||
const mergedKey = getKey(key, pos);
|
||||
|
||||
posEntities[pos] = entity;
|
||||
keyEntities[mergedKey] = entity;
|
||||
|
||||
// Fill children
|
||||
entity.parent = posEntities[parentPos];
|
||||
if (entity.parent) {
|
||||
entity.parent.children = entity.parent.children || [];
|
||||
entity.parent.children.push(entity);
|
||||
}
|
||||
|
||||
if (processEntity) {
|
||||
processEntity(entity, wrapper);
|
||||
}
|
||||
},
|
||||
{ externalGetKey: mergedExternalGetKey, childrenPropName, fieldNames },
|
||||
);
|
||||
|
||||
if (onProcessFinished) {
|
||||
onProcessFinished(wrapper);
|
||||
}
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
export interface TreeNodeRequiredProps {
|
||||
expandedKeys: Key[];
|
||||
selectedKeys: Key[];
|
||||
loadedKeys: Key[];
|
||||
loadingKeys: Key[];
|
||||
checkedKeys: Key[];
|
||||
halfCheckedKeys: Key[];
|
||||
dragOverNodeKey: Key;
|
||||
dropPosition: number;
|
||||
keyEntities: Record<Key, DataEntity>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get TreeNode props with Tree props.
|
||||
*/
|
||||
export function getTreeNodeProps(
|
||||
key: Key,
|
||||
{
|
||||
expandedKeys,
|
||||
selectedKeys,
|
||||
loadedKeys,
|
||||
loadingKeys,
|
||||
checkedKeys,
|
||||
halfCheckedKeys,
|
||||
dragOverNodeKey,
|
||||
dropPosition,
|
||||
keyEntities,
|
||||
}: TreeNodeRequiredProps,
|
||||
) {
|
||||
const entity = keyEntities[key];
|
||||
|
||||
const treeNodeProps = {
|
||||
eventKey: key,
|
||||
expanded: expandedKeys.indexOf(key) !== -1,
|
||||
selected: selectedKeys.indexOf(key) !== -1,
|
||||
loaded: loadedKeys.indexOf(key) !== -1,
|
||||
loading: loadingKeys.indexOf(key) !== -1,
|
||||
checked: checkedKeys.indexOf(key) !== -1,
|
||||
halfChecked: halfCheckedKeys.indexOf(key) !== -1,
|
||||
pos: String(entity ? entity.pos : ''),
|
||||
|
||||
// [Legacy] Drag props
|
||||
// Since the interaction of drag is changed, the semantic of the props are
|
||||
// not accuracy, I think it should be finally removed
|
||||
dragOver: dragOverNodeKey === key && dropPosition === 0,
|
||||
dragOverGapTop: dragOverNodeKey === key && dropPosition === -1,
|
||||
dragOverGapBottom: dragOverNodeKey === key && dropPosition === 1,
|
||||
};
|
||||
|
||||
return treeNodeProps;
|
||||
}
|
||||
|
||||
export function convertNodePropsToEventData(props: TreeNodeProps): EventDataNode {
|
||||
const {
|
||||
data,
|
||||
expanded,
|
||||
selected,
|
||||
checked,
|
||||
loaded,
|
||||
loading,
|
||||
halfChecked,
|
||||
dragOver,
|
||||
dragOverGapTop,
|
||||
dragOverGapBottom,
|
||||
pos,
|
||||
active,
|
||||
} = props;
|
||||
|
||||
const eventData = {
|
||||
...data,
|
||||
expanded,
|
||||
selected,
|
||||
checked,
|
||||
loaded,
|
||||
loading,
|
||||
halfChecked,
|
||||
dragOver,
|
||||
dragOverGapTop,
|
||||
dragOverGapBottom,
|
||||
pos,
|
||||
active,
|
||||
};
|
||||
|
||||
if (!('props' in eventData)) {
|
||||
Object.defineProperty(eventData, 'props', {
|
||||
get() {
|
||||
warning(
|
||||
false,
|
||||
'Second param return from event is node data instead of TreeNode instance. Please read value directly instead of reading from `props`.',
|
||||
);
|
||||
return props;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return eventData;
|
||||
}
|
|
@ -30,6 +30,20 @@ const ScrollStyle: CSSProperties = {
|
|||
overflowAnchor: 'none',
|
||||
};
|
||||
|
||||
export type ScrollAlign = 'top' | 'bottom' | 'auto';
|
||||
export type ScrollConfig =
|
||||
| {
|
||||
index: number;
|
||||
align?: ScrollAlign;
|
||||
offset?: number;
|
||||
}
|
||||
| {
|
||||
key: Key;
|
||||
align?: ScrollAlign;
|
||||
offset?: number;
|
||||
};
|
||||
export type ScrollTo = (arg: number | ScrollConfig) => void;
|
||||
|
||||
function renderChildren<T>(
|
||||
list: T[],
|
||||
startIndex: number,
|
||||
|
@ -68,7 +82,7 @@ const List = defineComponent({
|
|||
/** If not match virtual scroll condition, Set List still use height of container. */
|
||||
fullHeight: PropTypes.looseBool,
|
||||
itemKey: {
|
||||
type: [String, Number, Function] as PropType<Key | ((item: object) => Key)>,
|
||||
type: [String, Number, Function] as PropType<Key | ((item: Record<string, any>) => Key)>,
|
||||
required: true,
|
||||
},
|
||||
component: {
|
||||
|
@ -81,7 +95,7 @@ const List = defineComponent({
|
|||
onMousedown: PropTypes.func,
|
||||
onMouseenter: PropTypes.func,
|
||||
},
|
||||
setup(props) {
|
||||
setup(props, { expose }) {
|
||||
// ================================= MISC =================================
|
||||
const useVirtual = computed(() => {
|
||||
const { height, itemHeight, virtual } = props;
|
||||
|
@ -323,6 +337,10 @@ const List = defineComponent({
|
|||
},
|
||||
);
|
||||
|
||||
expose({
|
||||
scrollTo,
|
||||
});
|
||||
|
||||
const componentStyle = computed(() => {
|
||||
let cs: CSSProperties | null = null;
|
||||
if (props.height) {
|
||||
|
@ -343,7 +361,6 @@ const List = defineComponent({
|
|||
state,
|
||||
mergedData,
|
||||
componentStyle,
|
||||
scrollTo,
|
||||
onFallbackScroll,
|
||||
onScrollBar,
|
||||
componentRef,
|
||||
|
|
2
v2-doc
2
v2-doc
|
@ -1 +1 @@
|
|||
Subproject commit 7a7b52df8b3b69d8b1a8b8dcd96e1b0f7bb3f8c9
|
||||
Subproject commit d571ad4bf772cfc372511dc1dedf07981dc56ae8
|
Loading…
Reference in New Issue