459 lines
15 KiB
JavaScript
459 lines
15 KiB
JavaScript
import { computed, defineComponent, h, inject, onMounted, ref } from 'vue';
|
|
import { useMemo } from 'vooks';
|
|
import { happensIn, repeat } from 'seemly';
|
|
import { createDataKey } from "../../_utils/index.mjs";
|
|
import NTreeNodeSwitcher from "./TreeNodeSwitcher.mjs";
|
|
import NTreeNodeCheckbox from "./TreeNodeCheckbox.mjs";
|
|
import NTreeNodeContent from "./TreeNodeContent.mjs";
|
|
import { treeInjectionKey } from "./interface.mjs";
|
|
import { renderDropMark } from "./dnd.mjs";
|
|
import { isNodeDisabled } from "./utils.mjs";
|
|
const TreeNode = defineComponent({
|
|
name: 'TreeNode',
|
|
props: {
|
|
clsPrefix: {
|
|
type: String,
|
|
required: true
|
|
},
|
|
tmNode: {
|
|
type: Object,
|
|
required: true
|
|
}
|
|
},
|
|
setup(props) {
|
|
const NTree = inject(treeInjectionKey);
|
|
const {
|
|
droppingNodeParentRef,
|
|
droppingMouseNodeRef,
|
|
draggingNodeRef,
|
|
droppingPositionRef,
|
|
droppingOffsetLevelRef,
|
|
nodePropsRef,
|
|
indentRef,
|
|
blockLineRef,
|
|
checkboxPlacementRef,
|
|
checkOnClickRef,
|
|
disabledFieldRef,
|
|
showLineRef,
|
|
renderSwitcherIconRef,
|
|
overrideDefaultNodeClickBehaviorRef
|
|
} = NTree;
|
|
const checkboxDisabledRef = useMemo(() => !!props.tmNode.rawNode.checkboxDisabled);
|
|
const nodeIsDisabledRef = useMemo(() => {
|
|
return isNodeDisabled(props.tmNode, disabledFieldRef.value);
|
|
});
|
|
const disabledRef = useMemo(() => NTree.disabledRef.value || nodeIsDisabledRef.value);
|
|
const resolvedNodePropsRef = computed(() => {
|
|
const {
|
|
value: nodeProps
|
|
} = nodePropsRef;
|
|
if (!nodeProps) return undefined;
|
|
return nodeProps({
|
|
option: props.tmNode.rawNode
|
|
});
|
|
});
|
|
// used for drag and drop
|
|
const contentInstRef = ref(null);
|
|
// must be non-reactive
|
|
const contentElRef = {
|
|
value: null
|
|
};
|
|
onMounted(() => {
|
|
contentElRef.value = contentInstRef.value.$el;
|
|
});
|
|
function handleSwitcherClick() {
|
|
const callback = () => {
|
|
const {
|
|
tmNode
|
|
} = props;
|
|
if (!tmNode.isLeaf && !tmNode.shallowLoaded) {
|
|
if (!NTree.loadingKeysRef.value.has(tmNode.key)) {
|
|
NTree.loadingKeysRef.value.add(tmNode.key);
|
|
} else {
|
|
return;
|
|
}
|
|
const {
|
|
onLoadRef: {
|
|
value: onLoad
|
|
}
|
|
} = NTree;
|
|
if (onLoad) {
|
|
void onLoad(tmNode.rawNode).then(value => {
|
|
if (value !== false) {
|
|
NTree.handleSwitcherClick(tmNode);
|
|
}
|
|
}).finally(() => {
|
|
NTree.loadingKeysRef.value.delete(tmNode.key);
|
|
});
|
|
}
|
|
} else {
|
|
NTree.handleSwitcherClick(tmNode);
|
|
}
|
|
};
|
|
if (renderSwitcherIconRef.value) {
|
|
// if renderSwitcherIcon is set, icon dom may be altered before event
|
|
// bubbles to parent dom, so that target check fails. Call it in next
|
|
// event loop so that event bubble phase is finishes.
|
|
setTimeout(callback, 0);
|
|
} else {
|
|
callback();
|
|
}
|
|
}
|
|
const selectableRef = useMemo(() => !nodeIsDisabledRef.value && NTree.selectableRef.value && (NTree.internalTreeSelect ? NTree.mergedCheckStrategyRef.value !== 'child' || NTree.multipleRef.value && NTree.cascadeRef.value || props.tmNode.isLeaf : true));
|
|
const checkableRef = useMemo(() => NTree.checkableRef.value && (NTree.cascadeRef.value || NTree.mergedCheckStrategyRef.value !== 'child' || props.tmNode.isLeaf));
|
|
const checkedRef = useMemo(() => NTree.displayedCheckedKeysRef.value.includes(props.tmNode.key));
|
|
const mergedCheckOnClickRef = useMemo(() => {
|
|
const {
|
|
value: checkable
|
|
} = checkableRef;
|
|
if (!checkable) return false;
|
|
const {
|
|
value: checkOnClick
|
|
} = checkOnClickRef;
|
|
const {
|
|
tmNode
|
|
} = props;
|
|
if (typeof checkOnClick === 'boolean') {
|
|
return !tmNode.disabled && checkOnClick;
|
|
}
|
|
return checkOnClick(props.tmNode.rawNode);
|
|
});
|
|
function _handleClick(e) {
|
|
const {
|
|
value: expandOnClick
|
|
} = NTree.expandOnClickRef;
|
|
const {
|
|
value: selectable
|
|
} = selectableRef;
|
|
const {
|
|
value: mergedCheckOnClick
|
|
} = mergedCheckOnClickRef;
|
|
if (!selectable && !expandOnClick && !mergedCheckOnClick) return;
|
|
if (happensIn(e, 'checkbox') || happensIn(e, 'switcher')) return;
|
|
const {
|
|
tmNode
|
|
} = props;
|
|
if (selectable) {
|
|
NTree.handleSelect(tmNode);
|
|
}
|
|
if (expandOnClick && !tmNode.isLeaf) {
|
|
handleSwitcherClick();
|
|
}
|
|
if (mergedCheckOnClick) {
|
|
handleCheck(!checkedRef.value);
|
|
}
|
|
}
|
|
function handleNodeClick(e) {
|
|
var _a, _b;
|
|
if (happensIn(e, 'checkbox') || happensIn(e, 'switcher')) return;
|
|
if (!disabledRef.value) {
|
|
const overrideDefaultNodeClickBehavior = overrideDefaultNodeClickBehaviorRef.value;
|
|
let shouldOverride = false;
|
|
if (overrideDefaultNodeClickBehavior) {
|
|
switch (overrideDefaultNodeClickBehavior({
|
|
option: props.tmNode.rawNode
|
|
})) {
|
|
case 'toggleCheck':
|
|
shouldOverride = true;
|
|
handleCheck(!checkedRef.value);
|
|
break;
|
|
case 'toggleSelect':
|
|
shouldOverride = true;
|
|
NTree.handleSelect(props.tmNode);
|
|
break;
|
|
case 'toggleExpand':
|
|
shouldOverride = true;
|
|
handleSwitcherClick();
|
|
shouldOverride = true;
|
|
break;
|
|
case 'none':
|
|
shouldOverride = true;
|
|
shouldOverride = true;
|
|
return;
|
|
case 'default':
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
if (!shouldOverride) {
|
|
_handleClick(e);
|
|
}
|
|
}
|
|
(_b = (_a = resolvedNodePropsRef.value) === null || _a === void 0 ? void 0 : _a.onClick) === null || _b === void 0 ? void 0 : _b.call(_a, e);
|
|
}
|
|
function handleContentClick(e) {
|
|
if (blockLineRef.value) return;
|
|
handleNodeClick(e);
|
|
}
|
|
function handleLineClick(e) {
|
|
if (!blockLineRef.value) return;
|
|
handleNodeClick(e);
|
|
}
|
|
function handleCheck(checked) {
|
|
NTree.handleCheck(props.tmNode, checked);
|
|
}
|
|
// Dnd
|
|
function handleDragStart(e) {
|
|
NTree.handleDragStart({
|
|
event: e,
|
|
node: props.tmNode
|
|
});
|
|
}
|
|
function handleDragEnter(e) {
|
|
if (e.currentTarget !== e.target) {
|
|
return;
|
|
}
|
|
NTree.handleDragEnter({
|
|
event: e,
|
|
node: props.tmNode
|
|
});
|
|
}
|
|
function handleDragOver(e) {
|
|
e.preventDefault(); // if not prevent, drop event won't be fired...
|
|
NTree.handleDragOver({
|
|
event: e,
|
|
node: props.tmNode
|
|
});
|
|
}
|
|
function handleDragEnd(e) {
|
|
NTree.handleDragEnd({
|
|
event: e,
|
|
node: props.tmNode
|
|
});
|
|
}
|
|
function handleDragLeave(e) {
|
|
if (e.currentTarget !== e.target) {
|
|
return;
|
|
}
|
|
NTree.handleDragLeave({
|
|
event: e,
|
|
node: props.tmNode
|
|
});
|
|
}
|
|
function handleDrop(e) {
|
|
e.preventDefault();
|
|
if (droppingPositionRef.value !== null) {
|
|
NTree.handleDrop({
|
|
event: e,
|
|
node: props.tmNode,
|
|
dropPosition: droppingPositionRef.value
|
|
});
|
|
}
|
|
}
|
|
const indentNodes = computed(() => {
|
|
const {
|
|
clsPrefix
|
|
} = props;
|
|
const {
|
|
value: indent
|
|
} = indentRef;
|
|
if (showLineRef.value) {
|
|
const indentNodes = [];
|
|
let cursor = props.tmNode.parent;
|
|
while (cursor) {
|
|
if (cursor.isLastChild) {
|
|
indentNodes.push(h("div", {
|
|
class: `${clsPrefix}-tree-node-indent`
|
|
}, h("div", {
|
|
style: {
|
|
width: `${indent}px`
|
|
}
|
|
})));
|
|
} else {
|
|
indentNodes.push(h("div", {
|
|
class: [`${clsPrefix}-tree-node-indent`, `${clsPrefix}-tree-node-indent--show-line`]
|
|
}, h("div", {
|
|
style: {
|
|
width: `${indent}px`
|
|
}
|
|
})));
|
|
}
|
|
cursor = cursor.parent;
|
|
}
|
|
return indentNodes.reverse();
|
|
} else {
|
|
return repeat(props.tmNode.level, h("div", {
|
|
class: `${props.clsPrefix}-tree-node-indent`
|
|
}, h("div", {
|
|
style: {
|
|
width: `${indent}px`
|
|
}
|
|
})));
|
|
}
|
|
});
|
|
return {
|
|
showDropMark: useMemo(() => {
|
|
const {
|
|
value: draggingNode
|
|
} = draggingNodeRef;
|
|
if (!draggingNode) return;
|
|
const {
|
|
value: droppingPosition
|
|
} = droppingPositionRef;
|
|
if (!droppingPosition) return;
|
|
const {
|
|
value: droppingMouseNode
|
|
} = droppingMouseNodeRef;
|
|
if (!droppingMouseNode) {
|
|
return;
|
|
}
|
|
const {
|
|
tmNode
|
|
} = props;
|
|
if (tmNode.key === droppingMouseNode.key) return true;
|
|
return false;
|
|
}),
|
|
showDropMarkAsParent: useMemo(() => {
|
|
const {
|
|
value: droppingNodeParent
|
|
} = droppingNodeParentRef;
|
|
if (!droppingNodeParent) return false;
|
|
const {
|
|
tmNode
|
|
} = props;
|
|
const {
|
|
value: droppingPosition
|
|
} = droppingPositionRef;
|
|
if (droppingPosition === 'before' || droppingPosition === 'after') {
|
|
return droppingNodeParent.key === tmNode.key;
|
|
}
|
|
return false;
|
|
}),
|
|
pending: useMemo(() => NTree.pendingNodeKeyRef.value === props.tmNode.key),
|
|
loading: useMemo(() => NTree.loadingKeysRef.value.has(props.tmNode.key)),
|
|
highlight: useMemo(() => {
|
|
var _a;
|
|
return (_a = NTree.highlightKeySetRef.value) === null || _a === void 0 ? void 0 : _a.has(props.tmNode.key);
|
|
}),
|
|
checked: checkedRef,
|
|
indeterminate: useMemo(() => NTree.displayedIndeterminateKeysRef.value.includes(props.tmNode.key)),
|
|
selected: useMemo(() => NTree.mergedSelectedKeysRef.value.includes(props.tmNode.key)),
|
|
expanded: useMemo(() => NTree.mergedExpandedKeysRef.value.includes(props.tmNode.key)),
|
|
disabled: disabledRef,
|
|
checkable: checkableRef,
|
|
mergedCheckOnClick: mergedCheckOnClickRef,
|
|
checkboxDisabled: checkboxDisabledRef,
|
|
selectable: selectableRef,
|
|
expandOnClick: NTree.expandOnClickRef,
|
|
internalScrollable: NTree.internalScrollableRef,
|
|
draggable: NTree.draggableRef,
|
|
blockLine: blockLineRef,
|
|
nodeProps: resolvedNodePropsRef,
|
|
checkboxFocusable: NTree.internalCheckboxFocusableRef,
|
|
droppingPosition: droppingPositionRef,
|
|
droppingOffsetLevel: droppingOffsetLevelRef,
|
|
indent: indentRef,
|
|
checkboxPlacement: checkboxPlacementRef,
|
|
showLine: showLineRef,
|
|
contentInstRef,
|
|
contentElRef,
|
|
indentNodes,
|
|
handleCheck,
|
|
handleDrop,
|
|
handleDragStart,
|
|
handleDragEnter,
|
|
handleDragOver,
|
|
handleDragEnd,
|
|
handleDragLeave,
|
|
handleLineClick,
|
|
handleContentClick,
|
|
handleSwitcherClick
|
|
};
|
|
},
|
|
render() {
|
|
const {
|
|
tmNode,
|
|
clsPrefix,
|
|
checkable,
|
|
expandOnClick,
|
|
selectable,
|
|
selected,
|
|
checked,
|
|
highlight,
|
|
draggable,
|
|
blockLine,
|
|
indent,
|
|
indentNodes,
|
|
disabled,
|
|
pending,
|
|
internalScrollable,
|
|
nodeProps,
|
|
checkboxPlacement
|
|
} = this;
|
|
// drag start not inside
|
|
// it need to be append to node itself, not wrapper
|
|
const dragEventHandlers = draggable && !disabled ? {
|
|
onDragenter: this.handleDragEnter,
|
|
onDragleave: this.handleDragLeave,
|
|
onDragend: this.handleDragEnd,
|
|
onDrop: this.handleDrop,
|
|
onDragover: this.handleDragOver
|
|
} : undefined;
|
|
// In non virtual mode, there's no evidence that which element should be
|
|
// scrolled to, so we need data-key to query the target element.
|
|
const dataKey = internalScrollable ? createDataKey(tmNode.key) : undefined;
|
|
const checkboxOnRight = checkboxPlacement === 'right';
|
|
const checkboxNode = checkable ? h(NTreeNodeCheckbox, {
|
|
indent: indent,
|
|
right: checkboxOnRight,
|
|
focusable: this.checkboxFocusable,
|
|
disabled: disabled || this.checkboxDisabled,
|
|
clsPrefix: clsPrefix,
|
|
checked: this.checked,
|
|
indeterminate: this.indeterminate,
|
|
onCheck: this.handleCheck
|
|
}) : null;
|
|
return h("div", Object.assign({
|
|
class: `${clsPrefix}-tree-node-wrapper`
|
|
}, dragEventHandlers), h("div", Object.assign({}, blockLine ? nodeProps : undefined, {
|
|
class: [`${clsPrefix}-tree-node`, {
|
|
[`${clsPrefix}-tree-node--selected`]: selected,
|
|
[`${clsPrefix}-tree-node--checkable`]: checkable,
|
|
[`${clsPrefix}-tree-node--highlight`]: highlight,
|
|
[`${clsPrefix}-tree-node--pending`]: pending,
|
|
[`${clsPrefix}-tree-node--disabled`]: disabled,
|
|
[`${clsPrefix}-tree-node--selectable`]: selectable,
|
|
[`${clsPrefix}-tree-node--clickable`]: selectable || expandOnClick || this.mergedCheckOnClick
|
|
}, nodeProps === null || nodeProps === void 0 ? void 0 : nodeProps.class],
|
|
"data-key": dataKey,
|
|
draggable: draggable && blockLine,
|
|
onClick: this.handleLineClick,
|
|
onDragstart: draggable && blockLine && !disabled ? this.handleDragStart : undefined
|
|
}), indentNodes, tmNode.isLeaf && this.showLine ? h("div", {
|
|
class: [`${clsPrefix}-tree-node-indent`, `${clsPrefix}-tree-node-indent--show-line`, tmNode.isLeaf && `${clsPrefix}-tree-node-indent--is-leaf`, tmNode.isLastChild && `${clsPrefix}-tree-node-indent--last-child`]
|
|
}, h("div", {
|
|
style: {
|
|
width: `${indent}px`
|
|
}
|
|
})) : h(NTreeNodeSwitcher, {
|
|
clsPrefix: clsPrefix,
|
|
expanded: this.expanded,
|
|
selected: selected,
|
|
loading: this.loading,
|
|
hide: tmNode.isLeaf,
|
|
tmNode: this.tmNode,
|
|
indent: indent,
|
|
onClick: this.handleSwitcherClick
|
|
}), !checkboxOnRight ? checkboxNode : null, h(NTreeNodeContent, {
|
|
ref: "contentInstRef",
|
|
clsPrefix: clsPrefix,
|
|
checked: checked,
|
|
selected: selected,
|
|
onClick: this.handleContentClick,
|
|
nodeProps: blockLine ? undefined : nodeProps,
|
|
onDragstart: draggable && !blockLine && !disabled ? this.handleDragStart : undefined,
|
|
tmNode: tmNode
|
|
}), draggable ? this.showDropMark ? renderDropMark({
|
|
el: this.contentElRef.value,
|
|
position: this.droppingPosition,
|
|
offsetLevel: this.droppingOffsetLevel,
|
|
indent
|
|
}) : this.showDropMarkAsParent ? renderDropMark({
|
|
el: this.contentElRef.value,
|
|
position: 'inside',
|
|
offsetLevel: this.droppingOffsetLevel,
|
|
indent
|
|
}) : null : null, checkboxOnRight ? checkboxNode : null));
|
|
}
|
|
});
|
|
export default TreeNode; |