@forgecart/designer-runtime 1.0.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 +166 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/dist/loader-cjs/package.json +3 -0
- package/dist/loader-cjs/turbo-loader.d.ts +13 -0
- package/dist/loader-cjs/turbo-loader.js +190 -0
- package/dist/loader-cjs/turbo-loader.js.map +1 -0
- package/dist/protocol/element-reference-formatter.d.ts +28 -0
- package/dist/protocol/element-reference-formatter.js +60 -0
- package/dist/protocol/element-reference-formatter.js.map +1 -0
- package/dist/protocol/element.types.d.ts +75 -0
- package/dist/protocol/element.types.js +7 -0
- package/dist/protocol/element.types.js.map +1 -0
- package/dist/protocol/index.d.ts +5 -0
- package/dist/protocol/index.js +6 -0
- package/dist/protocol/index.js.map +1 -0
- package/dist/protocol/message-codec.d.ts +50 -0
- package/dist/protocol/message-codec.js +209 -0
- package/dist/protocol/message-codec.js.map +1 -0
- package/dist/protocol/message.types.d.ts +122 -0
- package/dist/protocol/message.types.js +2 -0
- package/dist/protocol/message.types.js.map +1 -0
- package/dist/protocol/protocol.constant.d.ts +30 -0
- package/dist/protocol/protocol.constant.js +31 -0
- package/dist/protocol/protocol.constant.js.map +1 -0
- package/dist/runtime/descriptor/element-descriptor-extractor.d.ts +38 -0
- package/dist/runtime/descriptor/element-descriptor-extractor.js +190 -0
- package/dist/runtime/descriptor/element-descriptor-extractor.js.map +1 -0
- package/dist/runtime/designer-runtime.d.ts +85 -0
- package/dist/runtime/designer-runtime.js +321 -0
- package/dist/runtime/designer-runtime.js.map +1 -0
- package/dist/runtime/guest-messenger.d.ts +37 -0
- package/dist/runtime/guest-messenger.js +41 -0
- package/dist/runtime/guest-messenger.js.map +1 -0
- package/dist/runtime/handshake-gate.d.ts +35 -0
- package/dist/runtime/handshake-gate.js +81 -0
- package/dist/runtime/handshake-gate.js.map +1 -0
- package/dist/runtime/highlight/highlight-overlay.d.ts +25 -0
- package/dist/runtime/highlight/highlight-overlay.js +80 -0
- package/dist/runtime/highlight/highlight-overlay.js.map +1 -0
- package/dist/runtime/region/dom-hit-tester.d.ts +28 -0
- package/dist/runtime/region/dom-hit-tester.js +45 -0
- package/dist/runtime/region/dom-hit-tester.js.map +1 -0
- package/dist/runtime/region/element-filter.d.ts +60 -0
- package/dist/runtime/region/element-filter.js +146 -0
- package/dist/runtime/region/element-filter.js.map +1 -0
- package/dist/runtime/region/region-sampler.d.ts +41 -0
- package/dist/runtime/region/region-sampler.js +100 -0
- package/dist/runtime/region/region-sampler.js.map +1 -0
- package/dist/runtime/region/spray-region-resolver.d.ts +53 -0
- package/dist/runtime/region/spray-region-resolver.js +113 -0
- package/dist/runtime/region/spray-region-resolver.js.map +1 -0
- package/dist/util/geometry.util.d.ts +29 -0
- package/dist/util/geometry.util.js +68 -0
- package/dist/util/geometry.util.js.map +1 -0
- package/package.json +49 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { PROTOCOL_NAMESPACE, PROTOCOL_VERSION } from '../protocol/protocol.constant.js';
|
|
2
|
+
/**
|
|
3
|
+
* The guest's only postMessage exit. Every message except the boot READY
|
|
4
|
+
* beacon is posted with the origin pinned by the handshake — the browser
|
|
5
|
+
* drops the message if the embedding page navigated elsewhere, so guest data
|
|
6
|
+
* can never leak to a window that is no longer the dashboard.
|
|
7
|
+
*/
|
|
8
|
+
export class GuestMessenger {
|
|
9
|
+
targetWindow;
|
|
10
|
+
pinnedOrigin = null;
|
|
11
|
+
constructor(targetWindow) {
|
|
12
|
+
this.targetWindow = targetWindow;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The pre-handshake boot beacon — the single message ever posted with
|
|
16
|
+
* targetOrigin `'*'`. It carries zero payload beyond the envelope, so an
|
|
17
|
+
* arbitrary embedder learns nothing it could not infer from the public
|
|
18
|
+
* storefront template.
|
|
19
|
+
*/
|
|
20
|
+
postReady() {
|
|
21
|
+
this.targetWindow.postMessage({ ns: PROTOCOL_NAMESPACE, v: PROTOCOL_VERSION, type: 'READY' }, '*');
|
|
22
|
+
}
|
|
23
|
+
/** Pin the host origin after a validated HELLO; later pins must match. */
|
|
24
|
+
pinOrigin(origin) {
|
|
25
|
+
this.pinnedOrigin = origin;
|
|
26
|
+
}
|
|
27
|
+
getPinnedOrigin() {
|
|
28
|
+
return this.pinnedOrigin;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Post a protocol message to the pinned host origin. Before the handshake
|
|
32
|
+
* pinned an origin this is a silent no-op — pre-handshake the guest says
|
|
33
|
+
* nothing but READY.
|
|
34
|
+
*/
|
|
35
|
+
post(body) {
|
|
36
|
+
if (this.pinnedOrigin === null)
|
|
37
|
+
return;
|
|
38
|
+
this.targetWindow.postMessage({ ns: PROTOCOL_NAMESPACE, v: PROTOCOL_VERSION, ...body }, this.pinnedOrigin);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=guest-messenger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guest-messenger.js","sourceRoot":"","sources":["../../src/runtime/guest-messenger.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAUxF;;;;;GAKG;AACH,MAAM,OAAO,cAAc;IAGI;IAFrB,YAAY,GAAkB,IAAI,CAAC;IAE3C,YAA6B,YAAoB;QAApB,iBAAY,GAAZ,YAAY,CAAQ;IAAG,CAAC;IAErD;;;;;OAKG;IACH,SAAS;QACP,IAAI,CAAC,YAAY,CAAC,WAAW,CAC3B,EAAE,EAAE,EAAE,kBAAkB,EAAE,CAAC,EAAE,gBAAgB,EAAE,IAAI,EAAE,OAAO,EAAE,EAC9D,GAAG,CACJ,CAAC;IACJ,CAAC;IAED,0EAA0E;IAC1E,SAAS,CAAC,MAAc;QACtB,IAAI,CAAC,YAAY,GAAG,MAAM,CAAC;IAC7B,CAAC;IAED,eAAe;QACb,OAAO,IAAI,CAAC,YAAY,CAAC;IAC3B,CAAC;IAED;;;;OAIG;IACH,IAAI,CAAC,IAAsB;QACzB,IAAI,IAAI,CAAC,YAAY,KAAK,IAAI;YAAE,OAAO;QACvC,IAAI,CAAC,YAAY,CAAC,WAAW,CAC3B,EAAE,EAAE,EAAE,kBAAkB,EAAE,CAAC,EAAE,gBAAgB,EAAE,GAAG,IAAI,EAAE,EACxD,IAAI,CAAC,YAAY,CAClB,CAAC;IACJ,CAAC;CACF"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { HelloMessage } from '../protocol/message.types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Guards the handshake with the capability nonce the host appended to the
|
|
4
|
+
* preview URL as `#fcd=<uuid>`. The fragment never reaches the server; the
|
|
5
|
+
* gate captures it on construction, parks it in sessionStorage (so it
|
|
6
|
+
* survives iframe reloads and client-side navigations) and strips only the
|
|
7
|
+
* `fcd` key from the hash. Without a nonce every HELLO is ignored silently —
|
|
8
|
+
* an arbitrary page embedding the storefront learns nothing.
|
|
9
|
+
*/
|
|
10
|
+
export declare class HandshakeGate {
|
|
11
|
+
private readonly win;
|
|
12
|
+
private readonly nonce;
|
|
13
|
+
private pinnedOrigin;
|
|
14
|
+
constructor(win: Window);
|
|
15
|
+
/** True when a nonce was captured from the fragment or sessionStorage. */
|
|
16
|
+
get hasNonce(): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Validates a HELLO against the full rejection matrix: the sender must be
|
|
19
|
+
* the direct parent window, carry a real (non-opaque) origin, present the
|
|
20
|
+
* exact nonce and speak our protocol version. The first success pins the
|
|
21
|
+
* sender's origin; a re-HELLO (host re-handshake after an iframe reload it
|
|
22
|
+
* did not observe) must come from that same pinned origin.
|
|
23
|
+
*/
|
|
24
|
+
acceptHello(event: MessageEvent, message: HelloMessage): boolean;
|
|
25
|
+
/** The origin pinned by the first accepted HELLO, or null before that. */
|
|
26
|
+
getPinnedOrigin(): string | null;
|
|
27
|
+
private captureNonce;
|
|
28
|
+
private extractFragmentNonce;
|
|
29
|
+
/**
|
|
30
|
+
* Removes only the `fcd` key from the hash via history.replaceState —
|
|
31
|
+
* sibling fragment keys (`#a=b&fcd=x`) and the rest of the URL stay
|
|
32
|
+
* untouched, and no navigation or hashchange event is triggered.
|
|
33
|
+
*/
|
|
34
|
+
private stripFragmentKey;
|
|
35
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { FRAGMENT_KEY, NONCE_STORAGE_KEY, PROTOCOL_VERSION, } from '../protocol/protocol.constant.js';
|
|
2
|
+
/**
|
|
3
|
+
* Guards the handshake with the capability nonce the host appended to the
|
|
4
|
+
* preview URL as `#fcd=<uuid>`. The fragment never reaches the server; the
|
|
5
|
+
* gate captures it on construction, parks it in sessionStorage (so it
|
|
6
|
+
* survives iframe reloads and client-side navigations) and strips only the
|
|
7
|
+
* `fcd` key from the hash. Without a nonce every HELLO is ignored silently —
|
|
8
|
+
* an arbitrary page embedding the storefront learns nothing.
|
|
9
|
+
*/
|
|
10
|
+
export class HandshakeGate {
|
|
11
|
+
win;
|
|
12
|
+
nonce;
|
|
13
|
+
pinnedOrigin = null;
|
|
14
|
+
constructor(win) {
|
|
15
|
+
this.win = win;
|
|
16
|
+
this.nonce = this.captureNonce();
|
|
17
|
+
}
|
|
18
|
+
/** True when a nonce was captured from the fragment or sessionStorage. */
|
|
19
|
+
get hasNonce() {
|
|
20
|
+
return this.nonce !== null;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Validates a HELLO against the full rejection matrix: the sender must be
|
|
24
|
+
* the direct parent window, carry a real (non-opaque) origin, present the
|
|
25
|
+
* exact nonce and speak our protocol version. The first success pins the
|
|
26
|
+
* sender's origin; a re-HELLO (host re-handshake after an iframe reload it
|
|
27
|
+
* did not observe) must come from that same pinned origin.
|
|
28
|
+
*/
|
|
29
|
+
acceptHello(event, message) {
|
|
30
|
+
if (this.nonce === null)
|
|
31
|
+
return false;
|
|
32
|
+
if (event.source !== this.win.parent)
|
|
33
|
+
return false;
|
|
34
|
+
if (!event.origin || event.origin === 'null')
|
|
35
|
+
return false;
|
|
36
|
+
if (message.nonce !== this.nonce)
|
|
37
|
+
return false;
|
|
38
|
+
if (message.protocolVersion !== PROTOCOL_VERSION)
|
|
39
|
+
return false;
|
|
40
|
+
if (this.pinnedOrigin !== null && event.origin !== this.pinnedOrigin)
|
|
41
|
+
return false;
|
|
42
|
+
this.pinnedOrigin = event.origin;
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
/** The origin pinned by the first accepted HELLO, or null before that. */
|
|
46
|
+
getPinnedOrigin() {
|
|
47
|
+
return this.pinnedOrigin;
|
|
48
|
+
}
|
|
49
|
+
captureNonce() {
|
|
50
|
+
const fromFragment = this.extractFragmentNonce();
|
|
51
|
+
if (fromFragment === null) {
|
|
52
|
+
return this.win.sessionStorage.getItem(NONCE_STORAGE_KEY);
|
|
53
|
+
}
|
|
54
|
+
this.win.sessionStorage.setItem(NONCE_STORAGE_KEY, fromFragment);
|
|
55
|
+
this.stripFragmentKey();
|
|
56
|
+
return fromFragment;
|
|
57
|
+
}
|
|
58
|
+
extractFragmentNonce() {
|
|
59
|
+
const hash = this.win.location.hash;
|
|
60
|
+
if (!hash.startsWith('#'))
|
|
61
|
+
return null;
|
|
62
|
+
const value = new URLSearchParams(hash.slice(1)).get(FRAGMENT_KEY);
|
|
63
|
+
if (value === null || value.length === 0)
|
|
64
|
+
return null;
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Removes only the `fcd` key from the hash via history.replaceState —
|
|
69
|
+
* sibling fragment keys (`#a=b&fcd=x`) and the rest of the URL stay
|
|
70
|
+
* untouched, and no navigation or hashchange event is triggered.
|
|
71
|
+
*/
|
|
72
|
+
stripFragmentKey() {
|
|
73
|
+
const { location, history } = this.win;
|
|
74
|
+
const params = new URLSearchParams(location.hash.slice(1));
|
|
75
|
+
params.delete(FRAGMENT_KEY);
|
|
76
|
+
const remaining = params.toString();
|
|
77
|
+
const hash = remaining.length > 0 ? `#${remaining}` : '';
|
|
78
|
+
history.replaceState(history.state, '', `${location.pathname}${location.search}${hash}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=handshake-gate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handshake-gate.js","sourceRoot":"","sources":["../../src/runtime/handshake-gate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visual highlight for host-selected elements. One container `<div>` marked
|
|
3
|
+
* with `data-fc-designer-overlay` is lazily appended to `<body>` (fixed,
|
|
4
|
+
* full-viewport, pointer-events none — invisible to hit-testing via the
|
|
5
|
+
* overlay-skip in DomHitTester) and filled with absolutely positioned boxes
|
|
6
|
+
* at each shown element's current rect. The shown element list is retained
|
|
7
|
+
* so `refresh()` can re-position the boxes after scroll/resize.
|
|
8
|
+
*/
|
|
9
|
+
export declare class HighlightOverlay {
|
|
10
|
+
private readonly doc;
|
|
11
|
+
private container;
|
|
12
|
+
private shown;
|
|
13
|
+
constructor(doc: Document);
|
|
14
|
+
/** Replaces all highlight boxes with boxes for `elements`. */
|
|
15
|
+
show(elements: Element[]): void;
|
|
16
|
+
/** Re-positions the boxes at the shown elements' current rects. */
|
|
17
|
+
refresh(): void;
|
|
18
|
+
/** Removes every box; the container stays mounted for the next show. */
|
|
19
|
+
clear(): void;
|
|
20
|
+
/** Full teardown: boxes and container are removed from the document. */
|
|
21
|
+
dispose(): void;
|
|
22
|
+
private renderBoxes;
|
|
23
|
+
private ensureContainer;
|
|
24
|
+
private createBox;
|
|
25
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { OVERLAY_ATTRIBUTE } from '../../protocol/protocol.constant.js';
|
|
2
|
+
/** z-index one below the maximum, leaving room for true emergency chrome. */
|
|
3
|
+
const OVERLAY_Z_INDEX = '2147483646';
|
|
4
|
+
const BOX_BORDER = '2px solid rgba(99, 102, 241, 0.95)';
|
|
5
|
+
const BOX_FILL = 'rgba(99, 102, 241, 0.12)';
|
|
6
|
+
/**
|
|
7
|
+
* Visual highlight for host-selected elements. One container `<div>` marked
|
|
8
|
+
* with `data-fc-designer-overlay` is lazily appended to `<body>` (fixed,
|
|
9
|
+
* full-viewport, pointer-events none — invisible to hit-testing via the
|
|
10
|
+
* overlay-skip in DomHitTester) and filled with absolutely positioned boxes
|
|
11
|
+
* at each shown element's current rect. The shown element list is retained
|
|
12
|
+
* so `refresh()` can re-position the boxes after scroll/resize.
|
|
13
|
+
*/
|
|
14
|
+
export class HighlightOverlay {
|
|
15
|
+
doc;
|
|
16
|
+
container = null;
|
|
17
|
+
shown = [];
|
|
18
|
+
constructor(doc) {
|
|
19
|
+
this.doc = doc;
|
|
20
|
+
}
|
|
21
|
+
/** Replaces all highlight boxes with boxes for `elements`. */
|
|
22
|
+
show(elements) {
|
|
23
|
+
this.shown = [...elements];
|
|
24
|
+
this.renderBoxes();
|
|
25
|
+
}
|
|
26
|
+
/** Re-positions the boxes at the shown elements' current rects. */
|
|
27
|
+
refresh() {
|
|
28
|
+
if (this.shown.length === 0)
|
|
29
|
+
return;
|
|
30
|
+
this.renderBoxes();
|
|
31
|
+
}
|
|
32
|
+
/** Removes every box; the container stays mounted for the next show. */
|
|
33
|
+
clear() {
|
|
34
|
+
this.shown = [];
|
|
35
|
+
this.container?.replaceChildren();
|
|
36
|
+
}
|
|
37
|
+
/** Full teardown: boxes and container are removed from the document. */
|
|
38
|
+
dispose() {
|
|
39
|
+
this.shown = [];
|
|
40
|
+
this.container?.remove();
|
|
41
|
+
this.container = null;
|
|
42
|
+
}
|
|
43
|
+
renderBoxes() {
|
|
44
|
+
const container = this.ensureContainer();
|
|
45
|
+
container.replaceChildren();
|
|
46
|
+
for (const element of this.shown) {
|
|
47
|
+
const rect = element.getBoundingClientRect();
|
|
48
|
+
if (rect.width <= 0 || rect.height <= 0)
|
|
49
|
+
continue;
|
|
50
|
+
container.appendChild(this.createBox(rect));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
ensureContainer() {
|
|
54
|
+
if (this.container !== null)
|
|
55
|
+
return this.container;
|
|
56
|
+
const container = this.doc.createElement('div');
|
|
57
|
+
container.setAttribute(OVERLAY_ATTRIBUTE, '');
|
|
58
|
+
container.style.position = 'fixed';
|
|
59
|
+
container.style.inset = '0';
|
|
60
|
+
container.style.pointerEvents = 'none';
|
|
61
|
+
container.style.zIndex = OVERLAY_Z_INDEX;
|
|
62
|
+
this.doc.body.appendChild(container);
|
|
63
|
+
this.container = container;
|
|
64
|
+
return container;
|
|
65
|
+
}
|
|
66
|
+
createBox(rect) {
|
|
67
|
+
const box = this.doc.createElement('div');
|
|
68
|
+
box.style.position = 'absolute';
|
|
69
|
+
box.style.left = `${String(rect.left)}px`;
|
|
70
|
+
box.style.top = `${String(rect.top)}px`;
|
|
71
|
+
box.style.width = `${String(rect.width)}px`;
|
|
72
|
+
box.style.height = `${String(rect.height)}px`;
|
|
73
|
+
box.style.boxSizing = 'border-box';
|
|
74
|
+
box.style.border = BOX_BORDER;
|
|
75
|
+
box.style.backgroundColor = BOX_FILL;
|
|
76
|
+
box.style.borderRadius = '2px';
|
|
77
|
+
return box;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=highlight-overlay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"highlight-overlay.js","sourceRoot":"","sources":["../../../src/runtime/highlight/highlight-overlay.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Point } from '../../protocol/element.types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Hit-testing seam between the region pipeline and the live document.
|
|
4
|
+
* jsdom does not implement `elementsFromPoint`, so tests inject a fake that
|
|
5
|
+
* resolves points against scripted bounds instead of a layout engine.
|
|
6
|
+
*/
|
|
7
|
+
export interface HitTester {
|
|
8
|
+
/** The full element stack under `point`, topmost first. */
|
|
9
|
+
elementsAt(point: Point): Element[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Production {@link HitTester} backed by `document.elementsFromPoint`. The
|
|
13
|
+
* raw stack is normalized for selection: our own highlight overlay subtree
|
|
14
|
+
* is invisible to hit-testing, and SVG-internal elements (paths, groups,
|
|
15
|
+
* uses) collapse onto their owning `<svg>` root — an icon is one selectable
|
|
16
|
+
* thing, not a stack of vector primitives.
|
|
17
|
+
*/
|
|
18
|
+
export declare class DomHitTester implements HitTester {
|
|
19
|
+
private readonly doc;
|
|
20
|
+
constructor(doc: Document);
|
|
21
|
+
elementsAt(point: Point): Element[];
|
|
22
|
+
/**
|
|
23
|
+
* Climbs an SVG-namespace element to the outermost `<svg>` ancestor.
|
|
24
|
+
* Namespace comparison (instead of instanceof) is realm-safe and stops at
|
|
25
|
+
* `<foreignObject>` boundaries, where children are HTML again.
|
|
26
|
+
*/
|
|
27
|
+
private resolveSvgOwner;
|
|
28
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { OVERLAY_ATTRIBUTE } from '../../protocol/protocol.constant.js';
|
|
2
|
+
const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
|
|
3
|
+
/**
|
|
4
|
+
* Production {@link HitTester} backed by `document.elementsFromPoint`. The
|
|
5
|
+
* raw stack is normalized for selection: our own highlight overlay subtree
|
|
6
|
+
* is invisible to hit-testing, and SVG-internal elements (paths, groups,
|
|
7
|
+
* uses) collapse onto their owning `<svg>` root — an icon is one selectable
|
|
8
|
+
* thing, not a stack of vector primitives.
|
|
9
|
+
*/
|
|
10
|
+
export class DomHitTester {
|
|
11
|
+
doc;
|
|
12
|
+
constructor(doc) {
|
|
13
|
+
this.doc = doc;
|
|
14
|
+
}
|
|
15
|
+
elementsAt(point) {
|
|
16
|
+
const stack = this.doc.elementsFromPoint(point.x, point.y);
|
|
17
|
+
const seen = new Set();
|
|
18
|
+
const elements = [];
|
|
19
|
+
for (const element of stack) {
|
|
20
|
+
if (element.closest(`[${OVERLAY_ATTRIBUTE}]`) !== null)
|
|
21
|
+
continue;
|
|
22
|
+
const resolved = this.resolveSvgOwner(element);
|
|
23
|
+
if (seen.has(resolved))
|
|
24
|
+
continue;
|
|
25
|
+
seen.add(resolved);
|
|
26
|
+
elements.push(resolved);
|
|
27
|
+
}
|
|
28
|
+
return elements;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Climbs an SVG-namespace element to the outermost `<svg>` ancestor.
|
|
32
|
+
* Namespace comparison (instead of instanceof) is realm-safe and stops at
|
|
33
|
+
* `<foreignObject>` boundaries, where children are HTML again.
|
|
34
|
+
*/
|
|
35
|
+
resolveSvgOwner(element) {
|
|
36
|
+
if (element.namespaceURI !== SVG_NAMESPACE)
|
|
37
|
+
return element;
|
|
38
|
+
let owner = element;
|
|
39
|
+
while (owner.parentElement !== null && owner.parentElement.namespaceURI === SVG_NAMESPACE) {
|
|
40
|
+
owner = owner.parentElement;
|
|
41
|
+
}
|
|
42
|
+
return owner;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=dom-hit-tester.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dom-hit-tester.js","sourceRoot":"","sources":["../../../src/runtime/region/dom-hit-tester.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,qCAAqC,CAAC;AAYxE,MAAM,aAAa,GAAG,4BAA4B,CAAC;AAEnD;;;;;;GAMG;AACH,MAAM,OAAO,YAAY;IACM;IAA7B,YAA6B,GAAa;QAAb,QAAG,GAAH,GAAG,CAAU;IAAG,CAAC;IAE9C,UAAU,CAAC,KAAY;QACrB,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;QAC3D,MAAM,IAAI,GAAG,IAAI,GAAG,EAAW,CAAC;QAChC,MAAM,QAAQ,GAAc,EAAE,CAAC;QAC/B,KAAK,MAAM,OAAO,IAAI,KAAK,EAAE,CAAC;YAC5B,IAAI,OAAO,CAAC,OAAO,CAAC,IAAI,iBAAiB,GAAG,CAAC,KAAK,IAAI;gBAAE,SAAS;YACjE,MAAM,QAAQ,GAAG,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;YAC/C,IAAI,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC;gBAAE,SAAS;YACjC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YACnB,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC1B,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED;;;;OAIG;IACK,eAAe,CAAC,OAAgB;QACtC,IAAI,OAAO,CAAC,YAAY,KAAK,aAAa;YAAE,OAAO,OAAO,CAAC;QAC3D,IAAI,KAAK,GAAG,OAAO,CAAC;QACpB,OAAO,KAAK,CAAC,aAAa,KAAK,IAAI,IAAI,KAAK,CAAC,aAAa,CAAC,YAAY,KAAK,aAAa,EAAE,CAAC;YAC1F,KAAK,GAAG,KAAK,CAAC,aAAa,CAAC;QAC9B,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;CACF"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Rect, Viewport } from '../../protocol/element.types.js';
|
|
2
|
+
/** One element surviving hit-testing, with its spray-coverage evidence. */
|
|
3
|
+
export interface RegionCandidate {
|
|
4
|
+
element: Element;
|
|
5
|
+
bounds: Rect;
|
|
6
|
+
/** Indices of the sampled grid cells whose hit stack contained the element. */
|
|
7
|
+
hitCells: ReadonlySet<number>;
|
|
8
|
+
/** Painted fraction of the element: hit cells / cells inside its bounds. */
|
|
9
|
+
coverage: number;
|
|
10
|
+
}
|
|
11
|
+
/** Ranked, capped selection plus whether candidates were cut by the cap. */
|
|
12
|
+
export interface RankedSelection {
|
|
13
|
+
candidates: RegionCandidate[];
|
|
14
|
+
truncated: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Selection heuristics for the spray-region pipeline: rejects elements that
|
|
18
|
+
* can never be meaningful targets, collapses ancestor/descendant stacks down
|
|
19
|
+
* to the elements the admin plausibly meant, and ranks what is left.
|
|
20
|
+
*/
|
|
21
|
+
export declare class ElementFilter {
|
|
22
|
+
private readonly win;
|
|
23
|
+
constructor(win: Window);
|
|
24
|
+
/**
|
|
25
|
+
* True when the element is a plausible selection target: visible, has
|
|
26
|
+
* area, is not document chrome (html/body/head metadata), not a
|
|
27
|
+
* page-spanning wrapper and not Next.js dev-overlay chrome.
|
|
28
|
+
*/
|
|
29
|
+
isMeaningful(element: Element, viewport: Viewport): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Collapses ancestor/descendant stacks. Candidates are processed
|
|
32
|
+
* ancestors-first (sorted by DOM depth) and each surviving parent is
|
|
33
|
+
* weighed against its surviving candidate descendants:
|
|
34
|
+
*
|
|
35
|
+
* 1. Parent wins — the parent itself is well painted (coverage ≥ 0.6) and
|
|
36
|
+
* absorbs ≥ 2 candidate descendants: the admin sprayed the group.
|
|
37
|
+
* 2. Children win — the parent's hit cells are a subset of its candidate
|
|
38
|
+
* descendants' union (≤ 1 exclusive cell of tolerance): the parent was
|
|
39
|
+
* only ever hit through its children.
|
|
40
|
+
* 3. Pure-wrapper collapse — exactly one descendant fills ≥ 90% of the
|
|
41
|
+
* parent's rect: the parent is a layout wrapper. Wrapper chains unwind
|
|
42
|
+
* across the ancestors-first pass because each level sees exactly one
|
|
43
|
+
* near-full-size descendant.
|
|
44
|
+
* 4. Otherwise both genuinely overlap the spray — keep both.
|
|
45
|
+
*/
|
|
46
|
+
dedupeAncestors(candidates: RegionCandidate[]): RegionCandidate[];
|
|
47
|
+
/**
|
|
48
|
+
* Orders survivors by coverage (descending) then area (ascending —
|
|
49
|
+
* tighter elements over loose containers) and cuts at the cap.
|
|
50
|
+
*/
|
|
51
|
+
rankAndCap(survivors: RegionCandidate[], maxElements: number | undefined): RankedSelection;
|
|
52
|
+
private resolveOverlap;
|
|
53
|
+
/** Cells the parent was hit in that none of its descendants were. */
|
|
54
|
+
private exclusiveCellCount;
|
|
55
|
+
private isPureWrapper;
|
|
56
|
+
private isInvisible;
|
|
57
|
+
private isNextDevChrome;
|
|
58
|
+
private domDepth;
|
|
59
|
+
private rectArea;
|
|
60
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/** Tags that are never meaningful selection targets. */
|
|
2
|
+
const EXCLUDED_TAGS = new Set([
|
|
3
|
+
'html',
|
|
4
|
+
'body',
|
|
5
|
+
'script',
|
|
6
|
+
'style',
|
|
7
|
+
'template',
|
|
8
|
+
'noscript',
|
|
9
|
+
'link',
|
|
10
|
+
'meta',
|
|
11
|
+
'nextjs-portal',
|
|
12
|
+
]);
|
|
13
|
+
/** Elements covering more than this viewport fraction are page wrappers. */
|
|
14
|
+
const VIEWPORT_COVER_LIMIT = 0.85;
|
|
15
|
+
/** Parent-wins threshold: painted fraction at which a parent absorbs children. */
|
|
16
|
+
const PARENT_COVERAGE_THRESHOLD = 0.6;
|
|
17
|
+
/** Children-win tolerance: cells a parent may own beyond its children's union. */
|
|
18
|
+
const EXCLUSIVE_CELL_TOLERANCE = 1;
|
|
19
|
+
/** Wrapper collapse: child rect fraction at which the parent adds nothing. */
|
|
20
|
+
const WRAPPER_AREA_RATIO = 0.9;
|
|
21
|
+
/** Default and hard maximum for the selection cap. */
|
|
22
|
+
const DEFAULT_MAX_ELEMENTS = 12;
|
|
23
|
+
const HARD_MAX_ELEMENTS = 25;
|
|
24
|
+
/**
|
|
25
|
+
* Selection heuristics for the spray-region pipeline: rejects elements that
|
|
26
|
+
* can never be meaningful targets, collapses ancestor/descendant stacks down
|
|
27
|
+
* to the elements the admin plausibly meant, and ranks what is left.
|
|
28
|
+
*/
|
|
29
|
+
export class ElementFilter {
|
|
30
|
+
win;
|
|
31
|
+
constructor(win) {
|
|
32
|
+
this.win = win;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* True when the element is a plausible selection target: visible, has
|
|
36
|
+
* area, is not document chrome (html/body/head metadata), not a
|
|
37
|
+
* page-spanning wrapper and not Next.js dev-overlay chrome.
|
|
38
|
+
*/
|
|
39
|
+
isMeaningful(element, viewport) {
|
|
40
|
+
if (EXCLUDED_TAGS.has(element.tagName.toLowerCase()))
|
|
41
|
+
return false;
|
|
42
|
+
if (this.isNextDevChrome(element))
|
|
43
|
+
return false;
|
|
44
|
+
if (this.isInvisible(element))
|
|
45
|
+
return false;
|
|
46
|
+
const rect = element.getBoundingClientRect();
|
|
47
|
+
if (rect.width <= 0 || rect.height <= 0)
|
|
48
|
+
return false;
|
|
49
|
+
const viewportArea = viewport.width * viewport.height;
|
|
50
|
+
if (viewportArea > 0 && rect.width * rect.height > VIEWPORT_COVER_LIMIT * viewportArea) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Collapses ancestor/descendant stacks. Candidates are processed
|
|
57
|
+
* ancestors-first (sorted by DOM depth) and each surviving parent is
|
|
58
|
+
* weighed against its surviving candidate descendants:
|
|
59
|
+
*
|
|
60
|
+
* 1. Parent wins — the parent itself is well painted (coverage ≥ 0.6) and
|
|
61
|
+
* absorbs ≥ 2 candidate descendants: the admin sprayed the group.
|
|
62
|
+
* 2. Children win — the parent's hit cells are a subset of its candidate
|
|
63
|
+
* descendants' union (≤ 1 exclusive cell of tolerance): the parent was
|
|
64
|
+
* only ever hit through its children.
|
|
65
|
+
* 3. Pure-wrapper collapse — exactly one descendant fills ≥ 90% of the
|
|
66
|
+
* parent's rect: the parent is a layout wrapper. Wrapper chains unwind
|
|
67
|
+
* across the ancestors-first pass because each level sees exactly one
|
|
68
|
+
* near-full-size descendant.
|
|
69
|
+
* 4. Otherwise both genuinely overlap the spray — keep both.
|
|
70
|
+
*/
|
|
71
|
+
dedupeAncestors(candidates) {
|
|
72
|
+
const sorted = [...candidates].sort((a, b) => this.domDepth(a.element) - this.domDepth(b.element));
|
|
73
|
+
const dropped = new Set();
|
|
74
|
+
for (const parent of sorted) {
|
|
75
|
+
if (dropped.has(parent))
|
|
76
|
+
continue;
|
|
77
|
+
const descendants = sorted.filter((candidate) => candidate !== parent &&
|
|
78
|
+
!dropped.has(candidate) &&
|
|
79
|
+
parent.element.contains(candidate.element));
|
|
80
|
+
if (descendants.length === 0)
|
|
81
|
+
continue;
|
|
82
|
+
this.resolveOverlap(parent, descendants, dropped);
|
|
83
|
+
}
|
|
84
|
+
return sorted.filter((candidate) => !dropped.has(candidate));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Orders survivors by coverage (descending) then area (ascending —
|
|
88
|
+
* tighter elements over loose containers) and cuts at the cap.
|
|
89
|
+
*/
|
|
90
|
+
rankAndCap(survivors, maxElements) {
|
|
91
|
+
const cap = Math.min(maxElements ?? DEFAULT_MAX_ELEMENTS, HARD_MAX_ELEMENTS);
|
|
92
|
+
const ranked = [...survivors].sort((a, b) => b.coverage - a.coverage || this.rectArea(a.bounds) - this.rectArea(b.bounds));
|
|
93
|
+
return { candidates: ranked.slice(0, cap), truncated: ranked.length > cap };
|
|
94
|
+
}
|
|
95
|
+
resolveOverlap(parent, descendants, dropped) {
|
|
96
|
+
if (parent.coverage >= PARENT_COVERAGE_THRESHOLD && descendants.length >= 2) {
|
|
97
|
+
for (const descendant of descendants)
|
|
98
|
+
dropped.add(descendant);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (this.exclusiveCellCount(parent, descendants) <= EXCLUSIVE_CELL_TOLERANCE) {
|
|
102
|
+
dropped.add(parent);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (this.isPureWrapper(parent, descendants))
|
|
106
|
+
dropped.add(parent);
|
|
107
|
+
}
|
|
108
|
+
/** Cells the parent was hit in that none of its descendants were. */
|
|
109
|
+
exclusiveCellCount(parent, descendants) {
|
|
110
|
+
let exclusive = 0;
|
|
111
|
+
for (const cell of parent.hitCells) {
|
|
112
|
+
if (!descendants.some((descendant) => descendant.hitCells.has(cell)))
|
|
113
|
+
exclusive++;
|
|
114
|
+
}
|
|
115
|
+
return exclusive;
|
|
116
|
+
}
|
|
117
|
+
isPureWrapper(parent, descendants) {
|
|
118
|
+
const parentArea = this.rectArea(parent.bounds);
|
|
119
|
+
const nearFullSize = descendants.filter((descendant) => this.rectArea(descendant.bounds) >= WRAPPER_AREA_RATIO * parentArea);
|
|
120
|
+
return nearFullSize.length === 1;
|
|
121
|
+
}
|
|
122
|
+
isInvisible(element) {
|
|
123
|
+
const style = this.win.getComputedStyle(element);
|
|
124
|
+
if (style.visibility === 'hidden' || style.display === 'none')
|
|
125
|
+
return true;
|
|
126
|
+
return style.opacity.length > 0 && Number(style.opacity) === 0;
|
|
127
|
+
}
|
|
128
|
+
isNextDevChrome(element) {
|
|
129
|
+
return element
|
|
130
|
+
.getAttributeNames()
|
|
131
|
+
.some((attribute) => attribute.startsWith('data-nextjs-'));
|
|
132
|
+
}
|
|
133
|
+
domDepth(element) {
|
|
134
|
+
let depth = 0;
|
|
135
|
+
let current = element.parentElement;
|
|
136
|
+
while (current !== null) {
|
|
137
|
+
depth++;
|
|
138
|
+
current = current.parentElement;
|
|
139
|
+
}
|
|
140
|
+
return depth;
|
|
141
|
+
}
|
|
142
|
+
rectArea(rect) {
|
|
143
|
+
return rect.width * rect.height;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=element-filter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"element-filter.js","sourceRoot":"","sources":["../../../src/runtime/region/element-filter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Point, Viewport } from '../../protocol/element.types.js';
|
|
2
|
+
/** A successfully rasterized spray region. */
|
|
3
|
+
export interface RegionSampleOk {
|
|
4
|
+
kind: 'ok';
|
|
5
|
+
/** Viewport-clamped centers of every grid cell covered by the stroke. */
|
|
6
|
+
cellCenters: Point[];
|
|
7
|
+
/** The cell size the budget loop settled on, in CSS pixels. */
|
|
8
|
+
cellSize: number;
|
|
9
|
+
}
|
|
10
|
+
/** The stroke's padded bounding box exceeds the region budget. */
|
|
11
|
+
export interface RegionSampleTooLarge {
|
|
12
|
+
kind: 'too-large';
|
|
13
|
+
}
|
|
14
|
+
export type RegionSampleResult = RegionSampleOk | RegionSampleTooLarge;
|
|
15
|
+
/**
|
|
16
|
+
* Pure stroke-to-grid rasterizer for spray selection. The stroke is walked
|
|
17
|
+
* at a fixed step and every grid cell whose center lies within the spray
|
|
18
|
+
* radius of a step point is marked. The grid covers the stroke's padded
|
|
19
|
+
* bounding box; when the marked-cell count blows the budget the cell size is
|
|
20
|
+
* doubled and the stroke re-rasterized, trading sampling density for a
|
|
21
|
+
* bounded number of elementsFromPoint calls downstream.
|
|
22
|
+
*/
|
|
23
|
+
export declare class RegionSampler {
|
|
24
|
+
sample(stroke: readonly Point[], radius: number, viewport: Viewport): RegionSampleResult;
|
|
25
|
+
/**
|
|
26
|
+
* Resamples the stroke polyline at `step`-pixel intervals. Both endpoints
|
|
27
|
+
* of every segment are included; a single-point stroke yields that point,
|
|
28
|
+
* so a click-without-drag still samples around the click.
|
|
29
|
+
*/
|
|
30
|
+
private walkPolyline;
|
|
31
|
+
/**
|
|
32
|
+
* Marks the cells covered by the stroke. Instead of testing every grid
|
|
33
|
+
* cell against every step point, each step point only visits the cell
|
|
34
|
+
* window its radius can reach — O(steps × (radius / cellSize)²) — and the
|
|
35
|
+
* covered set is deduplicated through linear cell indices.
|
|
36
|
+
*/
|
|
37
|
+
private rasterize;
|
|
38
|
+
private cellCenter;
|
|
39
|
+
private clampIndex;
|
|
40
|
+
private isInsideViewport;
|
|
41
|
+
}
|