effect-browser 0.3.0-beta.15 → 0.3.0-beta.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -6
- package/dist/{Browser-BBSySyjT.mjs → Browser-H63wiekr.mjs} +5 -5
- package/dist/Browser-H63wiekr.mjs.map +1 -0
- package/dist/Browser.d.mts +2 -2
- package/dist/Browser.mjs +1 -1
- package/dist/{BrowserEvent-BRu2juEz.d.mts → BrowserEvent-dq6FRT8w.d.mts} +9 -9
- package/dist/BrowserEvent.d.mts +1 -1
- package/dist/BrowserEvent.mjs +7 -12
- package/dist/BrowserEvent.mjs.map +1 -1
- package/dist/Cdp.mjs +2 -2
- package/dist/{Change-iMlcuzVD.d.mts → Change-DqING3tr.d.mts} +2 -2
- package/dist/Change.d.mts +1 -1
- package/dist/Chromium.mjs +2 -2
- package/dist/Expect.d.mts +1 -1
- package/dist/Expect.mjs +2 -2
- package/dist/Moment.d.mts +1 -1
- package/dist/Moment.mjs +3 -3
- package/dist/Moment.mjs.map +1 -1
- package/dist/Motion-DND6I-64.d.mts +40 -0
- package/dist/Motion.d.mts +2 -40
- package/dist/Motion.mjs +1 -2
- package/dist/Motion.mjs.map +1 -1
- package/dist/{Page-B5cjaUp5.mjs → Page-CfB6Zn1p.mjs} +7 -15
- package/dist/Page-CfB6Zn1p.mjs.map +1 -0
- package/dist/{Page-DL9eylp3.d.mts → Page-ku6j5t-v.d.mts} +23 -25
- package/dist/Page.d.mts +1 -1
- package/dist/Page.mjs +1 -1
- package/dist/{Plan-CXA2BW-G.mjs → Plan-C-d5QYS8.mjs} +6 -9
- package/dist/Plan-C-d5QYS8.mjs.map +1 -0
- package/dist/{Plan-6As6vcwT.d.mts → Plan-DJ4hEEna.d.mts} +3 -3
- package/dist/Plan.d.mts +1 -1
- package/dist/Plan.mjs +1 -1
- package/dist/Policy.d.mts +1 -1
- package/dist/Policy.mjs +1 -1
- package/dist/Presentation.d.mts +2 -2
- package/dist/Presentation.mjs +3 -2
- package/dist/Presentation.mjs.map +1 -1
- package/dist/Stage.d.mts +1 -1
- package/dist/Stage.mjs +8 -7
- package/dist/Stage.mjs.map +1 -1
- package/dist/{Supervisor-DaGlQVjv.mjs → Supervisor-2mPjIYYs.mjs} +44 -19
- package/dist/Supervisor-2mPjIYYs.mjs.map +1 -0
- package/dist/{Supervisor-5tZ47hR5.d.mts → Supervisor-mVPeEUp8.d.mts} +12 -5
- package/dist/Supervisor.d.mts +1 -1
- package/dist/Supervisor.mjs +1 -1
- package/dist/{context-BAjRj7Xo.mjs → context-w3NJPRLo.mjs} +81 -9
- package/dist/context-w3NJPRLo.mjs.map +1 -0
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +4 -4
- package/dist/match.inpage-xOe9LNO0.mjs.map +1 -1
- package/dist/{page-B92QnRfT.mjs → page-BKwfF6z-.mjs} +213 -218
- package/dist/page-BKwfF6z-.mjs.map +1 -0
- package/package.json +1 -1
- package/src/Browser.ts +1 -1
- package/src/BrowserEvent.ts +7 -12
- package/src/Moment.ts +3 -6
- package/src/Motion.ts +1 -2
- package/src/Page.ts +18 -20
- package/src/Plan.ts +1 -2
- package/src/Presentation.ts +2 -1
- package/src/Stage.ts +10 -17
- package/src/Supervisor.ts +9 -9
- package/src/internal/input/actions.ts +53 -97
- package/src/internal/input/dispatch.ts +25 -11
- package/src/internal/input/edit.inpage.ts +26 -11
- package/src/internal/input/evidence.inpage.ts +1 -21
- package/src/internal/input/guard.inpage.ts +9 -29
- package/src/internal/input/guard.ts +15 -10
- package/src/internal/input/perform.ts +14 -10
- package/src/internal/input/pointer.ts +3 -6
- package/src/internal/input/replies.ts +19 -8
- package/src/internal/input/settle.ts +3 -2
- package/src/internal/input/targets.inpage.ts +6 -13
- package/src/internal/input/targets.ts +13 -32
- package/src/internal/page/bridge.ts +2 -2
- package/src/internal/page/context.ts +15 -3
- package/src/internal/page/navigation.ts +3 -12
- package/src/internal/pictures/capture.ts +11 -4
- package/src/internal/pictures/clock.ts +8 -3
- package/src/internal/pictures/motion.ts +65 -0
- package/src/internal/pictures/pictures.ts +22 -17
- package/src/internal/pictures/utilsBundle.d.ts +9 -0
- package/src/internal/reading/controls.inpage.ts +8 -1
- package/src/internal/reading/match.inpage.ts +2 -1
- package/src/internal/reading/outline.inpage.ts +7 -4
- package/src/internal/reading/reading.ts +10 -25
- package/src/internal/reading/ready.ts +66 -14
- package/src/internal/supervisor/lifecycle.ts +56 -13
- package/src/internal/timeline/events.ts +5 -8
- package/src/internal/timeline/window.ts +3 -8
- package/dist/Browser-BBSySyjT.mjs.map +0 -1
- package/dist/Page-B5cjaUp5.mjs.map +0 -1
- package/dist/Plan-CXA2BW-G.mjs.map +0 -1
- package/dist/Supervisor-DaGlQVjv.mjs.map +0 -1
- package/dist/context-BAjRj7Xo.mjs.map +0 -1
- package/dist/page-B92QnRfT.mjs.map +0 -1
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { Duration, Effect } from "effect";
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { NotActionable, StaleRef } from "../../BrowserError.ts";
|
|
10
10
|
import type { ClickOptions, PressOptions, ScrollOptions, Target, TypeOptions } from "../../Page.ts";
|
|
11
11
|
import { type Bridge, scriptCall } from "../page/bridge.ts";
|
|
12
|
-
import { decodeWith, failWith, type PageContext } from "../page/context.ts";
|
|
12
|
+
import { decodeWith, failWith, type PageContext, refuse } from "../page/context.ts";
|
|
13
13
|
import type { Viewport } from "../page/viewport.ts";
|
|
14
14
|
import * as Dispatch from "./dispatch.ts";
|
|
15
|
+
import { buttonAt } from "./dispatch.ts";
|
|
15
16
|
import * as Script from "./edit.inpage.ts";
|
|
16
17
|
import * as Guard from "./guard.ts";
|
|
17
18
|
import * as Keyboard from "./keyboard.ts";
|
|
@@ -22,8 +23,6 @@ import * as Settle from "./settle.ts";
|
|
|
22
23
|
import { plain, type Style } from "./style.ts";
|
|
23
24
|
import * as Targets from "./targets.ts";
|
|
24
25
|
|
|
25
|
-
const buttonMask = { none: 0, left: 1, right: 2, middle: 4 } as const;
|
|
26
|
-
|
|
27
26
|
const partsOf = (page: PageContext, bridge: Bridge, viewport: Viewport) => {
|
|
28
27
|
const dispatch = Dispatch.make(page);
|
|
29
28
|
const pointer = Pointer.make(page, dispatch, viewport);
|
|
@@ -32,12 +31,11 @@ const partsOf = (page: PageContext, bridge: Bridge, viewport: Viewport) => {
|
|
|
32
31
|
|
|
33
32
|
return {
|
|
34
33
|
page,
|
|
35
|
-
bridge,
|
|
36
34
|
...settling,
|
|
37
35
|
viewportFor: viewport.viewportFor,
|
|
38
36
|
perform: Perform.make(page, dispatch),
|
|
39
37
|
...guard,
|
|
40
|
-
...Targets.make(
|
|
38
|
+
...Targets.make(viewport, guard, pointer, dispatch, settling),
|
|
41
39
|
...pointer,
|
|
42
40
|
...Keyboard.make(page, bridge, dispatch),
|
|
43
41
|
sendMouse: dispatch.sendMouse,
|
|
@@ -53,25 +51,23 @@ const finite = (...values: ReadonlyArray<number | undefined>) =>
|
|
|
53
51
|
|
|
54
52
|
const named = (target: Target) => (typeof target === "string" ? target : `${target.x},${target.y}`);
|
|
55
53
|
|
|
54
|
+
/** The ref a target names, if it names one rather than a point. */
|
|
55
|
+
const refOf = (target: Target | undefined) => (typeof target === "string" ? target : undefined);
|
|
56
|
+
|
|
56
57
|
/** A click's options, once its counts are finite. */
|
|
57
58
|
const clickable = (options: ClickOptions) =>
|
|
58
59
|
finite(options.clickCount, options.holdMillis)
|
|
59
60
|
? Effect.void
|
|
60
|
-
:
|
|
61
|
+
: refuse("click", "clickCount and holdMillis must be finite");
|
|
61
62
|
|
|
62
63
|
/** The keys a press sends, once they and its options are valid. */
|
|
63
64
|
const pressed = (keys: string, options: PressOptions) => {
|
|
64
65
|
const parts = Keys.parts(keys);
|
|
65
66
|
|
|
66
67
|
if (!finite(options.times, options.holdMillis))
|
|
67
|
-
return
|
|
68
|
+
return refuse("press", "times and holdMillis must be finite");
|
|
68
69
|
if (parts === undefined)
|
|
69
|
-
return
|
|
70
|
-
"press",
|
|
71
|
-
new InvalidRequest({
|
|
72
|
-
detail: `"${keys}" is not a key; try Enter, Space, ArrowLeft or Control+A`,
|
|
73
|
-
}),
|
|
74
|
-
);
|
|
70
|
+
return refuse("press", `"${keys}" is not a key; try Enter, Space, ArrowLeft or Control+A`);
|
|
75
71
|
|
|
76
72
|
return Effect.succeed(parts);
|
|
77
73
|
};
|
|
@@ -93,11 +89,7 @@ const click = (input: Parts) => {
|
|
|
93
89
|
},
|
|
94
90
|
settings.actionTimeout,
|
|
95
91
|
Effect.suspend(() => clickable(clickOptions)).pipe(
|
|
96
|
-
Effect.andThen(
|
|
97
|
-
preparePolicy("click", { target: typeof target === "string" ? target : undefined }, [
|
|
98
|
-
target,
|
|
99
|
-
]),
|
|
100
|
-
),
|
|
92
|
+
Effect.andThen(preparePolicy("click", { target: refOf(target) }, [target])),
|
|
101
93
|
),
|
|
102
94
|
(marks, approval) =>
|
|
103
95
|
Effect.gen(function* () {
|
|
@@ -110,16 +102,19 @@ const click = (input: Parts) => {
|
|
|
110
102
|
yield* marks.at(point);
|
|
111
103
|
yield* marks.on(resolved);
|
|
112
104
|
yield* moveTo("click", marks, point, resolved.cursor);
|
|
113
|
-
|
|
105
|
+
yield* Guard.check(approval, { presses: [{ index: 0, ...point }] });
|
|
114
106
|
const before = mark();
|
|
115
107
|
|
|
116
108
|
yield* marks.sent;
|
|
117
109
|
for (let index = 1; index <= count; index++) {
|
|
118
110
|
// An earlier click of a multi-click can replace the approved control under the
|
|
119
111
|
// pointer. Under a guard, its handlers run before the next press is checked and sent.
|
|
120
|
-
if (
|
|
121
|
-
yield*
|
|
122
|
-
|
|
112
|
+
if (index > 1)
|
|
113
|
+
yield* Guard.check(
|
|
114
|
+
approval,
|
|
115
|
+
{ presses: [{ index: 0, ...point }] },
|
|
116
|
+
flush("click", marks.input),
|
|
117
|
+
).pipe(
|
|
123
118
|
Effect.catchIf(
|
|
124
119
|
(error) => error.reason._tag === "StaleRef",
|
|
125
120
|
() =>
|
|
@@ -132,27 +127,14 @@ const click = (input: Parts) => {
|
|
|
132
127
|
),
|
|
133
128
|
),
|
|
134
129
|
);
|
|
135
|
-
|
|
136
|
-
yield* sendMouse("click", marks.input, {
|
|
137
|
-
type: "mousePressed",
|
|
138
|
-
...point,
|
|
139
|
-
button,
|
|
140
|
-
buttons: buttonMask[button],
|
|
141
|
-
clickCount: index,
|
|
142
|
-
});
|
|
130
|
+
yield* sendMouse("click", marks.input, buttonAt("mousePressed", point, button, index));
|
|
143
131
|
|
|
144
132
|
// A hold the caller asks for is the click's own; the style's is presentation.
|
|
145
133
|
const hold = clickOptions.holdMillis ?? (yield* marks.style.hold);
|
|
146
134
|
|
|
147
135
|
if (clickOptions.holdMillis === undefined) yield* marks.present(hold);
|
|
148
136
|
if (hold > 0) yield* Effect.sleep(Duration.millis(hold));
|
|
149
|
-
yield* sendMouse("click", marks.input,
|
|
150
|
-
type: "mouseReleased",
|
|
151
|
-
...point,
|
|
152
|
-
button,
|
|
153
|
-
buttons: 0,
|
|
154
|
-
clickCount: index,
|
|
155
|
-
});
|
|
137
|
+
yield* sendMouse("click", marks.input, buttonAt("mouseReleased", point, button, index));
|
|
156
138
|
}
|
|
157
139
|
yield* flush("click", marks.input);
|
|
158
140
|
yield* settle("click", before);
|
|
@@ -172,7 +154,7 @@ const hover = (input: Parts) => {
|
|
|
172
154
|
"hover",
|
|
173
155
|
{ target: named(target) },
|
|
174
156
|
settings.actionTimeout,
|
|
175
|
-
preparePolicy("hover", { target:
|
|
157
|
+
preparePolicy("hover", { target: refOf(target) }, [target]),
|
|
176
158
|
(marks, approval) =>
|
|
177
159
|
Effect.gen(function* () {
|
|
178
160
|
const resolved = yield* targetFor("hover", target, approval, marks);
|
|
@@ -236,29 +218,16 @@ const drag = (input: Parts) => {
|
|
|
236
218
|
yield* moveTo("drag", marks, start.point, start.cursor);
|
|
237
219
|
// Both ends are checked before the button goes down: once it is down, a release cannot
|
|
238
220
|
// be withheld, and a dragged element under the pointer would hide the drop target.
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
],
|
|
245
|
-
});
|
|
246
|
-
yield* marks.sent;
|
|
247
|
-
yield* sendMouse("drag", marks.input, {
|
|
248
|
-
type: "mousePressed",
|
|
249
|
-
...start.point,
|
|
250
|
-
button: "left",
|
|
251
|
-
buttons: 1,
|
|
252
|
-
clickCount: 1,
|
|
221
|
+
yield* Guard.check(approval, {
|
|
222
|
+
presses: [
|
|
223
|
+
{ index: 0, ...start.point },
|
|
224
|
+
{ index: 1, ...end.point },
|
|
225
|
+
],
|
|
253
226
|
});
|
|
227
|
+
yield* marks.sent;
|
|
228
|
+
yield* sendMouse("drag", marks.input, buttonAt("mousePressed", start.point));
|
|
254
229
|
yield* moveTo("drag", marks, end.point, end.cursor, "drag");
|
|
255
|
-
yield* sendMouse("drag", marks.input,
|
|
256
|
-
type: "mouseReleased",
|
|
257
|
-
...end.point,
|
|
258
|
-
button: "left",
|
|
259
|
-
buttons: 0,
|
|
260
|
-
clickCount: 1,
|
|
261
|
-
});
|
|
230
|
+
yield* sendMouse("drag", marks.input, buttonAt("mouseReleased", end.point));
|
|
262
231
|
yield* flush("drag", marks.input);
|
|
263
232
|
}),
|
|
264
233
|
style,
|
|
@@ -267,7 +236,7 @@ const drag = (input: Parts) => {
|
|
|
267
236
|
|
|
268
237
|
const typeText = (input: Parts) => {
|
|
269
238
|
const { settings, now } = input.page;
|
|
270
|
-
const { perform, preparePolicy, mutate, targetFor, moveTo, sendMouse, flush,
|
|
239
|
+
const { perform, preparePolicy, mutate, targetFor, moveTo, sendMouse, flush, evaluate } = input;
|
|
271
240
|
const { currentDocument, sameDocument, keyStroke, typeEvent, mark, settle } = input;
|
|
272
241
|
|
|
273
242
|
return (text: string, typeOptions: TypeOptions = {}, style: Style = plain) =>
|
|
@@ -292,20 +261,15 @@ const typeText = (input: Parts) => {
|
|
|
292
261
|
const into = typeOptions.into;
|
|
293
262
|
|
|
294
263
|
if (into !== undefined && !/^e\d+$/.test(into))
|
|
295
|
-
return yield*
|
|
296
|
-
"type",
|
|
297
|
-
new InvalidRequest({ detail: `"${into}" is not a ref; refs look like e12` }),
|
|
298
|
-
);
|
|
264
|
+
return yield* refuse("type", `"${into}" is not a ref; refs look like e12`);
|
|
299
265
|
|
|
300
266
|
// A typed space or letter can press a focused button or change a control. Refuse
|
|
301
267
|
// those before any input, on every path.
|
|
302
268
|
const asked = scriptCall("typeable", into ?? null, typeOptions.secret === true);
|
|
303
269
|
|
|
304
|
-
const typeable = yield* (
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
: mutate("type", asked, approval)
|
|
308
|
-
).pipe(Effect.flatMap(decodeWith("type", Script.TypeableResultSchema)));
|
|
270
|
+
const typeable = yield* evaluate("type", asked, approval).pipe(
|
|
271
|
+
Effect.flatMap(decodeWith("type", Script.TypeableResultSchema)),
|
|
272
|
+
);
|
|
309
273
|
|
|
310
274
|
if ("error" in typeable)
|
|
311
275
|
return yield* failWith(
|
|
@@ -327,26 +291,27 @@ const typeText = (input: Parts) => {
|
|
|
327
291
|
// Viewers see the pointer click into the field.
|
|
328
292
|
if (marks.style.shown) {
|
|
329
293
|
yield* moveTo("type", marks, target.point, target.cursor);
|
|
330
|
-
|
|
331
|
-
yield* approval.check({ presses: [{ index: 0, ...target.point }] });
|
|
294
|
+
yield* Guard.check(approval, { presses: [{ index: 0, ...target.point }] });
|
|
332
295
|
yield* marks.sent;
|
|
333
296
|
for (const type of ["mousePressed", "mouseReleased"] as const)
|
|
334
|
-
yield* sendMouse("type", marks.input,
|
|
335
|
-
type,
|
|
336
|
-
...target.point,
|
|
337
|
-
button: "left",
|
|
338
|
-
buttons: type === "mousePressed" ? 1 : 0,
|
|
339
|
-
clickCount: 1,
|
|
340
|
-
});
|
|
297
|
+
yield* sendMouse("type", marks.input, buttonAt(type, target.point));
|
|
341
298
|
yield* flush("type", marks.input);
|
|
342
299
|
}
|
|
343
300
|
|
|
344
301
|
// Focusing can run page handlers, including navigation, before the script returns.
|
|
345
302
|
yield* marks.sent;
|
|
346
303
|
|
|
304
|
+
// A page may answer the press with another field of the same name in the field's
|
|
305
|
+
// place, focused, as a typeahead does: the keys follow it, unless a guard approved
|
|
306
|
+
// this field and no other, or the field is secret, as its replacement may not be.
|
|
307
|
+
const follow =
|
|
308
|
+
marks.style.shown && approval === undefined && !secret && target.name !== ""
|
|
309
|
+
? target.name
|
|
310
|
+
: null;
|
|
311
|
+
|
|
347
312
|
const focused = yield* mutate(
|
|
348
313
|
"type",
|
|
349
|
-
scriptCall("focus", into, replace),
|
|
314
|
+
scriptCall("focus", into, replace, follow),
|
|
350
315
|
approval,
|
|
351
316
|
).pipe(Effect.flatMap(decodeWith("type", Script.FocusResultSchema)));
|
|
352
317
|
|
|
@@ -354,7 +319,7 @@ const typeText = (input: Parts) => {
|
|
|
354
319
|
secret ||= focused.secret;
|
|
355
320
|
}
|
|
356
321
|
if (!secret) yield* marks.reveal;
|
|
357
|
-
|
|
322
|
+
yield* Guard.check(approval, { focused: true });
|
|
358
323
|
yield* marks.sent;
|
|
359
324
|
|
|
360
325
|
// A key's handlers can move focus, and later keys then reach a control nobody approved.
|
|
@@ -362,10 +327,7 @@ const typeText = (input: Parts) => {
|
|
|
362
327
|
// handler can split, and keys typed one at a time check that the field still has focus,
|
|
363
328
|
// once the keys before are answered, before each space, which could press a button, and
|
|
364
329
|
// after the last key.
|
|
365
|
-
const focusHeld =
|
|
366
|
-
approval === undefined
|
|
367
|
-
? Effect.void
|
|
368
|
-
: flush("type", marks.input).pipe(Effect.andThen(approval.check({ focused: true })));
|
|
330
|
+
const focusHeld = Guard.check(approval, { focused: true }, flush("type", marks.input));
|
|
369
331
|
|
|
370
332
|
if (text === "" && replace && into !== undefined) {
|
|
371
333
|
yield* sameDocument("type", since);
|
|
@@ -396,7 +358,7 @@ const typeText = (input: Parts) => {
|
|
|
396
358
|
yield* flush("type", marks.input);
|
|
397
359
|
if (typeOptions.submit === true) {
|
|
398
360
|
// Typing can move focus or change the form; Enter goes only to the approved field.
|
|
399
|
-
|
|
361
|
+
yield* Guard.check(approval, { focused: true });
|
|
400
362
|
yield* sameDocument("type", since);
|
|
401
363
|
const before = mark();
|
|
402
364
|
|
|
@@ -447,7 +409,7 @@ const press = (input: Parts) => {
|
|
|
447
409
|
);
|
|
448
410
|
|
|
449
411
|
if ("error" in focused) return yield* Guard.editFailure("press", on, focused);
|
|
450
|
-
|
|
412
|
+
yield* Guard.check(approval, { focused: true });
|
|
451
413
|
}
|
|
452
414
|
const before = mark();
|
|
453
415
|
let due = now();
|
|
@@ -456,10 +418,8 @@ const press = (input: Parts) => {
|
|
|
456
418
|
for (let index = 0; index < times; index++) {
|
|
457
419
|
// A repeated key stays in the document it began in. Under a guard, a repeated Enter
|
|
458
420
|
// or Space must also reach the approved element after the previous press settled.
|
|
459
|
-
if (index > 0 &&
|
|
460
|
-
yield* flush("press", marks.input);
|
|
461
|
-
yield* approval.check({ focused: true });
|
|
462
|
-
}
|
|
421
|
+
if (index > 0 && activates)
|
|
422
|
+
yield* Guard.check(approval, { focused: true }, flush("press", marks.input));
|
|
463
423
|
yield* sameDocument("press", since);
|
|
464
424
|
yield* keyStroke("press", marks.input, parts, hold);
|
|
465
425
|
if (index + 1 < times) {
|
|
@@ -488,7 +448,7 @@ const scroll = (input: Parts) => {
|
|
|
488
448
|
const valid =
|
|
489
449
|
Number.isFinite(scrollOptions.dx ?? 0) && Number.isFinite(scrollOptions.dy ?? 0)
|
|
490
450
|
? Effect.void
|
|
491
|
-
:
|
|
451
|
+
: refuse("scroll", "scroll deltas must be finite");
|
|
492
452
|
|
|
493
453
|
return perform(
|
|
494
454
|
"scroll",
|
|
@@ -500,11 +460,7 @@ const scroll = (input: Parts) => {
|
|
|
500
460
|
valid.pipe(
|
|
501
461
|
Effect.andThen(
|
|
502
462
|
// Scrolling the page has no target: whatever sits mid-viewport is not what is scrolled.
|
|
503
|
-
preparePolicy(
|
|
504
|
-
"scroll",
|
|
505
|
-
{ target: typeof target === "string" ? target : undefined },
|
|
506
|
-
target === undefined ? [] : [target],
|
|
507
|
-
),
|
|
463
|
+
preparePolicy("scroll", { target: refOf(target) }, target === undefined ? [] : [target]),
|
|
508
464
|
),
|
|
509
465
|
),
|
|
510
466
|
(marks, approval) =>
|
|
@@ -12,20 +12,35 @@ import {
|
|
|
12
12
|
TextInserted,
|
|
13
13
|
WheelScrolled,
|
|
14
14
|
} from "../../BrowserEvent.ts";
|
|
15
|
+
import type { Point } from "../../Motion.ts";
|
|
15
16
|
import { type PageContext, reasonOf, undispatched } from "../page/context.ts";
|
|
16
17
|
import * as BrowserClock from "../pictures/clock.ts";
|
|
17
18
|
import type * as Replies from "./replies.ts";
|
|
18
19
|
|
|
19
|
-
export
|
|
20
|
+
export interface MouseEvent extends Point {
|
|
20
21
|
readonly type: "mouseMoved" | "mousePressed" | "mouseReleased" | "mouseWheel";
|
|
21
|
-
readonly x: number;
|
|
22
|
-
readonly y: number;
|
|
23
22
|
readonly button?: "none" | "left" | "right" | "middle";
|
|
24
23
|
readonly buttons?: number;
|
|
25
24
|
readonly clickCount?: number;
|
|
26
25
|
readonly deltaX?: number;
|
|
27
26
|
readonly deltaY?: number;
|
|
28
|
-
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const buttonMask = { none: 0, left: 1, right: 2, middle: 4 } as const;
|
|
30
|
+
|
|
31
|
+
/** A mouse button going down or up at a point, as the `clickCount`th click. */
|
|
32
|
+
export const buttonAt = (
|
|
33
|
+
type: "mousePressed" | "mouseReleased",
|
|
34
|
+
point: Point,
|
|
35
|
+
button: keyof typeof buttonMask = "left",
|
|
36
|
+
clickCount = 1,
|
|
37
|
+
): MouseEvent => ({
|
|
38
|
+
type,
|
|
39
|
+
...point,
|
|
40
|
+
button,
|
|
41
|
+
buttons: type === "mousePressed" ? buttonMask[button] : 0,
|
|
42
|
+
clickCount,
|
|
43
|
+
});
|
|
29
44
|
|
|
30
45
|
export const make = (page: PageContext) => {
|
|
31
46
|
const { id, pointer, publish, now, noteInput, closedBy } = page;
|
|
@@ -99,13 +114,12 @@ export const make = (page: PageContext) => {
|
|
|
99
114
|
() => dispatchMouse(event, estimate, submitted),
|
|
100
115
|
() =>
|
|
101
116
|
dispatchMouse(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
},
|
|
117
|
+
buttonAt(
|
|
118
|
+
"mouseReleased",
|
|
119
|
+
Option.getOrElse(MutableRef.get(pointer), () => ({ x: event.x, y: event.y })),
|
|
120
|
+
event.button,
|
|
121
|
+
event.clickCount,
|
|
122
|
+
),
|
|
109
123
|
estimate,
|
|
110
124
|
),
|
|
111
125
|
);
|
|
@@ -10,28 +10,23 @@ import type { ContextReader } from "../reading/context.inpage.ts";
|
|
|
10
10
|
import type { Names } from "../reading/names.inpage.ts";
|
|
11
11
|
import type { Guard } from "./guard.inpage.ts";
|
|
12
12
|
|
|
13
|
-
export type EditResult =
|
|
14
|
-
| { readonly ok: true; readonly detail: string }
|
|
15
|
-
| { readonly error: string; readonly stale?: boolean };
|
|
13
|
+
export type EditResult = typeof EditResultSchema.Encoded;
|
|
16
14
|
|
|
17
15
|
/** A focused field, and whether it is secret. */
|
|
18
|
-
export type FocusResult =
|
|
19
|
-
| { readonly ok: true; readonly detail: string; readonly secret: boolean }
|
|
20
|
-
| { readonly error: string; readonly stale?: boolean };
|
|
16
|
+
export type FocusResult = typeof FocusResultSchema.Encoded;
|
|
21
17
|
|
|
22
18
|
/**
|
|
23
19
|
* Whether text may be typed, and whether its field is secret. Typing into focus also names the
|
|
24
20
|
* focused field, when it is one, so the action records what it typed into.
|
|
25
21
|
*/
|
|
26
|
-
export type TypeableResult =
|
|
27
|
-
| { readonly ok: true; readonly secret: boolean; readonly subject?: typeof Subject.Encoded }
|
|
28
|
-
| { readonly error: "stale" | "untypeable"; readonly detail: string };
|
|
22
|
+
export type TypeableResult = typeof TypeableResultSchema.Encoded;
|
|
29
23
|
|
|
30
24
|
export const edit = (names: Names, guard: Guard, placing: ContextReader) => {
|
|
31
25
|
const {
|
|
32
26
|
activeElement,
|
|
33
27
|
clean,
|
|
34
28
|
describe,
|
|
29
|
+
inCurrentDocument,
|
|
35
30
|
isDisabled,
|
|
36
31
|
isHtml,
|
|
37
32
|
isInput,
|
|
@@ -78,8 +73,28 @@ export const edit = (names: Names, guard: Guard, placing: ContextReader) => {
|
|
|
78
73
|
};
|
|
79
74
|
};
|
|
80
75
|
|
|
81
|
-
|
|
82
|
-
|
|
76
|
+
/**
|
|
77
|
+
* The focused field, when it is a text field named `name`: one a page put in place of a field
|
|
78
|
+
* just pressed and gave focus to, as a typeahead does.
|
|
79
|
+
*/
|
|
80
|
+
const replacement = (name: string): Element | undefined => {
|
|
81
|
+
const active = activeElement();
|
|
82
|
+
|
|
83
|
+
return active !== null &&
|
|
84
|
+
textEntry(active) &&
|
|
85
|
+
inCurrentDocument(active) &&
|
|
86
|
+
nameOf(active, roleOf(active)) === name
|
|
87
|
+
? active
|
|
88
|
+
: undefined;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Focus a field to type into. `follow` is the field's name when the action pressed it first: a
|
|
93
|
+
* page may answer the press with another field of that name in its place, focused, and the keys
|
|
94
|
+
* follow focus there, as a person's would.
|
|
95
|
+
*/
|
|
96
|
+
const focus = (ref: string, replace: boolean, follow: string | null): FocusResult => {
|
|
97
|
+
const element = lookup(ref) ?? (follow === null ? undefined : replacement(follow));
|
|
83
98
|
|
|
84
99
|
if (element === undefined) return { error: `${ref} is not on the page any more`, stale: true };
|
|
85
100
|
if (isDisabled(element)) return { error: `${ref} is disabled` };
|
|
@@ -7,28 +7,8 @@ import { Schema } from "effect";
|
|
|
7
7
|
import type { ContextReader } from "../reading/context.inpage.ts";
|
|
8
8
|
import type { Names } from "../reading/names.inpage.ts";
|
|
9
9
|
|
|
10
|
-
export interface FormField {
|
|
11
|
-
readonly type: string;
|
|
12
|
-
readonly name: string;
|
|
13
|
-
readonly autocomplete?: string | undefined;
|
|
14
|
-
readonly filled: boolean;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
10
|
/** Page text about the first target. It never includes what a field holds. */
|
|
18
|
-
export
|
|
19
|
-
readonly title: string;
|
|
20
|
-
readonly description?: string | undefined;
|
|
21
|
-
readonly dialog?: string | undefined;
|
|
22
|
-
readonly heading?: string | undefined;
|
|
23
|
-
readonly nearby?: string | undefined;
|
|
24
|
-
readonly form?:
|
|
25
|
-
| {
|
|
26
|
-
readonly method: string;
|
|
27
|
-
readonly action: string;
|
|
28
|
-
readonly fields: ReadonlyArray<FormField>;
|
|
29
|
-
}
|
|
30
|
-
| undefined;
|
|
31
|
-
}
|
|
11
|
+
export type Evidence = typeof EvidenceSchema.Encoded;
|
|
32
12
|
|
|
33
13
|
type Field = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
|
|
34
14
|
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { Schema } from "effect";
|
|
7
7
|
|
|
8
|
+
import type { Point } from "../../Motion.ts";
|
|
8
9
|
import type { Names } from "../reading/names.inpage.ts";
|
|
9
10
|
import { type Evidence, type EvidenceReader, EvidenceSchema } from "./evidence.inpage.ts";
|
|
10
11
|
import type { Targets } from "./targets.inpage.ts";
|
|
@@ -23,48 +24,26 @@ export type Fact = typeof Fact.Type;
|
|
|
23
24
|
|
|
24
25
|
export interface InputPlan {
|
|
25
26
|
readonly action: string;
|
|
26
|
-
readonly targets: ReadonlyArray<string |
|
|
27
|
+
readonly targets: ReadonlyArray<string | Point | null>;
|
|
27
28
|
readonly submit: boolean;
|
|
28
29
|
readonly keys: string | null;
|
|
29
30
|
readonly destination: string | null;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
export
|
|
33
|
-
readonly ref: string;
|
|
34
|
-
readonly element: string;
|
|
35
|
-
readonly role: string | null;
|
|
36
|
-
readonly name: string;
|
|
37
|
-
readonly cursor: string;
|
|
38
|
-
readonly href?: string | undefined;
|
|
39
|
-
/** A field the DOM marks as holding a password, a one-time code or a card's details. */
|
|
40
|
-
readonly secret: boolean;
|
|
41
|
-
readonly fingerprint: string;
|
|
42
|
-
}
|
|
33
|
+
export type InspectedTarget = NonNullable<PreparedInput["targets"][number]>;
|
|
43
34
|
|
|
44
|
-
export
|
|
45
|
-
readonly url: string;
|
|
46
|
-
readonly targets: ReadonlyArray<InspectedTarget | null>;
|
|
47
|
-
readonly facts: ReadonlyArray<Fact>;
|
|
48
|
-
readonly destination?: string | undefined;
|
|
49
|
-
readonly evidence: Evidence;
|
|
50
|
-
}
|
|
35
|
+
export type PreparedInput = Exclude<PreparedInputResult, InputFailure>;
|
|
51
36
|
|
|
52
37
|
/** A refusal; a stale target is named by its index in the plan. */
|
|
53
|
-
export
|
|
54
|
-
readonly error: string;
|
|
55
|
-
readonly detail: string;
|
|
56
|
-
readonly index?: number | undefined;
|
|
57
|
-
}
|
|
38
|
+
export type InputFailure = typeof InputPreparationError.Encoded;
|
|
58
39
|
|
|
59
|
-
export type PreparedInputResult =
|
|
40
|
+
export type PreparedInputResult = typeof PreparedInputResultSchema.Encoded;
|
|
60
41
|
|
|
61
|
-
export type ValidatedInputResult =
|
|
42
|
+
export type ValidatedInputResult = typeof ValidatedInputResultSchema.Encoded;
|
|
62
43
|
|
|
63
44
|
/** A point where a target, by its index in the plan, is about to receive a press. */
|
|
64
|
-
export interface Press {
|
|
45
|
+
export interface Press extends Point {
|
|
65
46
|
readonly index: number;
|
|
66
|
-
readonly x: number;
|
|
67
|
-
readonly y: number;
|
|
68
47
|
}
|
|
69
48
|
|
|
70
49
|
export interface ValidationOptions {
|
|
@@ -587,6 +566,7 @@ export const PreparedInputResultSchema = Schema.Union([
|
|
|
587
566
|
name: Schema.String,
|
|
588
567
|
cursor: Schema.String,
|
|
589
568
|
href: Schema.optional(Schema.String),
|
|
569
|
+
/** A field the DOM marks as holding a password, a one-time code or a card's details. */
|
|
590
570
|
secret: Schema.Boolean,
|
|
591
571
|
fingerprint: Schema.String,
|
|
592
572
|
}),
|
|
@@ -8,7 +8,7 @@ import { Effect } from "effect";
|
|
|
8
8
|
import { type BrowserError, InvalidRequest, NotActionable, StaleRef } from "../../BrowserError.ts";
|
|
9
9
|
import { InputRequest, redacted } from "../../Page.ts";
|
|
10
10
|
import { type Bridge, scriptCall } from "../page/bridge.ts";
|
|
11
|
-
import { contextGone, decodeWith, failWith, type PageContext } from "../page/context.ts";
|
|
11
|
+
import { contextGone, decodeWith, failWith, type PageContext, refuse } from "../page/context.ts";
|
|
12
12
|
import * as Url from "../page/url.ts";
|
|
13
13
|
import * as Script from "./guard.inpage.ts";
|
|
14
14
|
|
|
@@ -22,6 +22,13 @@ export interface PolicyPlan {
|
|
|
22
22
|
readonly validate: Effect.Effect<Approval, BrowserError>;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/** Recheck an approval, after any pending input it must see; neither runs without a guard. */
|
|
26
|
+
export const check = (
|
|
27
|
+
approval: Approval | undefined,
|
|
28
|
+
options: Script.ValidationOptions = {},
|
|
29
|
+
before: Effect.Effect<void, BrowserError> = Effect.void,
|
|
30
|
+
) => (approval === undefined ? Effect.void : before.pipe(Effect.andThen(approval.check(options))));
|
|
31
|
+
|
|
25
32
|
// A ref that names nothing is stale on every path, named by the target that failed.
|
|
26
33
|
export const inputFailure = (
|
|
27
34
|
operation: string,
|
|
@@ -67,19 +74,13 @@ export const make = (page: PageContext, bridge: Bridge) => {
|
|
|
67
74
|
Effect.gen(function* () {
|
|
68
75
|
for (const target of targets) {
|
|
69
76
|
if (typeof target === "string" && !/^e\d+$/.test(target))
|
|
70
|
-
return yield*
|
|
71
|
-
action,
|
|
72
|
-
new InvalidRequest({ detail: `"${target}" is not a ref; refs look like e12` }),
|
|
73
|
-
);
|
|
77
|
+
return yield* refuse(action, `"${target}" is not a ref; refs look like e12`);
|
|
74
78
|
if (
|
|
75
79
|
target !== null &&
|
|
76
80
|
typeof target !== "string" &&
|
|
77
81
|
(!Number.isFinite(target.x) || !Number.isFinite(target.y))
|
|
78
82
|
)
|
|
79
|
-
return yield*
|
|
80
|
-
action,
|
|
81
|
-
new InvalidRequest({ detail: "point coordinates must be finite" }),
|
|
82
|
-
);
|
|
83
|
+
return yield* refuse(action, "point coordinates must be finite");
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
const input: Script.InputPlan = {
|
|
@@ -178,7 +179,11 @@ export const make = (page: PageContext, bridge: Bridge) => {
|
|
|
178
179
|
),
|
|
179
180
|
);
|
|
180
181
|
|
|
181
|
-
|
|
182
|
+
/** A read may follow the current document only when no approval binds it to one. */
|
|
183
|
+
const evaluate = (operation: string, call: string, approval: Approval | undefined) =>
|
|
184
|
+
approval === undefined ? bridge.evaluate(operation, call) : mutate(operation, call, approval);
|
|
185
|
+
|
|
186
|
+
return { preparePolicy, mutate, evaluate };
|
|
182
187
|
};
|
|
183
188
|
|
|
184
189
|
export type Guard = ReturnType<typeof make>;
|
|
@@ -137,19 +137,14 @@ const useRun = <A>(
|
|
|
137
137
|
name: string,
|
|
138
138
|
run: Replies.Run,
|
|
139
139
|
action: Effect.Effect<A, BrowserError>,
|
|
140
|
+
canStop: Effect.Effect<boolean>,
|
|
140
141
|
) =>
|
|
141
142
|
action.pipe(
|
|
142
|
-
Effect.
|
|
143
|
-
|
|
144
|
-
run.close.pipe(
|
|
145
|
-
Effect.andThen(sender.flush(name, run).pipe(Effect.ignore)),
|
|
146
|
-
Effect.andThen(Effect.fail(error)),
|
|
147
|
-
),
|
|
148
|
-
onSuccess: (value) =>
|
|
149
|
-
run.close.pipe(Effect.andThen(sender.flush(name, run)), Effect.as(value)),
|
|
150
|
-
}),
|
|
143
|
+
Effect.tapError(() => run.close.pipe(Effect.andThen(sender.flush(name, run)), Effect.ignore)),
|
|
144
|
+
Effect.tap(() => run.close.pipe(Effect.andThen(sender.flush(name, run)))),
|
|
151
145
|
// Cleanup submits missing releases; the page retains replies after caller interruption.
|
|
152
146
|
Effect.ensuring(run.close.pipe(Effect.andThen(page.openings.clear))),
|
|
147
|
+
Effect.onError(() => run.recover(page.stopLoading, page.scope).pipe(Effect.when(canStop))),
|
|
153
148
|
Effect.provideService(Replies.Current, run),
|
|
154
149
|
);
|
|
155
150
|
|
|
@@ -224,7 +219,16 @@ export const make = (page: PageContext, sender: Dispatch) => {
|
|
|
224
219
|
if (estimate !== undefined) inputClocks.set(current, estimate);
|
|
225
220
|
}),
|
|
226
221
|
),
|
|
227
|
-
Effect.flatMap((run) =>
|
|
222
|
+
Effect.flatMap((run) =>
|
|
223
|
+
useRun(
|
|
224
|
+
page,
|
|
225
|
+
sender,
|
|
226
|
+
name,
|
|
227
|
+
run,
|
|
228
|
+
action(run),
|
|
229
|
+
info.input === false ? Effect.succeed(false) : Ref.get(sent),
|
|
230
|
+
),
|
|
231
|
+
),
|
|
228
232
|
);
|
|
229
233
|
|
|
230
234
|
// How long its turn and the page's replies kept the action waiting, once it had both.
|