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 ? ( {switcherIconDom} ) : null; } const switcherCls = classNames( `${prefixCls}-switcher`, `${prefixCls}-switcher_${expanded ? ICON_OPEN : ICON_CLOSE}`, ); return switcherIconDom !== false ? ( {switcherIconDom} ) : null; }; // Checkbox const renderCheckbox = () => { const { checked, halfChecked, disableCheckbox } = props; const { prefixCls } = context.value; const disabled = isDisabled.value; const checkable = isCheckable.value; if (!checkable) return null; return ( {context.value.customCheckable?.()} ); }; const renderIcon = () => { const { loading } = props; const { prefixCls } = context.value; return ( ); }; const renderDropIndicator = () => { const { disabled, eventKey } = props; const { draggable, dropLevelOffset, dropPosition, prefixCls, indent, dropIndicatorRender, dragOverNodeKey, direction, } = context.value; const mergedDraggable = draggable !== false; // allowDrop is calculated in Tree.tsx, there is no need for calc it here const showIndicator = !disabled && mergedDraggable && dragOverNodeKey === eventKey; return showIndicator ? dropIndicatorRender({ dropPosition, dropLevelOffset, indent, prefixCls, direction }) : null; }; // Icon + Title const renderSelector = () => { const { // title = slots.title || // 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 ? ( ) : ( 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 = {titleNode}; return ( {$icon} {$title} {renderDropIndicator()} ); }; return () => { const { eventKey, dragOver, dragOverGapTop, dragOverGapBottom, isLeaf, isStart, isEnd, expanded, selected, checked, halfChecked, loading, domRef, active, data, onMousemove, ...otherProps } = { ...props, ...attrs }; const { prefixCls, filterTreeNode, draggable, keyEntities, dropContainerKey, dropTargetKey } = context.value; const disabled = isDisabled.value; const dataOrAriaAttributeProps = getDataAndAria(otherProps); const { level } = keyEntities[eventKey] || {}; const isEndNode = isEnd[isEnd.length - 1]; const mergedDraggable = typeof draggable === 'function' ? draggable(data) : draggable; return (