@splicetree/plugin-checkable 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,25 @@
1
+ # @splicetree/plugin-checkable
2
+
3
+ ## 0.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - chore: 搭建 SpliceTree 框架基础架构与工程体系
8
+
9
+ 框架架构:
10
+ - 建立无头树(Headless Tree)核心模型与数据结构
11
+ - 定义节点操作 API、事件系统和状态管理机制
12
+ - 设计并实现插件体系(生命周期、注册机制、能力扩展)
13
+ - 引入模块化架构,明确 core / plugins / adapters 的边界
14
+ - 预留扩展点与内部协议,构建可插拔式架构基础
15
+
16
+ 适配层与生态:
17
+ - 添加 Vue 3 适配层(渲染无关、纯接口绑定)
18
+ - 设计独立的 UI 层解耦策略,确保跨框架可迁移
19
+ - 预留未来 React/Svelte/WebComponents 的适配接口
20
+
21
+ 工程化与工具链:
22
+ - 初始化 monorepo 工程结构(packages + docs)
23
+ - 配置构建工具链 tsdown(多包构建、类型输出)
24
+ - 构建文档系统(VitePress)与基础导航结构
25
+ - 设置开发环境、代码规范(ESLint)、格式化流程
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Michael Cocova
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # @splicetree/plugin-checkable
2
+
3
+ [![version](https://img.shields.io/npm/v/@splicetree/plugin-checkable.svg?label=version)](https://www.npmjs.com/package/@splicetree/plugin-checkable)
4
+ [![downloads](https://img.shields.io/npm/dm/@splicetree/plugin-checkable.svg)](https://npmcharts.com/compare/%40splicetree%2Fplugin-checkable?minimal=true)
5
+ [![license](https://img.shields.io/npm/l/@splicetree/plugin-checkable.svg)](https://www.npmjs.com/package/@splicetree/plugin-checkable)
6
+ [![Website](https://img.shields.io/static/v1?label=Website&message=splicetree.dev&color=blue)](https://www.splicetree.dev)
7
+ [![GitHub](https://img.shields.io/static/v1?label=GitHub&message=splicetree%2Fsplicetree&logo=github)](https://github.com/michaelcocova/splicetree)
8
+
9
+ 为树节点提供勾选与半选能力,支持向下级联与向上计算半选。
10
+
11
+ ## 安装
12
+
13
+ `pnpm add @splicetree/plugin-checkable`
14
+
15
+ ## 使用
16
+
17
+ ```ts
18
+ import { createSpliceTree } from '@splicetree/core'
19
+ import checkable from '@splicetree/plugin-checkable'
20
+
21
+ const tree = createSpliceTree(data, {
22
+ plugins: [checkable],
23
+ defaultExpanded: ['a'],
24
+ defaultChecked: ['a'],
25
+ })
26
+ ```
27
+
28
+ ## Api
29
+
30
+ ### Options
31
+
32
+ | 选项 | 类型 | 默认值 | 说明 |
33
+ | ---------------- | ---------- | ------ | ------------ |
34
+ | `defaultChecked` | `string[]` | `[]` | 初始勾选集合 |
35
+
36
+ ### Events
37
+
38
+ | 事件 | 负载 | 说明 |
39
+ | --------- | -------------------- | ------------------ |
40
+ | `checked` | `{ keys: string[] }` | 勾选状态变化时触发 |
41
+
42
+ ### 实例方法
43
+
44
+ | 名称 | 参数 | 说明 |
45
+ | ------------------- | ---- | ------------ |
46
+ | `checkedKeys` | `无` | 当前勾选集合 |
47
+ | `indeterminateKeys` | `无` | 当前半选集合 |
48
+
49
+ ### 节点方法
50
+
51
+ | 名称 | 参数 | 说明 |
52
+ | ----------------------- | ------------------- | -------------- |
53
+ | `isChecked()` | `无` | 是否勾选 |
54
+ | `isIndeterminate()` | `无` | 是否半选 |
55
+ | `toggleCheck(checked?)` | `checked?: boolean` | 切换或显式设置 |
@@ -0,0 +1,89 @@
1
+ import { SpliceTreePlugin } from "@splicetree/core";
2
+
3
+ //#region src/index.d.ts
4
+ declare module '@splicetree/core' {
5
+ /**
6
+ * 选项扩展(Checkable)
7
+ * - defaultChecked:初始勾选的节点 id 列表
8
+ */
9
+ interface UseSpliceTreeOptions {
10
+ /**
11
+ * 初始勾选的节点 id 列表
12
+ * 在初始化时将这些节点设置为已勾选
13
+ */
14
+ defaultChecked?: string[];
15
+ }
16
+ /**
17
+ * 节点扩展(Checkable)
18
+ * - isChecked:是否已勾选
19
+ * - isIndeterminate:是否半选
20
+ * - toggleCheck:切换或显式设置勾选状态
21
+ */
22
+ interface SpliceTreeNode {
23
+ /**
24
+ * 当前节点是否已勾选
25
+ */
26
+ isChecked: () => boolean;
27
+ /**
28
+ * 当前节点是否为半选状态
29
+ */
30
+ isIndeterminate: () => boolean;
31
+ /**
32
+ * 切换或显式设置当前节点的勾选状态
33
+ * @param checked 不传表示切换;true/false 表示显式设置
34
+ */
35
+ toggleCheck: (checked?: boolean) => void;
36
+ }
37
+ /**
38
+ * 事件扩展(Checkable)
39
+ * - checked:勾选集合变化时派发
40
+ */
41
+ interface SpliceTreeEventPayloadMap {
42
+ /**
43
+ * 勾选集合变化事件负载
44
+ * @property keys 当前所有已勾选节点的 id 列表
45
+ */
46
+ checked: {
47
+ keys: string[];
48
+ };
49
+ }
50
+ /**
51
+ * 实例扩展(Checkable)
52
+ * - checkedKeys/indeterminateKeys:当前勾选/半选集合
53
+ * - isChecked/isIndeterminate:查询节点状态
54
+ * - check/uncheck/toggleCheck:操作勾选状态
55
+ */
56
+ interface SpliceTreeInstance {
57
+ /**
58
+ * 已勾选的节点 id 集合
59
+ */
60
+ checkedKeys: Set<string>;
61
+ /**
62
+ * 半选的节点 id 集合
63
+ */
64
+ indeterminateKeys: Set<string>;
65
+ /**
66
+ * 查询指定节点是否已勾选
67
+ */
68
+ isChecked: (id: string) => boolean;
69
+ /**
70
+ * 查询指定节点是否为半选状态
71
+ */
72
+ isIndeterminate: (id: string) => boolean;
73
+ /**
74
+ * 勾选指定节点
75
+ */
76
+ check: (id: string) => void;
77
+ /**
78
+ * 取消勾选指定节点
79
+ */
80
+ uncheck: (id: string) => void;
81
+ /**
82
+ * 切换指定节点的勾选状态
83
+ */
84
+ toggleCheck: (id: string) => void;
85
+ }
86
+ }
87
+ declare const checkable: SpliceTreePlugin;
88
+ //#endregion
89
+ export { checkable, checkable as default };
package/dist/index.js ADDED
@@ -0,0 +1,129 @@
1
+ //#region src/index.ts
2
+ /**
3
+ * 设置某节点的勾选状态并维护集合
4
+ * @param id 节点 id
5
+ * @param state 目标状态(checked/unchecked/indeterminate)
6
+ * @param checked 已勾选集合
7
+ * @param indeterminate 半选集合
8
+ */
9
+ function setChecked(id, state, checked, indeterminate) {
10
+ if (state === "checked") {
11
+ checked.add(id);
12
+ indeterminate.delete(id);
13
+ } else if (state === "unchecked") {
14
+ checked.delete(id);
15
+ indeterminate.delete(id);
16
+ } else {
17
+ checked.delete(id);
18
+ indeterminate.add(id);
19
+ }
20
+ }
21
+ const checkable = {
22
+ name: "checkable",
23
+ setup(ctx) {
24
+ const { defaultChecked = [] } = ctx.options;
25
+ const checkedKeys = new Set(defaultChecked);
26
+ const indeterminateKeys = /* @__PURE__ */ new Set();
27
+ const isChecked = (id) => checkedKeys.has(id);
28
+ const isIndeterminate = (id) => indeterminateKeys.has(id);
29
+ /**
30
+ * 向下遍历所有子节点并执行回调
31
+ * @param id 起始父节点 id
32
+ * @param visit 对每个子节点执行的回调
33
+ */
34
+ const descend = (id, visit) => {
35
+ const n = ctx.tree.getNode(id);
36
+ if (!n) return;
37
+ for (const c of n.getChildren()) {
38
+ visit(c.id);
39
+ descend(c.id, visit);
40
+ }
41
+ };
42
+ /**
43
+ * 自底向上更新祖先节点的勾选/半选状态
44
+ * - 所有子节点全勾选 → 祖先设为勾选
45
+ * - 所有子节点全未勾选 → 祖先设为未勾选
46
+ * - 否则 → 祖先设为半选
47
+ * 同步派发 visibility 与 checked 事件
48
+ * @param id 起始子节点 id
49
+ */
50
+ const updateAncestors = (id) => {
51
+ let cur = ctx.tree.getNode(id)?.getParent();
52
+ while (cur) {
53
+ const children = cur.getChildren();
54
+ const allChecked = children.every((ch) => checkedKeys.has(ch.id));
55
+ const noneChecked = children.every((ch) => !checkedKeys.has(ch.id) && !indeterminateKeys.has(ch.id));
56
+ if (allChecked) setChecked(cur.id, "checked", checkedKeys, indeterminateKeys);
57
+ else if (noneChecked) setChecked(cur.id, "unchecked", checkedKeys, indeterminateKeys);
58
+ else setChecked(cur.id, "indeterminate", checkedKeys, indeterminateKeys);
59
+ cur = cur.getParent();
60
+ }
61
+ ctx.events.emit({
62
+ name: "visibility",
63
+ keys: ctx.tree.expandedKeys()
64
+ });
65
+ ctx.events.emit({
66
+ name: "checked",
67
+ keys: Array.from(checkedKeys)
68
+ });
69
+ };
70
+ /**
71
+ * 勾选指定节点,并向下级联勾选所有子节点
72
+ * @param id 节点 id
73
+ */
74
+ const check = (id) => {
75
+ setChecked(id, "checked", checkedKeys, indeterminateKeys);
76
+ descend(id, (nid) => setChecked(nid, "checked", checkedKeys, indeterminateKeys));
77
+ updateAncestors(id);
78
+ };
79
+ /**
80
+ * 取消勾选指定节点,并向下级联取消所有子节点
81
+ * @param id 节点 id
82
+ */
83
+ const uncheck = (id) => {
84
+ setChecked(id, "unchecked", checkedKeys, indeterminateKeys);
85
+ descend(id, (nid) => setChecked(nid, "unchecked", checkedKeys, indeterminateKeys));
86
+ updateAncestors(id);
87
+ };
88
+ /**
89
+ * 切换指定节点的勾选状态
90
+ * @param id 节点 id
91
+ */
92
+ const toggleCheck = (id) => {
93
+ if (isChecked(id)) uncheck(id);
94
+ else check(id);
95
+ updateAncestors(id);
96
+ };
97
+ for (const id of defaultChecked) check(id);
98
+ ctx.events.emit({
99
+ name: "visibility",
100
+ keys: ctx.tree.expandedKeys()
101
+ });
102
+ ctx.events.emit({
103
+ name: "checked",
104
+ keys: Array.from(checkedKeys)
105
+ });
106
+ return {
107
+ checkedKeys,
108
+ indeterminateKeys,
109
+ isChecked,
110
+ isIndeterminate,
111
+ check,
112
+ uncheck,
113
+ toggleCheck
114
+ };
115
+ },
116
+ extendNode(node, ctx) {
117
+ node.isChecked = () => ctx.tree.isChecked(node.id);
118
+ node.isIndeterminate = () => ctx.tree.isIndeterminate(node.id);
119
+ node.toggleCheck = (checked) => {
120
+ if (checked === void 0) ctx.tree.toggleCheck(node.id);
121
+ else if (checked) ctx.tree.check(node.id);
122
+ else ctx.tree.uncheck(node.id);
123
+ };
124
+ }
125
+ };
126
+ var src_default = checkable;
127
+
128
+ //#endregion
129
+ export { checkable, src_default as default };
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@splicetree/plugin-checkable",
3
+ "type": "module",
4
+ "version": "0.0.1",
5
+ "author": {
6
+ "email": "michael.cocova@gmail.com",
7
+ "name": "Michael Cocova"
8
+ },
9
+ "license": "MIT",
10
+ "homepage": "https://www.splicetree.dev",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/michaelcocova/splicetree"
14
+ },
15
+ "exports": {
16
+ ".": "./dist/index.js",
17
+ "./package.json": "./package.json"
18
+ },
19
+ "main": "./dist/index.js",
20
+ "module": "./dist/index.js",
21
+ "types": "./dist/index.d.ts",
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "devDependencies": {
26
+ "@splicetree/core": "0.0.1"
27
+ },
28
+ "scripts": {
29
+ "dev": "tsdown --watch",
30
+ "build": "tsdown"
31
+ }
32
+ }
package/src/index.ts ADDED
@@ -0,0 +1,241 @@
1
+ import type { SpliceTreePlugin, SpliceTreePluginContext } from '@splicetree/core'
2
+ import '@splicetree/core'
3
+
4
+ declare module '@splicetree/core' {
5
+ /**
6
+ * 选项扩展(Checkable)
7
+ * - defaultChecked:初始勾选的节点 id 列表
8
+ */
9
+ interface UseSpliceTreeOptions {
10
+ /**
11
+ * 初始勾选的节点 id 列表
12
+ * 在初始化时将这些节点设置为已勾选
13
+ */
14
+ defaultChecked?: string[]
15
+ }
16
+
17
+ /**
18
+ * 节点扩展(Checkable)
19
+ * - isChecked:是否已勾选
20
+ * - isIndeterminate:是否半选
21
+ * - toggleCheck:切换或显式设置勾选状态
22
+ */
23
+ interface SpliceTreeNode {
24
+ /**
25
+ * 当前节点是否已勾选
26
+ */
27
+ isChecked: () => boolean
28
+ /**
29
+ * 当前节点是否为半选状态
30
+ */
31
+ isIndeterminate: () => boolean
32
+ /**
33
+ * 切换或显式设置当前节点的勾选状态
34
+ * @param checked 不传表示切换;true/false 表示显式设置
35
+ */
36
+ toggleCheck: (checked?: boolean) => void
37
+ }
38
+
39
+ /**
40
+ * 事件扩展(Checkable)
41
+ * - checked:勾选集合变化时派发
42
+ */
43
+ interface SpliceTreeEventPayloadMap {
44
+ /**
45
+ * 勾选集合变化事件负载
46
+ * @property keys 当前所有已勾选节点的 id 列表
47
+ */
48
+ checked: { keys: string[] }
49
+ }
50
+
51
+ /**
52
+ * 实例扩展(Checkable)
53
+ * - checkedKeys/indeterminateKeys:当前勾选/半选集合
54
+ * - isChecked/isIndeterminate:查询节点状态
55
+ * - check/uncheck/toggleCheck:操作勾选状态
56
+ */
57
+ interface SpliceTreeInstance {
58
+ /**
59
+ * 已勾选的节点 id 集合
60
+ */
61
+ checkedKeys: Set<string>
62
+ /**
63
+ * 半选的节点 id 集合
64
+ */
65
+ indeterminateKeys: Set<string>
66
+ /**
67
+ * 查询指定节点是否已勾选
68
+ */
69
+ isChecked: (id: string) => boolean
70
+ /**
71
+ * 查询指定节点是否为半选状态
72
+ */
73
+ isIndeterminate: (id: string) => boolean
74
+ /**
75
+ * 勾选指定节点
76
+ */
77
+ check: (id: string) => void
78
+ /**
79
+ * 取消勾选指定节点
80
+ */
81
+ uncheck: (id: string) => void
82
+ /**
83
+ * 切换指定节点的勾选状态
84
+ */
85
+ toggleCheck: (id: string) => void
86
+ }
87
+ }
88
+
89
+ /**
90
+ * 设置某节点的勾选状态并维护集合
91
+ * @param id 节点 id
92
+ * @param state 目标状态(checked/unchecked/indeterminate)
93
+ * @param checked 已勾选集合
94
+ * @param indeterminate 半选集合
95
+ */
96
+ function setChecked(
97
+ id: string,
98
+ state: 'checked' | 'unchecked' | 'indeterminate',
99
+ checked: Set<string>,
100
+ indeterminate: Set<string>,
101
+ ) {
102
+ if (state === 'checked') {
103
+ checked.add(id)
104
+ indeterminate.delete(id)
105
+ } else if (state === 'unchecked') {
106
+ checked.delete(id)
107
+ indeterminate.delete(id)
108
+ } else {
109
+ checked.delete(id)
110
+ indeterminate.add(id)
111
+ }
112
+ }
113
+
114
+ export const checkable: SpliceTreePlugin = {
115
+ name: 'checkable',
116
+
117
+ /**
118
+ * 勾选/半选插件:支持向下级联与向上计算半选
119
+ */
120
+ setup(ctx: SpliceTreePluginContext) {
121
+ const { defaultChecked = [] } = ctx.options
122
+
123
+ const checkedKeys = new Set<string>(defaultChecked)
124
+ const indeterminateKeys = new Set<string>()
125
+
126
+ const isChecked = (id: string) => checkedKeys.has(id)
127
+ const isIndeterminate = (id: string) => indeterminateKeys.has(id)
128
+
129
+ /**
130
+ * 向下遍历所有子节点并执行回调
131
+ * @param id 起始父节点 id
132
+ * @param visit 对每个子节点执行的回调
133
+ */
134
+ const descend = (id: string, visit: (nid: string) => void) => {
135
+ const n = ctx.tree.getNode(id)
136
+ if (!n) {
137
+ return
138
+ }
139
+ for (const c of n.getChildren()) {
140
+ visit(c.id)
141
+ descend(c.id, visit)
142
+ }
143
+ }
144
+
145
+ /**
146
+ * 自底向上更新祖先节点的勾选/半选状态
147
+ * - 所有子节点全勾选 → 祖先设为勾选
148
+ * - 所有子节点全未勾选 → 祖先设为未勾选
149
+ * - 否则 → 祖先设为半选
150
+ * 同步派发 visibility 与 checked 事件
151
+ * @param id 起始子节点 id
152
+ */
153
+ const updateAncestors = (id: string) => {
154
+ let cur = ctx.tree.getNode(id)?.getParent()
155
+ while (cur) {
156
+ const children = cur.getChildren()
157
+ const allChecked = children.every(ch => checkedKeys.has(ch.id))
158
+ const noneChecked = children.every(ch => !checkedKeys.has(ch.id) && !indeterminateKeys.has(ch.id))
159
+ if (allChecked) {
160
+ setChecked(cur.id, 'checked', checkedKeys, indeterminateKeys)
161
+ } else if (noneChecked) {
162
+ setChecked(cur.id, 'unchecked', checkedKeys, indeterminateKeys)
163
+ } else {
164
+ setChecked(cur.id, 'indeterminate', checkedKeys, indeterminateKeys)
165
+ }
166
+ cur = cur.getParent()
167
+ }
168
+
169
+ ctx.events.emit({ name: 'visibility', keys: ctx.tree.expandedKeys() })
170
+ ctx.events.emit({ name: 'checked', keys: Array.from(checkedKeys) })
171
+ }
172
+
173
+ /**
174
+ * 勾选指定节点,并向下级联勾选所有子节点
175
+ * @param id 节点 id
176
+ */
177
+ const check = (id: string) => {
178
+ setChecked(id, 'checked', checkedKeys, indeterminateKeys)
179
+ descend(id, nid => setChecked(nid, 'checked', checkedKeys, indeterminateKeys))
180
+ updateAncestors(id)
181
+ }
182
+
183
+ /**
184
+ * 取消勾选指定节点,并向下级联取消所有子节点
185
+ * @param id 节点 id
186
+ */
187
+ const uncheck = (id: string) => {
188
+ setChecked(id, 'unchecked', checkedKeys, indeterminateKeys)
189
+ descend(id, nid => setChecked(nid, 'unchecked', checkedKeys, indeterminateKeys))
190
+ updateAncestors(id)
191
+ }
192
+
193
+ /**
194
+ * 切换指定节点的勾选状态
195
+ * @param id 节点 id
196
+ */
197
+ const toggleCheck = (id: string) => {
198
+ if (isChecked(id)) {
199
+ uncheck(id)
200
+ } else {
201
+ check(id)
202
+ }
203
+ updateAncestors(id)
204
+ }
205
+
206
+ for (const id of defaultChecked) {
207
+ check(id)
208
+ }
209
+ ctx.events.emit({ name: 'visibility', keys: ctx.tree.expandedKeys() })
210
+ ctx.events.emit({ name: 'checked', keys: Array.from(checkedKeys) } as any)
211
+
212
+ return {
213
+ checkedKeys,
214
+ indeterminateKeys,
215
+ isChecked,
216
+ isIndeterminate,
217
+ check,
218
+ uncheck,
219
+ toggleCheck,
220
+ }
221
+ },
222
+
223
+ /**
224
+ * 为节点扩展勾选相关方法
225
+ */
226
+ extendNode(node, ctx) {
227
+ node.isChecked = () => ctx.tree.isChecked(node.id)
228
+ node.isIndeterminate = () => ctx.tree.isIndeterminate(node.id)
229
+ node.toggleCheck = (checked?: boolean) => {
230
+ if (checked === undefined) {
231
+ ctx.tree.toggleCheck(node.id)
232
+ } else if (checked) {
233
+ ctx.tree.check(node.id)
234
+ } else {
235
+ ctx.tree.uncheck(node.id)
236
+ }
237
+ }
238
+ },
239
+ }
240
+
241
+ export default checkable
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "declaration": true,
5
+ "outDir": "dist"
6
+ },
7
+ "include": ["src"],
8
+ "exclude": ["dist", "node_modules"]
9
+ }
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from 'tsdown'
2
+
3
+ export default defineConfig({
4
+ clean: true,
5
+ sourcemap: false,
6
+ treeshake: true,
7
+ unused: true,
8
+ unbundle: false,
9
+ platform: 'neutral',
10
+ entry: ['./src/index.ts'],
11
+ })