@reticlehq/browser 2.14.0 → 3.2.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/dist/actions/actions-dom.d.ts +9 -0
- package/dist/actions/actions-dom.js +81 -28
- package/dist/actions/actions-press.d.ts +4 -0
- package/dist/actions/actions-press.js +46 -0
- package/dist/actions/actions.d.ts +1 -0
- package/dist/actions/actions.js +50 -13
- package/dist/actions/appeared-text.js +6 -0
- package/dist/actions/scroll.d.ts +8 -1
- package/dist/actions/scroll.js +28 -10
- package/dist/commands/commands.js +10 -3
- package/dist/dom/a11y.js +2 -2
- package/dist/dom/desktop-capture.js +1 -1
- package/dist/dom/dom-ignore.js +1 -1
- package/dist/dom/occlusion.js +31 -5
- package/dist/dom/paint-context.d.ts +6 -0
- package/dist/dom/paint-context.js +38 -0
- package/dist/dom/query.js +24 -7
- package/dist/dom/snapshot.js +1 -1
- package/dist/edit-epoch.d.ts +1 -1
- package/dist/edit-epoch.js +1 -1
- package/dist/index.d.ts +4 -5
- package/dist/index.js +3 -5
- package/dist/observers/animation.js +1 -1
- package/dist/observers/blind-spots.js +1 -1
- package/dist/observers/departure.js +16 -8
- package/dist/observers/dialogs.js +1 -1
- package/dist/observers/dom.js +28 -8
- package/dist/observers/download.js +1 -1
- package/dist/observers/health.js +1 -1
- package/dist/observers/{network-body.js → net-detail/network-body.js} +2 -2
- package/dist/observers/network-stream.js +2 -2
- package/dist/observers/network.d.ts +1 -1
- package/dist/observers/network.js +51 -10
- package/dist/observers/scroll.js +2 -2
- package/dist/presenter/chrome/layers.d.ts +2 -0
- package/dist/presenter/chrome/layers.js +2 -0
- package/dist/presenter/chrome/log-kinds.d.ts +12 -0
- package/dist/presenter/chrome/log-kinds.js +7 -0
- package/dist/presenter/{presenter-effects.js → chrome/presenter-effects.js} +1 -1
- package/dist/presenter/{presenter-log.d.ts → chrome/presenter-log.d.ts} +3 -12
- package/dist/presenter/{presenter-log.js → chrome/presenter-log.js} +4 -9
- package/dist/presenter/chrome/presenter-safe-html.d.ts +2 -0
- package/dist/presenter/chrome/presenter-safe-html.js +16 -0
- package/dist/presenter/{presenter-tally.js → chrome/presenter-tally.js} +1 -1
- package/dist/presenter/{presenter-heroicons-data.d.ts → icons/presenter-heroicons-data.d.ts} +1 -0
- package/dist/presenter/{presenter-heroicons-data.js → icons/presenter-heroicons-data.js} +1 -0
- package/dist/presenter/{presenter-icons.d.ts → icons/presenter-icons.d.ts} +1 -0
- package/dist/presenter/{presenter-icons.js → icons/presenter-icons.js} +1 -0
- package/dist/presenter/presenter-account-styles.d.ts +1 -0
- package/dist/presenter/presenter-account-styles.js +99 -0
- package/dist/presenter/presenter-account.d.ts +40 -0
- package/dist/presenter/presenter-account.js +178 -0
- package/dist/presenter/presenter-config.d.ts +9 -2
- package/dist/presenter/presenter-config.js +3 -1
- package/dist/presenter/presenter-controls.d.ts +1 -6
- package/dist/presenter/presenter-controls.js +7 -2
- package/dist/presenter/presenter-dock-layout.js +1 -1
- package/dist/presenter/presenter-drag.d.ts +1 -19
- package/dist/presenter/presenter-drag.js +3 -50
- package/dist/presenter/presenter-glow.js +1 -1
- package/dist/presenter/presenter-hud-position.d.ts +18 -0
- package/dist/presenter/presenter-hud-position.js +50 -0
- package/dist/presenter/presenter-offer.d.ts +25 -0
- package/dist/presenter/presenter-offer.js +87 -0
- package/dist/presenter/presenter-report-styles.js +20 -1
- package/dist/presenter/presenter-report.d.ts +2 -1
- package/dist/presenter/presenter-report.js +31 -23
- package/dist/presenter/presenter-run-state.d.ts +1 -1
- package/dist/presenter/presenter-run-state.js +1 -1
- package/dist/presenter/presenter-settings-styles.js +2 -1
- package/dist/presenter/presenter-settings.d.ts +8 -0
- package/dist/presenter/presenter-settings.js +58 -3
- package/dist/presenter/presenter-shell-styles.js +33 -15
- package/dist/presenter/presenter-shell.d.ts +8 -0
- package/dist/presenter/presenter-shell.js +47 -4
- package/dist/presenter/presenter-styles.d.ts +2 -1
- package/dist/presenter/presenter-styles.js +8 -3
- package/dist/presenter/presenter-workspace.d.ts +4 -0
- package/dist/presenter/presenter-workspace.js +29 -2
- package/dist/presenter/presenter.d.ts +3 -3
- package/dist/presenter/presenter.js +28 -14
- package/dist/presenter/tour/tour-view.d.ts +33 -0
- package/dist/presenter/tour/tour-view.js +192 -0
- package/dist/presenter/tour/tour.d.ts +25 -0
- package/dist/presenter/tour/tour.js +264 -0
- package/dist/recorder/recorder.js +6 -0
- package/dist/registry/declared-channels.d.ts +2 -0
- package/dist/registry/declared-channels.js +14 -0
- package/dist/registry/declared-platform.d.ts +4 -0
- package/dist/registry/declared-platform.js +34 -0
- package/dist/registry/emitter.js +1 -1
- package/dist/registry/store-adapters.js +1 -1
- package/dist/registry/stores.js +1 -1
- package/dist/reticle-presenter-helpers.js +10 -6
- package/dist/reticle-singleton.d.ts +2 -0
- package/dist/reticle-singleton.js +3 -0
- package/dist/reticle.js +99 -35
- package/dist/review/annotator-styles.js +1 -1
- package/dist/review/annotator.js +2 -2
- package/dist/review/mark-anchor.js +3 -3
- package/dist/transport/transport.js +2 -2
- package/package.json +5 -4
- package/dist/presenter/presenter-test-helpers.d.ts +0 -14
- package/dist/presenter/presenter-test-helpers.js +0 -39
- /package/dist/actions/{synthetic-input.d.ts → synthetic/synthetic-input.d.ts} +0 -0
- /package/dist/actions/{synthetic-input.js → synthetic/synthetic-input.js} +0 -0
- /package/dist/dom/{auto-anchor.d.ts → addressing/auto-anchor.d.ts} +0 -0
- /package/dist/dom/{auto-anchor.js → addressing/auto-anchor.js} +0 -0
- /package/dist/dom/{refs.d.ts → addressing/refs.d.ts} +0 -0
- /package/dist/dom/{refs.js → addressing/refs.js} +0 -0
- /package/dist/dom/{source.d.ts → addressing/source.d.ts} +0 -0
- /package/dist/dom/{source.js → addressing/source.js} +0 -0
- /package/dist/observers/{network-body.d.ts → net-detail/network-body.d.ts} +0 -0
- /package/dist/observers/{network-redact.d.ts → net-detail/network-redact.d.ts} +0 -0
- /package/dist/observers/{network-redact.js → net-detail/network-redact.js} +0 -0
- /package/dist/presenter/{overlay.d.ts → chrome/overlay.d.ts} +0 -0
- /package/dist/presenter/{overlay.js → chrome/overlay.js} +0 -0
- /package/dist/presenter/{presenter-brand.d.ts → chrome/presenter-brand.d.ts} +0 -0
- /package/dist/presenter/{presenter-brand.js → chrome/presenter-brand.js} +0 -0
- /package/dist/presenter/{presenter-effects.d.ts → chrome/presenter-effects.d.ts} +0 -0
- /package/dist/presenter/{presenter-hud-chrome.d.ts → chrome/presenter-hud-chrome.d.ts} +0 -0
- /package/dist/presenter/{presenter-hud-chrome.js → chrome/presenter-hud-chrome.js} +0 -0
- /package/dist/presenter/{presenter-report-copy.d.ts → chrome/presenter-report-copy.d.ts} +0 -0
- /package/dist/presenter/{presenter-report-copy.js → chrome/presenter-report-copy.js} +0 -0
- /package/dist/presenter/{presenter-tally.d.ts → chrome/presenter-tally.d.ts} +0 -0
- /package/dist/presenter/{presenter-verbs.d.ts → chrome/presenter-verbs.d.ts} +0 -0
- /package/dist/presenter/{presenter-verbs.js → chrome/presenter-verbs.js} +0 -0
- /package/dist/registry/{adapters.d.ts → stores/adapters.d.ts} +0 -0
- /package/dist/registry/{adapters.js → stores/adapters.js} +0 -0
- /package/dist/timers/{native-console.d.ts → console/native-console.d.ts} +0 -0
- /package/dist/timers/{native-console.js → console/native-console.js} +0 -0
- /package/dist/timers/{native-timers.d.ts → native/native-timers.d.ts} +0 -0
- /package/dist/timers/{native-timers.js → native/native-timers.js} +0 -0
|
@@ -4,6 +4,7 @@ interface ClickGeometry {
|
|
|
4
4
|
scrolledIntoView: boolean;
|
|
5
5
|
}
|
|
6
6
|
export declare const NO_GEOMETRY: ClickGeometry;
|
|
7
|
+
export declare function mouseEventFor(el: Element, type: string, init?: MouseEventInit): MouseEvent;
|
|
7
8
|
export declare function fireClickSequence(el: HTMLElement, hold?: {
|
|
8
9
|
ms: number;
|
|
9
10
|
sleep: (ms: number) => Promise<void>;
|
|
@@ -16,4 +17,12 @@ export declare function clickGeometry(el: HTMLElement): ClickGeometry;
|
|
|
16
17
|
export declare function firePointer(el: Element, type: string, relatedTarget?: EventTarget | null): void;
|
|
17
18
|
export declare function firePointerNonBubbling(el: Element, type: string, relatedTarget?: EventTarget | null): void;
|
|
18
19
|
export declare function dragElement(source: HTMLElement, target: HTMLElement | null, data: unknown): Promise<boolean>;
|
|
20
|
+
export declare function fireTapSequence(el: HTMLElement, hold: {
|
|
21
|
+
ms: number;
|
|
22
|
+
sleep: (ms: number) => Promise<void>;
|
|
23
|
+
now: () => number;
|
|
24
|
+
} | undefined): Promise<{
|
|
25
|
+
prevented: boolean;
|
|
26
|
+
heldMs: number;
|
|
27
|
+
}>;
|
|
19
28
|
export {};
|
|
@@ -1,17 +1,26 @@
|
|
|
1
|
-
import { refs } from '../dom/refs.js';
|
|
1
|
+
import { refs } from '../dom/addressing/refs.js';
|
|
2
2
|
import { hitTestOccluder } from '../dom/occlusion.js';
|
|
3
|
-
import { nativeFrame } from '../timers/native-timers.js';
|
|
4
|
-
import { asSyntheticInput } from './synthetic-input.js';
|
|
3
|
+
import { nativeFrame } from '../timers/native/native-timers.js';
|
|
4
|
+
import { asSyntheticInput } from './synthetic/synthetic-input.js';
|
|
5
5
|
export const NO_GEOMETRY = {
|
|
6
6
|
occluded: false,
|
|
7
7
|
occludedBy: null,
|
|
8
8
|
scrolledIntoView: false,
|
|
9
9
|
};
|
|
10
|
+
export function mouseEventFor(el, type, init = {}) {
|
|
11
|
+
return new MouseEvent(type, { ...init, view: el.ownerDocument.defaultView });
|
|
12
|
+
}
|
|
13
|
+
function pointerEventFor(el, type, init) {
|
|
14
|
+
const full = { ...init, view: el.ownerDocument.defaultView };
|
|
15
|
+
return 'function' === typeof PointerEvent
|
|
16
|
+
? new PointerEvent(type, full)
|
|
17
|
+
: new MouseEvent(type, full);
|
|
18
|
+
}
|
|
10
19
|
export async function fireClickSequence(el, hold) {
|
|
11
20
|
const doc = el.ownerDocument;
|
|
12
21
|
const from = doc.activeElement ?? doc.body;
|
|
13
22
|
firePointer(el, 'pointerdown', from);
|
|
14
|
-
asSyntheticInput(() => el.dispatchEvent(
|
|
23
|
+
asSyntheticInput(() => el.dispatchEvent(mouseEventFor(el, 'mousedown', { bubbles: true, cancelable: true })));
|
|
15
24
|
if (el.tabIndex >= 0 && 'function' === typeof el.focus)
|
|
16
25
|
el.focus();
|
|
17
26
|
let heldMs = 0;
|
|
@@ -21,8 +30,8 @@ export async function fireClickSequence(el, hold) {
|
|
|
21
30
|
heldMs = hold.now() - startedAt;
|
|
22
31
|
}
|
|
23
32
|
firePointer(el, 'pointerup', from);
|
|
24
|
-
asSyntheticInput(() => el.dispatchEvent(
|
|
25
|
-
const notPrevented = asSyntheticInput(() => el.dispatchEvent(
|
|
33
|
+
asSyntheticInput(() => el.dispatchEvent(mouseEventFor(el, 'mouseup', { bubbles: true, cancelable: true })));
|
|
34
|
+
const notPrevented = asSyntheticInput(() => el.dispatchEvent(mouseEventFor(el, 'click', { bubbles: true, cancelable: true })));
|
|
26
35
|
return { prevented: !notPrevented, heldMs };
|
|
27
36
|
}
|
|
28
37
|
function isMeasurable(rect) {
|
|
@@ -57,24 +66,10 @@ export function clickGeometry(el) {
|
|
|
57
66
|
return { ...hitTest(el, rect), scrolledIntoView };
|
|
58
67
|
}
|
|
59
68
|
export function firePointer(el, type, relatedTarget = null) {
|
|
60
|
-
asSyntheticInput(() => {
|
|
61
|
-
if ('function' === typeof PointerEvent) {
|
|
62
|
-
el.dispatchEvent(new PointerEvent(type, { bubbles: true, cancelable: true, relatedTarget }));
|
|
63
|
-
}
|
|
64
|
-
else {
|
|
65
|
-
el.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, relatedTarget }));
|
|
66
|
-
}
|
|
67
|
-
});
|
|
69
|
+
asSyntheticInput(() => el.dispatchEvent(pointerEventFor(el, type, { bubbles: true, cancelable: true, relatedTarget })));
|
|
68
70
|
}
|
|
69
71
|
export function firePointerNonBubbling(el, type, relatedTarget = null) {
|
|
70
|
-
asSyntheticInput(() => {
|
|
71
|
-
if ('function' === typeof PointerEvent) {
|
|
72
|
-
el.dispatchEvent(new PointerEvent(type, { bubbles: false, cancelable: true, relatedTarget }));
|
|
73
|
-
}
|
|
74
|
-
else {
|
|
75
|
-
el.dispatchEvent(new MouseEvent(type, { bubbles: false, cancelable: true, relatedTarget }));
|
|
76
|
-
}
|
|
77
|
-
});
|
|
72
|
+
asSyntheticInput(() => el.dispatchEvent(pointerEventFor(el, type, { bubbles: false, cancelable: true, relatedTarget })));
|
|
78
73
|
}
|
|
79
74
|
function makeDataTransfer(data) {
|
|
80
75
|
if (typeof DataTransfer !== 'function')
|
|
@@ -117,12 +112,9 @@ export async function dragElement(source, target, data) {
|
|
|
117
112
|
button: 0,
|
|
118
113
|
...(related !== undefined ? { relatedTarget: related } : {}),
|
|
119
114
|
};
|
|
120
|
-
|
|
121
|
-
el
|
|
122
|
-
|
|
123
|
-
else {
|
|
124
|
-
el.dispatchEvent(new MouseEvent(type, init));
|
|
125
|
-
}
|
|
115
|
+
el.dispatchEvent(type.startsWith('pointer')
|
|
116
|
+
? pointerEventFor(el, type, { ...init, pointerId: 1, isPrimary: true })
|
|
117
|
+
: mouseEventFor(el, type, init));
|
|
126
118
|
};
|
|
127
119
|
const sourceRect = source.getBoundingClientRect();
|
|
128
120
|
const destRect = target !== null ? dest.getBoundingClientRect() : null;
|
|
@@ -176,3 +168,64 @@ export async function dragElement(source, target, data) {
|
|
|
176
168
|
}
|
|
177
169
|
return dropPrevented;
|
|
178
170
|
}
|
|
171
|
+
export async function fireTapSequence(el, hold) {
|
|
172
|
+
const touches = touchListFor(el);
|
|
173
|
+
firePointerTouch(el, 'pointerdown');
|
|
174
|
+
asSyntheticInput(() => el.dispatchEvent(makeTouchEvent('touchstart', touches)));
|
|
175
|
+
if (el.tabIndex >= 0 && 'function' === typeof el.focus)
|
|
176
|
+
el.focus();
|
|
177
|
+
let heldMs = 0;
|
|
178
|
+
if (hold !== undefined && hold.ms > 0) {
|
|
179
|
+
const started = hold.now();
|
|
180
|
+
await hold.sleep(hold.ms);
|
|
181
|
+
heldMs = hold.now() - started;
|
|
182
|
+
}
|
|
183
|
+
asSyntheticInput(() => el.dispatchEvent(makeTouchEvent('touchend', [])));
|
|
184
|
+
firePointerTouch(el, 'pointerup');
|
|
185
|
+
const clicked = asSyntheticInput(() => el.dispatchEvent(mouseEventFor(el, 'click', { bubbles: true, cancelable: true })));
|
|
186
|
+
return { prevented: !clicked, heldMs };
|
|
187
|
+
}
|
|
188
|
+
function firePointerTouch(el, type) {
|
|
189
|
+
asSyntheticInput(() => el.dispatchEvent(pointerEventFor(el, type, {
|
|
190
|
+
bubbles: true,
|
|
191
|
+
cancelable: true,
|
|
192
|
+
pointerType: 'touch',
|
|
193
|
+
isPrimary: true,
|
|
194
|
+
width: 23,
|
|
195
|
+
height: 23,
|
|
196
|
+
})));
|
|
197
|
+
}
|
|
198
|
+
function touchListFor(el) {
|
|
199
|
+
if ('function' !== typeof Touch || 'function' !== typeof el.getBoundingClientRect)
|
|
200
|
+
return [];
|
|
201
|
+
const rect = el.getBoundingClientRect();
|
|
202
|
+
try {
|
|
203
|
+
return [
|
|
204
|
+
new Touch({
|
|
205
|
+
identifier: 0,
|
|
206
|
+
target: el,
|
|
207
|
+
clientX: rect.left + rect.width / 2,
|
|
208
|
+
clientY: rect.top + rect.height / 2,
|
|
209
|
+
}),
|
|
210
|
+
];
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function makeTouchEvent(type, touches) {
|
|
217
|
+
if ('function' === typeof TouchEvent) {
|
|
218
|
+
try {
|
|
219
|
+
return new TouchEvent(type, {
|
|
220
|
+
bubbles: true,
|
|
221
|
+
cancelable: true,
|
|
222
|
+
touches,
|
|
223
|
+
targetTouches: touches,
|
|
224
|
+
changedTouches: touches,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return new Event(type, { bubbles: true, cancelable: true });
|
|
231
|
+
}
|
|
@@ -8,3 +8,7 @@ export declare function pressModifiers(args: Record<string, unknown>): {
|
|
|
8
8
|
export declare function pressCode(args: Record<string, unknown>, key: string): string;
|
|
9
9
|
export declare function focusedOrDocument(): HTMLElement;
|
|
10
10
|
export declare function isReflessDocumentPress(ref: string, action: string, args: Record<string, unknown>): boolean;
|
|
11
|
+
export declare function pressKeys(args: Record<string, unknown>): string[];
|
|
12
|
+
export declare function holdKey(el: HTMLElement, key: string, code: string, mods: Record<string, boolean>, ms: number): Promise<void>;
|
|
13
|
+
export declare function pressCombo(el: HTMLElement, keys: readonly string[], mods: Record<string, boolean>, holdMs: number): Promise<boolean>;
|
|
14
|
+
export declare const ZOOM_NEEDS_REAL_BROWSER_MSG: string;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { ActionType, isGlobalPress, pressKeyFromArgs } from '@reticlehq/core';
|
|
2
|
+
import { asSyntheticInput } from './synthetic/synthetic-input.js';
|
|
3
|
+
import { nativeSetTimeout } from '../timers/native/native-timers.js';
|
|
4
|
+
const sleep = (ms) => new Promise((r) => nativeSetTimeout(r, ms));
|
|
2
5
|
import { isHtmlElement } from '../dom/realm.js';
|
|
3
6
|
function asString(value, fallback = '') {
|
|
4
7
|
return 'string' === typeof value ? value : fallback;
|
|
@@ -75,3 +78,46 @@ export function focusedOrDocument() {
|
|
|
75
78
|
export function isReflessDocumentPress(ref, action, args) {
|
|
76
79
|
return 0 === ref.length && isGlobalPress(args) && ActionType.PRESS === action;
|
|
77
80
|
}
|
|
81
|
+
export function pressKeys(args) {
|
|
82
|
+
const raw = args['keys'];
|
|
83
|
+
if (!Array.isArray(raw))
|
|
84
|
+
return [];
|
|
85
|
+
return raw.map((k) => asString(k)).filter((k) => k.length > 0);
|
|
86
|
+
}
|
|
87
|
+
const KEY_REPEAT_MS = 33;
|
|
88
|
+
const KEY_REPEAT_DELAY_MS = 500;
|
|
89
|
+
export async function holdKey(el, key, code, mods, ms) {
|
|
90
|
+
const started = Date.now();
|
|
91
|
+
if (ms > KEY_REPEAT_DELAY_MS)
|
|
92
|
+
await sleep(KEY_REPEAT_DELAY_MS);
|
|
93
|
+
while (Date.now() - started < ms) {
|
|
94
|
+
asSyntheticInput(() => el.dispatchEvent(new KeyboardEvent('keydown', {
|
|
95
|
+
key,
|
|
96
|
+
code,
|
|
97
|
+
bubbles: true,
|
|
98
|
+
cancelable: true,
|
|
99
|
+
repeat: true,
|
|
100
|
+
...mods,
|
|
101
|
+
})));
|
|
102
|
+
await sleep(Math.min(KEY_REPEAT_MS, Math.max(0, ms - (Date.now() - started))));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export async function pressCombo(el, keys, mods, holdMs) {
|
|
106
|
+
let prevented = false;
|
|
107
|
+
for (const key of keys) {
|
|
108
|
+
const code = pressCode({}, key);
|
|
109
|
+
const ok = asSyntheticInput(() => el.dispatchEvent(new KeyboardEvent('keydown', { key, code, bubbles: true, cancelable: true, ...mods })));
|
|
110
|
+
if (!ok)
|
|
111
|
+
prevented = true;
|
|
112
|
+
}
|
|
113
|
+
if (holdMs > 0)
|
|
114
|
+
await sleep(holdMs);
|
|
115
|
+
for (const key of [...keys].reverse()) {
|
|
116
|
+
const code = pressCode({}, key);
|
|
117
|
+
asSyntheticInput(() => el.dispatchEvent(new KeyboardEvent('keyup', { key, code, bubbles: true, ...mods })));
|
|
118
|
+
}
|
|
119
|
+
return prevented;
|
|
120
|
+
}
|
|
121
|
+
export const ZOOM_NEEDS_REAL_BROWSER_MSG = 'cannot zoom from inside the page — CSS zoom changes how it LOOKS without changing the layout ' +
|
|
122
|
+
'viewport, visualViewport or media queries, so a layout that breaks at 200% would be reported as ' +
|
|
123
|
+
'checked and passing. Drive a real browser (reticle drive <url>, or reticle_lease) and zoom there.';
|
|
@@ -35,6 +35,7 @@ interface ActionResult {
|
|
|
35
35
|
warning?: string;
|
|
36
36
|
}
|
|
37
37
|
export declare const MAX_HOLD_MS = 30000;
|
|
38
|
+
export declare const ZOOM_NEEDS_REAL_BROWSER_MSG: string;
|
|
38
39
|
export declare function executeAction(ref: string, action: string, args?: Record<string, unknown>): Promise<ActionResult>;
|
|
39
40
|
export declare function dispatchWebMcp(tool: string, params: Record<string, unknown>, confirmDangerous?: boolean): Promise<unknown>;
|
|
40
41
|
export interface ActionStep {
|
package/dist/actions/actions.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { ActionType, ActionWarning, DANGEROUS_ACTION_CONFIRM_ARG, ElementState, NATIVE_INPUT_ARG, SettleReason, } from '@reticlehq/core';
|
|
2
|
-
import { asSyntheticInput } from './synthetic-input.js';
|
|
3
|
-
import { echoRef, refs } from '../dom/refs.js';
|
|
2
|
+
import { asSyntheticInput } from './synthetic/synthetic-input.js';
|
|
3
|
+
import { echoRef, refs } from '../dom/addressing/refs.js';
|
|
4
4
|
import { dangerousActionContext, requiresDangerousConfirmation, submitControlFor, } from './danger-context.js';
|
|
5
5
|
import { assertEditable, assertNotRichText, setNativeValue } from './value-input.js';
|
|
6
6
|
import { getAccessibleName, getRole, isVisible, getStates } from '../dom/a11y.js';
|
|
7
|
-
import { elementHasHoverHandlers, identifyComponent } from '../registry/adapters.js';
|
|
7
|
+
import { elementHasHoverHandlers, identifyComponent } from '../registry/stores/adapters.js';
|
|
8
8
|
import { isForm, isHtmlElement, isInput, isSelect, isTextArea } from '../dom/realm.js';
|
|
9
|
-
import { nativeSetTimeout, settle } from '../timers/native-timers.js';
|
|
9
|
+
import { nativeSetTimeout, settle } from '../timers/native/native-timers.js';
|
|
10
10
|
import { AppearedText } from './appeared-text.js';
|
|
11
|
-
import { focusedOrDocument, isReflessDocumentPress, pressCode, pressKey, pressModifiers, } from './actions-press.js';
|
|
11
|
+
import { focusedOrDocument, isReflessDocumentPress, pressCode, pressKey, pressModifiers, pressKeys, holdKey, pressCombo, } from './actions-press.js';
|
|
12
12
|
function asString(value, fallback = '') {
|
|
13
13
|
return 'string' === typeof value ? value : fallback;
|
|
14
14
|
}
|
|
@@ -124,7 +124,8 @@ function assertActionAllowed(el, action, args) {
|
|
|
124
124
|
throw new Error(`potentially destructive action blocked; retry with args.${DANGEROUS_ACTION_CONFIRM_ARG}=true`);
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
|
-
import { NO_GEOMETRY, fireClickSequence, clickGeometry, firePointer, firePointerNonBubbling, dragElement, } from './actions-dom.js';
|
|
127
|
+
import { NO_GEOMETRY, fireClickSequence, fireTapSequence, clickGeometry, firePointer, firePointerNonBubbling, dragElement, mouseEventFor, } from './actions-dom.js';
|
|
128
|
+
import { scrollFrom } from './scroll.js';
|
|
128
129
|
function enabledOf(el) {
|
|
129
130
|
return !getStates(el).includes(ElementState.DISABLED);
|
|
130
131
|
}
|
|
@@ -151,21 +152,31 @@ async function dispatchFor(el, action, args) {
|
|
|
151
152
|
const hold = clampHold(args['holdMs']);
|
|
152
153
|
return await fireClickSequence(el, 0 === hold ? undefined : { ms: hold, sleep, now: () => Date.now() });
|
|
153
154
|
}
|
|
155
|
+
if (ActionType.TAP === action) {
|
|
156
|
+
const hold = clampHold(args['holdMs']);
|
|
157
|
+
return await fireTapSequence(el, 0 === hold ? undefined : { ms: hold, sleep, now: () => Date.now() });
|
|
158
|
+
}
|
|
154
159
|
return { prevented: await dispatchOther(el, action, args), heldMs: 0 };
|
|
155
160
|
}
|
|
161
|
+
export const ZOOM_NEEDS_REAL_BROWSER_MSG = 'cannot zoom from inside the page — CSS zoom changes how it LOOKS without changing the layout ' +
|
|
162
|
+
'viewport, visualViewport or media queries, so a layout that breaks at 200% would be reported as ' +
|
|
163
|
+
'checked and passing. Drive a real browser (reticle drive <url>, or reticle_lease) and zoom there.';
|
|
164
|
+
function asFiniteNumber(raw) {
|
|
165
|
+
return 'number' === typeof raw && Number.isFinite(raw) ? raw : undefined;
|
|
166
|
+
}
|
|
156
167
|
async function dispatchOther(el, action, args) {
|
|
157
168
|
switch (action) {
|
|
158
169
|
case ActionType.DBLCLICK:
|
|
159
|
-
return !asSyntheticInput(() => el.dispatchEvent(
|
|
170
|
+
return !asSyntheticInput(() => el.dispatchEvent(mouseEventFor(el, 'dblclick', { bubbles: true, cancelable: true })));
|
|
160
171
|
case ActionType.HOVER: {
|
|
161
172
|
const doc = el.ownerDocument;
|
|
162
173
|
const from = doc.activeElement ?? doc.body;
|
|
163
174
|
firePointer(el, 'pointerover', from);
|
|
164
|
-
el.dispatchEvent(
|
|
175
|
+
el.dispatchEvent(mouseEventFor(el, 'mouseover', { bubbles: true, relatedTarget: from }));
|
|
165
176
|
firePointerNonBubbling(el, 'pointerenter', from);
|
|
166
|
-
el.dispatchEvent(
|
|
177
|
+
el.dispatchEvent(mouseEventFor(el, 'mouseenter', { relatedTarget: from }));
|
|
167
178
|
firePointer(el, 'pointermove', from);
|
|
168
|
-
const moved = el.dispatchEvent(
|
|
179
|
+
const moved = el.dispatchEvent(mouseEventFor(el, 'mousemove', { bubbles: true, cancelable: true }));
|
|
169
180
|
const holdMs = clampHold(args['holdMs']);
|
|
170
181
|
if (holdMs > 0)
|
|
171
182
|
await sleep(holdMs);
|
|
@@ -241,7 +252,11 @@ async function dispatchOther(el, action, args) {
|
|
|
241
252
|
}
|
|
242
253
|
if (alreadyAtCheckedState(el, action))
|
|
243
254
|
return false;
|
|
244
|
-
const event =
|
|
255
|
+
const event = mouseEventFor(el, 'click', {
|
|
256
|
+
bubbles: true,
|
|
257
|
+
cancelable: true,
|
|
258
|
+
composed: true,
|
|
259
|
+
});
|
|
245
260
|
const notPrevented = asSyntheticInput(() => el.dispatchEvent(event));
|
|
246
261
|
return !notPrevented || event.defaultPrevented;
|
|
247
262
|
}
|
|
@@ -253,16 +268,31 @@ async function dispatchOther(el, action, args) {
|
|
|
253
268
|
return false;
|
|
254
269
|
}
|
|
255
270
|
case ActionType.PRESS: {
|
|
271
|
+
const mods = pressModifiers(args);
|
|
272
|
+
const combo = pressKeys(args);
|
|
273
|
+
if (combo.length > 0)
|
|
274
|
+
return await pressCombo(el, combo, mods, clampHold(args['holdMs']));
|
|
256
275
|
const key = pressKey(args);
|
|
257
276
|
const code = pressCode(args, key);
|
|
258
|
-
const mods = pressModifiers(args);
|
|
259
277
|
const down = asSyntheticInput(() => el.dispatchEvent(new KeyboardEvent('keydown', { key, code, bubbles: true, cancelable: true, ...mods })));
|
|
278
|
+
const hold = clampHold(args['holdMs']);
|
|
279
|
+
if (hold > 0)
|
|
280
|
+
await holdKey(el, key, code, mods, hold);
|
|
260
281
|
asSyntheticInput(() => el.dispatchEvent(new KeyboardEvent('keyup', { key, code, bubbles: true, ...mods })));
|
|
261
282
|
return !down;
|
|
262
283
|
}
|
|
263
284
|
case ActionType.SCROLL_INTO_VIEW:
|
|
264
285
|
el.scrollIntoView();
|
|
265
286
|
return false;
|
|
287
|
+
case ActionType.ZOOM:
|
|
288
|
+
throw new Error(ZOOM_NEEDS_REAL_BROWSER_MSG);
|
|
289
|
+
case ActionType.SCROLL: {
|
|
290
|
+
const dy = asFiniteNumber(args['dy']);
|
|
291
|
+
const dx = asFiniteNumber(args['dx']);
|
|
292
|
+
const fraction = asFiniteNumber(args['fraction']);
|
|
293
|
+
scrollFrom(el, dy, fraction, dx);
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
266
296
|
case ActionType.UPLOAD: {
|
|
267
297
|
if (!isInput(el) || el.type !== 'file') {
|
|
268
298
|
throw new Error('upload target must be a <input type="file">');
|
|
@@ -302,6 +332,12 @@ async function dispatchOther(el, action, args) {
|
|
|
302
332
|
throw new Error(`unknown action '${action}'`);
|
|
303
333
|
}
|
|
304
334
|
}
|
|
335
|
+
function asSettleBudget(raw) {
|
|
336
|
+
if ('number' !== typeof raw || !Number.isFinite(raw))
|
|
337
|
+
return undefined;
|
|
338
|
+
return Math.max(0, Math.min(raw, FRAME_BUDGET_MS_CEILING));
|
|
339
|
+
}
|
|
340
|
+
const FRAME_BUDGET_MS_CEILING = 200;
|
|
305
341
|
export async function executeAction(ref, action, args = {}) {
|
|
306
342
|
const named = 0 < ref.length;
|
|
307
343
|
const el = isReflessDocumentPress(ref, action, args) ? focusedOrDocument() : requireElement(ref);
|
|
@@ -336,7 +372,8 @@ export async function executeAction(ref, action, args = {}) {
|
|
|
336
372
|
heldMs = outcome.heldMs;
|
|
337
373
|
}
|
|
338
374
|
finally {
|
|
339
|
-
const
|
|
375
|
+
const budget = asSettleBudget(args['settleMs']);
|
|
376
|
+
const outcome = await settle(budget);
|
|
340
377
|
settled = outcome.settled;
|
|
341
378
|
settleReason = outcome.settled ? null : SettleReason.TIMEOUT;
|
|
342
379
|
obs.disconnect();
|
|
@@ -6,6 +6,10 @@ const HAS_LETTER = /\p{L}/u;
|
|
|
6
6
|
function saysSomething(text) {
|
|
7
7
|
return HAS_LETTER.test(text);
|
|
8
8
|
}
|
|
9
|
+
const MAX_MESSAGE_ELEMENTS = 3;
|
|
10
|
+
function isRenderedView(node) {
|
|
11
|
+
return node instanceof Element && node.querySelectorAll('*').length > MAX_MESSAGE_ELEMENTS;
|
|
12
|
+
}
|
|
9
13
|
export class AppearedText {
|
|
10
14
|
#seen = new Set();
|
|
11
15
|
#length = 0;
|
|
@@ -20,6 +24,8 @@ export class AppearedText {
|
|
|
20
24
|
for (const node of record.addedNodes) {
|
|
21
25
|
if (this.#full())
|
|
22
26
|
return;
|
|
27
|
+
if (isRenderedView(node))
|
|
28
|
+
continue;
|
|
23
29
|
const owner = TEXT_NODE === node.nodeType ? node.parentElement : elementOf(node);
|
|
24
30
|
this.#add(node.textContent, owner);
|
|
25
31
|
}
|
package/dist/actions/scroll.d.ts
CHANGED
|
@@ -4,6 +4,13 @@ interface ScrollResult {
|
|
|
4
4
|
scrollHeight: number;
|
|
5
5
|
clientHeight: number;
|
|
6
6
|
atEnd: boolean;
|
|
7
|
+
atStart: boolean;
|
|
8
|
+
scrollLeft: number;
|
|
9
|
+
scrollWidth: number;
|
|
10
|
+
clientWidth: number;
|
|
11
|
+
atLeftEnd: boolean;
|
|
12
|
+
atRightEnd: boolean;
|
|
7
13
|
}
|
|
8
|
-
export declare function scrollContainer(ref: string | undefined, dy: number | undefined, fraction?: number): ScrollResult;
|
|
14
|
+
export declare function scrollContainer(ref: string | undefined, dy: number | undefined, fraction?: number, dx?: number): ScrollResult;
|
|
15
|
+
export declare function scrollFrom(base: Element | undefined, dy: number | undefined, fraction?: number, dx?: number): ScrollResult;
|
|
9
16
|
export {};
|
package/dist/actions/scroll.js
CHANGED
|
@@ -1,36 +1,54 @@
|
|
|
1
|
-
import { refs } from '../dom/refs.js';
|
|
1
|
+
import { refs } from '../dom/addressing/refs.js';
|
|
2
2
|
const FALLBACK_STEP_PX = 400;
|
|
3
3
|
const VIEWPORT_FRACTION = 0.8;
|
|
4
|
-
function nearestScrollable(el) {
|
|
4
|
+
function nearestScrollable(el, axis) {
|
|
5
5
|
let cur = el;
|
|
6
6
|
while (cur !== null && cur !== document.body) {
|
|
7
|
-
const
|
|
8
|
-
|
|
7
|
+
const style = getComputedStyle(cur);
|
|
8
|
+
const overflow = 'x' === axis ? style.overflowX : style.overflowY;
|
|
9
|
+
const scrollable = 'x' === axis ? cur.scrollWidth > cur.clientWidth : cur.scrollHeight > cur.clientHeight;
|
|
10
|
+
if (('auto' === overflow || 'scroll' === overflow) && scrollable)
|
|
9
11
|
return cur;
|
|
10
12
|
cur = cur.parentElement;
|
|
11
13
|
}
|
|
12
14
|
return document.scrollingElement ?? document.documentElement;
|
|
13
15
|
}
|
|
14
|
-
export function scrollContainer(ref, dy, fraction) {
|
|
16
|
+
export function scrollContainer(ref, dy, fraction, dx) {
|
|
15
17
|
const base = ref !== undefined ? refs.resolve(ref) : null;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
return scrollFrom(base instanceof Element ? base : undefined, dy, fraction, dx);
|
|
19
|
+
}
|
|
20
|
+
export function scrollFrom(base, dy, fraction, dx) {
|
|
21
|
+
const axis = dx !== undefined && dx !== 0 && (dy === undefined || 0 === dy) ? 'x' : 'y';
|
|
22
|
+
const target = base !== undefined
|
|
23
|
+
? nearestScrollable(base, axis)
|
|
18
24
|
: (document.scrollingElement ?? document.documentElement);
|
|
19
25
|
const before = target.scrollTop;
|
|
26
|
+
const beforeLeft = target.scrollLeft;
|
|
20
27
|
if (fraction !== undefined && fraction >= 0 && fraction <= 1) {
|
|
21
28
|
target.scrollTop = Math.round(target.scrollHeight * fraction);
|
|
22
29
|
}
|
|
23
30
|
else {
|
|
24
|
-
|
|
25
|
-
|
|
31
|
+
if (dx !== undefined && dx !== 0)
|
|
32
|
+
target.scrollLeft = beforeLeft + dx;
|
|
33
|
+
if (dx === undefined || (dy !== undefined && 0 !== dy)) {
|
|
34
|
+
const step = dy ?? (Math.round(target.clientHeight * VIEWPORT_FRACTION) || FALLBACK_STEP_PX);
|
|
35
|
+
target.scrollTop = before + step;
|
|
36
|
+
}
|
|
26
37
|
}
|
|
27
38
|
target.dispatchEvent(new Event('scroll', { bubbles: false }));
|
|
28
39
|
const after = target.scrollTop;
|
|
40
|
+
const afterLeft = target.scrollLeft;
|
|
29
41
|
return {
|
|
30
|
-
scrolled: after !== before,
|
|
42
|
+
scrolled: after !== before || afterLeft !== beforeLeft,
|
|
31
43
|
scrollTop: after,
|
|
32
44
|
scrollHeight: target.scrollHeight,
|
|
33
45
|
clientHeight: target.clientHeight,
|
|
34
46
|
atEnd: after + target.clientHeight >= target.scrollHeight - 1,
|
|
47
|
+
atStart: after <= 1,
|
|
48
|
+
scrollLeft: afterLeft,
|
|
49
|
+
scrollWidth: target.scrollWidth,
|
|
50
|
+
clientWidth: target.clientWidth,
|
|
51
|
+
atLeftEnd: afterLeft <= 1,
|
|
52
|
+
atRightEnd: afterLeft + target.clientWidth >= target.scrollWidth - 1,
|
|
35
53
|
};
|
|
36
54
|
}
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { ActionType, ComponentStateReason, DANGEROUS_ACTION_CONFIRM_ARG, ElementQuerySchema, ReticleCommand, SnapshotMode, TRANSPORT_LIMITS, selectPath, capDepth, } from '@reticlehq/core';
|
|
2
2
|
import { buildSnapshot } from '../dom/snapshot.js';
|
|
3
|
+
import { paintContextOf } from '../dom/paint-context.js';
|
|
3
4
|
import { matchQuery, runQuery } from '../dom/query.js';
|
|
4
5
|
import { executeAction, executeSequence, dispatchWebMcp, } from '../actions/actions.js';
|
|
5
6
|
import { describe } from '../dom/a11y.js';
|
|
6
|
-
import { documentHasSourceStamps, sourceFor, formatSource } from '../dom/source.js';
|
|
7
|
+
import { documentHasSourceStamps, sourceFor, formatSource } from '../dom/addressing/source.js';
|
|
7
8
|
import { themeReport } from '../dom/theme.js';
|
|
8
|
-
import { refs } from '../dom/refs.js';
|
|
9
|
+
import { refs } from '../dom/addressing/refs.js';
|
|
9
10
|
import { editEpoch } from '../edit-epoch.js';
|
|
10
11
|
import { isButton, isInput } from '../dom/realm.js';
|
|
11
12
|
import { hitTestOccluder } from '../dom/occlusion.js';
|
|
12
13
|
import { readStorage } from '../observers/storage.js';
|
|
13
14
|
import { captureDesktopWindow } from '../dom/desktop-capture.js';
|
|
14
|
-
import { identifyComponent, readComponentState } from '../registry/adapters.js';
|
|
15
|
+
import { identifyComponent, readComponentState } from '../registry/stores/adapters.js';
|
|
15
16
|
import { readStoresWithTruncation, readStoresRaw, storeNames } from '../registry/stores.js';
|
|
16
17
|
import { sanitizeWithReport } from '../security/serialization.js';
|
|
17
18
|
import { getCapabilities } from '../registry/capabilities.js';
|
|
@@ -84,10 +85,16 @@ function inspect(ref) {
|
|
|
84
85
|
scrollHeight: el.scrollHeight,
|
|
85
86
|
clientHeight: el.clientHeight,
|
|
86
87
|
overflowY: cs?.overflowY ?? 'visible',
|
|
88
|
+
scrollLeft: el.scrollLeft,
|
|
89
|
+
scrollWidth: el.scrollWidth,
|
|
90
|
+
clientWidth: el.clientWidth,
|
|
91
|
+
overflowX: cs?.overflowX ?? 'visible',
|
|
87
92
|
};
|
|
93
|
+
const paintContext = paintContextOf(el);
|
|
88
94
|
return {
|
|
89
95
|
...describe(el),
|
|
90
96
|
...(source !== undefined ? { source } : {}),
|
|
97
|
+
...(paintContext.length > 0 ? { paintContext } : {}),
|
|
91
98
|
...(sourceUnavailable !== undefined ? { sourceUnavailable } : {}),
|
|
92
99
|
tag: el.tagName.toLowerCase(),
|
|
93
100
|
href: el.getAttribute('href') ?? undefined,
|
package/dist/dom/a11y.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { ElementState, REDACTED_VALUE } from '@reticlehq/core';
|
|
2
2
|
import { isButton, isHtmlElement, isImage, isInput, isSelect, isTextArea } from './realm.js';
|
|
3
|
-
import { refs } from './refs.js';
|
|
3
|
+
import { refs } from './addressing/refs.js';
|
|
4
4
|
import { inspectChart } from './chart.js';
|
|
5
5
|
import { isSensitiveKey } from '../security/serialization.js';
|
|
6
|
-
import { formatSource, sourceFromDom } from './source.js';
|
|
6
|
+
import { formatSource, sourceFromDom } from './addressing/source.js';
|
|
7
7
|
const NAME_FROM_CONTENT = new Set([
|
|
8
8
|
'button',
|
|
9
9
|
'link',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RETICLE_IPC_GLOBAL, RETICLE_TAURI_CAPTURE_COMMAND, VisualReason } from '@reticlehq/core';
|
|
2
|
-
import { nativeFrame } from '../timers/native-timers.js';
|
|
2
|
+
import { nativeFrame } from '../timers/native/native-timers.js';
|
|
3
3
|
import { RETICLE_OVERLAY } from './dom-ignore.js';
|
|
4
4
|
const FULL_PAGE_UNSUPPORTED_MARKER = VisualReason.FULL_PAGE_UNSUPPORTED;
|
|
5
5
|
const RECOGNISED_MARKERS = [
|
package/dist/dom/dom-ignore.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const RETICLE_OVERLAY = '[data-reticle-overlay],[data-reticle-cursor],[data-reticle-hud],[data-reticle-glow],[data-reticle-mark],[data-reticle-blocker]';
|
|
1
|
+
export const RETICLE_OVERLAY = '[data-reticle-overlay],[data-reticle-cursor],[data-reticle-hud],[data-reticle-glow],[data-reticle-mark],[data-reticle-blocker],[data-reticle-tour]';
|
|
2
2
|
const DEV_OVERLAYS = '[data-agentation],#__next-build-watcher,nextjs-portal,[data-nextjs-dialog],[data-nextjs-toast]';
|
|
3
3
|
let extraIgnore = '';
|
|
4
4
|
let presenterVisible = false;
|
package/dist/dom/occlusion.js
CHANGED
|
@@ -1,13 +1,39 @@
|
|
|
1
1
|
import { isReticleUi } from './dom-ignore.js';
|
|
2
|
+
const SAMPLE_POINTS = [
|
|
3
|
+
[0.5, 0.5],
|
|
4
|
+
[0.25, 0.25],
|
|
5
|
+
[0.75, 0.25],
|
|
6
|
+
[0.25, 0.75],
|
|
7
|
+
[0.75, 0.75],
|
|
8
|
+
];
|
|
9
|
+
const BLOCKED_MAJORITY = 3;
|
|
2
10
|
export function hitTestOccluder(el, rect) {
|
|
3
11
|
if (0 === rect.width || 0 === rect.height)
|
|
4
12
|
return null;
|
|
5
13
|
const doc = el.ownerDocument;
|
|
6
14
|
if (typeof doc.elementFromPoint !== 'function')
|
|
7
15
|
return null;
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
16
|
+
const withStack = doc;
|
|
17
|
+
const topAt = (x, y) => {
|
|
18
|
+
const found = withStack.elementsFromPoint?.(x, y) ?? [doc.elementFromPoint(x, y)];
|
|
19
|
+
return found.find((c) => null !== c && !isReticleUi(c)) ?? null;
|
|
20
|
+
};
|
|
21
|
+
const blockers = new Map();
|
|
22
|
+
for (const [fx, fy] of SAMPLE_POINTS) {
|
|
23
|
+
const top = topAt(rect.left + rect.width * fx, rect.top + rect.height * fy);
|
|
24
|
+
if (null === top)
|
|
25
|
+
continue;
|
|
26
|
+
if (top === el || el.contains(top) || top.contains(el))
|
|
27
|
+
continue;
|
|
28
|
+
blockers.set(top, (blockers.get(top) ?? 0) + 1);
|
|
29
|
+
}
|
|
30
|
+
let worst = null;
|
|
31
|
+
let worstCount = 0;
|
|
32
|
+
for (const [element, count] of blockers) {
|
|
33
|
+
if (count > worstCount) {
|
|
34
|
+
worst = element;
|
|
35
|
+
worstCount = count;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return worstCount >= BLOCKED_MAJORITY ? worst : null;
|
|
13
39
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const PAINT_PROPERTIES = [
|
|
2
|
+
'filter',
|
|
3
|
+
'opacity',
|
|
4
|
+
'transform',
|
|
5
|
+
'mix-blend-mode',
|
|
6
|
+
'backdrop-filter',
|
|
7
|
+
];
|
|
8
|
+
const INERT = {
|
|
9
|
+
filter: 'none',
|
|
10
|
+
opacity: '1',
|
|
11
|
+
transform: 'none',
|
|
12
|
+
'mix-blend-mode': 'normal',
|
|
13
|
+
'backdrop-filter': 'none',
|
|
14
|
+
};
|
|
15
|
+
function describeElement(el) {
|
|
16
|
+
if (el.id.length > 0)
|
|
17
|
+
return `#${el.id}`;
|
|
18
|
+
const cls = el.getAttribute('class');
|
|
19
|
+
if (cls !== null && cls.trim().length > 0)
|
|
20
|
+
return `.${cls.trim().split(/\s+/).join('.')}`;
|
|
21
|
+
return el.tagName.toLowerCase();
|
|
22
|
+
}
|
|
23
|
+
export function paintContextOf(el) {
|
|
24
|
+
const view = el.ownerDocument.defaultView;
|
|
25
|
+
if (null === view)
|
|
26
|
+
return [];
|
|
27
|
+
const found = [];
|
|
28
|
+
for (let node = el; node !== null && node !== el.ownerDocument.documentElement; node = node.parentElement) {
|
|
29
|
+
const style = view.getComputedStyle(node);
|
|
30
|
+
for (const property of PAINT_PROPERTIES) {
|
|
31
|
+
const value = style.getPropertyValue(property);
|
|
32
|
+
if (value.length > 0 && value !== INERT[property]) {
|
|
33
|
+
found.push({ on: describeElement(node), property, value });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return found;
|
|
38
|
+
}
|