retake-dev 0.4.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/LICENSE +91 -0
- package/README.md +198 -0
- package/bin/retake.js +319 -0
- package/package.json +87 -0
- package/src/code-versions.js +357 -0
- package/src/core.js +265 -0
- package/src/plugin.js +137 -0
- package/src/runtime/00-core.js +308 -0
- package/src/runtime/10-animations.js +362 -0
- package/src/runtime/20-media.js +157 -0
- package/src/runtime/30-recorder.js +315 -0
- package/src/runtime/32-state.js +315 -0
- package/src/runtime/35-network.js +789 -0
- package/src/runtime/36-scripts.js +129 -0
- package/src/runtime/37-next.js +150 -0
- package/src/runtime/38-observers.js +252 -0
- package/src/runtime/40-input.js +795 -0
- package/src/runtime/45-hover.js +78 -0
- package/src/runtime/50-engine.js +341 -0
- package/src/runtime/60-preview.js +437 -0
- package/src/runtime/65-timeline.js +289 -0
- package/src/runtime/66-activity.js +114 -0
- package/src/runtime/67-csssource.js +226 -0
- package/src/runtime/70-boot.js +460 -0
- package/src/server/api.js +285 -0
- package/src/server/child.js +174 -0
- package/src/server/detect.js +167 -0
- package/src/server/front.js +647 -0
- package/src/server/mcp.js +332 -0
- package/src/shell/00-state.js +85 -0
- package/src/shell/05-api.js +136 -0
- package/src/shell/10-dock.js +754 -0
- package/src/shell/12-checkpoint.js +121 -0
- package/src/shell/15-session.js +291 -0
- package/src/shell/20-timeline.js +734 -0
- package/src/shell/25-input.js +537 -0
- package/src/shell/30-notes.js +870 -0
- package/src/shell/40-code.js +80 -0
- package/src/shell/90-handle.js +15 -0
- package/src/shell/shell.css +295 -0
- package/src/shell/shell.html +59 -0
- package/types/client.d.ts +73 -0
- package/types/index.d.ts +111 -0
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
// Captures real input into the recording and re-dispatches it during replay.
|
|
2
|
+
// Targets are stored as child-index paths from <html>; replay reproduces the
|
|
3
|
+
// same DOM, so the same path finds the same element.
|
|
4
|
+
//
|
|
5
|
+
// Events are stored compactly: fields at their usual value are left out and
|
|
6
|
+
// filled back in on replay (see DEFAULTS).
|
|
7
|
+
|
|
8
|
+
const POINTER = ["pointerdown", "pointerup", "pointermove", "pointerover", "pointerout", "pointerenter", "pointerleave", "pointercancel"]
|
|
9
|
+
const MOUSE = ["mousedown", "mouseup", "mousemove", "mouseover", "mouseout", "mouseenter", "mouseleave", "click", "dblclick", "contextmenu", "auxclick"]
|
|
10
|
+
const KEYS = ["keydown", "keyup", "keypress"]
|
|
11
|
+
const TOUCH = ["touchstart", "touchmove", "touchend", "touchcancel"]
|
|
12
|
+
const DRAG = ["dragstart", "drag", "dragenter", "dragover", "dragleave", "drop", "dragend"]
|
|
13
|
+
const CLIP = ["paste", "copy", "cut"]
|
|
14
|
+
const COMPOSE = ["compositionstart", "compositionupdate", "compositionend"]
|
|
15
|
+
const OTHER = ["input", "beforeinput", "change", "focusin", "focusout", "focus", "blur", "scroll", "wheel", "submit"]
|
|
16
|
+
const HOVER = new Set(["pointermove", "pointerover", "pointerout", "pointerenter", "pointerleave", "mousemove", "mouseover", "mouseout", "mouseenter", "mouseleave"])
|
|
17
|
+
const SCROLL_KEYS = new Set(["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "PageUp", "PageDown", "Home", "End", " "])
|
|
18
|
+
|
|
19
|
+
let dispatching = 0
|
|
20
|
+
let missingTargets = 0
|
|
21
|
+
let lastScreenOffset = null // recording
|
|
22
|
+
let replayScreenOffset = [0, 0] // replay
|
|
23
|
+
|
|
24
|
+
// True from an input event until the current task ends (a message on a
|
|
25
|
+
// private channel clears it at the start of the next task).
|
|
26
|
+
let inputTask = false
|
|
27
|
+
const taskChannel = new RealChannel()
|
|
28
|
+
taskChannel.port1.onmessage = () => (inputTask = false)
|
|
29
|
+
function markInputTask() {
|
|
30
|
+
if (inputTask) return
|
|
31
|
+
inputTask = true
|
|
32
|
+
portPost.call(taskChannel.port2, 0)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Virtual focus: while rebuilding, the frame being built doesn't have real
|
|
36
|
+
// focus (the visible frame or the dock does), so the browser keeps moving
|
|
37
|
+
// focus away from what the recording focused. The recording's own focus is
|
|
38
|
+
// tracked here: keys go to it, and document.activeElement reports it.
|
|
39
|
+
let vFocus = null
|
|
40
|
+
const realActive = Object.getOwnPropertyDescriptor(Document.prototype, "activeElement").get
|
|
41
|
+
const focused = () => (vFocus && vFocus.isConnected ? vFocus : null)
|
|
42
|
+
Object.defineProperty(document, "activeElement", {
|
|
43
|
+
configurable: true,
|
|
44
|
+
get() {
|
|
45
|
+
return (clock.seeking && focused()) || realActive.call(document)
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// Paths to an element. A recording made since pathV 2 (F51) starts at the
|
|
50
|
+
// nearest ancestor with a hand-written id (`["#banner", 0, 1]`) and counts
|
|
51
|
+
// element children only, leaving out the ones that come and go with the
|
|
52
|
+
// server or the dev tools (script, style, link, template, Next's portals,
|
|
53
|
+
// Vite's overlay...), so content a script adds in real time elsewhere doesn't
|
|
54
|
+
// shift it. A text node's last step is its child-node index, as -(i + 1).
|
|
55
|
+
// Older recordings index every child node from <html>.
|
|
56
|
+
const PATH_SKIP = /^(SCRIPT|STYLE|LINK|NOSCRIPT|TEMPLATE|NEXTJS-PORTAL|NEXT-ROUTE-ANNOUNCER|VITE-ERROR-OVERLAY|ASTRO-DEV-TOOLBAR)$/
|
|
57
|
+
// Ids a library made up (React's useId, Radix, Headless UI, MUI...) differ
|
|
58
|
+
// between renders; they're not anchors.
|
|
59
|
+
const MADE_UP_ID = /[:«»]|^_?r_|^(radix|headlessui|mui|react-aria|rc-|ember|ext-gen)|\d{4,}/i
|
|
60
|
+
const stepsOf = (parent) => Array.prototype.filter.call(parent.children, (c) => !PATH_SKIP.test(c.tagName))
|
|
61
|
+
const pathV2 = () => !!rec && rec.pathV === 2
|
|
62
|
+
|
|
63
|
+
function anchorId(el) {
|
|
64
|
+
const id = el.id
|
|
65
|
+
if (!id || MADE_UP_ID.test(id) || el === document.documentElement) return null
|
|
66
|
+
return document.getElementById(id) === el ? id : null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function pathOf(node) {
|
|
70
|
+
if (node === W) return "w"
|
|
71
|
+
if (node === document) return "d"
|
|
72
|
+
if (!pathV2()) return pathV1(node)
|
|
73
|
+
const path = []
|
|
74
|
+
if (node && node.nodeType !== 1) {
|
|
75
|
+
const parent = node.parentNode
|
|
76
|
+
if (!parent || parent.nodeType !== 1) return null
|
|
77
|
+
path.unshift(-1 - Array.prototype.indexOf.call(parent.childNodes, node))
|
|
78
|
+
node = parent
|
|
79
|
+
}
|
|
80
|
+
while (node && node !== document.documentElement) {
|
|
81
|
+
const id = anchorId(node)
|
|
82
|
+
if (id) {
|
|
83
|
+
path.unshift("#" + id)
|
|
84
|
+
return path
|
|
85
|
+
}
|
|
86
|
+
const parent = node.parentNode
|
|
87
|
+
if (!parent || parent.nodeType !== 1) return null
|
|
88
|
+
const i = stepsOf(parent).indexOf(node)
|
|
89
|
+
if (i < 0) return pathV1Within(path, node) // a skipped element itself (a <script>)
|
|
90
|
+
path.unshift(i)
|
|
91
|
+
node = parent
|
|
92
|
+
}
|
|
93
|
+
return node ? path : null
|
|
94
|
+
}
|
|
95
|
+
// A skipped element is targeted by its child-node index (like a text node).
|
|
96
|
+
function pathV1Within(path, node) {
|
|
97
|
+
const parent = node.parentNode
|
|
98
|
+
path.unshift(-1 - Array.prototype.indexOf.call(parent.childNodes, node))
|
|
99
|
+
const up = pathOf(parent)
|
|
100
|
+
return up && Array.isArray(up) ? [...up, ...path] : null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function pathV1(node) {
|
|
104
|
+
const path = []
|
|
105
|
+
while (node && node !== document.documentElement) {
|
|
106
|
+
const parent = node.parentNode
|
|
107
|
+
if (!parent || parent.nodeType !== 1) return null
|
|
108
|
+
path.unshift(Array.prototype.indexOf.call(parent.childNodes, node))
|
|
109
|
+
node = parent
|
|
110
|
+
}
|
|
111
|
+
return node ? path : null
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function resolvePath(path) {
|
|
115
|
+
if (path === "w") return W
|
|
116
|
+
if (path === "d") return document
|
|
117
|
+
if (!Array.isArray(path)) return null
|
|
118
|
+
/** @type {any} */
|
|
119
|
+
let node = document.documentElement
|
|
120
|
+
let i = 0
|
|
121
|
+
if (typeof path[0] === "string" && path[0][0] === "#") {
|
|
122
|
+
node = document.getElementById(path[0].slice(1))
|
|
123
|
+
i = 1
|
|
124
|
+
} else if (!pathV2()) {
|
|
125
|
+
for (const k of path) node = node && node.childNodes[k]
|
|
126
|
+
return node || null
|
|
127
|
+
}
|
|
128
|
+
for (; i < path.length && node; i++) {
|
|
129
|
+
const k = path[i]
|
|
130
|
+
node = k < 0 ? node.childNodes[-1 - k] : stepsOf(node)[k]
|
|
131
|
+
}
|
|
132
|
+
return node || null
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Usual values, left out of the recording.
|
|
136
|
+
const DEFAULTS = {
|
|
137
|
+
bubbles: true,
|
|
138
|
+
cancelable: true,
|
|
139
|
+
composed: true,
|
|
140
|
+
pointerId: 1,
|
|
141
|
+
pointerType: "mouse",
|
|
142
|
+
isPrimary: true,
|
|
143
|
+
width: 1,
|
|
144
|
+
height: 1,
|
|
145
|
+
}
|
|
146
|
+
const NUMS = ["clientX", "clientY", "screenX", "screenY", "button", "buttons", "detail", "movementX", "movementY", "pressure", "location", "keyCode", "which", "charCode", "deltaX", "deltaY", "deltaZ", "deltaMode"]
|
|
147
|
+
const BOOLS = ["ctrlKey", "shiftKey", "altKey", "metaKey", "repeat", "isComposing"]
|
|
148
|
+
|
|
149
|
+
function put(ev, k, v) {
|
|
150
|
+
if (v === undefined || v === null) return
|
|
151
|
+
if (k in DEFAULTS) {
|
|
152
|
+
if (v !== DEFAULTS[k]) ev[k] = v
|
|
153
|
+
} else if (typeof v === "number") {
|
|
154
|
+
if (v !== 0) ev[k] = v
|
|
155
|
+
} else if (typeof v === "boolean") {
|
|
156
|
+
if (v) ev[k] = v
|
|
157
|
+
} else if (v !== "") ev[k] = v
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function touchList(list) {
|
|
161
|
+
return [...(list || [])].map((t) => ({ id: t.identifier, x: t.clientX, y: t.clientY, sx: t.screenX, sy: t.screenY, path: pathOf(t.target) }))
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function transferData(dt) {
|
|
165
|
+
if (!dt) return undefined
|
|
166
|
+
const out = {}
|
|
167
|
+
try {
|
|
168
|
+
for (const type of dt.types || []) if (type !== "Files") out[type] = dt.getData(type)
|
|
169
|
+
} catch {}
|
|
170
|
+
return Object.keys(out).length ? out : undefined
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function serialize(e) {
|
|
174
|
+
const ev = { type: e.type, path: pathOf(e.target) }
|
|
175
|
+
for (const k of ["bubbles", "cancelable", "composed"]) put(ev, k, e[k])
|
|
176
|
+
if (e instanceof MouseEvent) {
|
|
177
|
+
for (const k of ["clientX", "clientY", "buttons", "detail", "movementX", "movementY"]) put(ev, k, e[k])
|
|
178
|
+
// button: -1 is usual for moves, 0 otherwise
|
|
179
|
+
if (e.button !== (/move|over|out|enter|leave/.test(e.type) ? -1 : 0)) ev.button = e.button
|
|
180
|
+
// screen = client + the window's offset, stored only when it changes
|
|
181
|
+
const so = [e.screenX - e.clientX, e.screenY - e.clientY]
|
|
182
|
+
if (!lastScreenOffset || lastScreenOffset[0] !== so[0] || lastScreenOffset[1] !== so[1]) ev.so = lastScreenOffset = so
|
|
183
|
+
if (e.relatedTarget) ev.related = pathOf(e.relatedTarget)
|
|
184
|
+
}
|
|
185
|
+
if (e instanceof PointerEvent) for (const k of ["pointerId", "pointerType", "isPrimary", "width", "height", "pressure"]) put(ev, k, e[k])
|
|
186
|
+
if (typeof WheelEvent !== "undefined" && e instanceof WheelEvent) for (const k of ["deltaX", "deltaY", "deltaZ", "deltaMode"]) put(ev, k, e[k])
|
|
187
|
+
if (e instanceof MouseEvent || e instanceof KeyboardEvent || (typeof TouchEvent !== "undefined" && e instanceof TouchEvent)) for (const k of BOOLS) put(ev, k, e[k])
|
|
188
|
+
if (e instanceof KeyboardEvent) {
|
|
189
|
+
if (e.target === document.activeElement) ev.active = true
|
|
190
|
+
for (const k of ["key", "code"]) put(ev, k, e[k])
|
|
191
|
+
for (const k of ["location", "keyCode", "which", "charCode"]) put(ev, k, e[k])
|
|
192
|
+
}
|
|
193
|
+
if (typeof TouchEvent !== "undefined" && e instanceof TouchEvent) {
|
|
194
|
+
ev.touches = touchList(e.touches)
|
|
195
|
+
ev.targetTouches = touchList(e.targetTouches)
|
|
196
|
+
ev.changedTouches = touchList(e.changedTouches)
|
|
197
|
+
}
|
|
198
|
+
if (typeof DragEvent !== "undefined" && e instanceof DragEvent) ev.dt = transferData(e.dataTransfer)
|
|
199
|
+
if (typeof ClipboardEvent !== "undefined" && e instanceof ClipboardEvent) ev.dt = transferData(e.clipboardData)
|
|
200
|
+
if (typeof CompositionEvent !== "undefined" && e instanceof CompositionEvent) put(ev, "data", e.data)
|
|
201
|
+
if (e.type === "beforeinput") {
|
|
202
|
+
put(ev, "inputType", e.inputType)
|
|
203
|
+
put(ev, "data", e.data)
|
|
204
|
+
}
|
|
205
|
+
if (e.type === "input" || e.type === "change") {
|
|
206
|
+
const el = e.target
|
|
207
|
+
ev.value = el.isContentEditable ? el.innerHTML : el.value
|
|
208
|
+
put(ev, "inputType", e.inputType)
|
|
209
|
+
put(ev, "data", e.data)
|
|
210
|
+
if (el.type === "checkbox" || el.type === "radio") ev.checked = !!el.checked
|
|
211
|
+
try {
|
|
212
|
+
if (typeof el.selectionStart === "number") ev.range = [el.selectionStart, el.selectionEnd]
|
|
213
|
+
} catch {}
|
|
214
|
+
}
|
|
215
|
+
if (e.type === "submit") {
|
|
216
|
+
if (e.submitter) ev.submitter = pathOf(e.submitter)
|
|
217
|
+
// Submitted by a click we recorded (a button, or Enter's implicit click)?
|
|
218
|
+
// Then replaying that click submits it; don't submit twice.
|
|
219
|
+
const last = rec.events[rec.events.length - 1]
|
|
220
|
+
ev.viaClick = !!(last && last.type === "click" && last.t === clock.now)
|
|
221
|
+
}
|
|
222
|
+
// For the dock's markers: what was acted on, readably.
|
|
223
|
+
if (e.type === "focusin" && isEditable(e.target)) {
|
|
224
|
+
ev.css = selectorOf(e.target)
|
|
225
|
+
ev.label = labelOf(e.target)
|
|
226
|
+
ev.editable = 1
|
|
227
|
+
}
|
|
228
|
+
if (e.type === "click" || e.type === "keydown" || e.type === "input" || e.type === "change" || e.type === "submit") {
|
|
229
|
+
const el = e.target && e.target.nodeType === 1 ? e.target : null
|
|
230
|
+
if (el) {
|
|
231
|
+
ev.css = selectorOf(el)
|
|
232
|
+
ev.label = e.type === "keydown" ? e.key : labelOf(el)
|
|
233
|
+
if (e.type === "keydown") ev.inField = !!(el.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName))
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (e.type === "scroll") {
|
|
237
|
+
const el = e.target === document ? document.scrollingElement : e.target
|
|
238
|
+
ev.path = e.target === document ? "d" : ev.path
|
|
239
|
+
ev.top = el.scrollTop
|
|
240
|
+
ev.left = el.scrollLeft
|
|
241
|
+
}
|
|
242
|
+
return ev
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ---- view-only input and scrolling while paused ----------------------------------
|
|
246
|
+
// Scroll positions as the recording has them (live recording and replay), and
|
|
247
|
+
// the elements the user scrolled just to look while paused. Those go back to
|
|
248
|
+
// the recorded position when playback resumes or on a seek.
|
|
249
|
+
const scrollRec = new Map() // path key -> { top, left }
|
|
250
|
+
const viewScrolled = new Map() // path key -> path
|
|
251
|
+
const restoredScroll = new Map() // path key -> { top, left } just set by the runtime itself
|
|
252
|
+
const keyOf = (path) => (Array.isArray(path) ? path.join(",") : String(path))
|
|
253
|
+
// Every recorded scroll over time, per scroller (for a preview of an earlier
|
|
254
|
+
// moment): { path, ref (the element), ts, tops, lefts }.
|
|
255
|
+
const scrollLog = new Map()
|
|
256
|
+
|
|
257
|
+
function noteScroll(ev, el) {
|
|
258
|
+
scrollRec.set(keyOf(ev.path), { top: ev.top, left: ev.left })
|
|
259
|
+
logScroll(ev.path, el, ev.top, ev.left)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function logScroll(path, el, top, left) {
|
|
263
|
+
const key = keyOf(path)
|
|
264
|
+
let L = scrollLog.get(key)
|
|
265
|
+
if (!L) scrollLog.set(key, (L = { path, ref: null, ts: [], tops: [], lefts: [] }))
|
|
266
|
+
if (el) L.ref = typeof WeakRef === "function" ? new WeakRef(el) : { deref: () => el }
|
|
267
|
+
const n = L.ts.length
|
|
268
|
+
if (n && L.ts[n - 1] === clock.now) {
|
|
269
|
+
L.tops[n - 1] = top
|
|
270
|
+
L.lefts[n - 1] = left
|
|
271
|
+
} else if (!n || L.tops[n - 1] !== top || L.lefts[n - 1] !== left) {
|
|
272
|
+
L.ts.push(clock.now)
|
|
273
|
+
L.tops.push(top)
|
|
274
|
+
L.lefts.push(left)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Where a scroller was at t on the recording (0 before it first scrolled).
|
|
279
|
+
function scrollAt(L, t) {
|
|
280
|
+
let lo = 0
|
|
281
|
+
let hi = L.ts.length
|
|
282
|
+
while (lo < hi) {
|
|
283
|
+
const mid = (lo + hi) >> 1
|
|
284
|
+
if (L.ts[mid] <= t) lo = mid + 1
|
|
285
|
+
else hi = mid
|
|
286
|
+
}
|
|
287
|
+
return lo ? { top: L.tops[lo - 1], left: L.lefts[lo - 1] } : { top: 0, left: 0 }
|
|
288
|
+
}
|
|
289
|
+
const scrollerOf = (L) => {
|
|
290
|
+
const el = L.path === "d" ? document.scrollingElement : L.ref && L.ref.deref()
|
|
291
|
+
return el && el.isConnected ? el : null
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function scrollElOf(path) {
|
|
295
|
+
if (path === "d") return document.scrollingElement
|
|
296
|
+
const n = resolvePath(path)
|
|
297
|
+
return n && n.nodeType === 1 ? n : null
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Scroll there at once, whatever the page's `scroll-behavior` (a smooth
|
|
301
|
+
// scroll set while replaying never lands, F32). Returns where it got to (the
|
|
302
|
+
// browser clamps to what's scrollable).
|
|
303
|
+
function setScroll(el, top, left) {
|
|
304
|
+
try {
|
|
305
|
+
el.scrollTo({ top, left, behavior: "instant" })
|
|
306
|
+
} catch {
|
|
307
|
+
el.scrollTop = top
|
|
308
|
+
el.scrollLeft = left
|
|
309
|
+
}
|
|
310
|
+
return { top: el.scrollTop, left: el.scrollLeft }
|
|
311
|
+
}
|
|
312
|
+
// The runtime moving a scroller itself: the scroll event that follows isn't the user's.
|
|
313
|
+
function setOwnScroll(key, el, top, left) {
|
|
314
|
+
const t0 = el.scrollTop
|
|
315
|
+
const l0 = el.scrollLeft
|
|
316
|
+
const r = setScroll(el, top, left)
|
|
317
|
+
if (r.top !== t0 || r.left !== l0) restoredScroll.set(key, r)
|
|
318
|
+
return r
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// What the user has scrolled to look while paused, to carry into the frame
|
|
322
|
+
// that replaces this one (the dock copies it across, see swapIn).
|
|
323
|
+
function viewScroll() {
|
|
324
|
+
const out = []
|
|
325
|
+
for (const [key, path] of viewScrolled) {
|
|
326
|
+
const el = scrollElOf(path)
|
|
327
|
+
if (el) out.push({ key, path, top: el.scrollTop, left: el.scrollLeft })
|
|
328
|
+
}
|
|
329
|
+
return out
|
|
330
|
+
}
|
|
331
|
+
// ...and taken here: the view stays where the user had it. Play still puts
|
|
332
|
+
// the recording's scroll back (each one that differs counts as looked-at).
|
|
333
|
+
function applyView(list) {
|
|
334
|
+
let view = []
|
|
335
|
+
try {
|
|
336
|
+
view = JSON.parse(JSON.stringify(list || []))
|
|
337
|
+
} catch {}
|
|
338
|
+
for (const v of view) {
|
|
339
|
+
const el = scrollElOf(v.path)
|
|
340
|
+
if (!el) continue
|
|
341
|
+
const key = keyOf(v.path)
|
|
342
|
+
setOwnScroll(key, el, v.top, v.left)
|
|
343
|
+
const r = scrollRec.get(key) || { top: 0, left: 0 }
|
|
344
|
+
if (r.top !== v.top || r.left !== v.left) viewScrolled.set(key, v.path)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Pausing hands focus to the dock (the app is view-only); playing gives the
|
|
349
|
+
// app's focused element (and its caret) back.
|
|
350
|
+
function restoreFocus() {
|
|
351
|
+
const f = focused()
|
|
352
|
+
if (!f || f === document.body) return
|
|
353
|
+
try {
|
|
354
|
+
if (W.frameElement && W.parent.document.activeElement !== W.frameElement) W.focus()
|
|
355
|
+
} catch {}
|
|
356
|
+
if (realActive.call(document) !== f) {
|
|
357
|
+
try {
|
|
358
|
+
dispatching++ // our own focus change isn't input to record
|
|
359
|
+
f.focus({ preventScroll: true })
|
|
360
|
+
} catch {} finally {
|
|
361
|
+
dispatching--
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function restoreScroll() {
|
|
367
|
+
for (const [key, path] of viewScrolled) {
|
|
368
|
+
const el = scrollElOf(path)
|
|
369
|
+
if (!el) continue
|
|
370
|
+
const r = scrollRec.get(key) || { top: 0, left: 0 }
|
|
371
|
+
if (el.scrollTop !== r.top || el.scrollLeft !== r.left) setOwnScroll(key, el, r.top, r.left)
|
|
372
|
+
}
|
|
373
|
+
viewScrolled.clear()
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// The scroll event that restoreScroll itself causes isn't the user's.
|
|
377
|
+
function ignoreRestoredScroll(e) {
|
|
378
|
+
const path = e.target === document ? "d" : pathOf(e.target)
|
|
379
|
+
const key = keyOf(path)
|
|
380
|
+
const r = restoredScroll.get(key)
|
|
381
|
+
if (!r) return false
|
|
382
|
+
restoredScroll.delete(key)
|
|
383
|
+
const el = scrollElOf(path)
|
|
384
|
+
return !!el && el.scrollTop === r.top && el.scrollLeft === r.left
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function isEditable(el) {
|
|
388
|
+
return !!el && el.nodeType === 1 && (el.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName))
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// The user's last scroll gesture (wheel, touch, a scroll key, a press on a
|
|
392
|
+
// scrollbar): a scroll counts as theirs only shortly after one. A page that
|
|
393
|
+
// moves because a preview changed it underneath isn't the user looking.
|
|
394
|
+
let lastGesture = -1e9
|
|
395
|
+
const GESTURES = new Set(["wheel", "touchstart", "touchmove", "pointerdown", "mousedown"])
|
|
396
|
+
|
|
397
|
+
function viewOnly(e) {
|
|
398
|
+
if (GESTURES.has(e.type) || (e.type === "keydown" && SCROLL_KEYS.has(e.key))) lastGesture = real.perfNow()
|
|
399
|
+
if (e.type === "scroll") {
|
|
400
|
+
// Looking around: the browser scrolls, the app doesn't hear of it, and
|
|
401
|
+
// it isn't part of the recording.
|
|
402
|
+
if (!ignoreRestoredScroll(e) && real.perfNow() - lastGesture < 500) {
|
|
403
|
+
const path = e.target === document ? "d" : pathOf(e.target)
|
|
404
|
+
if (path) viewScrolled.set(keyOf(path), path)
|
|
405
|
+
}
|
|
406
|
+
e.stopImmediatePropagation()
|
|
407
|
+
return
|
|
408
|
+
}
|
|
409
|
+
if (e.type === "wheel" || e.type === "touchstart" || e.type === "touchmove" || e.type === "touchend") {
|
|
410
|
+
e.stopImmediatePropagation() // default (scrolling) still happens
|
|
411
|
+
return
|
|
412
|
+
}
|
|
413
|
+
if (e.type === "keydown" || e.type === "keyup" || e.type === "keypress") {
|
|
414
|
+
// The dock drives space/arrows/F/+ while focus is in the app.
|
|
415
|
+
let handled = false
|
|
416
|
+
if (e.type === "keydown" && shell && typeof shell.key === "function") {
|
|
417
|
+
try {
|
|
418
|
+
handled = !!shell.key(e)
|
|
419
|
+
} catch {}
|
|
420
|
+
}
|
|
421
|
+
if (!handled && SCROLL_KEYS.has(e.key) && !isEditable(e.target)) {
|
|
422
|
+
e.stopImmediatePropagation() // keyboard scrolling still works
|
|
423
|
+
return
|
|
424
|
+
}
|
|
425
|
+
return block(e)
|
|
426
|
+
}
|
|
427
|
+
return block(e)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function block(e) {
|
|
431
|
+
e.stopImmediatePropagation()
|
|
432
|
+
if (e.cancelable && e.type !== "scroll") e.preventDefault()
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// A page that has just loaded live (the recording's first page, or one the app
|
|
436
|
+
// reloaded itself into: a Vite full reload, location.reload(), a navigation)
|
|
437
|
+
// and whose clock hasn't run yet: a scroll nobody gestured for is the browser
|
|
438
|
+
// putting the page back where it was (scroll restoration on a reload), or the
|
|
439
|
+
// app's own scroll as it loads. That's how this page starts, so it goes into
|
|
440
|
+
// the recording at the page's first moment, and a rebuild of the page replays
|
|
441
|
+
// it (else every rebuild after a reload would land at the top). Like any
|
|
442
|
+
// replayed scroll, the app doesn't hear it.
|
|
443
|
+
let clockRan = false // this document's clock has run (play())
|
|
444
|
+
function startingScroll(e) {
|
|
445
|
+
if (e.type !== "scroll" || clockRan || clock.seeking || previewing || !rec || rec.start == null || hasFuture()) return false
|
|
446
|
+
if (pending && !pending.reloaded && !pending.continue) return false // a rebuild: its scroll is replayed
|
|
447
|
+
if (real.perfNow() - lastGesture < 500) return false // the user's: view-only (paused)
|
|
448
|
+
e.stopImmediatePropagation()
|
|
449
|
+
if (ignoreRestoredScroll(e)) return true
|
|
450
|
+
const ev = serialize(e)
|
|
451
|
+
if (ev.path) {
|
|
452
|
+
noteScroll(ev, e.target === document ? document.scrollingElement : e.target)
|
|
453
|
+
recordEvent(ev)
|
|
454
|
+
}
|
|
455
|
+
return true
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function onInput(e) {
|
|
459
|
+
if (!e.isTrusted || dispatching || !rec) return
|
|
460
|
+
// Holding ⌘ is the dock's "pick an element" gesture, never app input.
|
|
461
|
+
// (The dock may still be wiring these up while the prototype boots.)
|
|
462
|
+
if (shell && shell.meta && e.key === "Meta") return shell.meta(e.type === "keydown")
|
|
463
|
+
if (shell && shell.pointer && e.type === "pointermove") shell.pointer(e.clientX, e.clientY, e.metaKey)
|
|
464
|
+
// A click in the prototype folds away an open note, like a click anywhere else.
|
|
465
|
+
if (shell && shell.appPointerDown && e.type === "pointerdown" && !shell.inspecting) shell.appPointerDown()
|
|
466
|
+
// Comment mode: the dock is picking an element. The app sees nothing.
|
|
467
|
+
if (shell && shell.inspecting) {
|
|
468
|
+
shell.inspect(e)
|
|
469
|
+
e.stopImmediatePropagation()
|
|
470
|
+
if (e.cancelable && e.type !== "scroll" && e.type !== "input") e.preventDefault()
|
|
471
|
+
return
|
|
472
|
+
}
|
|
473
|
+
// A dock tool (comment/select) is on: the app sees nothing.
|
|
474
|
+
if (toolActive) return block(e)
|
|
475
|
+
if ((e.type === "keydown" || e.type === "keyup") && PT.shortcut && PT.shortcut(e)) return
|
|
476
|
+
if (startingScroll(e)) return
|
|
477
|
+
// Input during a rewind would land at the wrong moment; drop it. A frame
|
|
478
|
+
// being built behind can have the window's focus (its replay focused a
|
|
479
|
+
// field), so its keys still go to the dock (space, arrows, +).
|
|
480
|
+
if (clock.seeking) {
|
|
481
|
+
if (e.type === "keydown" && shell && typeof shell.key === "function") {
|
|
482
|
+
try {
|
|
483
|
+
shell.key(e)
|
|
484
|
+
} catch {}
|
|
485
|
+
}
|
|
486
|
+
return block(e)
|
|
487
|
+
}
|
|
488
|
+
// While a past moment is on show (or being rebuilt), the page is a picture
|
|
489
|
+
// you can still scroll around in.
|
|
490
|
+
if (previewing || (shell && shell.rebuilding)) return viewOnly(e)
|
|
491
|
+
// Paused, or in the past (rewound, or playing the recorded future back):
|
|
492
|
+
// the app is view-only, like a paused video. Nothing it gets reaches it or
|
|
493
|
+
// moves time, except that you can scroll around to look (CONTRACT.md).
|
|
494
|
+
// Only Play starts the clock.
|
|
495
|
+
if (!clock.playing || hasFuture()) return viewOnly(e)
|
|
496
|
+
// A mousemove right after the pointermove it mirrors is one event.
|
|
497
|
+
// (It may come after the pointerover/out events the same move fired.)
|
|
498
|
+
if (e.type === "mousemove") {
|
|
499
|
+
for (let i = rec.events.length - 1, n = 0; i >= 0 && n < 8; i--, n++) {
|
|
500
|
+
const ev = rec.events[i]
|
|
501
|
+
if (ev.t !== clock.now) break
|
|
502
|
+
if (ev.type === "pointermove") {
|
|
503
|
+
if (Math.abs((ev.clientX || 0) - e.clientX) < 1 && Math.abs((ev.clientY || 0) - e.clientY) < 1 && !ev.mm) {
|
|
504
|
+
ev.mm = 1
|
|
505
|
+
return
|
|
506
|
+
}
|
|
507
|
+
break
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
// mouseover/mouseout mirror the pointerover/pointerout just before them:
|
|
512
|
+
// folded in the same way (replayed right after their pointer twin).
|
|
513
|
+
if (e.type === "mouseover" || e.type === "mouseout") {
|
|
514
|
+
const twin = e.type === "mouseover" ? "pointerover" : "pointerout"
|
|
515
|
+
const path = pathOf(e.target)
|
|
516
|
+
for (let i = rec.events.length - 1, n = 0; i >= 0 && n < 6; i--, n++) {
|
|
517
|
+
const ev = rec.events[i]
|
|
518
|
+
if (ev.t !== clock.now) break
|
|
519
|
+
if (ev.type === twin) {
|
|
520
|
+
if (!ev.mo && ev.path + "" === path + "") {
|
|
521
|
+
ev.mo = 1
|
|
522
|
+
return
|
|
523
|
+
}
|
|
524
|
+
break
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
// Wheel ticks within one frame add up to one event.
|
|
529
|
+
if (e.type === "wheel") {
|
|
530
|
+
const last = rec.events[rec.events.length - 1]
|
|
531
|
+
if (last && last.type === "wheel" && last.t === clock.now && last.path + "" === pathOf(e.target) + "") {
|
|
532
|
+
last.deltaX = (last.deltaX || 0) + e.deltaX
|
|
533
|
+
last.deltaY = (last.deltaY || 0) + e.deltaY
|
|
534
|
+
return
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
if (e.type === "focus" || e.type === "blur") return // focusin/focusout carry these
|
|
538
|
+
if (e.type === "focusin") vFocus = e.target
|
|
539
|
+
if (e.type === "focusout" && vFocus === e.target) vFocus = null
|
|
540
|
+
if (e.type === "scroll" && ignoreRestoredScroll(e)) return
|
|
541
|
+
appRan = true // the app is handling input: it may start animations
|
|
542
|
+
const ev = serialize(e)
|
|
543
|
+
if (ev.type === "scroll" && ev.path) noteScroll(ev, e.target === document ? document.scrollingElement : e.target)
|
|
544
|
+
if (ev.path) {
|
|
545
|
+
trace("input", ev.type)
|
|
546
|
+
const before = rec.events.length
|
|
547
|
+
// Fired by the browser in the same task as the previous input event?
|
|
548
|
+
if (inputTask && before && rec.events[before - 1].t === clock.now) ev.g = 1
|
|
549
|
+
recordEvent(ev)
|
|
550
|
+
markInputTask()
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
for (const type of [...POINTER, ...MOUSE, ...KEYS, ...TOUCH, ...DRAG, ...CLIP, ...COMPOSE, ...OTHER]) {
|
|
555
|
+
W.addEventListener(type, onInput, { capture: true, passive: false })
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Another frame of this app (one being built behind, a checkpoint) puts the
|
|
559
|
+
// origin's storage back to its own moment: an app that isn't running doesn't
|
|
560
|
+
// hear of it.
|
|
561
|
+
W.addEventListener("storage", (e) => (!clock.playing || clock.seeking) && e.stopImmediatePropagation(), true)
|
|
562
|
+
|
|
563
|
+
// Paused, the app doesn't hear the window resize either (a frame swapped in
|
|
564
|
+
// leaves the size it was built at, and on some pages a resize resets what's
|
|
565
|
+
// drawn); Play hands it one if the size changed meanwhile.
|
|
566
|
+
let heldResize = false
|
|
567
|
+
let heardSize = { w: W.innerWidth, h: W.innerHeight }
|
|
568
|
+
W.addEventListener(
|
|
569
|
+
"resize",
|
|
570
|
+
(e) => {
|
|
571
|
+
if (!e.isTrusted) return
|
|
572
|
+
if (clock.playing && !clock.seeking) {
|
|
573
|
+
heardSize = { w: W.innerWidth, h: W.innerHeight }
|
|
574
|
+
return
|
|
575
|
+
}
|
|
576
|
+
heldResize = true
|
|
577
|
+
e.stopImmediatePropagation()
|
|
578
|
+
},
|
|
579
|
+
true,
|
|
580
|
+
)
|
|
581
|
+
function deliverHeldResize() {
|
|
582
|
+
if (!heldResize) return
|
|
583
|
+
heldResize = false
|
|
584
|
+
if (W.innerWidth === heardSize.w && W.innerHeight === heardSize.h) return
|
|
585
|
+
heardSize = { w: W.innerWidth, h: W.innerHeight }
|
|
586
|
+
dispatching++
|
|
587
|
+
try {
|
|
588
|
+
W.dispatchEvent(new Event("resize"))
|
|
589
|
+
} finally {
|
|
590
|
+
dispatching--
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Back/forward (and hash changes) are navigation the app reacts to; they're
|
|
595
|
+
// recorded as one "nav" event with where the page ended up.
|
|
596
|
+
let lastNav = null
|
|
597
|
+
function onNav(e) {
|
|
598
|
+
if (!e.isTrusted || dispatching || !rec || hasFuture() || clock.seeking) return
|
|
599
|
+
const url = location.href
|
|
600
|
+
if (lastNav && lastNav.t === clock.now && lastNav.url === url) {
|
|
601
|
+
if (e.type === "hashchange") lastNav.hash = 1
|
|
602
|
+
return
|
|
603
|
+
}
|
|
604
|
+
let state = null
|
|
605
|
+
try {
|
|
606
|
+
state = JSON.parse(JSON.stringify(history.state))
|
|
607
|
+
} catch {}
|
|
608
|
+
const ev = { type: "nav", path: "w", url, state }
|
|
609
|
+
if (e.type === "hashchange") ev.hash = 1
|
|
610
|
+
recordEvent(ev)
|
|
611
|
+
lastNav = ev
|
|
612
|
+
}
|
|
613
|
+
W.addEventListener("popstate", onNav, true)
|
|
614
|
+
W.addEventListener("hashchange", onNav, true)
|
|
615
|
+
|
|
616
|
+
const setters = new Map()
|
|
617
|
+
function nativeSetter(el, prop) {
|
|
618
|
+
const proto = Object.getPrototypeOf(el)
|
|
619
|
+
const key = proto
|
|
620
|
+
if (!setters.has(key)) setters.set(key, {})
|
|
621
|
+
const cache = setters.get(key)
|
|
622
|
+
if (!(prop in cache)) {
|
|
623
|
+
let p = proto
|
|
624
|
+
let desc = null
|
|
625
|
+
while (p && !(desc = Object.getOwnPropertyDescriptor(p, prop))) p = Object.getPrototypeOf(p)
|
|
626
|
+
cache[prop] = desc && desc.set
|
|
627
|
+
}
|
|
628
|
+
return cache[prop]
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Keys go to whatever has focus now; pointers fall back to what's under the
|
|
632
|
+
// recorded coordinates if the DOM has shifted.
|
|
633
|
+
function findTarget(ev) {
|
|
634
|
+
if (ev.active) {
|
|
635
|
+
const f = focused()
|
|
636
|
+
if (f) return f
|
|
637
|
+
const a = realActive.call(document)
|
|
638
|
+
if (a && a !== document.body) return a
|
|
639
|
+
// Focus got lost altogether: fall back to what the key was pressed in.
|
|
640
|
+
if (ev.css) {
|
|
641
|
+
try {
|
|
642
|
+
const el = document.querySelector(ev.css)
|
|
643
|
+
if (el) return el
|
|
644
|
+
} catch {}
|
|
645
|
+
}
|
|
646
|
+
return a || document.body
|
|
647
|
+
}
|
|
648
|
+
const byPath = resolvePath(ev.path)
|
|
649
|
+
if (byPath) return byPath
|
|
650
|
+
if (ev.clientX == null || HOVER.has(ev.type)) return null
|
|
651
|
+
return document.elementFromPoint(ev.clientX, ev.clientY)
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function dispatchRecorded(ev) {
|
|
655
|
+
if (ev.type === "net") return deliverNet(ev)
|
|
656
|
+
if (ev.type === "fetch") return deliverNet({ list: "fetches", i: ev.i }) // older recordings
|
|
657
|
+
if (ev.type === "async") return settleAsync(ev)
|
|
658
|
+
if (ev.type === "obs") return deliverObserved(ev)
|
|
659
|
+
if (ev.type === "worker") return deliverWorker(ev)
|
|
660
|
+
if (ev.type === "ready") return deliverReady(ev)
|
|
661
|
+
if (ev.type === "script") return deliverScript(ev)
|
|
662
|
+
const target = findTarget(ev)
|
|
663
|
+
// Focus leaving an element that's gone: the browser fired that itself as it
|
|
664
|
+
// removed it (a soft navigation replacing the page), nothing to replay.
|
|
665
|
+
if (!target && ev.type === "focusout") return
|
|
666
|
+
if (!target) {
|
|
667
|
+
// The DOM came out different (code changed?). Say so a few times, not 10,000.
|
|
668
|
+
if (++missingTargets <= 3) console.warn("[retake] replay target missing", ev.type, ev.path, missingTargets === 3 ? "(further ones not shown)" : "")
|
|
669
|
+
return
|
|
670
|
+
}
|
|
671
|
+
trace("input", ev.type)
|
|
672
|
+
dispatching++
|
|
673
|
+
try {
|
|
674
|
+
replayOne(ev, target)
|
|
675
|
+
} finally {
|
|
676
|
+
dispatching--
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function makeTransfer(data) {
|
|
681
|
+
const dt = new DataTransfer()
|
|
682
|
+
for (const [type, value] of Object.entries(data || {})) {
|
|
683
|
+
try {
|
|
684
|
+
dt.setData(type, value)
|
|
685
|
+
} catch {}
|
|
686
|
+
}
|
|
687
|
+
return dt
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function makeTouches(list) {
|
|
691
|
+
return (list || []).map((t) => {
|
|
692
|
+
try {
|
|
693
|
+
const target = resolvePath(t.path) || document.body
|
|
694
|
+
return new Touch({ identifier: t.id, target, clientX: t.x, clientY: t.y, screenX: t.sx, screenY: t.sy, pageX: t.x + scrollX, pageY: t.y + scrollY })
|
|
695
|
+
} catch {
|
|
696
|
+
return null
|
|
697
|
+
}
|
|
698
|
+
}).filter(Boolean)
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Legacy key fields can't go through the KeyboardEvent constructor.
|
|
702
|
+
function withLegacyKeys(event, ev) {
|
|
703
|
+
for (const k of ["keyCode", "which", "charCode"]) {
|
|
704
|
+
const v = ev[k] || 0
|
|
705
|
+
try {
|
|
706
|
+
Object.defineProperty(event, k, { get: () => v })
|
|
707
|
+
} catch {}
|
|
708
|
+
}
|
|
709
|
+
return event
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function replayOne(ev, target) {
|
|
713
|
+
if (ev.type.startsWith("pointer") || ev.type.startsWith("mouse")) replayHover(ev, target)
|
|
714
|
+
const init = { ...DEFAULTS, ...ev, view: W }
|
|
715
|
+
if (ev.so) replayScreenOffset = ev.so
|
|
716
|
+
if (ev.clientX != null || ev.so) {
|
|
717
|
+
if (init.screenX == null) init.screenX = (ev.clientX || 0) + replayScreenOffset[0]
|
|
718
|
+
if (init.screenY == null) init.screenY = (ev.clientY || 0) + replayScreenOffset[1]
|
|
719
|
+
}
|
|
720
|
+
if (init.button == null && /move|over|out|enter|leave/.test(ev.type)) init.button = -1
|
|
721
|
+
if (ev.related) init.relatedTarget = resolvePath(ev.related)
|
|
722
|
+
switch (true) {
|
|
723
|
+
case ev.type === "scroll": {
|
|
724
|
+
const el = target === document ? document.scrollingElement : target
|
|
725
|
+
setScroll(el, ev.top, ev.left)
|
|
726
|
+
noteScroll(ev, el)
|
|
727
|
+
return
|
|
728
|
+
}
|
|
729
|
+
case ev.type === "nav": {
|
|
730
|
+
const moved = location.href !== ev.url
|
|
731
|
+
if (!moved) return // the replayed click already navigated there
|
|
732
|
+
const oldURL = location.href
|
|
733
|
+
history.replaceState(ev.state, "", ev.url)
|
|
734
|
+
W.dispatchEvent(new PopStateEvent("popstate", { state: ev.state }))
|
|
735
|
+
if (ev.hash) W.dispatchEvent(new HashChangeEvent("hashchange", { oldURL, newURL: ev.url }))
|
|
736
|
+
return
|
|
737
|
+
}
|
|
738
|
+
case ev.type === "focusin":
|
|
739
|
+
vFocus = target
|
|
740
|
+
return target.focus && target.focus({ preventScroll: true })
|
|
741
|
+
case ev.type === "focusout":
|
|
742
|
+
if (vFocus === target) vFocus = null
|
|
743
|
+
return realActive.call(document) === target && target.blur()
|
|
744
|
+
case ev.type === "submit": {
|
|
745
|
+
if (ev.viaClick) return // the replayed click submits it
|
|
746
|
+
const submitter = ev.submitter ? resolvePath(ev.submitter) : null
|
|
747
|
+
if (typeof target.requestSubmit === "function") {
|
|
748
|
+
try {
|
|
749
|
+
return submitter ? target.requestSubmit(submitter) : target.requestSubmit()
|
|
750
|
+
} catch {
|
|
751
|
+
return target.requestSubmit()
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return target.dispatchEvent(new SubmitEvent("submit", { bubbles: true, cancelable: true }))
|
|
755
|
+
}
|
|
756
|
+
case ev.type === "input" || ev.type === "change": {
|
|
757
|
+
// A checkbox or radio is toggled by the replayed click, which fires its
|
|
758
|
+
// own input/change; only step in if the state didn't come out the same.
|
|
759
|
+
if (target.type === "checkbox" || target.type === "radio") {
|
|
760
|
+
if (!!target.checked === !!ev.checked) return
|
|
761
|
+
nativeSetter(target, "checked").call(target, !!ev.checked)
|
|
762
|
+
} else if (target.isContentEditable) target.innerHTML = ev.value
|
|
763
|
+
else if (ev.value != null) nativeSetter(target, "value").call(target, ev.value)
|
|
764
|
+
if (ev.range && target.setSelectionRange) {
|
|
765
|
+
try {
|
|
766
|
+
target.setSelectionRange(ev.range[0], ev.range[1])
|
|
767
|
+
} catch {}
|
|
768
|
+
}
|
|
769
|
+
const E = ev.type === "input" ? InputEvent : Event
|
|
770
|
+
return target.dispatchEvent(new E(ev.type, init))
|
|
771
|
+
}
|
|
772
|
+
case ev.type === "beforeinput":
|
|
773
|
+
return target.dispatchEvent(new InputEvent("beforeinput", init))
|
|
774
|
+
case POINTER.includes(ev.type): {
|
|
775
|
+
target.dispatchEvent(new PointerEvent(ev.type, init))
|
|
776
|
+
if (ev.mm) target.dispatchEvent(new MouseEvent("mousemove", init))
|
|
777
|
+
if (ev.mo) target.dispatchEvent(new MouseEvent(ev.type === "pointerover" ? "mouseover" : "mouseout", init))
|
|
778
|
+
return
|
|
779
|
+
}
|
|
780
|
+
case ev.type === "wheel":
|
|
781
|
+
return target.dispatchEvent(new WheelEvent("wheel", init))
|
|
782
|
+
case MOUSE.includes(ev.type):
|
|
783
|
+
return target.dispatchEvent(new MouseEvent(ev.type, init))
|
|
784
|
+
case KEYS.includes(ev.type):
|
|
785
|
+
return target.dispatchEvent(withLegacyKeys(new KeyboardEvent(ev.type, init), ev))
|
|
786
|
+
case TOUCH.includes(ev.type):
|
|
787
|
+
return target.dispatchEvent(new TouchEvent(ev.type, { ...init, touches: makeTouches(ev.touches), targetTouches: makeTouches(ev.targetTouches), changedTouches: makeTouches(ev.changedTouches) }))
|
|
788
|
+
case DRAG.includes(ev.type):
|
|
789
|
+
return target.dispatchEvent(new DragEvent(ev.type, { ...init, dataTransfer: makeTransfer(ev.dt) }))
|
|
790
|
+
case CLIP.includes(ev.type):
|
|
791
|
+
return target.dispatchEvent(new ClipboardEvent(ev.type, { ...init, clipboardData: makeTransfer(ev.dt) }))
|
|
792
|
+
case COMPOSE.includes(ev.type):
|
|
793
|
+
return target.dispatchEvent(new CompositionEvent(ev.type, init))
|
|
794
|
+
}
|
|
795
|
+
}
|