@zalify/storefront-kit 0.3.0 → 0.3.1
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 +17 -12
- package/dist/editor/frame.js +50 -13
- package/dist/schemas/bridge.d.ts +10 -0
- package/package.json +1 -1
- package/src/editor/frame.ts +53 -13
- package/src/schemas/bridge.ts +10 -0
package/dist/editor/frame.d.ts
CHANGED
|
@@ -6,20 +6,24 @@
|
|
|
6
6
|
* sync — and hands app-specific concerns (template hot-apply, device
|
|
7
7
|
* emulation) to callbacks.
|
|
8
8
|
*
|
|
9
|
-
* Editor mode
|
|
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:
|
|
9
|
+
* Editor mode has two interaction modes, switched by the host:
|
|
12
10
|
*
|
|
13
|
-
* -
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* -
|
|
11
|
+
* - `select` (default): a click selects the block under it and nothing
|
|
12
|
+
* else happens — links and buttons do not fire, so a merchant can
|
|
13
|
+
* reach for a nav link to edit it without leaving the page. Form
|
|
14
|
+
* controls (inputs, selects, labels, buttons inside a form) still
|
|
15
|
+
* work, so a search box or a sign-up form can be exercised.
|
|
16
|
+
* - `interact`: the storefront is fully live — drawers, variant
|
|
17
|
+
* pickers, add to cart — and the bridge only polices navigation:
|
|
18
|
+
* same-origin links and GET forms become `location.replace`
|
|
19
|
+
* navigations that keep the editor-mode query param (so the next
|
|
20
|
+
* document mounts the bridge again) and add no browser-history
|
|
21
|
+
* entries; off-site links, new-tab links, modifier-clicks and
|
|
22
|
+
* off-site paths (checkout, account) are blocked.
|
|
19
23
|
*
|
|
20
|
-
* Double/middle clicks and context
|
|
21
|
-
* reports its URL as a `navigation`,
|
|
22
|
-
* in-preview browsing.
|
|
24
|
+
* Every click selects in both modes. Double/middle clicks and context
|
|
25
|
+
* menus stay suppressed. Each mount reports its URL as a `navigation`,
|
|
26
|
+
* which is how the editor follows in-preview browsing.
|
|
23
27
|
*
|
|
24
28
|
* Security: the first `bridge:init` pins the editor origin; every
|
|
25
29
|
* later message must match it, and nothing but `bridge:ready` is ever
|
|
@@ -75,6 +79,7 @@ export declare function decideNavigation(href: string, base: string, flags: {
|
|
|
75
79
|
}): NavigationDecision;
|
|
76
80
|
/** The storefront path the editor should remember: no editor-mode param. */
|
|
77
81
|
export declare function previewPathOf(href: string): string;
|
|
82
|
+
export declare function isFormControl(target: EventTarget | null): boolean;
|
|
78
83
|
export interface FrameBridgeController {
|
|
79
84
|
unmount: () => void;
|
|
80
85
|
/** Report that a persisted write round-tripped (HMR applied `hash`). */
|
package/dist/editor/frame.js
CHANGED
|
@@ -6,20 +6,24 @@
|
|
|
6
6
|
* sync — and hands app-specific concerns (template hot-apply, device
|
|
7
7
|
* emulation) to callbacks.
|
|
8
8
|
*
|
|
9
|
-
* Editor mode
|
|
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:
|
|
9
|
+
* Editor mode has two interaction modes, switched by the host:
|
|
12
10
|
*
|
|
13
|
-
* -
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* -
|
|
11
|
+
* - `select` (default): a click selects the block under it and nothing
|
|
12
|
+
* else happens — links and buttons do not fire, so a merchant can
|
|
13
|
+
* reach for a nav link to edit it without leaving the page. Form
|
|
14
|
+
* controls (inputs, selects, labels, buttons inside a form) still
|
|
15
|
+
* work, so a search box or a sign-up form can be exercised.
|
|
16
|
+
* - `interact`: the storefront is fully live — drawers, variant
|
|
17
|
+
* pickers, add to cart — and the bridge only polices navigation:
|
|
18
|
+
* same-origin links and GET forms become `location.replace`
|
|
19
|
+
* navigations that keep the editor-mode query param (so the next
|
|
20
|
+
* document mounts the bridge again) and add no browser-history
|
|
21
|
+
* entries; off-site links, new-tab links, modifier-clicks and
|
|
22
|
+
* off-site paths (checkout, account) are blocked.
|
|
19
23
|
*
|
|
20
|
-
* Double/middle clicks and context
|
|
21
|
-
* reports its URL as a `navigation`,
|
|
22
|
-
* in-preview browsing.
|
|
24
|
+
* Every click selects in both modes. Double/middle clicks and context
|
|
25
|
+
* menus stay suppressed. Each mount reports its URL as a `navigation`,
|
|
26
|
+
* which is how the editor follows in-preview browsing.
|
|
23
27
|
*
|
|
24
28
|
* Security: the first `bridge:init` pins the editor origin; every
|
|
25
29
|
* later message must match it, and nothing but `bridge:ready` is ever
|
|
@@ -100,6 +104,25 @@ export function previewPathOf(href) {
|
|
|
100
104
|
url.searchParams.delete(EDITOR_MODE_PARAM);
|
|
101
105
|
return url.pathname + url.search;
|
|
102
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Controls that keep their native behaviour in `select` mode: everything a
|
|
109
|
+
* form is made of, plus standalone inputs. Links and other buttons are the
|
|
110
|
+
* things a merchant reaches for to edit, so those stay selection-only.
|
|
111
|
+
*/
|
|
112
|
+
const FORM_CONTROL_SELECTOR = [
|
|
113
|
+
'input',
|
|
114
|
+
'textarea',
|
|
115
|
+
'select',
|
|
116
|
+
'option',
|
|
117
|
+
'label',
|
|
118
|
+
'[contenteditable=""]',
|
|
119
|
+
'[contenteditable="true"]',
|
|
120
|
+
'form button',
|
|
121
|
+
'form [role="button"]',
|
|
122
|
+
].join(',');
|
|
123
|
+
export function isFormControl(target) {
|
|
124
|
+
return isElement(target) && target.closest(FORM_CONTROL_SELECTOR) !== null;
|
|
125
|
+
}
|
|
103
126
|
function anchorOf(target) {
|
|
104
127
|
return isElement(target) ? target.closest('a[href]') : null;
|
|
105
128
|
}
|
|
@@ -111,6 +134,7 @@ export function mountFrameBridge(options) {
|
|
|
111
134
|
const win = options.window ?? window;
|
|
112
135
|
const doc = win.document;
|
|
113
136
|
let editorOrigin = null;
|
|
137
|
+
let interaction = 'select';
|
|
114
138
|
let lastHeight = 0;
|
|
115
139
|
let selectedPath = null;
|
|
116
140
|
const selectionHighlight = doc.createElement('div');
|
|
@@ -223,6 +247,7 @@ export function mountFrameBridge(options) {
|
|
|
223
247
|
editorOrigin = event.origin;
|
|
224
248
|
}
|
|
225
249
|
options.onDeviceChange?.(message.payload.device);
|
|
250
|
+
interaction = message.payload.interaction ?? 'select';
|
|
226
251
|
setSelection(message.payload.selectedPath);
|
|
227
252
|
// The first measurement can run before bridge:init arrives. It is
|
|
228
253
|
// intentionally not posted until the editor origin is pinned, so
|
|
@@ -280,6 +305,9 @@ export function mountFrameBridge(options) {
|
|
|
280
305
|
case 'device:set':
|
|
281
306
|
options.onDeviceChange?.(message.payload.device);
|
|
282
307
|
break;
|
|
308
|
+
case 'interaction:set':
|
|
309
|
+
interaction = message.payload.mode;
|
|
310
|
+
break;
|
|
283
311
|
case 'manifest:request': {
|
|
284
312
|
const manifest = await options.getManifest();
|
|
285
313
|
post({ type: 'manifest:response', payload: { manifest } });
|
|
@@ -309,7 +337,16 @@ export function mountFrameBridge(options) {
|
|
|
309
337
|
setSelection(path);
|
|
310
338
|
post({ type: 'block:clicked', payload: { path, rect: rectOf(node) } });
|
|
311
339
|
}
|
|
312
|
-
|
|
340
|
+
if (interaction === 'select') {
|
|
341
|
+
// Selection only: nothing fires except form controls.
|
|
342
|
+
if (!isFormControl(event.target)) {
|
|
343
|
+
event.preventDefault();
|
|
344
|
+
event.stopPropagation();
|
|
345
|
+
}
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
// Interact mode: the page keeps its interactivity; only navigation is
|
|
349
|
+
// policed.
|
|
313
350
|
const anchor = anchorOf(event.target);
|
|
314
351
|
if (!anchor)
|
|
315
352
|
return;
|
package/dist/schemas/bridge.d.ts
CHANGED
|
@@ -16,6 +16,12 @@ export declare const DATA_PATH_ATTR = "data-z-path";
|
|
|
16
16
|
/** DOM attribute the bridge sets on the selected node. */
|
|
17
17
|
export declare const DATA_SELECTED_ATTR = "data-z-selected";
|
|
18
18
|
export type Device = 'desktop' | 'mobile';
|
|
19
|
+
/**
|
|
20
|
+
* How clicks in the preview behave. `select` (default): a click selects the
|
|
21
|
+
* block; form controls still work but links and buttons do not fire.
|
|
22
|
+
* `interact`: the storefront is fully live and the editor follows navigation.
|
|
23
|
+
*/
|
|
24
|
+
export type InteractionMode = 'select' | 'interact';
|
|
19
25
|
export type EditorCapability = 'editor-bootstrap-v1' | 'apply-template-v1' | 'apply-groups-v1' | 'apply-settings-v1' | 'preview-navigation-v1';
|
|
20
26
|
export type PreviewResourceType = 'index' | 'product' | 'collection' | 'page' | 'blog' | 'article' | 'cart' | 'search' | 'list-collections' | '404';
|
|
21
27
|
export interface PreviewContext {
|
|
@@ -79,6 +85,8 @@ export type HostMessage = BridgeEnvelope<'bridge:init', {
|
|
|
79
85
|
editorOrigin: string;
|
|
80
86
|
device: Device;
|
|
81
87
|
selectedPath: string | null;
|
|
88
|
+
/** Omitted by older hosts: behaves as `select`. */
|
|
89
|
+
interaction?: InteractionMode;
|
|
82
90
|
}> | BridgeEnvelope<'block:select', {
|
|
83
91
|
path: string | null;
|
|
84
92
|
}> | BridgeEnvelope<'block:hover', {
|
|
@@ -92,6 +100,8 @@ export type HostMessage = BridgeEnvelope<'bridge:init', {
|
|
|
92
100
|
settingsData?: SettingsData;
|
|
93
101
|
}> | BridgeEnvelope<'device:set', {
|
|
94
102
|
device: Device;
|
|
103
|
+
}> | BridgeEnvelope<'interaction:set', {
|
|
104
|
+
mode: InteractionMode;
|
|
95
105
|
}> | BridgeEnvelope<'manifest:request', Record<string, never>> | BridgeEnvelope<'editor:bootstrap:request', Record<string, never>>;
|
|
96
106
|
export type FrameMessage = BridgeEnvelope<'bridge:ready', {
|
|
97
107
|
contractVersion: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zalify/storefront-kit",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
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,20 +6,24 @@
|
|
|
6
6
|
* sync — and hands app-specific concerns (template hot-apply, device
|
|
7
7
|
* emulation) to callbacks.
|
|
8
8
|
*
|
|
9
|
-
* Editor mode
|
|
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:
|
|
9
|
+
* Editor mode has two interaction modes, switched by the host:
|
|
12
10
|
*
|
|
13
|
-
* -
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* -
|
|
11
|
+
* - `select` (default): a click selects the block under it and nothing
|
|
12
|
+
* else happens — links and buttons do not fire, so a merchant can
|
|
13
|
+
* reach for a nav link to edit it without leaving the page. Form
|
|
14
|
+
* controls (inputs, selects, labels, buttons inside a form) still
|
|
15
|
+
* work, so a search box or a sign-up form can be exercised.
|
|
16
|
+
* - `interact`: the storefront is fully live — drawers, variant
|
|
17
|
+
* pickers, add to cart — and the bridge only polices navigation:
|
|
18
|
+
* same-origin links and GET forms become `location.replace`
|
|
19
|
+
* navigations that keep the editor-mode query param (so the next
|
|
20
|
+
* document mounts the bridge again) and add no browser-history
|
|
21
|
+
* entries; off-site links, new-tab links, modifier-clicks and
|
|
22
|
+
* off-site paths (checkout, account) are blocked.
|
|
19
23
|
*
|
|
20
|
-
* Double/middle clicks and context
|
|
21
|
-
* reports its URL as a `navigation`,
|
|
22
|
-
* in-preview browsing.
|
|
24
|
+
* Every click selects in both modes. Double/middle clicks and context
|
|
25
|
+
* menus stay suppressed. Each mount reports its URL as a `navigation`,
|
|
26
|
+
* which is how the editor follows in-preview browsing.
|
|
23
27
|
*
|
|
24
28
|
* Security: the first `bridge:init` pins the editor origin; every
|
|
25
29
|
* later message must match it, and nothing but `bridge:ready` is ever
|
|
@@ -37,6 +41,7 @@ import {
|
|
|
37
41
|
type DOMRectLike,
|
|
38
42
|
type EditorBootstrap,
|
|
39
43
|
type EditorCapability,
|
|
44
|
+
type InteractionMode,
|
|
40
45
|
type FrameMessage,
|
|
41
46
|
type HostMessage,
|
|
42
47
|
type SectionGroupData,
|
|
@@ -162,6 +167,27 @@ export function previewPathOf(href: string): string {
|
|
|
162
167
|
return url.pathname + url.search;
|
|
163
168
|
}
|
|
164
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Controls that keep their native behaviour in `select` mode: everything a
|
|
172
|
+
* form is made of, plus standalone inputs. Links and other buttons are the
|
|
173
|
+
* things a merchant reaches for to edit, so those stay selection-only.
|
|
174
|
+
*/
|
|
175
|
+
const FORM_CONTROL_SELECTOR = [
|
|
176
|
+
'input',
|
|
177
|
+
'textarea',
|
|
178
|
+
'select',
|
|
179
|
+
'option',
|
|
180
|
+
'label',
|
|
181
|
+
'[contenteditable=""]',
|
|
182
|
+
'[contenteditable="true"]',
|
|
183
|
+
'form button',
|
|
184
|
+
'form [role="button"]',
|
|
185
|
+
].join(',');
|
|
186
|
+
|
|
187
|
+
export function isFormControl(target: EventTarget | null): boolean {
|
|
188
|
+
return isElement(target) && target.closest(FORM_CONTROL_SELECTOR) !== null;
|
|
189
|
+
}
|
|
190
|
+
|
|
165
191
|
function anchorOf(target: EventTarget | null): HTMLAnchorElement | null {
|
|
166
192
|
return isElement(target) ? target.closest<HTMLAnchorElement>('a[href]') : null;
|
|
167
193
|
}
|
|
@@ -186,6 +212,7 @@ export function mountFrameBridge(
|
|
|
186
212
|
const win = options.window ?? window;
|
|
187
213
|
const doc = win.document;
|
|
188
214
|
let editorOrigin: string | null = null;
|
|
215
|
+
let interaction: InteractionMode = 'select';
|
|
189
216
|
let lastHeight = 0;
|
|
190
217
|
let selectedPath: string | null = null;
|
|
191
218
|
|
|
@@ -306,6 +333,7 @@ export function mountFrameBridge(
|
|
|
306
333
|
editorOrigin = event.origin;
|
|
307
334
|
}
|
|
308
335
|
options.onDeviceChange?.(message.payload.device);
|
|
336
|
+
interaction = message.payload.interaction ?? 'select';
|
|
309
337
|
setSelection(message.payload.selectedPath);
|
|
310
338
|
// The first measurement can run before bridge:init arrives. It is
|
|
311
339
|
// intentionally not posted until the editor origin is pinned, so
|
|
@@ -362,6 +390,9 @@ export function mountFrameBridge(
|
|
|
362
390
|
case 'device:set':
|
|
363
391
|
options.onDeviceChange?.(message.payload.device);
|
|
364
392
|
break;
|
|
393
|
+
case 'interaction:set':
|
|
394
|
+
interaction = message.payload.mode;
|
|
395
|
+
break;
|
|
365
396
|
case 'manifest:request': {
|
|
366
397
|
const manifest = await options.getManifest();
|
|
367
398
|
post({type: 'manifest:response', payload: {manifest}});
|
|
@@ -392,7 +423,16 @@ export function mountFrameBridge(
|
|
|
392
423
|
setSelection(path);
|
|
393
424
|
post({type: 'block:clicked', payload: {path, rect: rectOf(node)}});
|
|
394
425
|
}
|
|
395
|
-
|
|
426
|
+
if (interaction === 'select') {
|
|
427
|
+
// Selection only: nothing fires except form controls.
|
|
428
|
+
if (!isFormControl(event.target)) {
|
|
429
|
+
event.preventDefault();
|
|
430
|
+
event.stopPropagation();
|
|
431
|
+
}
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
// Interact mode: the page keeps its interactivity; only navigation is
|
|
435
|
+
// policed.
|
|
396
436
|
const anchor = anchorOf(event.target);
|
|
397
437
|
if (!anchor) return;
|
|
398
438
|
const decision = decideNavigation(anchor.getAttribute('href') ?? '', win.location.href, {
|
package/src/schemas/bridge.ts
CHANGED
|
@@ -25,6 +25,13 @@ export const DATA_SELECTED_ATTR = 'data-z-selected';
|
|
|
25
25
|
|
|
26
26
|
export type Device = 'desktop' | 'mobile';
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* How clicks in the preview behave. `select` (default): a click selects the
|
|
30
|
+
* block; form controls still work but links and buttons do not fire.
|
|
31
|
+
* `interact`: the storefront is fully live and the editor follows navigation.
|
|
32
|
+
*/
|
|
33
|
+
export type InteractionMode = 'select' | 'interact';
|
|
34
|
+
|
|
28
35
|
export type EditorCapability =
|
|
29
36
|
| 'editor-bootstrap-v1'
|
|
30
37
|
| 'apply-template-v1'
|
|
@@ -139,6 +146,8 @@ export type HostMessage =
|
|
|
139
146
|
editorOrigin: string;
|
|
140
147
|
device: Device;
|
|
141
148
|
selectedPath: string | null;
|
|
149
|
+
/** Omitted by older hosts: behaves as `select`. */
|
|
150
|
+
interaction?: InteractionMode;
|
|
142
151
|
}
|
|
143
152
|
>
|
|
144
153
|
| BridgeEnvelope<'block:select', {path: string | null}>
|
|
@@ -154,6 +163,7 @@ export type HostMessage =
|
|
|
154
163
|
}
|
|
155
164
|
>
|
|
156
165
|
| BridgeEnvelope<'device:set', {device: Device}>
|
|
166
|
+
| BridgeEnvelope<'interaction:set', {mode: InteractionMode}>
|
|
157
167
|
| BridgeEnvelope<'manifest:request', Record<string, never>>
|
|
158
168
|
| BridgeEnvelope<'editor:bootstrap:request', Record<string, never>>;
|
|
159
169
|
|