@dxos/keyboard 0.3.10 → 0.3.11-main.00a28cb

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/README.md CHANGED
@@ -1,28 +1,163 @@
1
- ## React Async
1
+ ## @dxos/keyboard
2
2
 
3
- ### Configuration
3
+ Utility for managing contextual keyboard bindings.
4
4
 
5
- Add the following to `package.json`.
5
+ ## Common Shortcuts
6
6
 
7
- ```json
8
- "toolchain": {
9
- "forceCloseTests": true,
10
- "jsdom": true,
11
- "testingFramework": "mocha"
12
- }
13
- ```
7
+ - Meta => Command (macos), Windows (Windows), Super (Linux)
8
+ - Alt => Option (macos)
9
+ - Tip: Use https://tableconvert.com/markdown-to-markdown to edit the table.
14
10
 
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
11
+ | | macos | windows | chrome (macos) | chrome (windows) |
12
+ |-------------------|------------------------------------------|----------------------------------------------------------------------------------------------------------------|----------------|-----------------------------------------------------------------------------------|
13
+ | | https://support.apple.com/en-us/HT201236 | https://support.microsoft.com/en-us/windows/keyboard-shortcuts-in-windows-dcc61a57-8ff0-cffe-9796-cb9706c75eec | | https://support.google.com/chrome/answer/157179?hl=en&co=GENIE.Platform%3DDesktop |
14
+ | Alt-Tab | | x | | |
15
+ | Alt-F4 | | x | | |
16
+ | Alt-F8 | | x | | |
17
+ | Alt-Esc | | x | | |
18
+ | Alt-Enter | | x | | |
19
+ | Alt-Space | | x | | |
20
+ | Alt-A | | x | | |
21
+ | Alt-E | | | | x |
22
+ | Alt-F | | | | x |
23
+ | Alt-Meta-Esc | x | | | |
24
+ | Alt-Meta-Enter | | x | | |
25
+ | Alt-Meta-B | | x | x | |
26
+ | Alt-Meta-F | | | x | |
27
+ | Alt-Meta-I | | | x | |
28
+ | Alt-Meta-J | | | x | |
29
+ | Alt-Meta-K | | x | | |
30
+ | Alt-Meta-P | | | x | |
31
+ | Alt-Meta-R | | x | | |
32
+ | Alt-Meta-U | | | x | |
33
+ | Alt-Meta-Shift-A | | | x | |
34
+ | Alt-Meta-Shift-I | | | x | |
35
+ | "Alt-Ctrl-Meta-," | x | | | |
36
+ | Alt-Ctrl-Meta-. | x | | | |
37
+ | Alt-Ctrl-Meta-8 | x | | | |
38
+ | Alt-Ctrl-Tab | | x | | |
39
+ | Alt-Shift-A | | | | x |
40
+ | Alt-Shift-I | | | | x |
41
+ | Alt-Shift-T | | | | x |
42
+ | Ctrl-Esc | | x | | |
43
+ | Ctrl-Backspace | | | | x |
44
+ | Ctrl-Space | | x | | |
45
+ | Ctrl-Tab | | | | x |
46
+ | Ctrl-+ | | | | x |
47
+ | Ctrl-- | | | | x |
48
+ | Ctrl-F2 | x | | x | |
49
+ | Ctrl-F3 | x | | | |
50
+ | Ctrl-F4 | x | | | |
51
+ | Ctrl-F5 | x | | | x |
52
+ | Ctrl-F6 | x | | | x |
53
+ | Ctrl-F7 | x | | | |
54
+ | Ctrl-F8 | x | | | |
55
+ | Ctrl-F4 | | x | | |
56
+ | Ctrl-A | x | x | | |
57
+ | Ctrl-B | x | | | |
58
+ | Ctrl-C | | x | | |
59
+ | Ctrl-D | | x | | x |
60
+ | Ctrl-E | x | | | x |
61
+ | Ctrl-F | x | | | x |
62
+ | Ctrl-G | | | | x |
63
+ | Ctrl-H | | | | x |
64
+ | Ctrl-J | | | | x |
65
+ | Ctrl-K | | | | x |
66
+ | Ctrl-L | x | | | x |
67
+ | Ctrl-N | x | | | x |
68
+ | Ctrl-O | x | | | x |
69
+ | Ctrl-P | x | | | x |
70
+ | Ctrl-R | | x | | x |
71
+ | Ctrl-S | | | | x |
72
+ | Ctrl-T | x | | | x |
73
+ | Ctrl-U | | | | x |
74
+ | Ctrl-V | | x | | |
75
+ | Ctrl-W | | | | x |
76
+ | Ctrl-X | | x | | |
77
+ | Ctrl-Y | | x | | |
78
+ | Ctrl-Z | | x | | |
79
+ | Ctrl-Meta-Enter | | | | |
80
+ | Ctrl-Meta-Space | x | | | |
81
+ | Ctrl-Meta-C | | x | | |
82
+ | Ctrl-Meta-Q | x | x | | |
83
+ | Ctrl-Meta-F | x | x | x | |
84
+ | Ctrl-Meta-Shift-B | | x | | |
85
+ | Ctrl-Shift-F6 | x | | | |
86
+ | Ctrl-Shift-Esc | | x | | |
87
+ | Ctrl-Shift-Delete | | | | x |
88
+ | Ctrl-Shift-B | | | | x |
89
+ | Ctrl-Shift-D | | | | x |
90
+ | Ctrl-Shift-G | | | | x |
91
+ | Ctrl-Shift-J | | | | x |
92
+ | Ctrl-Shift-M | | | | |
93
+ | Ctrl-Shift-N | | | | x |
94
+ | Ctrl-Shift-O | | | | x |
95
+ | Ctrl-Shift-R | | | | x |
96
+ | Ctrl-Shift-T | | | | x |
97
+ | Ctrl-Shift-V | | x | | |
98
+ | Meta-. | | x | | |
99
+ | Meta-; | | x | | |
100
+ | "Meta-," | | x | x | |
101
+ | Meta-` | x | | | |
102
+ | Meta-+ | | x | x | |
103
+ | Meta-- | | x | x | |
104
+ | Meta-/ | | x | | |
105
+ | Meta-[ | | | x | |
106
+ | Meta-] | | | x | |
107
+ | Meta-Backspace | x | | | |
108
+ | Meta-Esc | | x | | |
109
+ | Meta-Space | x | x | | |
110
+ | Meta-Tab | x | x | | |
111
+ | Meta-A | x | x | | |
112
+ | Meta-B | | x | | |
113
+ | Meta-C | x | x | | |
114
+ | Meta-D | | x | x | |
115
+ | Meta-E | | x | x | |
116
+ | Meta-F | x | x | x | |
117
+ | Meta-G | x | x | x | |
118
+ | Meta-H | x | x | x | |
119
+ | Meta-I | | x | | |
120
+ | Meta-J | | x | | |
121
+ | Meta-K | | x | | |
122
+ | Meta-L | | x | x | |
123
+ | Meta-M | x | x | x | |
124
+ | Meta-N | | x | x | |
125
+ | Meta-O | x | x | x | |
126
+ | Meta-P | x | x | x | |
127
+ | Meta-Q | x | x | x | |
128
+ | Meta-R | | x | | |
129
+ | Meta-S | x | x | x | |
130
+ | Meta-T | x | x | x | |
131
+ | Meta-U | | x | | |
132
+ | Meta-V | x | x | | |
133
+ | Meta-W | x | x | x | |
134
+ | Meta-X | x | x | | |
135
+ | Meta-Y | | x | x | |
136
+ | Meta-Z | x | x | | |
137
+ | Meta-Shift-Space | | x | | |
138
+ | Meta-Shift-Delete | | | x | |
139
+ | Meta-Shift-+ | x | | | |
140
+ | Meta-Shift-- | x | | | |
141
+ | Meta-Shift-? | x | | | |
142
+ | Meta-Shift-3 | x | | | |
143
+ | Meta-Shift-4 | x | | | |
144
+ | Meta-Shift-5 | x | | | |
145
+ | Meta-Shift-B | | | x | |
146
+ | Meta-Shift-C | | x | | |
147
+ | Meta-Shift-D | | | x | |
148
+ | Meta-Shift-G | | | x | |
149
+ | Meta-Shift-H | | | x | |
150
+ | Meta-Shift-I | | | x | |
151
+ | Meta-Shift-J | | | x | |
152
+ | Meta-Shift-M | | x | x | |
153
+ | Meta-Shift-N | | | x | |
154
+ | Meta-Shift-R | | | | |
155
+ | Meta-Shift-S | x | x | | |
156
+ | Meta-Shift-T | | | x | |
157
+ | Meta-Shift-V | | x | | |
158
+ | Meta-Shift-W | | | x | |
159
+ | Meta-Shift-Q | x | | | |
160
+ | Meta-Shift-` | x | | | |
161
+ | Alt-Meta-` | x | | | |
162
+ | Ctrl-Tab | x | | | |
163
+ | Ctrl-Shift-Tab | x | | | |
@@ -9,13 +9,30 @@ import {
9
9
 
10
10
  // packages/common/keyboard/src/keyboard.ts
11
11
  import { invariant } from "@dxos/invariant";
12
- var __dxlog_file = "/home/circleci/project/packages/common/keyboard/src/keyboard.ts";
12
+ var __dxlog_file = "/home/runner/work/dxos/dxos/packages/common/keyboard/src/keyboard.ts";
13
13
  var modifiers = [
14
- "alt",
15
14
  "ctrl",
16
15
  "shift",
16
+ "alt",
17
17
  "meta"
18
18
  ];
19
+ var parseShortcut = (shortcut, delimiter = /[+-]/) => {
20
+ const parts = shortcut.split(delimiter);
21
+ const mods = modifiers.filter((key) => parts.includes(key));
22
+ invariant(mods.length === 0 || mods.length === parts.length - 1, void 0, {
23
+ F: __dxlog_file,
24
+ L: 29,
25
+ S: void 0,
26
+ A: [
27
+ "mods.length === 0 || mods.length === parts.length - 1",
28
+ ""
29
+ ]
30
+ });
31
+ return mods.length ? [
32
+ ...mods,
33
+ parts[parts.length - 1]
34
+ ].join("+") : shortcut;
35
+ };
19
36
  var KeyboardContext = class {
20
37
  constructor() {
21
38
  this._keyMap = /* @__PURE__ */ new Map();
@@ -27,25 +44,8 @@ var KeyboardContext = class {
27
44
  return this._keyMap.get(binding);
28
45
  }
29
46
  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);
47
+ config.shortcut = parseShortcut(config.shortcut);
48
+ this._keyMap.set(config.shortcut, config);
49
49
  }
50
50
  unbind(binding) {
51
51
  this._keyMap.delete(binding);
@@ -100,7 +100,7 @@ var Keyboard = class _Keyboard {
100
100
  const path = this._contexts[i];
101
101
  if (this._path.startsWith(path)) {
102
102
  this.getContext(path).bindings.forEach((binding) => {
103
- bindings.set(binding.binding, binding);
103
+ bindings.set(binding.shortcut, binding);
104
104
  });
105
105
  }
106
106
  }
@@ -125,7 +125,7 @@ var Keyboard = class _Keyboard {
125
125
  if (handler && (!isInput || !disableInput)) {
126
126
  const result = handler({
127
127
  context: path,
128
- binding: str,
128
+ shortcut: str,
129
129
  data,
130
130
  event
131
131
  });
@@ -142,25 +142,62 @@ var Keyboard = class _Keyboard {
142
142
  };
143
143
 
144
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"
145
+ import { getHostPlatform } from "@dxos/util";
146
+ var ctrl = {
147
+ macos: "\u2303",
148
+ ios: "\u2303",
149
+ windows: "Ctrl",
150
+ linux: "Ctrl",
151
+ unknown: "Ctrl"
152
+ };
153
+ var alt = {
154
+ macos: "\u2325",
155
+ ios: "\u2325",
156
+ windows: "Alt",
157
+ linux: "Alt",
158
+ unknown: "Alt"
159
+ };
160
+ var meta = {
161
+ macos: "\u2318",
162
+ ios: "\u2318",
163
+ windows: "\u229E",
164
+ // TODO(wittjosiah): Use ⌘ or ⊞ instead? Wait for user feedback.
165
+ // From https://en.wikipedia.org/wiki/Super_key_(keyboard_button).
166
+ linux: "\u2756",
167
+ unknown: "\u2756"
168
+ };
169
+ var getSymbol = (part) => {
170
+ const platform = getHostPlatform();
171
+ switch (part) {
172
+ case "alt":
173
+ return alt[platform];
174
+ case "ctrl":
175
+ return ctrl[platform];
176
+ case "meta":
177
+ return meta[platform];
178
+ case "shift":
179
+ return "\u21E7";
180
+ case "Backspace":
181
+ return "\u232B";
182
+ case "Enter":
183
+ return "\u23CE";
184
+ case "Escape":
185
+ return "\u238B";
186
+ case "Space":
187
+ return "\u2423";
188
+ case "Tab":
189
+ return "\u21E5";
190
+ default:
191
+ return part.toUpperCase();
192
+ }
157
193
  };
158
194
  var keySymbols = (keyBinding) => {
159
195
  const parts = keyBinding.split("+");
160
- return parts.map((part) => symbols[part] ?? part.toUpperCase());
196
+ return parts.map(getSymbol);
161
197
  };
162
198
  export {
163
199
  Keyboard,
164
- keySymbols
200
+ keySymbols,
201
+ parseShortcut
165
202
  };
166
203
  //# sourceMappingURL=index.mjs.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
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"]
4
+ "sourcesContent": ["//\n// Copyright 2023 DXOS.org\n//\n\nimport { invariant } from '@dxos/invariant';\n\nexport type KeyHandler = (props: {\n context: string;\n shortcut: string;\n data?: any;\n event: KeyboardEvent;\n}) => boolean | void;\n\nexport type KeyBinding = {\n shortcut: string;\n handler: KeyHandler;\n disableInput?: boolean;\n data?: any;\n};\n\n// Keybinding is normalized to this order.\n// https://support.apple.com/en-us/HT201236\nconst modifiers = ['ctrl', 'shift', 'alt', 'meta'];\n\n// Normalize order of modifiers.\nexport const parseShortcut = (shortcut: string, delimiter = /[+-]/): string => {\n const parts = shortcut.split(delimiter);\n const mods = modifiers.filter((key) => parts.includes(key));\n invariant(mods.length === 0 || mods.length === parts.length - 1);\n // Assume single natural key.\n return mods.length ? [...mods, parts[parts.length - 1]].join('+') : shortcut;\n};\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 config.shortcut = parseShortcut(config.shortcut);\n this._keyMap.set(config.shortcut, 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.shortcut, 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, shortcut: 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\nimport { getHostPlatform } from '@dxos/util';\n\n// Resources.\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://developer.apple.com/design/human-interface-guidelines/designing-for-macos\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\nconst ctrl: Record<string, string> = {\n macos: '⌃',\n ios: '⌃',\n windows: 'Ctrl',\n linux: 'Ctrl',\n unknown: 'Ctrl',\n};\n\nconst alt: Record<string, string> = {\n macos: '⌥',\n ios: '',\n windows: 'Alt',\n linux: 'Alt',\n unknown: 'Alt',\n};\n\nconst meta: Record<string, string> = {\n macos: '⌘',\n ios: '⌘',\n windows: '⊞',\n // TODO(wittjosiah): Use ⌘ or ⊞ instead? Wait for user feedback.\n // From https://en.wikipedia.org/wiki/Super_key_(keyboard_button).\n linux: '❖',\n unknown: '❖',\n};\n\nconst getSymbol = (part: string) => {\n const platform = getHostPlatform();\n switch (part) {\n // Mods.\n case 'alt':\n return alt[platform];\n case 'ctrl':\n return ctrl[platform];\n case 'meta':\n return meta[platform];\n case 'shift':\n return '⇧';\n // Special keys.\n case 'Backspace':\n return '⌫';\n case 'Enter':\n return '⏎';\n case 'Escape':\n return '⎋';\n case 'Space':\n return '␣';\n case 'Tab':\n return '⇥';\n default:\n return part.toUpperCase();\n }\n};\n\nexport const keySymbols = (keyBinding: string): string[] => {\n const parts = keyBinding.split('+');\n return parts.map(getSymbol);\n};\n"],
5
+ "mappings": "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",
6
+ "names": ["invariant", "modifiers", "parseShortcut", "shortcut", "delimiter", "parts", "split", "mods", "filter", "key", "includes", "length", "join", "KeyboardContext", "_keyMap", "Map", "bindings", "Array", "from", "values", "get", "binding", "bind", "config", "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", "getHostPlatform", "ctrl", "macos", "ios", "windows", "linux", "unknown", "alt", "meta", "getSymbol", "part", "platform", "getHostPlatform", "toUpperCase", "keySymbols", "keyBinding", "parts", "split", "map"]
7
7
  }
@@ -1 +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":15096,"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":2728,"imports":[{"path":"@inject-globals","kind":"import-statement","external":true}],"format":"esm"},"packages/common/keyboard/src/index.ts":{"bytes":539,"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":3577},"packages/common/keyboard/src/index.ts":{"bytesInOutput":0},"packages/common/keyboard/src/util.ts":{"bytesInOutput":362}},"bytes":4244}}}
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":15531,"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":5602,"imports":[{"path":"@dxos/util","kind":"import-statement","external":true},{"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":10382},"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},{"path":"@dxos/util","kind":"import-statement","external":true}],"exports":["Keyboard","keySymbols","parseShortcut"],"entryPoint":"packages/common/keyboard/src/index.ts","inputs":{"inject-globals:@inject-globals":{"bytesInOutput":79},"packages/common/keyboard/src/keyboard.ts":{"bytesInOutput":3623},"packages/common/keyboard/src/index.ts":{"bytesInOutput":0},"packages/common/keyboard/src/util.ts":{"bytesInOutput":1136}},"bytes":5081}}}
@@ -19,17 +19,36 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
19
19
  var node_exports = {};
20
20
  __export(node_exports, {
21
21
  Keyboard: () => Keyboard,
22
- keySymbols: () => keySymbols
22
+ keySymbols: () => keySymbols,
23
+ parseShortcut: () => parseShortcut
23
24
  });
24
25
  module.exports = __toCommonJS(node_exports);
25
26
  var import_invariant = require("@dxos/invariant");
26
- var __dxlog_file = "/home/circleci/project/packages/common/keyboard/src/keyboard.ts";
27
+ var import_util = require("@dxos/util");
28
+ var __dxlog_file = "/home/runner/work/dxos/dxos/packages/common/keyboard/src/keyboard.ts";
27
29
  var modifiers = [
28
- "alt",
29
30
  "ctrl",
30
31
  "shift",
32
+ "alt",
31
33
  "meta"
32
34
  ];
35
+ var parseShortcut = (shortcut, delimiter = /[+-]/) => {
36
+ const parts = shortcut.split(delimiter);
37
+ const mods = modifiers.filter((key) => parts.includes(key));
38
+ (0, import_invariant.invariant)(mods.length === 0 || mods.length === parts.length - 1, void 0, {
39
+ F: __dxlog_file,
40
+ L: 29,
41
+ S: void 0,
42
+ A: [
43
+ "mods.length === 0 || mods.length === parts.length - 1",
44
+ ""
45
+ ]
46
+ });
47
+ return mods.length ? [
48
+ ...mods,
49
+ parts[parts.length - 1]
50
+ ].join("+") : shortcut;
51
+ };
33
52
  var KeyboardContext = class {
34
53
  constructor() {
35
54
  this._keyMap = /* @__PURE__ */ new Map();
@@ -41,25 +60,8 @@ var KeyboardContext = class {
41
60
  return this._keyMap.get(binding);
42
61
  }
43
62
  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
+ config.shortcut = parseShortcut(config.shortcut);
64
+ this._keyMap.set(config.shortcut, config);
63
65
  }
64
66
  unbind(binding) {
65
67
  this._keyMap.delete(binding);
@@ -114,7 +116,7 @@ var Keyboard = class _Keyboard {
114
116
  const path = this._contexts[i];
115
117
  if (this._path.startsWith(path)) {
116
118
  this.getContext(path).bindings.forEach((binding) => {
117
- bindings.set(binding.binding, binding);
119
+ bindings.set(binding.shortcut, binding);
118
120
  });
119
121
  }
120
122
  }
@@ -139,7 +141,7 @@ var Keyboard = class _Keyboard {
139
141
  if (handler && (!isInput || !disableInput)) {
140
142
  const result = handler({
141
143
  context: path,
142
- binding: str,
144
+ shortcut: str,
143
145
  data,
144
146
  event
145
147
  });
@@ -154,26 +156,62 @@ var Keyboard = class _Keyboard {
154
156
  }
155
157
  }
156
158
  };
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"
159
+ var ctrl = {
160
+ macos: "\u2303",
161
+ ios: "\u2303",
162
+ windows: "Ctrl",
163
+ linux: "Ctrl",
164
+ unknown: "Ctrl"
165
+ };
166
+ var alt = {
167
+ macos: "\u2325",
168
+ ios: "\u2325",
169
+ windows: "Alt",
170
+ linux: "Alt",
171
+ unknown: "Alt"
172
+ };
173
+ var meta = {
174
+ macos: "\u2318",
175
+ ios: "\u2318",
176
+ windows: "\u229E",
177
+ // TODO(wittjosiah): Use ⌘ or ⊞ instead? Wait for user feedback.
178
+ // From https://en.wikipedia.org/wiki/Super_key_(keyboard_button).
179
+ linux: "\u2756",
180
+ unknown: "\u2756"
181
+ };
182
+ var getSymbol = (part) => {
183
+ const platform = (0, import_util.getHostPlatform)();
184
+ switch (part) {
185
+ case "alt":
186
+ return alt[platform];
187
+ case "ctrl":
188
+ return ctrl[platform];
189
+ case "meta":
190
+ return meta[platform];
191
+ case "shift":
192
+ return "\u21E7";
193
+ case "Backspace":
194
+ return "\u232B";
195
+ case "Enter":
196
+ return "\u23CE";
197
+ case "Escape":
198
+ return "\u238B";
199
+ case "Space":
200
+ return "\u2423";
201
+ case "Tab":
202
+ return "\u21E5";
203
+ default:
204
+ return part.toUpperCase();
205
+ }
169
206
  };
170
207
  var keySymbols = (keyBinding) => {
171
208
  const parts = keyBinding.split("+");
172
- return parts.map((part) => symbols[part] ?? part.toUpperCase());
209
+ return parts.map(getSymbol);
173
210
  };
174
211
  // Annotate the CommonJS export names for ESM import in node:
175
212
  0 && (module.exports = {
176
213
  Keyboard,
177
- keySymbols
214
+ keySymbols,
215
+ parseShortcut
178
216
  });
179
217
  //# sourceMappingURL=index.cjs.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
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": ["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"]
4
+ "sourcesContent": ["//\n// Copyright 2023 DXOS.org\n//\n\nimport { invariant } from '@dxos/invariant';\n\nexport type KeyHandler = (props: {\n context: string;\n shortcut: string;\n data?: any;\n event: KeyboardEvent;\n}) => boolean | void;\n\nexport type KeyBinding = {\n shortcut: string;\n handler: KeyHandler;\n disableInput?: boolean;\n data?: any;\n};\n\n// Keybinding is normalized to this order.\n// https://support.apple.com/en-us/HT201236\nconst modifiers = ['ctrl', 'shift', 'alt', 'meta'];\n\n// Normalize order of modifiers.\nexport const parseShortcut = (shortcut: string, delimiter = /[+-]/): string => {\n const parts = shortcut.split(delimiter);\n const mods = modifiers.filter((key) => parts.includes(key));\n invariant(mods.length === 0 || mods.length === parts.length - 1);\n // Assume single natural key.\n return mods.length ? [...mods, parts[parts.length - 1]].join('+') : shortcut;\n};\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 config.shortcut = parseShortcut(config.shortcut);\n this._keyMap.set(config.shortcut, 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.shortcut, 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, shortcut: 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\nimport { getHostPlatform } from '@dxos/util';\n\n// Resources.\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://developer.apple.com/design/human-interface-guidelines/designing-for-macos\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\nconst ctrl: Record<string, string> = {\n macos: '⌃',\n ios: '⌃',\n windows: 'Ctrl',\n linux: 'Ctrl',\n unknown: 'Ctrl',\n};\n\nconst alt: Record<string, string> = {\n macos: '⌥',\n ios: '',\n windows: 'Alt',\n linux: 'Alt',\n unknown: 'Alt',\n};\n\nconst meta: Record<string, string> = {\n macos: '⌘',\n ios: '⌘',\n windows: '⊞',\n // TODO(wittjosiah): Use ⌘ or ⊞ instead? Wait for user feedback.\n // From https://en.wikipedia.org/wiki/Super_key_(keyboard_button).\n linux: '❖',\n unknown: '❖',\n};\n\nconst getSymbol = (part: string) => {\n const platform = getHostPlatform();\n switch (part) {\n // Mods.\n case 'alt':\n return alt[platform];\n case 'ctrl':\n return ctrl[platform];\n case 'meta':\n return meta[platform];\n case 'shift':\n return '⇧';\n // Special keys.\n case 'Backspace':\n return '⌫';\n case 'Enter':\n return '⏎';\n case 'Escape':\n return '⎋';\n case 'Space':\n return '␣';\n case 'Tab':\n return '⇥';\n default:\n return part.toUpperCase();\n }\n};\n\nexport const keySymbols = (keyBinding: string): string[] => {\n const parts = keyBinding.split('+');\n return parts.map(getSymbol);\n};\n"],
5
+ "mappings": "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",
6
+ "names": ["modifiers", "parseShortcut", "shortcut", "delimiter", "parts", "split", "mods", "filter", "key", "includes", "invariant", "length", "join", "KeyboardContext", "_keyMap", "Map", "bindings", "Array", "from", "values", "get", "binding", "bind", "config", "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", "ctrl", "macos", "ios", "windows", "linux", "unknown", "alt", "meta", "getSymbol", "part", "platform", "getHostPlatform", "toUpperCase", "keySymbols", "keyBinding", "map"]
7
7
  }
@@ -1 +1 @@
1
- {"inputs":{"packages/common/keyboard/src/keyboard.ts":{"bytes":15096,"imports":[{"path":"@dxos/invariant","kind":"import-statement","external":true}],"format":"esm"},"packages/common/keyboard/src/util.ts":{"bytes":2728,"imports":[],"format":"esm"},"packages/common/keyboard/src/index.ts":{"bytes":539,"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":3577},"packages/common/keyboard/src/index.ts":{"bytesInOutput":0},"packages/common/keyboard/src/util.ts":{"bytesInOutput":362}},"bytes":4096}}}
1
+ {"inputs":{"packages/common/keyboard/src/keyboard.ts":{"bytes":15531,"imports":[{"path":"@dxos/invariant","kind":"import-statement","external":true}],"format":"esm"},"packages/common/keyboard/src/util.ts":{"bytes":5602,"imports":[{"path":"@dxos/util","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"}],"format":"esm"}},"outputs":{"packages/common/keyboard/dist/lib/node/index.cjs.map":{"imports":[],"exports":[],"inputs":{},"bytes":10373},"packages/common/keyboard/dist/lib/node/index.cjs":{"imports":[{"path":"@dxos/invariant","kind":"import-statement","external":true},{"path":"@dxos/util","kind":"import-statement","external":true}],"exports":["Keyboard","keySymbols","parseShortcut"],"entryPoint":"packages/common/keyboard/src/index.ts","inputs":{"packages/common/keyboard/src/keyboard.ts":{"bytesInOutput":3623},"packages/common/keyboard/src/index.ts":{"bytesInOutput":0},"packages/common/keyboard/src/util.ts":{"bytesInOutput":1136}},"bytes":4933}}}
@@ -1,15 +1,16 @@
1
1
  export type KeyHandler = (props: {
2
2
  context: string;
3
- binding: string;
3
+ shortcut: string;
4
4
  data?: any;
5
5
  event: KeyboardEvent;
6
6
  }) => boolean | void;
7
7
  export type KeyBinding = {
8
- binding: string;
8
+ shortcut: string;
9
9
  handler: KeyHandler;
10
10
  disableInput?: boolean;
11
11
  data?: any;
12
12
  };
13
+ export declare const parseShortcut: (shortcut: string, delimiter?: RegExp) => string;
13
14
  declare class KeyboardContext {
14
15
  readonly _keyMap: Map<string, KeyBinding>;
15
16
  get bindings(): KeyBinding[];
@@ -1 +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"}
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,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,KAAK,EAAE,aAAa,CAAC;CACtB,KAAK,OAAO,GAAG,IAAI,CAAC;AAErB,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,UAAU,CAAC;IACpB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,IAAI,CAAC,EAAE,GAAG,CAAC;CACZ,CAAC;AAOF,eAAO,MAAM,aAAa,aAAc,MAAM,yBAAuB,MAMpE,CAAC;AAEF,cAAM,eAAe;IACnB,QAAQ,CAAC,OAAO,0BAAiC;IAEjD,IAAI,QAAQ,iBAEX;IAED,GAAG,CAAC,OAAO,EAAE,MAAM;IAInB,IAAI,CAAC,MAAM,EAAE,UAAU;IAKvB,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"}
@@ -1 +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"}
1
+ {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../../../src/util.ts"],"names":[],"mappings":"AAmEA,eAAO,MAAM,UAAU,eAAgB,MAAM,KAAG,MAAM,EAGrD,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/keyboard",
3
- "version": "0.3.10",
3
+ "version": "0.3.11-main.00a28cb",
4
4
  "description": "Keyboard bindings",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -16,15 +16,16 @@
16
16
  "src"
17
17
  ],
18
18
  "dependencies": {
19
- "@dxos/node-std": "0.3.10",
20
- "@dxos/invariant": "0.3.10"
19
+ "@dxos/invariant": "0.3.11-main.00a28cb",
20
+ "@dxos/node-std": "0.3.11-main.00a28cb",
21
+ "@dxos/util": "0.3.11-main.00a28cb"
21
22
  },
22
23
  "devDependencies": {
23
24
  "react": "^18.2.0",
24
25
  "react-dom": "^18.2.0",
25
- "@dxos/react-ui-theme": "0.3.10",
26
- "@dxos/storybook-utils": "0.3.10",
27
- "@dxos/react-ui": "0.3.10"
26
+ "@dxos/react-ui-theme": "0.3.11-main.00a28cb",
27
+ "@dxos/storybook-utils": "0.3.11-main.00a28cb",
28
+ "@dxos/react-ui": "0.3.11-main.00a28cb"
28
29
  },
29
30
  "publishConfig": {
30
31
  "access": "public"
@@ -18,13 +18,13 @@ const Story = () => {
18
18
  const [bindings, setBindings] = useState({});
19
19
 
20
20
  useEffect(() => {
21
- const handler: KeyHandler = ({ context, binding, data }) => console.log({ context, binding, data });
21
+ const handler: KeyHandler = ({ context, shortcut, data }) => console.log({ context, shorcut: shortcut, data });
22
22
 
23
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' });
24
+ Keyboard.singleton.bind({ shortcut: 'meta+k', handler, data: 'commands' });
25
+ Keyboard.singleton.getContext('/space/2').bind({ shortcut: 'meta+/', handler, data: 'space 2' });
26
+ Keyboard.singleton.getContext('/space/2/b').bind({ shortcut: 'shift+meta+.', handler });
27
+ Keyboard.singleton.getContext('/space/1').bind({ shortcut: 'meta+/', handler, data: 'space 1' });
28
28
 
29
29
  setBindings(Keyboard.singleton.getBindings());
30
30
 
@@ -11,7 +11,7 @@ import { Keyboard } from './keyboard';
11
11
  describe('keyboard', () => {
12
12
  test('should pass', () => {
13
13
  const keyboard = new Keyboard();
14
- keyboard.bind({ binding: 'meta+k', handler: () => true });
14
+ keyboard.bind({ shortcut: 'meta+k', handler: () => true });
15
15
  expect(keyboard.getBindings()).to.have.length(1);
16
16
  });
17
17
  });
package/src/keyboard.ts CHANGED
@@ -6,20 +6,30 @@ import { invariant } from '@dxos/invariant';
6
6
 
7
7
  export type KeyHandler = (props: {
8
8
  context: string;
9
- binding: string;
9
+ shortcut: string;
10
10
  data?: any;
11
11
  event: KeyboardEvent;
12
12
  }) => boolean | void;
13
13
 
14
14
  export type KeyBinding = {
15
- binding: string;
15
+ shortcut: string;
16
16
  handler: KeyHandler;
17
17
  disableInput?: boolean;
18
18
  data?: any;
19
19
  };
20
20
 
21
21
  // Keybinding is normalized to this order.
22
- const modifiers = ['alt', 'ctrl', 'shift', 'meta'];
22
+ // https://support.apple.com/en-us/HT201236
23
+ const modifiers = ['ctrl', 'shift', 'alt', 'meta'];
24
+
25
+ // Normalize order of modifiers.
26
+ export const parseShortcut = (shortcut: string, delimiter = /[+-]/): string => {
27
+ const parts = shortcut.split(delimiter);
28
+ const mods = modifiers.filter((key) => parts.includes(key));
29
+ invariant(mods.length === 0 || mods.length === parts.length - 1);
30
+ // Assume single natural key.
31
+ return mods.length ? [...mods, parts[parts.length - 1]].join('+') : shortcut;
32
+ };
23
33
 
24
34
  class KeyboardContext {
25
35
  readonly _keyMap = new Map<string, KeyBinding>();
@@ -33,16 +43,8 @@ class KeyboardContext {
33
43
  }
34
44
 
35
45
  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
+ config.shortcut = parseShortcut(config.shortcut);
47
+ this._keyMap.set(config.shortcut, config);
46
48
  }
47
49
 
48
50
  unbind(binding: string) {
@@ -101,7 +103,7 @@ export class Keyboard {
101
103
  const path = this._contexts[i];
102
104
  if (this._path.startsWith(path)) {
103
105
  this.getContext(path).bindings.forEach((binding) => {
104
- bindings.set(binding.binding, binding);
106
+ bindings.set(binding.shortcut, binding);
105
107
  });
106
108
  }
107
109
  }
@@ -129,7 +131,7 @@ export class Keyboard {
129
131
  if (this._path.startsWith(path)) {
130
132
  const { data, handler, disableInput } = this.getContext(path).get(str) ?? {};
131
133
  if (handler && (!isInput || !disableInput)) {
132
- const result = handler({ context: path, binding: str, data, event });
134
+ const result = handler({ context: path, shortcut: str, data, event });
133
135
  if (result !== false) {
134
136
  // TODO(burdon): This doesn't prevent actions in markdown editor.
135
137
  // Reference: https://codemirror.net/docs/ref/#view.KeyBinding
package/src/util.ts CHANGED
@@ -2,28 +2,70 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
+ import { getHostPlatform } from '@dxos/util';
6
+
7
+ // Resources.
5
8
  // https://www.w3.org/TR/DOM-Level-3-Events/#events-keyboardevents
6
9
  // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key
10
+ // https://developer.apple.com/design/human-interface-guidelines/designing-for-macos
7
11
  // https://support.apple.com/en-us/HT201236
8
12
  // https://support.apple.com/guide/mac-help/what-are-those-symbols-shown-in-menus-cpmh0011/mac
9
13
 
10
- // TODO(burdon): Show Mac/Windows/Linux variants.
11
- const symbols: Record<string, string> = {
12
- // Mods.
13
- ctrl: '',
14
- shift: '',
15
- alt: '',
16
- meta: '⌘',
14
+ const ctrl: Record<string, string> = {
15
+ macos: '⌃',
16
+ ios: '⌃',
17
+ windows: 'Ctrl',
18
+ linux: 'Ctrl',
19
+ unknown: 'Ctrl',
20
+ };
21
+
22
+ const alt: Record<string, string> = {
23
+ macos: '⌥',
24
+ ios: '⌥',
25
+ windows: 'Alt',
26
+ linux: 'Alt',
27
+ unknown: 'Alt',
28
+ };
29
+
30
+ const meta: Record<string, string> = {
31
+ macos: '⌘',
32
+ ios: '⌘',
33
+ windows: '⊞',
34
+ // TODO(wittjosiah): Use ⌘ or ⊞ instead? Wait for user feedback.
35
+ // From https://en.wikipedia.org/wiki/Super_key_(keyboard_button).
36
+ linux: '❖',
37
+ unknown: '❖',
38
+ };
17
39
 
18
- // Special keys.
19
- Backspace: '⌫',
20
- Enter: '⏎',
21
- Escape: '⎋',
22
- Space: '',
23
- Tab: '⇥',
40
+ const getSymbol = (part: string) => {
41
+ const platform = getHostPlatform();
42
+ switch (part) {
43
+ // Mods.
44
+ case 'alt':
45
+ return alt[platform];
46
+ case 'ctrl':
47
+ return ctrl[platform];
48
+ case 'meta':
49
+ return meta[platform];
50
+ case 'shift':
51
+ return '⇧';
52
+ // Special keys.
53
+ case 'Backspace':
54
+ return '⌫';
55
+ case 'Enter':
56
+ return '⏎';
57
+ case 'Escape':
58
+ return '⎋';
59
+ case 'Space':
60
+ return '␣';
61
+ case 'Tab':
62
+ return '⇥';
63
+ default:
64
+ return part.toUpperCase();
65
+ }
24
66
  };
25
67
 
26
68
  export const keySymbols = (keyBinding: string): string[] => {
27
69
  const parts = keyBinding.split('+');
28
- return parts.map((part) => symbols[part] ?? part.toUpperCase());
70
+ return parts.map(getSymbol);
29
71
  };