@sametalmdr/word-editor 0.2.0-alpha → 0.3.0-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +108 -70
- package/LICENSE +21 -21
- package/README.md +286 -247
- package/dist/canvas/box-clipboard.js +238 -238
- package/dist/canvas/box-selection.js +80 -80
- package/dist/canvas/canvas-editor.js +502 -502
- package/dist/canvas/controller.js +717 -717
- package/dist/canvas/geometry.js +131 -131
- package/dist/canvas/gesture.d.ts +0 -1
- package/dist/canvas/gesture.js +289 -289
- package/dist/canvas/handles.js +212 -212
- package/dist/canvas/menu.js +82 -82
- package/dist/canvas/mode-controller.js +81 -81
- package/dist/canvas/nudge.js +108 -108
- package/dist/canvas/overlay.js +149 -149
- package/dist/canvas/snap.js +132 -132
- package/dist/canvas/viewport.js +112 -112
- package/dist/canvas/z-order.js +171 -171
- package/dist/commands/alignment.js +150 -150
- package/dist/commands/box.js +272 -272
- package/dist/commands/clipboard.js +93 -93
- package/dist/commands/graphemes.js +112 -112
- package/dist/commands/marks.js +153 -153
- package/dist/commands/paragraph-props.js +176 -176
- package/dist/commands/size.js +368 -368
- package/dist/commands/text.js +291 -291
- package/dist/docx.d.ts +1 -0
- package/dist/docx.js +15 -13
- package/dist/editor/editing-host.js +434 -434
- package/dist/element.js +41 -41
- package/dist/fonts/OFL-Carlito.txt +94 -0
- package/dist/fonts/calibri-metrics.d.ts +27 -0
- package/dist/fonts/calibri-metrics.js +88 -0
- package/dist/history/history.js +433 -433
- package/dist/index.js +63 -63
- package/dist/input/beforeinput.js +224 -224
- package/dist/input/clipboard.js +107 -107
- package/dist/input/composition.js +341 -341
- package/dist/input/keymap.js +54 -54
- package/dist/io/ooxml/custgeom.js +72 -72
- package/dist/io/ooxml/drawingml.js +232 -232
- package/dist/io/ooxml/edits.js +67 -67
- package/dist/io/ooxml/export-splice.js +880 -880
- package/dist/io/ooxml/export.js +365 -365
- package/dist/io/ooxml/fill.d.ts +93 -8
- package/dist/io/ooxml/fill.js +618 -562
- package/dist/io/ooxml/fit.d.ts +252 -0
- package/dist/io/ooxml/fit.js +598 -0
- package/dist/io/ooxml/import-gate.js +200 -200
- package/dist/io/ooxml/import.js +691 -691
- package/dist/io/ooxml/inflate.js +126 -126
- package/dist/io/ooxml/media.js +87 -87
- package/dist/io/ooxml/ns.d.ts +2 -0
- package/dist/io/ooxml/ns.js +26 -21
- package/dist/io/ooxml/numbering.js +290 -290
- package/dist/io/ooxml/opc-write.js +171 -171
- package/dist/io/ooxml/provenance.js +93 -93
- package/dist/io/ooxml/recipe.js +708 -708
- package/dist/io/ooxml/relationships.js +59 -59
- package/dist/io/ooxml/run-fonts.d.ts +89 -0
- package/dist/io/ooxml/run-fonts.js +378 -0
- package/dist/io/ooxml/slots.d.ts +4 -0
- package/dist/io/ooxml/slots.js +582 -581
- package/dist/io/ooxml/styles-resolve.js +257 -257
- package/dist/io/ooxml/units.js +43 -43
- package/dist/io/ooxml/value.js +120 -120
- package/dist/io/ooxml/xml-scan.js +616 -616
- package/dist/io/ooxml/zip-container.js +228 -228
- package/dist/io/ooxml/zip-reject.js +106 -106
- package/dist/measure/placement.js +83 -83
- package/dist/measure/report.js +75 -75
- package/dist/model/ids.js +70 -70
- package/dist/model/normalize.js +146 -146
- package/dist/model/position.js +283 -283
- package/dist/model/transaction.js +209 -209
- package/dist/platform/capabilities.js +25 -25
- package/dist/react/index.js +21 -21
- package/dist/selection/controller.js +315 -315
- package/dist/selection/dom-map.js +334 -334
- package/dist/state/editor-state.js +109 -109
- package/dist/styles.css +66 -66
- package/package.json +94 -93
|
@@ -1,81 +1,81 @@
|
|
|
1
|
-
// The box-mode <-> text-mode state machine for the unified canvas editor
|
|
2
|
-
// (ADR-0024, P3b mode-merge parça 1). It owns ONLY the mode state and its two
|
|
3
|
-
// transitions. Deliberately inert otherwise: it listens to NO DOM events (parça
|
|
4
|
-
// 3's click-router calls enterTextMode/exitToBoxMode), and touches NO model, NO
|
|
5
|
-
// renderer, NO editing. SelectionController and the future click-router only
|
|
6
|
-
// READ `mode`; nothing writes it except the two methods here.
|
|
7
|
-
//
|
|
8
|
-
// - box mode (initial): a single click selects/moves a box (CanvasController).
|
|
9
|
-
// - text mode: a caret lives in one box; typing edits it (editing assembly).
|
|
10
|
-
//
|
|
11
|
-
// Double-click a box enters text mode on it; Esc — or a single click on another
|
|
12
|
-
// box / outside — returns to box mode (ADR-0024 §1). Those DOM triggers are
|
|
13
|
-
// parça 3's job; here they are just the two methods those handlers will call.
|
|
14
|
-
|
|
15
|
-
/** @typedef {"box" | "text"} Mode */
|
|
16
|
-
|
|
17
|
-
export class ModeController {
|
|
18
|
-
/**
|
|
19
|
-
* @param {object} [options]
|
|
20
|
-
* @param {(mode: Mode, activeContainerId: import("../model/node.js").NodeId | null) => void} [options.onChange]
|
|
21
|
-
* Fired after every REAL transition (never on a no-op). Omitted -> no observer.
|
|
22
|
-
*/
|
|
23
|
-
constructor({ onChange } = {}) {
|
|
24
|
-
/** @type {Mode} */
|
|
25
|
-
this._mode = "box";
|
|
26
|
-
/** @type {import("../model/node.js").NodeId | null} */
|
|
27
|
-
this._activeContainerId = null;
|
|
28
|
-
this._onChange = onChange ?? null;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** @returns {Mode} */
|
|
32
|
-
get mode() {
|
|
33
|
-
return this._mode;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* The box currently in text mode, or null in box mode.
|
|
38
|
-
* @returns {import("../model/node.js").NodeId | null}
|
|
39
|
-
*/
|
|
40
|
-
get activeContainerId() {
|
|
41
|
-
return this._activeContainerId;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* box -> text on `containerId` (a double-click on a box, ADR-0024). Also
|
|
46
|
-
* switches the active box when already in text mode on a DIFFERENT one
|
|
47
|
-
* (double-click box B while editing box A). A no-op — no `onChange` — when
|
|
48
|
-
* already in text mode on the SAME box.
|
|
49
|
-
* @param {import("../model/node.js").NodeId} containerId
|
|
50
|
-
* @returns {void}
|
|
51
|
-
*/
|
|
52
|
-
enterTextMode(containerId) {
|
|
53
|
-
if (this._mode === "text" && this._activeContainerId === containerId) {
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
this._mode = "text";
|
|
57
|
-
this._activeContainerId = containerId;
|
|
58
|
-
this._emit();
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* text -> box (Esc, or a single click on another box / outside — ADR-0024).
|
|
63
|
-
* A no-op — no `onChange` — when already in box mode.
|
|
64
|
-
* @returns {void}
|
|
65
|
-
*/
|
|
66
|
-
exitToBoxMode() {
|
|
67
|
-
if (this._mode === "box") {
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
this._mode = "box";
|
|
71
|
-
this._activeContainerId = null;
|
|
72
|
-
this._emit();
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** @returns {void} */
|
|
76
|
-
_emit() {
|
|
77
|
-
if (this._onChange !== null) {
|
|
78
|
-
this._onChange(this._mode, this._activeContainerId);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
1
|
+
// The box-mode <-> text-mode state machine for the unified canvas editor
|
|
2
|
+
// (ADR-0024, P3b mode-merge parça 1). It owns ONLY the mode state and its two
|
|
3
|
+
// transitions. Deliberately inert otherwise: it listens to NO DOM events (parça
|
|
4
|
+
// 3's click-router calls enterTextMode/exitToBoxMode), and touches NO model, NO
|
|
5
|
+
// renderer, NO editing. SelectionController and the future click-router only
|
|
6
|
+
// READ `mode`; nothing writes it except the two methods here.
|
|
7
|
+
//
|
|
8
|
+
// - box mode (initial): a single click selects/moves a box (CanvasController).
|
|
9
|
+
// - text mode: a caret lives in one box; typing edits it (editing assembly).
|
|
10
|
+
//
|
|
11
|
+
// Double-click a box enters text mode on it; Esc — or a single click on another
|
|
12
|
+
// box / outside — returns to box mode (ADR-0024 §1). Those DOM triggers are
|
|
13
|
+
// parça 3's job; here they are just the two methods those handlers will call.
|
|
14
|
+
|
|
15
|
+
/** @typedef {"box" | "text"} Mode */
|
|
16
|
+
|
|
17
|
+
export class ModeController {
|
|
18
|
+
/**
|
|
19
|
+
* @param {object} [options]
|
|
20
|
+
* @param {(mode: Mode, activeContainerId: import("../model/node.js").NodeId | null) => void} [options.onChange]
|
|
21
|
+
* Fired after every REAL transition (never on a no-op). Omitted -> no observer.
|
|
22
|
+
*/
|
|
23
|
+
constructor({ onChange } = {}) {
|
|
24
|
+
/** @type {Mode} */
|
|
25
|
+
this._mode = "box";
|
|
26
|
+
/** @type {import("../model/node.js").NodeId | null} */
|
|
27
|
+
this._activeContainerId = null;
|
|
28
|
+
this._onChange = onChange ?? null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** @returns {Mode} */
|
|
32
|
+
get mode() {
|
|
33
|
+
return this._mode;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The box currently in text mode, or null in box mode.
|
|
38
|
+
* @returns {import("../model/node.js").NodeId | null}
|
|
39
|
+
*/
|
|
40
|
+
get activeContainerId() {
|
|
41
|
+
return this._activeContainerId;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* box -> text on `containerId` (a double-click on a box, ADR-0024). Also
|
|
46
|
+
* switches the active box when already in text mode on a DIFFERENT one
|
|
47
|
+
* (double-click box B while editing box A). A no-op — no `onChange` — when
|
|
48
|
+
* already in text mode on the SAME box.
|
|
49
|
+
* @param {import("../model/node.js").NodeId} containerId
|
|
50
|
+
* @returns {void}
|
|
51
|
+
*/
|
|
52
|
+
enterTextMode(containerId) {
|
|
53
|
+
if (this._mode === "text" && this._activeContainerId === containerId) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
this._mode = "text";
|
|
57
|
+
this._activeContainerId = containerId;
|
|
58
|
+
this._emit();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* text -> box (Esc, or a single click on another box / outside — ADR-0024).
|
|
63
|
+
* A no-op — no `onChange` — when already in box mode.
|
|
64
|
+
* @returns {void}
|
|
65
|
+
*/
|
|
66
|
+
exitToBoxMode() {
|
|
67
|
+
if (this._mode === "box") {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
this._mode = "box";
|
|
71
|
+
this._activeContainerId = null;
|
|
72
|
+
this._emit();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** @returns {void} */
|
|
76
|
+
_emit() {
|
|
77
|
+
if (this._onChange !== null) {
|
|
78
|
+
this._onChange(this._mode, this._activeContainerId);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
package/dist/canvas/nudge.js
CHANGED
|
@@ -1,108 +1,108 @@
|
|
|
1
|
-
// The keyboard nudge bracket (ADR-0016 §4, measured in two engines). No
|
|
2
|
-
// src/, DOM, or history import.
|
|
3
|
-
//
|
|
4
|
-
// ⚠ SENTETİK DİZİLER KANIT DEĞİL — same disclaimer as gesture.js: this
|
|
5
|
-
// file's tests drive hand-written keydown/keyup sequences. That proves the
|
|
6
|
-
// state machine is correct GIVEN that sequence, not that a real keyboard
|
|
7
|
-
// produces it. The real evidence is ADR-0016's G9/G10 measurements.
|
|
8
|
-
//
|
|
9
|
-
// MEASURED RULES this module encodes:
|
|
10
|
-
// - PRIMARY signal is the `repeat: true` flag, NOT a time window. Measured
|
|
11
|
-
// reason: first-repeat delay is 501.1-562.5ms (Chrome) / 500-593ms
|
|
12
|
-
// (Firefox) — every single measured first repeat lands 1-64ms ABOVE
|
|
13
|
-
// `COALESCE_MS` (500), so a time-window bracket sits right on the
|
|
14
|
-
// boundary and would jitter between one undo entry and two.
|
|
15
|
-
// - `keyup` is NOT reliable: an orphaned keyup (no matching keydown,
|
|
16
|
-
// key: "Tab") was observed twice in Chrome, seven times in Firefox.
|
|
17
|
-
// - G10 (Firefox): a key's own `keyup` can arrive AFTER the NEXT key's
|
|
18
|
-
// `keydown` (observed twice) — `keyUpSealsOpenBracket` must still
|
|
19
|
-
// recognize it, checking only which KEY it belongs to, never assuming
|
|
20
|
-
// arrival order.
|
|
21
|
-
// - A `repeat: false` keydown after a focus loss always opens a NEW
|
|
22
|
-
// bracket, even on the same key — the conservative choice (splitting
|
|
23
|
-
// one gesture into two costs less than wrongly merging two).
|
|
24
|
-
// - The nudge window is `idleAbandonMs`-shaped but must NEVER reuse
|
|
25
|
-
// `COALESCE_MS` — see the first bullet. Reuses `isIdleAbandoned`
|
|
26
|
-
// (gesture.js) rather than redefining its own copy: both brackets ask
|
|
27
|
-
// "how long since the last event", the SAME question `Gesture` already
|
|
28
|
-
// answers, just with a different injected threshold.
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* @typedef {object} NudgeBracket
|
|
32
|
-
* @property {number} gestureId minted by the caller's IdFactory
|
|
33
|
-
* @property {string} key e.g. "ArrowUp" — the physical key this bracket belongs to
|
|
34
|
-
* @property {number} lastEventAt
|
|
35
|
-
*/
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* @typedef {object} NudgeKeyEvent
|
|
39
|
-
* @property {"keydown" | "keyup"} type
|
|
40
|
-
* @property {string} key
|
|
41
|
-
* @property {boolean} [repeat] keydown only
|
|
42
|
-
* @property {number} timestamp
|
|
43
|
-
* @property {number} [gestureId] keydown only: pre-minted id for the bracket this event WOULD open
|
|
44
|
-
*/
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* @typedef {object} NudgePlan
|
|
48
|
-
* @property {NudgeBracket} bracket the bracket to use for THIS keydown
|
|
49
|
-
* @property {boolean} extend true = the SAME gesture as an already-open bracket; false = a NEW one
|
|
50
|
-
*/
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* PRIMARY signal only: `event.repeat` on the SAME key extends the existing
|
|
54
|
-
* bracket. Anything else — a different key, `repeat: false` even on the
|
|
55
|
-
* same key (the post-focus-loss reset case), or no bracket open at all —
|
|
56
|
-
* opens a NEW one. Never mints its own id; `event.gestureId` is the
|
|
57
|
-
* caller's pre-minted candidate, used only when actually needed.
|
|
58
|
-
*
|
|
59
|
-
* @param {NudgeBracket | null} bracket
|
|
60
|
-
* @param {NudgeKeyEvent} event a "keydown"
|
|
61
|
-
* @returns {NudgePlan}
|
|
62
|
-
*/
|
|
63
|
-
export function nudgeTransition(bracket, event) {
|
|
64
|
-
if (bracket !== null && bracket.key === event.key && event.repeat) {
|
|
65
|
-
return { bracket: { ...bracket, lastEventAt: event.timestamp }, extend: true };
|
|
66
|
-
}
|
|
67
|
-
return {
|
|
68
|
-
bracket: { gestureId: /** @type {number} */ (event.gestureId), key: event.key, lastEventAt: event.timestamp },
|
|
69
|
-
extend: false,
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Whether `event` (a "keyup") finalizes `bracket` — true purely by KEY
|
|
75
|
-
* match, never by arrival order (G10: a key's own keyup can arrive after
|
|
76
|
-
* the NEXT key's keydown has already opened a new bracket). Sealing is
|
|
77
|
-
* IDEMPOTENT: calling this again for an already-finalized bracket, or for
|
|
78
|
-
* an unmatched/orphaned keyup (G9: a keyup with no matching keydown was
|
|
79
|
-
* observed for "Tab"), is harmless — it simply returns `false` because no
|
|
80
|
-
* candidate bracket has that key.
|
|
81
|
-
*
|
|
82
|
-
* @param {NudgeBracket | null} bracket
|
|
83
|
-
* @param {NudgeKeyEvent} event a "keyup"
|
|
84
|
-
* @returns {boolean}
|
|
85
|
-
*/
|
|
86
|
-
export function keyUpSealsOpenBracket(bracket, event) {
|
|
87
|
-
return bracket !== null && bracket.key === event.key;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const KEY_DIRECTIONS = {
|
|
91
|
-
ArrowUp: { dx: 0, dy: -1 },
|
|
92
|
-
ArrowDown: { dx: 0, dy: 1 },
|
|
93
|
-
ArrowLeft: { dx: -1, dy: 0 },
|
|
94
|
-
ArrowRight: { dx: 1, dy: 0 },
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* @param {string} key one of ArrowUp/ArrowDown/ArrowLeft/ArrowRight
|
|
99
|
-
* @param {number} step pt — injected, no default (an unmeasured UI constant, CanvasMetrics' `nudgeStep`)
|
|
100
|
-
* @returns {{dx: number, dy: number}}
|
|
101
|
-
*/
|
|
102
|
-
export function nudgeDelta(key, step) {
|
|
103
|
-
const direction = KEY_DIRECTIONS[/** @type {keyof typeof KEY_DIRECTIONS} */ (key)];
|
|
104
|
-
if (direction === undefined) {
|
|
105
|
-
throw new TypeError(`nudgeDelta: unknown key ${JSON.stringify(key)}`);
|
|
106
|
-
}
|
|
107
|
-
return { dx: direction.dx * step, dy: direction.dy * step };
|
|
108
|
-
}
|
|
1
|
+
// The keyboard nudge bracket (ADR-0016 §4, measured in two engines). No
|
|
2
|
+
// src/, DOM, or history import.
|
|
3
|
+
//
|
|
4
|
+
// ⚠ SENTETİK DİZİLER KANIT DEĞİL — same disclaimer as gesture.js: this
|
|
5
|
+
// file's tests drive hand-written keydown/keyup sequences. That proves the
|
|
6
|
+
// state machine is correct GIVEN that sequence, not that a real keyboard
|
|
7
|
+
// produces it. The real evidence is ADR-0016's G9/G10 measurements.
|
|
8
|
+
//
|
|
9
|
+
// MEASURED RULES this module encodes:
|
|
10
|
+
// - PRIMARY signal is the `repeat: true` flag, NOT a time window. Measured
|
|
11
|
+
// reason: first-repeat delay is 501.1-562.5ms (Chrome) / 500-593ms
|
|
12
|
+
// (Firefox) — every single measured first repeat lands 1-64ms ABOVE
|
|
13
|
+
// `COALESCE_MS` (500), so a time-window bracket sits right on the
|
|
14
|
+
// boundary and would jitter between one undo entry and two.
|
|
15
|
+
// - `keyup` is NOT reliable: an orphaned keyup (no matching keydown,
|
|
16
|
+
// key: "Tab") was observed twice in Chrome, seven times in Firefox.
|
|
17
|
+
// - G10 (Firefox): a key's own `keyup` can arrive AFTER the NEXT key's
|
|
18
|
+
// `keydown` (observed twice) — `keyUpSealsOpenBracket` must still
|
|
19
|
+
// recognize it, checking only which KEY it belongs to, never assuming
|
|
20
|
+
// arrival order.
|
|
21
|
+
// - A `repeat: false` keydown after a focus loss always opens a NEW
|
|
22
|
+
// bracket, even on the same key — the conservative choice (splitting
|
|
23
|
+
// one gesture into two costs less than wrongly merging two).
|
|
24
|
+
// - The nudge window is `idleAbandonMs`-shaped but must NEVER reuse
|
|
25
|
+
// `COALESCE_MS` — see the first bullet. Reuses `isIdleAbandoned`
|
|
26
|
+
// (gesture.js) rather than redefining its own copy: both brackets ask
|
|
27
|
+
// "how long since the last event", the SAME question `Gesture` already
|
|
28
|
+
// answers, just with a different injected threshold.
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @typedef {object} NudgeBracket
|
|
32
|
+
* @property {number} gestureId minted by the caller's IdFactory
|
|
33
|
+
* @property {string} key e.g. "ArrowUp" — the physical key this bracket belongs to
|
|
34
|
+
* @property {number} lastEventAt
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @typedef {object} NudgeKeyEvent
|
|
39
|
+
* @property {"keydown" | "keyup"} type
|
|
40
|
+
* @property {string} key
|
|
41
|
+
* @property {boolean} [repeat] keydown only
|
|
42
|
+
* @property {number} timestamp
|
|
43
|
+
* @property {number} [gestureId] keydown only: pre-minted id for the bracket this event WOULD open
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @typedef {object} NudgePlan
|
|
48
|
+
* @property {NudgeBracket} bracket the bracket to use for THIS keydown
|
|
49
|
+
* @property {boolean} extend true = the SAME gesture as an already-open bracket; false = a NEW one
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* PRIMARY signal only: `event.repeat` on the SAME key extends the existing
|
|
54
|
+
* bracket. Anything else — a different key, `repeat: false` even on the
|
|
55
|
+
* same key (the post-focus-loss reset case), or no bracket open at all —
|
|
56
|
+
* opens a NEW one. Never mints its own id; `event.gestureId` is the
|
|
57
|
+
* caller's pre-minted candidate, used only when actually needed.
|
|
58
|
+
*
|
|
59
|
+
* @param {NudgeBracket | null} bracket
|
|
60
|
+
* @param {NudgeKeyEvent} event a "keydown"
|
|
61
|
+
* @returns {NudgePlan}
|
|
62
|
+
*/
|
|
63
|
+
export function nudgeTransition(bracket, event) {
|
|
64
|
+
if (bracket !== null && bracket.key === event.key && event.repeat) {
|
|
65
|
+
return { bracket: { ...bracket, lastEventAt: event.timestamp }, extend: true };
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
bracket: { gestureId: /** @type {number} */ (event.gestureId), key: event.key, lastEventAt: event.timestamp },
|
|
69
|
+
extend: false,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Whether `event` (a "keyup") finalizes `bracket` — true purely by KEY
|
|
75
|
+
* match, never by arrival order (G10: a key's own keyup can arrive after
|
|
76
|
+
* the NEXT key's keydown has already opened a new bracket). Sealing is
|
|
77
|
+
* IDEMPOTENT: calling this again for an already-finalized bracket, or for
|
|
78
|
+
* an unmatched/orphaned keyup (G9: a keyup with no matching keydown was
|
|
79
|
+
* observed for "Tab"), is harmless — it simply returns `false` because no
|
|
80
|
+
* candidate bracket has that key.
|
|
81
|
+
*
|
|
82
|
+
* @param {NudgeBracket | null} bracket
|
|
83
|
+
* @param {NudgeKeyEvent} event a "keyup"
|
|
84
|
+
* @returns {boolean}
|
|
85
|
+
*/
|
|
86
|
+
export function keyUpSealsOpenBracket(bracket, event) {
|
|
87
|
+
return bracket !== null && bracket.key === event.key;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const KEY_DIRECTIONS = {
|
|
91
|
+
ArrowUp: { dx: 0, dy: -1 },
|
|
92
|
+
ArrowDown: { dx: 0, dy: 1 },
|
|
93
|
+
ArrowLeft: { dx: -1, dy: 0 },
|
|
94
|
+
ArrowRight: { dx: 1, dy: 0 },
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @param {string} key one of ArrowUp/ArrowDown/ArrowLeft/ArrowRight
|
|
99
|
+
* @param {number} step pt — injected, no default (an unmeasured UI constant, CanvasMetrics' `nudgeStep`)
|
|
100
|
+
* @returns {{dx: number, dy: number}}
|
|
101
|
+
*/
|
|
102
|
+
export function nudgeDelta(key, step) {
|
|
103
|
+
const direction = KEY_DIRECTIONS[/** @type {keyof typeof KEY_DIRECTIONS} */ (key)];
|
|
104
|
+
if (direction === undefined) {
|
|
105
|
+
throw new TypeError(`nudgeDelta: unknown key ${JSON.stringify(key)}`);
|
|
106
|
+
}
|
|
107
|
+
return { dx: direction.dx * step, dy: direction.dy * step };
|
|
108
|
+
}
|