2024-08-02 18:19:39 +08:00

1512 lines
49 KiB
JavaScript

var __awaiter = this && this.__awaiter || function (thisArg, _arguments, P, generator) {
function adopt(value) {
return value instanceof P ? value : new P(function (resolve) {
resolve(value);
});
}
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
}
function rejected(value) {
try {
step(generator["throw"](value));
} catch (e) {
reject(e);
}
}
function step(result) {
result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
}
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { computed, defineComponent, h, inject, nextTick, provide, ref, toRef, watch, watchEffect } from 'vue';
import { createIndexGetter, createTreeMate, flatten } from 'treemate';
import { useMergedState } from 'vooks';
import { VVirtualList } from 'vueuc';
import { depx, getPadding, pxfy } from 'seemly';
import { treeSelectInjectionKey } from "../../tree-select/src/interface.mjs";
import { useConfig, useRtl, useTheme, useThemeClass } from "../../_mixins/index.mjs";
import { call, createDataKey, resolveSlot, warn, warnOnce } from "../../_utils/index.mjs";
import { NxScrollbar } from "../../_internal/index.mjs";
import { treeLight } from "../styles/index.mjs";
import { NEmpty } from "../../empty/index.mjs";
import NTreeNode from "./TreeNode.mjs";
import { emptyImage, filterTree, isNodeDisabled, keysWithFilter, useMergedCheckStrategy } from "./utils.mjs";
import { useKeyboard } from "./keyboard.mjs";
import { treeInjectionKey } from "./interface.mjs";
import MotionWrapper from "./MotionWrapper.mjs";
import { defaultAllowDrop } from "./dnd.mjs";
import style from "./styles/index.cssr.mjs";
export function createTreeMateOptions(keyField, childrenField, disabledField, getChildren) {
const settledGetChildren = getChildren || (node => {
return node[childrenField];
});
return {
getIsGroup() {
return false;
},
getKey(node) {
return node[keyField];
},
getChildren: settledGetChildren,
getDisabled(node) {
return !!(node[disabledField] || node.checkboxDisabled);
}
};
}
export const treeSharedProps = {
allowCheckingNotLoaded: Boolean,
filter: Function,
defaultExpandAll: Boolean,
expandedKeys: Array,
keyField: {
type: String,
default: 'key'
},
labelField: {
type: String,
default: 'label'
},
childrenField: {
type: String,
default: 'children'
},
disabledField: {
type: String,
default: 'disabled'
},
defaultExpandedKeys: {
type: Array,
default: () => []
},
indeterminateKeys: Array,
renderSwitcherIcon: Function,
onUpdateIndeterminateKeys: [Function, Array],
'onUpdate:indeterminateKeys': [Function, Array],
onUpdateExpandedKeys: [Function, Array],
'onUpdate:expandedKeys': [Function, Array],
overrideDefaultNodeClickBehavior: Function
};
export const treeProps = Object.assign(Object.assign(Object.assign(Object.assign({}, useTheme.props), {
accordion: Boolean,
showIrrelevantNodes: {
type: Boolean,
default: true
},
data: {
type: Array,
default: () => []
},
expandOnDragenter: {
type: Boolean,
default: true
},
expandOnClick: Boolean,
checkOnClick: {
type: [Boolean, Function],
default: false
},
cancelable: {
type: Boolean,
default: true
},
checkable: Boolean,
draggable: Boolean,
blockNode: Boolean,
blockLine: Boolean,
showLine: Boolean,
disabled: Boolean,
checkedKeys: Array,
defaultCheckedKeys: {
type: Array,
default: () => []
},
selectedKeys: Array,
defaultSelectedKeys: {
type: Array,
default: () => []
},
multiple: Boolean,
pattern: {
type: String,
default: ''
},
onLoad: Function,
cascade: Boolean,
selectable: {
type: Boolean,
default: true
},
scrollbarProps: Object,
indent: {
type: Number,
default: 24
},
allowDrop: {
type: Function,
default: defaultAllowDrop
},
animated: {
type: Boolean,
default: true
},
checkboxPlacement: {
type: String,
default: 'left'
},
virtualScroll: Boolean,
watchProps: Array,
renderLabel: Function,
renderPrefix: Function,
renderSuffix: Function,
nodeProps: Function,
keyboard: {
type: Boolean,
default: true
},
getChildren: Function,
onDragenter: [Function, Array],
onDragleave: [Function, Array],
onDragend: [Function, Array],
onDragstart: [Function, Array],
onDragover: [Function, Array],
onDrop: [Function, Array],
onUpdateCheckedKeys: [Function, Array],
'onUpdate:checkedKeys': [Function, Array],
onUpdateSelectedKeys: [Function, Array],
'onUpdate:selectedKeys': [Function, Array]
}), treeSharedProps), {
// internal props for tree-select
internalTreeSelect: Boolean,
internalScrollable: Boolean,
internalScrollablePadding: String,
// use it to display
internalRenderEmpty: Function,
internalHighlightKeySet: Object,
internalUnifySelectCheck: Boolean,
internalCheckboxFocusable: {
type: Boolean,
default: true
},
internalFocusable: {
// Make tree-select take over keyboard operations
type: Boolean,
default: true
},
checkStrategy: {
type: String,
default: 'all'
},
/**
* @deprecated
*/
leafOnly: Boolean
});
export default defineComponent({
name: 'Tree',
props: treeProps,
setup(props) {
if (process.env.NODE_ENV !== 'production') {
watchEffect(() => {
if (props.leafOnly) {
warnOnce('tree', '`leaf-only` is deprecated, please use `check-strategy="child"` instead');
}
});
}
const {
mergedClsPrefixRef,
inlineThemeDisabled,
mergedRtlRef
} = useConfig(props);
const rtlEnabledRef = useRtl('Tree', mergedRtlRef, mergedClsPrefixRef);
const themeRef = useTheme('Tree', '-tree', style, treeLight, props, mergedClsPrefixRef);
const selfElRef = ref(null);
const scrollbarInstRef = ref(null);
const virtualListInstRef = ref(null);
function getScrollContainer() {
var _a;
return (_a = virtualListInstRef.value) === null || _a === void 0 ? void 0 : _a.listElRef;
}
function getScrollContent() {
var _a;
return (_a = virtualListInstRef.value) === null || _a === void 0 ? void 0 : _a.itemsElRef;
}
const mergedFilterRef = computed(() => {
const {
filter
} = props;
if (filter) return filter;
const {
labelField
} = props;
return (pattern, node) => {
if (!pattern.length) return true;
const label = node[labelField];
if (typeof label === 'string') {
return label.toLowerCase().includes(pattern.toLowerCase());
}
return false;
};
});
const filteredTreeInfoRef = computed(() => {
const {
pattern
} = props;
if (!pattern) {
return {
filteredTree: props.data,
highlightKeySet: null,
expandedKeys: undefined
};
}
if (!pattern.length || !mergedFilterRef.value) {
return {
filteredTree: props.data,
highlightKeySet: null,
expandedKeys: undefined
};
}
return filterTree(props.data, mergedFilterRef.value, pattern, props.keyField, props.childrenField);
});
// We don't expect data source to change so we just determine it once
const displayTreeMateRef = computed(() => createTreeMate(props.showIrrelevantNodes ? props.data : filteredTreeInfoRef.value.filteredTree, createTreeMateOptions(props.keyField, props.childrenField, props.disabledField, props.getChildren)));
const treeSelectInjection = inject(treeSelectInjectionKey, null);
const dataTreeMateRef = props.internalTreeSelect ? treeSelectInjection.dataTreeMate : computed(() => props.showIrrelevantNodes ? displayTreeMateRef.value : createTreeMate(props.data, createTreeMateOptions(props.keyField, props.childrenField, props.disabledField, props.getChildren)));
const {
watchProps
} = props;
const uncontrolledCheckedKeysRef = ref([]);
if (watchProps === null || watchProps === void 0 ? void 0 : watchProps.includes('defaultCheckedKeys')) {
watchEffect(() => {
uncontrolledCheckedKeysRef.value = props.defaultCheckedKeys;
});
} else {
uncontrolledCheckedKeysRef.value = props.defaultCheckedKeys;
}
const controlledCheckedKeysRef = toRef(props, 'checkedKeys');
const mergedCheckedKeysRef = useMergedState(controlledCheckedKeysRef, uncontrolledCheckedKeysRef);
const checkedStatusRef = computed(() => {
const value = dataTreeMateRef.value.getCheckedKeys(mergedCheckedKeysRef.value, {
cascade: props.cascade,
allowNotLoaded: props.allowCheckingNotLoaded
});
return value;
});
const mergedCheckStrategyRef = useMergedCheckStrategy(props);
const displayedCheckedKeysRef = computed(() => {
return checkedStatusRef.value.checkedKeys;
});
const displayedIndeterminateKeysRef = computed(() => {
const {
indeterminateKeys
} = props;
if (indeterminateKeys !== undefined) return indeterminateKeys;
return checkedStatusRef.value.indeterminateKeys;
});
const uncontrolledSelectedKeysRef = ref([]);
if (watchProps === null || watchProps === void 0 ? void 0 : watchProps.includes('defaultSelectedKeys')) {
watchEffect(() => {
uncontrolledSelectedKeysRef.value = props.defaultSelectedKeys;
});
} else {
uncontrolledSelectedKeysRef.value = props.defaultSelectedKeys;
}
const controlledSelectedKeysRef = toRef(props, 'selectedKeys');
const mergedSelectedKeysRef = useMergedState(controlledSelectedKeysRef, uncontrolledSelectedKeysRef);
const uncontrolledExpandedKeysRef = ref([]);
const initUncontrolledExpandedKeys = keys => {
uncontrolledExpandedKeysRef.value = props.defaultExpandAll ? dataTreeMateRef.value.getNonLeafKeys() : keys === undefined ? props.defaultExpandedKeys : keys;
};
if (watchProps === null || watchProps === void 0 ? void 0 : watchProps.includes('defaultExpandedKeys')) {
// if watching defaultExpandedKeys, we use access props.defaultExpandedKeys inside initiator
watchEffect(() => {
initUncontrolledExpandedKeys(undefined);
});
} else {
// We by default watchEffect since if defaultExpandAll is true, we should remain tree expand if data changes
watchEffect(() => {
initUncontrolledExpandedKeys(props.defaultExpandedKeys);
});
}
const controlledExpandedKeysRef = toRef(props, 'expandedKeys');
const mergedExpandedKeysRef = useMergedState(controlledExpandedKeysRef, uncontrolledExpandedKeysRef);
const fNodesRef = computed(() => displayTreeMateRef.value.getFlattenedNodes(mergedExpandedKeysRef.value));
const {
pendingNodeKeyRef,
handleKeydown
} = useKeyboard({
props,
mergedCheckedKeysRef,
mergedSelectedKeysRef,
fNodesRef,
mergedExpandedKeysRef,
handleCheck,
handleSelect,
handleSwitcherClick
});
let expandTimerId = null;
let nodeKeyToBeExpanded = null;
const uncontrolledHighlightKeySetRef = ref(new Set());
const controlledHighlightKeySetRef = computed(() => {
return props.internalHighlightKeySet || filteredTreeInfoRef.value.highlightKeySet;
});
const mergedHighlightKeySetRef = useMergedState(controlledHighlightKeySetRef, uncontrolledHighlightKeySetRef);
const loadingKeysRef = ref(new Set());
const expandedNonLoadingKeysRef = computed(() => {
return mergedExpandedKeysRef.value.filter(key => !loadingKeysRef.value.has(key));
});
let dragStartX = 0;
const draggingNodeRef = ref(null);
const droppingNodeRef = ref(null);
const droppingMouseNodeRef = ref(null);
const droppingPositionRef = ref(null);
const droppingOffsetLevelRef = ref(0);
const droppingNodeParentRef = computed(() => {
const {
value: droppingNode
} = droppingNodeRef;
if (!droppingNode) return null;
// May avoid overlap between line mark of first child & rect mark of parent
// if (droppingNode.isFirstChild && droppingPositionRef.value === 'before') {
// return null
// }
return droppingNode.parent;
});
// shallow watch data
let isDataReset = false;
watch(toRef(props, 'data'), () => {
isDataReset = true;
void nextTick(() => {
isDataReset = false;
});
loadingKeysRef.value.clear();
pendingNodeKeyRef.value = null;
resetDndState();
}, {
deep: false
});
let expandAnimationDisabled = false;
const disableExpandAnimationForOneTick = () => {
expandAnimationDisabled = true;
void nextTick(() => {
expandAnimationDisabled = false;
});
};
let memoizedExpandedKeys;
watch(toRef(props, 'pattern'), (value, oldValue) => {
if (props.showIrrelevantNodes) {
memoizedExpandedKeys = undefined;
if (value) {
const {
expandedKeys: expandedKeysAfterChange,
highlightKeySet
} = keysWithFilter(props.data, props.pattern, props.keyField, props.childrenField, mergedFilterRef.value);
uncontrolledHighlightKeySetRef.value = highlightKeySet;
disableExpandAnimationForOneTick();
doUpdateExpandedKeys(expandedKeysAfterChange, getOptionsByKeys(expandedKeysAfterChange), {
node: null,
action: 'filter'
});
} else {
uncontrolledHighlightKeySetRef.value = new Set();
}
} else {
if (!value.length) {
if (memoizedExpandedKeys !== undefined) {
disableExpandAnimationForOneTick();
doUpdateExpandedKeys(memoizedExpandedKeys, getOptionsByKeys(memoizedExpandedKeys), {
node: null,
action: 'filter'
});
}
} else {
if (!oldValue.length) {
memoizedExpandedKeys = mergedExpandedKeysRef.value;
}
const {
expandedKeys
} = filteredTreeInfoRef.value;
if (expandedKeys !== undefined) {
disableExpandAnimationForOneTick();
doUpdateExpandedKeys(expandedKeys, getOptionsByKeys(expandedKeys), {
node: null,
action: 'filter'
});
}
}
}
});
function triggerLoading(node) {
return __awaiter(this, void 0, void 0, function* () {
const {
onLoad
} = props;
if (!onLoad) {
if (process.env.NODE_ENV !== 'production') {
warn('tree', 'There is unloaded node in data but props.onLoad is not specified.');
}
yield Promise.resolve();
return;
}
const {
value: loadingKeys
} = loadingKeysRef;
if (!loadingKeys.has(node.key)) {
loadingKeys.add(node.key);
try {
const loadResult = yield onLoad(node.rawNode);
if (loadResult === false) {
resetDragExpandState();
}
} catch (loadError) {
console.error(loadError);
resetDragExpandState();
}
loadingKeys.delete(node.key);
}
});
}
watchEffect(() => {
var _a;
const {
value: displayTreeMate
} = displayTreeMateRef;
if (!displayTreeMate) return;
const {
getNode
} = displayTreeMate;
(_a = mergedExpandedKeysRef.value) === null || _a === void 0 ? void 0 : _a.forEach(key => {
const node = getNode(key);
if (node && !node.shallowLoaded) {
void triggerLoading(node);
}
});
});
// animation in progress
const aipRef = ref(false);
// animation flattened nodes
const afNodesRef = ref([]);
// Note: Since the virtual list depends on min height, if there's a node
// whose height starts from 0, the virtual list will have a wrong height
// during animation. This will seldom cause wired scrollbar status. It is
// fixable and need some changes in vueuc, I've no time so I just leave it
// here. Maybe the bug won't be fixed during the life time of the project.
watch(expandedNonLoadingKeysRef, (value, prevValue) => {
if (!props.animated || expandAnimationDisabled) {
void nextTick(syncScrollbar);
return;
}
if (isDataReset) {
return;
}
const nodeHeight = depx(themeRef.value.self.nodeHeight);
const prevVSet = new Set(prevValue);
let addedKey = null;
let removedKey = null;
for (const expandedKey of value) {
if (!prevVSet.has(expandedKey)) {
if (addedKey !== null) return; // multi expand, not triggered by click
addedKey = expandedKey;
}
}
const currentVSet = new Set(value);
for (const expandedKey of prevValue) {
if (!currentVSet.has(expandedKey)) {
if (removedKey !== null) return; // multi collapse, not triggered by click
removedKey = expandedKey;
}
}
if (addedKey === null && removedKey === null) {
// 1. multi action, not triggered by click
// 2. no action, don't know what happened
return;
}
const {
virtualScroll
} = props;
const viewportHeight = (virtualScroll ? virtualListInstRef.value.listElRef : selfElRef.value).offsetHeight;
const viewportItemCount = Math.ceil(viewportHeight / nodeHeight) + 1;
// play add animation
let baseExpandedKeys;
if (addedKey !== null) {
baseExpandedKeys = prevValue;
}
if (removedKey !== null) {
if (baseExpandedKeys === undefined) {
baseExpandedKeys = value;
} else {
baseExpandedKeys = baseExpandedKeys.filter(key => key !== removedKey);
}
}
aipRef.value = true;
afNodesRef.value = displayTreeMateRef.value.getFlattenedNodes(baseExpandedKeys);
if (addedKey !== null) {
const expandedNodeIndex = afNodesRef.value.findIndex(node => node.key === addedKey);
if (~expandedNodeIndex) {
const children = afNodesRef.value[expandedNodeIndex].children;
// sometimes user will pass leaf keys in
if (children) {
const expandedChildren = flatten(children, value);
afNodesRef.value.splice(expandedNodeIndex + 1, 0, {
__motion: true,
mode: 'expand',
height: virtualScroll ? expandedChildren.length * nodeHeight : undefined,
nodes: virtualScroll ? expandedChildren.slice(0, viewportItemCount) : expandedChildren
});
}
}
}
if (removedKey !== null) {
const collapsedNodeIndex = afNodesRef.value.findIndex(node => node.key === removedKey);
if (~collapsedNodeIndex) {
const collapsedNodeChildren = afNodesRef.value[collapsedNodeIndex].children;
// Sometime the whole tree is change, remove a key doesn't mean it is collapsed,
// but maybe children removed
if (!collapsedNodeChildren) return;
// play remove animation
aipRef.value = true;
const collapsedChildren = flatten(collapsedNodeChildren, value);
afNodesRef.value.splice(collapsedNodeIndex + 1, 0, {
__motion: true,
mode: 'collapse',
height: virtualScroll ? collapsedChildren.length * nodeHeight : undefined,
nodes: virtualScroll ? collapsedChildren.slice(0, viewportItemCount) : collapsedChildren
});
}
}
});
const getFIndexRef = computed(() => {
return createIndexGetter(fNodesRef.value);
});
const mergedFNodesRef = computed(() => {
if (aipRef.value) return afNodesRef.value;else return fNodesRef.value;
});
function syncScrollbar() {
const {
value: scrollbarInst
} = scrollbarInstRef;
if (scrollbarInst) scrollbarInst.sync();
}
function handleAfterEnter() {
aipRef.value = false;
if (props.virtualScroll) {
// If virtual scroll, we won't listen to resize during animation, so
// resize callback of virtual list won't be called and as a result
// scrollbar won't sync. We need to sync scrollbar manually.
void nextTick(syncScrollbar);
}
}
function getOptionsByKeys(keys) {
const {
getNode
} = dataTreeMateRef.value;
return keys.map(key => {
var _a;
return ((_a = getNode(key)) === null || _a === void 0 ? void 0 : _a.rawNode) || null;
});
}
function doUpdateExpandedKeys(value, option, meta) {
const {
'onUpdate:expandedKeys': _onUpdateExpandedKeys,
onUpdateExpandedKeys
} = props;
uncontrolledExpandedKeysRef.value = value;
if (_onUpdateExpandedKeys) {
call(_onUpdateExpandedKeys, value, option, meta);
}
if (onUpdateExpandedKeys) {
call(onUpdateExpandedKeys, value, option, meta);
}
}
function doUpdateCheckedKeys(value, option, meta) {
const {
'onUpdate:checkedKeys': _onUpdateCheckedKeys,
onUpdateCheckedKeys
} = props;
uncontrolledCheckedKeysRef.value = value;
if (onUpdateCheckedKeys) {
call(onUpdateCheckedKeys, value, option, meta);
}
if (_onUpdateCheckedKeys) {
call(_onUpdateCheckedKeys, value, option, meta);
}
}
function doUpdateIndeterminateKeys(value, option) {
const {
'onUpdate:indeterminateKeys': _onUpdateIndeterminateKeys,
onUpdateIndeterminateKeys
} = props;
if (_onUpdateIndeterminateKeys) {
call(_onUpdateIndeterminateKeys, value, option);
}
if (onUpdateIndeterminateKeys) {
call(onUpdateIndeterminateKeys, value, option);
}
}
function doUpdateSelectedKeys(value, option, meta) {
const {
'onUpdate:selectedKeys': _onUpdateSelectedKeys,
onUpdateSelectedKeys
} = props;
uncontrolledSelectedKeysRef.value = value;
if (onUpdateSelectedKeys) {
call(onUpdateSelectedKeys, value, option, meta);
}
if (_onUpdateSelectedKeys) {
call(_onUpdateSelectedKeys, value, option, meta);
}
}
// Drag & Drop
function doDragEnter(info) {
const {
onDragenter
} = props;
if (onDragenter) call(onDragenter, info);
}
function doDragLeave(info) {
const {
onDragleave
} = props;
if (onDragleave) call(onDragleave, info);
}
function doDragEnd(info) {
const {
onDragend
} = props;
if (onDragend) call(onDragend, info);
}
function doDragStart(info) {
const {
onDragstart
} = props;
if (onDragstart) call(onDragstart, info);
}
function doDragOver(info) {
const {
onDragover
} = props;
if (onDragover) call(onDragover, info);
}
function doDrop(info) {
const {
onDrop
} = props;
if (onDrop) call(onDrop, info);
}
function resetDndState() {
resetDragState();
resetDropState();
}
function resetDragState() {
draggingNodeRef.value = null;
}
function resetDropState() {
droppingOffsetLevelRef.value = 0;
droppingNodeRef.value = null;
droppingMouseNodeRef.value = null;
droppingPositionRef.value = null;
resetDragExpandState();
}
function resetDragExpandState() {
if (expandTimerId) {
window.clearTimeout(expandTimerId);
expandTimerId = null;
}
nodeKeyToBeExpanded = null;
}
function handleCheck(node, checked) {
// We don't guard for leaf only since we have done it in view layer
if (props.disabled || isNodeDisabled(node, props.disabledField)) {
return;
}
if (props.internalUnifySelectCheck && !props.multiple) {
handleSelect(node);
return;
}
const checkedAction = checked ? 'check' : 'uncheck';
const {
checkedKeys,
indeterminateKeys
} = dataTreeMateRef.value[checkedAction](node.key, displayedCheckedKeysRef.value, {
cascade: props.cascade,
checkStrategy: mergedCheckStrategyRef.value,
allowNotLoaded: props.allowCheckingNotLoaded
});
doUpdateCheckedKeys(checkedKeys, getOptionsByKeys(checkedKeys), {
node: node.rawNode,
action: checkedAction
});
doUpdateIndeterminateKeys(indeterminateKeys, getOptionsByKeys(indeterminateKeys));
}
function toggleExpand(node) {
if (props.disabled) return;
const {
key
} = node;
const {
value: mergedExpandedKeys
} = mergedExpandedKeysRef;
const index = mergedExpandedKeys.findIndex(expandNodeId => expandNodeId === key);
if (~index) {
const expandedKeysAfterChange = Array.from(mergedExpandedKeys);
expandedKeysAfterChange.splice(index, 1);
doUpdateExpandedKeys(expandedKeysAfterChange, getOptionsByKeys(expandedKeysAfterChange), {
node: node.rawNode,
action: 'collapse'
});
} else {
const nodeToBeExpanded = displayTreeMateRef.value.getNode(key);
if (!nodeToBeExpanded || nodeToBeExpanded.isLeaf) {
return;
}
let nextKeys;
if (props.accordion) {
const siblingKeySet = new Set(node.siblings.map(({
key
}) => key));
nextKeys = mergedExpandedKeys.filter(expandedKey => {
return !siblingKeySet.has(expandedKey);
});
nextKeys.push(key);
} else {
nextKeys = mergedExpandedKeys.concat(key);
}
doUpdateExpandedKeys(nextKeys, getOptionsByKeys(nextKeys), {
node: node.rawNode,
action: 'expand'
});
}
}
function handleSwitcherClick(node) {
if (props.disabled || aipRef.value) return;
toggleExpand(node);
}
function handleSelect(node) {
if (props.disabled || !props.selectable) {
return;
}
pendingNodeKeyRef.value = node.key;
if (props.internalUnifySelectCheck) {
const {
value: {
checkedKeys,
indeterminateKeys
}
} = checkedStatusRef;
if (props.multiple) {
handleCheck(node, !(checkedKeys.includes(node.key) || indeterminateKeys.includes(node.key)));
} else {
doUpdateCheckedKeys([node.key], getOptionsByKeys([node.key]), {
node: node.rawNode,
action: 'check'
});
}
}
if (props.multiple) {
const selectedKeys = Array.from(mergedSelectedKeysRef.value);
const index = selectedKeys.findIndex(key => key === node.key);
if (~index) {
if (props.cancelable) {
selectedKeys.splice(index, 1);
}
} else if (!~index) {
selectedKeys.push(node.key);
}
doUpdateSelectedKeys(selectedKeys, getOptionsByKeys(selectedKeys), {
node: node.rawNode,
action: ~index ? 'unselect' : 'select'
});
} else {
const selectedKeys = mergedSelectedKeysRef.value;
if (selectedKeys.includes(node.key)) {
if (props.cancelable) {
doUpdateSelectedKeys([], [], {
node: node.rawNode,
action: 'unselect'
});
}
} else {
doUpdateSelectedKeys([node.key], getOptionsByKeys([node.key]), {
node: node.rawNode,
action: 'select'
});
}
}
}
function expandDragEnterNode(node) {
if (expandTimerId) {
window.clearTimeout(expandTimerId);
expandTimerId = null;
}
// Don't expand leaf node.
if (node.isLeaf) return;
nodeKeyToBeExpanded = node.key;
const expand = () => {
if (nodeKeyToBeExpanded !== node.key) return;
const {
value: droppingMouseNode
} = droppingMouseNodeRef;
if (droppingMouseNode && droppingMouseNode.key === node.key && !mergedExpandedKeysRef.value.includes(node.key)) {
const nextKeys = mergedExpandedKeysRef.value.concat(node.key);
doUpdateExpandedKeys(nextKeys, getOptionsByKeys(nextKeys), {
node: node.rawNode,
action: 'expand'
});
}
expandTimerId = null;
nodeKeyToBeExpanded = null;
};
if (!node.shallowLoaded) {
expandTimerId = window.setTimeout(() => {
void triggerLoading(node).then(() => {
expand();
});
}, 1000);
} else {
expandTimerId = window.setTimeout(() => {
expand();
}, 1000);
}
}
// Dnd
function handleDragEnter({
event,
node
}) {
// node should be a tmNode
if (!props.draggable || props.disabled || isNodeDisabled(node, props.disabledField)) {
return;
}
handleDragOver({
event,
node
}, false);
doDragEnter({
event,
node: node.rawNode
});
}
function handleDragLeave({
event,
node
}) {
if (!props.draggable || props.disabled || isNodeDisabled(node, props.disabledField)) {
return;
}
doDragLeave({
event,
node: node.rawNode
});
}
function handleDragLeaveTree(e) {
if (e.target !== e.currentTarget) return;
resetDropState();
}
// Dragend is ok, we don't need to add global listener to reset drag status
function handleDragEnd({
event,
node
}) {
resetDndState();
if (!props.draggable || props.disabled || isNodeDisabled(node, props.disabledField)) {
return;
}
doDragEnd({
event,
node: node.rawNode
});
}
function handleDragStart({
event,
node
}) {
var _a;
if (!props.draggable || props.disabled || isNodeDisabled(node, props.disabledField)) {
return;
}
// Most of time, the image will block user's view
if (emptyImage) {
(_a = event.dataTransfer) === null || _a === void 0 ? void 0 : _a.setDragImage(emptyImage, 0, 0);
}
dragStartX = event.clientX;
draggingNodeRef.value = node;
doDragStart({
event,
node: node.rawNode
});
}
function handleDragOver({
event,
node
}, emit = true) {
var _a;
if (!props.draggable || props.disabled || isNodeDisabled(node, props.disabledField)) {
return;
}
const {
value: draggingNode
} = draggingNodeRef;
if (!draggingNode) return;
const {
allowDrop,
indent
} = props;
if (emit) doDragOver({
event,
node: node.rawNode
});
// Update dropping node
const el = event.currentTarget;
const {
height: elOffsetHeight,
top: elClientTop
} = el.getBoundingClientRect();
const eventOffsetY = event.clientY - elClientTop;
let mousePosition;
const allowDropInside = allowDrop({
node: node.rawNode,
dropPosition: 'inside',
phase: 'drag'
});
if (allowDropInside) {
if (eventOffsetY <= 8) {
mousePosition = 'before';
} else if (eventOffsetY >= elOffsetHeight - 8) {
mousePosition = 'after';
} else {
mousePosition = 'inside';
}
} else {
if (eventOffsetY <= elOffsetHeight / 2) {
mousePosition = 'before';
} else {
mousePosition = 'after';
}
}
const {
value: getFindex
} = getFIndexRef;
/** determine the drop position and drop node */
/** the dropping node needn't to be the mouse hovering node! */
/**
* if there is something i've learned from implementing a complex
* drag & drop. that is never write unit test before you really figure
* out what behavior is exactly you want.
*/
let finalDropNode;
let finalDropPosition;
const hoverNodeFIndex = getFindex(node.key);
if (hoverNodeFIndex === null) {
resetDropState();
return;
}
let mouseAtExpandedNonLeafNode = false;
if (mousePosition === 'inside') {
finalDropNode = node;
finalDropPosition = 'inside';
} else {
if (mousePosition === 'before') {
if (node.isFirstChild) {
finalDropNode = node;
finalDropPosition = 'before';
} else {
finalDropNode = fNodesRef.value[hoverNodeFIndex - 1];
finalDropPosition = 'after';
}
} else {
finalDropNode = node;
finalDropPosition = 'after';
}
}
// If the node is non-leaf and it is expanded, we don't allow it to
// drop after it and change it to drop before its next view sibling
if (!finalDropNode.isLeaf && mergedExpandedKeysRef.value.includes(finalDropNode.key)) {
mouseAtExpandedNonLeafNode = true;
if (finalDropPosition === 'after') {
finalDropNode = fNodesRef.value[hoverNodeFIndex + 1];
if (!finalDropNode) {
// maybe there is no next view sibling when non-leaf node has no
// children and it is the last node in the tree
finalDropNode = node;
finalDropPosition = 'inside';
} else {
finalDropPosition = 'before';
}
}
}
const droppingMouseNode = finalDropNode;
droppingMouseNodeRef.value = droppingMouseNode;
// This is a speacial case, user is dragging a last child itself, so we
// only view it as they are trying to drop after it.
// There are some relevant codes in bailout 1's child branch.
// Also, the expand bailout should have a high priority. If it's non-leaf
// node and expanded, keep its origin drop position
if (!mouseAtExpandedNonLeafNode && draggingNode.isLastChild && draggingNode.key === finalDropNode.key) {
finalDropPosition = 'after';
}
if (finalDropPosition === 'after') {
let offset = dragStartX - event.clientX; // drag left => > 0
let offsetLevel = 0;
while (offset >= indent / 2 // divide by 2 to make it easier to trigger
&& finalDropNode.parent !== null && finalDropNode.isLastChild && offsetLevel < 1) {
offset -= indent;
offsetLevel += 1;
finalDropNode = finalDropNode.parent;
}
droppingOffsetLevelRef.value = offsetLevel;
} else {
droppingOffsetLevelRef.value = 0;
}
// Bailout 1
// Drag self into self
// Drag it into direct parent
if (draggingNode.contains(finalDropNode) || finalDropPosition === 'inside' && ((_a = draggingNode.parent) === null || _a === void 0 ? void 0 : _a.key) === finalDropNode.key) {
if (draggingNode.key === droppingMouseNode.key && draggingNode.key === finalDropNode.key) {
// This is special case that we want ui to show a mark to guide user
// to start dragging. Nor they will think nothing happens.
// However this is an invalid drop, we need to guard it inside
// handleDrop
} else {
resetDropState();
return;
}
}
// Bailout 3
if (!allowDrop({
node: finalDropNode.rawNode,
dropPosition: finalDropPosition,
phase: 'drag'
})) {
resetDropState();
return;
}
if (draggingNode.key === finalDropNode.key) {
// don't expand when drag on itself
resetDragExpandState();
} else {
if (nodeKeyToBeExpanded !== finalDropNode.key) {
if (finalDropPosition === 'inside') {
if (props.expandOnDragenter) {
expandDragEnterNode(finalDropNode);
// Bailout 4
// not try to loading
if (!finalDropNode.shallowLoaded && nodeKeyToBeExpanded !== finalDropNode.key) {
resetDndState();
return;
}
} else {
// Bailout 5
// never expands on drag
if (!finalDropNode.shallowLoaded) {
resetDndState();
return;
}
}
} else {
resetDragExpandState();
}
} else {
if (finalDropPosition !== 'inside') {
resetDragExpandState();
}
}
}
droppingPositionRef.value = finalDropPosition;
droppingNodeRef.value = finalDropNode;
}
function handleDrop({
event,
node,
dropPosition
}) {
if (!props.draggable || props.disabled || isNodeDisabled(node, props.disabledField)) {
return;
}
const {
value: draggingNode
} = draggingNodeRef;
const {
value: droppingNode
} = droppingNodeRef;
const {
value: droppingPosition
} = droppingPositionRef;
if (!draggingNode || !droppingNode || !droppingPosition) {
return;
}
// Bailout 1
if (!props.allowDrop({
node: droppingNode.rawNode,
dropPosition: droppingPosition,
phase: 'drag'
})) {
return;
}
// Bailout 2
// This is a special case to guard since we want ui to show the status
// but not to emit a event
if (draggingNode.key === droppingNode.key) {
return;
}
// Bailout 3
// insert before its next node
// insert after its prev node
if (droppingPosition === 'before') {
const nextNode = draggingNode.getNext({
includeDisabled: true
});
if (nextNode) {
if (nextNode.key === droppingNode.key) {
resetDropState();
return;
}
}
}
if (droppingPosition === 'after') {
const prevNode = draggingNode.getPrev({
includeDisabled: true
});
if (prevNode) {
if (prevNode.key === droppingNode.key) {
resetDropState();
return;
}
}
}
doDrop({
event,
node: droppingNode.rawNode,
dragNode: draggingNode.rawNode,
dropPosition
});
resetDndState();
}
function handleScroll() {
syncScrollbar();
}
function handleResize() {
syncScrollbar();
}
function handleFocusout(e) {
var _a;
if (props.virtualScroll || props.internalScrollable) {
const {
value: scrollbarInst
} = scrollbarInstRef;
if ((_a = scrollbarInst === null || scrollbarInst === void 0 ? void 0 : scrollbarInst.containerRef) === null || _a === void 0 ? void 0 : _a.contains(e.relatedTarget)) {
return;
}
pendingNodeKeyRef.value = null;
} else {
const {
value: selfEl
} = selfElRef;
if (selfEl === null || selfEl === void 0 ? void 0 : selfEl.contains(e.relatedTarget)) return;
pendingNodeKeyRef.value = null;
}
}
watch(pendingNodeKeyRef, value => {
var _a, _b;
if (value === null) return;
if (props.virtualScroll) {
(_a = virtualListInstRef.value) === null || _a === void 0 ? void 0 : _a.scrollTo({
key: value
});
} else if (props.internalScrollable) {
const {
value: scrollbarInst
} = scrollbarInstRef;
if (scrollbarInst === null) return;
const targetEl = (_b = scrollbarInst.contentRef) === null || _b === void 0 ? void 0 : _b.querySelector(`[data-key="${createDataKey(value)}"]`);
if (!targetEl) return;
scrollbarInst.scrollTo({
el: targetEl
});
}
});
provide(treeInjectionKey, {
loadingKeysRef,
highlightKeySetRef: mergedHighlightKeySetRef,
displayedCheckedKeysRef,
displayedIndeterminateKeysRef,
mergedSelectedKeysRef,
mergedExpandedKeysRef,
mergedThemeRef: themeRef,
mergedCheckStrategyRef,
nodePropsRef: toRef(props, 'nodeProps'),
disabledRef: toRef(props, 'disabled'),
checkableRef: toRef(props, 'checkable'),
selectableRef: toRef(props, 'selectable'),
expandOnClickRef: toRef(props, 'expandOnClick'),
onLoadRef: toRef(props, 'onLoad'),
draggableRef: toRef(props, 'draggable'),
blockLineRef: toRef(props, 'blockLine'),
indentRef: toRef(props, 'indent'),
cascadeRef: toRef(props, 'cascade'),
checkOnClickRef: toRef(props, 'checkOnClick'),
checkboxPlacementRef: props.checkboxPlacement,
droppingMouseNodeRef,
droppingNodeParentRef,
draggingNodeRef,
droppingPositionRef,
droppingOffsetLevelRef,
fNodesRef,
pendingNodeKeyRef,
showLineRef: toRef(props, 'showLine'),
disabledFieldRef: toRef(props, 'disabledField'),
internalScrollableRef: toRef(props, 'internalScrollable'),
internalCheckboxFocusableRef: toRef(props, 'internalCheckboxFocusable'),
internalTreeSelect: props.internalTreeSelect,
renderLabelRef: toRef(props, 'renderLabel'),
renderPrefixRef: toRef(props, 'renderPrefix'),
renderSuffixRef: toRef(props, 'renderSuffix'),
renderSwitcherIconRef: toRef(props, 'renderSwitcherIcon'),
labelFieldRef: toRef(props, 'labelField'),
multipleRef: toRef(props, 'multiple'),
overrideDefaultNodeClickBehaviorRef: toRef(props, 'overrideDefaultNodeClickBehavior'),
handleSwitcherClick,
handleDragEnd,
handleDragEnter,
handleDragLeave,
handleDragStart,
handleDrop,
handleDragOver,
handleSelect,
handleCheck
});
function scrollTo(options, y) {
var _a, _b;
if (typeof options === 'number') {
(_a = virtualListInstRef.value) === null || _a === void 0 ? void 0 : _a.scrollTo(options, y || 0);
} else {
(_b = virtualListInstRef.value) === null || _b === void 0 ? void 0 : _b.scrollTo(options);
}
}
const exposedMethods = {
handleKeydown,
scrollTo,
getCheckedData: () => {
if (!props.checkable) return {
keys: [],
options: []
};
const {
checkedKeys
} = checkedStatusRef.value;
return {
keys: checkedKeys,
options: getOptionsByKeys(checkedKeys)
};
},
getIndeterminateData: () => {
if (!props.checkable) return {
keys: [],
options: []
};
const {
indeterminateKeys
} = checkedStatusRef.value;
return {
keys: indeterminateKeys,
options: getOptionsByKeys(indeterminateKeys)
};
}
};
const cssVarsRef = computed(() => {
const {
common: {
cubicBezierEaseInOut
},
self: {
fontSize,
nodeBorderRadius,
nodeColorHover,
nodeColorPressed,
nodeColorActive,
arrowColor,
loadingColor,
nodeTextColor,
nodeTextColorDisabled,
dropMarkColor,
nodeWrapperPadding,
nodeHeight,
lineHeight,
lineColor
}
} = themeRef.value;
const lineOffsetTop = getPadding(nodeWrapperPadding, 'top');
const lineOffsetBottom = getPadding(nodeWrapperPadding, 'bottom');
const nodeContentHeight = pxfy(depx(nodeHeight) - depx(lineOffsetTop) - depx(lineOffsetBottom));
return {
'--n-arrow-color': arrowColor,
'--n-loading-color': loadingColor,
'--n-bezier': cubicBezierEaseInOut,
'--n-font-size': fontSize,
'--n-node-border-radius': nodeBorderRadius,
'--n-node-color-active': nodeColorActive,
'--n-node-color-hover': nodeColorHover,
'--n-node-color-pressed': nodeColorPressed,
'--n-node-text-color': nodeTextColor,
'--n-node-text-color-disabled': nodeTextColorDisabled,
'--n-drop-mark-color': dropMarkColor,
'--n-node-wrapper-padding': nodeWrapperPadding,
'--n-line-offset-top': `-${lineOffsetTop}`,
'--n-line-offset-bottom': `-${lineOffsetBottom}`,
'--n-node-content-height': nodeContentHeight,
'--n-line-height': lineHeight,
'--n-line-color': lineColor
};
});
const themeClassHandle = inlineThemeDisabled ? useThemeClass('tree', undefined, cssVarsRef, props) : undefined;
return Object.assign(Object.assign({}, exposedMethods), {
mergedClsPrefix: mergedClsPrefixRef,
mergedTheme: themeRef,
rtlEnabled: rtlEnabledRef,
fNodes: mergedFNodesRef,
aip: aipRef,
selfElRef,
virtualListInstRef,
scrollbarInstRef,
handleFocusout,
handleDragLeaveTree,
handleScroll,
getScrollContainer,
getScrollContent,
handleAfterEnter,
handleResize,
cssVars: inlineThemeDisabled ? undefined : cssVarsRef,
themeClass: themeClassHandle === null || themeClassHandle === void 0 ? void 0 : themeClassHandle.themeClass,
onRender: themeClassHandle === null || themeClassHandle === void 0 ? void 0 : themeClassHandle.onRender
});
},
render() {
var _a;
const {
fNodes,
internalRenderEmpty
} = this;
if (!fNodes.length && internalRenderEmpty) {
return internalRenderEmpty();
}
const {
mergedClsPrefix,
blockNode,
blockLine,
draggable,
disabled,
internalFocusable,
checkable,
handleKeydown,
rtlEnabled,
handleFocusout,
scrollbarProps
} = this;
const mergedFocusable = internalFocusable && !disabled;
const tabindex = mergedFocusable ? '0' : undefined;
const treeClass = [`${mergedClsPrefix}-tree`, rtlEnabled && `${mergedClsPrefix}-tree--rtl`, checkable && `${mergedClsPrefix}-tree--checkable`, (blockLine || blockNode) && `${mergedClsPrefix}-tree--block-node`, blockLine && `${mergedClsPrefix}-tree--block-line`];
const createNode = tmNode => {
return '__motion' in tmNode ? h(MotionWrapper, {
height: tmNode.height,
nodes: tmNode.nodes,
clsPrefix: mergedClsPrefix,
mode: tmNode.mode,
onAfterEnter: this.handleAfterEnter
}) : h(NTreeNode, {
key: tmNode.key,
tmNode: tmNode,
clsPrefix: mergedClsPrefix
});
};
if (this.virtualScroll) {
const {
mergedTheme,
internalScrollablePadding
} = this;
const padding = getPadding(internalScrollablePadding || '0');
return h(NxScrollbar, Object.assign({}, scrollbarProps, {
ref: "scrollbarInstRef",
onDragleave: draggable ? this.handleDragLeaveTree : undefined,
container: this.getScrollContainer,
content: this.getScrollContent,
class: treeClass,
theme: mergedTheme.peers.Scrollbar,
themeOverrides: mergedTheme.peerOverrides.Scrollbar,
tabindex: tabindex,
onKeydown: mergedFocusable ? handleKeydown : undefined,
onFocusout: mergedFocusable ? handleFocusout : undefined
}), {
default: () => {
var _a;
(_a = this.onRender) === null || _a === void 0 ? void 0 : _a.call(this);
return !fNodes.length ? resolveSlot(this.$slots.empty, () => [h(NEmpty, {
class: `${mergedClsPrefix}-tree__empty`,
theme: this.mergedTheme.peers.Empty,
themeOverrides: this.mergedTheme.peerOverrides.Empty
})]) : h(VVirtualList, {
ref: "virtualListInstRef",
items: this.fNodes,
itemSize: depx(mergedTheme.self.nodeHeight),
ignoreItemResize: this.aip,
paddingTop: padding.top,
paddingBottom: padding.bottom,
class: this.themeClass,
style: [this.cssVars, {
paddingLeft: padding.left,
paddingRight: padding.right
}],
onScroll: this.handleScroll,
onResize: this.handleResize,
showScrollbar: false,
itemResizable: true
}, {
default: ({
item
}) => createNode(item)
});
}
});
}
const {
internalScrollable
} = this;
treeClass.push(this.themeClass);
(_a = this.onRender) === null || _a === void 0 ? void 0 : _a.call(this);
if (internalScrollable) {
return h(NxScrollbar, Object.assign({}, scrollbarProps, {
class: treeClass,
tabindex: tabindex,
onKeydown: mergedFocusable ? handleKeydown : undefined,
onFocusout: mergedFocusable ? handleFocusout : undefined,
style: this.cssVars,
contentStyle: {
padding: this.internalScrollablePadding
}
}), {
default: () => h("div", {
onDragleave: draggable ? this.handleDragLeaveTree : undefined,
ref: "selfElRef"
}, this.fNodes.map(createNode))
});
} else {
return h("div", {
class: treeClass,
tabindex: tabindex,
ref: "selfElRef",
style: this.cssVars,
onKeydown: mergedFocusable ? handleKeydown : undefined,
onFocusout: mergedFocusable ? handleFocusout : undefined,
onDragleave: draggable ? this.handleDragLeaveTree : undefined
}, !fNodes.length ? resolveSlot(this.$slots.empty, () => [h(NEmpty, {
class: `${mergedClsPrefix}-tree__empty`,
theme: this.mergedTheme.peers.Empty,
themeOverrides: this.mergedTheme.peerOverrides.Empty
})]) : fNodes.map(createNode));
}
}
});