browser-debugger-cli 0.8.0 → 0.9.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/README.md +4 -1
- package/dist/cdp/schema.d.ts +4 -1
- package/dist/cdp/schema.js +48 -7
- package/dist/commands/cdp.js +3 -2
- package/dist/commands/cleanup.d.ts +11 -0
- package/dist/commands/cleanup.js +161 -57
- package/dist/commands/console.d.ts +20 -1
- package/dist/commands/console.js +57 -17
- package/dist/commands/details.js +3 -2
- package/dist/commands/dom/DomElementResolver.d.ts +10 -3
- package/dist/commands/dom/DomElementResolver.js +35 -17
- package/dist/commands/dom/a11y.d.ts +10 -0
- package/dist/commands/dom/a11y.js +27 -5
- package/dist/commands/dom/eval.d.ts +3 -1
- package/dist/commands/dom/eval.js +29 -4
- package/dist/commands/dom/form.js +16 -62
- package/dist/commands/dom/formInteraction.js +152 -113
- package/dist/commands/dom/formSummary.d.ts +49 -0
- package/dist/commands/dom/formSummary.js +180 -0
- package/dist/commands/dom/frames.d.ts +2 -1
- package/dist/commands/dom/frames.js +17 -2
- package/dist/commands/dom/get.d.ts +6 -5
- package/dist/commands/dom/get.js +92 -82
- package/dist/commands/dom/helpers/index.d.ts +1 -1
- package/dist/commands/dom/helpers/index.js +1 -1
- package/dist/commands/dom/helpers/query.d.ts +44 -17
- package/dist/commands/dom/helpers/query.js +244 -97
- package/dist/commands/dom/helpers/runElementCommand.d.ts +10 -2
- package/dist/commands/dom/helpers/runElementCommand.js +97 -30
- package/dist/commands/dom/helpers/screenshot.d.ts +4 -1
- package/dist/commands/dom/helpers/screenshot.js +164 -49
- package/dist/commands/dom/index.d.ts +3 -1
- package/dist/commands/dom/index.js +16 -6
- package/dist/commands/dom/layout.d.ts +14 -0
- package/dist/commands/dom/layout.js +54 -0
- package/dist/commands/dom/listeners.d.ts +5 -1
- package/dist/commands/dom/listeners.js +13 -3
- package/dist/commands/dom/query.js +2 -3
- package/dist/commands/dom/screenshot.d.ts +12 -2
- package/dist/commands/dom/screenshot.js +27 -3
- package/dist/commands/dom/semanticUtils.d.ts +6 -13
- package/dist/commands/dom/semanticUtils.js +15 -19
- package/dist/commands/dom/wait.d.ts +13 -0
- package/dist/commands/dom/wait.js +83 -0
- package/dist/commands/helpJson.js +2 -2
- package/dist/commands/network/list.js +4 -11
- package/dist/commands/optionBehaviors.js +112 -21
- package/dist/commands/page.d.ts +2 -1
- package/dist/commands/page.js +41 -5
- package/dist/commands/peek.js +4 -11
- package/dist/commands/sessions.d.ts +8 -0
- package/dist/commands/sessions.js +19 -0
- package/dist/commands/shared/CommandRunner.js +4 -4
- package/dist/commands/shared/dataFetcher.js +2 -2
- package/dist/commands/shared/followMode.d.ts +21 -1
- package/dist/commands/shared/followMode.js +29 -2
- package/dist/commands/shared/handleValidationError.d.ts +2 -2
- package/dist/commands/shared/handleValidationError.js +12 -3
- package/dist/commands/shared/optionTypes.d.ts +40 -5
- package/dist/commands/shared/startHelpers.js +12 -3
- package/dist/commands/shared/validation.d.ts +3 -2
- package/dist/commands/shared/validation.js +4 -3
- package/dist/commands/start.d.ts +63 -0
- package/dist/commands/start.js +115 -15
- package/dist/commands/status.js +29 -7
- package/dist/commands/stop.js +7 -6
- package/dist/commands/tail.js +4 -11
- package/dist/commands/types.d.ts +2 -0
- package/dist/commands.js +2 -0
- package/dist/connection/chromeIdentity.d.ts +65 -0
- package/dist/connection/chromeIdentity.js +143 -0
- package/dist/connection/launcher/profilePreferences.d.ts +47 -0
- package/dist/connection/launcher/profilePreferences.js +151 -0
- package/dist/connection/launcher.d.ts +21 -2
- package/dist/connection/launcher.js +42 -16
- package/dist/connection/portReservation.d.ts +14 -4
- package/dist/connection/portReservation.js +21 -6
- package/dist/connection/startupExit.d.ts +8 -0
- package/dist/connection/startupExit.js +15 -6
- package/dist/constants.d.ts +6 -2
- package/dist/constants.js +9 -2
- package/dist/daemon/SessionController.js +23 -7
- package/dist/daemon/errors.d.ts +1 -1
- package/dist/daemon/errors.js +1 -1
- package/dist/daemon/launcher.d.ts +2 -1
- package/dist/daemon/launcher.js +5 -6
- package/dist/daemon/server/SocketServer.js +1 -2
- package/dist/daemon/session/Session.d.ts +13 -0
- package/dist/daemon/session/Session.js +57 -8
- package/dist/daemon/session/chromeConnection.d.ts +9 -0
- package/dist/daemon/session/chromeConnection.js +45 -8
- package/dist/daemon/session/commandRegistry.js +52 -62
- package/dist/daemon/session/interactions.d.ts +35 -9
- package/dist/daemon/session/interactions.js +36 -9
- package/dist/daemon/session/triggeredRequests.d.ts +67 -0
- package/dist/daemon/session/triggeredRequests.js +157 -0
- package/dist/daemon/session/types.d.ts +5 -1
- package/dist/daemon.js +5393 -1600
- package/dist/errors/messages.d.ts +387 -24
- package/dist/errors/messages.js +761 -67
- package/dist/index.js +3976 -1558
- package/dist/ipc/client.d.ts +12 -1
- package/dist/ipc/client.js +22 -3
- package/dist/ipc/protocol/commands.d.ts +89 -4
- package/dist/ipc/protocol/commands.js +2 -0
- package/dist/ipc/protocol/domTypes.d.ts +258 -7
- package/dist/ipc/session/lifecycle.d.ts +8 -1
- package/dist/ipc/session/queries.d.ts +5 -1
- package/dist/ipc/session/types.d.ts +5 -0
- package/dist/ipc/transport/index.d.ts +2 -1
- package/dist/ipc/transport/index.js +2 -2
- package/dist/runtime/dom/actionEffects.d.ts +106 -0
- package/dist/runtime/dom/actionEffects.js +256 -0
- package/dist/runtime/dom/actionEffectsScripts.d.ts +52 -0
- package/dist/runtime/dom/actionEffectsScripts.js +234 -0
- package/dist/runtime/dom/elementGeometry.d.ts +170 -0
- package/dist/runtime/dom/elementGeometry.js +553 -0
- package/dist/runtime/dom/elementInfo.d.ts +77 -0
- package/dist/runtime/dom/elementInfo.js +191 -0
- package/dist/runtime/dom/evalHelpers.d.ts +51 -6
- package/dist/runtime/dom/evalHelpers.js +136 -26
- package/dist/runtime/dom/eventListeners.d.ts +2 -1
- package/dist/runtime/dom/eventListeners.js +174 -47
- package/dist/runtime/dom/formDiscovery.d.ts +1 -1
- package/dist/runtime/dom/formDiscovery.js +116 -16
- package/dist/runtime/dom/formFillHelpers/fill.d.ts +10 -0
- package/dist/runtime/dom/formFillHelpers/fill.js +125 -10
- package/dist/runtime/dom/formFillHelpers/index.d.ts +2 -2
- package/dist/runtime/dom/formFillHelpers/index.js +2 -2
- package/dist/runtime/dom/formFillHelpers/pressKey.js +14 -3
- package/dist/runtime/dom/formFillHelpers/scroll.d.ts +3 -0
- package/dist/runtime/dom/formFillHelpers/scroll.js +60 -18
- package/dist/runtime/dom/formFillHelpers/shared.d.ts +17 -0
- package/dist/runtime/dom/formFillHelpers/shared.js +25 -1
- package/dist/runtime/dom/formFillHelpers/stability.d.ts +20 -6
- package/dist/runtime/dom/formFillHelpers/stability.js +50 -19
- package/dist/runtime/dom/formSubmitHelpers.d.ts +3 -0
- package/dist/runtime/dom/formSubmitHelpers.js +89 -15
- package/dist/runtime/dom/frameLayout.d.ts +60 -0
- package/dist/runtime/dom/frameLayout.js +140 -0
- package/dist/runtime/dom/frameOrigin.d.ts +50 -0
- package/dist/runtime/dom/frameOrigin.js +62 -0
- package/dist/runtime/dom/frameScopedConnection.d.ts +92 -0
- package/dist/runtime/dom/frameScopedConnection.js +252 -0
- package/dist/runtime/dom/frameSelection.d.ts +1 -1
- package/dist/runtime/dom/frameSelection.js +2 -2
- package/dist/runtime/dom/frames.d.ts +25 -2
- package/dist/runtime/dom/frames.js +202 -63
- package/dist/runtime/dom/layout.d.ts +67 -0
- package/dist/runtime/dom/layout.js +333 -0
- package/dist/runtime/dom/listenerPageScripts.d.ts +66 -0
- package/dist/runtime/dom/listenerPageScripts.js +279 -0
- package/dist/runtime/dom/listenerSummary.d.ts +132 -11
- package/dist/runtime/dom/listenerSummary.js +344 -22
- package/dist/runtime/dom/pageActivity.d.ts +41 -0
- package/dist/runtime/dom/pageActivity.js +123 -0
- package/dist/runtime/dom/reactEventHelpers.d.ts +58 -2
- package/dist/runtime/dom/reactEventHelpers.js +212 -41
- package/dist/runtime/dom/targetNode.d.ts +80 -27
- package/dist/runtime/dom/targetNode.js +249 -33
- package/dist/runtime/dom/wait.d.ts +25 -0
- package/dist/runtime/dom/wait.js +199 -0
- package/dist/runtime/dom/waitCondition.d.ts +71 -0
- package/dist/runtime/dom/waitCondition.js +75 -0
- package/dist/runtime/page/emulation.d.ts +51 -0
- package/dist/runtime/page/emulation.js +80 -0
- package/dist/runtime/page/loadingState.d.ts +36 -0
- package/dist/runtime/page/loadingState.js +86 -0
- package/dist/runtime/page/navigation.d.ts +46 -2
- package/dist/runtime/page/navigation.js +69 -33
- package/dist/session/QueryCacheManager.d.ts +11 -1
- package/dist/session/QueryCacheManager.js +25 -3
- package/dist/session/chromeOwners.d.ts +34 -0
- package/dist/session/chromeOwners.js +51 -0
- package/dist/session/cleanup/staleSession.d.ts +11 -1
- package/dist/session/cleanup/staleSession.js +17 -6
- package/dist/session/cleanup/userCommands.js +2 -4
- package/dist/session/metadata.d.ts +5 -1
- package/dist/session/metadata.js +2 -1
- package/dist/session/paths.d.ts +77 -3
- package/dist/session/paths.js +111 -5
- package/dist/session/port.d.ts +31 -7
- package/dist/session/port.js +50 -43
- package/dist/session/portClaims.d.ts +66 -0
- package/dist/session/portClaims.js +284 -0
- package/dist/session/sessionList.d.ts +58 -0
- package/dist/session/sessionList.js +199 -0
- package/dist/session/sessionName.d.ts +46 -0
- package/dist/session/sessionName.js +97 -0
- package/dist/telemetry/a11y.d.ts +8 -3
- package/dist/telemetry/a11y.js +92 -28
- package/dist/telemetry/requestKinds.d.ts +32 -0
- package/dist/telemetry/requestKinds.js +61 -0
- package/dist/telemetry/requestState.d.ts +31 -0
- package/dist/telemetry/requestState.js +38 -0
- package/dist/types.d.ts +80 -3
- package/dist/ui/formatters/a11y.js +3 -0
- package/dist/ui/formatters/console/chronological.d.ts +8 -0
- package/dist/ui/formatters/console/chronological.js +17 -4
- package/dist/ui/formatters/console/json.js +3 -4
- package/dist/ui/formatters/console/shared.d.ts +12 -0
- package/dist/ui/formatters/console.d.ts +2 -2
- package/dist/ui/formatters/console.js +1 -1
- package/dist/ui/formatters/details.js +2 -1
- package/dist/ui/formatters/dom.d.ts +26 -14
- package/dist/ui/formatters/dom.js +65 -52
- package/dist/ui/formatters/form.js +29 -18
- package/dist/ui/formatters/layout.d.ts +31 -0
- package/dist/ui/formatters/layout.js +53 -0
- package/dist/ui/formatters/listeners.d.ts +3 -2
- package/dist/ui/formatters/listeners.js +73 -9
- package/dist/ui/formatters/networkHeaders.js +13 -0
- package/dist/ui/formatters/preview.js +2 -1
- package/dist/ui/formatters/requestStatus.d.ts +1 -17
- package/dist/ui/formatters/requestStatus.js +2 -30
- package/dist/ui/formatters/sessions.d.ts +12 -0
- package/dist/ui/formatters/sessions.js +40 -0
- package/dist/ui/formatters/status.d.ts +21 -2
- package/dist/ui/formatters/status.js +47 -10
- package/dist/ui/formatters/triggeredRequests.d.ts +36 -0
- package/dist/ui/formatters/triggeredRequests.js +65 -0
- package/dist/ui/formatting.d.ts +10 -0
- package/dist/ui/formatting.js +28 -36
- package/dist/ui/messages/chrome.d.ts +9 -0
- package/dist/ui/messages/chrome.js +17 -5
- package/dist/ui/messages/commands.d.ts +388 -14
- package/dist/ui/messages/commands.js +664 -21
- package/dist/ui/messages/consoleMessages.d.ts +10 -0
- package/dist/ui/messages/consoleMessages.js +17 -0
- package/dist/ui/messages/hints.js +2 -1
- package/dist/ui/messages/preview.js +5 -4
- package/dist/ui/messages/session.d.ts +16 -21
- package/dist/ui/messages/session.js +28 -26
- package/dist/ui/messages/sessionCommand.d.ts +43 -0
- package/dist/ui/messages/sessionCommand.js +52 -0
- package/dist/utils/async.d.ts +8 -0
- package/dist/utils/async.js +19 -0
- package/dist/utils/http.d.ts +22 -1
- package/dist/utils/http.js +28 -9
- package/dist/utils/selectorFilters.d.ts +36 -8
- package/dist/utils/selectorFilters.js +267 -53
- package/dist/utils/shellDetection.d.ts +8 -2
- package/dist/utils/shellDetection.js +120 -33
- package/dist/utils/suggestions.d.ts +26 -0
- package/dist/utils/suggestions.js +73 -0
- package/dist/utils/taskMappings.js +10 -0
- package/dist/utils/url.d.ts +12 -2
- package/dist/utils/url.js +69 -7
- package/package.json +1 -1
- package/dist/ui/formatters/sessionFormatters.d.ts +0 -58
- package/dist/ui/formatters/sessionFormatters.js +0 -121
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where elements are relative to the viewport of the top-level page.
|
|
3
|
+
*
|
|
4
|
+
* The page-side scripts measure an element in top-level viewport coordinates
|
|
5
|
+
* (adding the offsets of the same-origin iframes it is in) together with what
|
|
6
|
+
* clips it: the viewports of those iframes and ancestors that cut off
|
|
7
|
+
* overflowing content (scroll containers, `overflow: hidden`). Classifying the
|
|
8
|
+
* measurements is done here, outside the page, so `dom query` and `dom layout`
|
|
9
|
+
* agree and the rules can be tested without a browser.
|
|
10
|
+
*/
|
|
11
|
+
import { ELEMENT_DESCRIPTION_JS } from './elementInfo.js';
|
|
12
|
+
import { LAYOUT_REASONS, scrollLockedReason } from '../../ui/messages/commands.js';
|
|
13
|
+
/** Why page scroll cannot bring an element that is not fully in view into view. */
|
|
14
|
+
export const OFF_SCREEN_REASONS = {
|
|
15
|
+
fixed: 'fixed position, page scroll does not move it',
|
|
16
|
+
sticky: 'sticky position, page scroll moves it only until it sticks',
|
|
17
|
+
outOfRange: "beyond the page's scroll range",
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Page-side size of a window's viewport without scrollbars (the window size
|
|
21
|
+
* for quirks-mode documents, whose root element is as large as the content).
|
|
22
|
+
*/
|
|
23
|
+
export const VIEWPORT_SIZE_JS = `(view) => {
|
|
24
|
+
const root = view.document.documentElement;
|
|
25
|
+
const standards = view.document.compatMode === 'CSS1Compat' && root;
|
|
26
|
+
return {
|
|
27
|
+
width: standards ? root.clientWidth : view.innerWidth,
|
|
28
|
+
height: standards ? root.clientHeight : view.innerHeight
|
|
29
|
+
};
|
|
30
|
+
}`;
|
|
31
|
+
/**
|
|
32
|
+
* Page-side distances a window's page can scroll in each direction from its
|
|
33
|
+
* current position. Right-to-left pages scroll to negative `scrollX`. Content
|
|
34
|
+
* that a scrolling body cuts off is reported through the body as a clipper.
|
|
35
|
+
*/
|
|
36
|
+
export const SCROLL_RANGE_JS = `(view) => {
|
|
37
|
+
const doc = view.document;
|
|
38
|
+
const scroller = doc.scrollingElement || doc.documentElement;
|
|
39
|
+
const maxX = Math.max(0, scroller.scrollWidth - scroller.clientWidth);
|
|
40
|
+
const maxY = Math.max(0, scroller.scrollHeight - scroller.clientHeight);
|
|
41
|
+
const rtl = view.scrollX < 0 || view.getComputedStyle(doc.documentElement).direction === 'rtl';
|
|
42
|
+
const left = rtl ? maxX + view.scrollX : view.scrollX;
|
|
43
|
+
return { left: Math.max(0, left), up: Math.max(0, view.scrollY), right: Math.max(0, maxX - left), down: Math.max(0, maxY - view.scrollY) };
|
|
44
|
+
}`;
|
|
45
|
+
/**
|
|
46
|
+
* Page-side reason the page of a window cannot be scrolled although its
|
|
47
|
+
* content may extend below the fold: the scrolling element is no taller than
|
|
48
|
+
* the viewport while the body or root element is `position: fixed` or has
|
|
49
|
+
* `overflow: hidden` (or `clip`), which is how dialogs lock page scrolling,
|
|
50
|
+
* with the first visible dialog of the page (`dialog[open]`,
|
|
51
|
+
* `[aria-modal=true]`, `[role=dialog]`, `[role=alertdialog]`) as the likely
|
|
52
|
+
* reason. Null when the page can scroll or nothing locks it.
|
|
53
|
+
*/
|
|
54
|
+
const SCROLL_LOCK_JS = `(view, describe) => {
|
|
55
|
+
const doc = view.document;
|
|
56
|
+
const scroller = doc.scrollingElement || doc.documentElement;
|
|
57
|
+
if (scroller.scrollHeight > scroller.clientHeight + 1) return null;
|
|
58
|
+
const lockOf = (node, name) => {
|
|
59
|
+
if (!node) return null;
|
|
60
|
+
const style = view.getComputedStyle(node);
|
|
61
|
+
const parts = [];
|
|
62
|
+
if (style.position === 'fixed') parts.push('position: fixed');
|
|
63
|
+
if (style.overflowY === 'hidden' || style.overflowY === 'clip') parts.push('overflow: ' + style.overflowY);
|
|
64
|
+
return parts.length > 0 ? parts.join(', ') + ' on ' + name : null;
|
|
65
|
+
};
|
|
66
|
+
const by = lockOf(doc.body, 'body') || lockOf(doc.documentElement, 'html');
|
|
67
|
+
if (!by) return null;
|
|
68
|
+
const shown = (el) => {
|
|
69
|
+
const r = el.getBoundingClientRect();
|
|
70
|
+
return r.width > 0 && r.height > 0 && (!el.checkVisibility || el.checkVisibility({ visibilityProperty: true }));
|
|
71
|
+
};
|
|
72
|
+
const dialogs = doc.querySelectorAll('dialog[open], [aria-modal="true"], [role="dialog"], [role="alertdialog"]');
|
|
73
|
+
const dialog = Array.from(dialogs).find(shown);
|
|
74
|
+
return { by: by, dialog: dialog ? describe(dialog) : null };
|
|
75
|
+
}`;
|
|
76
|
+
/**
|
|
77
|
+
* Page-side offset of an iframe's content (inside its border and padding)
|
|
78
|
+
* within the viewport of the document holding the iframe.
|
|
79
|
+
*/
|
|
80
|
+
export const FRAME_OFFSET_JS = `(frame) => {
|
|
81
|
+
const rect = frame.getBoundingClientRect();
|
|
82
|
+
const style = frame.ownerDocument.defaultView.getComputedStyle(frame);
|
|
83
|
+
return {
|
|
84
|
+
x: rect.left + frame.clientLeft + parseFloat(style.paddingLeft),
|
|
85
|
+
y: rect.top + frame.clientTop + parseFloat(style.paddingTop)
|
|
86
|
+
};
|
|
87
|
+
}`;
|
|
88
|
+
/** Page-side parent in the flat tree: a slotted node's slot, then its parent, then a shadow root's host. */
|
|
89
|
+
const FLAT_PARENT_JS = `(n) => n.assignedSlot || n.parentElement || (n.parentNode && n.parentNode.host) || null`;
|
|
90
|
+
/**
|
|
91
|
+
* Page-side test of computed styles: true when they only scale or move the
|
|
92
|
+
* box (no `rotate`, and a 2D `transform` matrix without rotation or skew), so
|
|
93
|
+
* its bounding box is the box itself, scaled.
|
|
94
|
+
*/
|
|
95
|
+
export const SCALES_ONLY_JS = `(style) => {
|
|
96
|
+
if (style.rotate && style.rotate !== 'none') return false;
|
|
97
|
+
if (!style.transform || style.transform === 'none') return true;
|
|
98
|
+
const matrix = /^matrix\\(([^)]*)\\)$/.exec(style.transform);
|
|
99
|
+
if (!matrix) return false;
|
|
100
|
+
const values = matrix[1].split(',').map(parseFloat);
|
|
101
|
+
return Math.abs(values[1]) < 1e-6 && Math.abs(values[2]) < 1e-6;
|
|
102
|
+
}`;
|
|
103
|
+
/**
|
|
104
|
+
* Page-side padding box of a clipping container in its document's viewport
|
|
105
|
+
* coordinates. It is scaled by the container's rendered to layout size ratio
|
|
106
|
+
* (`getBoundingClientRect()` against `offsetWidth`/`offsetHeight`), so CSS
|
|
107
|
+
* `zoom` and `transform: scale()` on it or around it clip where they show;
|
|
108
|
+
* when it or an ancestor is rotated or skewed the bounding box is no longer
|
|
109
|
+
* its box, and the unscaled size is used. `clientWidth`/`clientHeight` are
|
|
110
|
+
* whole pixels: a container they report as 0 is measured from its rendered
|
|
111
|
+
* size minus borders, so only one with no area at all counts as collapsed.
|
|
112
|
+
*/
|
|
113
|
+
const CLIP_BOX_JS = `(p) => {
|
|
114
|
+
const scalesOnly = ${SCALES_ONLY_JS};
|
|
115
|
+
const parentOf = ${FLAT_PARENT_JS};
|
|
116
|
+
const styleOf = (n) => n.ownerDocument.defaultView.getComputedStyle(n);
|
|
117
|
+
let plain = true;
|
|
118
|
+
for (let n = p; n && plain; n = parentOf(n)) plain = scalesOnly(styleOf(n));
|
|
119
|
+
const style = styleOf(p);
|
|
120
|
+
const r = p.getBoundingClientRect();
|
|
121
|
+
const scaleX = plain && p.offsetWidth > 0 ? r.width / p.offsetWidth : 1;
|
|
122
|
+
const scaleY = plain && p.offsetHeight > 0 ? r.height / p.offsetHeight : 1;
|
|
123
|
+
const inner = (client, rendered, borders, scale) =>
|
|
124
|
+
client > 0 ? client * scale : Math.max(0, rendered - borders * scale);
|
|
125
|
+
const width = inner(p.clientWidth, plain ? r.width : p.offsetWidth, parseFloat(style.borderLeftWidth) + parseFloat(style.borderRightWidth), scaleX);
|
|
126
|
+
const height = inner(p.clientHeight, plain ? r.height : p.offsetHeight, parseFloat(style.borderTopWidth) + parseFloat(style.borderBottomWidth), scaleY);
|
|
127
|
+
const left = r.left + p.clientLeft * scaleX;
|
|
128
|
+
const top = r.top + p.clientTop * scaleY;
|
|
129
|
+
return { left: left, top: top, right: left + width, bottom: top + height };
|
|
130
|
+
}`;
|
|
131
|
+
/**
|
|
132
|
+
* Page-side overlay scrollbars of a clipping container: whether it scrolls
|
|
133
|
+
* along an axis with a scrollbar that takes no space (macOS, mobile), which
|
|
134
|
+
* shows over its right (vertical) or bottom (horizontal) edge after a scroll.
|
|
135
|
+
*/
|
|
136
|
+
const OVERLAY_SCROLLBARS_JS = `(p) => {
|
|
137
|
+
const style = p.ownerDocument.defaultView.getComputedStyle(p);
|
|
138
|
+
const scrolls = (overflow, content, client) => /auto|scroll/.test(overflow) && content > client;
|
|
139
|
+
const spare = (outer, client, a, b) => outer - client - parseFloat(a) - parseFloat(b) < 1;
|
|
140
|
+
return {
|
|
141
|
+
right: scrolls(style.overflowY, p.scrollHeight, p.clientHeight) && spare(p.offsetWidth, p.clientWidth, style.borderLeftWidth, style.borderRightWidth),
|
|
142
|
+
bottom: scrolls(style.overflowX, p.scrollWidth, p.clientWidth) && spare(p.offsetHeight, p.clientHeight, style.borderTopWidth, style.borderBottomWidth)
|
|
143
|
+
};
|
|
144
|
+
}`;
|
|
145
|
+
/**
|
|
146
|
+
* Page-side test of whether a container lets a `position: fixed` descendant
|
|
147
|
+
* escape to the viewport: false when it is transformed (or filtered,
|
|
148
|
+
* contained, …) and so holds it like an absolutely positioned one.
|
|
149
|
+
*/
|
|
150
|
+
const HOLDS_FIXED_JS = `(style) =>
|
|
151
|
+
style.transform !== 'none' || style.filter !== 'none' || style.perspective !== 'none' ||
|
|
152
|
+
/transform|filter|perspective/.test(style.willChange) || /paint|layout|strict|content/.test(style.contain)`;
|
|
153
|
+
/**
|
|
154
|
+
* Page-side test of whether a `position: fixed` node is fixed to its
|
|
155
|
+
* document's viewport (no ancestor holds it, {@link HOLDS_FIXED_JS}).
|
|
156
|
+
*/
|
|
157
|
+
const FIXED_TO_VIEWPORT_JS = `(n) => {
|
|
158
|
+
const holdsFixed = ${HOLDS_FIXED_JS};
|
|
159
|
+
const parentOf = (node) => node.parentElement || (node.parentNode && node.parentNode.host) || null;
|
|
160
|
+
for (let p = parentOf(n); p && p !== n.ownerDocument.documentElement; p = parentOf(p)) {
|
|
161
|
+
if (holdsFixed(p.ownerDocument.defaultView.getComputedStyle(p))) return false;
|
|
162
|
+
}
|
|
163
|
+
return true;
|
|
164
|
+
}`;
|
|
165
|
+
/**
|
|
166
|
+
* Page-side clip of a node by its ancestors (looked up through open shadow
|
|
167
|
+
* roots): the padding boxes ({@link CLIP_BOX_JS}) of those that cut off
|
|
168
|
+
* overflowing content and hold the node in their containing-block chain. An
|
|
169
|
+
* absolutely positioned node skips static ancestors (that are not
|
|
170
|
+
* transformed) up to its containing block, a fixed one is not clipped at all
|
|
171
|
+
* unless a transformed (or filtered, contained, …) ancestor holds it like an
|
|
172
|
+
* absolute one, and inline ancestors and `display: contents` ones have no box
|
|
173
|
+
* to clip with. The root element is left out (its overflow belongs to the
|
|
174
|
+
* viewport), and so is the body's overflow unless the root element's
|
|
175
|
+
* overflow is not `visible` (then the body keeps its own overflow and, e.g.
|
|
176
|
+
* as the page's scroller, clips like any container).
|
|
177
|
+
* Returns the clip, whether overlay scrollbars of the containers show along
|
|
178
|
+
* its right and bottom edges ({@link OVERLAY_SCROLLBARS_JS}), the innermost
|
|
179
|
+
* ancestor cutting off part of `rect` (null when none does), why the
|
|
180
|
+
* innermost clipping ancestor with no area (a collapsed
|
|
181
|
+
* `height: 0; overflow: hidden` accordion) hides it, e.g.
|
|
182
|
+
* `clipped by div#acc: zero height` (null when none has), and whether the
|
|
183
|
+
* node is fixed to the viewport (it or a container in its containing-block
|
|
184
|
+
* chain is `position: fixed`; `fixedBy` is that node).
|
|
185
|
+
*/
|
|
186
|
+
const ANCESTOR_CLIP_JS = `(node, rect, describe) => {
|
|
187
|
+
const clipBox = ${CLIP_BOX_JS};
|
|
188
|
+
const overlayScrollbars = ${OVERLAY_SCROLLBARS_JS};
|
|
189
|
+
const holdsFixed = ${HOLDS_FIXED_JS};
|
|
190
|
+
const fixedToViewport = ${FIXED_TO_VIEWPORT_JS};
|
|
191
|
+
const reasons = ${JSON.stringify(LAYOUT_REASONS)};
|
|
192
|
+
const styleOf = (n) => n.ownerDocument.defaultView.getComputedStyle(n);
|
|
193
|
+
const parentOf = (n) => n.parentElement || (n.parentNode && n.parentNode.host) || null;
|
|
194
|
+
const doc = node.ownerDocument;
|
|
195
|
+
const rootStyle = styleOf(doc.documentElement);
|
|
196
|
+
const bodyClips = rootStyle.overflowX !== 'visible' || rootStyle.overflowY !== 'visible';
|
|
197
|
+
const result = { clip: null, overlay: { right: false, bottom: false }, clipper: null, collapsed: null, fixed: false, fixedBy: null };
|
|
198
|
+
const add = (p) => {
|
|
199
|
+
const box = clipBox(p);
|
|
200
|
+
const contains = rect.left >= box.left && rect.top >= box.top && rect.right <= box.right && rect.bottom <= box.bottom;
|
|
201
|
+
if (!result.clipper && !contains) result.clipper = describe(p);
|
|
202
|
+
const empty = box.bottom <= box.top ? reasons.zeroHeight : box.right <= box.left ? reasons.zeroWidth : null;
|
|
203
|
+
if (!result.collapsed && empty) result.collapsed = reasons.clippedBy + describe(p) + ': ' + empty;
|
|
204
|
+
const clip = result.clip || box;
|
|
205
|
+
const bars = overlayScrollbars(p);
|
|
206
|
+
result.overlay = {
|
|
207
|
+
right: box.right < clip.right ? bars.right : box.right > clip.right ? result.overlay.right : result.overlay.right || bars.right,
|
|
208
|
+
bottom: box.bottom < clip.bottom ? bars.bottom : box.bottom > clip.bottom ? result.overlay.bottom : result.overlay.bottom || bars.bottom
|
|
209
|
+
};
|
|
210
|
+
result.clip = { left: Math.max(clip.left, box.left), top: Math.max(clip.top, box.top), right: Math.min(clip.right, box.right), bottom: Math.min(clip.bottom, box.bottom) };
|
|
211
|
+
};
|
|
212
|
+
let position = styleOf(node).position;
|
|
213
|
+
if (position === 'fixed') {
|
|
214
|
+
if (fixedToViewport(node)) return Object.assign(result, { fixed: true, fixedBy: node });
|
|
215
|
+
position = 'absolute';
|
|
216
|
+
}
|
|
217
|
+
for (let p = parentOf(node); p && p !== doc.documentElement; p = parentOf(p)) {
|
|
218
|
+
const style = styleOf(p);
|
|
219
|
+
if (position === 'absolute' && style.position === 'static' && !holdsFixed(style)) continue;
|
|
220
|
+
const boxed = style.display !== 'inline' && style.display !== 'contents' && (bodyClips || p !== doc.body);
|
|
221
|
+
if (boxed && (style.overflowX !== 'visible' || style.overflowY !== 'visible')) add(p);
|
|
222
|
+
position = style.position;
|
|
223
|
+
if (position === 'fixed') {
|
|
224
|
+
if (fixedToViewport(p)) return Object.assign(result, { fixed: true, fixedBy: p });
|
|
225
|
+
position = 'absolute';
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return result;
|
|
229
|
+
}`;
|
|
230
|
+
/** Page-side test of a `clip-path` that cuts everything away: `inset()` with percentages leaving no area. */
|
|
231
|
+
export const CLIP_PATH_CUTS_ALL_JS = `(clipPath) => {
|
|
232
|
+
const inset = /^inset\\(([^)]*)\\)/.exec(clipPath || '');
|
|
233
|
+
const values = inset ? inset[1].split(' round ')[0].trim().split(/\\s+/) : [];
|
|
234
|
+
if (values.length === 0 || values.some((v) => !/%$/.test(v))) return false;
|
|
235
|
+
const [top, right = top, bottom = top, left = right] = values.map(parseFloat);
|
|
236
|
+
return top + bottom >= 100 || left + right >= 100;
|
|
237
|
+
}`;
|
|
238
|
+
/**
|
|
239
|
+
* Page-side test of a `clip` that cuts everything away: `rect()` with no area
|
|
240
|
+
* on an absolutely positioned or fixed element (the "visually hidden"
|
|
241
|
+
* pattern; `clip` applies to no other element).
|
|
242
|
+
*/
|
|
243
|
+
export const CLIP_RECT_CUTS_ALL_JS = `(position, clip) => {
|
|
244
|
+
if ((position !== 'absolute' && position !== 'fixed') || !/^rect\\(/.test(clip || '')) return false;
|
|
245
|
+
const edges = (clip.match(/-?[\\d.]+/g) || []).map(Number);
|
|
246
|
+
return edges.length === 4 && (edges[1] <= edges[3] || edges[2] <= edges[0]);
|
|
247
|
+
}`;
|
|
248
|
+
/**
|
|
249
|
+
* Page-side reason an element that is rendered still cannot be seen: it or an
|
|
250
|
+
* ancestor in the flat tree (slots, shadow hosts, iframes) is fully
|
|
251
|
+
* transparent (`opacity: 0`), or cuts everything away
|
|
252
|
+
* ({@link CLIP_PATH_CUTS_ALL_JS}, {@link CLIP_RECT_CUTS_ALL_JS}). Other
|
|
253
|
+
* `clip-path` shapes are not evaluated. Null when none applies.
|
|
254
|
+
*/
|
|
255
|
+
const INVISIBLE_REASON_JS = `(el, describe) => {
|
|
256
|
+
const flatParent = ${FLAT_PARENT_JS};
|
|
257
|
+
const clipPathCutsAll = ${CLIP_PATH_CUTS_ALL_JS};
|
|
258
|
+
const clipRectCutsAll = ${CLIP_RECT_CUTS_ALL_JS};
|
|
259
|
+
const reasons = ${JSON.stringify(LAYOUT_REASONS)};
|
|
260
|
+
const parentOf = (n) => flatParent(n) || n.ownerDocument.defaultView.frameElement || null;
|
|
261
|
+
for (let n = el; n; n = parentOf(n)) {
|
|
262
|
+
const style = n.ownerDocument.defaultView.getComputedStyle(n);
|
|
263
|
+
const cause = parseFloat(style.opacity) === 0 ? reasons.transparent
|
|
264
|
+
: clipPathCutsAll(style.clipPath) ? 'clip-path: ' + style.clipPath
|
|
265
|
+
: clipRectCutsAll(style.position, style.clip) ? 'clip: ' + style.clip
|
|
266
|
+
: null;
|
|
267
|
+
if (cause) return n === el ? cause : cause + reasons.on + describe(n);
|
|
268
|
+
}
|
|
269
|
+
return null;
|
|
270
|
+
}`;
|
|
271
|
+
/**
|
|
272
|
+
* Page-side measurement of an element ({@link ElementGeometry}): its box in
|
|
273
|
+
* top-level viewport coordinates, its clip by ancestors
|
|
274
|
+
* ({@link ANCESTOR_CLIP_JS}) and by the viewports of its iframes, why it
|
|
275
|
+
* cannot be seen at all (not rendered, or inside a clipping container
|
|
276
|
+
* collapsed to zero size), why a rendered one is still invisible
|
|
277
|
+
* ({@link INVISIBLE_REASON_JS}), whether it is inert (an `inert` element
|
|
278
|
+
* around it, through shadow roots) or fixed to the top-level viewport, and how
|
|
279
|
+
* far the top-level page can scroll ({@link SCROLL_RANGE_JS}) or what locks
|
|
280
|
+
* its scrolling ({@link SCROLL_LOCK_JS}). Content of a body fixed to lock the
|
|
281
|
+
* page is not counted as fixed: it is in-flow content the lock holds in place.
|
|
282
|
+
*
|
|
283
|
+
* Rendering is decided by `checkVisibility()`, so content Chrome skips
|
|
284
|
+
* (inside a closed `<details>`, under `content-visibility: hidden`) is hidden
|
|
285
|
+
* although it still has a box. Content skipped by `content-visibility: auto`
|
|
286
|
+
* is not: it is rendered once scrolled near the viewport.
|
|
287
|
+
*/
|
|
288
|
+
export const ELEMENT_GEOMETRY_JS = `(el) => {
|
|
289
|
+
const viewportSize = ${VIEWPORT_SIZE_JS};
|
|
290
|
+
const frameOffset = ${FRAME_OFFSET_JS};
|
|
291
|
+
const ancestorClip = ${ANCESTOR_CLIP_JS};
|
|
292
|
+
const scrollRange = ${SCROLL_RANGE_JS};
|
|
293
|
+
const scrollLock = ${SCROLL_LOCK_JS};
|
|
294
|
+
const invisibleReason = ${INVISIBLE_REASON_JS};
|
|
295
|
+
const describe = ${ELEMENT_DESCRIPTION_JS};
|
|
296
|
+
const reasons = ${JSON.stringify(LAYOUT_REASONS)};
|
|
297
|
+
const styleOf = (node) => node.ownerDocument.defaultView.getComputedStyle(node);
|
|
298
|
+
const parentOf = (node) => node.parentElement || (node.parentNode && node.parentNode.host) || null;
|
|
299
|
+
const shift = (r, x, y) => r && { left: r.left + x, top: r.top + y, right: r.right + x, bottom: r.bottom + y };
|
|
300
|
+
const intersect = (a, b) => {
|
|
301
|
+
if (!a || !b) return a || b;
|
|
302
|
+
const left = Math.max(a.left, b.left);
|
|
303
|
+
const top = Math.max(a.top, b.top);
|
|
304
|
+
return { left: left, top: top, right: Math.max(left, Math.min(a.right, b.right)), bottom: Math.max(top, Math.min(a.bottom, b.bottom)) };
|
|
305
|
+
};
|
|
306
|
+
const skippedReason = (node) => {
|
|
307
|
+
if (typeof node.checkVisibility !== 'function' || node.checkVisibility()) return null;
|
|
308
|
+
for (let child = node, p = parentOf(node); p; child = p, p = parentOf(p)) {
|
|
309
|
+
if (p.tagName === 'DETAILS' && !p.open && p.querySelector(':scope > summary') !== child) return 'inside a closed <details>';
|
|
310
|
+
if (styleOf(p).contentVisibility === 'hidden') return 'content-visibility: hidden on ' + describe(p);
|
|
311
|
+
}
|
|
312
|
+
return 'not rendered';
|
|
313
|
+
};
|
|
314
|
+
const hiddenReason = (style, box) => {
|
|
315
|
+
if (style.display === 'none') return 'display: none';
|
|
316
|
+
if (style.display === 'contents') return 'display: contents (no box of its own)';
|
|
317
|
+
if (el.getClientRects().length === 0) {
|
|
318
|
+
return el.tagName === 'OPTION' ? reasons.option : 'not rendered (an ancestor has display: none)';
|
|
319
|
+
}
|
|
320
|
+
const skipped = skippedReason(el);
|
|
321
|
+
if (skipped) return skipped;
|
|
322
|
+
if (style.visibility !== 'visible') return 'visibility: ' + style.visibility;
|
|
323
|
+
if (box.width === 0 || box.height === 0) return 'zero size';
|
|
324
|
+
return null;
|
|
325
|
+
};
|
|
326
|
+
const isSticky = () => {
|
|
327
|
+
for (let n = el; n; n = parentOf(n)) {
|
|
328
|
+
if (styleOf(n).position === 'sticky') return true;
|
|
329
|
+
}
|
|
330
|
+
return false;
|
|
331
|
+
};
|
|
332
|
+
const isInert = () => {
|
|
333
|
+
for (let n = el; n; n = n.parentElement || (n.getRootNode() && n.getRootNode().host) || null) {
|
|
334
|
+
if (n.hasAttribute('inert')) return true;
|
|
335
|
+
}
|
|
336
|
+
return false;
|
|
337
|
+
};
|
|
338
|
+
const box = el.getBoundingClientRect();
|
|
339
|
+
let rect = { left: box.left, top: box.top, right: box.right, bottom: box.bottom };
|
|
340
|
+
const own = ancestorClip(el, rect, describe);
|
|
341
|
+
let clip = own.clip;
|
|
342
|
+
let overlay = own.overlay;
|
|
343
|
+
let clipper = own.clipper;
|
|
344
|
+
let fixed = own.fixed;
|
|
345
|
+
let fixedBy = own.fixedBy;
|
|
346
|
+
let hidden = hiddenReason(styleOf(el), box) || own.collapsed;
|
|
347
|
+
let x = 0;
|
|
348
|
+
let y = 0;
|
|
349
|
+
for (let view = el.ownerDocument.defaultView; view && view.frameElement; view = view.parent) {
|
|
350
|
+
const frame = view.frameElement;
|
|
351
|
+
const size = viewportSize(view);
|
|
352
|
+
if (!clipper && (rect.left < 0 || rect.top < 0 || rect.right > size.width || rect.bottom > size.height)) clipper = describe(frame);
|
|
353
|
+
const unframed = clip;
|
|
354
|
+
clip = intersect(clip, { left: 0, top: 0, right: size.width, bottom: size.height });
|
|
355
|
+
overlay = { right: !!unframed && unframed.right <= clip.right && overlay.right, bottom: !!unframed && unframed.bottom <= clip.bottom && overlay.bottom };
|
|
356
|
+
const offset = frameOffset(frame);
|
|
357
|
+
rect = shift(rect, offset.x, offset.y);
|
|
358
|
+
const outer = ancestorClip(frame, rect, describe);
|
|
359
|
+
const inner = shift(clip, offset.x, offset.y);
|
|
360
|
+
clip = intersect(inner, outer.clip);
|
|
361
|
+
overlay = { right: clip.right < inner.right ? outer.overlay.right : overlay.right, bottom: clip.bottom < inner.bottom ? outer.overlay.bottom : overlay.bottom };
|
|
362
|
+
clipper = clipper || outer.clipper;
|
|
363
|
+
fixed = outer.fixed;
|
|
364
|
+
fixedBy = outer.fixedBy;
|
|
365
|
+
hidden = hidden || outer.collapsed;
|
|
366
|
+
x += offset.x;
|
|
367
|
+
y += offset.y;
|
|
368
|
+
if (!hidden && (frame.getClientRects().length === 0 || styleOf(frame).visibility !== 'visible' || skippedReason(frame))) hidden = 'inside a hidden iframe';
|
|
369
|
+
}
|
|
370
|
+
let top = el.ownerDocument.defaultView;
|
|
371
|
+
while (top.frameElement) top = top.parent;
|
|
372
|
+
const toBox = (r) => r && { x: r.left, y: r.top, width: r.right - r.left, height: r.bottom - r.top };
|
|
373
|
+
const invisible = hidden ? null : invisibleReason(el, describe);
|
|
374
|
+
const lock = scrollLock(top, describe);
|
|
375
|
+
if (lock && fixedBy === top.document.body) fixed = false;
|
|
376
|
+
return { rect: toBox(rect), clip: toBox(clip), clipOverlay: overlay, clipper: clipper, hidden: hidden, invisible: invisible, inert: isInert(), fixed: fixed, sticky: !fixed && isSticky(), pageScroll: scrollRange(top), scrollLock: lock, offset: { x: x, y: y } };
|
|
377
|
+
}`;
|
|
378
|
+
/**
|
|
379
|
+
* Edges of a box.
|
|
380
|
+
*
|
|
381
|
+
* @param box - Position and size
|
|
382
|
+
* @returns Its edges
|
|
383
|
+
*/
|
|
384
|
+
function edgesOf(box) {
|
|
385
|
+
return { left: box.x, top: box.y, right: box.x + box.width, bottom: box.y + box.height };
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Area of the overlap of two boxes.
|
|
389
|
+
*
|
|
390
|
+
* @param a - First box
|
|
391
|
+
* @param b - Second box
|
|
392
|
+
* @returns Overlap, or null when they do not overlap
|
|
393
|
+
*/
|
|
394
|
+
function overlap(a, b) {
|
|
395
|
+
const result = {
|
|
396
|
+
left: Math.max(a.left, b.left),
|
|
397
|
+
top: Math.max(a.top, b.top),
|
|
398
|
+
right: Math.min(a.right, b.right),
|
|
399
|
+
bottom: Math.min(a.bottom, b.bottom),
|
|
400
|
+
};
|
|
401
|
+
return result.right > result.left && result.bottom > result.top ? result : null;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* The direction in which a box lies entirely outside another, vertical first
|
|
405
|
+
* (pages mostly scroll vertically).
|
|
406
|
+
*
|
|
407
|
+
* @param rect - Element box
|
|
408
|
+
* @param view - Visible area
|
|
409
|
+
* @returns Direction, or null when they overlap on both axes
|
|
410
|
+
*/
|
|
411
|
+
function directionOutside(rect, view) {
|
|
412
|
+
if (rect.bottom <= view.top)
|
|
413
|
+
return 'above';
|
|
414
|
+
if (rect.top >= view.bottom)
|
|
415
|
+
return 'below';
|
|
416
|
+
if (rect.right <= view.left)
|
|
417
|
+
return 'left';
|
|
418
|
+
if (rect.left >= view.right)
|
|
419
|
+
return 'right';
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
/** Rounding slack (px) when checking that a span is fully in view */
|
|
423
|
+
const IN_VIEW_SLACK = 1;
|
|
424
|
+
/**
|
|
425
|
+
* Length of a span that is inside the viewport after a scroll.
|
|
426
|
+
*
|
|
427
|
+
* @param start - Start of the span in viewport coordinates
|
|
428
|
+
* @param size - Length of the span
|
|
429
|
+
* @param viewSize - Length of the viewport
|
|
430
|
+
* @param scroll - Scroll along the axis (negative: back)
|
|
431
|
+
* @returns Visible length (0 when out of view)
|
|
432
|
+
*/
|
|
433
|
+
function visibleLength(start, size, viewSize, scroll) {
|
|
434
|
+
return Math.max(0, Math.min(start - scroll + size, viewSize) - Math.max(start - scroll, 0));
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Where along one axis a scroll should bring a span's start (in the current
|
|
438
|
+
* viewport coordinates): centred, or just inside the edge cutting it off; a
|
|
439
|
+
* span larger than the viewport gets its start aligned either way.
|
|
440
|
+
*
|
|
441
|
+
* @param start - Start of the span in viewport coordinates
|
|
442
|
+
* @param size - Length of the span
|
|
443
|
+
* @param viewSize - Length of the viewport
|
|
444
|
+
* @param aim - Centre it, or the smallest scroll showing all of it
|
|
445
|
+
* @returns Scroll that would do it (negative: back), before limiting it to the page
|
|
446
|
+
*/
|
|
447
|
+
export function scrollTarget(start, size, viewSize, aim) {
|
|
448
|
+
if (size > viewSize)
|
|
449
|
+
return start;
|
|
450
|
+
if (aim === 'centre')
|
|
451
|
+
return start + size / 2 - viewSize / 2;
|
|
452
|
+
return start < 0 ? start : start + size - viewSize;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Page scroll along one axis that brings a span into view ({@link ScrollAim}).
|
|
456
|
+
* Limited to how far the page can scroll; when that is not far enough to
|
|
457
|
+
* show the span fully, no scroll helps.
|
|
458
|
+
*
|
|
459
|
+
* @param start - Start of the span in viewport coordinates
|
|
460
|
+
* @param size - Length of the span
|
|
461
|
+
* @param viewSize - Length of the viewport
|
|
462
|
+
* @param range - How far the page can scroll back (up or left) and forward (down or right)
|
|
463
|
+
* @param aim - Centre it, or the smallest scroll showing all of it
|
|
464
|
+
* @returns Pixels to scroll (negative: back), 0 when it is fully in view, null
|
|
465
|
+
* when the page cannot scroll far enough
|
|
466
|
+
*/
|
|
467
|
+
function axisScroll(start, size, viewSize, range, aim) {
|
|
468
|
+
const fully = Math.min(size, viewSize) - IN_VIEW_SLACK;
|
|
469
|
+
if (visibleLength(start, size, viewSize, 0) >= fully)
|
|
470
|
+
return 0;
|
|
471
|
+
const target = scrollTarget(start, size, viewSize, aim);
|
|
472
|
+
const scroll = Math.round(Math.min(range.forward, Math.max(-range.back, target))) || 0;
|
|
473
|
+
return visibleLength(start, size, viewSize, scroll) >= fully ? scroll : null;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Classify where an element is relative to the top-level viewport.
|
|
477
|
+
*
|
|
478
|
+
* Visible parts are what lies inside the viewport and the element's clip
|
|
479
|
+
* (iframes, overflow containers). An element with no visible part is `above`,
|
|
480
|
+
* `below`, `left` or `right` of the viewport (or of its clip, e.g. scrolled
|
|
481
|
+
* out of a list), and `hidden` when it is not rendered at all. Being inert
|
|
482
|
+
* does not change the position: inert elements are shown.
|
|
483
|
+
*
|
|
484
|
+
* @param geometry - Page-side measurements
|
|
485
|
+
* @param viewport - Top-level viewport size
|
|
486
|
+
* @returns Position, with the visible share, hidden reason, clipping
|
|
487
|
+
* ancestor or page scroll when relevant
|
|
488
|
+
*/
|
|
489
|
+
export function classifyViewportPosition(geometry, viewport) {
|
|
490
|
+
if (geometry.hidden)
|
|
491
|
+
return { inViewport: 'hidden', hiddenReason: geometry.hidden };
|
|
492
|
+
const rect = edgesOf(geometry.rect);
|
|
493
|
+
const screen = edgesOf({ x: 0, y: 0, ...viewport });
|
|
494
|
+
const view = geometry.clip ? overlap(screen, edgesOf(geometry.clip)) : screen;
|
|
495
|
+
const seen = view && overlap(rect, view);
|
|
496
|
+
if (!seen) {
|
|
497
|
+
const direction = directionOutside(rect, screen) ??
|
|
498
|
+
(geometry.clip && directionOutside(rect, edgesOf(geometry.clip)));
|
|
499
|
+
if (!direction) {
|
|
500
|
+
return {
|
|
501
|
+
inViewport: 'hidden',
|
|
502
|
+
hiddenReason: `clipped by ${geometry.clipper ?? 'an ancestor'}`,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
return { inViewport: direction, ...outOfViewAdvice(geometry, viewport, 'centre') };
|
|
506
|
+
}
|
|
507
|
+
const area = (seen.right - seen.left) * (seen.bottom - seen.top);
|
|
508
|
+
const share = (area / (geometry.rect.width * geometry.rect.height)) * 100;
|
|
509
|
+
if (share >= 99.5)
|
|
510
|
+
return { inViewport: 'visible' };
|
|
511
|
+
const percentVisible = Math.min(99, Math.max(1, Math.round(share)));
|
|
512
|
+
return {
|
|
513
|
+
inViewport: 'partly',
|
|
514
|
+
percentVisible,
|
|
515
|
+
...outOfViewAdvice(geometry, viewport, 'minimal'),
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* What brings a not fully visible element into view: the ancestor or iframe
|
|
520
|
+
* cutting it off when there is one (page scroll alone would not help),
|
|
521
|
+
* otherwise the page scroll, unless the page scroll does not move it (fixed)
|
|
522
|
+
* or not all the way (sticky: the scroll needed cannot be told), or cannot go far enough (it is beyond the scrollable area, or the page does
|
|
523
|
+
* not scroll at all, e.g. locked by a dialog).
|
|
524
|
+
*
|
|
525
|
+
* @param geometry - Page-side measurements
|
|
526
|
+
* @param viewport - Top-level viewport size
|
|
527
|
+
* @param aim - Centre it (out of view), or the smallest scroll showing all of it (partly in view)
|
|
528
|
+
* @returns `clippedBy`, `scrollBy` or `offScreenReason`, or nothing when no
|
|
529
|
+
* scroll is needed
|
|
530
|
+
*/
|
|
531
|
+
function outOfViewAdvice(geometry, viewport, aim) {
|
|
532
|
+
if (geometry.clipper)
|
|
533
|
+
return { clippedBy: geometry.clipper };
|
|
534
|
+
if (geometry.fixed)
|
|
535
|
+
return { offScreenReason: OFF_SCREEN_REASONS.fixed };
|
|
536
|
+
if (geometry.sticky)
|
|
537
|
+
return { offScreenReason: OFF_SCREEN_REASONS.sticky };
|
|
538
|
+
const { rect, pageScroll } = geometry;
|
|
539
|
+
const horizontal = { back: pageScroll.left, forward: pageScroll.right };
|
|
540
|
+
const vertical = { back: pageScroll.up, forward: pageScroll.down };
|
|
541
|
+
const x = axisScroll(rect.x, rect.width, viewport.width, horizontal, aim);
|
|
542
|
+
const y = axisScroll(rect.y, rect.height, viewport.height, vertical, aim);
|
|
543
|
+
if (x === null || y === null) {
|
|
544
|
+
const { scrollLock } = geometry;
|
|
545
|
+
return {
|
|
546
|
+
offScreenReason: scrollLock
|
|
547
|
+
? scrollLockedReason(scrollLock.by, scrollLock.dialog)
|
|
548
|
+
: OFF_SCREEN_REASONS.outOfRange,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
return x === 0 && y === 0 ? {} : { scrollBy: { x, y } };
|
|
552
|
+
}
|
|
553
|
+
//# sourceMappingURL=elementGeometry.js.map
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page-side descriptions of elements for DOM command output (`dom query`,
|
|
3
|
+
* `dom layout`, DOM context of console messages): their text as a user sees
|
|
4
|
+
* it and where they live (iframes, shadow roots).
|
|
5
|
+
*/
|
|
6
|
+
/** Length of the text `dom get` shows for an element. */
|
|
7
|
+
export declare const ELEMENT_TEXT_LENGTH = 500;
|
|
8
|
+
/**
|
|
9
|
+
* Page-side removal of decorations from an element's text: close buttons
|
|
10
|
+
* (`.close`, `aria-label="Close"` or `"Dismiss"`, a button or link showing
|
|
11
|
+
* just `×`) and `aria-hidden` icons (text without letters or digits), so a
|
|
12
|
+
* flash message does not end in "×". `aria-hidden` text with words is kept:
|
|
13
|
+
* it is often the visible twin of a screen-reader text. Each decoration's
|
|
14
|
+
* text is removed once, from the end.
|
|
15
|
+
*/
|
|
16
|
+
export declare const WITHOUT_DECORATIONS_JS = "(el, text) => {\n if (!text || typeof el.querySelectorAll !== 'function') return text;\n const glyph = /^\\s*[\u00D7\u2715\u2716\u2717\u2A2F]\\s*$/;\n const textOf = (node) => (typeof node.innerText === 'string' ? node.innerText : node.textContent || '').trim();\n const closer = '.close, [aria-label=\"close\" i], [aria-label=\"dismiss\" i]';\n const rendered = (node) => !node.checkVisibility || node.checkVisibility();\n const found = Array.from(el.querySelectorAll(closer + ', [aria-hidden=\"true\"]'))\n .filter((node) => rendered(node) && (node.matches(closer) || !/[\\p{L}\\p{N}]/u.test(textOf(node))));\n if (/[\u00D7\u2715\u2716\u2717\u2A2F]/.test(text)) {\n found.push(...Array.from(el.querySelectorAll('button, a, [role=\"button\"]')).filter((node) => glyph.test(node.textContent || '')));\n }\n const unique = Array.from(new Set(found));\n const outermost = unique.filter((node) => !unique.some((other) => other !== node && other.contains(node)));\n let result = text;\n for (const node of outermost) {\n const part = textOf(node);\n const at = part ? result.lastIndexOf(part) : -1;\n if (at >= 0) result = result.slice(0, at) + result.slice(at + part.length);\n }\n return result;\n}";
|
|
17
|
+
/**
|
|
18
|
+
* Page-side text of an element as a user sees it: `innerText` for a rendered
|
|
19
|
+
* element (CSS-hidden parts left out, inline elements not split apart), none
|
|
20
|
+
* for an element that is not rendered, `textContent` for SVG and other
|
|
21
|
+
* elements without `innerText` and for `display: contents` wrappers (no box
|
|
22
|
+
* of their own, but their children are shown), and the label of an
|
|
23
|
+
* `<option>` (which its `<select>` renders). For large containers (more
|
|
24
|
+
* than 2000 characters of text) only the start is read, from the text nodes
|
|
25
|
+
* whose parent is rendered, so a preview never lays out a whole page's text,
|
|
26
|
+
* unless `full` is set. Decorations are left out ({@link WITHOUT_DECORATIONS_JS}).
|
|
27
|
+
*/
|
|
28
|
+
export declare const ELEMENT_TEXT_JS = "(el, full) => {\n const withoutDecorations = (el, text) => {\n if (!text || typeof el.querySelectorAll !== 'function') return text;\n const glyph = /^\\s*[\u00D7\u2715\u2716\u2717\u2A2F]\\s*$/;\n const textOf = (node) => (typeof node.innerText === 'string' ? node.innerText : node.textContent || '').trim();\n const closer = '.close, [aria-label=\"close\" i], [aria-label=\"dismiss\" i]';\n const rendered = (node) => !node.checkVisibility || node.checkVisibility();\n const found = Array.from(el.querySelectorAll(closer + ', [aria-hidden=\"true\"]'))\n .filter((node) => rendered(node) && (node.matches(closer) || !/[\\p{L}\\p{N}]/u.test(textOf(node))));\n if (/[\u00D7\u2715\u2716\u2717\u2A2F]/.test(text)) {\n found.push(...Array.from(el.querySelectorAll('button, a, [role=\"button\"]')).filter((node) => glyph.test(node.textContent || '')));\n }\n const unique = Array.from(new Set(found));\n const outermost = unique.filter((node) => !unique.some((other) => other !== node && other.contains(node)));\n let result = text;\n for (const node of outermost) {\n const part = textOf(node);\n const at = part ? result.lastIndexOf(part) : -1;\n if (at >= 0) result = result.slice(0, at) + result.slice(at + part.length);\n }\n return result;\n};\n const all = el.textContent || '';\n if (el.tagName === 'OPTION') return el.label;\n if (typeof el.innerText !== 'string') return full ? all : all.slice(0, 2000);\n const rendered = (node) => !node.checkVisibility || node.checkVisibility();\n if (!rendered(el)) {\n const boxless = el.ownerDocument.defaultView.getComputedStyle(el).display === 'contents';\n return boxless ? withoutDecorations(el, full ? all : all.slice(0, 2000)) : '';\n }\n if (full || all.length <= 2000) return withoutDecorations(el, el.innerText);\n const walker = el.ownerDocument.createTreeWalker(el, NodeFilter.SHOW_TEXT);\n let start = '';\n while (start.length < 1000 && walker.nextNode()) {\n const parent = walker.currentNode.parentElement;\n if (!parent || rendered(parent)) start += walker.currentNode.data;\n }\n return withoutDecorations(el, start);\n}";
|
|
29
|
+
/**
|
|
30
|
+
* Page-side short description of an element: tag, id and up to two classes,
|
|
31
|
+
* e.g. `button#save.primary.large`.
|
|
32
|
+
*/
|
|
33
|
+
export declare const ELEMENT_DESCRIPTION_JS = "(node) => node.tagName.toLowerCase() +\n (node.id ? '#' + node.id : '') +\n (node.classList && node.classList.length ? '.' + Array.from(node.classList).slice(0, 2).join('.') : '')";
|
|
34
|
+
/**
|
|
35
|
+
* Page-side position of an element among its parent's children with the
|
|
36
|
+
* same short description ({@link ELEMENT_DESCRIPTION_JS}), e.g. `(2nd of 3)`;
|
|
37
|
+
* empty when it is the only one.
|
|
38
|
+
*/
|
|
39
|
+
export declare const SIBLING_POSITION_JS = "(el) => {\n const describe = (node) => node.tagName.toLowerCase() +\n (node.id ? '#' + node.id : '') +\n (node.classList && node.classList.length ? '.' + Array.from(node.classList).slice(0, 2).join('.') : '');\n const parent = el.parentElement;\n if (!parent) return '';\n const own = describe(el);\n const same = Array.from(parent.children).filter((child) => describe(child) === own);\n if (same.length < 2) return '';\n const n = same.indexOf(el) + 1;\n const teen = n % 100 >= 11 && n % 100 <= 13;\n const suffix = teen ? 'th' : ({ 1: 'st', 2: 'nd', 3: 'rd' })[n % 10] || 'th';\n return '(' + n + suffix + ' of ' + same.length + ')';\n}";
|
|
40
|
+
/**
|
|
41
|
+
* Page-side identity of an element an action hit, so the output says which
|
|
42
|
+
* one it was, as one string: its short description
|
|
43
|
+
* ({@link ELEMENT_DESCRIPTION_JS}) and visible text (button value for button
|
|
44
|
+
* inputs, decorations left out), e.g. `button#add.btn "Add to cart"`. A
|
|
45
|
+
* `<select>` is named by its label, aria-label or name, else by its selected
|
|
46
|
+
* option, e.g. `select.sort "Sort products"`. An element without visible
|
|
47
|
+
* text is described by itself first: by its position among same-looking
|
|
48
|
+
* siblings ({@link SIBLING_POSITION_JS}), e.g. `div.figure (2nd of 3)`, with
|
|
49
|
+
* its aria-label, placeholder or title when it has one. Only an element
|
|
50
|
+
* without an id that is the only one of its kind is named by the nearest of
|
|
51
|
+
* three ancestors that has text, e.g.
|
|
52
|
+
* `input.toggle in div.view "Write report"` (rows of a list share their
|
|
53
|
+
* aria-label), leaving out the options
|
|
54
|
+
* of `<select>`s in it (read like `innerText`: CSS-hidden text is left out of
|
|
55
|
+
* a rendered ancestor; at most 500 text nodes; selects in shadow roots are
|
|
56
|
+
* not looked into), and never past an editable ancestor, whose text may be
|
|
57
|
+
* typed input; otherwise its aria-label, placeholder or title is used. Texts
|
|
58
|
+
* are cut at 40 characters. Contenteditable elements count as controls: what
|
|
59
|
+
* was typed into them is never echoed (nor their ancestors' text).
|
|
60
|
+
*/
|
|
61
|
+
export declare const ELEMENT_IDENTITY_JS = "(el) => {\n const describe = (node) => node.tagName.toLowerCase() +\n (node.id ? '#' + node.id : '') +\n (node.classList && node.classList.length ? '.' + Array.from(node.classList).slice(0, 2).join('.') : '');\n const siblingPosition = (el) => {\n const describe = (node) => node.tagName.toLowerCase() +\n (node.id ? '#' + node.id : '') +\n (node.classList && node.classList.length ? '.' + Array.from(node.classList).slice(0, 2).join('.') : '');\n const parent = el.parentElement;\n if (!parent) return '';\n const own = describe(el);\n const same = Array.from(parent.children).filter((child) => describe(child) === own);\n if (same.length < 2) return '';\n const n = same.indexOf(el) + 1;\n const teen = n % 100 >= 11 && n % 100 <= 13;\n const suffix = teen ? 'th' : ({ 1: 'st', 2: 'nd', 3: 'rd' })[n % 10] || 'th';\n return '(' + n + suffix + ' of ' + same.length + ')';\n};\n const withoutDecorations = (el, text) => {\n if (!text || typeof el.querySelectorAll !== 'function') return text;\n const glyph = /^\\s*[\u00D7\u2715\u2716\u2717\u2A2F]\\s*$/;\n const textOf = (node) => (typeof node.innerText === 'string' ? node.innerText : node.textContent || '').trim();\n const closer = '.close, [aria-label=\"close\" i], [aria-label=\"dismiss\" i]';\n const rendered = (node) => !node.checkVisibility || node.checkVisibility();\n const found = Array.from(el.querySelectorAll(closer + ', [aria-hidden=\"true\"]'))\n .filter((node) => rendered(node) && (node.matches(closer) || !/[\\p{L}\\p{N}]/u.test(textOf(node))));\n if (/[\u00D7\u2715\u2716\u2717\u2A2F]/.test(text)) {\n found.push(...Array.from(el.querySelectorAll('button, a, [role=\"button\"]')).filter((node) => glyph.test(node.textContent || '')));\n }\n const unique = Array.from(new Set(found));\n const outermost = unique.filter((node) => !unique.some((other) => other !== node && other.contains(node)));\n let result = text;\n for (const node of outermost) {\n const part = textOf(node);\n const at = part ? result.lastIndexOf(part) : -1;\n if (at >= 0) result = result.slice(0, at) + result.slice(at + part.length);\n }\n return result;\n};\n const clean = (text) => (text || '').replace(/\\s+/g, ' ').trim();\n const cut = (text) => {\n const characters = Array.from(text);\n return characters.length > 40 ? characters.slice(0, 40).join('') + '\u2026' : text;\n };\n const isControl = (node) => /^(input|select|textarea)$/.test(node.localName) || node.isContentEditable;\n const shownText = (node) =>\n isControl(node) ? '' : clean(withoutDecorations(node, typeof node.innerText === 'string' ? node.innerText : node.textContent));\n const buttonValue = (node) => (node.localName === 'input' && /^(submit|button|reset)$/i.test(node.type) ? clean(node.value) : '');\n const attributeText = (node) =>\n clean(node.getAttribute('aria-label')) || clean(node.getAttribute('placeholder')) || clean(node.getAttribute('title'));\n const selectName = (node) =>\n clean(node.labels && node.labels[0] && node.labels[0].innerText) ||\n clean(node.getAttribute('aria-label')) ||\n clean(node.getAttribute('name')) ||\n clean(node.selectedOptions && node.selectedOptions[0] && node.selectedOptions[0].label);\n const shown = (node, options) => !node.checkVisibility || node.checkVisibility(options);\n const textOutsideSelects = (node) => {\n if (!node.querySelector('select')) return shownText(node);\n const hiddenLeftOut = shown(node);\n const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_TEXT);\n let text = '';\n for (let visited = 0; visited < 500 && text.length < 200 && walker.nextNode(); visited++) {\n const parent = walker.currentNode.parentElement;\n if (!parent || parent.closest('select, textarea')) continue;\n if (!hiddenLeftOut || shown(parent, { visibilityProperty: true })) text += ' ' + walker.currentNode.data;\n }\n return clean(text);\n };\n const quoted = (text) => (text ? ' \"' + cut(text) + '\"' : '');\n const visible = shownText(el) || buttonValue(el) || (el.localName === 'select' ? selectName(el) : '');\n if (visible) return describe(el) + quoted(visible);\n const attribute = attributeText(el);\n const position = siblingPosition(el);\n if (position) return describe(el) + ' ' + position + quoted(attribute);\n let ancestor = el.id || el.isContentEditable ? null : el.parentElement;\n for (let depth = 0; ancestor && depth < 3; depth++, ancestor = ancestor.parentElement) {\n if (isControl(ancestor)) break;\n const text = textOutsideSelects(ancestor);\n if (text) return describe(el) + ' in ' + describe(ancestor) + quoted(text);\n }\n return describe(el) + quoted(attribute);\n}";
|
|
62
|
+
/**
|
|
63
|
+
* Page-side location of an element: the iframes it is in (outermost first)
|
|
64
|
+
* and the shadow root holding it, e.g. `iframe#pay > shadow root of <x-card>`;
|
|
65
|
+
* empty for the main document.
|
|
66
|
+
*/
|
|
67
|
+
export declare const ELEMENT_CONTEXT_JS = "(el) => {\n const describe = (node) => node.tagName.toLowerCase() + (node.id ? '#' + node.id : '');\n const parts = [];\n for (let doc = el.ownerDocument; doc && doc.defaultView && doc.defaultView.frameElement; ) {\n const frame = doc.defaultView.frameElement;\n parts.unshift(describe(frame));\n doc = frame.ownerDocument;\n }\n const root = el.getRootNode();\n if (root.host) parts.push('shadow root of <' + describe(root.host) + '>');\n return parts.join(' > ');\n}";
|
|
68
|
+
/**
|
|
69
|
+
* Short text preview of an element's text: whitespace collapsed, cut on a
|
|
70
|
+
* whole character (an emoji is never split, which would make JSON invalid).
|
|
71
|
+
*
|
|
72
|
+
* @param text - Element text as the page renders it
|
|
73
|
+
* @param length - Characters kept
|
|
74
|
+
* @returns Collapsed text, truncated to `length` characters (followed by `...`)
|
|
75
|
+
*/
|
|
76
|
+
export declare function textPreview(text: string, length?: number): string;
|
|
77
|
+
//# sourceMappingURL=elementInfo.d.ts.map
|