entangled-worlds-orbiters-shared 4.2.0 → 5.1.0
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 +3 -2
- package/dist/shortcuts/catalogue.d.ts +72 -0
- package/dist/shortcuts/catalogue.js +118 -0
- package/dist/shortcuts/chord.d.ts +24 -2
- package/dist/shortcuts/chord.js +86 -8
- package/dist/shortcuts/format.d.ts +7 -4
- package/dist/shortcuts/format.js +40 -14
- package/dist/shortcuts/index.d.ts +4 -8
- package/dist/shortcuts/index.js +8 -11
- package/dist/shortcuts/match.d.ts +14 -6
- package/dist/shortcuts/match.js +14 -8
- package/dist/shortcuts/source.d.ts +62 -0
- package/dist/shortcuts/source.js +139 -0
- package/dist/sky/geometry.d.ts +31 -0
- package/dist/sky/geometry.js +84 -0
- package/dist/sky/index.d.ts +6 -0
- package/dist/sky/index.js +6 -0
- package/dist/sky/params.d.ts +44 -0
- package/dist/sky/params.js +52 -0
- package/dist/sky/shaders.d.ts +14 -0
- package/dist/sky/shaders.js +149 -0
- package/dist/sky/sky.d.ts +24 -0
- package/dist/sky/sky.js +268 -0
- package/dist/sky/worldSkyFile.d.ts +16 -0
- package/dist/sky/worldSkyFile.js +51 -0
- package/package.json +8 -2
- package/src/shortcuts/catalogue.ts +172 -0
- package/src/shortcuts/chord.ts +96 -6
- package/src/shortcuts/format.ts +44 -17
- package/src/shortcuts/index.ts +31 -28
- package/src/shortcuts/match.ts +25 -13
- package/src/shortcuts/source.ts +204 -0
- package/src/sky/geometry.ts +99 -0
- package/src/sky/index.ts +16 -0
- package/src/sky/params.ts +89 -0
- package/src/sky/shaders.ts +159 -0
- package/src/sky/sky.ts +330 -0
- package/src/sky/worldSkyFile.ts +60 -0
- package/dist/shortcuts/entangledWorlds.d.ts +0 -21
- package/dist/shortcuts/entangledWorlds.js +0 -40
- package/dist/shortcuts/labels.d.ts +0 -10
- package/dist/shortcuts/labels.js +0 -130
- package/dist/shortcuts/orbiters.d.ts +0 -55
- package/dist/shortcuts/orbiters.js +0 -109
- package/dist/shortcuts/overrides.d.ts +0 -30
- package/dist/shortcuts/overrides.js +0 -81
- package/dist/shortcuts/sections.d.ts +0 -23
- package/dist/shortcuts/sections.js +0 -19
- package/dist/shortcuts/site.d.ts +0 -9
- package/dist/shortcuts/site.js +0 -25
- package/src/shortcuts/entangledWorlds.ts +0 -51
- package/src/shortcuts/labels.ts +0 -139
- package/src/shortcuts/orbiters.ts +0 -132
- package/src/shortcuts/overrides.ts +0 -110
- package/src/shortcuts/sections.ts +0 -36
- package/src/shortcuts/site.ts +0 -33
package/README.md
CHANGED
|
@@ -99,12 +99,13 @@ The capability registry (`createCapabilityRegistry`, `validate` / `assertValid`,
|
|
|
99
99
|
- `largestFunction`, `shrinkWasm` - The size check and the shrink step
|
|
100
100
|
|
|
101
101
|
### Keyboard shortcuts (`entangled-worlds-orbiters-shared/shortcuts`)
|
|
102
|
-
- `ShortcutAction` - One catalogue entry: `id`, `
|
|
102
|
+
- `ShortcutAction` - One catalogue entry: `id`, `label` (+ `names` by language), `defaultKeys` (named as a US keyboard names them, `event.code`; a press matches by the character it types, by its place only where it types no US character, so keys work on any keyboard), `scope`
|
|
103
103
|
- `matchShortcut(event, bindings)` - Which action a press is, or `null`: modifiers, a field being typed in and surface claims decided in one place
|
|
104
104
|
- Key strings: `Shift+KeyR`, `Mod+KeyZ` (Ctrl or ⌘), `Shift?+KeyQ` (Shift may be held); `parseChord`, `chordsOverlap`
|
|
105
105
|
- `resolveBindings(catalogue, overrides)`, `findClashes` - Defaults plus a person's overrides (`[{ id, keys }]` as their settings store them, `keys: []` unbinds), and the clash check
|
|
106
106
|
- `claimKeys`, `isKeyClaimed` - A surface (an on-screen keyboard) takes letters away from the shortcuts while it is up
|
|
107
|
-
- `
|
|
107
|
+
- `createShortcutSource({ readCatalogue, readAccount })` - The catalogue (every app's actions and default keys, served by the backend: this package holds none) and a person's own keys, read at start and again when the page comes back to the front; `actions(...sectionIds)`, `bindings(actions)`, `subscribe`
|
|
108
|
+
- `parseCatalogue`, `sectionActions`, `findSection`, `shortcutApps`, `localName` - Reading a served catalogue and its names in a language
|
|
108
109
|
|
|
109
110
|
### Uploads (`entangled-worlds-orbiters-shared/uploads`)
|
|
110
111
|
- `uploadViaSignedUrl` - The API's signed-URL upload: `/spaces/upload-init` → PUT → `/spaces/upload-complete`, over the app's own authenticated JSON caller
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ShortcutAction, ShortcutNames } from './match.js';
|
|
2
|
+
/** Something with a name: English `label`, other languages in `names`. */
|
|
3
|
+
export interface ShortcutNamed {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly label: string;
|
|
6
|
+
readonly names?: ShortcutNames;
|
|
7
|
+
}
|
|
8
|
+
/** A row of a keys sheet (`?`): one line for one or more actions whose keys show together. */
|
|
9
|
+
export interface ShortcutSheetRow extends ShortcutNamed {
|
|
10
|
+
readonly actions: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
export interface ShortcutSheetColumn extends ShortcutNamed {
|
|
13
|
+
readonly rows: readonly ShortcutSheetRow[];
|
|
14
|
+
}
|
|
15
|
+
/** One surface's keys (`orbiters-play`, `ew-studio`, `site`). Two actions clash only inside a section. */
|
|
16
|
+
export interface ShortcutSection {
|
|
17
|
+
readonly id: string;
|
|
18
|
+
/** The app it belongs to (`orbiters`) and that app's name. */
|
|
19
|
+
readonly app: string;
|
|
20
|
+
readonly appLabel: string;
|
|
21
|
+
readonly appNames?: ShortcutNames;
|
|
22
|
+
readonly label: string;
|
|
23
|
+
readonly names?: ShortcutNames;
|
|
24
|
+
/** Its name inside the app (`Play`, `Studio`); none when the app has only this section. */
|
|
25
|
+
readonly part?: string;
|
|
26
|
+
readonly partNames?: ShortcutNames;
|
|
27
|
+
/** How a command menu groups the actions, in its order. */
|
|
28
|
+
readonly groups: readonly ShortcutNamed[];
|
|
29
|
+
/** The keys sheet, where the app shows one in columns. */
|
|
30
|
+
readonly sheet?: readonly ShortcutSheetColumn[];
|
|
31
|
+
readonly actions: readonly ShortcutAction[];
|
|
32
|
+
/**
|
|
33
|
+
* Letters a surface of this section plays while it is open (`event.key`, lower-case; the Orbiters
|
|
34
|
+
* on-screen keyboard): a shortcut on one of them waits until it closes.
|
|
35
|
+
*/
|
|
36
|
+
readonly heldKeys?: readonly string[];
|
|
37
|
+
}
|
|
38
|
+
export interface ShortcutCatalogue {
|
|
39
|
+
/** Changes whenever the catalogue is edited. */
|
|
40
|
+
readonly version: string;
|
|
41
|
+
readonly sections: readonly ShortcutSection[];
|
|
42
|
+
}
|
|
43
|
+
/** Before the catalogue is read: no sections, so no key does anything yet. */
|
|
44
|
+
export declare const EMPTY_CATALOGUE: ShortcutCatalogue;
|
|
45
|
+
/**
|
|
46
|
+
* The catalogue a response holds (`{ version, sections }`), or `null` when it holds none. A malformed
|
|
47
|
+
* section or action is left out rather than failing the whole read.
|
|
48
|
+
*/
|
|
49
|
+
export declare function parseCatalogue(value: unknown): ShortcutCatalogue | null;
|
|
50
|
+
/** A name in a language (`es`, `es-AR`), the English when there is none in it. */
|
|
51
|
+
export declare function localName(item: {
|
|
52
|
+
readonly label: string;
|
|
53
|
+
readonly names?: ShortcutNames;
|
|
54
|
+
}, locale?: string): string;
|
|
55
|
+
/** A section's app name in a language. */
|
|
56
|
+
export declare function localAppName(section: Pick<ShortcutSection, 'appLabel' | 'appNames'>, locale?: string): string;
|
|
57
|
+
/** A section's part name (`Play`, `Studio`) in a language; `undefined` when it has none. */
|
|
58
|
+
export declare function localPartName(section: Pick<ShortcutSection, 'part' | 'partNames'>, locale?: string): string | undefined;
|
|
59
|
+
/** The apps the catalogue has, in its order. */
|
|
60
|
+
export declare function shortcutApps(catalogue: ShortcutCatalogue): ReadonlyArray<{
|
|
61
|
+
id: string;
|
|
62
|
+
label: string;
|
|
63
|
+
names?: ShortcutNames;
|
|
64
|
+
}>;
|
|
65
|
+
/**
|
|
66
|
+
* The actions of these sections, in the order named: where two sections hold one key, the first
|
|
67
|
+
* named wins (the Orbiter Studio lies over play). The same list object for as long as the catalogue
|
|
68
|
+
* is the same, so a cache keyed by it holds.
|
|
69
|
+
*/
|
|
70
|
+
export declare function sectionActions(catalogue: ShortcutCatalogue, ...sectionIds: string[]): readonly ShortcutAction[];
|
|
71
|
+
/** One section, if the catalogue has it. */
|
|
72
|
+
export declare function findSection(catalogue: ShortcutCatalogue, sectionId: string): ShortcutSection | undefined;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// The shortcut catalogue as the apps read it: every app's sections of actions, their default keys and
|
|
2
|
+
// names, as the backend serves them (from its config, so a key or a name changes without a library
|
|
3
|
+
// release). This file only reads what was served; it holds no actions of its own.
|
|
4
|
+
/** Before the catalogue is read: no sections, so no key does anything yet. */
|
|
5
|
+
export const EMPTY_CATALOGUE = Object.freeze({ version: '', sections: Object.freeze([]) });
|
|
6
|
+
const isRecord = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
|
|
7
|
+
const isString = (value) => typeof value === 'string' && value.length > 0;
|
|
8
|
+
function names(value) {
|
|
9
|
+
if (!isRecord(value))
|
|
10
|
+
return undefined;
|
|
11
|
+
const out = {};
|
|
12
|
+
for (const [locale, name] of Object.entries(value))
|
|
13
|
+
if (isString(name))
|
|
14
|
+
out[locale] = name;
|
|
15
|
+
return Object.freeze(out);
|
|
16
|
+
}
|
|
17
|
+
function named(value) {
|
|
18
|
+
if (!isRecord(value) || !isString(value.id) || !isString(value.label))
|
|
19
|
+
return null;
|
|
20
|
+
return { id: value.id, label: value.label, names: names(value.names) };
|
|
21
|
+
}
|
|
22
|
+
function action(value) {
|
|
23
|
+
const base = named(value);
|
|
24
|
+
if (!base || !isRecord(value) || !isString(value.scope) || !Array.isArray(value.defaultKeys))
|
|
25
|
+
return null;
|
|
26
|
+
return Object.freeze({
|
|
27
|
+
...base,
|
|
28
|
+
defaultKeys: Object.freeze(value.defaultKeys.filter(isString)),
|
|
29
|
+
scope: value.scope,
|
|
30
|
+
...(isString(value.group) ? { group: value.group } : {}),
|
|
31
|
+
...(value.command === false ? { command: false } : {}),
|
|
32
|
+
...(isString(value.href) ? { href: value.href } : {}),
|
|
33
|
+
...(value.signedIn === true ? { signedIn: true } : {}),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
const list = (value, read) => Object.freeze(Array.isArray(value) ? value.map(read).filter((item) => item !== null) : []);
|
|
37
|
+
function section(value) {
|
|
38
|
+
const base = named(value);
|
|
39
|
+
if (!base || !isRecord(value) || !isString(value.app) || !isString(value.appLabel))
|
|
40
|
+
return null;
|
|
41
|
+
return Object.freeze({
|
|
42
|
+
...base,
|
|
43
|
+
app: value.app,
|
|
44
|
+
appLabel: value.appLabel,
|
|
45
|
+
appNames: names(value.appNames),
|
|
46
|
+
...(isString(value.part) ? { part: value.part, partNames: names(value.partNames) } : {}),
|
|
47
|
+
groups: list(value.groups, named),
|
|
48
|
+
...(Array.isArray(value.sheet)
|
|
49
|
+
? {
|
|
50
|
+
sheet: list(value.sheet, (column) => {
|
|
51
|
+
const head = named(column);
|
|
52
|
+
if (!head || !isRecord(column))
|
|
53
|
+
return null;
|
|
54
|
+
return Object.freeze({
|
|
55
|
+
...head,
|
|
56
|
+
rows: list(column.rows, (row) => {
|
|
57
|
+
const line = named(row);
|
|
58
|
+
return line && isRecord(row) ? Object.freeze({ ...line, actions: list(row.actions, (id) => (isString(id) ? id : null)) }) : null;
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
}),
|
|
62
|
+
}
|
|
63
|
+
: {}),
|
|
64
|
+
actions: list(value.actions, action),
|
|
65
|
+
...(Array.isArray(value.heldKeys) ? { heldKeys: list(value.heldKeys, (key) => (isString(key) ? key : null)) } : {}),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The catalogue a response holds (`{ version, sections }`), or `null` when it holds none. A malformed
|
|
70
|
+
* section or action is left out rather than failing the whole read.
|
|
71
|
+
*/
|
|
72
|
+
export function parseCatalogue(value) {
|
|
73
|
+
if (!isRecord(value) || !Array.isArray(value.sections))
|
|
74
|
+
return null;
|
|
75
|
+
return Object.freeze({ version: typeof value.version === 'string' ? value.version : '', sections: list(value.sections, section) });
|
|
76
|
+
}
|
|
77
|
+
/** A name in a language (`es`, `es-AR`), the English when there is none in it. */
|
|
78
|
+
export function localName(item, locale) {
|
|
79
|
+
return item.names?.[(locale ?? '').slice(0, 2).toLowerCase()] ?? item.label;
|
|
80
|
+
}
|
|
81
|
+
/** A section's app name in a language. */
|
|
82
|
+
export function localAppName(section, locale) {
|
|
83
|
+
return localName({ label: section.appLabel, names: section.appNames }, locale);
|
|
84
|
+
}
|
|
85
|
+
/** A section's part name (`Play`, `Studio`) in a language; `undefined` when it has none. */
|
|
86
|
+
export function localPartName(section, locale) {
|
|
87
|
+
return section.part === undefined ? undefined : localName({ label: section.part, names: section.partNames }, locale);
|
|
88
|
+
}
|
|
89
|
+
/** The apps the catalogue has, in its order. */
|
|
90
|
+
export function shortcutApps(catalogue) {
|
|
91
|
+
const seen = new Map();
|
|
92
|
+
for (const s of catalogue.sections)
|
|
93
|
+
if (!seen.has(s.app))
|
|
94
|
+
seen.set(s.app, { id: s.app, label: s.appLabel, names: s.appNames });
|
|
95
|
+
return [...seen.values()];
|
|
96
|
+
}
|
|
97
|
+
const actionLists = new WeakMap();
|
|
98
|
+
/**
|
|
99
|
+
* The actions of these sections, in the order named: where two sections hold one key, the first
|
|
100
|
+
* named wins (the Orbiter Studio lies over play). The same list object for as long as the catalogue
|
|
101
|
+
* is the same, so a cache keyed by it holds.
|
|
102
|
+
*/
|
|
103
|
+
export function sectionActions(catalogue, ...sectionIds) {
|
|
104
|
+
let lists = actionLists.get(catalogue);
|
|
105
|
+
if (!lists)
|
|
106
|
+
actionLists.set(catalogue, (lists = new Map()));
|
|
107
|
+
const key = sectionIds.join('\n');
|
|
108
|
+
let found = lists.get(key);
|
|
109
|
+
if (!found) {
|
|
110
|
+
found = Object.freeze(sectionIds.flatMap((id) => catalogue.sections.find((s) => s.id === id)?.actions ?? []));
|
|
111
|
+
lists.set(key, found);
|
|
112
|
+
}
|
|
113
|
+
return found;
|
|
114
|
+
}
|
|
115
|
+
/** One section, if the catalogue has it. */
|
|
116
|
+
export function findSection(catalogue, sectionId) {
|
|
117
|
+
return catalogue.sections.find((s) => s.id === sectionId);
|
|
118
|
+
}
|
|
@@ -18,8 +18,30 @@ export interface ModifierState {
|
|
|
18
18
|
}
|
|
19
19
|
/** The chord a key string names, or `null` when it names none (an unknown modifier, a doubled one, no key). */
|
|
20
20
|
export declare function parseChord(keys: string): Chord | null;
|
|
21
|
-
/**
|
|
22
|
-
export
|
|
21
|
+
/** A press as a chord reads it: the modifiers, the key's place and, when the browser says, the character it typed. */
|
|
22
|
+
export interface ChordPress extends ModifierState {
|
|
23
|
+
readonly code: string;
|
|
24
|
+
readonly key?: string;
|
|
25
|
+
}
|
|
26
|
+
/** How a press is this chord: by the character it typed, by the key's place, or not at all. */
|
|
27
|
+
export type ChordMatch = 'character' | 'place' | null;
|
|
28
|
+
/**
|
|
29
|
+
* How a press is this chord. `character` beats `place`: on a Spanish keyboard Shift+7 types `/`, so it
|
|
30
|
+
* is the `/` key before it is the `7` key. A press with no `key` (an old caller) matches by place only.
|
|
31
|
+
*/
|
|
32
|
+
export declare function chordMatch(chord: Chord, press: ChordPress): ChordMatch;
|
|
33
|
+
/** Whether a press of `code` with these modifiers (and, when given, the character it typed) is this chord. */
|
|
34
|
+
export declare function chordMatches(chord: Chord, code: string, state: ModifierState & {
|
|
35
|
+
readonly key?: string;
|
|
36
|
+
}): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* The key string a typed character names (`/` → `Slash`, `?` → `Shift+Slash`, `q` → `KeyQ`), for
|
|
39
|
+
* someone setting a key; `null` for a character a US keyboard does not have, or none.
|
|
40
|
+
*/
|
|
41
|
+
export declare function typedKey(key: string | undefined): {
|
|
42
|
+
readonly code: string;
|
|
43
|
+
readonly shift?: boolean;
|
|
44
|
+
} | null;
|
|
23
45
|
/** A chord that needs Ctrl or ⌘: a command, never a character typed. */
|
|
24
46
|
export declare function isCommandChord(chord: Chord): boolean;
|
|
25
47
|
/** Whether one press could be both chords — the test for a clash. */
|
package/dist/shortcuts/chord.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
// One key with its modifiers, written as a string: `Shift+KeyR`, `Mod+KeyZ`, `Shift?+Digit1`.
|
|
2
2
|
//
|
|
3
|
-
// The last part
|
|
4
|
-
//
|
|
3
|
+
// The last part names the key the way a US keyboard does (`KeyboardEvent.code`: `KeyQ`, `Slash`,
|
|
4
|
+
// `Digit1`). A press matches it by the CHARACTER it types wherever it types one, so `/` is the key
|
|
5
|
+
// that prints `/` on this person's keyboard (Shift+7 on a Spanish one), and `KeyQ` is the key that
|
|
6
|
+
// prints Q (top left on a French one). Only where the key types no character the US keyboard has
|
|
7
|
+
// (a Greek or Cyrillic letter, ⌥ on a Mac changing it, a dead key, no letter on a letter key) is the
|
|
8
|
+
// key matched by its place.
|
|
9
|
+
// Digits match by character first, then by place: a French keyboard types `&` there.
|
|
10
|
+
// Before the key, the modifiers:
|
|
5
11
|
//
|
|
6
12
|
// Shift, Alt, Ctrl, Meta held. One left out must NOT be held.
|
|
7
13
|
// Mod Ctrl or ⌘ (either, or both), the command key of every platform.
|
|
@@ -48,18 +54,90 @@ function parse(keys) {
|
|
|
48
54
|
return { code, ...rules };
|
|
49
55
|
}
|
|
50
56
|
const fits = (rule, down) => rule === 'any' || (rule === 'held') === Boolean(down);
|
|
51
|
-
/**
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
/**
|
|
58
|
+
* The punctuation keys a chord may name, and what a US keyboard types there without and with Shift:
|
|
59
|
+
* what the chord means on every keyboard. With these, Shift is part of the character (`?` is
|
|
60
|
+
* `Shift+Slash`), so a keyboard that needs Shift, or AltGr, for a `/` still types the `/`.
|
|
61
|
+
*/
|
|
62
|
+
const PUNCTUATION = {
|
|
63
|
+
Minus: ['-', '_'], Equal: ['=', '+'], BracketLeft: ['[', '{'], BracketRight: [']', '}'], Backslash: ['\\', '|'],
|
|
64
|
+
Semicolon: [';', ':'], Quote: ["'", '"'], Backquote: ['`', '~'], Comma: [',', '<'], Period: ['.', '>'], Slash: ['/', '?'],
|
|
65
|
+
};
|
|
66
|
+
/** The code a US keyboard types each punctuation character at, and whether it takes Shift. */
|
|
67
|
+
const PUNCTUATION_PLACE = new Map(Object.entries(PUNCTUATION).flatMap(([code, [plain, shifted]]) => [[plain, [code, false]], [shifted, [code, true]]]));
|
|
68
|
+
/** The character a press typed, when it is one a US keyboard has (printable ASCII); else `null`. */
|
|
69
|
+
function typed(key) {
|
|
70
|
+
return typeof key === 'string' && key.length === 1 && key >= '!' && key <= '~' ? key : null;
|
|
71
|
+
}
|
|
72
|
+
function commandFits(chord, state) {
|
|
57
73
|
if (chord.mod === 'held')
|
|
58
74
|
return Boolean(state.ctrlKey || state.metaKey);
|
|
59
75
|
if (chord.mod === 'any')
|
|
60
76
|
return true;
|
|
61
77
|
return fits(chord.ctrl, state.ctrlKey) && fits(chord.meta, state.metaKey);
|
|
62
78
|
}
|
|
79
|
+
function placeMatch(chord, code, state) {
|
|
80
|
+
return chord.code === code && fits(chord.shift, state.shiftKey) && fits(chord.alt, state.altKey) && commandFits(chord, state);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* How a press is this chord. `character` beats `place`: on a Spanish keyboard Shift+7 types `/`, so it
|
|
84
|
+
* is the `/` key before it is the `7` key. A press with no `key` (an old caller) matches by place only.
|
|
85
|
+
*/
|
|
86
|
+
export function chordMatch(chord, press) {
|
|
87
|
+
const char = typed(press.key);
|
|
88
|
+
if (char === null)
|
|
89
|
+
return placeMatch(chord, press.code, press) ? 'place' : null;
|
|
90
|
+
const punctuation = PUNCTUATION[chord.code];
|
|
91
|
+
if (punctuation) {
|
|
92
|
+
// Shift is in the character (a Spanish `/` is Shift+7); with ⌘ some browsers report the key
|
|
93
|
+
// unshifted, so `⇧⌘,` is also a `,` with Shift held. Alt only when the chord asks for it (AltGr
|
|
94
|
+
// and ⌥ type characters).
|
|
95
|
+
const [plain, shifted] = punctuation;
|
|
96
|
+
const asShifted = char === shifted || (isCommandChord(chord) && char === plain && press.shiftKey);
|
|
97
|
+
const ok = chord.shift === 'held' ? asShifted : chord.shift === 'any' ? char === plain || asShifted : char === plain;
|
|
98
|
+
// A command (⇧⌘,) where this keyboard types another character there: the key's place, as menus do.
|
|
99
|
+
if (!ok)
|
|
100
|
+
return isCommandChord(chord) && placeMatch(chord, press.code, press) ? 'place' : null;
|
|
101
|
+
if (chord.alt === 'held' && !press.altKey)
|
|
102
|
+
return null;
|
|
103
|
+
// AltGr arrives as Ctrl+Alt on Windows: that Ctrl typed the character, it is no command.
|
|
104
|
+
const altGr = press.altKey && press.ctrlKey && !press.metaKey;
|
|
105
|
+
return commandFits(chord, { ctrlKey: press.ctrlKey && !altGr, metaKey: press.metaKey, shiftKey: press.shiftKey, altKey: press.altKey }) ? 'character' : null;
|
|
106
|
+
}
|
|
107
|
+
const letter = /^Key([A-Z])$/.exec(chord.code)?.[1];
|
|
108
|
+
if (letter) {
|
|
109
|
+
// A key that types no letter (`;` where Q is on a Greek keyboard) is read by its place.
|
|
110
|
+
if (!/^[a-zA-Z]$/.test(char))
|
|
111
|
+
return placeMatch(chord, press.code, press) ? 'place' : null;
|
|
112
|
+
if (char.toUpperCase() !== letter)
|
|
113
|
+
return null;
|
|
114
|
+
return fits(chord.shift, press.shiftKey) && fits(chord.alt, press.altKey) && commandFits(chord, press) ? 'character' : null;
|
|
115
|
+
}
|
|
116
|
+
const digit = /^Digit(\d)$/.exec(chord.code)?.[1];
|
|
117
|
+
// A digit typed; where the keyboard types something else there (`&` on a French one), its place.
|
|
118
|
+
if (digit && char === digit && fits(chord.shift, press.shiftKey) && fits(chord.alt, press.altKey) && commandFits(chord, press))
|
|
119
|
+
return 'character';
|
|
120
|
+
return placeMatch(chord, press.code, press) ? 'place' : null;
|
|
121
|
+
}
|
|
122
|
+
/** Whether a press of `code` with these modifiers (and, when given, the character it typed) is this chord. */
|
|
123
|
+
export function chordMatches(chord, code, state) {
|
|
124
|
+
// Field by field: a browser's KeyboardEvent keeps them on its prototype, where a spread does not reach.
|
|
125
|
+
const { shiftKey, altKey, ctrlKey, metaKey, key } = state;
|
|
126
|
+
return chordMatch(chord, { code, key, shiftKey, altKey, ctrlKey, metaKey }) !== null;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The key string a typed character names (`/` → `Slash`, `?` → `Shift+Slash`, `q` → `KeyQ`), for
|
|
130
|
+
* someone setting a key; `null` for a character a US keyboard does not have, or none.
|
|
131
|
+
*/
|
|
132
|
+
export function typedKey(key) {
|
|
133
|
+
const char = typed(key);
|
|
134
|
+
if (char === null)
|
|
135
|
+
return null;
|
|
136
|
+
if (/^[a-zA-Z]$/.test(char))
|
|
137
|
+
return { code: `Key${char.toUpperCase()}` };
|
|
138
|
+
const place = PUNCTUATION_PLACE.get(char);
|
|
139
|
+
return place ? { code: place[0], shift: place[1] } : null;
|
|
140
|
+
}
|
|
63
141
|
/** A chord that needs Ctrl or ⌘: a command, never a character typed. */
|
|
64
142
|
export function isCommandChord(chord) {
|
|
65
143
|
return chord.mod === 'held' || chord.ctrl === 'held' || chord.meta === 'held';
|
|
@@ -4,8 +4,8 @@ export type ShortcutPlatform = 'apple' | 'other';
|
|
|
4
4
|
export declare function shortcutPlatform(): ShortcutPlatform;
|
|
5
5
|
/**
|
|
6
6
|
* The character a key place prints on this person's keyboard, when the browser can say
|
|
7
|
-
* (`navigator.keyboard.getLayoutMap()`, Chromium only)
|
|
8
|
-
*
|
|
7
|
+
* (`navigator.keyboard.getLayoutMap()`, Chromium only). Only keys matched by their place use it:
|
|
8
|
+
* a letter, digit or punctuation key matches the character it names, so it shows that character.
|
|
9
9
|
*/
|
|
10
10
|
export type KeyLayout = ReadonlyMap<string, string>;
|
|
11
11
|
/** What a key place is called on a keyboard: `KeyQ` → `Q`, `Digit1` → `1`, `Numpad4` → `Num 4`. */
|
|
@@ -20,11 +20,14 @@ export declare function chordParts(keys: string, platform?: ShortcutPlatform, la
|
|
|
20
20
|
export declare function formatChord(keys: string, platform?: ShortcutPlatform, layout?: KeyLayout): string;
|
|
21
21
|
/**
|
|
22
22
|
* The key string a press makes, for someone setting a key: exactly the modifiers held, the command
|
|
23
|
-
* key of their platform written `Mod` (⌘ on Apple, Ctrl elsewhere) so the key works on both.
|
|
24
|
-
*
|
|
23
|
+
* key of their platform written `Mod` (⌘ on Apple, Ctrl elsewhere) so the key works on both. A letter
|
|
24
|
+
* or punctuation character typed is written as the key that types it on a US keyboard (`/` is
|
|
25
|
+
* `Slash` whichever keys typed it), the way the matcher reads it back. `null` while only modifiers
|
|
26
|
+
* are down, or for a press with no key place.
|
|
25
27
|
*/
|
|
26
28
|
export declare function chordFromEvent(event: {
|
|
27
29
|
code: string;
|
|
30
|
+
key?: string;
|
|
28
31
|
shiftKey: boolean;
|
|
29
32
|
altKey: boolean;
|
|
30
33
|
ctrlKey: boolean;
|
package/dist/shortcuts/format.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// A key string for people: how a chord is shown (`⌘⇧Z`, `Ctrl+Shift+Z`, `Q`), and the key string a
|
|
2
2
|
// press makes when someone sets a key ("press a key to change").
|
|
3
|
-
import { isModifierCode, parseChord } from './chord.js';
|
|
3
|
+
import { isModifierCode, parseChord, typedKey } from './chord.js';
|
|
4
4
|
/** The platform of this browser, by its own report; `other` outside a browser. */
|
|
5
5
|
export function shortcutPlatform() {
|
|
6
6
|
const nav = globalThis.navigator;
|
|
@@ -18,7 +18,8 @@ const NAMED = {
|
|
|
18
18
|
};
|
|
19
19
|
/** What a key place is called on a keyboard: `KeyQ` → `Q`, `Digit1` → `1`, `Numpad4` → `Num 4`. */
|
|
20
20
|
export function keyLabel(code, layout) {
|
|
21
|
-
const
|
|
21
|
+
const byCharacter = /^(Key[A-Z]|Digit\d)$/.test(code) || typedKey(NAMED[code])?.code === code;
|
|
22
|
+
const printed = byCharacter ? undefined : layout?.get(code);
|
|
22
23
|
if (printed && printed.trim())
|
|
23
24
|
return printed.length === 1 ? printed.toUpperCase() : printed;
|
|
24
25
|
if (/^Key[A-Z]$/.test(code))
|
|
@@ -29,6 +30,11 @@ export function keyLabel(code, layout) {
|
|
|
29
30
|
return `Num ${code.slice(6)}`;
|
|
30
31
|
return NAMED[code] ?? code;
|
|
31
32
|
}
|
|
33
|
+
/** What a US keyboard types with Shift at each punctuation key. */
|
|
34
|
+
const SHIFTED = {
|
|
35
|
+
Minus: '_', Equal: '+', BracketLeft: '{', BracketRight: '}', Backslash: '|', Semicolon: ':', Quote: '"',
|
|
36
|
+
Backquote: '~', Comma: '<', Period: '>', Slash: '?',
|
|
37
|
+
};
|
|
32
38
|
const SYMBOLS = { ctrl: '⌃', alt: '⌥', shift: '⇧', meta: '⌘' };
|
|
33
39
|
const WORDS = { ctrl: 'Ctrl', alt: 'Alt', shift: 'Shift', meta: 'Meta' };
|
|
34
40
|
/**
|
|
@@ -55,12 +61,16 @@ export function chordParts(keys, platform = shortcutPlatform(), layout) {
|
|
|
55
61
|
parts.push(names.ctrl);
|
|
56
62
|
if (held(chord.alt))
|
|
57
63
|
parts.push(platform === 'apple' ? names.alt : 'Alt');
|
|
58
|
-
|
|
64
|
+
// A punctuation key is the character it types, Shift included: `Shift+Slash` is `?`.
|
|
65
|
+
// A command (⇧⌘,) is shown as written: its Shift is a key to press, not part of a character.
|
|
66
|
+
const punctuation = !ctrl && !meta && typedKey(NAMED[chord.code])?.code === chord.code ? SHIFTED[chord.code] : undefined;
|
|
67
|
+
if (held(chord.shift) && !punctuation)
|
|
59
68
|
parts.push(names.shift);
|
|
60
69
|
if (meta)
|
|
61
70
|
parts.push(names.meta);
|
|
62
|
-
// The help key is `?` wherever it lets Shift be held (
|
|
63
|
-
|
|
71
|
+
// The help key is `?` wherever it lets Shift be held (`/` or `?`): show what people press.
|
|
72
|
+
const shifted = punctuation && (held(chord.shift) || (chord.code === 'Slash' && chord.shift === 'any'));
|
|
73
|
+
parts.push(shifted ? punctuation : keyLabel(chord.code, layout));
|
|
64
74
|
return parts;
|
|
65
75
|
}
|
|
66
76
|
/** One line for a chord: `⌘⇧Z` on Apple keyboards, `Ctrl+Shift+Z` elsewhere. */
|
|
@@ -74,28 +84,44 @@ export function formatChord(keys, platform = shortcutPlatform(), layout) {
|
|
|
74
84
|
}
|
|
75
85
|
/**
|
|
76
86
|
* The key string a press makes, for someone setting a key: exactly the modifiers held, the command
|
|
77
|
-
* key of their platform written `Mod` (⌘ on Apple, Ctrl elsewhere) so the key works on both.
|
|
78
|
-
*
|
|
87
|
+
* key of their platform written `Mod` (⌘ on Apple, Ctrl elsewhere) so the key works on both. A letter
|
|
88
|
+
* or punctuation character typed is written as the key that types it on a US keyboard (`/` is
|
|
89
|
+
* `Slash` whichever keys typed it), the way the matcher reads it back. `null` while only modifiers
|
|
90
|
+
* are down, or for a press with no key place.
|
|
79
91
|
*/
|
|
80
92
|
export function chordFromEvent(event, platform = shortcutPlatform()) {
|
|
81
93
|
if (!event.code || isModifierCode(event.code))
|
|
82
94
|
return null;
|
|
95
|
+
const typed = typedKey(event.key);
|
|
96
|
+
const punctuation = typed?.shift !== undefined;
|
|
97
|
+
// A punctuation character carries its own Shift, and the AltGr or ⌥ that typed it is no modifier.
|
|
98
|
+
const altGr = punctuation && event.altKey && event.ctrlKey && !event.metaKey;
|
|
99
|
+
// Field by field: a browser's KeyboardEvent keeps them on its prototype, where a spread does not reach.
|
|
100
|
+
const press = punctuation
|
|
101
|
+
? {
|
|
102
|
+
// With ⌘ or Ctrl some browsers report the key unshifted: the Shift held is then part of the chord.
|
|
103
|
+
shiftKey: Boolean(typed.shift) || (event.shiftKey && !altGr && (event.ctrlKey || event.metaKey)),
|
|
104
|
+
altKey: false,
|
|
105
|
+
ctrlKey: altGr ? false : event.ctrlKey,
|
|
106
|
+
metaKey: event.metaKey,
|
|
107
|
+
}
|
|
108
|
+
: event;
|
|
83
109
|
const parts = [];
|
|
84
110
|
// ⌘ (Ctrl elsewhere) is written Mod so the key works on both — unless Ctrl and ⌘ are both held,
|
|
85
111
|
// which Mod cannot say: then both by name.
|
|
86
|
-
if (
|
|
112
|
+
if (press.ctrlKey && press.metaKey)
|
|
87
113
|
parts.push('Ctrl', 'Meta');
|
|
88
|
-
else if (platform === 'apple' ?
|
|
114
|
+
else if (platform === 'apple' ? press.metaKey : press.ctrlKey)
|
|
89
115
|
parts.push('Mod');
|
|
90
|
-
else if (
|
|
116
|
+
else if (press.ctrlKey)
|
|
91
117
|
parts.push('Ctrl');
|
|
92
|
-
else if (
|
|
118
|
+
else if (press.metaKey)
|
|
93
119
|
parts.push('Meta');
|
|
94
|
-
if (
|
|
120
|
+
if (press.altKey)
|
|
95
121
|
parts.push('Alt');
|
|
96
|
-
if (
|
|
122
|
+
if (press.shiftKey)
|
|
97
123
|
parts.push('Shift');
|
|
98
|
-
parts.push(event.code);
|
|
124
|
+
parts.push(typed?.code ?? event.code);
|
|
99
125
|
const keys = parts.join('+');
|
|
100
126
|
return parseChord(keys) ? keys : null;
|
|
101
127
|
}
|
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
export { parseChord, parseKeys, keysOverlap, chordMatches, chordsOverlap, isCommandChord, type Chord, type ModifierRule, type ModifierState, } from './chord.js';
|
|
2
|
-
export { matchShortcut, createShortcutMatcher, SEQUENCE_WAIT_MS, resolveBindings, findClashes, claimKeys, isKeyClaimed, isTypingTarget, type ShortcutAction, type ShortcutBindings, type ShortcutOverrides, type ShortcutEvent, } from './match.js';
|
|
3
|
-
export { ORBITERS_PLAY_SHORTCUTS, ORBITERS_STUDIO_SHORTCUTS, ORBITERS_PLAY_SECTION, ORBITERS_STUDIO_SECTION, ORBITERS_KEYBOARD_SURFACE_KEYS, ORBITERS_COMMAND_GROUPS, ORBITERS_PLAY_SHEET, type OrbitersShortcut, type OrbitersShortcutScope, } from './orbiters.js';
|
|
1
|
+
export { parseChord, parseKeys, keysOverlap, chordMatch, chordMatches, chordsOverlap, typedKey, isCommandChord, type Chord, type ModifierRule, type ModifierState, type ChordMatch, type ChordPress, } from './chord.js';
|
|
2
|
+
export { matchShortcut, createShortcutMatcher, SEQUENCE_WAIT_MS, resolveBindings, findClashes, claimKeys, isKeyClaimed, isTypingTarget, type ShortcutAction, type ShortcutNames, type ShortcutBindings, type ShortcutOverrides, type ShortcutEvent, } from './match.js';
|
|
4
3
|
export { shortcutPlatform, keyLabel, chordParts, formatChord, chordFromEvent, mergedKeyParts, recordedKeys, type ShortcutPlatform, type KeyLayout, } from './format.js';
|
|
5
|
-
export {
|
|
6
|
-
export {
|
|
7
|
-
export { EW_STUDIO_SHORTCUTS, EW_PLAYER_SHORTCUTS, EW_STUDIO_SECTION, EW_PLAYER_SECTION, EW_STUDIO_COMMAND_GROUPS, type EntangledWorldsShortcut, } from './entangledWorlds.js';
|
|
8
|
-
export { shortcutLabel, shortcutSectionLabel, shortcutName } from './labels.js';
|
|
9
|
-
export { createShortcutOverridesSource, parseOverrides, type ShortcutOverridesSource, type ReadAccountSettings, } from './overrides.js';
|
|
4
|
+
export { EMPTY_CATALOGUE, parseCatalogue, localName, localAppName, localPartName, shortcutApps, sectionActions, findSection, type ShortcutCatalogue, type ShortcutSection, type ShortcutNamed, type ShortcutSheetColumn, type ShortcutSheetRow, } from './catalogue.js';
|
|
5
|
+
export { createShortcutSource, localShortcutCatalogueStore, CATALOGUE_STORAGE_KEY, parseOverrides, type ShortcutCatalogueStore, type ShortcutSource, type ReadAccountSettings, type ReadShortcutCatalogue, } from './source.js';
|
package/dist/shortcuts/index.js
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
// Keyboard shortcuts, one system for every app:
|
|
2
|
-
// default keys,
|
|
3
|
-
//
|
|
4
|
-
// a
|
|
5
|
-
|
|
1
|
+
// Keyboard shortcuts, one system for every app: the reader of the shortcut catalogue (every app's
|
|
2
|
+
// actions and default keys, served by the backend) and of a person's own keys, and one matcher that
|
|
3
|
+
// says which action a key press is. Keys are named as a US keyboard names them
|
|
4
|
+
// (`KeyboardEvent.code`) and matched by the character a press types, so they work on any keyboard.
|
|
5
|
+
// Framework-free: no React, no DOM beyond reading a key event.
|
|
6
|
+
export { parseChord, parseKeys, keysOverlap, chordMatch, chordMatches, chordsOverlap, typedKey, isCommandChord, } from './chord.js';
|
|
6
7
|
export { matchShortcut, createShortcutMatcher, SEQUENCE_WAIT_MS, resolveBindings, findClashes, claimKeys, isKeyClaimed, isTypingTarget, } from './match.js';
|
|
7
|
-
export { ORBITERS_PLAY_SHORTCUTS, ORBITERS_STUDIO_SHORTCUTS, ORBITERS_PLAY_SECTION, ORBITERS_STUDIO_SECTION, ORBITERS_KEYBOARD_SURFACE_KEYS, ORBITERS_COMMAND_GROUPS, ORBITERS_PLAY_SHEET, } from './orbiters.js';
|
|
8
8
|
export { shortcutPlatform, keyLabel, chordParts, formatChord, chordFromEvent, mergedKeyParts, recordedKeys, } from './format.js';
|
|
9
|
-
export {
|
|
10
|
-
export {
|
|
11
|
-
export { EW_STUDIO_SHORTCUTS, EW_PLAYER_SHORTCUTS, EW_STUDIO_SECTION, EW_PLAYER_SECTION, EW_STUDIO_COMMAND_GROUPS, } from './entangledWorlds.js';
|
|
12
|
-
export { shortcutLabel, shortcutSectionLabel, shortcutName } from './labels.js';
|
|
13
|
-
export { createShortcutOverridesSource, parseOverrides, } from './overrides.js';
|
|
9
|
+
export { EMPTY_CATALOGUE, parseCatalogue, localName, localAppName, localPartName, shortcutApps, sectionActions, findSection, } from './catalogue.js';
|
|
10
|
+
export { createShortcutSource, localShortcutCatalogueStore, CATALOGUE_STORAGE_KEY, parseOverrides, } from './source.js';
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { type ModifierState } from './chord.js';
|
|
2
|
-
/** One thing a key can do, as
|
|
2
|
+
/** One thing a key can do, as the shortcut catalogue (served by the backend) lists it. */
|
|
3
3
|
export interface ShortcutAction<Scope extends string = string> {
|
|
4
4
|
/** `<app>.<area>.<action>`, e.g. `orbiters.play.panel.jamming`: the key a person's override is stored under. */
|
|
5
5
|
readonly id: string;
|
|
6
|
-
/**
|
|
7
|
-
readonly section: string;
|
|
8
|
-
/** What it does, in sentence case. */
|
|
6
|
+
/** What it does, in sentence case, in English. */
|
|
9
7
|
readonly label: string;
|
|
8
|
+
/** Its name in other languages, by locale (`{ es: '…' }`); see `localName`. */
|
|
9
|
+
readonly names?: ShortcutNames;
|
|
10
10
|
/** The keys it has until the person changes them (chord strings, see `chord.ts`). */
|
|
11
11
|
readonly defaultKeys: readonly string[];
|
|
12
12
|
/** What it acts on, in the app's own terms (Orbiters: one deck, or this machine). */
|
|
@@ -15,12 +15,18 @@ export interface ShortcutAction<Scope extends string = string> {
|
|
|
15
15
|
readonly group?: string;
|
|
16
16
|
/** `false`: a key only, never a command to pick from a list (a key held down, the menu itself). */
|
|
17
17
|
readonly command?: boolean;
|
|
18
|
+
/** A site action: the page it opens (`{username}` stands for the signed-in person's). */
|
|
19
|
+
readonly href?: string;
|
|
20
|
+
/** It needs someone signed in. */
|
|
21
|
+
readonly signedIn?: boolean;
|
|
18
22
|
}
|
|
23
|
+
/** A name in other languages than English, by locale (`es`). */
|
|
24
|
+
export type ShortcutNames = Readonly<Record<string, string>>;
|
|
19
25
|
/** The keys of each action, in the order they are tried: the first action a press matches is the one. */
|
|
20
26
|
export type ShortcutBindings = Iterable<readonly [actionId: string, keys: readonly string[]]>;
|
|
21
27
|
/**
|
|
22
28
|
* A person's changes, as their settings store them: an action's own keys (`[]` = no key). An action
|
|
23
|
-
* left out keeps its defaults, so a default changed in
|
|
29
|
+
* left out keeps its defaults, so a default changed in the catalogue reaches everyone who never touched it.
|
|
24
30
|
*/
|
|
25
31
|
export type ShortcutOverrides = ReadonlyArray<{
|
|
26
32
|
readonly id: string;
|
|
@@ -52,7 +58,9 @@ export declare function isKeyClaimed(event: Pick<ShortcutEvent, 'key' | 'metaKey
|
|
|
52
58
|
export declare function isTypingTarget(target: unknown): boolean;
|
|
53
59
|
/**
|
|
54
60
|
* The action this press is, or `null`. Not one while the person is typing in a field, nor a key a
|
|
55
|
-
* surface holds; a widget that reads characters keeps its characters but not the ⌘ commands.
|
|
61
|
+
* surface holds; a widget that reads characters keeps its characters but not the ⌘ commands. A key
|
|
62
|
+
* that matches by the character typed wins over one that only matches by the key's place, whatever
|
|
63
|
+
* their order (Shift+7 types `/` on a Spanish keyboard: the `/` action, not the `7` one).
|
|
56
64
|
*/
|
|
57
65
|
export declare function matchShortcut(event: ShortcutEvent, bindings: ShortcutBindings): string | null;
|
|
58
66
|
/** How long the second key of a sequence (`G`, then `H`) may follow the first. */
|
package/dist/shortcuts/match.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Which action a key press is. Each app keeps what its actions DO; this only answers which one a
|
|
2
2
|
// press names, so every app reads keys the same way: the modifiers, a field being typed in, and a
|
|
3
3
|
// surface that holds letters of its own are decided here, once.
|
|
4
|
-
import { chordMatches, isCommandChord, isModifierCode, keysOverlap, parseChord, parseKeys } from './chord.js';
|
|
4
|
+
import { chordMatch, chordMatches, isCommandChord, isModifierCode, keysOverlap, parseChord, parseKeys } from './chord.js';
|
|
5
5
|
/**
|
|
6
6
|
* Every action's keys: its defaults, or the person's override (unknown actions and keys that name no
|
|
7
7
|
* chord are dropped). Catalogue order is kept, so the earlier catalogue wins where two match one press.
|
|
@@ -81,23 +81,29 @@ function readsCharacters(target) {
|
|
|
81
81
|
}
|
|
82
82
|
/**
|
|
83
83
|
* The action this press is, or `null`. Not one while the person is typing in a field, nor a key a
|
|
84
|
-
* surface holds; a widget that reads characters keeps its characters but not the ⌘ commands.
|
|
84
|
+
* surface holds; a widget that reads characters keeps its characters but not the ⌘ commands. A key
|
|
85
|
+
* that matches by the character typed wins over one that only matches by the key's place, whatever
|
|
86
|
+
* their order (Shift+7 types `/` on a Spanish keyboard: the `/` action, not the `7` one).
|
|
85
87
|
*/
|
|
86
88
|
export function matchShortcut(event, bindings) {
|
|
87
89
|
if (!event.code || isTypingTarget(event.target) || isKeyClaimed(event))
|
|
88
90
|
return null;
|
|
89
91
|
const characters = readsCharacters(event.target);
|
|
92
|
+
let byPlace = null;
|
|
90
93
|
for (const [actionId, keys] of bindings) {
|
|
91
94
|
for (const key of keys) {
|
|
92
95
|
const chord = parseChord(key);
|
|
93
|
-
|
|
96
|
+
const how = chord && chordMatch(chord, event);
|
|
97
|
+
if (!chord || !how)
|
|
94
98
|
continue;
|
|
95
|
-
if (
|
|
96
|
-
return null;
|
|
97
|
-
|
|
99
|
+
if (how === 'character')
|
|
100
|
+
return characters && !isCommandChord(chord) ? null : actionId;
|
|
101
|
+
byPlace ??= { id: actionId, command: isCommandChord(chord) };
|
|
98
102
|
}
|
|
99
103
|
}
|
|
100
|
-
|
|
104
|
+
if (!byPlace || (characters && !byPlace.command))
|
|
105
|
+
return null;
|
|
106
|
+
return byPlace.id;
|
|
101
107
|
}
|
|
102
108
|
// ---------------------------------------------------------------------------------------------
|
|
103
109
|
// Sequences
|
|
@@ -137,7 +143,7 @@ export function createShortcutMatcher(waitMs = SEQUENCE_WAIT_MS, now = Date.now)
|
|
|
137
143
|
return id;
|
|
138
144
|
}
|
|
139
145
|
if (known.some(([, [one]]) => chordMatches(one, event.code, event))) {
|
|
140
|
-
first = { code: event.code, shiftKey: event.shiftKey, altKey: event.altKey, ctrlKey: event.ctrlKey, metaKey: event.metaKey, at: now() };
|
|
146
|
+
first = { code: event.code, key: event.key, shiftKey: event.shiftKey, altKey: event.altKey, ctrlKey: event.ctrlKey, metaKey: event.metaKey, at: now() };
|
|
141
147
|
return null;
|
|
142
148
|
}
|
|
143
149
|
return matchShortcut(event, bindings);
|