|
|
|
import { useInjectTreeContext } from './contextTypes';
|
|
|
|
import { getDataAndAria } from './util';
|
|
|
|
import Indent from './Indent';
|
|
|
|
import { convertNodePropsToEventData } from './utils/treeUtil';
|
|
|
|
import {
|
|
|
|
computed,
|
|
|
|
defineComponent,
|
|
|
|
getCurrentInstance,
|
|
|
|
onMounted,
|
|
|
|
onUpdated,
|
|
|
|
reactive,
|
|
|
|
ref,
|
|
|
|
} from 'vue';
|
|
|
|
import { treeNodeProps } from './props';
|
|
|
|
import classNames from '../_util/classNames';
|
|
|
|
import { warning } from '../vc-util/warning';
|
|
|
|
import type { DragNodeEvent, Key } from './interface';
|
|
|
|
import pick from 'lodash-es/pick';
|
|
|
|
|
|
|
|
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, slots, expose }) {
|
|
|
|
warning(
|
|
|
|
!('slots' in props.data),
|
|
|
|
`treeData slots is deprecated, please use ${Object.keys(props.data.slots || {}).map(
|
|
|
|
key => '`v-slot:' + key + '` ',
|
|
|
|
)}instead`,
|
|
|
|
);
|
|
|
|
const dragNodeHighlight = ref(false);
|
|
|
|
const context = useInjectTreeContext();
|
|
|
|
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 renderArgsData = computed(() => {
|
|
|
|
return {
|
|
|
|
...pick(props, [
|
|
|
|
'active',
|
|
|
|
'checkable',
|
|
|
|
'checked',
|
|
|
|
'disableCheckbox',
|
|
|
|
'disabled',
|
|
|
|
'expanded',
|
|
|
|
'isLeaf',
|
|
|
|
'loading',
|
|
|
|
'selectable',
|
|
|
|
'selected',
|
|
|
|
'halfChecked',
|
|
|
|
]),
|
|
|
|
...props.data,
|
|
|
|
dataRef: props.data,
|
|
|
|
data: props.data,
|
|
|
|
isLeaf: isLeaf.value,
|
|
|
|
};
|
|
|
|
});
|
|
|
|
const instance = getCurrentInstance();
|
|
|
|
const eventData = computed(() => {
|
|
|
|
return convertNodePropsToEventData(props);
|
|
|
|
});
|
|
|
|
const dragNodeEvent: DragNodeEvent = reactive({
|
|
|
|
eventData,
|
|
|
|
eventKey: computed(() => props.eventKey),
|
|
|
|
selectHandle,
|
|
|
|
pos: computed(() => props.pos),
|
|
|
|
key: instance.vnode.key as Key,
|
|
|
|
});
|
|
|
|
expose(dragNodeEvent);
|
|
|
|
const onSelectorDoubleClick = (e: MouseEvent) => {
|
|
|
|
const { onNodeDoubleClick } = context.value;
|
|
|
|
onNodeDoubleClick(e, eventData.value);
|
|
|
|
};
|
|
|
|
|
|
|
|
const onSelect = (e: MouseEvent) => {
|
|
|
|
if (isDisabled.value) return;
|
|
|
|
|
|
|
|
const { onNodeSelect } = context.value;
|
|
|
|
e.preventDefault();
|
|
|
|
onNodeSelect(e, eventData.value);
|
|
|
|
};
|
|
|
|
|
|
|
|
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, eventData.value, targetChecked);
|
|
|
|
};
|
|
|
|
|
|
|
|
const onSelectorClick = (e: MouseEvent) => {
|
|
|
|
// Click trigger before select/check operation
|
|
|
|
const { onNodeClick } = context.value;
|
|
|
|
onNodeClick(e, eventData.value);
|
|
|
|
|
|
|
|
if (isSelectable.value) {
|
|
|
|
onSelect(e);
|
|
|
|
} else {
|
|
|
|
onCheck(e);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const onMouseEnter = (e: MouseEvent) => {
|
|
|
|
const { onNodeMouseEnter } = context.value;
|
|
|
|
onNodeMouseEnter(e, eventData.value);
|
|
|
|
};
|
|
|
|
|
|
|
|
const onMouseLeave = (e: MouseEvent) => {
|
|
|
|
const { onNodeMouseLeave } = context.value;
|
|
|
|
onNodeMouseLeave(e, eventData.value);
|
|
|
|
};
|
|
|
|
|
|
|
|
const onContextmenu = (e: MouseEvent) => {
|
|
|
|
const { onNodeContextMenu } = context.value;
|
|
|
|
onNodeContextMenu(e, eventData.value);
|
|
|
|
};
|
|
|
|
|
|
|
|
const onDragStart = (e: DragEvent) => {
|
|
|
|
const { onNodeDragStart } = context.value;
|
|
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
dragNodeHighlight.value = true;
|
|
|
|
onNodeDragStart(e, dragNodeEvent);
|
|
|
|
|
|
|
|
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, dragNodeEvent);
|
|
|
|
};
|
|
|
|
|
|
|
|
const onDragOver = (e: DragEvent) => {
|
|
|
|
const { onNodeDragOver } = context.value;
|
|
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
e.stopPropagation();
|
|
|
|
onNodeDragOver(e, dragNodeEvent);
|
|
|
|
};
|
|
|
|
|
|
|
|
const onDragLeave = (e: DragEvent) => {
|
|
|
|
const { onNodeDragLeave } = context.value;
|
|
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
onNodeDragLeave(e, dragNodeEvent);
|
|
|
|
};
|
|
|
|
|
|
|
|
const onDragEnd = (e: DragEvent) => {
|
|
|
|
const { onNodeDragEnd } = context.value;
|
|
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
dragNodeHighlight.value = false;
|
|
|
|
onNodeDragEnd(e, dragNodeEvent);
|
|
|
|
};
|
|
|
|
|
|
|
|
const onDrop = (e: DragEvent) => {
|
|
|
|
const { onNodeDrop } = context.value;
|
|
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
e.stopPropagation();
|
|
|
|
dragNodeHighlight.value = false;
|
|
|
|
onNodeDrop(e, dragNodeEvent);
|
|
|
|
};
|
|
|
|
|
|
|
|
// Disabled item still can be switch
|
|
|
|
const onExpand = e => {
|
|
|
|
const { onNodeExpand } = context.value;
|
|
|
|
if (props.loading) return;
|
|
|
|
onNodeExpand(e, eventData.value);
|
|
|
|
};
|
|
|
|
|
|
|
|
const renderSwitcherIconDom = () => {
|
|
|
|
const {
|
|
|
|
switcherIcon: switcherIconFromProps = slots.switcherIcon ||
|
|
|
|
context.value.slots?.[props.data?.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(renderArgsData.value);
|
|
|
|
}
|
|
|
|
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(eventData.value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
syncLoadData();
|
|
|
|
});
|
|
|
|
onUpdated(() => {
|
|
|
|
//syncLoadData();
|
|
|
|
});
|
|
|
|
|
|
|
|
// Switcher
|
|
|
|
const renderSwitcher = () => {
|
|
|
|
const { expanded } = props;
|
|
|
|
const { prefixCls } = context.value;
|
|
|
|
// if switcherIconDom is null, no render switcher span
|
|
|
|
const switcherIconDom = renderSwitcherIconDom();
|
|
|
|
if (isLeaf.value) {
|
|
|
|
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}`,
|
|
|
|
);
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<span
|
|
|
|
class={classNames(
|
|
|
|
`${prefixCls}-checkbox`,
|
|
|
|
checked && `${prefixCls}-checkbox-checked`,
|
|
|
|
!checked && halfChecked && `${prefixCls}-checkbox-indeterminate`,
|
|
|
|
(disabled || disableCheckbox) && `${prefixCls}-checkbox-disabled`,
|
|
|
|
)}
|
|
|
|
onClick={onCheck}
|
|
|
|
>
|
|
|
|
{context.value.customCheckable?.()}
|
|
|
|
</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 ||
|
|
|
|
// context.value.slots?.[props.data?.slots?.title] ||
|
|
|
|
// context.value.slots?.title,
|
|
|
|
selected,
|
|
|
|
icon = slots.icon,
|
|
|
|
loading,
|
|
|
|
data,
|
|
|
|
} = props;
|
|
|
|
const title =
|
|
|
|
slots.title ||
|
|
|
|
context.value.slots?.[props.data?.slots?.title] ||
|
|
|
|
context.value.slots?.title ||
|
|
|
|
props.title;
|
|
|
|
const {
|
|
|
|
prefixCls,
|
|
|
|
showIcon,
|
|
|
|
icon: treeIcon,
|
|
|
|
draggable,
|
|
|
|
loadData,
|
|
|
|
// slots: contextSlots,
|
|
|
|
} = 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 || context.value.slots?.[data?.slots?.icon] || treeIcon;
|
|
|
|
|
|
|
|
$icon = currentIcon ? (
|
|
|
|
<span class={classNames(`${prefixCls}-iconEle`, `${prefixCls}-icon__customize`)}>
|
|
|
|
{typeof currentIcon === 'function' ? currentIcon(renderArgsData.value) : currentIcon}
|
|
|
|
</span>
|
|
|
|
) : (
|
|
|
|
renderIcon()
|
|
|
|
);
|
|
|
|
} else if (loadData && loading) {
|
|
|
|
$icon = renderIcon();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Title
|
|
|
|
let titleNode: any;
|
|
|
|
if (typeof title === 'function') {
|
|
|
|
titleNode = title(renderArgsData.value);
|
|
|
|
// } else if (contextSlots.titleRender) {
|
|
|
|
// titleNode = contextSlots.titleRender(renderArgsData.value);
|
|
|
|
} else {
|
|
|
|
titleNode = title;
|
|
|
|
}
|
|
|
|
titleNode = titleNode === undefined ? defaultTitle : titleNode;
|
|
|
|
|
|
|
|
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.value) && `${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(eventData.value),
|
|
|
|
})}
|
|
|
|
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>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
},
|
|
|
|
});
|