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',
|
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>(
|
function renderChildren<T>(
|
||||||
list: T[],
|
list: T[],
|
||||||
startIndex: number,
|
startIndex: number,
|
||||||
|
@ -68,7 +82,7 @@ const List = defineComponent({
|
||||||
/** If not match virtual scroll condition, Set List still use height of container. */
|
/** If not match virtual scroll condition, Set List still use height of container. */
|
||||||
fullHeight: PropTypes.looseBool,
|
fullHeight: PropTypes.looseBool,
|
||||||
itemKey: {
|
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,
|
required: true,
|
||||||
},
|
},
|
||||||
component: {
|
component: {
|
||||||
|
@ -81,7 +95,7 @@ const List = defineComponent({
|
||||||
onMousedown: PropTypes.func,
|
onMousedown: PropTypes.func,
|
||||||
onMouseenter: PropTypes.func,
|
onMouseenter: PropTypes.func,
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props, { expose }) {
|
||||||
// ================================= MISC =================================
|
// ================================= MISC =================================
|
||||||
const useVirtual = computed(() => {
|
const useVirtual = computed(() => {
|
||||||
const { height, itemHeight, virtual } = props;
|
const { height, itemHeight, virtual } = props;
|
||||||
|
@ -323,6 +337,10 @@ const List = defineComponent({
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
expose({
|
||||||
|
scrollTo,
|
||||||
|
});
|
||||||
|
|
||||||
const componentStyle = computed(() => {
|
const componentStyle = computed(() => {
|
||||||
let cs: CSSProperties | null = null;
|
let cs: CSSProperties | null = null;
|
||||||
if (props.height) {
|
if (props.height) {
|
||||||
|
@ -343,7 +361,6 @@ const List = defineComponent({
|
||||||
state,
|
state,
|
||||||
mergedData,
|
mergedData,
|
||||||
componentStyle,
|
componentStyle,
|
||||||
scrollTo,
|
|
||||||
onFallbackScroll,
|
onFallbackScroll,
|
||||||
onScrollBar,
|
onScrollBar,
|
||||||
componentRef,
|
componentRef,
|
||||||
|
|
2
v2-doc
2
v2-doc
|
@ -1 +1 @@
|
||||||
Subproject commit 7a7b52df8b3b69d8b1a8b8dcd96e1b0f7bb3f8c9
|
Subproject commit d571ad4bf772cfc372511dc1dedf07981dc56ae8
|
Loading…
Reference in New Issue