@zalify/storefront-kit 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/editor/frame.d.ts +33 -5
- package/dist/editor/frame.js +102 -18
- package/package.json +1 -1
- package/src/editor/frame.ts +109 -18
package/dist/editor/frame.d.ts
CHANGED
|
@@ -6,11 +6,20 @@
|
|
|
6
6
|
* sync — and hands app-specific concerns (template hot-apply, device
|
|
7
7
|
* emulation) to callbacks.
|
|
8
8
|
*
|
|
9
|
-
* Editor mode
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* Editor mode keeps the page interactive — buttons, drawers, variant
|
|
10
|
+
* pickers, forms all work, so any state can be previewed — while the
|
|
11
|
+
* bridge takes over two things a click must never do on its own:
|
|
12
|
+
*
|
|
13
|
+
* - Leave the storefront. Same-origin links and GET forms are turned
|
|
14
|
+
* into `location.replace` navigations that keep the editor-mode
|
|
15
|
+
* query param (so the next document mounts the bridge again) and
|
|
16
|
+
* add no browser-history entries; off-site links, new-tab links,
|
|
17
|
+
* modifier-clicks and off-site paths (checkout, account) are blocked.
|
|
18
|
+
* - Escape selection: every click still selects the enclosing block.
|
|
19
|
+
*
|
|
20
|
+
* Double/middle clicks and context menus stay suppressed. Each mount
|
|
21
|
+
* reports its URL as a `navigation`, which is how the editor follows
|
|
22
|
+
* in-preview browsing.
|
|
14
23
|
*
|
|
15
24
|
* Security: the first `bridge:init` pins the editor origin; every
|
|
16
25
|
* later message must match it, and nothing but `bridge:ready` is ever
|
|
@@ -47,6 +56,25 @@ export interface FrameBridgeOptions {
|
|
|
47
56
|
}
|
|
48
57
|
/** True when this document should mount the editor bridge. */
|
|
49
58
|
export declare function isEditorMode(win?: Window): boolean;
|
|
59
|
+
export type NavigationDecision = {
|
|
60
|
+
kind: 'allow';
|
|
61
|
+
url: string;
|
|
62
|
+
} | {
|
|
63
|
+
kind: 'block';
|
|
64
|
+
reason: 'off-origin' | 'new-tab' | 'modifier' | 'off-site' | 'download';
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Where a click on `anchor` may take the preview. Same-origin page loads are
|
|
68
|
+
* allowed (with the editor-mode param re-attached); anything that would leave
|
|
69
|
+
* the sandbox or open another window is blocked.
|
|
70
|
+
*/
|
|
71
|
+
export declare function decideNavigation(href: string, base: string, flags: {
|
|
72
|
+
target?: string | null;
|
|
73
|
+
download?: boolean;
|
|
74
|
+
modifier?: boolean;
|
|
75
|
+
}): NavigationDecision;
|
|
76
|
+
/** The storefront path the editor should remember: no editor-mode param. */
|
|
77
|
+
export declare function previewPathOf(href: string): string;
|
|
50
78
|
export interface FrameBridgeController {
|
|
51
79
|
unmount: () => void;
|
|
52
80
|
/** Report that a persisted write round-tripped (HMR applied `hash`). */
|
package/dist/editor/frame.js
CHANGED
|
@@ -6,11 +6,20 @@
|
|
|
6
6
|
* sync — and hands app-specific concerns (template hot-apply, device
|
|
7
7
|
* emulation) to callbacks.
|
|
8
8
|
*
|
|
9
|
-
* Editor mode
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* Editor mode keeps the page interactive — buttons, drawers, variant
|
|
10
|
+
* pickers, forms all work, so any state can be previewed — while the
|
|
11
|
+
* bridge takes over two things a click must never do on its own:
|
|
12
|
+
*
|
|
13
|
+
* - Leave the storefront. Same-origin links and GET forms are turned
|
|
14
|
+
* into `location.replace` navigations that keep the editor-mode
|
|
15
|
+
* query param (so the next document mounts the bridge again) and
|
|
16
|
+
* add no browser-history entries; off-site links, new-tab links,
|
|
17
|
+
* modifier-clicks and off-site paths (checkout, account) are blocked.
|
|
18
|
+
* - Escape selection: every click still selects the enclosing block.
|
|
19
|
+
*
|
|
20
|
+
* Double/middle clicks and context menus stay suppressed. Each mount
|
|
21
|
+
* reports its URL as a `navigation`, which is how the editor follows
|
|
22
|
+
* in-preview browsing.
|
|
14
23
|
*
|
|
15
24
|
* Security: the first `bridge:init` pins the editor origin; every
|
|
16
25
|
* later message must match it, and nothing but `bridge:ready` is ever
|
|
@@ -49,10 +58,51 @@ function visibleRectOf(element) {
|
|
|
49
58
|
return { x: left, y: top, width: right - left, height: bottom - top };
|
|
50
59
|
}
|
|
51
60
|
function pathNodeOf(target) {
|
|
52
|
-
if (!(target
|
|
61
|
+
if (!isElement(target))
|
|
53
62
|
return null;
|
|
54
63
|
return target.closest(`[${DATA_PATH_ATTR}]`);
|
|
55
64
|
}
|
|
65
|
+
function isElement(target) {
|
|
66
|
+
return (typeof Element !== 'undefined' && target instanceof Element);
|
|
67
|
+
}
|
|
68
|
+
/** Storefront paths that hand off to Shopify-hosted pages; never previewable. */
|
|
69
|
+
const OFF_SITE_PATHS = [/^\/checkout(?:s)?(?:\/|$)/, /^\/cart\/c\//, /^\/account(?:\/|$)/, /^\/admin(?:\/|$)/];
|
|
70
|
+
/**
|
|
71
|
+
* Where a click on `anchor` may take the preview. Same-origin page loads are
|
|
72
|
+
* allowed (with the editor-mode param re-attached); anything that would leave
|
|
73
|
+
* the sandbox or open another window is blocked.
|
|
74
|
+
*/
|
|
75
|
+
export function decideNavigation(href, base, flags) {
|
|
76
|
+
if (flags.modifier)
|
|
77
|
+
return { kind: 'block', reason: 'modifier' };
|
|
78
|
+
if (flags.download)
|
|
79
|
+
return { kind: 'block', reason: 'download' };
|
|
80
|
+
if (flags.target && flags.target !== '_self')
|
|
81
|
+
return { kind: 'block', reason: 'new-tab' };
|
|
82
|
+
let url;
|
|
83
|
+
try {
|
|
84
|
+
url = new URL(href, base);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return { kind: 'block', reason: 'off-origin' };
|
|
88
|
+
}
|
|
89
|
+
if (url.origin !== new URL(base).origin)
|
|
90
|
+
return { kind: 'block', reason: 'off-origin' };
|
|
91
|
+
if (OFF_SITE_PATHS.some((pattern) => pattern.test(url.pathname))) {
|
|
92
|
+
return { kind: 'block', reason: 'off-site' };
|
|
93
|
+
}
|
|
94
|
+
url.searchParams.set(EDITOR_MODE_PARAM, '1');
|
|
95
|
+
return { kind: 'allow', url: url.toString() };
|
|
96
|
+
}
|
|
97
|
+
/** The storefront path the editor should remember: no editor-mode param. */
|
|
98
|
+
export function previewPathOf(href) {
|
|
99
|
+
const url = new URL(href);
|
|
100
|
+
url.searchParams.delete(EDITOR_MODE_PARAM);
|
|
101
|
+
return url.pathname + url.search;
|
|
102
|
+
}
|
|
103
|
+
function anchorOf(target) {
|
|
104
|
+
return isElement(target) ? target.closest('a[href]') : null;
|
|
105
|
+
}
|
|
56
106
|
/**
|
|
57
107
|
* Mount the frame bridge. Returns a controller with an `unmount`
|
|
58
108
|
* function. Call only when `isEditorMode()` is true.
|
|
@@ -82,6 +132,15 @@ export function mountFrameBridge(options) {
|
|
|
82
132
|
return;
|
|
83
133
|
win.parent.postMessage({ z: BRIDGE_NAMESPACE, v: CONTRACT_VERSION, ...message }, target);
|
|
84
134
|
};
|
|
135
|
+
// Every mount tells the editor where the preview is now: after an
|
|
136
|
+
// in-preview link or search the host would otherwise keep showing the
|
|
137
|
+
// page it originally asked for. Sent once the editor origin is pinned.
|
|
138
|
+
const reportNavigation = () => {
|
|
139
|
+
post({
|
|
140
|
+
type: 'navigation',
|
|
141
|
+
payload: { templateName: options.templateName, url: previewPathOf(win.location.href) },
|
|
142
|
+
});
|
|
143
|
+
};
|
|
85
144
|
const setUnique = (attr, path) => {
|
|
86
145
|
for (const node of doc.querySelectorAll(`[${attr}]`)) {
|
|
87
146
|
node.removeAttribute(attr);
|
|
@@ -171,6 +230,7 @@ export function mountFrameBridge(options) {
|
|
|
171
230
|
queueMicrotask(() => {
|
|
172
231
|
lastHeight = 0;
|
|
173
232
|
measure();
|
|
233
|
+
reportNavigation();
|
|
174
234
|
});
|
|
175
235
|
break;
|
|
176
236
|
}
|
|
@@ -244,14 +304,41 @@ export function mountFrameBridge(options) {
|
|
|
244
304
|
};
|
|
245
305
|
const handleClick = (event) => {
|
|
246
306
|
const node = pathNodeOf(event.target);
|
|
247
|
-
|
|
307
|
+
if (node) {
|
|
308
|
+
const path = node.getAttribute(DATA_PATH_ATTR);
|
|
309
|
+
setSelection(path);
|
|
310
|
+
post({ type: 'block:clicked', payload: { path, rect: rectOf(node) } });
|
|
311
|
+
}
|
|
312
|
+
// The page keeps its interactivity; only navigation is policed.
|
|
313
|
+
const anchor = anchorOf(event.target);
|
|
314
|
+
if (!anchor)
|
|
315
|
+
return;
|
|
316
|
+
const decision = decideNavigation(anchor.getAttribute('href') ?? '', win.location.href, {
|
|
317
|
+
target: anchor.getAttribute('target'),
|
|
318
|
+
download: anchor.hasAttribute('download'),
|
|
319
|
+
modifier: event.metaKey || event.ctrlKey || event.shiftKey || event.altKey,
|
|
320
|
+
});
|
|
248
321
|
event.preventDefault();
|
|
249
|
-
|
|
250
|
-
|
|
322
|
+
if (decision.kind === 'allow')
|
|
323
|
+
win.location.replace(decision.url);
|
|
324
|
+
};
|
|
325
|
+
const handleSubmit = (event) => {
|
|
326
|
+
const form = event.target;
|
|
327
|
+
if (!(form instanceof win.HTMLFormElement))
|
|
251
328
|
return;
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
329
|
+
if ((form.getAttribute('method') ?? 'get').toLowerCase() !== 'get')
|
|
330
|
+
return;
|
|
331
|
+
// A GET form is a navigation: route it like a link so the editor-mode
|
|
332
|
+
// param survives and history stays clean. POST forms run as they are.
|
|
333
|
+
const action = new URL(form.getAttribute('action') || win.location.pathname, win.location.href);
|
|
334
|
+
for (const [key, value] of new FormData(form)) {
|
|
335
|
+
if (typeof value === 'string')
|
|
336
|
+
action.searchParams.set(key, value);
|
|
337
|
+
}
|
|
338
|
+
const decision = decideNavigation(action.toString(), win.location.href, {});
|
|
339
|
+
event.preventDefault();
|
|
340
|
+
if (decision.kind === 'allow')
|
|
341
|
+
win.location.replace(decision.url);
|
|
255
342
|
};
|
|
256
343
|
const handleWheel = (event) => {
|
|
257
344
|
// Cross-origin iframe wheel events never reach the host canvas. Forward
|
|
@@ -274,12 +361,7 @@ export function mountFrameBridge(options) {
|
|
|
274
361
|
event.preventDefault();
|
|
275
362
|
event.stopPropagation();
|
|
276
363
|
};
|
|
277
|
-
const SUPPRESSED_EVENTS = [
|
|
278
|
-
'dblclick',
|
|
279
|
-
'auxclick',
|
|
280
|
-
'submit',
|
|
281
|
-
'contextmenu',
|
|
282
|
-
];
|
|
364
|
+
const SUPPRESSED_EVENTS = ['dblclick', 'auxclick', 'contextmenu'];
|
|
283
365
|
const handleMouseMove = (() => {
|
|
284
366
|
let lastPath = null;
|
|
285
367
|
return (event) => {
|
|
@@ -315,6 +397,7 @@ export function mountFrameBridge(options) {
|
|
|
315
397
|
win.addEventListener('resize', syncSelectionHighlight);
|
|
316
398
|
win.addEventListener('scroll', syncSelectionHighlight, true);
|
|
317
399
|
doc.addEventListener('click', handleClick, true);
|
|
400
|
+
doc.addEventListener('submit', handleSubmit, true);
|
|
318
401
|
doc.addEventListener('mousemove', handleMouseMove, true);
|
|
319
402
|
doc.addEventListener('wheel', handleWheel, { capture: true, passive: false });
|
|
320
403
|
for (const type of SUPPRESSED_EVENTS) {
|
|
@@ -336,6 +419,7 @@ export function mountFrameBridge(options) {
|
|
|
336
419
|
win.removeEventListener('resize', syncSelectionHighlight);
|
|
337
420
|
win.removeEventListener('scroll', syncSelectionHighlight, true);
|
|
338
421
|
doc.removeEventListener('click', handleClick, true);
|
|
422
|
+
doc.removeEventListener('submit', handleSubmit, true);
|
|
339
423
|
doc.removeEventListener('mousemove', handleMouseMove, true);
|
|
340
424
|
doc.removeEventListener('wheel', handleWheel, true);
|
|
341
425
|
for (const type of SUPPRESSED_EVENTS) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zalify/storefront-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The Zalify storefront SDK: framework-agnostic commerce logic (/commerce), the theme contract types and validators (/schemas), the canvas-editor bridge (/editor), and the React theme engine + shared components (/ui, /react/server). Consumed as TypeScript source inside the zalify-storefronts monorepo; published as compiled ESM + d.ts.",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE.md",
|
package/src/editor/frame.ts
CHANGED
|
@@ -6,11 +6,20 @@
|
|
|
6
6
|
* sync — and hands app-specific concerns (template hot-apply, device
|
|
7
7
|
* emulation) to callbacks.
|
|
8
8
|
*
|
|
9
|
-
* Editor mode
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* Editor mode keeps the page interactive — buttons, drawers, variant
|
|
10
|
+
* pickers, forms all work, so any state can be previewed — while the
|
|
11
|
+
* bridge takes over two things a click must never do on its own:
|
|
12
|
+
*
|
|
13
|
+
* - Leave the storefront. Same-origin links and GET forms are turned
|
|
14
|
+
* into `location.replace` navigations that keep the editor-mode
|
|
15
|
+
* query param (so the next document mounts the bridge again) and
|
|
16
|
+
* add no browser-history entries; off-site links, new-tab links,
|
|
17
|
+
* modifier-clicks and off-site paths (checkout, account) are blocked.
|
|
18
|
+
* - Escape selection: every click still selects the enclosing block.
|
|
19
|
+
*
|
|
20
|
+
* Double/middle clicks and context menus stay suppressed. Each mount
|
|
21
|
+
* reports its URL as a `navigation`, which is how the editor follows
|
|
22
|
+
* in-preview browsing.
|
|
14
23
|
*
|
|
15
24
|
* Security: the first `bridge:init` pins the editor origin; every
|
|
16
25
|
* later message must match it, and nothing but `bridge:ready` is ever
|
|
@@ -102,10 +111,61 @@ function visibleRectOf(element: Element): DOMRectLike | null {
|
|
|
102
111
|
}
|
|
103
112
|
|
|
104
113
|
function pathNodeOf(target: EventTarget | null): HTMLElement | null {
|
|
105
|
-
if (!(target
|
|
114
|
+
if (!isElement(target)) return null;
|
|
106
115
|
return target.closest<HTMLElement>(`[${DATA_PATH_ATTR}]`);
|
|
107
116
|
}
|
|
108
117
|
|
|
118
|
+
function isElement(target: EventTarget | null): target is Element {
|
|
119
|
+
return (
|
|
120
|
+
typeof Element !== 'undefined' && target instanceof Element
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Storefront paths that hand off to Shopify-hosted pages; never previewable. */
|
|
125
|
+
const OFF_SITE_PATHS = [/^\/checkout(?:s)?(?:\/|$)/, /^\/cart\/c\//, /^\/account(?:\/|$)/, /^\/admin(?:\/|$)/];
|
|
126
|
+
|
|
127
|
+
export type NavigationDecision =
|
|
128
|
+
| {kind: 'allow'; url: string}
|
|
129
|
+
| {kind: 'block'; reason: 'off-origin' | 'new-tab' | 'modifier' | 'off-site' | 'download'};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Where a click on `anchor` may take the preview. Same-origin page loads are
|
|
133
|
+
* allowed (with the editor-mode param re-attached); anything that would leave
|
|
134
|
+
* the sandbox or open another window is blocked.
|
|
135
|
+
*/
|
|
136
|
+
export function decideNavigation(
|
|
137
|
+
href: string,
|
|
138
|
+
base: string,
|
|
139
|
+
flags: {target?: string | null; download?: boolean; modifier?: boolean},
|
|
140
|
+
): NavigationDecision {
|
|
141
|
+
if (flags.modifier) return {kind: 'block', reason: 'modifier'};
|
|
142
|
+
if (flags.download) return {kind: 'block', reason: 'download'};
|
|
143
|
+
if (flags.target && flags.target !== '_self') return {kind: 'block', reason: 'new-tab'};
|
|
144
|
+
let url: URL;
|
|
145
|
+
try {
|
|
146
|
+
url = new URL(href, base);
|
|
147
|
+
} catch {
|
|
148
|
+
return {kind: 'block', reason: 'off-origin'};
|
|
149
|
+
}
|
|
150
|
+
if (url.origin !== new URL(base).origin) return {kind: 'block', reason: 'off-origin'};
|
|
151
|
+
if (OFF_SITE_PATHS.some((pattern) => pattern.test(url.pathname))) {
|
|
152
|
+
return {kind: 'block', reason: 'off-site'};
|
|
153
|
+
}
|
|
154
|
+
url.searchParams.set(EDITOR_MODE_PARAM, '1');
|
|
155
|
+
return {kind: 'allow', url: url.toString()};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** The storefront path the editor should remember: no editor-mode param. */
|
|
159
|
+
export function previewPathOf(href: string): string {
|
|
160
|
+
const url = new URL(href);
|
|
161
|
+
url.searchParams.delete(EDITOR_MODE_PARAM);
|
|
162
|
+
return url.pathname + url.search;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function anchorOf(target: EventTarget | null): HTMLAnchorElement | null {
|
|
166
|
+
return isElement(target) ? target.closest<HTMLAnchorElement>('a[href]') : null;
|
|
167
|
+
}
|
|
168
|
+
|
|
109
169
|
export interface FrameBridgeController {
|
|
110
170
|
unmount: () => void;
|
|
111
171
|
/** Report that a persisted write round-tripped (HMR applied `hash`). */
|
|
@@ -152,6 +212,16 @@ export function mountFrameBridge(
|
|
|
152
212
|
);
|
|
153
213
|
};
|
|
154
214
|
|
|
215
|
+
// Every mount tells the editor where the preview is now: after an
|
|
216
|
+
// in-preview link or search the host would otherwise keep showing the
|
|
217
|
+
// page it originally asked for. Sent once the editor origin is pinned.
|
|
218
|
+
const reportNavigation = (): void => {
|
|
219
|
+
post({
|
|
220
|
+
type: 'navigation',
|
|
221
|
+
payload: {templateName: options.templateName, url: previewPathOf(win.location.href)},
|
|
222
|
+
});
|
|
223
|
+
};
|
|
224
|
+
|
|
155
225
|
const setUnique = (attr: string, path: string | null): void => {
|
|
156
226
|
for (const node of doc.querySelectorAll(`[${attr}]`)) {
|
|
157
227
|
node.removeAttribute(attr);
|
|
@@ -243,6 +313,7 @@ export function mountFrameBridge(
|
|
|
243
313
|
queueMicrotask(() => {
|
|
244
314
|
lastHeight = 0;
|
|
245
315
|
measure();
|
|
316
|
+
reportNavigation();
|
|
246
317
|
});
|
|
247
318
|
break;
|
|
248
319
|
}
|
|
@@ -316,13 +387,36 @@ export function mountFrameBridge(
|
|
|
316
387
|
|
|
317
388
|
const handleClick = (event: MouseEvent): void => {
|
|
318
389
|
const node = pathNodeOf(event.target);
|
|
319
|
-
|
|
390
|
+
if (node) {
|
|
391
|
+
const path = node.getAttribute(DATA_PATH_ATTR)!;
|
|
392
|
+
setSelection(path);
|
|
393
|
+
post({type: 'block:clicked', payload: {path, rect: rectOf(node)}});
|
|
394
|
+
}
|
|
395
|
+
// The page keeps its interactivity; only navigation is policed.
|
|
396
|
+
const anchor = anchorOf(event.target);
|
|
397
|
+
if (!anchor) return;
|
|
398
|
+
const decision = decideNavigation(anchor.getAttribute('href') ?? '', win.location.href, {
|
|
399
|
+
target: anchor.getAttribute('target'),
|
|
400
|
+
download: anchor.hasAttribute('download'),
|
|
401
|
+
modifier: event.metaKey || event.ctrlKey || event.shiftKey || event.altKey,
|
|
402
|
+
});
|
|
320
403
|
event.preventDefault();
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
404
|
+
if (decision.kind === 'allow') win.location.replace(decision.url);
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
const handleSubmit = (event: Event): void => {
|
|
408
|
+
const form = event.target;
|
|
409
|
+
if (!(form instanceof win.HTMLFormElement)) return;
|
|
410
|
+
if ((form.getAttribute('method') ?? 'get').toLowerCase() !== 'get') return;
|
|
411
|
+
// A GET form is a navigation: route it like a link so the editor-mode
|
|
412
|
+
// param survives and history stays clean. POST forms run as they are.
|
|
413
|
+
const action = new URL(form.getAttribute('action') || win.location.pathname, win.location.href);
|
|
414
|
+
for (const [key, value] of new FormData(form)) {
|
|
415
|
+
if (typeof value === 'string') action.searchParams.set(key, value);
|
|
416
|
+
}
|
|
417
|
+
const decision = decideNavigation(action.toString(), win.location.href, {});
|
|
418
|
+
event.preventDefault();
|
|
419
|
+
if (decision.kind === 'allow') win.location.replace(decision.url);
|
|
326
420
|
};
|
|
327
421
|
|
|
328
422
|
const handleWheel = (event: WheelEvent): void => {
|
|
@@ -347,12 +441,7 @@ export function mountFrameBridge(
|
|
|
347
441
|
event.preventDefault();
|
|
348
442
|
event.stopPropagation();
|
|
349
443
|
};
|
|
350
|
-
const SUPPRESSED_EVENTS = [
|
|
351
|
-
'dblclick',
|
|
352
|
-
'auxclick',
|
|
353
|
-
'submit',
|
|
354
|
-
'contextmenu',
|
|
355
|
-
] as const;
|
|
444
|
+
const SUPPRESSED_EVENTS = ['dblclick', 'auxclick', 'contextmenu'] as const;
|
|
356
445
|
|
|
357
446
|
const handleMouseMove = (() => {
|
|
358
447
|
let lastPath: string | null = null;
|
|
@@ -390,6 +479,7 @@ export function mountFrameBridge(
|
|
|
390
479
|
win.addEventListener('resize', syncSelectionHighlight);
|
|
391
480
|
win.addEventListener('scroll', syncSelectionHighlight, true);
|
|
392
481
|
doc.addEventListener('click', handleClick, true);
|
|
482
|
+
doc.addEventListener('submit', handleSubmit, true);
|
|
393
483
|
doc.addEventListener('mousemove', handleMouseMove, true);
|
|
394
484
|
doc.addEventListener('wheel', handleWheel, {capture: true, passive: false});
|
|
395
485
|
for (const type of SUPPRESSED_EVENTS) {
|
|
@@ -413,6 +503,7 @@ export function mountFrameBridge(
|
|
|
413
503
|
win.removeEventListener('resize', syncSelectionHighlight);
|
|
414
504
|
win.removeEventListener('scroll', syncSelectionHighlight, true);
|
|
415
505
|
doc.removeEventListener('click', handleClick, true);
|
|
506
|
+
doc.removeEventListener('submit', handleSubmit, true);
|
|
416
507
|
doc.removeEventListener('mousemove', handleMouseMove, true);
|
|
417
508
|
doc.removeEventListener('wheel', handleWheel, true);
|
|
418
509
|
for (const type of SUPPRESSED_EVENTS) {
|