@splicetree/plugin-keyboard 0.0.1 → 0.1.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 +15 -18
- package/README.md +15 -19
- package/dist/index.d.ts +30 -60
- package/dist/index.js +56 -124
- package/package.json +2 -2
- package/src/index.ts +106 -211
package/CHANGELOG.md
CHANGED
|
@@ -1,25 +1,22 @@
|
|
|
1
1
|
# @splicetree/plugin-keyboard
|
|
2
2
|
|
|
3
|
+
## 0.1.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- fix: republish plugin-keyboard after partial release failure
|
|
8
|
+
|
|
9
|
+
## 0.1.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- ### @splicetree/plugin-keyboard
|
|
14
|
+
- 只采集方向键并统一派发 `input:direction`
|
|
15
|
+
- 配置聚合到 `configuration.keyboard`:`autoListen/target/keymap`
|
|
16
|
+
- 自动为目标容器添加 `tabindex="0"` 并聚焦
|
|
17
|
+
|
|
3
18
|
## 0.0.1
|
|
4
19
|
|
|
5
20
|
### Patch Changes
|
|
6
21
|
|
|
7
22
|
- 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/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# @splicetree/plugin-keyboard
|
|
2
2
|
|
|
3
|
+
提供快捷键导航能力:上下移动、展开与收起。
|
|
4
|
+
|
|
3
5
|
[](https://www.npmjs.com/package/@splicetree/plugin-keyboard)
|
|
4
6
|
[](https://npmcharts.com/compare/%40splicetree%2Fplugin-keyboard?minimal=true)
|
|
5
7
|
[](https://www.npmjs.com/package/@splicetree/plugin-keyboard)
|
|
6
8
|
[](https://www.splicetree.dev)
|
|
7
9
|
[](https://github.com/michaelcocova/splicetree)
|
|
8
10
|
|
|
9
|
-
提供快捷键导航能力:上下移动、展开与收起。
|
|
10
|
-
|
|
11
11
|
## 安装
|
|
12
12
|
|
|
13
13
|
`pnpm add @splicetree/plugin-keyboard`
|
|
@@ -20,32 +20,28 @@ import keyboardNavigation from '@splicetree/plugin-keyboard'
|
|
|
20
20
|
|
|
21
21
|
const tree = createSpliceTree(data, {
|
|
22
22
|
plugins: [keyboardNavigation],
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
configuration: {
|
|
24
|
+
keyboard: {
|
|
25
|
+
autoListen: true,
|
|
26
|
+
target: '.keyboard-wrap',
|
|
27
|
+
keymap: { up: 'ArrowUp', down: 'ArrowDown', left: 'ArrowLeft', right: 'ArrowRight' },
|
|
28
|
+
},
|
|
29
|
+
},
|
|
26
30
|
})
|
|
27
31
|
```
|
|
28
32
|
|
|
29
33
|
## Api
|
|
30
34
|
|
|
31
|
-
###
|
|
35
|
+
### Configuration
|
|
32
36
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
| `autoListenKeyboard` | `boolean` | `true` | 是否自动监听键盘事件 |
|
|
37
|
-
| `keymap` | `{ expand?: string; collapse?: string; next?: string; prev?: string }` | `见下` | 快捷键映射(默认:左右上下箭头) |
|
|
38
|
-
| `keyboardTarget` | `HTMLElement` | `document.body` | 键盘监听目标 |
|
|
37
|
+
- `configuration.keyboard.autoListen: boolean` 是否自动监听键盘(默认 `true`)
|
|
38
|
+
- `configuration.keyboard.target: HTMLElement | string | (() => HTMLElement | null)` 监听目标
|
|
39
|
+
- `configuration.keyboard.keymap: { up?: string; down?: string; left?: string; right?: string }` 快捷键映射
|
|
39
40
|
|
|
40
41
|
### 实例方法
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
| ----------------------- | -------------------------- | ---------------------- |
|
|
44
|
-
| `activeId` | `无` | 当前激活的节点 |
|
|
45
|
-
| `toggleActive(id, on?)` | `id: string; on?: boolean` | 切换或显式设置激活状态 |
|
|
43
|
+
- `activeId?: string` 当前激活节点 id(与选择插件协同)
|
|
46
44
|
|
|
47
45
|
### 节点方法
|
|
48
46
|
|
|
49
|
-
|
|
50
|
-
| ------------------- | -------------- | -------------------- |
|
|
51
|
-
| `toggleActive(on?)` | `on?: boolean` | 切换或显式设置激活态 |
|
|
47
|
+
- 无(键盘插件不扩展节点方法,专注输入事件)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,72 +1,42 @@
|
|
|
1
1
|
import { SpliceTreePlugin } from "@splicetree/core";
|
|
2
2
|
|
|
3
3
|
//#region src/index.d.ts
|
|
4
|
+
interface KeyboardKeymap {
|
|
5
|
+
up?: string;
|
|
6
|
+
down?: string;
|
|
7
|
+
left?: string;
|
|
8
|
+
right?: string;
|
|
9
|
+
}
|
|
4
10
|
type KeyboardTargetType = HTMLElement | string | null | undefined;
|
|
5
11
|
type KeyboardTarget = KeyboardTargetType | (() => KeyboardTargetType);
|
|
12
|
+
interface Modifiers {
|
|
13
|
+
shift: boolean;
|
|
14
|
+
ctrl: boolean;
|
|
15
|
+
meta: boolean;
|
|
16
|
+
alt: boolean;
|
|
17
|
+
}
|
|
6
18
|
declare module '@splicetree/core' {
|
|
7
|
-
interface
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
defaultActive?: string;
|
|
13
|
-
/**
|
|
14
|
-
* 是否自动监听键盘事件
|
|
15
|
-
* 开启后,插件会自动监听键盘事件
|
|
16
|
-
* 关闭后,需要手动调用 `listenKeyboard` 方法监听键盘事件
|
|
17
|
-
* @default true
|
|
18
|
-
*/
|
|
19
|
-
autoListenKeyboard?: boolean;
|
|
20
|
-
/**
|
|
21
|
-
* 键盘导航快捷键
|
|
22
|
-
* @default { expand: 'ArrowRight', collapse: 'ArrowLeft', next: 'ArrowDown', prev: 'ArrowUp' }
|
|
23
|
-
*/
|
|
24
|
-
keymap?: {
|
|
25
|
-
expand?: string;
|
|
26
|
-
collapse?: string;
|
|
27
|
-
next?: string;
|
|
28
|
-
prev?: string;
|
|
19
|
+
interface SpliceTreeConfiguration {
|
|
20
|
+
keyboard?: {
|
|
21
|
+
autoListen?: boolean;
|
|
22
|
+
target?: KeyboardTarget;
|
|
23
|
+
keymap?: KeyboardKeymap;
|
|
29
24
|
};
|
|
30
|
-
/**
|
|
31
|
-
* 键盘导航目标元素
|
|
32
|
-
* @default document.body
|
|
33
|
-
*/
|
|
34
|
-
keyboardTarget?: KeyboardTarget;
|
|
35
25
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
activeId?: string;
|
|
46
|
-
/**
|
|
47
|
-
* 切换或显式设置某节点的激活态
|
|
48
|
-
* @param id 节点 id
|
|
49
|
-
* @param active 不传表示切换;true/false 表示显式设置
|
|
50
|
-
*/
|
|
51
|
-
toggleActive: (id: string, active?: boolean) => void;
|
|
26
|
+
interface SpliceTreeEventPayloadMap {
|
|
27
|
+
'input:direction': {
|
|
28
|
+
direction: 'up' | 'down' | 'left' | 'right';
|
|
29
|
+
modifiers: Modifiers;
|
|
30
|
+
};
|
|
31
|
+
'input:node-click': {
|
|
32
|
+
nodeId: string;
|
|
33
|
+
modifiers: Modifiers;
|
|
34
|
+
};
|
|
52
35
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
* - isActive:当前节点是否激活
|
|
56
|
-
* - toggleActive:切换当前节点的激活态
|
|
57
|
-
*/
|
|
58
|
-
interface SpliceTreeNode {
|
|
59
|
-
/**
|
|
60
|
-
* 当前节点是否为激活态
|
|
61
|
-
*/
|
|
62
|
-
isActive: () => boolean;
|
|
63
|
-
/**
|
|
64
|
-
* 切换当前节点的激活态
|
|
65
|
-
* @param active 不传表示切换;true/false 表示显式设置
|
|
66
|
-
*/
|
|
67
|
-
toggleActive: (active?: boolean) => void;
|
|
36
|
+
interface SpliceTreeInstance {
|
|
37
|
+
emitNodeClick: (nodeId: string, e: MouseEvent) => void;
|
|
68
38
|
}
|
|
69
39
|
}
|
|
70
|
-
declare const
|
|
40
|
+
declare const keyboardPlugin: SpliceTreePlugin;
|
|
71
41
|
//#endregion
|
|
72
|
-
export {
|
|
42
|
+
export { keyboardPlugin as default, keyboardPlugin };
|
package/dist/index.js
CHANGED
|
@@ -1,147 +1,79 @@
|
|
|
1
1
|
//#region src/index.ts
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
function resolveKeyboardTarget(targe) {
|
|
7
|
-
if (typeof targe === "string") return document.querySelector(targe);
|
|
8
|
-
if (targe instanceof HTMLElement) return targe;
|
|
9
|
-
if (typeof targe === "function") return resolveKeyboardTarget(targe?.());
|
|
2
|
+
function resolveTarget(target) {
|
|
3
|
+
if (typeof target === "string") return document.querySelector(target);
|
|
4
|
+
if (target instanceof HTMLElement) return target;
|
|
5
|
+
if (typeof target === "function") return resolveTarget(target());
|
|
10
6
|
return null;
|
|
11
7
|
}
|
|
12
|
-
|
|
8
|
+
function getModifiers(e) {
|
|
9
|
+
return {
|
|
10
|
+
shift: e.shiftKey,
|
|
11
|
+
ctrl: e.ctrlKey,
|
|
12
|
+
meta: e.metaKey,
|
|
13
|
+
alt: e.altKey
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
const keyboardPlugin = {
|
|
13
17
|
name: "keyboard",
|
|
14
18
|
setup(ctx) {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* 切换或设置激活节点
|
|
26
|
-
* 更新后派发 visibility 事件以刷新视图
|
|
27
|
-
* @param id 节点 id
|
|
28
|
-
* @param active 不传表示切换;true/false 表示显式设置
|
|
29
|
-
*/
|
|
30
|
-
const toggleActive = (id, active) => {
|
|
31
|
-
if (active === void 0) activeId = activeId === id ? void 0 : id;
|
|
32
|
-
else if (active) activeId = id;
|
|
33
|
-
else if (activeId === id) activeId = void 0;
|
|
34
|
-
ctx.events.emit({
|
|
35
|
-
name: "visibility",
|
|
36
|
-
keys: ctx.tree.expandedKeys()
|
|
37
|
-
});
|
|
19
|
+
const config = ctx.options?.configuration?.keyboard ?? {};
|
|
20
|
+
const autoListenKeyboard = config.autoListen ?? true;
|
|
21
|
+
const keyboardTarget = config.target;
|
|
22
|
+
const keymap = config.keymap;
|
|
23
|
+
const keys = {
|
|
24
|
+
up: keymap?.up ?? "ArrowUp",
|
|
25
|
+
down: keymap?.down ?? "ArrowDown",
|
|
26
|
+
left: keymap?.left ?? "ArrowLeft",
|
|
27
|
+
right: keymap?.right ?? "ArrowRight"
|
|
38
28
|
};
|
|
39
|
-
|
|
40
|
-
* 按可见序列移动激活项
|
|
41
|
-
* @param delta 移动步长(-1 上一个,1 下一个)
|
|
42
|
-
*/
|
|
43
|
-
const moveActive = (delta) => {
|
|
44
|
-
const items = ctx.tree.items();
|
|
45
|
-
if (!items.length) return;
|
|
46
|
-
const idx = activeId ? items.findIndex((n) => n.id === activeId) : -1;
|
|
47
|
-
activeId = items[Math.max(0, Math.min(items.length - 1, idx < 0 ? 0 : idx + delta))]?.id;
|
|
29
|
+
const emitDirection = (direction, e) => {
|
|
48
30
|
ctx.events.emit({
|
|
49
|
-
name: "
|
|
50
|
-
|
|
31
|
+
name: "input:direction",
|
|
32
|
+
direction,
|
|
33
|
+
modifiers: getModifiers(e)
|
|
51
34
|
});
|
|
52
35
|
};
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
name: "visibility",
|
|
72
|
-
keys: ctx.tree.expandedKeys()
|
|
73
|
-
});
|
|
74
|
-
};
|
|
75
|
-
/**
|
|
76
|
-
* 右方向键:优先展开;已展开则进入第一个子节点
|
|
77
|
-
*/
|
|
78
|
-
const keyRight = () => {
|
|
79
|
-
if (!activeId) return;
|
|
80
|
-
const node = ctx.tree.getNode(activeId);
|
|
81
|
-
if (!node) return;
|
|
82
|
-
if (!ctx.tree.isExpanded(node.id)) ctx.tree.expand(node.id);
|
|
83
|
-
else {
|
|
84
|
-
const first = node.getChildren()[0];
|
|
85
|
-
if (first) activeId = first.id;
|
|
36
|
+
const onKeydown = (e) => {
|
|
37
|
+
switch (e.key) {
|
|
38
|
+
case keys.up:
|
|
39
|
+
e.preventDefault();
|
|
40
|
+
emitDirection("up", e);
|
|
41
|
+
break;
|
|
42
|
+
case keys.down:
|
|
43
|
+
e.preventDefault();
|
|
44
|
+
emitDirection("down", e);
|
|
45
|
+
break;
|
|
46
|
+
case keys.left:
|
|
47
|
+
e.preventDefault();
|
|
48
|
+
emitDirection("left", e);
|
|
49
|
+
break;
|
|
50
|
+
case keys.right:
|
|
51
|
+
e.preventDefault();
|
|
52
|
+
emitDirection("right", e);
|
|
53
|
+
break;
|
|
86
54
|
}
|
|
55
|
+
};
|
|
56
|
+
const emitNodeClick = (nodeId, e) => {
|
|
87
57
|
ctx.events.emit({
|
|
88
|
-
name: "
|
|
89
|
-
|
|
58
|
+
name: "input:node-click",
|
|
59
|
+
nodeId,
|
|
60
|
+
modifiers: getModifiers(e)
|
|
90
61
|
});
|
|
91
62
|
};
|
|
92
|
-
/**
|
|
93
|
-
* 键盘按下事件处理
|
|
94
|
-
* - 根据 keymap 映射调用对应处理函数
|
|
95
|
-
* - 自动阻止默认行为以避免滚动
|
|
96
|
-
*/
|
|
97
|
-
const onKeydown = (e) => {
|
|
98
|
-
const kExpand = keymap?.expand ?? "ArrowRight";
|
|
99
|
-
const kCollapse = keymap?.collapse ?? "ArrowLeft";
|
|
100
|
-
const kNext = keymap?.next ?? "ArrowDown";
|
|
101
|
-
const kPrev = keymap?.prev ?? "ArrowUp";
|
|
102
|
-
const key = e.key;
|
|
103
|
-
if (key === kPrev) {
|
|
104
|
-
e.preventDefault();
|
|
105
|
-
keyUp();
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
if (key === kNext) {
|
|
109
|
-
e.preventDefault();
|
|
110
|
-
keyDown();
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
if (key === kCollapse) {
|
|
114
|
-
e.preventDefault();
|
|
115
|
-
keyLeft();
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
if (key === kExpand) {
|
|
119
|
-
e.preventDefault();
|
|
120
|
-
keyRight();
|
|
121
|
-
}
|
|
122
|
-
};
|
|
123
63
|
if (autoListenKeyboard && typeof document !== "undefined") setTimeout(() => {
|
|
124
|
-
const root =
|
|
125
|
-
if (root)
|
|
126
|
-
|
|
127
|
-
root?.focus?.();
|
|
128
|
-
}
|
|
64
|
+
const root = resolveTarget(keyboardTarget);
|
|
65
|
+
if (!root) return;
|
|
66
|
+
root.setAttribute("tabindex", "0");
|
|
129
67
|
const handler = (e) => {
|
|
130
68
|
const active = document.activeElement;
|
|
131
|
-
if (
|
|
69
|
+
if (root.contains(active)) onKeydown(e);
|
|
132
70
|
};
|
|
133
71
|
document.addEventListener("keydown", handler);
|
|
134
72
|
});
|
|
135
|
-
return {
|
|
136
|
-
},
|
|
137
|
-
extendNode(node, ctx) {
|
|
138
|
-
node.isActive = () => ctx.tree?.activeId === node.id;
|
|
139
|
-
node.toggleActive = (active) => {
|
|
140
|
-
ctx.tree.toggleActive(node.id, active);
|
|
141
|
-
};
|
|
73
|
+
return { emitNodeClick };
|
|
142
74
|
}
|
|
143
75
|
};
|
|
144
|
-
var src_default =
|
|
76
|
+
var src_default = keyboardPlugin;
|
|
145
77
|
|
|
146
78
|
//#endregion
|
|
147
|
-
export { src_default as default,
|
|
79
|
+
export { src_default as default, keyboardPlugin };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@splicetree/plugin-keyboard",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.1.1",
|
|
5
5
|
"author": {
|
|
6
6
|
"email": "michael.cocova@gmail.com",
|
|
7
7
|
"name": "Michael Cocova"
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"access": "public"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@splicetree/core": "0.
|
|
26
|
+
"@splicetree/core": "0.1.1"
|
|
27
27
|
},
|
|
28
28
|
"scripts": {
|
|
29
29
|
"dev": "tsdown --watch",
|
package/src/index.ts
CHANGED
|
@@ -1,252 +1,147 @@
|
|
|
1
1
|
import type { SpliceTreePlugin, SpliceTreePluginContext } from '@splicetree/core'
|
|
2
2
|
import '@splicetree/core'
|
|
3
3
|
|
|
4
|
+
interface KeyboardKeymap {
|
|
5
|
+
up?: string
|
|
6
|
+
down?: string
|
|
7
|
+
left?: string
|
|
8
|
+
right?: string
|
|
9
|
+
}
|
|
4
10
|
type KeyboardTargetType = HTMLElement | string | null | undefined
|
|
5
11
|
type KeyboardTarget = KeyboardTargetType | (() => KeyboardTargetType)
|
|
12
|
+
|
|
13
|
+
interface Modifiers {
|
|
14
|
+
shift: boolean
|
|
15
|
+
ctrl: boolean
|
|
16
|
+
meta: boolean
|
|
17
|
+
alt: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
6
20
|
declare module '@splicetree/core' {
|
|
7
|
-
export interface
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
defaultActive?: string
|
|
13
|
-
/**
|
|
14
|
-
* 是否自动监听键盘事件
|
|
15
|
-
* 开启后,插件会自动监听键盘事件
|
|
16
|
-
* 关闭后,需要手动调用 `listenKeyboard` 方法监听键盘事件
|
|
17
|
-
* @default true
|
|
18
|
-
*/
|
|
19
|
-
autoListenKeyboard?: boolean
|
|
20
|
-
/**
|
|
21
|
-
* 键盘导航快捷键
|
|
22
|
-
* @default { expand: 'ArrowRight', collapse: 'ArrowLeft', next: 'ArrowDown', prev: 'ArrowUp' }
|
|
23
|
-
*/
|
|
24
|
-
keymap?: {
|
|
25
|
-
expand?: string
|
|
26
|
-
collapse?: string
|
|
27
|
-
next?: string
|
|
28
|
-
prev?: string
|
|
21
|
+
export interface SpliceTreeConfiguration {
|
|
22
|
+
keyboard?: {
|
|
23
|
+
autoListen?: boolean
|
|
24
|
+
target?: KeyboardTarget
|
|
25
|
+
keymap?: KeyboardKeymap
|
|
29
26
|
}
|
|
30
|
-
/**
|
|
31
|
-
* 键盘导航目标元素
|
|
32
|
-
* @default document.body
|
|
33
|
-
*/
|
|
34
|
-
keyboardTarget?: KeyboardTarget
|
|
35
27
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
*/
|
|
46
|
-
activeId?: string
|
|
47
|
-
/**
|
|
48
|
-
* 切换或显式设置某节点的激活态
|
|
49
|
-
* @param id 节点 id
|
|
50
|
-
* @param active 不传表示切换;true/false 表示显式设置
|
|
51
|
-
*/
|
|
52
|
-
toggleActive: (id: string, active?: boolean) => void
|
|
28
|
+
export interface SpliceTreeEventPayloadMap {
|
|
29
|
+
'input:direction': {
|
|
30
|
+
direction: 'up' | 'down' | 'left' | 'right'
|
|
31
|
+
modifiers: Modifiers
|
|
32
|
+
}
|
|
33
|
+
'input:node-click': {
|
|
34
|
+
nodeId: string
|
|
35
|
+
modifiers: Modifiers
|
|
36
|
+
}
|
|
53
37
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
* 节点扩展(Keyboard)
|
|
57
|
-
* - isActive:当前节点是否激活
|
|
58
|
-
* - toggleActive:切换当前节点的激活态
|
|
59
|
-
*/
|
|
60
|
-
interface SpliceTreeNode {
|
|
61
|
-
/**
|
|
62
|
-
* 当前节点是否为激活态
|
|
63
|
-
*/
|
|
64
|
-
isActive: () => boolean
|
|
65
|
-
/**
|
|
66
|
-
* 切换当前节点的激活态
|
|
67
|
-
* @param active 不传表示切换;true/false 表示显式设置
|
|
68
|
-
*/
|
|
69
|
-
toggleActive: (active?: boolean) => void
|
|
38
|
+
export interface SpliceTreeInstance {
|
|
39
|
+
emitNodeClick: (nodeId: string, e: MouseEvent) => void
|
|
70
40
|
}
|
|
71
41
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
* 支持传入选择器、元素实例或函数
|
|
76
|
-
*/
|
|
77
|
-
function resolveKeyboardTarget(targe?: KeyboardTarget): HTMLElement | null {
|
|
78
|
-
if (typeof targe === 'string') {
|
|
79
|
-
return document.querySelector(targe)
|
|
42
|
+
function resolveTarget(target?: KeyboardTarget): HTMLElement | null {
|
|
43
|
+
if (typeof target === 'string') {
|
|
44
|
+
return document.querySelector(target)
|
|
80
45
|
}
|
|
81
|
-
if (
|
|
82
|
-
return
|
|
46
|
+
if (target instanceof HTMLElement) {
|
|
47
|
+
return target
|
|
83
48
|
}
|
|
84
|
-
if (typeof
|
|
85
|
-
return
|
|
49
|
+
if (typeof target === 'function') {
|
|
50
|
+
return resolveTarget(target())
|
|
86
51
|
}
|
|
87
52
|
return null
|
|
88
53
|
}
|
|
89
|
-
|
|
54
|
+
|
|
55
|
+
function getModifiers(e: KeyboardEvent | MouseEvent) {
|
|
56
|
+
return {
|
|
57
|
+
shift: e.shiftKey,
|
|
58
|
+
ctrl: e.ctrlKey,
|
|
59
|
+
meta: e.metaKey,
|
|
60
|
+
alt: e.altKey,
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const keyboardPlugin: SpliceTreePlugin = {
|
|
90
65
|
name: 'keyboard',
|
|
91
|
-
|
|
92
|
-
* 键盘导航插件
|
|
93
|
-
* - 提供 activeId 概念与切换 API
|
|
94
|
-
* - 支持上下移动、展开/收起与进入子级
|
|
95
|
-
* - 自动或自定义监听目标元素的键盘事件
|
|
96
|
-
*/
|
|
66
|
+
|
|
97
67
|
setup(ctx: SpliceTreePluginContext) {
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
get() {
|
|
103
|
-
return activeId
|
|
104
|
-
},
|
|
105
|
-
configurable: true,
|
|
106
|
-
enumerable: true,
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* 切换或设置激活节点
|
|
111
|
-
* 更新后派发 visibility 事件以刷新视图
|
|
112
|
-
* @param id 节点 id
|
|
113
|
-
* @param active 不传表示切换;true/false 表示显式设置
|
|
114
|
-
*/
|
|
115
|
-
const toggleActive = (id: string, active?: boolean) => {
|
|
116
|
-
if (active === undefined) {
|
|
117
|
-
activeId = activeId === id ? undefined : id
|
|
118
|
-
} else {
|
|
119
|
-
if (active)
|
|
120
|
-
activeId = id
|
|
121
|
-
else if (activeId === id)
|
|
122
|
-
activeId = undefined
|
|
123
|
-
}
|
|
124
|
-
ctx.events.emit({ name: 'visibility', keys: ctx.tree.expandedKeys() })
|
|
68
|
+
const config = (ctx.options?.configuration?.keyboard ?? {}) as {
|
|
69
|
+
autoListen?: boolean
|
|
70
|
+
target?: KeyboardTarget
|
|
71
|
+
keymap?: KeyboardKeymap
|
|
125
72
|
}
|
|
73
|
+
const autoListenKeyboard = config.autoListen ?? true
|
|
74
|
+
const keyboardTarget = config.target
|
|
75
|
+
const keymap = config.keymap
|
|
126
76
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const items = ctx.tree.items()
|
|
133
|
-
if (!items.length) {
|
|
134
|
-
return
|
|
135
|
-
}
|
|
136
|
-
const idx = activeId ? items.findIndex(n => n.id === activeId) : -1
|
|
137
|
-
const next = Math.max(0, Math.min(items.length - 1, (idx < 0 ? 0 : idx + delta)))
|
|
138
|
-
activeId = items[next]?.id
|
|
139
|
-
ctx.events.emit({ name: 'visibility', keys: ctx.tree.expandedKeys() })
|
|
77
|
+
const keys = {
|
|
78
|
+
up: keymap?.up ?? 'ArrowUp',
|
|
79
|
+
down: keymap?.down ?? 'ArrowDown',
|
|
80
|
+
left: keymap?.left ?? 'ArrowLeft',
|
|
81
|
+
right: keymap?.right ?? 'ArrowRight',
|
|
140
82
|
}
|
|
141
83
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
*/
|
|
149
|
-
const keyDown = () => moveActive(1)
|
|
150
|
-
/**
|
|
151
|
-
* 左方向键:优先收起;否则激活父节点
|
|
152
|
-
*/
|
|
153
|
-
const keyLeft = () => {
|
|
154
|
-
if (!activeId) {
|
|
155
|
-
return
|
|
156
|
-
}
|
|
157
|
-
const node = ctx.tree.getNode(activeId)
|
|
158
|
-
if (!node) {
|
|
159
|
-
return
|
|
160
|
-
}
|
|
161
|
-
if (ctx.tree.isExpanded(node.id)) {
|
|
162
|
-
ctx.tree.collapse(node.id)
|
|
163
|
-
} else if (node.getParent()?.id) {
|
|
164
|
-
activeId = node.getParent()!.id
|
|
165
|
-
}
|
|
166
|
-
ctx.events.emit({ name: 'visibility', keys: ctx.tree.expandedKeys() })
|
|
167
|
-
}
|
|
168
|
-
/**
|
|
169
|
-
* 右方向键:优先展开;已展开则进入第一个子节点
|
|
170
|
-
*/
|
|
171
|
-
const keyRight = () => {
|
|
172
|
-
if (!activeId) {
|
|
173
|
-
return
|
|
174
|
-
}
|
|
175
|
-
const node = ctx.tree.getNode(activeId)
|
|
176
|
-
if (!node) {
|
|
177
|
-
return
|
|
178
|
-
}
|
|
179
|
-
if (!ctx.tree.isExpanded(node.id)) {
|
|
180
|
-
ctx.tree.expand(node.id)
|
|
181
|
-
} else {
|
|
182
|
-
const first = node.getChildren()[0]
|
|
183
|
-
if (first) {
|
|
184
|
-
activeId = first.id
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
ctx.events.emit({ name: 'visibility', keys: ctx.tree.expandedKeys() })
|
|
84
|
+
const emitDirection = (direction: 'up' | 'down' | 'left' | 'right', e: KeyboardEvent) => {
|
|
85
|
+
ctx.events.emit({
|
|
86
|
+
name: 'input:direction',
|
|
87
|
+
direction,
|
|
88
|
+
modifiers: getModifiers(e),
|
|
89
|
+
})
|
|
188
90
|
}
|
|
189
|
-
|
|
190
|
-
* 键盘按下事件处理
|
|
191
|
-
* - 根据 keymap 映射调用对应处理函数
|
|
192
|
-
* - 自动阻止默认行为以避免滚动
|
|
193
|
-
*/
|
|
91
|
+
|
|
194
92
|
const onKeydown = (e: KeyboardEvent) => {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
keyLeft()
|
|
213
|
-
return
|
|
214
|
-
}
|
|
215
|
-
if (key === kExpand) {
|
|
216
|
-
e.preventDefault()
|
|
217
|
-
keyRight()
|
|
93
|
+
switch (e.key) {
|
|
94
|
+
case keys.up:
|
|
95
|
+
e.preventDefault()
|
|
96
|
+
emitDirection('up', e)
|
|
97
|
+
break
|
|
98
|
+
case keys.down:
|
|
99
|
+
e.preventDefault()
|
|
100
|
+
emitDirection('down', e)
|
|
101
|
+
break
|
|
102
|
+
case keys.left:
|
|
103
|
+
e.preventDefault()
|
|
104
|
+
emitDirection('left', e)
|
|
105
|
+
break
|
|
106
|
+
case keys.right:
|
|
107
|
+
e.preventDefault()
|
|
108
|
+
emitDirection('right', e)
|
|
109
|
+
break
|
|
218
110
|
}
|
|
219
111
|
}
|
|
112
|
+
|
|
113
|
+
const emitNodeClick = (nodeId: string, e: MouseEvent) => {
|
|
114
|
+
ctx.events.emit({
|
|
115
|
+
name: 'input:node-click',
|
|
116
|
+
nodeId,
|
|
117
|
+
modifiers: getModifiers(e),
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
220
121
|
if (autoListenKeyboard && typeof document !== 'undefined') {
|
|
221
122
|
setTimeout(() => {
|
|
222
|
-
const root =
|
|
223
|
-
if (root) {
|
|
224
|
-
|
|
225
|
-
root?.focus?.()
|
|
123
|
+
const root = resolveTarget(keyboardTarget)
|
|
124
|
+
if (!root) {
|
|
125
|
+
return
|
|
226
126
|
}
|
|
127
|
+
|
|
128
|
+
root.setAttribute('tabindex', '0')
|
|
129
|
+
|
|
227
130
|
const handler = (e: KeyboardEvent) => {
|
|
228
131
|
const active = document.activeElement
|
|
229
|
-
|
|
230
|
-
if (!root || root.contains(active)) {
|
|
132
|
+
if (root.contains(active)) {
|
|
231
133
|
onKeydown(e)
|
|
232
134
|
}
|
|
233
135
|
}
|
|
136
|
+
|
|
234
137
|
document.addEventListener('keydown', handler)
|
|
235
138
|
})
|
|
236
139
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
* 为节点扩展激活态判断方法
|
|
241
|
-
* - isActive:是否为当前激活项
|
|
242
|
-
* - toggleActive:切换当前节点的激活态
|
|
243
|
-
*/
|
|
244
|
-
extendNode(node, ctx) {
|
|
245
|
-
node.isActive = () => ctx.tree?.activeId === node.id
|
|
246
|
-
node.toggleActive = (active?: boolean) => {
|
|
247
|
-
ctx.tree.toggleActive(node.id, active)
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
emitNodeClick,
|
|
248
143
|
}
|
|
249
144
|
},
|
|
250
145
|
}
|
|
251
146
|
|
|
252
|
-
export default
|
|
147
|
+
export default keyboardPlugin
|