@dxos/keyboard 0.3.10-main.08c6b67
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/LICENSE +8 -0
- package/README.md +28 -0
- package/dist/lib/browser/index.mjs +166 -0
- package/dist/lib/browser/index.mjs.map +7 -0
- package/dist/lib/browser/meta.json +1 -0
- package/dist/lib/node/index.cjs +179 -0
- package/dist/lib/node/index.cjs.map +7 -0
- package/dist/lib/node/meta.json +1 -0
- package/dist/types/src/index.d.ts +3 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/keyboard.d.ts +41 -0
- package/dist/types/src/keyboard.d.ts.map +1 -0
- package/dist/types/src/keyboard.stories.d.ts +10 -0
- package/dist/types/src/keyboard.stories.d.ts.map +1 -0
- package/dist/types/src/keyboard.test.d.ts +2 -0
- package/dist/types/src/keyboard.test.d.ts.map +1 -0
- package/dist/types/src/util.d.ts +2 -0
- package/dist/types/src/util.d.ts.map +1 -0
- package/package.json +32 -0
- package/src/index.ts +6 -0
- package/src/keyboard.stories.tsx +76 -0
- package/src/keyboard.test.ts +17 -0
- package/src/keyboard.ts +146 -0
- package/src/util.ts +29 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
Copyright (c) 2022 DXOS
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
5
|
+
|
|
6
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
7
|
+
|
|
8
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
## React Async
|
|
2
|
+
|
|
3
|
+
### Configuration
|
|
4
|
+
|
|
5
|
+
Add the following to `package.json`.
|
|
6
|
+
|
|
7
|
+
```json
|
|
8
|
+
"toolchain": {
|
|
9
|
+
"forceCloseTests": true,
|
|
10
|
+
"jsdom": true,
|
|
11
|
+
"testingFramework": "mocha"
|
|
12
|
+
}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The `react-dom/test-utils` requires the `raf` (RequestAnimationFrame) polyfill to run headless.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import 'raf/polyfill';
|
|
19
|
+
import { act } from 'react-dom/test-utils';
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Next
|
|
23
|
+
|
|
24
|
+
Evaluate the following:
|
|
25
|
+
|
|
26
|
+
- https://www.npmjs.com/package/react-async-hook (popular)
|
|
27
|
+
- https://www.npmjs.com/package/use-enhanced-state
|
|
28
|
+
- https://www.npmjs.com/package/react-hooks-lib
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import "@dxos/node-std/globals";
|
|
2
|
+
|
|
3
|
+
// inject-globals:@inject-globals
|
|
4
|
+
import {
|
|
5
|
+
global,
|
|
6
|
+
Buffer,
|
|
7
|
+
process
|
|
8
|
+
} from "@dxos/node-std/inject-globals";
|
|
9
|
+
|
|
10
|
+
// packages/common/keyboard/src/keyboard.ts
|
|
11
|
+
import { invariant } from "@dxos/invariant";
|
|
12
|
+
var __dxlog_file = "/home/runner/work/dxos/dxos/packages/common/keyboard/src/keyboard.ts";
|
|
13
|
+
var modifiers = [
|
|
14
|
+
"alt",
|
|
15
|
+
"ctrl",
|
|
16
|
+
"shift",
|
|
17
|
+
"meta"
|
|
18
|
+
];
|
|
19
|
+
var KeyboardContext = class {
|
|
20
|
+
constructor() {
|
|
21
|
+
this._keyMap = /* @__PURE__ */ new Map();
|
|
22
|
+
}
|
|
23
|
+
get bindings() {
|
|
24
|
+
return Array.from(this._keyMap.values());
|
|
25
|
+
}
|
|
26
|
+
get(binding) {
|
|
27
|
+
return this._keyMap.get(binding);
|
|
28
|
+
}
|
|
29
|
+
bind(config) {
|
|
30
|
+
const { binding } = config;
|
|
31
|
+
const parts = binding.split("+");
|
|
32
|
+
const mods = modifiers.filter((key) => parts.includes(key));
|
|
33
|
+
invariant(mods.length === 0 || mods.length === parts.length - 1, void 0, {
|
|
34
|
+
F: __dxlog_file,
|
|
35
|
+
L: 40,
|
|
36
|
+
S: this,
|
|
37
|
+
A: [
|
|
38
|
+
"mods.length === 0 || mods.length === parts.length - 1",
|
|
39
|
+
""
|
|
40
|
+
]
|
|
41
|
+
});
|
|
42
|
+
if (mods.length) {
|
|
43
|
+
config.binding = [
|
|
44
|
+
...mods,
|
|
45
|
+
parts[parts.length - 1]
|
|
46
|
+
].join("+");
|
|
47
|
+
}
|
|
48
|
+
this._keyMap.set(config.binding, config);
|
|
49
|
+
}
|
|
50
|
+
unbind(binding) {
|
|
51
|
+
this._keyMap.delete(binding);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
var ROOT = "";
|
|
55
|
+
var Keyboard = class _Keyboard {
|
|
56
|
+
constructor() {
|
|
57
|
+
this._root = new KeyboardContext();
|
|
58
|
+
this._keyMap = /* @__PURE__ */ new Map([
|
|
59
|
+
[
|
|
60
|
+
ROOT,
|
|
61
|
+
this._root
|
|
62
|
+
]
|
|
63
|
+
]);
|
|
64
|
+
this._contexts = [
|
|
65
|
+
ROOT
|
|
66
|
+
];
|
|
67
|
+
this._keyHandler = this.handleKeyDown.bind(this);
|
|
68
|
+
this._path = ROOT;
|
|
69
|
+
this.bind = this._root.bind.bind(this._root);
|
|
70
|
+
this.unbind = this._root.unbind.bind(this._root);
|
|
71
|
+
}
|
|
72
|
+
static {
|
|
73
|
+
this.singleton = new _Keyboard();
|
|
74
|
+
}
|
|
75
|
+
initialize() {
|
|
76
|
+
document.addEventListener("keydown", this._keyHandler);
|
|
77
|
+
}
|
|
78
|
+
destroy() {
|
|
79
|
+
document.removeEventListener("keydown", this._keyHandler);
|
|
80
|
+
}
|
|
81
|
+
setCurrentContext(path = ROOT) {
|
|
82
|
+
this._path = path;
|
|
83
|
+
}
|
|
84
|
+
getCurrentContext() {
|
|
85
|
+
return this._path;
|
|
86
|
+
}
|
|
87
|
+
getContext(path = ROOT) {
|
|
88
|
+
let context = this._keyMap.get(path);
|
|
89
|
+
if (!context) {
|
|
90
|
+
context = new KeyboardContext();
|
|
91
|
+
this._keyMap.set(path, context);
|
|
92
|
+
this._contexts.push(path);
|
|
93
|
+
this._contexts.sort();
|
|
94
|
+
}
|
|
95
|
+
return context;
|
|
96
|
+
}
|
|
97
|
+
getBindings() {
|
|
98
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
99
|
+
for (let i = 0; i < this._contexts.length; ++i) {
|
|
100
|
+
const path = this._contexts[i];
|
|
101
|
+
if (this._path.startsWith(path)) {
|
|
102
|
+
this.getContext(path).bindings.forEach((binding) => {
|
|
103
|
+
bindings.set(binding.binding, binding);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return Array.from(bindings.values());
|
|
108
|
+
}
|
|
109
|
+
handleKeyDown(event) {
|
|
110
|
+
const { altKey, ctrlKey, metaKey, shiftKey, key } = event;
|
|
111
|
+
if (key !== "Alt" && key !== "Control" && key !== "Meta" && key !== "Shift") {
|
|
112
|
+
const tagName = event.target?.tagName;
|
|
113
|
+
const isInput = tagName === "INPUT" || tagName === "TEXTAREA" || event.target?.getAttribute("contenteditable");
|
|
114
|
+
const str = [
|
|
115
|
+
altKey && "alt",
|
|
116
|
+
ctrlKey && "ctrl",
|
|
117
|
+
shiftKey && "shift",
|
|
118
|
+
metaKey && "meta",
|
|
119
|
+
key
|
|
120
|
+
].filter(Boolean).join("+");
|
|
121
|
+
for (let i = this._contexts.length - 1; i >= 0; --i) {
|
|
122
|
+
const path = this._contexts[i];
|
|
123
|
+
if (this._path.startsWith(path)) {
|
|
124
|
+
const { data, handler, disableInput } = this.getContext(path).get(str) ?? {};
|
|
125
|
+
if (handler && (!isInput || !disableInput)) {
|
|
126
|
+
const result = handler({
|
|
127
|
+
context: path,
|
|
128
|
+
binding: str,
|
|
129
|
+
data,
|
|
130
|
+
event
|
|
131
|
+
});
|
|
132
|
+
if (result !== false) {
|
|
133
|
+
event.preventDefault();
|
|
134
|
+
event.stopPropagation();
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// packages/common/keyboard/src/util.ts
|
|
145
|
+
var symbols = {
|
|
146
|
+
// Mods.
|
|
147
|
+
ctrl: "\u2303",
|
|
148
|
+
shift: "\u21E7",
|
|
149
|
+
alt: "\u2325",
|
|
150
|
+
meta: "\u2318",
|
|
151
|
+
// Special keys.
|
|
152
|
+
Backspace: "\u232B",
|
|
153
|
+
Enter: "\u23CE",
|
|
154
|
+
Escape: "\u238B",
|
|
155
|
+
Space: "\u2423",
|
|
156
|
+
Tab: "\u21E5"
|
|
157
|
+
};
|
|
158
|
+
var keySymbols = (keyBinding) => {
|
|
159
|
+
const parts = keyBinding.split("+");
|
|
160
|
+
return parts.map((part) => symbols[part] ?? part.toUpperCase());
|
|
161
|
+
};
|
|
162
|
+
export {
|
|
163
|
+
Keyboard,
|
|
164
|
+
keySymbols
|
|
165
|
+
};
|
|
166
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/keyboard.ts", "../../../src/util.ts"],
|
|
4
|
+
"sourcesContent": ["//\n// Copyright 2023 DXOS.org\n//\n\nimport { invariant } from '@dxos/invariant';\n\nexport type KeyHandler = (props: {\n context: string;\n binding: string;\n data?: any;\n event: KeyboardEvent;\n}) => boolean | void;\n\nexport type KeyBinding = {\n binding: string;\n handler: KeyHandler;\n disableInput?: boolean;\n data?: any;\n};\n\n// Keybinding is normalized to this order.\nconst modifiers = ['alt', 'ctrl', 'shift', 'meta'];\n\nclass KeyboardContext {\n readonly _keyMap = new Map<string, KeyBinding>();\n\n get bindings() {\n return Array.from(this._keyMap.values());\n }\n\n get(binding: string) {\n return this._keyMap.get(binding);\n }\n\n bind(config: KeyBinding) {\n // Normalize order of modifiers.\n const { binding } = config;\n const parts = binding.split('+');\n const mods = modifiers.filter((key) => parts.includes(key));\n invariant(mods.length === 0 || mods.length === parts.length - 1);\n if (mods.length) {\n config.binding = [...mods, parts[parts.length - 1]].join('+');\n }\n\n this._keyMap.set(config.binding, config);\n }\n\n unbind(binding: string) {\n this._keyMap.delete(binding);\n }\n}\n\nconst ROOT = '';\n\n/**\n * Manages context-aware key bindings.\n */\nexport class Keyboard {\n static singleton = new Keyboard();\n\n private readonly _root = new KeyboardContext();\n private readonly _keyMap = new Map<string, KeyboardContext>([[ROOT, this._root]]);\n private readonly _contexts: string[] = [ROOT];\n private readonly _keyHandler = this.handleKeyDown.bind(this);\n private _path = ROOT;\n\n initialize() {\n document.addEventListener('keydown', this._keyHandler);\n }\n\n destroy() {\n document.removeEventListener('keydown', this._keyHandler);\n }\n\n bind = this._root.bind.bind(this._root);\n unbind = this._root.unbind.bind(this._root);\n\n setCurrentContext(path = ROOT) {\n this._path = path;\n }\n\n getCurrentContext() {\n return this._path;\n }\n\n getContext(path = ROOT): KeyboardContext {\n let context = this._keyMap.get(path);\n if (!context) {\n context = new KeyboardContext();\n this._keyMap.set(path, context);\n this._contexts.push(path);\n this._contexts.sort();\n }\n\n return context;\n }\n\n getBindings() {\n const bindings = new Map<string, KeyBinding>();\n for (let i = 0; i < this._contexts.length; ++i) {\n const path = this._contexts[i];\n if (this._path.startsWith(path)) {\n this.getContext(path).bindings.forEach((binding) => {\n bindings.set(binding.binding, binding);\n });\n }\n }\n\n return Array.from(bindings.values());\n }\n\n handleKeyDown(event: KeyboardEvent) {\n const { altKey, ctrlKey, metaKey, shiftKey, key } = event;\n\n if (key !== 'Alt' && key !== 'Control' && key !== 'Meta' && key !== 'Shift') {\n // Binding option to check for input or contenteditable.\n const tagName = (event.target as any)?.tagName;\n const isInput =\n tagName === 'INPUT' || tagName === 'TEXTAREA' || (event.target as any)?.getAttribute('contenteditable');\n\n // Normalized key binding.\n const str = [altKey && 'alt', ctrlKey && 'ctrl', shiftKey && 'shift', metaKey && 'meta', key]\n .filter(Boolean)\n .join('+');\n\n // Scan matching contexts.\n for (let i = this._contexts.length - 1; i >= 0; --i) {\n const path = this._contexts[i];\n if (this._path.startsWith(path)) {\n const { data, handler, disableInput } = this.getContext(path).get(str) ?? {};\n if (handler && (!isInput || !disableInput)) {\n const result = handler({ context: path, binding: str, data, event });\n if (result !== false) {\n // TODO(burdon): This doesn't prevent actions in markdown editor.\n // Reference: https://codemirror.net/docs/ref/#view.KeyBinding\n event.preventDefault();\n event.stopPropagation();\n }\n\n return;\n }\n }\n }\n }\n }\n}\n", "//\n// Copyright 2023 DXOS.org\n//\n\n// https://www.w3.org/TR/DOM-Level-3-Events/#events-keyboardevents\n// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key\n// https://support.apple.com/en-us/HT201236\n// https://support.apple.com/guide/mac-help/what-are-those-symbols-shown-in-menus-cpmh0011/mac\n\n// TODO(burdon): Show Mac/Windows/Linux variants.\nconst symbols: Record<string, string> = {\n // Mods.\n ctrl: '⌃',\n shift: '⇧',\n alt: '⌥',\n meta: '⌘',\n\n // Special keys.\n Backspace: '⌫',\n Enter: '⏎',\n Escape: '⎋',\n Space: '␣',\n Tab: '⇥',\n};\n\nexport const keySymbols = (keyBinding: string): string[] => {\n const parts = keyBinding.split('+');\n return parts.map((part) => symbols[part] ?? part.toUpperCase());\n};\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["invariant", "modifiers", "KeyboardContext", "_keyMap", "Map", "bindings", "Array", "from", "values", "get", "binding", "bind", "config", "parts", "split", "mods", "filter", "key", "includes", "length", "join", "set", "unbind", "delete", "ROOT", "Keyboard", "_root", "_contexts", "_keyHandler", "handleKeyDown", "_path", "singleton", "initialize", "document", "addEventListener", "destroy", "removeEventListener", "setCurrentContext", "path", "getCurrentContext", "getContext", "context", "push", "sort", "getBindings", "i", "startsWith", "forEach", "event", "altKey", "ctrlKey", "metaKey", "shiftKey", "tagName", "target", "isInput", "getAttribute", "str", "Boolean", "data", "handler", "disableInput", "result", "preventDefault", "stopPropagation", "symbols", "ctrl", "shift", "alt", "meta", "Backspace", "Enter", "Escape", "Space", "Tab", "keySymbols", "keyBinding", "parts", "split", "map", "part", "toUpperCase"]
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"inputs":{"inject-globals:@inject-globals":{"bytes":384,"imports":[{"path":"@dxos/node-std/inject-globals","kind":"import-statement","external":true}],"format":"esm"},"packages/common/keyboard/src/keyboard.ts":{"bytes":15101,"imports":[{"path":"@dxos/invariant","kind":"import-statement","external":true},{"path":"@inject-globals","kind":"import-statement","external":true}],"format":"esm"},"packages/common/keyboard/src/util.ts":{"bytes":2733,"imports":[{"path":"@inject-globals","kind":"import-statement","external":true}],"format":"esm"},"packages/common/keyboard/src/index.ts":{"bytes":544,"imports":[{"path":"packages/common/keyboard/src/keyboard.ts","kind":"import-statement","original":"./keyboard"},{"path":"packages/common/keyboard/src/util.ts","kind":"import-statement","original":"./util"},{"path":"@inject-globals","kind":"import-statement","external":true}],"format":"esm"}},"outputs":{"packages/common/keyboard/dist/lib/browser/index.mjs.map":{"imports":[],"exports":[],"inputs":{},"bytes":8722},"packages/common/keyboard/dist/lib/browser/index.mjs":{"imports":[{"path":"@dxos/node-std/inject-globals","kind":"import-statement","external":true},{"path":"@dxos/invariant","kind":"import-statement","external":true}],"exports":["Keyboard","keySymbols"],"entryPoint":"packages/common/keyboard/src/index.ts","inputs":{"inject-globals:@inject-globals":{"bytesInOutput":79},"packages/common/keyboard/src/keyboard.ts":{"bytesInOutput":3582},"packages/common/keyboard/src/index.ts":{"bytesInOutput":0},"packages/common/keyboard/src/util.ts":{"bytesInOutput":362}},"bytes":4249}}}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var node_exports = {};
|
|
20
|
+
__export(node_exports, {
|
|
21
|
+
Keyboard: () => Keyboard,
|
|
22
|
+
keySymbols: () => keySymbols
|
|
23
|
+
});
|
|
24
|
+
module.exports = __toCommonJS(node_exports);
|
|
25
|
+
var import_invariant = require("@dxos/invariant");
|
|
26
|
+
var __dxlog_file = "/home/runner/work/dxos/dxos/packages/common/keyboard/src/keyboard.ts";
|
|
27
|
+
var modifiers = [
|
|
28
|
+
"alt",
|
|
29
|
+
"ctrl",
|
|
30
|
+
"shift",
|
|
31
|
+
"meta"
|
|
32
|
+
];
|
|
33
|
+
var KeyboardContext = class {
|
|
34
|
+
constructor() {
|
|
35
|
+
this._keyMap = /* @__PURE__ */ new Map();
|
|
36
|
+
}
|
|
37
|
+
get bindings() {
|
|
38
|
+
return Array.from(this._keyMap.values());
|
|
39
|
+
}
|
|
40
|
+
get(binding) {
|
|
41
|
+
return this._keyMap.get(binding);
|
|
42
|
+
}
|
|
43
|
+
bind(config) {
|
|
44
|
+
const { binding } = config;
|
|
45
|
+
const parts = binding.split("+");
|
|
46
|
+
const mods = modifiers.filter((key) => parts.includes(key));
|
|
47
|
+
(0, import_invariant.invariant)(mods.length === 0 || mods.length === parts.length - 1, void 0, {
|
|
48
|
+
F: __dxlog_file,
|
|
49
|
+
L: 40,
|
|
50
|
+
S: this,
|
|
51
|
+
A: [
|
|
52
|
+
"mods.length === 0 || mods.length === parts.length - 1",
|
|
53
|
+
""
|
|
54
|
+
]
|
|
55
|
+
});
|
|
56
|
+
if (mods.length) {
|
|
57
|
+
config.binding = [
|
|
58
|
+
...mods,
|
|
59
|
+
parts[parts.length - 1]
|
|
60
|
+
].join("+");
|
|
61
|
+
}
|
|
62
|
+
this._keyMap.set(config.binding, config);
|
|
63
|
+
}
|
|
64
|
+
unbind(binding) {
|
|
65
|
+
this._keyMap.delete(binding);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
var ROOT = "";
|
|
69
|
+
var Keyboard = class _Keyboard {
|
|
70
|
+
constructor() {
|
|
71
|
+
this._root = new KeyboardContext();
|
|
72
|
+
this._keyMap = /* @__PURE__ */ new Map([
|
|
73
|
+
[
|
|
74
|
+
ROOT,
|
|
75
|
+
this._root
|
|
76
|
+
]
|
|
77
|
+
]);
|
|
78
|
+
this._contexts = [
|
|
79
|
+
ROOT
|
|
80
|
+
];
|
|
81
|
+
this._keyHandler = this.handleKeyDown.bind(this);
|
|
82
|
+
this._path = ROOT;
|
|
83
|
+
this.bind = this._root.bind.bind(this._root);
|
|
84
|
+
this.unbind = this._root.unbind.bind(this._root);
|
|
85
|
+
}
|
|
86
|
+
static {
|
|
87
|
+
this.singleton = new _Keyboard();
|
|
88
|
+
}
|
|
89
|
+
initialize() {
|
|
90
|
+
document.addEventListener("keydown", this._keyHandler);
|
|
91
|
+
}
|
|
92
|
+
destroy() {
|
|
93
|
+
document.removeEventListener("keydown", this._keyHandler);
|
|
94
|
+
}
|
|
95
|
+
setCurrentContext(path = ROOT) {
|
|
96
|
+
this._path = path;
|
|
97
|
+
}
|
|
98
|
+
getCurrentContext() {
|
|
99
|
+
return this._path;
|
|
100
|
+
}
|
|
101
|
+
getContext(path = ROOT) {
|
|
102
|
+
let context = this._keyMap.get(path);
|
|
103
|
+
if (!context) {
|
|
104
|
+
context = new KeyboardContext();
|
|
105
|
+
this._keyMap.set(path, context);
|
|
106
|
+
this._contexts.push(path);
|
|
107
|
+
this._contexts.sort();
|
|
108
|
+
}
|
|
109
|
+
return context;
|
|
110
|
+
}
|
|
111
|
+
getBindings() {
|
|
112
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
113
|
+
for (let i = 0; i < this._contexts.length; ++i) {
|
|
114
|
+
const path = this._contexts[i];
|
|
115
|
+
if (this._path.startsWith(path)) {
|
|
116
|
+
this.getContext(path).bindings.forEach((binding) => {
|
|
117
|
+
bindings.set(binding.binding, binding);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return Array.from(bindings.values());
|
|
122
|
+
}
|
|
123
|
+
handleKeyDown(event) {
|
|
124
|
+
const { altKey, ctrlKey, metaKey, shiftKey, key } = event;
|
|
125
|
+
if (key !== "Alt" && key !== "Control" && key !== "Meta" && key !== "Shift") {
|
|
126
|
+
const tagName = event.target?.tagName;
|
|
127
|
+
const isInput = tagName === "INPUT" || tagName === "TEXTAREA" || event.target?.getAttribute("contenteditable");
|
|
128
|
+
const str = [
|
|
129
|
+
altKey && "alt",
|
|
130
|
+
ctrlKey && "ctrl",
|
|
131
|
+
shiftKey && "shift",
|
|
132
|
+
metaKey && "meta",
|
|
133
|
+
key
|
|
134
|
+
].filter(Boolean).join("+");
|
|
135
|
+
for (let i = this._contexts.length - 1; i >= 0; --i) {
|
|
136
|
+
const path = this._contexts[i];
|
|
137
|
+
if (this._path.startsWith(path)) {
|
|
138
|
+
const { data, handler, disableInput } = this.getContext(path).get(str) ?? {};
|
|
139
|
+
if (handler && (!isInput || !disableInput)) {
|
|
140
|
+
const result = handler({
|
|
141
|
+
context: path,
|
|
142
|
+
binding: str,
|
|
143
|
+
data,
|
|
144
|
+
event
|
|
145
|
+
});
|
|
146
|
+
if (result !== false) {
|
|
147
|
+
event.preventDefault();
|
|
148
|
+
event.stopPropagation();
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
var symbols = {
|
|
158
|
+
// Mods.
|
|
159
|
+
ctrl: "\u2303",
|
|
160
|
+
shift: "\u21E7",
|
|
161
|
+
alt: "\u2325",
|
|
162
|
+
meta: "\u2318",
|
|
163
|
+
// Special keys.
|
|
164
|
+
Backspace: "\u232B",
|
|
165
|
+
Enter: "\u23CE",
|
|
166
|
+
Escape: "\u238B",
|
|
167
|
+
Space: "\u2423",
|
|
168
|
+
Tab: "\u21E5"
|
|
169
|
+
};
|
|
170
|
+
var keySymbols = (keyBinding) => {
|
|
171
|
+
const parts = keyBinding.split("+");
|
|
172
|
+
return parts.map((part) => symbols[part] ?? part.toUpperCase());
|
|
173
|
+
};
|
|
174
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
175
|
+
0 && (module.exports = {
|
|
176
|
+
Keyboard,
|
|
177
|
+
keySymbols
|
|
178
|
+
});
|
|
179
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/keyboard.ts", "../../../src/util.ts"],
|
|
4
|
+
"sourcesContent": ["//\n// Copyright 2023 DXOS.org\n//\n\nimport { invariant } from '@dxos/invariant';\n\nexport type KeyHandler = (props: {\n context: string;\n binding: string;\n data?: any;\n event: KeyboardEvent;\n}) => boolean | void;\n\nexport type KeyBinding = {\n binding: string;\n handler: KeyHandler;\n disableInput?: boolean;\n data?: any;\n};\n\n// Keybinding is normalized to this order.\nconst modifiers = ['alt', 'ctrl', 'shift', 'meta'];\n\nclass KeyboardContext {\n readonly _keyMap = new Map<string, KeyBinding>();\n\n get bindings() {\n return Array.from(this._keyMap.values());\n }\n\n get(binding: string) {\n return this._keyMap.get(binding);\n }\n\n bind(config: KeyBinding) {\n // Normalize order of modifiers.\n const { binding } = config;\n const parts = binding.split('+');\n const mods = modifiers.filter((key) => parts.includes(key));\n invariant(mods.length === 0 || mods.length === parts.length - 1);\n if (mods.length) {\n config.binding = [...mods, parts[parts.length - 1]].join('+');\n }\n\n this._keyMap.set(config.binding, config);\n }\n\n unbind(binding: string) {\n this._keyMap.delete(binding);\n }\n}\n\nconst ROOT = '';\n\n/**\n * Manages context-aware key bindings.\n */\nexport class Keyboard {\n static singleton = new Keyboard();\n\n private readonly _root = new KeyboardContext();\n private readonly _keyMap = new Map<string, KeyboardContext>([[ROOT, this._root]]);\n private readonly _contexts: string[] = [ROOT];\n private readonly _keyHandler = this.handleKeyDown.bind(this);\n private _path = ROOT;\n\n initialize() {\n document.addEventListener('keydown', this._keyHandler);\n }\n\n destroy() {\n document.removeEventListener('keydown', this._keyHandler);\n }\n\n bind = this._root.bind.bind(this._root);\n unbind = this._root.unbind.bind(this._root);\n\n setCurrentContext(path = ROOT) {\n this._path = path;\n }\n\n getCurrentContext() {\n return this._path;\n }\n\n getContext(path = ROOT): KeyboardContext {\n let context = this._keyMap.get(path);\n if (!context) {\n context = new KeyboardContext();\n this._keyMap.set(path, context);\n this._contexts.push(path);\n this._contexts.sort();\n }\n\n return context;\n }\n\n getBindings() {\n const bindings = new Map<string, KeyBinding>();\n for (let i = 0; i < this._contexts.length; ++i) {\n const path = this._contexts[i];\n if (this._path.startsWith(path)) {\n this.getContext(path).bindings.forEach((binding) => {\n bindings.set(binding.binding, binding);\n });\n }\n }\n\n return Array.from(bindings.values());\n }\n\n handleKeyDown(event: KeyboardEvent) {\n const { altKey, ctrlKey, metaKey, shiftKey, key } = event;\n\n if (key !== 'Alt' && key !== 'Control' && key !== 'Meta' && key !== 'Shift') {\n // Binding option to check for input or contenteditable.\n const tagName = (event.target as any)?.tagName;\n const isInput =\n tagName === 'INPUT' || tagName === 'TEXTAREA' || (event.target as any)?.getAttribute('contenteditable');\n\n // Normalized key binding.\n const str = [altKey && 'alt', ctrlKey && 'ctrl', shiftKey && 'shift', metaKey && 'meta', key]\n .filter(Boolean)\n .join('+');\n\n // Scan matching contexts.\n for (let i = this._contexts.length - 1; i >= 0; --i) {\n const path = this._contexts[i];\n if (this._path.startsWith(path)) {\n const { data, handler, disableInput } = this.getContext(path).get(str) ?? {};\n if (handler && (!isInput || !disableInput)) {\n const result = handler({ context: path, binding: str, data, event });\n if (result !== false) {\n // TODO(burdon): This doesn't prevent actions in markdown editor.\n // Reference: https://codemirror.net/docs/ref/#view.KeyBinding\n event.preventDefault();\n event.stopPropagation();\n }\n\n return;\n }\n }\n }\n }\n }\n}\n", "//\n// Copyright 2023 DXOS.org\n//\n\n// https://www.w3.org/TR/DOM-Level-3-Events/#events-keyboardevents\n// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key\n// https://support.apple.com/en-us/HT201236\n// https://support.apple.com/guide/mac-help/what-are-those-symbols-shown-in-menus-cpmh0011/mac\n\n// TODO(burdon): Show Mac/Windows/Linux variants.\nconst symbols: Record<string, string> = {\n // Mods.\n ctrl: '⌃',\n shift: '⇧',\n alt: '⌥',\n meta: '⌘',\n\n // Special keys.\n Backspace: '⌫',\n Enter: '⏎',\n Escape: '⎋',\n Space: '␣',\n Tab: '⇥',\n};\n\nexport const keySymbols = (keyBinding: string): string[] => {\n const parts = keyBinding.split('+');\n return parts.map((part) => symbols[part] ?? part.toUpperCase());\n};\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;AAIA,uBAA0B;;AAiB1B,IAAMA,YAAY;EAAC;EAAO;EAAQ;EAAS;;AAE3C,IAAMC,kBAAN,MAAMA;EAAN,cAAA;AACWC,SAAAA,UAAU,oBAAIC,IAAAA;;EAEvB,IAAIC,WAAW;AACb,WAAOC,MAAMC,KAAK,KAAKJ,QAAQK,OAAM,CAAA;EACvC;EAEAC,IAAIC,SAAiB;AACnB,WAAO,KAAKP,QAAQM,IAAIC,OAAAA;EAC1B;EAEAC,KAAKC,QAAoB;AAEvB,UAAM,EAAEF,QAAO,IAAKE;AACpB,UAAMC,QAAQH,QAAQI,MAAM,GAAA;AAC5B,UAAMC,OAAOd,UAAUe,OAAO,CAACC,QAAQJ,MAAMK,SAASD,GAAAA,CAAAA;AACtDE,oCAAUJ,KAAKK,WAAW,KAAKL,KAAKK,WAAWP,MAAMO,SAAS,GAAA,QAAA;;;;;;;;;AAC9D,QAAIL,KAAKK,QAAQ;AACfR,aAAOF,UAAU;WAAIK;QAAMF,MAAMA,MAAMO,SAAS,CAAA;QAAIC,KAAK,GAAA;IAC3D;AAEA,SAAKlB,QAAQmB,IAAIV,OAAOF,SAASE,MAAAA;EACnC;EAEAW,OAAOb,SAAiB;AACtB,SAAKP,QAAQqB,OAAOd,OAAAA;EACtB;AACF;AAEA,IAAMe,OAAO;AAKN,IAAMC,WAAN,MAAMA,UAAAA;EAAN,cAAA;AAGYC,SAAAA,QAAQ,IAAIzB,gBAAAA;AACZC,SAAAA,UAAU,oBAAIC,IAA6B;MAAC;QAACqB;QAAM,KAAKE;;KAAO;AAC/DC,SAAAA,YAAsB;MAACH;;AACvBI,SAAAA,cAAc,KAAKC,cAAcnB,KAAK,IAAI;AACnDoB,SAAAA,QAAQN;AAUhBd,SAAAA,OAAO,KAAKgB,MAAMhB,KAAKA,KAAK,KAAKgB,KAAK;AACtCJ,SAAAA,SAAS,KAAKI,MAAMJ,OAAOZ,KAAK,KAAKgB,KAAK;;EAjB1C,OAAA;SAAOK,YAAY,IAAIN,UAAAA;;EAQvBO,aAAa;AACXC,aAASC,iBAAiB,WAAW,KAAKN,WAAW;EACvD;EAEAO,UAAU;AACRF,aAASG,oBAAoB,WAAW,KAAKR,WAAW;EAC1D;EAKAS,kBAAkBC,OAAOd,MAAM;AAC7B,SAAKM,QAAQQ;EACf;EAEAC,oBAAoB;AAClB,WAAO,KAAKT;EACd;EAEAU,WAAWF,OAAOd,MAAuB;AACvC,QAAIiB,UAAU,KAAKvC,QAAQM,IAAI8B,IAAAA;AAC/B,QAAI,CAACG,SAAS;AACZA,gBAAU,IAAIxC,gBAAAA;AACd,WAAKC,QAAQmB,IAAIiB,MAAMG,OAAAA;AACvB,WAAKd,UAAUe,KAAKJ,IAAAA;AACpB,WAAKX,UAAUgB,KAAI;IACrB;AAEA,WAAOF;EACT;EAEAG,cAAc;AACZ,UAAMxC,WAAW,oBAAID,IAAAA;AACrB,aAAS0C,IAAI,GAAGA,IAAI,KAAKlB,UAAUR,QAAQ,EAAE0B,GAAG;AAC9C,YAAMP,OAAO,KAAKX,UAAUkB,CAAAA;AAC5B,UAAI,KAAKf,MAAMgB,WAAWR,IAAAA,GAAO;AAC/B,aAAKE,WAAWF,IAAAA,EAAMlC,SAAS2C,QAAQ,CAACtC,YAAAA;AACtCL,mBAASiB,IAAIZ,QAAQA,SAASA,OAAAA;QAChC,CAAA;MACF;IACF;AAEA,WAAOJ,MAAMC,KAAKF,SAASG,OAAM,CAAA;EACnC;EAEAsB,cAAcmB,OAAsB;AAClC,UAAM,EAAEC,QAAQC,SAASC,SAASC,UAAUpC,IAAG,IAAKgC;AAEpD,QAAIhC,QAAQ,SAASA,QAAQ,aAAaA,QAAQ,UAAUA,QAAQ,SAAS;AAE3E,YAAMqC,UAAWL,MAAMM,QAAgBD;AACvC,YAAME,UACJF,YAAY,WAAWA,YAAY,cAAeL,MAAMM,QAAgBE,aAAa,iBAAA;AAGvF,YAAMC,MAAM;QAACR,UAAU;QAAOC,WAAW;QAAQE,YAAY;QAASD,WAAW;QAAQnC;QACtFD,OAAO2C,OAAAA,EACPtC,KAAK,GAAA;AAGR,eAASyB,IAAI,KAAKlB,UAAUR,SAAS,GAAG0B,KAAK,GAAG,EAAEA,GAAG;AACnD,cAAMP,OAAO,KAAKX,UAAUkB,CAAAA;AAC5B,YAAI,KAAKf,MAAMgB,WAAWR,IAAAA,GAAO;AAC/B,gBAAM,EAAEqB,MAAMC,SAASC,aAAY,IAAK,KAAKrB,WAAWF,IAAAA,EAAM9B,IAAIiD,GAAAA,KAAQ,CAAC;AAC3E,cAAIG,YAAY,CAACL,WAAW,CAACM,eAAe;AAC1C,kBAAMC,SAASF,QAAQ;cAAEnB,SAASH;cAAM7B,SAASgD;cAAKE;cAAMX;YAAM,CAAA;AAClE,gBAAIc,WAAW,OAAO;AAGpBd,oBAAMe,eAAc;AACpBf,oBAAMgB,gBAAe;YACvB;AAEA;UACF;QACF;MACF;IACF;EACF;AACF;ACvIA,IAAMC,UAAkC;;EAEtCC,MAAM;EACNC,OAAO;EACPC,KAAK;EACLC,MAAM;;EAGNC,WAAW;EACXC,OAAO;EACPC,QAAQ;EACRC,OAAO;EACPC,KAAK;AACP;AAEO,IAAMC,aAAa,CAACC,eAAAA;AACzB,QAAMhE,QAAQgE,WAAW/D,MAAM,GAAA;AAC/B,SAAOD,MAAMiE,IAAI,CAACC,SAASb,QAAQa,IAAAA,KAASA,KAAKC,YAAW,CAAA;AAC9D;",
|
|
6
|
+
"names": ["modifiers", "KeyboardContext", "_keyMap", "Map", "bindings", "Array", "from", "values", "get", "binding", "bind", "config", "parts", "split", "mods", "filter", "key", "includes", "invariant", "length", "join", "set", "unbind", "delete", "ROOT", "Keyboard", "_root", "_contexts", "_keyHandler", "handleKeyDown", "_path", "singleton", "initialize", "document", "addEventListener", "destroy", "removeEventListener", "setCurrentContext", "path", "getCurrentContext", "getContext", "context", "push", "sort", "getBindings", "i", "startsWith", "forEach", "event", "altKey", "ctrlKey", "metaKey", "shiftKey", "tagName", "target", "isInput", "getAttribute", "str", "Boolean", "data", "handler", "disableInput", "result", "preventDefault", "stopPropagation", "symbols", "ctrl", "shift", "alt", "meta", "Backspace", "Enter", "Escape", "Space", "Tab", "keySymbols", "keyBinding", "map", "part", "toUpperCase"]
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"inputs":{"packages/common/keyboard/src/keyboard.ts":{"bytes":15101,"imports":[{"path":"@dxos/invariant","kind":"import-statement","external":true}],"format":"esm"},"packages/common/keyboard/src/util.ts":{"bytes":2733,"imports":[],"format":"esm"},"packages/common/keyboard/src/index.ts":{"bytes":544,"imports":[{"path":"packages/common/keyboard/src/keyboard.ts","kind":"import-statement","original":"./keyboard"},{"path":"packages/common/keyboard/src/util.ts","kind":"import-statement","original":"./util"}],"format":"esm"}},"outputs":{"packages/common/keyboard/dist/lib/node/index.cjs.map":{"imports":[],"exports":[],"inputs":{},"bytes":8713},"packages/common/keyboard/dist/lib/node/index.cjs":{"imports":[{"path":"@dxos/invariant","kind":"import-statement","external":true}],"exports":["Keyboard","keySymbols"],"entryPoint":"packages/common/keyboard/src/index.ts","inputs":{"packages/common/keyboard/src/keyboard.ts":{"bytesInOutput":3582},"packages/common/keyboard/src/index.ts":{"bytesInOutput":0},"packages/common/keyboard/src/util.ts":{"bytesInOutput":362}},"bytes":4101}}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAIA,cAAc,YAAY,CAAC;AAC3B,cAAc,QAAQ,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export type KeyHandler = (props: {
|
|
2
|
+
context: string;
|
|
3
|
+
binding: string;
|
|
4
|
+
data?: any;
|
|
5
|
+
event: KeyboardEvent;
|
|
6
|
+
}) => boolean | void;
|
|
7
|
+
export type KeyBinding = {
|
|
8
|
+
binding: string;
|
|
9
|
+
handler: KeyHandler;
|
|
10
|
+
disableInput?: boolean;
|
|
11
|
+
data?: any;
|
|
12
|
+
};
|
|
13
|
+
declare class KeyboardContext {
|
|
14
|
+
readonly _keyMap: Map<string, KeyBinding>;
|
|
15
|
+
get bindings(): KeyBinding[];
|
|
16
|
+
get(binding: string): KeyBinding | undefined;
|
|
17
|
+
bind(config: KeyBinding): void;
|
|
18
|
+
unbind(binding: string): void;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Manages context-aware key bindings.
|
|
22
|
+
*/
|
|
23
|
+
export declare class Keyboard {
|
|
24
|
+
static singleton: Keyboard;
|
|
25
|
+
private readonly _root;
|
|
26
|
+
private readonly _keyMap;
|
|
27
|
+
private readonly _contexts;
|
|
28
|
+
private readonly _keyHandler;
|
|
29
|
+
private _path;
|
|
30
|
+
initialize(): void;
|
|
31
|
+
destroy(): void;
|
|
32
|
+
bind: (config: KeyBinding) => void;
|
|
33
|
+
unbind: (binding: string) => void;
|
|
34
|
+
setCurrentContext(path?: string): void;
|
|
35
|
+
getCurrentContext(): string;
|
|
36
|
+
getContext(path?: string): KeyboardContext;
|
|
37
|
+
getBindings(): KeyBinding[];
|
|
38
|
+
handleKeyDown(event: KeyboardEvent): void;
|
|
39
|
+
}
|
|
40
|
+
export {};
|
|
41
|
+
//# sourceMappingURL=keyboard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keyboard.d.ts","sourceRoot":"","sources":["../../../src/keyboard.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,UAAU,GAAG,CAAC,KAAK,EAAE;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,KAAK,EAAE,aAAa,CAAC;CACtB,KAAK,OAAO,GAAG,IAAI,CAAC;AAErB,MAAM,MAAM,UAAU,GAAG;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,UAAU,CAAC;IACpB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,IAAI,CAAC,EAAE,GAAG,CAAC;CACZ,CAAC;AAKF,cAAM,eAAe;IACnB,QAAQ,CAAC,OAAO,0BAAiC;IAEjD,IAAI,QAAQ,iBAEX;IAED,GAAG,CAAC,OAAO,EAAE,MAAM;IAInB,IAAI,CAAC,MAAM,EAAE,UAAU;IAavB,MAAM,CAAC,OAAO,EAAE,MAAM;CAGvB;AAID;;GAEG;AACH,qBAAa,QAAQ;IACnB,MAAM,CAAC,SAAS,WAAkB;IAElC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAyB;IAC/C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0D;IAClF,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAoB;IAC9C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAiC;IAC7D,OAAO,CAAC,KAAK,CAAQ;IAErB,UAAU;IAIV,OAAO;IAIP,IAAI,+BAAoC;IACxC,MAAM,4BAAsC;IAE5C,iBAAiB,CAAC,IAAI,SAAO;IAI7B,iBAAiB;IAIjB,UAAU,CAAC,IAAI,SAAO,GAAG,eAAe;IAYxC,WAAW;IAcX,aAAa,CAAC,KAAK,EAAE,aAAa;CAkCnC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import '@dxosTheme';
|
|
3
|
+
declare const _default: {
|
|
4
|
+
title: string;
|
|
5
|
+
decorators: import("@storybook/react").Decorator[];
|
|
6
|
+
render: () => JSX.Element;
|
|
7
|
+
};
|
|
8
|
+
export default _default;
|
|
9
|
+
export declare const Default: {};
|
|
10
|
+
//# sourceMappingURL=keyboard.stories.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keyboard.stories.d.ts","sourceRoot":"","sources":["../../../src/keyboard.stories.tsx"],"names":[],"mappings":";AAIA,OAAO,YAAY,CAAC;;;;;;AAiEpB,wBAIE;AAEF,eAAO,MAAM,OAAO,IAAK,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keyboard.test.d.ts","sourceRoot":"","sources":["../../../src/keyboard.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../../../src/util.ts"],"names":[],"mappings":"AAyBA,eAAO,MAAM,UAAU,eAAgB,MAAM,KAAG,MAAM,EAGrD,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dxos/keyboard",
|
|
3
|
+
"version": "0.3.10-main.08c6b67",
|
|
4
|
+
"description": "Keyboard bindings",
|
|
5
|
+
"homepage": "https://dxos.org",
|
|
6
|
+
"bugs": "https://github.com/dxos/dxos/issues",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"author": "DXOS.org",
|
|
9
|
+
"main": "dist/lib/node/index.cjs",
|
|
10
|
+
"browser": {
|
|
11
|
+
"./dist/lib/node/index.cjs": "./dist/lib/browser/index.mjs"
|
|
12
|
+
},
|
|
13
|
+
"types": "dist/types/src/index.d.ts",
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"src"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@dxos/invariant": "0.3.10-main.08c6b67",
|
|
20
|
+
"@dxos/node-std": "0.3.10-main.08c6b67"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"react": "^18.2.0",
|
|
24
|
+
"react-dom": "^18.2.0",
|
|
25
|
+
"@dxos/react-ui": "0.3.10-main.08c6b67",
|
|
26
|
+
"@dxos/react-ui-theme": "0.3.10-main.08c6b67",
|
|
27
|
+
"@dxos/storybook-utils": "0.3.10-main.08c6b67"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import '@dxosTheme';
|
|
6
|
+
|
|
7
|
+
import React, { useEffect, useState } from 'react';
|
|
8
|
+
|
|
9
|
+
import { Select, Input } from '@dxos/react-ui';
|
|
10
|
+
import { withTheme } from '@dxos/storybook-utils';
|
|
11
|
+
|
|
12
|
+
import { type KeyHandler, Keyboard } from './keyboard';
|
|
13
|
+
|
|
14
|
+
const contexts = ['/', '/space/1', '/space/2', '/space/2/a', '/space/2/b'];
|
|
15
|
+
|
|
16
|
+
const Story = () => {
|
|
17
|
+
const [context, setContext] = useState<string | undefined>(undefined);
|
|
18
|
+
const [bindings, setBindings] = useState({});
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const handler: KeyHandler = ({ context, binding, data }) => console.log({ context, binding, data });
|
|
22
|
+
|
|
23
|
+
Keyboard.singleton.initialize();
|
|
24
|
+
Keyboard.singleton.bind({ binding: 'meta+k', handler, data: 'commands' });
|
|
25
|
+
Keyboard.singleton.getContext('/space/2').bind({ binding: 'meta+/', handler, data: 'space 2' });
|
|
26
|
+
Keyboard.singleton.getContext('/space/2/b').bind({ binding: 'shift+meta+.', handler });
|
|
27
|
+
Keyboard.singleton.getContext('/space/1').bind({ binding: 'meta+/', handler, data: 'space 1' });
|
|
28
|
+
|
|
29
|
+
setBindings(Keyboard.singleton.getBindings());
|
|
30
|
+
|
|
31
|
+
return () => Keyboard.singleton.destroy();
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
const handleChange = (path: string) => {
|
|
35
|
+
Keyboard.singleton.setCurrentContext(path);
|
|
36
|
+
setContext(path);
|
|
37
|
+
setBindings(Keyboard.singleton.getBindings());
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// TODO(burdon): BUG: Error in order of hooks.
|
|
41
|
+
return (
|
|
42
|
+
<div className='flex flex-col gap-2'>
|
|
43
|
+
<div className='flex gap-2'>
|
|
44
|
+
<Select.Root value={context} onValueChange={handleChange}>
|
|
45
|
+
<Select.TriggerButton placeholder='Select context' />
|
|
46
|
+
<Select.Portal>
|
|
47
|
+
<Select.Content>
|
|
48
|
+
<Select.ScrollUpButton />
|
|
49
|
+
<Select.Viewport>
|
|
50
|
+
{contexts.map((context, i) => (
|
|
51
|
+
<Select.Option key={i} value={context}>
|
|
52
|
+
{context}
|
|
53
|
+
</Select.Option>
|
|
54
|
+
))}
|
|
55
|
+
</Select.Viewport>
|
|
56
|
+
<Select.ScrollDownButton />
|
|
57
|
+
<Select.Arrow />
|
|
58
|
+
</Select.Content>
|
|
59
|
+
</Select.Portal>
|
|
60
|
+
</Select.Root>
|
|
61
|
+
<Input.Root>
|
|
62
|
+
<Input.TextInput placeholder='Test input' />
|
|
63
|
+
</Input.Root>
|
|
64
|
+
</div>
|
|
65
|
+
<pre>{JSON.stringify(bindings, undefined, 2)}</pre>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export default {
|
|
71
|
+
title: 'keyboard/Keyboard',
|
|
72
|
+
decorators: [withTheme],
|
|
73
|
+
render: Story,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const Default = {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { expect } from 'chai';
|
|
6
|
+
|
|
7
|
+
import { describe, test } from '@dxos/test';
|
|
8
|
+
|
|
9
|
+
import { Keyboard } from './keyboard';
|
|
10
|
+
|
|
11
|
+
describe('keyboard', () => {
|
|
12
|
+
test('should pass', () => {
|
|
13
|
+
const keyboard = new Keyboard();
|
|
14
|
+
keyboard.bind({ binding: 'meta+k', handler: () => true });
|
|
15
|
+
expect(keyboard.getBindings()).to.have.length(1);
|
|
16
|
+
});
|
|
17
|
+
});
|
package/src/keyboard.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { invariant } from '@dxos/invariant';
|
|
6
|
+
|
|
7
|
+
export type KeyHandler = (props: {
|
|
8
|
+
context: string;
|
|
9
|
+
binding: string;
|
|
10
|
+
data?: any;
|
|
11
|
+
event: KeyboardEvent;
|
|
12
|
+
}) => boolean | void;
|
|
13
|
+
|
|
14
|
+
export type KeyBinding = {
|
|
15
|
+
binding: string;
|
|
16
|
+
handler: KeyHandler;
|
|
17
|
+
disableInput?: boolean;
|
|
18
|
+
data?: any;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Keybinding is normalized to this order.
|
|
22
|
+
const modifiers = ['alt', 'ctrl', 'shift', 'meta'];
|
|
23
|
+
|
|
24
|
+
class KeyboardContext {
|
|
25
|
+
readonly _keyMap = new Map<string, KeyBinding>();
|
|
26
|
+
|
|
27
|
+
get bindings() {
|
|
28
|
+
return Array.from(this._keyMap.values());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get(binding: string) {
|
|
32
|
+
return this._keyMap.get(binding);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
bind(config: KeyBinding) {
|
|
36
|
+
// Normalize order of modifiers.
|
|
37
|
+
const { binding } = config;
|
|
38
|
+
const parts = binding.split('+');
|
|
39
|
+
const mods = modifiers.filter((key) => parts.includes(key));
|
|
40
|
+
invariant(mods.length === 0 || mods.length === parts.length - 1);
|
|
41
|
+
if (mods.length) {
|
|
42
|
+
config.binding = [...mods, parts[parts.length - 1]].join('+');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this._keyMap.set(config.binding, config);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
unbind(binding: string) {
|
|
49
|
+
this._keyMap.delete(binding);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const ROOT = '';
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Manages context-aware key bindings.
|
|
57
|
+
*/
|
|
58
|
+
export class Keyboard {
|
|
59
|
+
static singleton = new Keyboard();
|
|
60
|
+
|
|
61
|
+
private readonly _root = new KeyboardContext();
|
|
62
|
+
private readonly _keyMap = new Map<string, KeyboardContext>([[ROOT, this._root]]);
|
|
63
|
+
private readonly _contexts: string[] = [ROOT];
|
|
64
|
+
private readonly _keyHandler = this.handleKeyDown.bind(this);
|
|
65
|
+
private _path = ROOT;
|
|
66
|
+
|
|
67
|
+
initialize() {
|
|
68
|
+
document.addEventListener('keydown', this._keyHandler);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
destroy() {
|
|
72
|
+
document.removeEventListener('keydown', this._keyHandler);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
bind = this._root.bind.bind(this._root);
|
|
76
|
+
unbind = this._root.unbind.bind(this._root);
|
|
77
|
+
|
|
78
|
+
setCurrentContext(path = ROOT) {
|
|
79
|
+
this._path = path;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
getCurrentContext() {
|
|
83
|
+
return this._path;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
getContext(path = ROOT): KeyboardContext {
|
|
87
|
+
let context = this._keyMap.get(path);
|
|
88
|
+
if (!context) {
|
|
89
|
+
context = new KeyboardContext();
|
|
90
|
+
this._keyMap.set(path, context);
|
|
91
|
+
this._contexts.push(path);
|
|
92
|
+
this._contexts.sort();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return context;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
getBindings() {
|
|
99
|
+
const bindings = new Map<string, KeyBinding>();
|
|
100
|
+
for (let i = 0; i < this._contexts.length; ++i) {
|
|
101
|
+
const path = this._contexts[i];
|
|
102
|
+
if (this._path.startsWith(path)) {
|
|
103
|
+
this.getContext(path).bindings.forEach((binding) => {
|
|
104
|
+
bindings.set(binding.binding, binding);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return Array.from(bindings.values());
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
handleKeyDown(event: KeyboardEvent) {
|
|
113
|
+
const { altKey, ctrlKey, metaKey, shiftKey, key } = event;
|
|
114
|
+
|
|
115
|
+
if (key !== 'Alt' && key !== 'Control' && key !== 'Meta' && key !== 'Shift') {
|
|
116
|
+
// Binding option to check for input or contenteditable.
|
|
117
|
+
const tagName = (event.target as any)?.tagName;
|
|
118
|
+
const isInput =
|
|
119
|
+
tagName === 'INPUT' || tagName === 'TEXTAREA' || (event.target as any)?.getAttribute('contenteditable');
|
|
120
|
+
|
|
121
|
+
// Normalized key binding.
|
|
122
|
+
const str = [altKey && 'alt', ctrlKey && 'ctrl', shiftKey && 'shift', metaKey && 'meta', key]
|
|
123
|
+
.filter(Boolean)
|
|
124
|
+
.join('+');
|
|
125
|
+
|
|
126
|
+
// Scan matching contexts.
|
|
127
|
+
for (let i = this._contexts.length - 1; i >= 0; --i) {
|
|
128
|
+
const path = this._contexts[i];
|
|
129
|
+
if (this._path.startsWith(path)) {
|
|
130
|
+
const { data, handler, disableInput } = this.getContext(path).get(str) ?? {};
|
|
131
|
+
if (handler && (!isInput || !disableInput)) {
|
|
132
|
+
const result = handler({ context: path, binding: str, data, event });
|
|
133
|
+
if (result !== false) {
|
|
134
|
+
// TODO(burdon): This doesn't prevent actions in markdown editor.
|
|
135
|
+
// Reference: https://codemirror.net/docs/ref/#view.KeyBinding
|
|
136
|
+
event.preventDefault();
|
|
137
|
+
event.stopPropagation();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
package/src/util.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
// https://www.w3.org/TR/DOM-Level-3-Events/#events-keyboardevents
|
|
6
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key
|
|
7
|
+
// https://support.apple.com/en-us/HT201236
|
|
8
|
+
// https://support.apple.com/guide/mac-help/what-are-those-symbols-shown-in-menus-cpmh0011/mac
|
|
9
|
+
|
|
10
|
+
// TODO(burdon): Show Mac/Windows/Linux variants.
|
|
11
|
+
const symbols: Record<string, string> = {
|
|
12
|
+
// Mods.
|
|
13
|
+
ctrl: '⌃',
|
|
14
|
+
shift: '⇧',
|
|
15
|
+
alt: '⌥',
|
|
16
|
+
meta: '⌘',
|
|
17
|
+
|
|
18
|
+
// Special keys.
|
|
19
|
+
Backspace: '⌫',
|
|
20
|
+
Enter: '⏎',
|
|
21
|
+
Escape: '⎋',
|
|
22
|
+
Space: '␣',
|
|
23
|
+
Tab: '⇥',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const keySymbols = (keyBinding: string): string[] => {
|
|
27
|
+
const parts = keyBinding.split('+');
|
|
28
|
+
return parts.map((part) => symbols[part] ?? part.toUpperCase());
|
|
29
|
+
};
|