feat: 初始化

This commit is contained in:
George
2025-07-07 15:55:44 +08:00
commit 9b7bfcfe5a
969 changed files with 123036 additions and 0 deletions

View File

@ -0,0 +1,25 @@
import { makeTreeProcessor, Options } from "./tree-utils";
export * from "./tree-utils";
import { reactive } from "./vue";
export function vueMakeTreeProcessor<T>(data: T[], options: Options = {}) {
const opt = {
...options,
statHandler(input) {
if (this["_statHandler2"]) {
input = this["_statHandler2"](input);
}
return filter(options.statHandler, reactive(input));
},
statsHandler(input) {
return filter(options.statsHandler, reactive(input));
},
statsFlatHandler(input) {
return filter(options.statsFlatHandler, reactive(input));
},
};
return makeTreeProcessor(data, opt);
}
function filter<T>(func: Function | null | undefined, input: T): T {
return func ? func(input) : input;
}

View File

@ -0,0 +1,411 @@
<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>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,140 @@
<template>
<view class="VirtualizationList virtualization-list" @scroll.passive="update" >
<component
:is="listTag"
:class="listClass"
>
<template v-for="(info, i) in visibleItems" :key="info.item.$id">
<slot
:item="info.item"
:index="info.index"
:renderIndex="i"
:itemStyle="{ marginBottom: gap + 'px' }"
{{ info.item.text }}
></slot>
</template>
</component>
</view>
</template>
<script lang="ts">
import { defineComponent, PropType, nextTick } from "./vue"
import { obj } from "./types"
export default defineComponent({
props: {
items: { type: Array as PropType<obj[]>, default: () => [] },
enabled: { type: Boolean, default: true },
buffer: {
type: Number,
default: 200,
},
minItemHeight: { type: Number, default: 20 },
prerender: { type: Number, default: 20 },
listTag: { type: String, default: "view" },
listClass: { type: String },
itemClass: { type: String, default: "vl-item" },
gap: { type: Number, default: 0 },
afterCalcTop2: { type: Function as PropType<(top2: number) => number> },
isForceVisible: {
type: Function as PropType<(node: obj, index: number) => boolean>,
},
},
data() {
return {
start: 0,
end: -1,
top: 0,
bottom: 0,
totalHeight: 0,
itemsHeight: <number[]>[],
mountedPromise: new Promise((resolve, reject) => {
this._mountedPromise_resolve = resolve;
}),
};
},
computed: {
visibleItems(): { item: obj; index: number }[] {
const r: { item: obj; index: number }[] = [];
console.log(this.items)
this.items.forEach((item: obj, index: number) => {
if (!this.enabled) {
r.push({ item, index });
} else if (
(index >= this.start && index <= this.end) ||
(this.isForceVisible && this.isForceVisible(item, index))
) {
r.push({ item, index });
}
});
console.log(r)
return r;
},
},
watch: {
enabled: {
immediate: true,
handler() {
if (!this.enabled) {
// @ts-ignore
this.totalHeight = undefined;
}
},
},
},
methods: {
getItemElHeight(el: UniApp.NodesRef) {
return new Promise((resolve, reject) => {
el.boundingClientRect(res => {
if(res) {
resolve(res)
} else {
reject(res)
}
}).exec()
})
},
update() {
const task = async () => {
this.start = 0;
this.end = this.items.length;
}
}
},
created() {
this.bottom = this.prerender - 1;
},
mounted() {
// @ts-ignore
this._mountedPromise_resolve!(null);
let updatedOnce = false;
this.$watch(
() => [this.items],
() => {
this.itemsHeight = [];
nextTick(() => {
this.update();
if (!updatedOnce) {
this.update();
}
updatedOnce = true;
});
},
{ immediate: true }
);
this.$watch(
() => [this.buffer],
() => {
this.update();
}
);
}
})
</script>
<style lang="scss">
.vl-items {
overflow: hidden;
box-sizing: border-box;
}
</style>

View File

@ -0,0 +1 @@
.tree-node--with-tree-line{position:relative}.tree-line{position:absolute;background-color:#bbb}.tree-vline{width:1px;top:0;bottom:0}.tree-hline{height:1px;top:50%;width:10px}.he-tree--rtl{direction:rtl}.he-tree-drag-placeholder{background:#ddf2f9;border:1px dashed #00d9ff;height:22px;width:100%}.he-tree__open-icon{cursor:pointer;-webkit-user-select:none;user-select:none;display:inline-block}.he-tree__open-icon.open{transform:rotate(90deg)}.he-tree__open-icon svg{width:1em}

View File

@ -0,0 +1,58 @@
.mtl-tree .tree-node-inner {
display: flex;
align-items: center;
font-size: 14px;
}
.mtl-tree .tree-node {
padding: 1px 0;
width: 100%;
}
.mtl-tree .tree-node:hover {
background-color: #ededed;
/* recommend: active #ddeff9, active & hover: #cfe6f2 */
}
.mtl-checkbox {
width: 14px;
height: 14px;
}
.mtl-ml {
margin-left: 4px;
}
.mtl-mr {
margin-right: 4px;
}
.mtl-tree table {
width: 100%;
border-collapse: collapse;
border-spacing: 0;
}
.mtl-tree td,
.mtl-tree th {
border-bottom: 1px solid rgba(224, 224, 224, 1);
line-height: 1.5;
}
.mtl-tree tr:last-child td,
.mtl-tree tr:last-child tr {
border-bottom: 0px;
}
.mtl-text-left {
text-align: left;
}
.mtl-text-center {
text-align: center;
}
.mtl-text-right {
text-align: right;
}

View File

@ -0,0 +1,432 @@
import * as hp from "./helper-js";
export const CHILDREN = "children"; // inner childrenKey
/**
* help to handle tree data. 帮助处理树形数据.
*/
export function makeTreeProcessor<T>(data: T[], opt: Options = {}) {
const opt2 = opt as Required<Options>;
const utilsBase = {
...defaultOptions,
...opt2,
data,
stats: null as Stat<T>[] | null,
statsFlat: null as Stat<T>[] | null,
_statsMap: null as Map<T, Stat<T>> | null,
initialized: false,
init() {
const { data, childrenKey } = this;
const td = new hp.TreeData([] as Stat<T>[]);
this._statsMap = new Map();
hp.walkTreeData(
data,
(nodeData, index, parent, path) => {
const stat = this.statHandler({
...statDefault(),
data: nodeData,
open: Boolean(this.defaultOpen),
parent: td.getParent(path),
children: [],
level: path.length,
});
this._statsMap!.set(nodeData, stat);
td.set(path, stat);
},
{ childrenKey }
);
const statsFlat: typeof td.rootChildren = [];
td.walk((stat) => {
statsFlat.push(stat);
});
this.stats = this.statsHandler(td.rootChildren);
this.statsFlat = this.statsFlatHandler(statsFlat);
this.initialized = true;
},
getStat(nodeData: T) {
let r: Stat<T> = this._statsMap!.get(nodeData)!;
if (!r) {
throw new StatNotFoundError(`Stat not found`);
}
return r;
},
has(nodeData: T | Stat<T>) {
if (nodeData["isStat"]) {
// @ts-ignore
return this.statsFlat.indexOf(nodeData) > -1;
} else {
try {
// @ts-ignore
let r = this.getStat(nodeData);
return Boolean(r);
} catch (error) {
if (error instanceof StatNotFoundError) {
return false;
}
throw error;
}
}
},
_getPathByStat(stat: Stat<T> | null) {
if (stat == null) {
return [];
}
const siblings = this.getSiblings(stat);
const index = siblings.indexOf(stat);
return [...(stat.parent ? this._getPathByStat(stat.parent) : []), index];
},
/**
* call it after a stat's `checked` changed
* @param stat
* @returns return false mean ignored
*/
afterOneCheckChanged(stat: Stat<T>) {
const { checked } = stat;
if (stat._ignoreCheckedOnce) {
delete stat._ignoreCheckedOnce;
return false;
}
// change parent
const checkParent = (stat: any) => {
const { parent } = stat;
if (parent) {
let hasChecked;
let hasUnchecked;
for (const child of parent.children) {
if (child.checked || child.checked === 0) {
hasChecked = true;
} else {
hasUnchecked = true;
if (hasChecked && hasUnchecked) {
break;
}
}
}
const parentChecked = !hasUnchecked ? true : hasChecked ? 0 : false;
if (parent.checked !== parentChecked) {
this._ignoreCheckedOnce(parent);
parent.checked = parentChecked;
}
checkParent(parent);
}
};
checkParent(stat);
// change children
hp.walkTreeData(
stat.children,
(child) => {
if (child.checked !== checked) {
this._ignoreCheckedOnce(child);
child.checked = checked;
}
},
{ childrenKey: CHILDREN }
);
return true;
},
_ignoreCheckedOnce(stat: Stat<T>) {
stat._ignoreCheckedOnce = true;
// cancel ignore immediately if not triggered
setTimeout(() => {
if (stat._ignoreCheckedOnce) {
stat._ignoreCheckedOnce = false;
}
}, 100);
},
isVisible(statOrNodeData: T | Stat<T>) {
// @ts-ignore
const stat: Stat<T> = statOrNodeData["isStat"] ? statOrNodeData : this.getStat(statOrNodeData); // prettier-ignore
const walk = (stat: Stat<T> | null) => {
return !stat || (!stat.hidden && stat.open && walk(stat.parent));
};
return Boolean(!stat.hidden && walk(stat.parent));
},
/**
* call it to update all stats' `checked`
*/
updateCheck() {
hp.walkTreeData(
this.stats!,
(stat) => {
if (stat.children && stat.children.length > 0) {
const checked = stat.children.every((v) => v.checked);
if (stat.checked !== checked) {
this._ignoreCheckedOnce(stat);
stat.checked = checked;
}
}
},
{ childFirst: true, childrenKey: CHILDREN }
);
},
getChecked(withDemi = false) {
return this.statsFlat!.filter((v) => {
return v.checked || (withDemi && v.checked === 0);
});
},
getUnchecked(withDemi = true) {
return this.statsFlat!.filter((v) => {
return withDemi ? !v.checked : v.checked === false;
});
},
/**
* open all nodes
*/
openAll() {
for (const stat of this.statsFlat!) {
stat.open = true;
}
},
/**
* close all nodes
*/
closeAll() {
for (const stat of this.statsFlat!) {
stat.open = false;
}
},
openNodeAndParents(nodeOrStat: T | Stat<T>) {
// @ts-ignore
const stat:Stat<T> = nodeOrStat["isStat"] ? nodeOrStat : this.getStat(nodeOrStat) // prettier-ignore
for (const parentStat of this.iterateParent(stat, {
withSelf: true,
})) {
parentStat.open = true;
}
},
// actions
_calcFlatIndex(parent: Stat<T> | null, index: number) {
let flatIndex = parent ? this.statsFlat!.indexOf(parent) + 1 : 0;
const siblings = parent ? parent.children : this.stats!;
for (let i = 0; i < index; i++) {
flatIndex += this._count(siblings[i]);
}
return flatIndex;
},
add(nodeData: T, parent?: Stat<T> | null, index?: number | null) {
if (this.has(nodeData)) {
throw `Can't add because data exists in tree`;
}
const siblings = parent ? parent.children : this.stats!;
if (index == null) {
index = siblings.length;
}
const stat: Stat<T> = this.statHandler({
...statDefault(),
open: Boolean(this.defaultOpen),
data: nodeData,
parent: parent || null,
children: [],
level: parent ? parent.level + 1 : 1,
});
this._setPosition(stat, parent || null, index);
const children = nodeData[this.childrenKey];
if (children) {
const childrenSnap = children.slice();
for (const child of childrenSnap) {
this.add(child, stat);
}
}
},
remove(stat: Stat<T>) {
const siblings = this.getSiblings(stat);
if (siblings.includes(stat)) {
hp.arrayRemove(siblings, stat);
const stats = this._flat(stat);
this.statsFlat!.splice(this.statsFlat!.indexOf(stat), stats.length);
for (const stat of stats) {
this._statsMap!.delete(stat.data);
}
this.afterRemoveStat(stat);
return true;
}
return false;
},
getSiblings(stat: Stat<T>) {
const { parent } = stat;
return parent ? parent.children : this.stats!;
},
/**
* The node should not exsit.
* @param node
* @param parent
* @param index
*/
_setPosition(stat: Stat<T>, parent: Stat<T> | null, index: number) {
const siblings = parent ? parent.children : this.stats!;
siblings.splice(index, 0, stat);
stat.parent = parent;
stat.level = parent ? parent.level + 1 : 1;
const flatIndex = this._calcFlatIndex(parent, index);
const stats = this._flat(stat);
this.statsFlat!.splice(flatIndex, 0, ...stats);
for (const stat of stats) {
if (!this._statsMap!.has(stat.data)) {
this._statsMap!.set(stat.data, stat);
}
}
hp.walkTreeData(
stat,
(node, index, parent) => {
if (parent) {
node.level = parent.level + 1;
}
},
{ childrenKey: CHILDREN }
);
this.afterSetStat(stat, parent, index);
},
*iterateParent(stat: Stat<T>, opt?: { withSelf: boolean }) {
let t = opt?.withSelf ? stat : stat.parent;
while (t) {
yield t;
t = t.parent;
}
},
move(stat: Stat<T>, parent: Stat<T> | null, index: number) {
if (this.has(stat)) {
if (
stat.parent === parent &&
this.getSiblings(stat).indexOf(stat) === index
) {
return false;
}
// check if is self
if (stat === parent) {
// 不允许移动目标为自己
throw new Error(`Can't move node to it self`);
}
// check if is descendant
if (parent && stat.level < parent.level) {
let t;
for (const item of this.iterateParent(parent)) {
if (item.level === stat.level) {
t = item;
break;
}
}
if (stat === t) {
// 不允许移动节点到其后代节点下
throw new Error(`Can't move node to its descendant`);
}
}
this.remove(stat);
}
this._setPosition(stat, parent, index);
return true;
},
/**
* convert stat and its children to one-dimensional array
* 转换节点和其后代节点为一维数组
* @param stat
* @returns
*/
_flat(stat: Stat<T>) {
const r: Stat<T>[] = [];
hp.walkTreeData(
stat,
(child) => {
r.push(child);
},
{ childrenKey: CHILDREN }
);
return r;
},
/**
* get count of stat and its all children
* 统计节点和其后代节点数量
* @param stat
*/
_count(stat: Stat<T>) {
return this._flat(stat).length;
},
getData(filter?: (data: T) => T, root?: Stat<T>) {
const { childrenKey } = this;
const td = new hp.TreeData<T>([]);
td.childrenKey = childrenKey;
hp.walkTreeData(
root || this.stats!,
(stat, index, parent, path) => {
let newData = { ...stat.data, [childrenKey]: [] };
if (filter) {
// @ts-ignore
newData = filter(newData);
}
td.set(path, newData);
},
{
childrenKey: CHILDREN,
}
);
return td.data;
},
};
type Base = typeof utilsBase;
const utils: Base & {} = utilsBase;
if (!utilsBase.noInitialization) {
utils.init();
}
return utils;
}
export type TreeProcessor = ReturnType<typeof makeTreeProcessor>;
export const defaultOptions = {
childrenKey: "children",
defaultOpen: false,
statsHandler(stats: Stat<any>[]) {
return stats;
},
statsFlatHandler(statsFlat: Stat<any>[]) {
return statsFlat;
},
afterSetStat(stat: Stat<any>, parent: Stat<any> | null, index: number) {},
afterRemoveStat(stat: Stat<any>) {},
statHandler(stat: Stat<any>) {
return stat;
},
};
export interface Options extends Partial<typeof defaultOptions> {
/**
* don't call init. You can call it manually later.
*/
noInitialization?: boolean;
}
export interface Stat<T> extends ReturnType<typeof statDefault> {
[x: string]: any;
data: T;
open: boolean;
parent: Stat<T> | null;
children: Stat<T>[];
level: number;
}
export function statDefault() {
return {
isStat: true,
hidden: false,
checked: false,
style: null,
class: null,
draggable: null,
droppable: null,
} as {
isStat: true;
hidden: boolean;
checked: boolean | 0; // 0 mean just part of children checked
draggable: boolean | null; //null mean inhert parent
droppable: boolean | null; //null mean inhert parent
style: any;
class: any;
};
}
class StatNotFoundError extends Error {
constructor(message: string) {
super(message);
this.name = "StatNotFoundError";
}
}

View File

@ -0,0 +1,158 @@
<template>
<view v-if="!table" class="tree-node" :class="{ 'tree-node--with-tree-line': treeLine }" :style="indentStyle" ref="el">
<template v-if="treeLine">
<view v-for="line in vLines" class="tree-line tree-vline" :style="line.style"></view>
<view v-if="stat.level > 1" class="tree-line tree-hline" :style="hLineStyle"></view>
</template>
<view class="tree-node-inner">
<slot :indentStyle="indentStyle"></slot>
</view>
</view>
<tr v-else class="tree-node" ref="el">
<slot :indentStyle="indentStyle"></slot>
</tr>
</template>
<script lang="ts">
import { defineComponent, computed, watch } from "./vue"
let justToggleOpen = false
const afterToggleOpen = () => {
justToggleOpen = true
setTimeout(() => {
justToggleOpen = false
}, 100)
}
const cpt = defineComponent({
// components: {},
props: ["stat", "rtl", "btt", "indent", "table", "treeLine", "treeLineOffset", "processor"],
emits: ["open", "close", "check"],
setup(props, { emit }) {
const indentStyle = computed(() => {
return {
[!props.rtl ? "paddingLeft" : "paddingRight"]:
props.indent * (props.stat.level - 1) + "px",
};
});
// watch checked
watch(
() => props.stat.checked,
(checked) => {
// fix issue: https://github.com/phphe/he-tree/issues/98
// when open/close above node, the after nodes' states 'checked' and 'open' will be updated. It should be caused by Vue's key. We don't use Vue's key prop.
if (justToggleOpen) {
return
}
if (props.processor.afterOneCheckChanged(props.stat)) {
emit("check", props.stat);
}
}
);
// watch open
watch(
() => props.stat.open,
(open) => {
if (justToggleOpen) {
return
}
if (open) {
emit("open", props.stat);
} else {
emit("close", props.stat);
}
afterToggleOpen()
}
);
// tree lines
const vLines = computed(() => {
const lines: { style: object }[] = [];
const hasNextVisibleNode = (stat) => {
if (stat.parent) {
let i = stat.parent?.children.indexOf(stat);
do {
i++
let next = stat.parent.children[i]
if (next) {
if (!next.hidden) {
return true
}
} else {
break
}
} while (true);
}
return false
}
const leftOrRight = props.rtl ? 'right' : 'left'
const bottomOrTop = props.btt ? 'top' : 'bottom'
let current = props.stat
while (current) {
let left = (current.level - 2) * props.indent + props.treeLineOffset
const hasNext = hasNextVisibleNode(current)
const addLine = () => {
lines.push({
style: {
[leftOrRight]: left + 'px',
[bottomOrTop]: hasNext ? 0 : '50%',
}
})
}
if (current === props.stat) {
if (current.level > 1) {
addLine()
}
} else if (hasNext) {
addLine()
}
current = current.parent
}
return lines
})
const hLineStyle = computed(() => {
let left = (props.stat.level - 2) * props.indent + props.treeLineOffset
const leftOrRight = props.rtl ? 'right' : 'left'
return {
[leftOrRight]: left + 'px',
}
})
return { indentStyle, vLines, hLineStyle, }
},
// data() {
// return {}
// },
// computed: {},
// watch: {},
// methods: {},
// created() {},
// mounted() {}
});
export default cpt;
export type TreeNodeType = InstanceType<typeof cpt>;
</script>
<style>
/* tree line start */
.tree-node--with-tree-line {
position: relative;
}
.tree-line {
position: absolute;
background-color: #bbbbbb;
}
.tree-vline {
width: 1px;
top: 0;
bottom: 0;
}
.tree-hline {
height: 1px;
top: 50%;
width: 10px;
}
/* tree line end */
</style>

View File

@ -0,0 +1,22 @@
export type obj = Record<string, unknown>; // equal to object
export interface BaseNode {
$id: string | number;
$pid?: string | number;
$level: number; // 0 is root
$hidden?: boolean;
$folded?: boolean;
$checked?: boolean | 0;
$children: Node[];
$childrenLoading?: boolean;
$childrenLoadStaus?: obj; // private
$draggable?: boolean;
$droppable?: boolean;
// style
$nodeStyle?: string | Record<string, string> | unknown;
$nodeClass?: string | unknown;
$outerStyle?: string | Record<string, string> | unknown;
$outerClass?: string | unknown;
}
export type Node = obj & BaseNode;

View File

@ -0,0 +1,91 @@
import * as hp from "./helper-js";
import { obj, BaseNode } from "./types";
export function genNodeID() {
return `ht_${hp.randString(12)}`;
}
export function initNode(node: obj) {
if (!node.$id) {
node.$id = genNodeID();
}
if (!node.$children) {
node.$children = [];
}
}
export function convertTreeDataToFlat<T extends obj>(
data: T[],
childrenKey = "children",
idKey = "id"
) {
const flatData: T[] = [];
const mapForPid = new Map();
hp.walkTreeData(
data,
(node, index, parent) => {
const newNode = { $id: node[idKey], $pid: "", ...node };
initNode(newNode);
mapForPid.set(node, newNode.$id);
newNode.$pid = (parent && mapForPid.get(parent)) || null;
flatData.push(newNode);
},
childrenKey
);
return convertFlatDataToStandard(flatData, "$id", "$pid");
}
export function convertFlatDataToStandard<T extends obj>(
data: T[],
idKey = "id",
pidKey = "parent_id"
) {
const nodesByID: Record<string, T & BaseNode> = {};
let nodes = data.map((node) => {
// @ts-ignore
const newNode: T & BaseNode = {
$id: <string>node[idKey],
$pid: <string>node[pidKey],
...node,
$children: [],
};
initNode(newNode);
nodesByID[<string>newNode.$id] = newNode;
return newNode;
});
const top = [];
for (const node of nodes) {
if (node.$level == null) {
node.$level = resolveLevel(node);
}
const parent = node.$pid && nodesByID[node.$pid];
if (parent) {
parent.$children.push(node);
}
if (node.$level === 1) {
top.push(node);
}
}
nodes = [];
hp.walkTreeData(
top,
(node) => {
nodes.push(node);
},
"$children"
);
return { nodes, nodesByID };
//
function resolveLevel(node: T & BaseNode): number {
if (node.$level && node.$level > 0) {
return node.$level;
} else {
const parent = nodesByID[node.$pid || ""];
if (!parent) {
return 1;
} else {
return resolveLevel(parent) + 1;
}
}
}
}

View File

@ -0,0 +1,16 @@
// @ts-nocheck
// #ifdef VUE3
export * from 'vue';
// #endif
// #ifndef VUE3
export * from '@vue/composition-api';
// #ifdef APP-NVUE
import Vue from 'vue'
import VueCompositionAPI from '@vue/composition-api'
Vue.use(VueCompositionAPI)
// #endif
// #endif