@reticlehq/browser 3.8.2 → 3.10.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/action-report.d.ts +14 -0
- package/dist/actions/action-report.js +95 -22
- package/dist/actions/actions-dom.d.ts +17 -6
- package/dist/actions/actions-dom.js +98 -47
- package/dist/actions/actions-press.d.ts +10 -0
- package/dist/actions/actions-press.js +146 -2
- package/dist/actions/actions.js +64 -24
- package/dist/actions/insert-text.d.ts +3 -0
- package/dist/actions/insert-text.js +90 -0
- package/dist/actions/scroll.d.ts +1 -1
- package/dist/actions/scroll.js +25 -1
- package/dist/actions/value-input.d.ts +2 -0
- package/dist/actions/value-input.js +30 -9
- package/dist/commands/commands.js +85 -9
- package/dist/connect-options.d.ts +11 -1
- package/dist/connect-options.js +27 -1
- package/dist/dom/a11y.d.ts +1 -1
- package/dist/dom/a11y.js +11 -10
- package/dist/dom/chart-geometry.d.ts +7 -0
- package/dist/dom/chart-geometry.js +39 -0
- package/dist/dom/occlusion.js +21 -0
- package/dist/dom/query-finders.d.ts +1 -1
- package/dist/dom/query-finders.js +10 -7
- package/dist/dom/query.d.ts +6 -2
- package/dist/dom/query.js +165 -54
- package/dist/dom/scope-roots.d.ts +8 -0
- package/dist/dom/scope-roots.js +44 -0
- package/dist/dom/snapshot.d.ts +3 -0
- package/dist/dom/snapshot.js +9 -2
- package/dist/dom/text-match.js +6 -4
- package/dist/dom/theme.js +6 -7
- package/dist/dom/visibility.d.ts +3 -0
- package/dist/dom/visibility.js +125 -10
- package/dist/observers/canvas.d.ts +1 -0
- package/dist/observers/canvas.js +10 -1
- package/dist/observers/console.js +46 -35
- package/dist/observers/dom.js +28 -4
- package/dist/observers/net-detail/resource-timing.d.ts +12 -0
- package/dist/observers/net-detail/resource-timing.js +69 -0
- package/dist/observers/network.js +13 -44
- package/dist/observers/perf.js +15 -1
- package/dist/presenter/chrome/presenter-verbs.js +1 -0
- package/dist/presenter/presenter-config.d.ts +1 -0
- package/dist/presenter/presenter-config.js +1 -0
- package/dist/presenter/presenter-shell.d.ts +1 -0
- package/dist/presenter/presenter-shell.js +6 -3
- package/dist/presenter/tour/tour.js +4 -0
- package/dist/recorder/recorder.js +82 -39
- package/dist/reticle-inject.js +618 -618
- package/dist/reticle.d.ts +3 -19
- package/dist/reticle.js +76 -110
- package/dist/review/annotator.js +4 -0
- package/dist/security/serialization.js +35 -0
- package/dist/session-continuity.d.ts +1 -0
- package/dist/session-continuity.js +18 -0
- package/dist/storage-keys.js +2 -1
- package/dist/timers/native/native-timers.js +21 -4
- package/dist/transport/bridge-url.d.ts +8 -0
- package/dist/transport/bridge-url.js +52 -0
- package/package.json +10 -10
|
@@ -7,7 +7,21 @@ function asString(value, fallback = '') {
|
|
|
7
7
|
return 'string' === typeof value ? value : fallback;
|
|
8
8
|
}
|
|
9
9
|
export function pressKey(args) {
|
|
10
|
-
return pressKeyFromArgs(args);
|
|
10
|
+
return splitChord(pressKeyFromArgs(args)).key;
|
|
11
|
+
}
|
|
12
|
+
function splitChord(raw) {
|
|
13
|
+
const at = raw.lastIndexOf('+', raw.length - 2);
|
|
14
|
+
const flags = 0 < at
|
|
15
|
+
? raw
|
|
16
|
+
.slice(0, at)
|
|
17
|
+
.split('+')
|
|
18
|
+
.map((name) => MODIFIER_FLAG_ALIASES[name.toLowerCase()])
|
|
19
|
+
: [];
|
|
20
|
+
if (0 === flags.length || flags.some((flag) => flag === undefined)) {
|
|
21
|
+
return { key: CODE_KEYS[raw] ?? raw, flags: [] };
|
|
22
|
+
}
|
|
23
|
+
const key = raw.slice(at + 1);
|
|
24
|
+
return { key: CODE_KEYS[key] ?? key, flags: flags.filter((flag) => flag !== undefined) };
|
|
11
25
|
}
|
|
12
26
|
const MODIFIER_FLAG_KEYS = [
|
|
13
27
|
'metaKey',
|
|
@@ -37,6 +51,9 @@ export function pressModifiers(args) {
|
|
|
37
51
|
if (flag !== undefined)
|
|
38
52
|
flags[flag] = true;
|
|
39
53
|
}
|
|
54
|
+
for (const flag of splitChord(pressKeyFromArgs(args)).flags) {
|
|
55
|
+
flags[flag] = true;
|
|
56
|
+
}
|
|
40
57
|
return flags;
|
|
41
58
|
}
|
|
42
59
|
function modifierFlagFor(key) {
|
|
@@ -53,7 +70,7 @@ export function pressCode(args, key) {
|
|
|
53
70
|
return `Key${key.toUpperCase()}`;
|
|
54
71
|
if (/[0-9]/.test(key))
|
|
55
72
|
return `Digit${key}`;
|
|
56
|
-
return '';
|
|
73
|
+
return PUNCTUATION_CODES[key] ?? '';
|
|
57
74
|
}
|
|
58
75
|
return /^[A-Z][A-Za-z0-9]*$/.test(key) && KNOWN_NAMED_KEYS.has(key) ? key : '';
|
|
59
76
|
}
|
|
@@ -74,6 +91,53 @@ export function dispatchKeypress(el, key, code, mods, repeat = false) {
|
|
|
74
91
|
}
|
|
75
92
|
asSyntheticInput(() => el.dispatchEvent(event));
|
|
76
93
|
}
|
|
94
|
+
const PUNCTUATION_CODES = {
|
|
95
|
+
'/': 'Slash',
|
|
96
|
+
'?': 'Slash',
|
|
97
|
+
'.': 'Period',
|
|
98
|
+
'>': 'Period',
|
|
99
|
+
',': 'Comma',
|
|
100
|
+
'<': 'Comma',
|
|
101
|
+
';': 'Semicolon',
|
|
102
|
+
':': 'Semicolon',
|
|
103
|
+
"'": 'Quote',
|
|
104
|
+
'"': 'Quote',
|
|
105
|
+
'[': 'BracketLeft',
|
|
106
|
+
'{': 'BracketLeft',
|
|
107
|
+
']': 'BracketRight',
|
|
108
|
+
'}': 'BracketRight',
|
|
109
|
+
'\\': 'Backslash',
|
|
110
|
+
'|': 'Backslash',
|
|
111
|
+
'-': 'Minus',
|
|
112
|
+
_: 'Minus',
|
|
113
|
+
'=': 'Equal',
|
|
114
|
+
'+': 'Equal',
|
|
115
|
+
'`': 'Backquote',
|
|
116
|
+
'~': 'Backquote',
|
|
117
|
+
'!': 'Digit1',
|
|
118
|
+
'@': 'Digit2',
|
|
119
|
+
'#': 'Digit3',
|
|
120
|
+
$: 'Digit4',
|
|
121
|
+
'%': 'Digit5',
|
|
122
|
+
'^': 'Digit6',
|
|
123
|
+
'&': 'Digit7',
|
|
124
|
+
'*': 'Digit8',
|
|
125
|
+
'(': 'Digit9',
|
|
126
|
+
')': 'Digit0',
|
|
127
|
+
};
|
|
128
|
+
const CODE_KEYS = {
|
|
129
|
+
Slash: '/',
|
|
130
|
+
Period: '.',
|
|
131
|
+
Comma: ',',
|
|
132
|
+
Semicolon: ';',
|
|
133
|
+
Quote: "'",
|
|
134
|
+
BracketLeft: '[',
|
|
135
|
+
BracketRight: ']',
|
|
136
|
+
Backslash: '\\',
|
|
137
|
+
Minus: '-',
|
|
138
|
+
Equal: '=',
|
|
139
|
+
Backquote: '`',
|
|
140
|
+
};
|
|
77
141
|
const ENTER_KEY = 'Enter';
|
|
78
142
|
const ENTER_CHAR_CODE = 13;
|
|
79
143
|
const LEGACY_KEY_CODE_FIELDS = ['keyCode', 'charCode', 'which'];
|
|
@@ -233,3 +297,83 @@ export function closeModalOnEscape(el, key, keydownProceeded) {
|
|
|
233
297
|
return;
|
|
234
298
|
}
|
|
235
299
|
}
|
|
300
|
+
export const SPACE_KEY = ' ';
|
|
301
|
+
const ENTER = 'Enter';
|
|
302
|
+
const TAB = 'Tab';
|
|
303
|
+
const ENTER_ACTIVATES = 'button, a[href], area[href], summary, input[type="submit"], input[type="button"], input[type="reset"], input[type="image"]';
|
|
304
|
+
const SPACE_ACTIVATES = 'button, summary, input[type="submit"], input[type="button"], input[type="reset"], input[type="image"], input[type="checkbox"], input[type="radio"]';
|
|
305
|
+
const IMPLICIT_SUBMIT_TYPES = new Set([
|
|
306
|
+
'text',
|
|
307
|
+
'search',
|
|
308
|
+
'url',
|
|
309
|
+
'tel',
|
|
310
|
+
'email',
|
|
311
|
+
'password',
|
|
312
|
+
'date',
|
|
313
|
+
'month',
|
|
314
|
+
'week',
|
|
315
|
+
'time',
|
|
316
|
+
'datetime-local',
|
|
317
|
+
'number',
|
|
318
|
+
]);
|
|
319
|
+
const FOCUSABLE = 'a[href], area[href], button, input, select, textarea, summary, iframe, [tabindex], [contenteditable=""], [contenteditable="true"]';
|
|
320
|
+
const shortcut = (held) => held.metaKey || held.ctrlKey || held.altKey;
|
|
321
|
+
export function runKeydownDefault(el, key, held) {
|
|
322
|
+
if (shortcut(held)) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (ENTER === key && !held.shiftKey) {
|
|
326
|
+
enterDefault(el);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (TAB === key) {
|
|
330
|
+
moveFocus(el, held.shiftKey ? -1 : 1);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
export function runKeyupDefault(el, key, held) {
|
|
334
|
+
if (SPACE_KEY !== key || shortcut(held) || held.shiftKey) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (el.matches(SPACE_ACTIVATES) && !isDisabled(el)) {
|
|
338
|
+
el.click();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
function enterDefault(el) {
|
|
342
|
+
if (el.matches(ENTER_ACTIVATES)) {
|
|
343
|
+
if (!isDisabled(el)) {
|
|
344
|
+
el.click();
|
|
345
|
+
}
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const type = 'input' === el.localName ? (el.getAttribute('type') ?? 'text').toLowerCase() : '';
|
|
349
|
+
if (!IMPLICIT_SUBMIT_TYPES.has(type)) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const form = el.form;
|
|
353
|
+
form?.requestSubmit();
|
|
354
|
+
}
|
|
355
|
+
function isDisabled(el) {
|
|
356
|
+
return el.matches(':disabled');
|
|
357
|
+
}
|
|
358
|
+
function tabbable(el) {
|
|
359
|
+
if (isDisabled(el) || null !== el.closest('[inert], [hidden]')) {
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
const html = el;
|
|
363
|
+
if (html.tabIndex < 0) {
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
return 'hidden' !== el.type;
|
|
367
|
+
}
|
|
368
|
+
function moveFocus(from, step) {
|
|
369
|
+
const doc = from.ownerDocument;
|
|
370
|
+
const all = [...doc.querySelectorAll(FOCUSABLE)].filter(tabbable);
|
|
371
|
+
const positive = all.filter((el) => el.tabIndex > 0).sort((a, b) => a.tabIndex - b.tabIndex);
|
|
372
|
+
const order = [...positive, ...all.filter((el) => 0 === el.tabIndex)];
|
|
373
|
+
let at = order.indexOf(from);
|
|
374
|
+
if (at < 0) {
|
|
375
|
+
at = order.findIndex((el) => from.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING);
|
|
376
|
+
at = at < 0 ? order.length : at - (1 === step ? 1 : 0);
|
|
377
|
+
}
|
|
378
|
+
order[at + step]?.focus();
|
|
379
|
+
}
|
package/dist/actions/actions.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { ActionType, ActionWarning, clampHoldMs, clickInputProblem, DANGEROUS_ACTION_CONFIRM_ARG, ElementState, NATIVE_INPUT_ARG, SettleReason, } from '@reticlehq/core';
|
|
1
|
+
import { ACT_POINT_ARG, ActionType, ActionWarning, actPointProblem, boxFractionFromArgs, clampHoldMs, clickInputProblem, DRAG_TO_POINT_ARG, DANGEROUS_ACTION_CONFIRM_ARG, ElementState, NATIVE_INPUT_ARG, SettleReason, } from '@reticlehq/core';
|
|
2
2
|
import { isHiddenNativeOption } from '../dom/query-finders.js';
|
|
3
|
-
import { isReticleUi } from '../dom/dom-ignore.js';
|
|
3
|
+
import { isReticleUi, isUnobserved } from '../dom/dom-ignore.js';
|
|
4
4
|
import { asSyntheticInput } from './synthetic/synthetic-input.js';
|
|
5
5
|
import { echoRef, refs } from '../dom/addressing/refs.js';
|
|
6
6
|
import { requiresDangerousConfirmation, assertOutwardAllowed, assertPaymentEntryAllowed, submitControlFor, } from './danger-context.js';
|
|
7
|
-
import { assertEditable, assertNotRichText, isAutofillSafeTarget, setNativeValue, } from './value-input.js';
|
|
7
|
+
import { assertEditable, assertNotEditorTextArea, assertNotRichText, isAutofillSafeTarget, isRichText, setNativeValue, } from './value-input.js';
|
|
8
8
|
import { getRole, getStates } from '../dom/a11y.js';
|
|
9
9
|
import { isVisible } from '../dom/visibility.js';
|
|
10
10
|
import { selectInCombobox } from './select-combobox.js';
|
|
@@ -14,7 +14,7 @@ import { asSettleBudget, nativeSetTimeout, settle } from '../timers/native/nativ
|
|
|
14
14
|
import { publishSettledField, watchActedField } from '../observers/field.js';
|
|
15
15
|
import { AppearedText } from './appeared-text.js';
|
|
16
16
|
import { alreadyAtCheckedState, assertAriaToggleLanded, isAriaToggle } from './check-state.js';
|
|
17
|
-
import { focusedOrDocument, isReflessDocumentPress, pressCode, pressKey, pressModifiers, pressKeys, holdKey, pressCombo, dispatchKeypress, closeModalOnEscape, keyboardEvent, } from './actions-press.js';
|
|
17
|
+
import { focusedOrDocument, isReflessDocumentPress, pressCode, pressKey, pressModifiers, pressKeys, holdKey, pressCombo, dispatchKeypress, closeModalOnEscape, keyboardEvent, runKeydownDefault, runKeyupDefault, SPACE_KEY, } from './actions-press.js';
|
|
18
18
|
import { anchorOf, result, visibleCanvases, } from './action-report.js';
|
|
19
19
|
function asString(value, fallback = '') {
|
|
20
20
|
return 'string' === typeof value ? value : fallback;
|
|
@@ -34,6 +34,7 @@ const FILL_LIKE = new Set([
|
|
|
34
34
|
ActionType.TYPE,
|
|
35
35
|
ActionType.CLEAR,
|
|
36
36
|
ActionType.SELECT,
|
|
37
|
+
ActionType.INSERT_TEXT,
|
|
37
38
|
]);
|
|
38
39
|
const isFillLike = (action) => FILL_LIKE.has(action);
|
|
39
40
|
const CLICK_LIKE = new Set([
|
|
@@ -72,7 +73,7 @@ function assertActionAllowed(el, action, args) {
|
|
|
72
73
|
action === ActionType.UNCHECK ||
|
|
73
74
|
action === ActionType.DRAG ||
|
|
74
75
|
action === ActionType.SUBMIT ||
|
|
75
|
-
(action === ActionType.PRESS && 'Enter' === pressKey(args));
|
|
76
|
+
(action === ActionType.PRESS && ('Enter' === pressKey(args) || SPACE_KEY === pressKey(args)));
|
|
76
77
|
const dragTarget = action === ActionType.DRAG ? refs.resolve(dragTargetRef(args)) : null;
|
|
77
78
|
const submitter = action === ActionType.PRESS && 'Enter' === pressKey(args) ? submitControlFor(el) : null;
|
|
78
79
|
if (canTrigger) {
|
|
@@ -100,7 +101,7 @@ function optionValueFor(options, named) {
|
|
|
100
101
|
const byLabel = options.filter((option) => option.label.trim() === named.trim());
|
|
101
102
|
return 1 === byLabel.length ? (byLabel[0]?.value ?? named) : named;
|
|
102
103
|
}
|
|
103
|
-
import { NO_GEOMETRY, fireClickSequence, fireTapSequence, clickGeometry, firePointer, firePointerNonBubbling, dragElement, mouseEventFor, assertNotLeaving, clickInputOf, } from './actions-dom.js';
|
|
104
|
+
import { NO_GEOMETRY, fireClickSequence, fireTapSequence, clickGeometry, firePointer, firePointerNonBubbling, leavePreviousHover, dragElement, mouseEventFor, assertNotLeaving, scrollIntoViewNow, clickInputOf, coordsIn, } from './actions-dom.js';
|
|
104
105
|
import { scrollFrom } from './scroll.js';
|
|
105
106
|
import { clickHitsNoControl } from './click-target.js';
|
|
106
107
|
function enabledOf(el) {
|
|
@@ -110,6 +111,9 @@ function valueOf(el) {
|
|
|
110
111
|
if (isInput(el) || isTextArea(el) || isSelect(el)) {
|
|
111
112
|
return el.value;
|
|
112
113
|
}
|
|
114
|
+
if (isRichText(el)) {
|
|
115
|
+
return el.textContent ?? '';
|
|
116
|
+
}
|
|
113
117
|
return undefined;
|
|
114
118
|
}
|
|
115
119
|
function activeRef(el) {
|
|
@@ -122,11 +126,11 @@ function activeRef(el) {
|
|
|
122
126
|
async function dispatchFor(el, action, args) {
|
|
123
127
|
if (ActionType.CLICK === action) {
|
|
124
128
|
const hold = clampHoldMs(args['holdMs']);
|
|
125
|
-
return await fireClickSequence(el, 0 === hold ? undefined : { ms: hold, sleep, now: () => Date.now() }, undefined, clickInputOf(args));
|
|
129
|
+
return await fireClickSequence(el, 0 === hold ? undefined : { ms: hold, sleep, now: () => Date.now() }, undefined, clickInputOf(args), boxFractionFromArgs(args, ACT_POINT_ARG));
|
|
126
130
|
}
|
|
127
131
|
if (ActionType.TAP === action) {
|
|
128
132
|
const hold = clampHoldMs(args['holdMs']);
|
|
129
|
-
return await fireTapSequence(el, 0 === hold ? undefined : { ms: hold, sleep, now: () => Date.now() });
|
|
133
|
+
return await fireTapSequence(el, 0 === hold ? undefined : { ms: hold, sleep, now: () => Date.now() }, boxFractionFromArgs(args, ACT_POINT_ARG));
|
|
130
134
|
}
|
|
131
135
|
return { prevented: await dispatchOther(el, action, args), heldMs: 0 };
|
|
132
136
|
}
|
|
@@ -145,24 +149,26 @@ async function dispatchOther(el, action, args) {
|
|
|
145
149
|
switch (action) {
|
|
146
150
|
case ActionType.DBLCLICK: {
|
|
147
151
|
const input = clickInputOf(args);
|
|
148
|
-
|
|
149
|
-
await fireClickSequence(el, undefined,
|
|
152
|
+
const point = boxFractionFromArgs(args, ACT_POINT_ARG);
|
|
153
|
+
await fireClickSequence(el, undefined, 1, input, point);
|
|
154
|
+
await fireClickSequence(el, undefined, 2, input, point);
|
|
150
155
|
return !asSyntheticInput(() => el.dispatchEvent(mouseEventFor(el, 'dblclick', {
|
|
151
156
|
bubbles: true,
|
|
152
157
|
cancelable: true,
|
|
153
158
|
detail: 2,
|
|
154
159
|
...input.modifiers,
|
|
160
|
+
...coordsIn(el, point),
|
|
155
161
|
})));
|
|
156
162
|
}
|
|
157
163
|
case ActionType.HOVER: {
|
|
158
|
-
const
|
|
159
|
-
const from =
|
|
160
|
-
firePointer(el, 'pointerover', from);
|
|
161
|
-
el.dispatchEvent(mouseEventFor(el, 'mouseover', { bubbles: true, relatedTarget: from }));
|
|
162
|
-
firePointerNonBubbling(el, 'pointerenter', from);
|
|
163
|
-
el.dispatchEvent(mouseEventFor(el, 'mouseenter', { relatedTarget: from }));
|
|
164
|
-
firePointer(el, 'pointermove', from);
|
|
165
|
-
const moved = el.dispatchEvent(mouseEventFor(el, 'mousemove', { bubbles: true, cancelable: true }));
|
|
164
|
+
const coords = coordsIn(el, boxFractionFromArgs(args, ACT_POINT_ARG));
|
|
165
|
+
const from = leavePreviousHover(el);
|
|
166
|
+
firePointer(el, 'pointerover', from, coords);
|
|
167
|
+
el.dispatchEvent(mouseEventFor(el, 'mouseover', { bubbles: true, relatedTarget: from, ...coords }));
|
|
168
|
+
firePointerNonBubbling(el, 'pointerenter', from, coords);
|
|
169
|
+
el.dispatchEvent(mouseEventFor(el, 'mouseenter', { relatedTarget: from, ...coords }));
|
|
170
|
+
firePointer(el, 'pointermove', from, coords);
|
|
171
|
+
const moved = el.dispatchEvent(mouseEventFor(el, 'mousemove', { bubbles: true, cancelable: true, ...coords }));
|
|
166
172
|
const holdMs = clampHoldMs(args['holdMs']);
|
|
167
173
|
if (holdMs > 0) {
|
|
168
174
|
await sleep(holdMs);
|
|
@@ -184,6 +190,7 @@ async function dispatchOther(el, action, args) {
|
|
|
184
190
|
return false;
|
|
185
191
|
}
|
|
186
192
|
case ActionType.FILL:
|
|
193
|
+
assertNotEditorTextArea(el, 'fill');
|
|
187
194
|
if (isInput(el) || isTextArea(el)) {
|
|
188
195
|
if (typeof args['value'] !== 'string') {
|
|
189
196
|
throw new Error("fill requires a string `value` — pass it nested, as args: { value: '…' }. " +
|
|
@@ -198,6 +205,7 @@ async function dispatchOther(el, action, args) {
|
|
|
198
205
|
assertNotRichText(el, 'fill');
|
|
199
206
|
throw new Error(`cannot fill a <${el.tagName.toLowerCase()}>`);
|
|
200
207
|
case ActionType.TYPE:
|
|
208
|
+
assertNotEditorTextArea(el, 'type into');
|
|
201
209
|
if (isInput(el) || isTextArea(el)) {
|
|
202
210
|
if (typeof args['text'] !== 'string') {
|
|
203
211
|
throw new Error("type requires a string `text` — pass it nested, as args: { text: '…' }.");
|
|
@@ -209,10 +217,15 @@ async function dispatchOther(el, action, args) {
|
|
|
209
217
|
assertNotRichText(el, 'type into');
|
|
210
218
|
throw new Error(`cannot type into a <${el.tagName.toLowerCase()}>`);
|
|
211
219
|
case ActionType.CLEAR:
|
|
220
|
+
assertNotEditorTextArea(el, 'clear');
|
|
212
221
|
if (isInput(el) || isTextArea(el)) {
|
|
213
222
|
return setNativeValue(el, '');
|
|
214
223
|
}
|
|
215
224
|
throw new Error(`cannot clear a <${el.tagName.toLowerCase()}>`);
|
|
225
|
+
case ActionType.INSERT_TEXT: {
|
|
226
|
+
const { insertText } = await import('./insert-text.js');
|
|
227
|
+
return insertText(el, args['text']);
|
|
228
|
+
}
|
|
216
229
|
case ActionType.SELECT:
|
|
217
230
|
if (isSelect(el)) {
|
|
218
231
|
if (typeof args['value'] !== 'string') {
|
|
@@ -281,16 +294,20 @@ async function dispatchOther(el, action, args) {
|
|
|
281
294
|
closeModalOnEscape(el, key, down);
|
|
282
295
|
if (down) {
|
|
283
296
|
dispatchKeypress(el, key, code, mods);
|
|
297
|
+
runKeydownDefault(el, key, mods);
|
|
284
298
|
}
|
|
285
299
|
const hold = clampHoldMs(args['holdMs']);
|
|
286
300
|
if (hold > 0) {
|
|
287
301
|
await holdKey(el, key, code, mods, hold);
|
|
288
302
|
}
|
|
289
303
|
asSyntheticInput(() => el.dispatchEvent(keyboardEvent('keyup', { key, code, bubbles: true, ...mods })));
|
|
304
|
+
if (down) {
|
|
305
|
+
runKeyupDefault(el, key, mods);
|
|
306
|
+
}
|
|
290
307
|
return !down;
|
|
291
308
|
}
|
|
292
309
|
case ActionType.SCROLL_INTO_VIEW:
|
|
293
|
-
el
|
|
310
|
+
scrollIntoViewNow(el);
|
|
294
311
|
return false;
|
|
295
312
|
case ActionType.ZOOM:
|
|
296
313
|
throw new Error(ZOOM_NEEDS_REAL_BROWSER_MSG);
|
|
@@ -298,7 +315,7 @@ async function dispatchOther(el, action, args) {
|
|
|
298
315
|
const dy = asFiniteNumber(args['dy']);
|
|
299
316
|
const dx = asFiniteNumber(args['dx']);
|
|
300
317
|
const fraction = asFiniteNumber(args['fraction']);
|
|
301
|
-
scrollFrom(el, dy, fraction, dx);
|
|
318
|
+
scrollFrom(el, dy, fraction, dx, true === args['wheel']);
|
|
302
319
|
return false;
|
|
303
320
|
}
|
|
304
321
|
case ActionType.UPLOAD: {
|
|
@@ -328,22 +345,35 @@ async function dispatchOther(el, action, args) {
|
|
|
328
345
|
}
|
|
329
346
|
case ActionType.DRAG: {
|
|
330
347
|
const toRef = dragTargetRef(args);
|
|
348
|
+
const points = {
|
|
349
|
+
at: boxFractionFromArgs(args, ACT_POINT_ARG),
|
|
350
|
+
to: boxFractionFromArgs(args, DRAG_TO_POINT_ARG),
|
|
351
|
+
};
|
|
331
352
|
if ('' === toRef) {
|
|
332
|
-
return await dragElement(el, null, args['data']);
|
|
353
|
+
return await dragElement(el, null, args['data'], points, clickInputOf(args));
|
|
333
354
|
}
|
|
334
355
|
const resolved = refs.resolve(toRef);
|
|
335
356
|
if (!isActionTarget(resolved)) {
|
|
336
357
|
throw new Error(`drag target '${echoRef(toRef)}' did not resolve to an element — pass a ref from ` +
|
|
337
358
|
'reticle_snapshot or reticle_query as args.toRef (alias: args.target)');
|
|
338
359
|
}
|
|
339
|
-
return await dragElement(el, resolved, args['data']);
|
|
360
|
+
return await dragElement(el, resolved, args['data'], points, clickInputOf(args));
|
|
340
361
|
}
|
|
341
362
|
default:
|
|
342
363
|
throw new Error(`unknown action '${action}'`);
|
|
343
364
|
}
|
|
344
365
|
}
|
|
366
|
+
function isPageMutation(record) {
|
|
367
|
+
const target = record.target;
|
|
368
|
+
const el = target instanceof Element ? target : target.parentElement;
|
|
369
|
+
return null === el || !isUnobserved(el);
|
|
370
|
+
}
|
|
345
371
|
export async function executeAction(ref, action, args = {}) {
|
|
346
372
|
const named = 0 < ref.length;
|
|
373
|
+
const pointProblem = actPointProblem(action, args);
|
|
374
|
+
if (pointProblem !== undefined) {
|
|
375
|
+
throw new Error(`cannot ${action}: ${pointProblem}`);
|
|
376
|
+
}
|
|
347
377
|
if (ActionType.CLICK === action || ActionType.DBLCLICK === action) {
|
|
348
378
|
const problem = clickInputProblem(action, args);
|
|
349
379
|
if (problem !== undefined) {
|
|
@@ -358,9 +388,19 @@ export async function executeAction(ref, action, args = {}) {
|
|
|
358
388
|
assertNotLeaving(el);
|
|
359
389
|
}
|
|
360
390
|
const anchor = named ? anchorOf(el) : {};
|
|
391
|
+
const prevFocus = activeRef(el);
|
|
392
|
+
if ((ActionType.CLICK === action || ActionType.DBLCLICK === action) && !enabledOf(el)) {
|
|
393
|
+
const active = el.ownerDocument.activeElement;
|
|
394
|
+
if (active !== null &&
|
|
395
|
+
active !== el &&
|
|
396
|
+
active !== el.ownerDocument.body &&
|
|
397
|
+
isActionTarget(active)) {
|
|
398
|
+
active.blur();
|
|
399
|
+
await Promise.resolve();
|
|
400
|
+
}
|
|
401
|
+
}
|
|
361
402
|
const visible = isVisible(el);
|
|
362
403
|
const enabled = enabledOf(el);
|
|
363
|
-
const prevFocus = activeRef(el);
|
|
364
404
|
const valueBefore = valueOf(el);
|
|
365
405
|
const actedField = watchActedField(el);
|
|
366
406
|
const geometry = CLICK_LIKE.has(action) ? clickGeometry(el) : NO_GEOMETRY;
|
|
@@ -369,7 +409,7 @@ export async function executeAction(ref, action, args = {}) {
|
|
|
369
409
|
let mutated = 0;
|
|
370
410
|
const said = new AppearedText();
|
|
371
411
|
const obs = new MutationObserver((records) => {
|
|
372
|
-
mutated += records.length;
|
|
412
|
+
mutated += records.filter(isPageMutation).length;
|
|
373
413
|
said.collect(records);
|
|
374
414
|
});
|
|
375
415
|
obs.observe(el.ownerDocument.documentElement, {
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { isRichText } from './value-input.js';
|
|
2
|
+
export function insertText(el, text) {
|
|
3
|
+
if (typeof text !== 'string') {
|
|
4
|
+
throw new Error("insertText requires a string `text` — pass it nested, as args: { text: '…' }.");
|
|
5
|
+
}
|
|
6
|
+
if (!isRichText(el)) {
|
|
7
|
+
throw new Error(`cannot insertText into a <${el.tagName.toLowerCase()}> — insertText needs a contenteditable rich-text editor (Tiptap, ProseMirror, Quill), not this surface`);
|
|
8
|
+
}
|
|
9
|
+
if (hasEditContext(el)) {
|
|
10
|
+
throw new Error('cannot insertText into an EditContext editor (Monaco, CodeMirror 6): code that calls updateText does not fire textupdate, so the editor model would keep the old text. Use press for a key.');
|
|
11
|
+
}
|
|
12
|
+
return insertTextInto(el, text);
|
|
13
|
+
}
|
|
14
|
+
function hasEditContext(el) {
|
|
15
|
+
if (!('editContext' in el)) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
const surface = el;
|
|
19
|
+
return null !== surface.editContext && undefined !== surface.editContext;
|
|
20
|
+
}
|
|
21
|
+
export function insertTextInto(el, text) {
|
|
22
|
+
el.focus();
|
|
23
|
+
const selection = window.getSelection();
|
|
24
|
+
if (selection !== null && !selectionInside(selection, el)) {
|
|
25
|
+
const range = document.createRange();
|
|
26
|
+
range.selectNodeContents(el);
|
|
27
|
+
range.collapse(false);
|
|
28
|
+
selection.removeAllRanges();
|
|
29
|
+
selection.addRange(range);
|
|
30
|
+
}
|
|
31
|
+
const beforeInput = new InputEvent('beforeinput', {
|
|
32
|
+
inputType: 'insertText',
|
|
33
|
+
data: text,
|
|
34
|
+
bubbles: true,
|
|
35
|
+
cancelable: true,
|
|
36
|
+
});
|
|
37
|
+
if (!el.dispatchEvent(beforeInput)) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
if ('function' !== typeof el.ownerDocument.execCommand) {
|
|
41
|
+
insertAtCaret(el, text);
|
|
42
|
+
const input = new InputEvent('input', {
|
|
43
|
+
inputType: 'insertText',
|
|
44
|
+
data: text,
|
|
45
|
+
bubbles: true,
|
|
46
|
+
cancelable: true,
|
|
47
|
+
});
|
|
48
|
+
return !el.dispatchEvent(input);
|
|
49
|
+
}
|
|
50
|
+
let sawInput = false;
|
|
51
|
+
const recordInput = (event) => {
|
|
52
|
+
if ('input' === event.type) {
|
|
53
|
+
sawInput = true;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
window.addEventListener('input', recordInput, false);
|
|
57
|
+
try {
|
|
58
|
+
if (el.ownerDocument.execCommand('insertText', false, text)) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
window.removeEventListener('input', recordInput, false);
|
|
66
|
+
}
|
|
67
|
+
if (sawInput) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
throw new Error('the editor did not accept the text: the native insertText command failed, so the editor model is unchanged. Check that the surface is editable and focused.');
|
|
71
|
+
}
|
|
72
|
+
function insertAtCaret(el, text) {
|
|
73
|
+
const selection = window.getSelection();
|
|
74
|
+
const range = selection !== null && 0 < selection.rangeCount ? selection.getRangeAt(0) : null;
|
|
75
|
+
if (range !== null && el.contains(range.commonAncestorContainer)) {
|
|
76
|
+
range.deleteContents();
|
|
77
|
+
const node = el.ownerDocument.createTextNode(text);
|
|
78
|
+
range.insertNode(node);
|
|
79
|
+
range.setStartAfter(node);
|
|
80
|
+
range.collapse(true);
|
|
81
|
+
selection?.removeAllRanges();
|
|
82
|
+
selection?.addRange(range);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
el.appendChild(el.ownerDocument.createTextNode(text));
|
|
86
|
+
}
|
|
87
|
+
function selectionInside(selection, el) {
|
|
88
|
+
const anchor = selection.anchorNode;
|
|
89
|
+
return anchor !== null && el.contains(anchor);
|
|
90
|
+
}
|
package/dist/actions/scroll.d.ts
CHANGED
|
@@ -14,5 +14,5 @@ interface ScrollResult {
|
|
|
14
14
|
export declare const SCROLL_FRACTION_RANGE_ERROR_PREFIX = "scroll fraction must be between 0 and 1, got ";
|
|
15
15
|
export declare function fractionOutOfRangeMessage(fraction: number): string;
|
|
16
16
|
export declare function scrollContainer(ref: string | undefined, dy: number | undefined, fraction?: number, dx?: number): ScrollResult;
|
|
17
|
-
export declare function scrollFrom(base: Element | undefined, dy: number | undefined, fraction?: number, dx?: number): ScrollResult;
|
|
17
|
+
export declare function scrollFrom(base: Element | undefined, dy: number | undefined, fraction?: number, dx?: number, wheel?: boolean): ScrollResult;
|
|
18
18
|
export {};
|
package/dist/actions/scroll.js
CHANGED
|
@@ -2,6 +2,7 @@ import { refs } from '../dom/addressing/refs.js';
|
|
|
2
2
|
const FALLBACK_STEP_PX = 400;
|
|
3
3
|
const VIEWPORT_FRACTION = 0.8;
|
|
4
4
|
export const SCROLL_FRACTION_RANGE_ERROR_PREFIX = 'scroll fraction must be between 0 and 1, got ';
|
|
5
|
+
const SCROLL_WHEEL_FRACTION_ERROR = 'scroll with wheel: true cannot use fraction; pass dy or dx';
|
|
5
6
|
export function fractionOutOfRangeMessage(fraction) {
|
|
6
7
|
return `${SCROLL_FRACTION_RANGE_ERROR_PREFIX}${fraction}`;
|
|
7
8
|
}
|
|
@@ -21,7 +22,7 @@ export function scrollContainer(ref, dy, fraction, dx) {
|
|
|
21
22
|
const base = ref !== undefined ? refs.resolve(ref) : null;
|
|
22
23
|
return scrollFrom(base instanceof Element ? base : undefined, dy, fraction, dx);
|
|
23
24
|
}
|
|
24
|
-
export function scrollFrom(base, dy, fraction, dx) {
|
|
25
|
+
export function scrollFrom(base, dy, fraction, dx, wheel) {
|
|
25
26
|
if (fraction !== undefined && (fraction < 0 || fraction > 1 || Number.isNaN(fraction))) {
|
|
26
27
|
throw new RangeError(fractionOutOfRangeMessage(fraction));
|
|
27
28
|
}
|
|
@@ -31,6 +32,26 @@ export function scrollFrom(base, dy, fraction, dx) {
|
|
|
31
32
|
: (document.scrollingElement ?? document.documentElement);
|
|
32
33
|
const before = target.scrollTop;
|
|
33
34
|
const beforeLeft = target.scrollLeft;
|
|
35
|
+
if (true === wheel) {
|
|
36
|
+
if (fraction !== undefined) {
|
|
37
|
+
throw new RangeError(SCROLL_WHEEL_FRACTION_ERROR);
|
|
38
|
+
}
|
|
39
|
+
const deltaY = dy ?? (Math.round(target.clientHeight * VIEWPORT_FRACTION) || FALLBACK_STEP_PX);
|
|
40
|
+
const deltaX = dx ?? 0;
|
|
41
|
+
const wheelTarget = base ?? target;
|
|
42
|
+
const rect = wheelTarget.getBoundingClientRect();
|
|
43
|
+
wheelTarget.dispatchEvent(new WheelEvent('wheel', {
|
|
44
|
+
deltaX,
|
|
45
|
+
deltaY,
|
|
46
|
+
deltaMode: WheelEvent.DOM_DELTA_PIXEL,
|
|
47
|
+
clientX: rect.width > 0 ? rect.left + rect.width / 2 : 0,
|
|
48
|
+
clientY: rect.height > 0 ? rect.top + rect.height / 2 : 0,
|
|
49
|
+
bubbles: true,
|
|
50
|
+
cancelable: true,
|
|
51
|
+
composed: true,
|
|
52
|
+
}));
|
|
53
|
+
return scrollResult(target, before, beforeLeft);
|
|
54
|
+
}
|
|
34
55
|
if (fraction !== undefined) {
|
|
35
56
|
target.scrollTop = Math.round(target.scrollHeight * fraction);
|
|
36
57
|
}
|
|
@@ -44,6 +65,9 @@ export function scrollFrom(base, dy, fraction, dx) {
|
|
|
44
65
|
}
|
|
45
66
|
}
|
|
46
67
|
target.dispatchEvent(new Event('scroll', { bubbles: false }));
|
|
68
|
+
return scrollResult(target, before, beforeLeft);
|
|
69
|
+
}
|
|
70
|
+
function scrollResult(target, before, beforeLeft) {
|
|
47
71
|
const after = target.scrollTop;
|
|
48
72
|
const afterLeft = target.scrollLeft;
|
|
49
73
|
return {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type ActionTarget } from '../dom/realm.js';
|
|
2
2
|
export declare function isAutofillSafeTarget(el: Element, ua?: string): boolean;
|
|
3
3
|
export declare function setNativeValue(el: HTMLInputElement | HTMLTextAreaElement, value: string): boolean;
|
|
4
|
+
export declare function isRichText(el: Element): boolean;
|
|
4
5
|
export declare function assertNotRichText(el: ActionTarget, action: string): void;
|
|
6
|
+
export declare function assertNotEditorTextArea(el: ActionTarget, action: string): void;
|
|
5
7
|
export declare function assertEditable(el: HTMLInputElement | HTMLTextAreaElement, action: string): void;
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { captureValueSetter } from '../patching/capture-method.js';
|
|
2
|
-
import { isHtmlElement, valuePrototypeOf } from '../dom/realm.js';
|
|
2
|
+
import { isHtmlElement, isTextArea, valuePrototypeOf } from '../dom/realm.js';
|
|
3
3
|
const PASSWORD_AUTOCOMPLETE = /\b(current|new)-password\b/;
|
|
4
4
|
const NOT_WEBKIT = /Chrome|Chromium|Edg\/|Firefox|Gecko\//;
|
|
5
5
|
export function isAutofillSafeTarget(el, ua = navigator.userAgent) {
|
|
6
|
-
if (!(el instanceof HTMLInputElement))
|
|
6
|
+
if (!(el instanceof HTMLInputElement)) {
|
|
7
7
|
return false;
|
|
8
|
+
}
|
|
8
9
|
const password = 'password' === el.type.toLowerCase() ||
|
|
9
10
|
PASSWORD_AUTOCOMPLETE.test(el.getAttribute('autocomplete') ?? '');
|
|
10
|
-
if (!password || NOT_WEBKIT.test(ua))
|
|
11
|
+
if (!password || NOT_WEBKIT.test(ua)) {
|
|
11
12
|
return false;
|
|
13
|
+
}
|
|
12
14
|
return 600 <= Number(/AppleWebKit\/(\d+)/.exec(ua)?.[1] ?? 0);
|
|
13
15
|
}
|
|
14
16
|
export function setNativeValue(el, value) {
|
|
@@ -24,21 +26,40 @@ export function setNativeValue(el, value) {
|
|
|
24
26
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
25
27
|
return !notPrevented;
|
|
26
28
|
}
|
|
27
|
-
function isRichText(el) {
|
|
29
|
+
export function isRichText(el) {
|
|
28
30
|
const flag = el.getAttribute('contenteditable');
|
|
29
|
-
if ((isHtmlElement(el) && el.isContentEditable) || (null !== flag && 'false' !== flag))
|
|
31
|
+
if ((isHtmlElement(el) && el.isContentEditable) || (null !== flag && 'false' !== flag)) {
|
|
30
32
|
return true;
|
|
31
|
-
|
|
33
|
+
}
|
|
34
|
+
if ('textbox' === el.getAttribute('role')) {
|
|
32
35
|
return true;
|
|
33
|
-
|
|
36
|
+
}
|
|
37
|
+
if (!('editContext' in el)) {
|
|
34
38
|
return false;
|
|
39
|
+
}
|
|
35
40
|
const surface = el;
|
|
36
41
|
return null !== surface.editContext && undefined !== surface.editContext;
|
|
37
42
|
}
|
|
43
|
+
const EDITOR_ROOT = '.monaco-editor, .CodeMirror, .cm-editor';
|
|
44
|
+
function isEditorTextArea(el) {
|
|
45
|
+
if (!isTextArea(el)) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
return 'textbox' === el.getAttribute('role') || null !== el.closest(EDITOR_ROOT);
|
|
49
|
+
}
|
|
50
|
+
function refuseEditor(action) {
|
|
51
|
+
throw new Error(`cannot ${action} a contenteditable or EditContext editor; use insertText, or press for a key`);
|
|
52
|
+
}
|
|
38
53
|
export function assertNotRichText(el, action) {
|
|
39
|
-
if (!isRichText(el))
|
|
54
|
+
if (!isRichText(el)) {
|
|
40
55
|
return;
|
|
41
|
-
|
|
56
|
+
}
|
|
57
|
+
refuseEditor(action);
|
|
58
|
+
}
|
|
59
|
+
export function assertNotEditorTextArea(el, action) {
|
|
60
|
+
if (isEditorTextArea(el)) {
|
|
61
|
+
refuseEditor(action);
|
|
62
|
+
}
|
|
42
63
|
}
|
|
43
64
|
export function assertEditable(el, action) {
|
|
44
65
|
if (el.disabled) {
|