ant-design-vue/components/vc-tree/src/TreeNode.jsx

579 lines
15 KiB
Vue

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,
[`${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;