@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
package/dist/canvas/gesture.js
CHANGED
|
@@ -1,289 +1,289 @@
|
|
|
1
|
-
// The pointer-gesture bracket: idle -> open -> sealed(reason) (ADR-0016 §1,
|
|
2
|
-
// measured in two engines). No src/, DOM, or history import — pure data in,
|
|
3
|
-
// data out, same discipline as every other file in this directory.
|
|
4
|
-
//
|
|
5
|
-
// ⚠ SENTETİK DİZİLER KANIT DEĞİL (test/input/composition.test.js's own
|
|
6
|
-
// header, restated here for the same reason): this file's own tests drive
|
|
7
|
-
// hand-written event sequences through these functions. That proves the
|
|
8
|
-
// state machine behaves correctly GIVEN that sequence. It does NOT prove a
|
|
9
|
-
// real browser produces that sequence — the real evidence is
|
|
10
|
-
// docs/adr/0016-gesture-bracket-and-accumulative-invert.md's G1-G10
|
|
11
|
-
// measurements, and ADR-0018 §10.2 is explicit that multi-selection
|
|
12
|
-
// dragging (Ö5) never reached the probe at all.
|
|
13
|
-
//
|
|
14
|
-
// MEASURED RULES this module encodes (ADR-0016, two engines — Chrome +
|
|
15
|
-
// Firefox):
|
|
16
|
-
// - `pointerup` closes the bracket, but MAY NOT ARRIVE (Chrome 6/17,
|
|
17
|
-
// Firefox 1/4 — right-click mid-drag). The one reliable backup is a
|
|
18
|
-
// NEW `pointerdown` on the SAME `pointerId` (G2) — pointerId's own
|
|
19
|
-
// VALUE is engine-dependent (Chrome always 1, Firefox always 0), so
|
|
20
|
-
// this file only ever compares two observed values for EQUALITY, never
|
|
21
|
-
// against a hardcoded constant.
|
|
22
|
-
// - `lostpointercapture` is NOT a seal signal (G3): 5/6 orphaned gestures
|
|
23
|
-
// never got it at all, and the one that did got it mid-gesture without
|
|
24
|
-
// closing.
|
|
25
|
-
// - `focusout`/`blur` are NOT seal signals (G4/G6): 20/20 focusout events
|
|
26
|
-
// fired with `hasFocus: true` still true, and 3/4 visibilitychange
|
|
27
|
-
// events during an open gesture still closed normally via pointerup.
|
|
28
|
-
// - Capture loss is not gesture end, only a NOTE (G5) — Blink-specific
|
|
29
|
-
// (14/14 Firefox gestures never showed it at all).
|
|
30
|
-
// - Sealing is IDEMPOTENT / order-tolerant: G10 (Firefox) found a key's
|
|
31
|
-
// own delayed signal can arrive out of the order a naive implementation
|
|
32
|
-
// would assume — nudge.js's `keyUpSealsOpenBracket` carries the same
|
|
33
|
-
// discipline for the keyboard bracket.
|
|
34
|
-
|
|
35
|
-
import { movePatch, resizePatch } from "./resize.js";
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* @typedef {"closed" | "abandoned:superseded" | "abandoned:timeout"} SealReason
|
|
39
|
-
*
|
|
40
|
-
* Terk (abandon) kapanıştan (closed) AYIRT EDİLEBİLİR tutulur (ADR-0016 §1):
|
|
41
|
-
* a mutation that let an abandoned gesture read back as "closed" would make
|
|
42
|
-
* "the bracket abandons cleanly" impossible to assert — it would only ever
|
|
43
|
-
* be TOLERATED, never verified.
|
|
44
|
-
*/
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* @typedef {object} Gesture
|
|
48
|
-
* @property {number} gestureId minted by the caller's IdFactory, monotonic, never reused after sealing
|
|
49
|
-
* @property {number} pointerId compared by EQUALITY only — never a hardcoded engine constant
|
|
50
|
-
* @property {number} lastEventAt timestamp of the most recent event this gesture's clock cares about
|
|
51
|
-
* @property {boolean} paused true while the page is hidden/unfocused — `isIdleAbandoned` does not advance the clock
|
|
52
|
-
* @property {boolean} captureLostDuringGesture note only, never a seal (path 4)
|
|
53
|
-
* @property {boolean} pageHiddenDuringGesture note only, never a seal (path 5)
|
|
54
|
-
*/
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* @typedef {object} GestureEvent
|
|
58
|
-
* @property {"pointerdown" | "pointerup" | "pointercancel" | "pointermove" | "lostpointercapture" | "visibilitychange" | "blur" | "focus"} type
|
|
59
|
-
* @property {number} [pointerId] absent for visibilitychange/blur/focus
|
|
60
|
-
* @property {number} timestamp
|
|
61
|
-
* @property {boolean} [hidden] visibilitychange only: `document.hidden`
|
|
62
|
-
* @property {number} [gestureId] pointerdown only: pre-minted id for the gesture this event WOULD open
|
|
63
|
-
*/
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* @typedef {object} GesturePlan
|
|
67
|
-
* @property {Gesture | null} gesture the gesture state AFTER this event; `null` once idle
|
|
68
|
-
* @property {SealReason | null} sealReason what happened to the PREVIOUS (pre-event) gesture this call, if anything
|
|
69
|
-
*/
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Whether `event`, against the CURRENTLY open `gesture`, closes or
|
|
73
|
-
* supersedes it (ADR-0016 §1 paths 1 and 2 only — path 3's timeout is
|
|
74
|
-
* time-based, not event-based, and lives in `isIdleAbandoned`; paths 4 and
|
|
75
|
-
* 5 never seal at all, they only leave a note, and are handled in
|
|
76
|
-
* `planGesture`). A different pointer's event never affects this gesture —
|
|
77
|
-
* multi-touch is out of scope, but the check costs nothing to make correct.
|
|
78
|
-
*
|
|
79
|
-
* @param {Gesture} gesture
|
|
80
|
-
* @param {GestureEvent} event
|
|
81
|
-
* @returns {SealReason | null}
|
|
82
|
-
*/
|
|
83
|
-
export function sealReasonFor(gesture, event) {
|
|
84
|
-
if (event.pointerId !== undefined && event.pointerId !== gesture.pointerId) {
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
if (event.type === "pointerup" || event.type === "pointercancel") {
|
|
88
|
-
return "closed";
|
|
89
|
-
}
|
|
90
|
-
if (event.type === "pointerdown") {
|
|
91
|
-
return "abandoned:superseded";
|
|
92
|
-
}
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* @param {number} idleAbandonMs
|
|
98
|
-
* @returns {void}
|
|
99
|
-
*/
|
|
100
|
-
export function requireIdleAbandonMs(idleAbandonMs) {
|
|
101
|
-
if (typeof idleAbandonMs !== "number" || !Number.isFinite(idleAbandonMs)) {
|
|
102
|
-
throw new TypeError(
|
|
103
|
-
`isIdleAbandoned: idleAbandonMs must be a finite number, got ${JSON.stringify(idleAbandonMs)}`,
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Path 3 (ADR-0016 §1): silence since the gesture's OWN last event, not the
|
|
110
|
-
* gesture's total duration — measured (`docs/adr/0016...md`'s DOĞRULANMALI
|
|
111
|
-
* #1) as the only metric that actually separates closed gestures from
|
|
112
|
-
* orphaned ones (total duration overlaps completely: closed 94-10325ms,
|
|
113
|
-
* orphaned 461-10323ms). `idleAbandonMs` is injected with NO default — the
|
|
114
|
-
* measured 1456.8ms is a FLOOR (the longest legitimate pause seen, n=31),
|
|
115
|
-
* never a ceiling a chosen value could safely equal.
|
|
116
|
-
*
|
|
117
|
-
* While the page is hidden (`gesture.paused`), the clock does not advance
|
|
118
|
-
* at all (ADR-0016 §1 path 5) — this function returns `false` unconditionally.
|
|
119
|
-
*
|
|
120
|
-
* @param {Gesture} gesture
|
|
121
|
-
* @param {number} now
|
|
122
|
-
* @param {number} idleAbandonMs
|
|
123
|
-
* @returns {boolean}
|
|
124
|
-
*/
|
|
125
|
-
export function isIdleAbandoned(gesture, now, idleAbandonMs) {
|
|
126
|
-
requireIdleAbandonMs(idleAbandonMs);
|
|
127
|
-
if (gesture.paused) {
|
|
128
|
-
return false;
|
|
129
|
-
}
|
|
130
|
-
return now - gesture.lastEventAt >= idleAbandonMs;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* @param {GestureEvent} event a "pointerdown"
|
|
135
|
-
* @returns {Gesture}
|
|
136
|
-
*/
|
|
137
|
-
function openGesture(event) {
|
|
138
|
-
return {
|
|
139
|
-
gestureId: /** @type {number} */ (event.gestureId),
|
|
140
|
-
pointerId: /** @type {number} */ (event.pointerId),
|
|
141
|
-
lastEventAt: event.timestamp,
|
|
142
|
-
paused: false,
|
|
143
|
-
captureLostDuringGesture: false,
|
|
144
|
-
pageHiddenDuringGesture: false,
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* @param {Gesture} gesture
|
|
150
|
-
* @param {GestureEvent} event
|
|
151
|
-
* @returns {boolean}
|
|
152
|
-
*/
|
|
153
|
-
function belongsToGesture(gesture, event) {
|
|
154
|
-
return event.pointerId === undefined || event.pointerId === gesture.pointerId;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* The full bracket state transition for one event. `gesture` is the state
|
|
159
|
-
* BEFORE `event`; the returned `GesturePlan.gesture` is the state AFTER.
|
|
160
|
-
*
|
|
161
|
-
* - Idle + `pointerdown` -> opens (uses `event.gestureId`, pre-minted by
|
|
162
|
-
* the caller's `IdFactory` — this function never mints one itself).
|
|
163
|
-
* - `sealReasonFor` decides "closed" (bracket fully ends) and
|
|
164
|
-
* "abandoned:superseded" (bracket ends AND a new one opens in the same
|
|
165
|
-
* call, since the superseding pointerdown IS the next gesture's start).
|
|
166
|
-
* - `lostpointercapture` and `visibilitychange`/`blur` never seal — they
|
|
167
|
-
* only set a note (`captureLostDuringGesture`/`pageHiddenDuringGesture`).
|
|
168
|
-
* `visibilitychange`(hidden)/`blur` ALSO pause the idle clock
|
|
169
|
-
* (ADR-0016 §1 path 5); becoming visible/focused again resumes it and
|
|
170
|
-
* resets `lastEventAt` to the resume moment — otherwise the entire
|
|
171
|
-
* hidden duration would count as "silence since the last event" the
|
|
172
|
-
* instant the page came back, defeating the point of pausing.
|
|
173
|
-
* - A `pointermove`/`lostpointercapture` from a DIFFERENT pointer than the
|
|
174
|
-
* one this gesture is tracking is a pure no-op.
|
|
175
|
-
*
|
|
176
|
-
* @param {Gesture | null} gesture
|
|
177
|
-
* @param {GestureEvent} event
|
|
178
|
-
* @returns {GesturePlan}
|
|
179
|
-
*/
|
|
180
|
-
export function planGesture(gesture, event) {
|
|
181
|
-
if (gesture === null) {
|
|
182
|
-
if (event.type === "pointerdown") {
|
|
183
|
-
return { gesture: openGesture(event), sealReason: null };
|
|
184
|
-
}
|
|
185
|
-
return { gesture: null, sealReason: null };
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const reason = sealReasonFor(gesture, event);
|
|
189
|
-
if (reason === "closed") {
|
|
190
|
-
return { gesture: null, sealReason: "closed" };
|
|
191
|
-
}
|
|
192
|
-
if (reason === "abandoned:superseded") {
|
|
193
|
-
return { gesture: openGesture(event), sealReason: "abandoned:superseded" };
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
switch (event.type) {
|
|
197
|
-
case "pointermove":
|
|
198
|
-
return belongsToGesture(gesture, event)
|
|
199
|
-
? { gesture: { ...gesture, lastEventAt: event.timestamp }, sealReason: null }
|
|
200
|
-
: { gesture, sealReason: null };
|
|
201
|
-
case "lostpointercapture":
|
|
202
|
-
return belongsToGesture(gesture, event)
|
|
203
|
-
? { gesture: { ...gesture, captureLostDuringGesture: true }, sealReason: null }
|
|
204
|
-
: { gesture, sealReason: null };
|
|
205
|
-
case "visibilitychange":
|
|
206
|
-
return event.hidden
|
|
207
|
-
? { gesture: { ...gesture, pageHiddenDuringGesture: true, paused: true }, sealReason: null }
|
|
208
|
-
: { gesture: { ...gesture, paused: false, lastEventAt: event.timestamp }, sealReason: null };
|
|
209
|
-
case "blur":
|
|
210
|
-
return { gesture: { ...gesture, pageHiddenDuringGesture: true, paused: true }, sealReason: null };
|
|
211
|
-
case "focus":
|
|
212
|
-
return { gesture: { ...gesture, paused: false, lastEventAt: event.timestamp }, sealReason: null };
|
|
213
|
-
default:
|
|
214
|
-
return { gesture, sealReason: null };
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* @typedef {object} DragEntry
|
|
220
|
-
* @property {import("../model/node.js").NodeId} containerId
|
|
221
|
-
* @property {import("./geometry.js").Rect} anchor the box's own layout at gesture start
|
|
222
|
-
* @property {(typeof import("./handles.js").ALL_HANDLES)[number] | "body"} handle "body" for a plain move
|
|
223
|
-
*/
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* The layout patch for every dragged box, for ONE frame of an open gesture
|
|
227
|
-
* — the array `transformBoxes`/`transformBox` (`src/commands/box.js`)
|
|
228
|
-
* expects. `entries`' order is FROZEN for the whole gesture by the caller
|
|
229
|
-
* (ADR-0018 Karar 3): `HistoryEngine` matches frames by INDEX, so a
|
|
230
|
-
* reordered `entries` array would silently coalesce one box's step into a
|
|
231
|
-
* different box's slot. This function does not enforce that — it is the
|
|
232
|
-
* caller's contract, the same way `resizePatch`/`movePatch` trust their own
|
|
233
|
-
* caller for `handle`'s validity.
|
|
234
|
-
*
|
|
235
|
-
* @param {DragEntry[]} entries
|
|
236
|
-
* @param {number} dx
|
|
237
|
-
* @param {number} dy
|
|
238
|
-
* @param {import("./resize.js").ResizeLimits} limits
|
|
239
|
-
* @returns {import("../commands/box.js").BoxTransformEntry[]}
|
|
240
|
-
*/
|
|
241
|
-
export function framePatches(entries, dx, dy, limits) {
|
|
242
|
-
return entries.map((entry) => ({
|
|
243
|
-
containerId: entry.containerId,
|
|
244
|
-
patch:
|
|
245
|
-
entry.handle === "body"
|
|
246
|
-
? movePatch(entry.anchor, dx, dy)
|
|
247
|
-
: resizePatch(entry.anchor, entry.handle, dx, dy, limits),
|
|
248
|
-
}));
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Whether two frames (the OUTPUT of `framePatches`, or any array shaped the
|
|
253
|
-
* same way) would produce coalescible steps — the CANVAS-layer twin of
|
|
254
|
-
* `HistoryEngine`'s `sameLayoutFrame` (`src/history/history.js`), asked
|
|
255
|
-
* before a `Step` ever exists. Deliberately NOT implemented by importing
|
|
256
|
-
* `sameLayoutFrame` — this file has no history import (this directory's own
|
|
257
|
-
* "No src/, DOM, or history import" rule), and the two questions are asked
|
|
258
|
-
* by different consumers for different reasons, the same "two related
|
|
259
|
-
* tables, two files" split `resizeKeys`/`handleEdges` already established.
|
|
260
|
-
*
|
|
261
|
-
* @param {import("../commands/box.js").BoxTransformEntry[]} previousPatches
|
|
262
|
-
* @param {import("../commands/box.js").BoxTransformEntry[]} patches
|
|
263
|
-
* @returns {boolean}
|
|
264
|
-
*/
|
|
265
|
-
export function framePatchesMatchKeys(previousPatches, patches) {
|
|
266
|
-
if (previousPatches.length !== patches.length) {
|
|
267
|
-
return false;
|
|
268
|
-
}
|
|
269
|
-
return previousPatches.every((previous, i) => {
|
|
270
|
-
const current = patches[i];
|
|
271
|
-
return previous.containerId === current.containerId && sameKeySet(previous.patch, current.patch);
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* Local to this file on purpose — see `framePatchesMatchKeys`'s own comment
|
|
277
|
-
* for why this is not imported from `history.js`.
|
|
278
|
-
* @param {Partial<import("../model/node.js").BoxLayout>} a
|
|
279
|
-
* @param {Partial<import("../model/node.js").BoxLayout>} b
|
|
280
|
-
* @returns {boolean}
|
|
281
|
-
*/
|
|
282
|
-
function sameKeySet(a, b) {
|
|
283
|
-
const aKeys = Object.keys(a);
|
|
284
|
-
const bKeys = Object.keys(b);
|
|
285
|
-
if (aKeys.length !== bKeys.length) {
|
|
286
|
-
return false;
|
|
287
|
-
}
|
|
288
|
-
return aKeys.every((key) => Object.prototype.hasOwnProperty.call(b, key));
|
|
289
|
-
}
|
|
1
|
+
// The pointer-gesture bracket: idle -> open -> sealed(reason) (ADR-0016 §1,
|
|
2
|
+
// measured in two engines). No src/, DOM, or history import — pure data in,
|
|
3
|
+
// data out, same discipline as every other file in this directory.
|
|
4
|
+
//
|
|
5
|
+
// ⚠ SENTETİK DİZİLER KANIT DEĞİL (test/input/composition.test.js's own
|
|
6
|
+
// header, restated here for the same reason): this file's own tests drive
|
|
7
|
+
// hand-written event sequences through these functions. That proves the
|
|
8
|
+
// state machine behaves correctly GIVEN that sequence. It does NOT prove a
|
|
9
|
+
// real browser produces that sequence — the real evidence is
|
|
10
|
+
// docs/adr/0016-gesture-bracket-and-accumulative-invert.md's G1-G10
|
|
11
|
+
// measurements, and ADR-0018 §10.2 is explicit that multi-selection
|
|
12
|
+
// dragging (Ö5) never reached the probe at all.
|
|
13
|
+
//
|
|
14
|
+
// MEASURED RULES this module encodes (ADR-0016, two engines — Chrome +
|
|
15
|
+
// Firefox):
|
|
16
|
+
// - `pointerup` closes the bracket, but MAY NOT ARRIVE (Chrome 6/17,
|
|
17
|
+
// Firefox 1/4 — right-click mid-drag). The one reliable backup is a
|
|
18
|
+
// NEW `pointerdown` on the SAME `pointerId` (G2) — pointerId's own
|
|
19
|
+
// VALUE is engine-dependent (Chrome always 1, Firefox always 0), so
|
|
20
|
+
// this file only ever compares two observed values for EQUALITY, never
|
|
21
|
+
// against a hardcoded constant.
|
|
22
|
+
// - `lostpointercapture` is NOT a seal signal (G3): 5/6 orphaned gestures
|
|
23
|
+
// never got it at all, and the one that did got it mid-gesture without
|
|
24
|
+
// closing.
|
|
25
|
+
// - `focusout`/`blur` are NOT seal signals (G4/G6): 20/20 focusout events
|
|
26
|
+
// fired with `hasFocus: true` still true, and 3/4 visibilitychange
|
|
27
|
+
// events during an open gesture still closed normally via pointerup.
|
|
28
|
+
// - Capture loss is not gesture end, only a NOTE (G5) — Blink-specific
|
|
29
|
+
// (14/14 Firefox gestures never showed it at all).
|
|
30
|
+
// - Sealing is IDEMPOTENT / order-tolerant: G10 (Firefox) found a key's
|
|
31
|
+
// own delayed signal can arrive out of the order a naive implementation
|
|
32
|
+
// would assume — nudge.js's `keyUpSealsOpenBracket` carries the same
|
|
33
|
+
// discipline for the keyboard bracket.
|
|
34
|
+
|
|
35
|
+
import { movePatch, resizePatch } from "./resize.js";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @typedef {"closed" | "abandoned:superseded" | "abandoned:timeout"} SealReason
|
|
39
|
+
*
|
|
40
|
+
* Terk (abandon) kapanıştan (closed) AYIRT EDİLEBİLİR tutulur (ADR-0016 §1):
|
|
41
|
+
* a mutation that let an abandoned gesture read back as "closed" would make
|
|
42
|
+
* "the bracket abandons cleanly" impossible to assert — it would only ever
|
|
43
|
+
* be TOLERATED, never verified.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @typedef {object} Gesture
|
|
48
|
+
* @property {number} gestureId minted by the caller's IdFactory, monotonic, never reused after sealing
|
|
49
|
+
* @property {number} pointerId compared by EQUALITY only — never a hardcoded engine constant
|
|
50
|
+
* @property {number} lastEventAt timestamp of the most recent event this gesture's clock cares about
|
|
51
|
+
* @property {boolean} paused true while the page is hidden/unfocused — `isIdleAbandoned` does not advance the clock
|
|
52
|
+
* @property {boolean} captureLostDuringGesture note only, never a seal (path 4)
|
|
53
|
+
* @property {boolean} pageHiddenDuringGesture note only, never a seal (path 5)
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @typedef {object} GestureEvent
|
|
58
|
+
* @property {"pointerdown" | "pointerup" | "pointercancel" | "pointermove" | "lostpointercapture" | "visibilitychange" | "blur" | "focus"} type
|
|
59
|
+
* @property {number} [pointerId] absent for visibilitychange/blur/focus
|
|
60
|
+
* @property {number} timestamp
|
|
61
|
+
* @property {boolean} [hidden] visibilitychange only: `document.hidden`
|
|
62
|
+
* @property {number} [gestureId] pointerdown only: pre-minted id for the gesture this event WOULD open
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @typedef {object} GesturePlan
|
|
67
|
+
* @property {Gesture | null} gesture the gesture state AFTER this event; `null` once idle
|
|
68
|
+
* @property {SealReason | null} sealReason what happened to the PREVIOUS (pre-event) gesture this call, if anything
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Whether `event`, against the CURRENTLY open `gesture`, closes or
|
|
73
|
+
* supersedes it (ADR-0016 §1 paths 1 and 2 only — path 3's timeout is
|
|
74
|
+
* time-based, not event-based, and lives in `isIdleAbandoned`; paths 4 and
|
|
75
|
+
* 5 never seal at all, they only leave a note, and are handled in
|
|
76
|
+
* `planGesture`). A different pointer's event never affects this gesture —
|
|
77
|
+
* multi-touch is out of scope, but the check costs nothing to make correct.
|
|
78
|
+
*
|
|
79
|
+
* @param {Gesture} gesture
|
|
80
|
+
* @param {GestureEvent} event
|
|
81
|
+
* @returns {SealReason | null}
|
|
82
|
+
*/
|
|
83
|
+
export function sealReasonFor(gesture, event) {
|
|
84
|
+
if (event.pointerId !== undefined && event.pointerId !== gesture.pointerId) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
if (event.type === "pointerup" || event.type === "pointercancel") {
|
|
88
|
+
return "closed";
|
|
89
|
+
}
|
|
90
|
+
if (event.type === "pointerdown") {
|
|
91
|
+
return "abandoned:superseded";
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @param {number} idleAbandonMs
|
|
98
|
+
* @returns {void}
|
|
99
|
+
*/
|
|
100
|
+
export function requireIdleAbandonMs(idleAbandonMs) {
|
|
101
|
+
if (typeof idleAbandonMs !== "number" || !Number.isFinite(idleAbandonMs)) {
|
|
102
|
+
throw new TypeError(
|
|
103
|
+
`isIdleAbandoned: idleAbandonMs must be a finite number, got ${JSON.stringify(idleAbandonMs)}`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Path 3 (ADR-0016 §1): silence since the gesture's OWN last event, not the
|
|
110
|
+
* gesture's total duration — measured (`docs/adr/0016...md`'s DOĞRULANMALI
|
|
111
|
+
* #1) as the only metric that actually separates closed gestures from
|
|
112
|
+
* orphaned ones (total duration overlaps completely: closed 94-10325ms,
|
|
113
|
+
* orphaned 461-10323ms). `idleAbandonMs` is injected with NO default — the
|
|
114
|
+
* measured 1456.8ms is a FLOOR (the longest legitimate pause seen, n=31),
|
|
115
|
+
* never a ceiling a chosen value could safely equal.
|
|
116
|
+
*
|
|
117
|
+
* While the page is hidden (`gesture.paused`), the clock does not advance
|
|
118
|
+
* at all (ADR-0016 §1 path 5) — this function returns `false` unconditionally.
|
|
119
|
+
*
|
|
120
|
+
* @param {Gesture} gesture
|
|
121
|
+
* @param {number} now
|
|
122
|
+
* @param {number} idleAbandonMs
|
|
123
|
+
* @returns {boolean}
|
|
124
|
+
*/
|
|
125
|
+
export function isIdleAbandoned(gesture, now, idleAbandonMs) {
|
|
126
|
+
requireIdleAbandonMs(idleAbandonMs);
|
|
127
|
+
if (gesture.paused) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
return now - gesture.lastEventAt >= idleAbandonMs;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* @param {GestureEvent} event a "pointerdown"
|
|
135
|
+
* @returns {Gesture}
|
|
136
|
+
*/
|
|
137
|
+
function openGesture(event) {
|
|
138
|
+
return {
|
|
139
|
+
gestureId: /** @type {number} */ (event.gestureId),
|
|
140
|
+
pointerId: /** @type {number} */ (event.pointerId),
|
|
141
|
+
lastEventAt: event.timestamp,
|
|
142
|
+
paused: false,
|
|
143
|
+
captureLostDuringGesture: false,
|
|
144
|
+
pageHiddenDuringGesture: false,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* @param {Gesture} gesture
|
|
150
|
+
* @param {GestureEvent} event
|
|
151
|
+
* @returns {boolean}
|
|
152
|
+
*/
|
|
153
|
+
function belongsToGesture(gesture, event) {
|
|
154
|
+
return event.pointerId === undefined || event.pointerId === gesture.pointerId;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The full bracket state transition for one event. `gesture` is the state
|
|
159
|
+
* BEFORE `event`; the returned `GesturePlan.gesture` is the state AFTER.
|
|
160
|
+
*
|
|
161
|
+
* - Idle + `pointerdown` -> opens (uses `event.gestureId`, pre-minted by
|
|
162
|
+
* the caller's `IdFactory` — this function never mints one itself).
|
|
163
|
+
* - `sealReasonFor` decides "closed" (bracket fully ends) and
|
|
164
|
+
* "abandoned:superseded" (bracket ends AND a new one opens in the same
|
|
165
|
+
* call, since the superseding pointerdown IS the next gesture's start).
|
|
166
|
+
* - `lostpointercapture` and `visibilitychange`/`blur` never seal — they
|
|
167
|
+
* only set a note (`captureLostDuringGesture`/`pageHiddenDuringGesture`).
|
|
168
|
+
* `visibilitychange`(hidden)/`blur` ALSO pause the idle clock
|
|
169
|
+
* (ADR-0016 §1 path 5); becoming visible/focused again resumes it and
|
|
170
|
+
* resets `lastEventAt` to the resume moment — otherwise the entire
|
|
171
|
+
* hidden duration would count as "silence since the last event" the
|
|
172
|
+
* instant the page came back, defeating the point of pausing.
|
|
173
|
+
* - A `pointermove`/`lostpointercapture` from a DIFFERENT pointer than the
|
|
174
|
+
* one this gesture is tracking is a pure no-op.
|
|
175
|
+
*
|
|
176
|
+
* @param {Gesture | null} gesture
|
|
177
|
+
* @param {GestureEvent} event
|
|
178
|
+
* @returns {GesturePlan}
|
|
179
|
+
*/
|
|
180
|
+
export function planGesture(gesture, event) {
|
|
181
|
+
if (gesture === null) {
|
|
182
|
+
if (event.type === "pointerdown") {
|
|
183
|
+
return { gesture: openGesture(event), sealReason: null };
|
|
184
|
+
}
|
|
185
|
+
return { gesture: null, sealReason: null };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const reason = sealReasonFor(gesture, event);
|
|
189
|
+
if (reason === "closed") {
|
|
190
|
+
return { gesture: null, sealReason: "closed" };
|
|
191
|
+
}
|
|
192
|
+
if (reason === "abandoned:superseded") {
|
|
193
|
+
return { gesture: openGesture(event), sealReason: "abandoned:superseded" };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
switch (event.type) {
|
|
197
|
+
case "pointermove":
|
|
198
|
+
return belongsToGesture(gesture, event)
|
|
199
|
+
? { gesture: { ...gesture, lastEventAt: event.timestamp }, sealReason: null }
|
|
200
|
+
: { gesture, sealReason: null };
|
|
201
|
+
case "lostpointercapture":
|
|
202
|
+
return belongsToGesture(gesture, event)
|
|
203
|
+
? { gesture: { ...gesture, captureLostDuringGesture: true }, sealReason: null }
|
|
204
|
+
: { gesture, sealReason: null };
|
|
205
|
+
case "visibilitychange":
|
|
206
|
+
return event.hidden
|
|
207
|
+
? { gesture: { ...gesture, pageHiddenDuringGesture: true, paused: true }, sealReason: null }
|
|
208
|
+
: { gesture: { ...gesture, paused: false, lastEventAt: event.timestamp }, sealReason: null };
|
|
209
|
+
case "blur":
|
|
210
|
+
return { gesture: { ...gesture, pageHiddenDuringGesture: true, paused: true }, sealReason: null };
|
|
211
|
+
case "focus":
|
|
212
|
+
return { gesture: { ...gesture, paused: false, lastEventAt: event.timestamp }, sealReason: null };
|
|
213
|
+
default:
|
|
214
|
+
return { gesture, sealReason: null };
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* @typedef {object} DragEntry
|
|
220
|
+
* @property {import("../model/node.js").NodeId} containerId
|
|
221
|
+
* @property {import("./geometry.js").Rect} anchor the box's own layout at gesture start
|
|
222
|
+
* @property {(typeof import("./handles.js").ALL_HANDLES)[number] | "body"} handle "body" for a plain move
|
|
223
|
+
*/
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* The layout patch for every dragged box, for ONE frame of an open gesture
|
|
227
|
+
* — the array `transformBoxes`/`transformBox` (`src/commands/box.js`)
|
|
228
|
+
* expects. `entries`' order is FROZEN for the whole gesture by the caller
|
|
229
|
+
* (ADR-0018 Karar 3): `HistoryEngine` matches frames by INDEX, so a
|
|
230
|
+
* reordered `entries` array would silently coalesce one box's step into a
|
|
231
|
+
* different box's slot. This function does not enforce that — it is the
|
|
232
|
+
* caller's contract, the same way `resizePatch`/`movePatch` trust their own
|
|
233
|
+
* caller for `handle`'s validity.
|
|
234
|
+
*
|
|
235
|
+
* @param {DragEntry[]} entries
|
|
236
|
+
* @param {number} dx
|
|
237
|
+
* @param {number} dy
|
|
238
|
+
* @param {import("./resize.js").ResizeLimits} limits
|
|
239
|
+
* @returns {import("../commands/box.js").BoxTransformEntry[]}
|
|
240
|
+
*/
|
|
241
|
+
export function framePatches(entries, dx, dy, limits) {
|
|
242
|
+
return entries.map((entry) => ({
|
|
243
|
+
containerId: entry.containerId,
|
|
244
|
+
patch:
|
|
245
|
+
entry.handle === "body"
|
|
246
|
+
? movePatch(entry.anchor, dx, dy)
|
|
247
|
+
: resizePatch(entry.anchor, entry.handle, dx, dy, limits),
|
|
248
|
+
}));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Whether two frames (the OUTPUT of `framePatches`, or any array shaped the
|
|
253
|
+
* same way) would produce coalescible steps — the CANVAS-layer twin of
|
|
254
|
+
* `HistoryEngine`'s `sameLayoutFrame` (`src/history/history.js`), asked
|
|
255
|
+
* before a `Step` ever exists. Deliberately NOT implemented by importing
|
|
256
|
+
* `sameLayoutFrame` — this file has no history import (this directory's own
|
|
257
|
+
* "No src/, DOM, or history import" rule), and the two questions are asked
|
|
258
|
+
* by different consumers for different reasons, the same "two related
|
|
259
|
+
* tables, two files" split `resizeKeys`/`handleEdges` already established.
|
|
260
|
+
*
|
|
261
|
+
* @param {import("../commands/box.js").BoxTransformEntry[]} previousPatches
|
|
262
|
+
* @param {import("../commands/box.js").BoxTransformEntry[]} patches
|
|
263
|
+
* @returns {boolean}
|
|
264
|
+
*/
|
|
265
|
+
export function framePatchesMatchKeys(previousPatches, patches) {
|
|
266
|
+
if (previousPatches.length !== patches.length) {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
return previousPatches.every((previous, i) => {
|
|
270
|
+
const current = patches[i];
|
|
271
|
+
return previous.containerId === current.containerId && sameKeySet(previous.patch, current.patch);
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Local to this file on purpose — see `framePatchesMatchKeys`'s own comment
|
|
277
|
+
* for why this is not imported from `history.js`.
|
|
278
|
+
* @param {Partial<import("../model/node.js").BoxLayout>} a
|
|
279
|
+
* @param {Partial<import("../model/node.js").BoxLayout>} b
|
|
280
|
+
* @returns {boolean}
|
|
281
|
+
*/
|
|
282
|
+
function sameKeySet(a, b) {
|
|
283
|
+
const aKeys = Object.keys(a);
|
|
284
|
+
const bKeys = Object.keys(b);
|
|
285
|
+
if (aKeys.length !== bKeys.length) {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
return aKeys.every((key) => Object.prototype.hasOwnProperty.call(b, key));
|
|
289
|
+
}
|