effect-browser 0.2.0-beta.9 → 0.3.0-beta.10
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 +849 -1161
- package/dist/Browser-S4kvpmqa.mjs +605 -0
- package/dist/Browser-S4kvpmqa.mjs.map +1 -0
- package/dist/Browser.d.mts +127 -3
- package/dist/Browser.mjs +2 -22
- package/dist/BrowserError-Coy7EW1N.d.mts +140 -0
- package/dist/BrowserError.d.mts +2 -0
- package/dist/BrowserError.mjs +211 -0
- package/dist/BrowserError.mjs.map +1 -0
- package/dist/BrowserEvent-BRu2juEz.d.mts +279 -0
- package/dist/BrowserEvent.d.mts +2 -0
- package/dist/BrowserEvent.mjs +297 -0
- package/dist/BrowserEvent.mjs.map +1 -0
- package/dist/Cdp.d.mts +32 -0
- package/dist/Cdp.mjs +61 -0
- package/dist/Cdp.mjs.map +1 -0
- package/dist/Change-DNvztNLu.mjs +138 -0
- package/dist/Change-DNvztNLu.mjs.map +1 -0
- package/dist/Change-iMlcuzVD.d.mts +71 -0
- package/dist/Change.d.mts +2 -0
- package/dist/Change.mjs +2 -0
- package/dist/Chromium.d.mts +29 -81
- package/dist/Chromium.mjs +57 -392
- package/dist/Chromium.mjs.map +1 -1
- package/dist/Expect.d.mts +62 -0
- package/dist/Expect.mjs +114 -0
- package/dist/Expect.mjs.map +1 -0
- package/dist/Frame-BERmf_gM.d.mts +117 -0
- package/dist/Frame.d.mts +2 -0
- package/dist/Frame.mjs +123 -0
- package/dist/Frame.mjs.map +1 -0
- package/dist/Moment.d.mts +2 -0
- package/dist/Moment.mjs +288 -0
- package/dist/Moment.mjs.map +1 -0
- package/dist/Motion.d.mts +40 -0
- package/dist/Motion.mjs +122 -0
- package/dist/Motion.mjs.map +1 -0
- package/dist/Page-B5cjaUp5.mjs +618 -0
- package/dist/Page-B5cjaUp5.mjs.map +1 -0
- package/dist/Page-BlPjfibv.d.mts +595 -0
- package/dist/Page.d.mts +2 -0
- package/dist/Page.mjs +2 -0
- package/dist/Plan-BI8XGvDu.d.mts +209 -0
- package/dist/Plan-D1wrJVs8.mjs +508 -0
- package/dist/Plan-D1wrJVs8.mjs.map +1 -0
- package/dist/Plan.d.mts +2 -3
- package/dist/Plan.mjs +2 -2
- package/dist/Policy.d.mts +76 -0
- package/dist/Policy.mjs +237 -0
- package/dist/Policy.mjs.map +1 -0
- package/dist/Presentation.d.mts +48 -0
- package/dist/Presentation.mjs +117 -0
- package/dist/Presentation.mjs.map +1 -0
- package/dist/Snapshot-B34hZ4Jl.d.mts +68 -0
- package/dist/Snapshot.d.mts +2 -0
- package/dist/Snapshot.mjs +79 -0
- package/dist/Snapshot.mjs.map +1 -0
- package/dist/Stage.d.mts +64 -0
- package/dist/Stage.mjs +237 -0
- package/dist/Stage.mjs.map +1 -0
- package/dist/Supervisor-CYWFK3vM.d.mts +280 -0
- package/dist/Supervisor-TNKNOVzu.mjs +599 -0
- package/dist/Supervisor-TNKNOVzu.mjs.map +1 -0
- package/dist/Supervisor.d.mts +2 -0
- package/dist/Supervisor.mjs +2 -0
- package/dist/context-B6xr8nVi.mjs +261 -0
- package/dist/context-B6xr8nVi.mjs.map +1 -0
- package/dist/index.d.mts +17 -14
- package/dist/index.mjs +18 -13
- package/dist/match.inpage-xOe9LNO0.mjs +48 -0
- package/dist/match.inpage-xOe9LNO0.mjs.map +1 -0
- package/dist/page-B8tUQELg.mjs +5073 -0
- package/dist/page-B8tUQELg.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/dist/url-Cd04Okid.mjs +264 -0
- package/dist/url-Cd04Okid.mjs.map +1 -0
- package/package.json +96 -61
- package/src/Browser.ts +666 -0
- package/src/BrowserError.ts +234 -0
- package/src/BrowserEvent.ts +296 -0
- package/src/Cdp.ts +101 -0
- package/src/Change.ts +88 -0
- package/src/Chromium.ts +102 -0
- package/src/Expect.ts +180 -0
- package/src/Frame.ts +130 -0
- package/src/Moment.ts +479 -0
- package/src/Motion.ts +166 -0
- package/src/Page.ts +522 -0
- package/src/Plan.ts +663 -0
- package/src/Policy.ts +325 -0
- package/src/Presentation.ts +195 -0
- package/src/Snapshot.ts +89 -0
- package/src/Stage.ts +467 -0
- package/src/Supervisor.ts +599 -0
- package/src/index.ts +20 -0
- package/src/internal/input/actions.ts +610 -0
- package/src/internal/input/dispatch.ts +161 -0
- package/src/internal/input/edit.inpage.ts +162 -0
- package/src/internal/input/evidence.inpage.ts +164 -0
- package/src/internal/input/gate.ts +224 -0
- package/src/internal/input/guard.inpage.ts +605 -0
- package/src/internal/input/guard.ts +184 -0
- package/src/internal/input/keyboard.ts +112 -0
- package/src/internal/input/keys.ts +127 -0
- package/src/internal/input/perform.ts +376 -0
- package/src/internal/input/pointer.ts +153 -0
- package/src/internal/input/replies.ts +195 -0
- package/src/internal/input/settle.ts +50 -0
- package/src/internal/input/style.ts +186 -0
- package/src/internal/input/targets.inpage.ts +293 -0
- package/src/internal/input/targets.ts +148 -0
- package/src/internal/page/bridge.ts +386 -0
- package/src/internal/page/context.ts +312 -0
- package/src/internal/page/lane.ts +311 -0
- package/src/internal/page/navigation.ts +183 -0
- package/src/internal/page/openings.ts +87 -0
- package/src/internal/page/page.ts +165 -0
- package/src/internal/page/url.ts +100 -0
- package/src/internal/page/viewport.ts +43 -0
- package/src/internal/pictures/capture.ts +776 -0
- package/src/internal/pictures/clock.ts +141 -0
- package/src/internal/pictures/counts.ts +92 -0
- package/src/internal/pictures/pictures.ts +520 -0
- package/src/internal/pictures/transport.ts +242 -0
- package/src/internal/reading/choose.ts +100 -0
- package/src/internal/reading/context.inpage.ts +201 -0
- package/src/internal/reading/controls.inpage.ts +158 -0
- package/src/internal/reading/expect.inpage.ts +70 -0
- package/src/internal/reading/match.inpage.ts +92 -0
- package/src/internal/reading/names.inpage.ts +306 -0
- package/src/internal/reading/outline.inpage.ts +260 -0
- package/src/internal/reading/reading.ts +223 -0
- package/src/internal/reading/ready.inpage.ts +164 -0
- package/src/internal/reading/ready.ts +111 -0
- package/src/internal/reading/subjects.inpage.ts +188 -0
- package/src/internal/reading/text.inpage.ts +135 -0
- package/src/internal/reading/walk.inpage.ts +220 -0
- package/src/internal/supervisor/lifecycle.ts +416 -0
- package/src/internal/timeline/changes.inpage.ts +216 -0
- package/src/internal/timeline/changes.ts +100 -0
- package/src/internal/timeline/events.ts +94 -0
- package/src/internal/timeline/fold.inpage.ts +123 -0
- package/src/internal/timeline/history.inpage.ts +229 -0
- package/src/internal/timeline/marks.inpage.ts +207 -0
- package/src/internal/timeline/record.inpage.ts +133 -0
- package/src/internal/timeline/sight.inpage.ts +122 -0
- package/src/internal/timeline/window.ts +96 -0
- package/src/internal/timeline/words.ts +65 -0
- package/dist/Bootstrap-B9UUXsvF.d.mts +0 -183
- package/dist/Bootstrap-CeQOWBy6.mjs +0 -189
- package/dist/Bootstrap-CeQOWBy6.mjs.map +0 -1
- package/dist/Bootstrap.d.mts +0 -3
- package/dist/Bootstrap.mjs +0 -2
- package/dist/Browser-BF9_sORP.d.mts +0 -388
- package/dist/Browser.mjs.map +0 -1
- package/dist/BrowserData-CPIIIHol.d.mts +0 -585
- package/dist/BrowserData.d.mts +0 -3
- package/dist/BrowserData.mjs +0 -695
- package/dist/BrowserData.mjs.map +0 -1
- package/dist/BrowserRuntime-B6nT1G4X.d.mts +0 -148
- package/dist/BrowserRuntime-C1r2aSRK.mjs +0 -6820
- package/dist/BrowserRuntime-C1r2aSRK.mjs.map +0 -1
- package/dist/BrowserRuntime.d.mts +0 -3
- package/dist/BrowserRuntime.mjs +0 -3
- package/dist/Capture.d.mts +0 -56
- package/dist/Capture.mjs +0 -678
- package/dist/Capture.mjs.map +0 -1
- package/dist/CaptureData-CYriZHX1.mjs +0 -201
- package/dist/CaptureData-CYriZHX1.mjs.map +0 -1
- package/dist/CaptureData.d.mts +0 -219
- package/dist/CaptureData.mjs +0 -2
- package/dist/Errors.d.mts +0 -158
- package/dist/Errors.mjs +0 -207
- package/dist/Errors.mjs.map +0 -1
- package/dist/Images-yCG9BmRU.mjs +0 -76
- package/dist/Images-yCG9BmRU.mjs.map +0 -1
- package/dist/OperationOptions-DAeRS6O9.mjs +0 -55
- package/dist/OperationOptions-DAeRS6O9.mjs.map +0 -1
- package/dist/PageControl.d.mts +0 -15
- package/dist/PageControl.mjs +0 -34
- package/dist/PageControl.mjs.map +0 -1
- package/dist/PageRegistry-B3q9DS3g.mjs +0 -164
- package/dist/PageRegistry-B3q9DS3g.mjs.map +0 -1
- package/dist/Plan-Dyxu2q8C.mjs +0 -543
- package/dist/Plan-Dyxu2q8C.mjs.map +0 -1
- package/dist/PlanData-B668RxbQ.d.mts +0 -4831
- package/dist/PlanData.d.mts +0 -3
- package/dist/PlanData.mjs +0 -268
- package/dist/PlanData.mjs.map +0 -1
- package/dist/PlanInput-BJcPPncs.mjs +0 -75
- package/dist/PlanInput-BJcPPncs.mjs.map +0 -1
- package/dist/PublicSession-DCSMMqOj.mjs +0 -5602
- package/dist/PublicSession-DCSMMqOj.mjs.map +0 -1
- package/dist/Testing.d.mts +0 -479
- package/dist/Testing.mjs +0 -2147
- package/dist/Testing.mjs.map +0 -1
- package/dist/Timeline-fV22Xk-S.d.mts +0 -29
- package/dist/Timeline.d.mts +0 -4
- package/dist/Timeline.mjs +0 -20
- package/dist/Timeline.mjs.map +0 -1
- package/dist/TimelineData-BsOn-YGZ.d.mts +0 -3818
- package/dist/TimelineData.d.mts +0 -3
- package/dist/TimelineData.mjs +0 -482
- package/dist/TimelineData.mjs.map +0 -1
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In the page: where input lands. It resolves a ref or a point to the control under it, in
|
|
3
|
+
* top-document viewport pixels, with the subject's context, and plans a wheel scroll that brings a
|
|
4
|
+
* ref into view. See `reading/names.inpage.ts` for what a page-side part may use.
|
|
5
|
+
*/
|
|
6
|
+
import { Schema } from "effect";
|
|
7
|
+
|
|
8
|
+
import { Box, SubjectContext } from "../../BrowserEvent.ts";
|
|
9
|
+
import type { ContextReader } from "../reading/context.inpage.ts";
|
|
10
|
+
import type { Names } from "../reading/names.inpage.ts";
|
|
11
|
+
import type { Walk } from "../reading/walk.inpage.ts";
|
|
12
|
+
|
|
13
|
+
export type ResolvedPoint = typeof ResolvedPointSchema.Type;
|
|
14
|
+
export type PointResult = typeof PointResultSchema.Type;
|
|
15
|
+
|
|
16
|
+
export const targets = (names: Names, walked: Walk, placing: ContextReader) => {
|
|
17
|
+
const { describe, isDisabled, lookup, nameOf, parentOf, roleOf } = names;
|
|
18
|
+
const { boxOf, controlOf, hitAt } = walked;
|
|
19
|
+
const { contextOf } = placing;
|
|
20
|
+
|
|
21
|
+
/** Whether `node` is `ancestor` or inside it, across shadow roots and same-origin frames. */
|
|
22
|
+
const within = (ancestor: Element, node: Element): boolean => {
|
|
23
|
+
for (let current: Element | null = node; current !== null;) {
|
|
24
|
+
if (current === ancestor) return true;
|
|
25
|
+
current = parentOf(current) ?? current.ownerDocument.defaultView?.frameElement ?? null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return false;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// A press at a point reaches the element when the top-document hit is the element, inside it,
|
|
32
|
+
// or one of its containers (whose activation the element's own facts already cover).
|
|
33
|
+
const receives = (element: Element, hit: Element): boolean =>
|
|
34
|
+
within(element, hit) || within(hit, element);
|
|
35
|
+
|
|
36
|
+
// HTML links, SVG links and image-map areas all navigate; SVG keeps lowercase tag names.
|
|
37
|
+
const hrefAttribute = (element: Element): string | null =>
|
|
38
|
+
element.localName === "a" || element.localName === "area"
|
|
39
|
+
? (element.getAttribute("href") ??
|
|
40
|
+
element.getAttributeNS("http://www.w3.org/1999/xlink", "href"))
|
|
41
|
+
: null;
|
|
42
|
+
|
|
43
|
+
const linkOf = (element: Element): Element | undefined => {
|
|
44
|
+
for (let node: Element | null = element; node !== null; node = parentOf(node))
|
|
45
|
+
if (hrefAttribute(node) !== null) return node;
|
|
46
|
+
|
|
47
|
+
return undefined;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const details = (
|
|
51
|
+
element: Element,
|
|
52
|
+
hit: Element,
|
|
53
|
+
x: number,
|
|
54
|
+
y: number,
|
|
55
|
+
): Omit<ResolvedPoint, "context"> => {
|
|
56
|
+
const link = linkOf(element);
|
|
57
|
+
let href: string | undefined;
|
|
58
|
+
|
|
59
|
+
if (link !== undefined) {
|
|
60
|
+
try {
|
|
61
|
+
href = new URL(hrefAttribute(link) ?? "", link.ownerDocument.baseURI).href;
|
|
62
|
+
} catch {
|
|
63
|
+
href = undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const role = roleOf(element);
|
|
67
|
+
const view = hit.ownerDocument.defaultView ?? window;
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
x,
|
|
71
|
+
y,
|
|
72
|
+
element: describe(element),
|
|
73
|
+
tag: element.tagName.toLowerCase(),
|
|
74
|
+
role,
|
|
75
|
+
name: nameOf(element, role),
|
|
76
|
+
cursor: view.getComputedStyle(hit).cursor,
|
|
77
|
+
...(href === undefined ? {} : { href }),
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const point = (
|
|
82
|
+
target: string | { readonly x: number; readonly y: number },
|
|
83
|
+
scroll = true,
|
|
84
|
+
): PointResult => {
|
|
85
|
+
if (typeof target !== "string") {
|
|
86
|
+
const { x, y } = target;
|
|
87
|
+
|
|
88
|
+
if (
|
|
89
|
+
!Number.isFinite(x) ||
|
|
90
|
+
!Number.isFinite(y) ||
|
|
91
|
+
x < 0 ||
|
|
92
|
+
y < 0 ||
|
|
93
|
+
x >= window.innerWidth ||
|
|
94
|
+
y >= window.innerHeight
|
|
95
|
+
)
|
|
96
|
+
return { error: "outside", detail: `(${x}, ${y}) is outside the viewport` };
|
|
97
|
+
const hit = hitAt(document, x, y);
|
|
98
|
+
|
|
99
|
+
if (hit === null) return { error: "offscreen", detail: `nothing is painted at (${x}, ${y})` };
|
|
100
|
+
const control = controlOf(hit);
|
|
101
|
+
|
|
102
|
+
return { ...details(control, hit, x, y), context: contextOf(control), box: boxOf(control) };
|
|
103
|
+
}
|
|
104
|
+
const ref = target;
|
|
105
|
+
const element = lookup(ref);
|
|
106
|
+
|
|
107
|
+
if (element === undefined)
|
|
108
|
+
return { error: "stale", detail: `${ref} is not on the page any more` };
|
|
109
|
+
if (isDisabled(element)) return { error: "disabled", detail: `${ref} is disabled` };
|
|
110
|
+
const rect = element.getBoundingClientRect();
|
|
111
|
+
|
|
112
|
+
if (rect.width === 0 && rect.height === 0)
|
|
113
|
+
return { error: "hidden", detail: `${ref} has no size on the page` };
|
|
114
|
+
|
|
115
|
+
// Input arrives in top-document viewport pixels, so geometry and occlusion are measured
|
|
116
|
+
// there: a frame's own viewport cannot say whether the frame is scrolled away or covered.
|
|
117
|
+
let box = { left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom };
|
|
118
|
+
let visible = { ...box };
|
|
119
|
+
let outside = false;
|
|
120
|
+
|
|
121
|
+
for (let inner: Element = element; ;) {
|
|
122
|
+
const view = inner.ownerDocument.defaultView ?? window;
|
|
123
|
+
|
|
124
|
+
outside ||=
|
|
125
|
+
box.left < 0 || box.top < 0 || box.right > view.innerWidth || box.bottom > view.innerHeight;
|
|
126
|
+
visible = {
|
|
127
|
+
left: Math.max(visible.left, 0),
|
|
128
|
+
top: Math.max(visible.top, 0),
|
|
129
|
+
right: Math.min(visible.right, view.innerWidth),
|
|
130
|
+
bottom: Math.min(visible.bottom, view.innerHeight),
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// A field can be inside the viewport and still clipped by a nested scrolling panel.
|
|
134
|
+
for (let ancestor = parentOf(inner); ancestor !== null; ancestor = parentOf(ancestor)) {
|
|
135
|
+
const style = view.getComputedStyle(ancestor);
|
|
136
|
+
const clip = ancestor.getBoundingClientRect();
|
|
137
|
+
|
|
138
|
+
if (/(auto|scroll|hidden|clip)/.test(style.overflowX)) {
|
|
139
|
+
visible.left = Math.max(visible.left, clip.left + ancestor.clientLeft);
|
|
140
|
+
visible.right = Math.min(
|
|
141
|
+
visible.right,
|
|
142
|
+
clip.left + ancestor.clientLeft + ancestor.clientWidth,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
if (/(auto|scroll|hidden|clip)/.test(style.overflowY)) {
|
|
146
|
+
visible.top = Math.max(visible.top, clip.top + ancestor.clientTop);
|
|
147
|
+
visible.bottom = Math.min(
|
|
148
|
+
visible.bottom,
|
|
149
|
+
clip.top + ancestor.clientTop + ancestor.clientHeight,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const frame = view.frameElement;
|
|
155
|
+
|
|
156
|
+
if (frame === null) break;
|
|
157
|
+
const frameBox = frame.getBoundingClientRect();
|
|
158
|
+
const dx = frameBox.left + frame.clientLeft;
|
|
159
|
+
const dy = frameBox.top + frame.clientTop;
|
|
160
|
+
|
|
161
|
+
box = {
|
|
162
|
+
left: box.left + dx,
|
|
163
|
+
top: box.top + dy,
|
|
164
|
+
right: box.right + dx,
|
|
165
|
+
bottom: box.bottom + dy,
|
|
166
|
+
};
|
|
167
|
+
visible = {
|
|
168
|
+
left: Math.max(visible.left + dx, dx),
|
|
169
|
+
top: Math.max(visible.top + dy, dy),
|
|
170
|
+
right: Math.min(visible.right + dx, dx + frame.clientWidth),
|
|
171
|
+
bottom: Math.min(visible.bottom + dy, dy + frame.clientHeight),
|
|
172
|
+
};
|
|
173
|
+
inner = frame;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (outside || visible.right <= visible.left || visible.bottom <= visible.top) {
|
|
177
|
+
if (scroll) {
|
|
178
|
+
element.scrollIntoView({ block: "center", inline: "center", behavior: "instant" });
|
|
179
|
+
|
|
180
|
+
return point(target, false);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
error: "offscreen",
|
|
185
|
+
detail: outside
|
|
186
|
+
? ref + " is outside the viewport"
|
|
187
|
+
: `${ref} could not be scrolled into view`,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const x = (visible.left + visible.right) / 2;
|
|
191
|
+
const y = (visible.top + visible.bottom) / 2;
|
|
192
|
+
const hit = hitAt(document, x, y);
|
|
193
|
+
|
|
194
|
+
if (hit !== null && !receives(element, hit))
|
|
195
|
+
return { error: "covered", detail: `${ref} is covered by ${describe(hit)}` };
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
...details(element, hit ?? element, Math.round(x), Math.round(y)),
|
|
199
|
+
context: contextOf(element),
|
|
200
|
+
};
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// Suggest one visible wheel origin. Unsupported frame geometry and fully clipped panels use
|
|
204
|
+
// the bounded instant fallback; guessing their coordinates could scroll an unrelated control.
|
|
205
|
+
const scrollPlan = (ref: string) => {
|
|
206
|
+
const element = lookup(ref);
|
|
207
|
+
|
|
208
|
+
if (element === undefined || element.ownerDocument !== document) return null;
|
|
209
|
+
const target = element.getBoundingClientRect();
|
|
210
|
+
const centerX = (target.left + target.right) / 2;
|
|
211
|
+
const centerY = (target.top + target.bottom) / 2;
|
|
212
|
+
|
|
213
|
+
for (let ancestor = parentOf(element); ancestor !== null; ancestor = parentOf(ancestor)) {
|
|
214
|
+
const style = window.getComputedStyle(ancestor);
|
|
215
|
+
|
|
216
|
+
const horizontal =
|
|
217
|
+
/^(auto|scroll)$/.test(style.overflowX) && ancestor.scrollWidth > ancestor.clientWidth;
|
|
218
|
+
|
|
219
|
+
const vertical =
|
|
220
|
+
/^(auto|scroll)$/.test(style.overflowY) && ancestor.scrollHeight > ancestor.clientHeight;
|
|
221
|
+
|
|
222
|
+
if (!horizontal && !vertical) continue;
|
|
223
|
+
const box = ancestor.getBoundingClientRect();
|
|
224
|
+
const left = Math.max(0, box.left + ancestor.clientLeft);
|
|
225
|
+
const top = Math.max(0, box.top + ancestor.clientTop);
|
|
226
|
+
|
|
227
|
+
const right = Math.min(
|
|
228
|
+
window.innerWidth,
|
|
229
|
+
box.left + ancestor.clientLeft + ancestor.clientWidth,
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
const bottom = Math.min(
|
|
233
|
+
window.innerHeight,
|
|
234
|
+
box.top + ancestor.clientTop + ancestor.clientHeight,
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
if (right <= left || bottom <= top) continue;
|
|
238
|
+
const x = (left + right) / 2;
|
|
239
|
+
const y = (top + bottom) / 2;
|
|
240
|
+
const dx = horizontal && (target.left < left || target.right > right) ? centerX - x : 0;
|
|
241
|
+
const dy = vertical && (target.top < top || target.bottom > bottom) ? centerY - y : 0;
|
|
242
|
+
|
|
243
|
+
if (dx === 0 && dy === 0) continue;
|
|
244
|
+
const hit = hitAt(document, x, y);
|
|
245
|
+
|
|
246
|
+
if (hit !== null && (hit === ancestor || ancestor.contains(hit))) return { x, y, dx, dy };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const dx =
|
|
250
|
+
target.left < 0 || target.right > window.innerWidth ? centerX - window.innerWidth / 2 : 0;
|
|
251
|
+
|
|
252
|
+
const dy =
|
|
253
|
+
target.top < 0 || target.bottom > window.innerHeight ? centerY - window.innerHeight / 2 : 0;
|
|
254
|
+
|
|
255
|
+
return dx === 0 && dy === 0
|
|
256
|
+
? null
|
|
257
|
+
: { x: window.innerWidth / 2, y: window.innerHeight / 2, dx, dy };
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
return { controlOf, details, hitAt, hrefAttribute, linkOf, point, receives, scrollPlan, within };
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
export type Targets = ReturnType<typeof targets>;
|
|
264
|
+
|
|
265
|
+
const ResolvedPointSchema = Schema.Struct({
|
|
266
|
+
x: Schema.Finite,
|
|
267
|
+
y: Schema.Finite,
|
|
268
|
+
element: Schema.String,
|
|
269
|
+
tag: Schema.String,
|
|
270
|
+
role: Schema.NullOr(Schema.String),
|
|
271
|
+
name: Schema.String,
|
|
272
|
+
context: SubjectContext,
|
|
273
|
+
box: Schema.optional(Box),
|
|
274
|
+
cursor: Schema.String,
|
|
275
|
+
href: Schema.optional(Schema.String),
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
export const PointResultSchema = Schema.Union([
|
|
279
|
+
ResolvedPointSchema,
|
|
280
|
+
Schema.Struct({
|
|
281
|
+
error: Schema.Literals(["stale", "hidden", "offscreen", "disabled", "covered", "outside"]),
|
|
282
|
+
detail: Schema.String,
|
|
283
|
+
}),
|
|
284
|
+
]);
|
|
285
|
+
|
|
286
|
+
export const ScrollPlanSchema = Schema.NullOr(
|
|
287
|
+
Schema.Struct({
|
|
288
|
+
x: Schema.Finite,
|
|
289
|
+
y: Schema.Finite,
|
|
290
|
+
dx: Schema.Finite,
|
|
291
|
+
dy: Schema.Finite,
|
|
292
|
+
}),
|
|
293
|
+
);
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where an action's input goes: a ref or a point resolved to a control and a viewport point,
|
|
3
|
+
* scrolled into view first when it is outside the viewport: at once, or with the wheel where
|
|
4
|
+
* viewers watch.
|
|
5
|
+
*/
|
|
6
|
+
import { Effect } from "effect";
|
|
7
|
+
|
|
8
|
+
import { InvalidRequest, NotActionable, StaleRef } from "../../BrowserError.ts";
|
|
9
|
+
import { ResolvedTarget, type Target } from "../../Page.ts";
|
|
10
|
+
import { type Bridge, scriptCall } from "../page/bridge.ts";
|
|
11
|
+
import { decodeWith, failWith } from "../page/context.ts";
|
|
12
|
+
import type { Viewport } from "../page/viewport.ts";
|
|
13
|
+
import type { Dispatch } from "./dispatch.ts";
|
|
14
|
+
import type { Approval, Guard } from "./guard.ts";
|
|
15
|
+
import type { InputMarks } from "./perform.ts";
|
|
16
|
+
import type { Pointer } from "./pointer.ts";
|
|
17
|
+
import type { Settle } from "./settle.ts";
|
|
18
|
+
import * as Script from "./targets.inpage.ts";
|
|
19
|
+
|
|
20
|
+
const pointFailure = (
|
|
21
|
+
operation: string,
|
|
22
|
+
target: Target,
|
|
23
|
+
result: Extract<Script.PointResult, { readonly error: string }>,
|
|
24
|
+
) =>
|
|
25
|
+
failWith(
|
|
26
|
+
operation,
|
|
27
|
+
result.error === "stale" && typeof target === "string"
|
|
28
|
+
? new StaleRef({ ref: target })
|
|
29
|
+
: result.error === "outside"
|
|
30
|
+
? new InvalidRequest({ detail: result.detail })
|
|
31
|
+
: new NotActionable({ detail: result.detail }),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const resolvedTarget = (result: Script.ResolvedPoint): ResolvedTarget =>
|
|
35
|
+
new ResolvedTarget({
|
|
36
|
+
point: { x: result.x, y: result.y },
|
|
37
|
+
element: result.element,
|
|
38
|
+
tag: result.tag,
|
|
39
|
+
role: result.role,
|
|
40
|
+
name: result.name,
|
|
41
|
+
context: result.context,
|
|
42
|
+
box: result.box,
|
|
43
|
+
cursor: result.cursor,
|
|
44
|
+
...(result.href === undefined ? {} : { href: result.href }),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export const make = (
|
|
48
|
+
bridge: Bridge,
|
|
49
|
+
viewport: Viewport,
|
|
50
|
+
guard: Guard,
|
|
51
|
+
pointer: Pointer,
|
|
52
|
+
dispatch: Dispatch,
|
|
53
|
+
settling: Settle,
|
|
54
|
+
) => {
|
|
55
|
+
const { evaluate } = bridge;
|
|
56
|
+
const { mutate } = guard;
|
|
57
|
+
const { moveTo, wheel } = pointer;
|
|
58
|
+
const { flush } = dispatch;
|
|
59
|
+
|
|
60
|
+
const readPoint = (
|
|
61
|
+
operation: string,
|
|
62
|
+
target: Target,
|
|
63
|
+
approval: Approval | undefined,
|
|
64
|
+
scroll: boolean,
|
|
65
|
+
) =>
|
|
66
|
+
(approval === undefined
|
|
67
|
+
? evaluate(operation, scriptCall("point", target, scroll))
|
|
68
|
+
: mutate(operation, scriptCall("point", target, scroll), approval)
|
|
69
|
+
).pipe(Effect.flatMap(decodeWith(operation, Script.PointResultSchema)));
|
|
70
|
+
|
|
71
|
+
const resolve = (operation: string, target: Target, approval?: Approval, scroll = true) => {
|
|
72
|
+
if (typeof target === "string" && !/^e\d+$/.test(target))
|
|
73
|
+
return failWith(
|
|
74
|
+
operation,
|
|
75
|
+
new InvalidRequest({ detail: target + " is not a ref; refs look like e12" }),
|
|
76
|
+
);
|
|
77
|
+
if (typeof target !== "string" && (!Number.isFinite(target.x) || !Number.isFinite(target.y)))
|
|
78
|
+
return failWith(
|
|
79
|
+
operation,
|
|
80
|
+
new InvalidRequest({ detail: "point coordinates must be finite" }),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
return readPoint(operation, target, approval, scroll).pipe(
|
|
84
|
+
Effect.flatMap((result) =>
|
|
85
|
+
"error" in result
|
|
86
|
+
? pointFailure(operation, target, result)
|
|
87
|
+
: Effect.succeed(resolvedTarget(result)),
|
|
88
|
+
),
|
|
89
|
+
);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const targetFor = Effect.fnUntraced(function* (
|
|
93
|
+
operation: string,
|
|
94
|
+
target: Target,
|
|
95
|
+
approval: Approval | undefined,
|
|
96
|
+
marks: InputMarks,
|
|
97
|
+
) {
|
|
98
|
+
if (!marks.style.shown || typeof target !== "string")
|
|
99
|
+
return yield* resolve(operation, target, approval);
|
|
100
|
+
|
|
101
|
+
// Inspection and policy approval happen before this point. Every retry is geometry only;
|
|
102
|
+
// scrolling never obtains a fresh approval or silently follows a replacement document.
|
|
103
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
104
|
+
const result = yield* readPoint(operation, target, approval, false);
|
|
105
|
+
|
|
106
|
+
if (!("error" in result)) return resolvedTarget(result);
|
|
107
|
+
if (result.error !== "offscreen") return yield* pointFailure(operation, target, result);
|
|
108
|
+
|
|
109
|
+
const plan = yield* mutate(operation, scriptCall("scrollPlan", target), approval).pipe(
|
|
110
|
+
Effect.flatMap(decodeWith(operation, Script.ScrollPlanSchema)),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
if (plan === null) break;
|
|
114
|
+
const size = yield* viewport.viewportFor(operation);
|
|
115
|
+
const point = { x: plan.x, y: plan.y };
|
|
116
|
+
|
|
117
|
+
yield* marks.at(point);
|
|
118
|
+
yield* moveTo(operation, marks, point);
|
|
119
|
+
const before = settling.mark();
|
|
120
|
+
|
|
121
|
+
yield* wheel(
|
|
122
|
+
operation,
|
|
123
|
+
marks,
|
|
124
|
+
point,
|
|
125
|
+
Math.max(-size.width * 0.9, Math.min(size.width * 0.9, plan.dx)),
|
|
126
|
+
Math.max(-size.height * 0.9, Math.min(size.height * 0.9, plan.dy)),
|
|
127
|
+
);
|
|
128
|
+
yield* flush(operation, marks.input);
|
|
129
|
+
yield* settling.settle(operation, before);
|
|
130
|
+
if (approval !== undefined) yield* approval.check();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Wheels may be prevented or the target may need unsupported nested/frame geometry. One
|
|
134
|
+
// explicit fallback preserves reachability without a distance-dependent protocol loop.
|
|
135
|
+
yield* marks.touched;
|
|
136
|
+
const before = settling.mark();
|
|
137
|
+
|
|
138
|
+
yield* resolve(operation, target, approval);
|
|
139
|
+
yield* settling.settle(operation, before);
|
|
140
|
+
if (approval !== undefined) yield* approval.check();
|
|
141
|
+
|
|
142
|
+
return yield* resolve(operation, target, approval, false);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
return { resolve, targetFor };
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export type Targets = ReturnType<typeof make>;
|