411 lines
12 KiB
Vue
411 lines
12 KiB
Vue
<template>
|
||
<List
|
||
class="he-tree"
|
||
:id="nodeKey"
|
||
ref="virtualizationList"
|
||
:items="visibleStats"
|
||
:enabled="virtualization"
|
||
:class="{'he-tree-rtl': rtl}"
|
||
>
|
||
<template #prepend>
|
||
<slot name="prepend" :tree="self"></slot>
|
||
</template>
|
||
<template #default="{ item: stat, index }">
|
||
<TreeNode :vt-index="index" :class="[
|
||
stat.class,
|
||
{
|
||
'drag-placeholder-wrapper': stat.data === placeholderData,
|
||
'dragging-node': stat === dragNode,
|
||
},
|
||
]" :style="stat.style" :stat="stat" :rtl="rtl" :btt="btt" :indent="indent" :table="table" :treeLine="treeLine"
|
||
:treeLineOffset="treeLineOffset" :processor="processor" @click="$emit('click:node', stat)"
|
||
@open="$emit('open:node', $event)" @close="$emit('close:node', $event)" @check="$emit('check:node', $event)">
|
||
<template #default="{ indentStyle }">
|
||
<template v-if="stat.data === placeholderData">
|
||
<view v-if="!table" class="drag-placeholder he-tree-drag-placeholder">
|
||
<slot name="placeholder" :tree="self"></slot>
|
||
</view>
|
||
<td v-else :style="indentStyle" :colspan="placeholderColspan">
|
||
<view class="drag-placeholder he-tree-drag-placeholder">
|
||
<slot name="placeholder" :tree="self"></slot>
|
||
</view>
|
||
</td>
|
||
</template>
|
||
<slot v-else :node="stat.data" :stat="stat" :indentStyle="indentStyle" :tree="self">{{ stat.data[textKey] }}
|
||
</slot>
|
||
</template>
|
||
</TreeNode>
|
||
</template>
|
||
<template #append>
|
||
<slot name="append" :tree="self"></slot>
|
||
</template>
|
||
</List>
|
||
</template>
|
||
|
||
<script lang="ts">
|
||
// 如果遇到滚动不流畅的情况,不用处理,因为Dev tool造成的。
|
||
// If the scrolling is not smooth, do not deal with it, because it is caused by the Dev tool.
|
||
import { PropType, defineComponent, reactive } from "./vue";
|
||
import * as hp from "./helper-js";
|
||
import List from "./list";
|
||
import TreeNode from "./treeNode.vue";
|
||
import { vueMakeTreeProcessor, Stat, TreeProcessor } from "./TreeProcessorVue";
|
||
|
||
export { walkTreeData } from "./helper-js";
|
||
|
||
const cpt = defineComponent({
|
||
components: { List, TreeNode },
|
||
props: {
|
||
// for vue3
|
||
modelValue: { required: true, type: Array as PropType<any[]> },
|
||
updateBehavior: {
|
||
type: String as PropType<"modify" | "new" | "disabled">,
|
||
default: "modify",
|
||
},
|
||
processor: {
|
||
type: Object as PropType<TreeProcessor>,
|
||
default: () =>
|
||
vueMakeTreeProcessor([], {
|
||
noInitialization: true,
|
||
}),
|
||
},
|
||
childrenKey: { type: String, default: "children" },
|
||
/**
|
||
* for default slot. 用于默认插槽
|
||
*/
|
||
textKey: { type: String, default: "text" },
|
||
/**
|
||
* node indent. 节点缩进
|
||
*/
|
||
indent: { type: Number, default: 20 },
|
||
/**
|
||
* Enable virtual list. 启用虚拟列表
|
||
*/
|
||
virtualization: { type: Boolean, default: false },
|
||
/**
|
||
* Render count for virtual list at start. 虚拟列表初始渲染数量.
|
||
*/
|
||
virtualizationPrerenderCount: { type: Number, default: 20 },
|
||
/**
|
||
* Open all nodes by default. 默认打开所有节点.
|
||
*/
|
||
defaultOpen: { type: Boolean, default: true },
|
||
statHandler: { type: Function as PropType<(stat: Stat<any>) => Stat<any>> },
|
||
/**
|
||
* From right to left. 由右向左显示.
|
||
*/
|
||
rtl: { type: Boolean, default: false },
|
||
/**
|
||
* From bottom to top. 由下向上显示
|
||
*/
|
||
btt: { type: Boolean, default: false },
|
||
/**
|
||
* Display as table
|
||
*/
|
||
table: { type: Boolean, default: false },
|
||
watermark: { type: Boolean, default: false },
|
||
nodeKey: {
|
||
type: [String, Function] as PropType<
|
||
"index" | ((stat: Stat<any>, index: number) => any)
|
||
>,
|
||
default: 'index',
|
||
},
|
||
treeLine: { type: Boolean, default: false },
|
||
treeLineOffset: { type: Number, default: 8 },
|
||
},
|
||
emits: [
|
||
"update:modelValue",
|
||
"click:node",
|
||
"open:node",
|
||
"close:node",
|
||
"check:node",
|
||
"beforeDragStart",
|
||
"before-drag-start",
|
||
"after-drop",
|
||
"change",
|
||
"enter",
|
||
"leave",
|
||
],
|
||
data() {
|
||
return {
|
||
treeID: hp.randString(),
|
||
stats: [],
|
||
statsFlat: [],
|
||
dragNode: null,
|
||
dragOvering: false,
|
||
placeholderData: {},
|
||
placeholderColspan: 1,
|
||
batchUpdateWaiting: false,
|
||
self: this,
|
||
_ignoreValueChangeOnce: false,
|
||
} as {
|
||
treeID: any,
|
||
stats: Exclude<TreeProcessor["stats"], null>;
|
||
statsFlat: Exclude<TreeProcessor["statsFlat"], null>;
|
||
dragNode: Stat<any> | null;
|
||
dragOvering: boolean;
|
||
placeholderData: {};
|
||
placeholderColspan: number;
|
||
batchUpdateWaiting: boolean;
|
||
self: any;
|
||
_ignoreValueChangeOnce: boolean;
|
||
};
|
||
},
|
||
computed: {
|
||
valueComputed() {
|
||
return (this.modelValue) || [];
|
||
},
|
||
visibleStats() {
|
||
const { statsFlat, isVisible } = this;
|
||
let items = statsFlat;
|
||
if (this.btt) {
|
||
items = items.slice();
|
||
items.reverse();
|
||
}
|
||
return items.filter((stat) => isVisible(stat));
|
||
},
|
||
rootChildren() {
|
||
return this.stats
|
||
},
|
||
},
|
||
methods: {
|
||
_emitValue(value: any[]) {
|
||
// @ts-ignore
|
||
this.$emit("update:modelValue", value);
|
||
},
|
||
/**
|
||
* private method
|
||
* @param value
|
||
*/
|
||
_updateValue(value: any[]) {
|
||
if (this.updateBehavior === "disabled") {
|
||
return false;
|
||
}
|
||
// if value changed, ignore change once
|
||
if (value !== this.valueComputed) {
|
||
this._ignoreValueChangeOnce = true;
|
||
}
|
||
this._emitValue(value);
|
||
return true;
|
||
},
|
||
getStat: reactiveFirstArg(
|
||
processorMethodProxy("getStat")
|
||
) as TreeProcessor["getStat"],
|
||
has: reactiveFirstArg(processorMethodProxy("has")) as TreeProcessor["has"],
|
||
updateCheck: processorMethodProxy(
|
||
"updateCheck"
|
||
) as TreeProcessor["updateCheck"],
|
||
getChecked: processorMethodProxy(
|
||
"getChecked"
|
||
) as TreeProcessor["getChecked"],
|
||
getUnchecked: processorMethodProxy(
|
||
"getUnchecked"
|
||
) as TreeProcessor["getUnchecked"],
|
||
openAll: processorMethodProxy("openAll") as TreeProcessor["openAll"],
|
||
closeAll: processorMethodProxy("closeAll") as TreeProcessor["closeAll"],
|
||
openNodeAndParents: processorMethodProxy("openNodeAndParents") as TreeProcessor["openNodeAndParents"],
|
||
isVisible: processorMethodProxy("isVisible") as TreeProcessor["isVisible"],
|
||
move: processorMethodProxyWithBatchUpdate("move") as TreeProcessor["move"],
|
||
add: reactiveFirstArg(
|
||
processorMethodProxyWithBatchUpdate("add")
|
||
) as TreeProcessor["add"],
|
||
addMulti(
|
||
dataArr: any[],
|
||
parent?: Stat<any> | null,
|
||
startIndex?: number | null
|
||
) {
|
||
this.batchUpdate(() => {
|
||
let index = startIndex;
|
||
for (const data of dataArr) {
|
||
this.add(data, parent, index);
|
||
if (index != null) {
|
||
index++;
|
||
}
|
||
}
|
||
});
|
||
},
|
||
remove: processorMethodProxy("remove") as TreeProcessor["remove"],
|
||
removeMulti(dataArr: any[]) {
|
||
let cloned = [...dataArr];
|
||
this.batchUpdate(() => {
|
||
for (const data of cloned) {
|
||
this.remove(data);
|
||
}
|
||
});
|
||
},
|
||
iterateParent: processorMethodProxy(
|
||
"iterateParent"
|
||
) as TreeProcessor["iterateParent"],
|
||
getSiblings: processorMethodProxy(
|
||
"getSiblings"
|
||
) as TreeProcessor["getSiblings"],
|
||
getData: processorMethodProxy("getData") as hp.ReplaceReturnType<
|
||
TreeProcessor["getData"],
|
||
any[]
|
||
>,
|
||
getRootEl() {
|
||
// @ts-ignore
|
||
return this.$refs.vtlist.listElRef as HTMLElement;
|
||
},
|
||
batchUpdate(task: () => any | Promise<any>) {
|
||
const r = this.ignoreUpdate(task);
|
||
if (!this.batchUpdateWaiting) {
|
||
this._updateValue(
|
||
this.updateBehavior === "new" ? this.getData() : this.valueComputed
|
||
);
|
||
}
|
||
return r;
|
||
},
|
||
ignoreUpdate(task: () => any | Promise<any>) {
|
||
const old = this.batchUpdateWaiting;
|
||
this.batchUpdateWaiting = true;
|
||
const r = task();
|
||
this.batchUpdateWaiting = old;
|
||
return r;
|
||
},
|
||
},
|
||
watch: {
|
||
processor: {
|
||
immediate: true,
|
||
handler(processor: typeof this.processor) {
|
||
if (processor) {
|
||
// hook
|
||
const getNodeDataChildren = (nodeData: any): any[] => {
|
||
if (!nodeData) {
|
||
return this.valueComputed;
|
||
} else {
|
||
const { childrenKey } = this;
|
||
if (!nodeData[childrenKey]) {
|
||
nodeData[childrenKey] = [];
|
||
}
|
||
return nodeData[childrenKey];
|
||
}
|
||
};
|
||
processor["_statHandler2"] = this.statHandler
|
||
? (stat) => {
|
||
if (stat.data === this.placeholderData) {
|
||
return stat;
|
||
}
|
||
return this.statHandler!(stat);
|
||
}
|
||
: null;
|
||
processor.afterSetStat = (stat, parent, index) => {
|
||
const { childrenKey, updateBehavior } = this;
|
||
let value = this.valueComputed;
|
||
if (updateBehavior === "new") {
|
||
if (this.batchUpdateWaiting) {
|
||
return;
|
||
}
|
||
value = this.getData();
|
||
} else if (updateBehavior === "modify") {
|
||
const siblings = getNodeDataChildren(parent?.data);
|
||
if (siblings.includes(stat.data)) {
|
||
// when call add -> add child -> _setPositionm ignore because the child already in parent.children
|
||
} else {
|
||
siblings.splice(index, 0, stat.data);
|
||
}
|
||
} else if (updateBehavior === "disabled") {
|
||
}
|
||
if (this.batchUpdateWaiting) {
|
||
return;
|
||
}
|
||
this._updateValue(value);
|
||
};
|
||
processor.afterRemoveStat = (stat) => {
|
||
const { childrenKey, updateBehavior } = this;
|
||
let value = this.valueComputed;
|
||
if (updateBehavior === "new") {
|
||
if (this.batchUpdateWaiting) {
|
||
return;
|
||
}
|
||
value = this.getData();
|
||
} else if (updateBehavior === "modify") {
|
||
const siblings = getNodeDataChildren(stat.parent?.data);
|
||
hp.arrayRemove(siblings, stat.data);
|
||
} else if (updateBehavior === "disabled") {
|
||
}
|
||
if (this.batchUpdateWaiting) {
|
||
return;
|
||
}
|
||
this._updateValue(value);
|
||
};
|
||
}
|
||
if (!processor.initialized) {
|
||
processor.data = this.valueComputed;
|
||
Object.assign(
|
||
processor,
|
||
hp.objectOnly(this, ["childrenKey", "defaultOpen"])
|
||
);
|
||
processor.init();
|
||
processor.updateCheck();
|
||
}
|
||
this.stats = processor.stats!;
|
||
this.statsFlat = processor.statsFlat!;
|
||
if (processor.data !== this.valueComputed) {
|
||
this._updateValue(processor.data);
|
||
}
|
||
},
|
||
},
|
||
valueComputed: {
|
||
handler(value) {
|
||
// isDragging triggered in Vue2 because its array is not same with Vue3
|
||
const isDragging = this.dragOvering || this.dragNode
|
||
if (isDragging || this._ignoreValueChangeOnce) {
|
||
this._ignoreValueChangeOnce = false;
|
||
} else {
|
||
const { processor } = this;
|
||
processor.data = value;
|
||
processor.init();
|
||
this.stats = processor.stats!;
|
||
this.statsFlat = processor.statsFlat!;
|
||
}
|
||
},
|
||
},
|
||
},
|
||
created() { },
|
||
mounted() {
|
||
|
||
},
|
||
});
|
||
|
||
export default cpt;
|
||
export type BaseTreeType = InstanceType<typeof cpt>;
|
||
|
||
function processorMethodProxy(name: string) {
|
||
return function (...args) {
|
||
// @ts-ignore
|
||
return this.processor[name](...args);
|
||
};
|
||
}
|
||
function processorMethodProxyWithBatchUpdate(name: string) {
|
||
return function (...args) {
|
||
// @ts-ignore
|
||
return this.batchUpdate(() => {
|
||
// @ts-ignore
|
||
return this.processor[name](...args);
|
||
});
|
||
};
|
||
}
|
||
function reactiveFirstArg(func: any) {
|
||
return function (arg1, ...args) {
|
||
if (arg1) {
|
||
arg1 = reactive(arg1);
|
||
}
|
||
// @ts-ignore
|
||
return func.call(this, arg1, ...args);
|
||
};
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
.he-tree--rtl {
|
||
direction: rtl;
|
||
}
|
||
|
||
.he-tree-drag-placeholder {
|
||
background: #ddf2f9;
|
||
border: 1px dashed #00d9ff;
|
||
height: 22px;
|
||
width: 100%;
|
||
}
|
||
</style> |