@shoppexio/builder-runtime 0.1.0 → 0.1.2
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/css-vars.d.ts.map +1 -1
- package/dist/css-vars.js +24 -6
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/layout.d.ts +5 -1
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +2 -0
- package/dist/preview-fixtures.d.ts +16 -0
- package/dist/preview-fixtures.d.ts.map +1 -0
- package/dist/preview-fixtures.js +40 -0
- package/dist/product-page.d.ts +13 -0
- package/dist/product-page.d.ts.map +1 -0
- package/dist/product-page.js +18 -0
- package/dist/react.d.ts +36 -224
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +606 -47
- package/dist/search-bar-settings.d.ts +33 -0
- package/dist/search-bar-settings.d.ts.map +1 -0
- package/dist/search-bar-settings.js +99 -0
- package/dist/standard-product-blocks.d.ts +48 -0
- package/dist/standard-product-blocks.d.ts.map +1 -0
- package/dist/standard-product-blocks.js +45 -0
- package/dist/standard-product-page.d.ts +69 -0
- package/dist/standard-product-page.d.ts.map +1 -0
- package/dist/standard-product-page.js +89 -0
- package/dist/storefront-google-fonts.d.ts +2 -0
- package/dist/storefront-google-fonts.d.ts.map +1 -0
- package/dist/storefront-google-fonts.js +28 -0
- package/package.json +3 -3
- package/src/builder-runtime.test.ts +57 -0
- package/src/css-vars.ts +29 -8
- package/src/index.ts +4 -0
- package/src/layout.ts +14 -1
- package/src/preview-fixtures.ts +56 -0
- package/src/product-page.test.ts +37 -0
- package/src/product-page.ts +32 -0
- package/src/react-runtime.test.tsx +215 -3
- package/src/react.tsx +769 -45
- package/src/search-bar-settings.test.ts +72 -0
- package/src/search-bar-settings.ts +176 -0
- package/src/standard-product-blocks.test.tsx +93 -0
- package/src/standard-product-blocks.tsx +121 -0
- package/src/standard-product-page.test.ts +171 -0
- package/src/standard-product-page.ts +169 -0
- package/src/storefront-google-fonts.test.ts +31 -0
- package/src/storefront-google-fonts.ts +43 -0
- package/dist/builder-runtime.test.d.ts +0 -2
- package/dist/builder-runtime.test.d.ts.map +0 -1
- package/dist/builder-runtime.test.js +0 -115
- package/dist/react-runtime.test.d.ts +0 -2
- package/dist/react-runtime.test.d.ts.map +0 -1
- package/dist/react-runtime.test.js +0 -292
package/dist/react.js
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { BuilderSettingsSchema, PreviewMessageSchema, createEmptyBuilderSettings, migrateLegacyBuilderSettings, } from '@shoppex/builder-contracts';
|
|
2
|
+
import { BuilderSettingsSchema, PreviewMessageSchema, createEmptyBuilderSettings, isTrustedPreviewParentOrigin, migrateLegacyBuilderSettings, normalizeDedicatedPageId, } from '@shoppex/builder-contracts';
|
|
3
3
|
import { createElement, createContext, useContext, useEffect, useMemo, useRef, useState, } from 'react';
|
|
4
4
|
import { getBlockSettingValue, getBuilderContentList, getBuilderContentRecord, getBuilderContentString, getBuilderContentValue, } from './content.js';
|
|
5
|
+
import { BUILDER_PREVIEW_REVIEWS } from './preview-fixtures.js';
|
|
5
6
|
import { createBuilderCss } from './css-vars.js';
|
|
7
|
+
import { builderBlock } from './attributes.js';
|
|
6
8
|
import { getPageBlocks, getPageLayout, getVisiblePageBlocks } from './layout.js';
|
|
9
|
+
import { getNavigationHeaderSettings, resolveSearchBarSettings, } from './search-bar-settings.js';
|
|
7
10
|
import { resolveStyleSlotValue } from './style-slots.js';
|
|
8
11
|
const BUILDER_SETTINGS_WINDOW_KEY = '__SHOPPEX_BUILDER_SETTINGS__';
|
|
9
12
|
const BUILDER_STATE_EVENT_NAME = 'shoppex:builder-state';
|
|
10
13
|
const BUILDER_BLOCK_SELECTOR = '[data-builder-block]';
|
|
11
14
|
const BUILDER_CONTENT_SELECTOR = '[data-builder-content]';
|
|
12
15
|
const BUILDER_SELECTED_SELECTOR = '[data-builder-selected="true"]';
|
|
16
|
+
const BUILDER_READY_HEALTH = {
|
|
17
|
+
reactMounted: true,
|
|
18
|
+
builderRuntimeProvider: true,
|
|
19
|
+
protocolVersion: 2,
|
|
20
|
+
};
|
|
13
21
|
const BuilderRuntimeContext = createContext(null);
|
|
14
22
|
const BuilderBlockContext = createContext(null);
|
|
15
23
|
export function BuilderRuntimeProvider({ settings, children }) {
|
|
@@ -27,10 +35,12 @@ export function BuilderRuntimePreviewProvider({ initialSettings, children, }) {
|
|
|
27
35
|
}, [settings.revision]);
|
|
28
36
|
useEffect(() => {
|
|
29
37
|
const currentWindow = window;
|
|
30
|
-
const parentOrigin = getPreviewParentOrigin(document.referrer);
|
|
38
|
+
const parentOrigin = getPreviewParentOrigin(window.location, document.referrer);
|
|
31
39
|
const isTrustedPreviewEmbed = isTrustedBuilderPreviewEmbed(window.location, parentOrigin);
|
|
32
40
|
const removeInspectorStyles = isTrustedPreviewEmbed ? installBuilderPreviewInspectorStyles() : () => { };
|
|
33
41
|
const removeHoverInspector = isTrustedPreviewEmbed ? installBuilderPreviewHoverInspector() : () => { };
|
|
42
|
+
let interactionMode = 'edit';
|
|
43
|
+
let removeDirectManipulation = () => { };
|
|
34
44
|
const postToParent = (event, response) => {
|
|
35
45
|
const target = event?.source && 'postMessage' in event.source ? event.source : window.parent;
|
|
36
46
|
if (!target || target === window)
|
|
@@ -38,6 +48,13 @@ export function BuilderRuntimePreviewProvider({ initialSettings, children, }) {
|
|
|
38
48
|
const targetOrigin = parentOrigin ?? event?.origin ?? '*';
|
|
39
49
|
target.postMessage(response, targetOrigin || '*');
|
|
40
50
|
};
|
|
51
|
+
const postReady = (event) => {
|
|
52
|
+
postToParent(event, {
|
|
53
|
+
type: 'READY',
|
|
54
|
+
revision: settingsRevisionRef.current,
|
|
55
|
+
health: BUILDER_READY_HEALTH,
|
|
56
|
+
});
|
|
57
|
+
};
|
|
41
58
|
const applySettings = (input) => {
|
|
42
59
|
const parsed = BuilderSettingsSchema.safeParse(input);
|
|
43
60
|
if (!parsed.success)
|
|
@@ -65,7 +82,7 @@ export function BuilderRuntimePreviewProvider({ initialSettings, children, }) {
|
|
|
65
82
|
return;
|
|
66
83
|
const message = parsed.data;
|
|
67
84
|
if (message.type === 'REQUEST_READY') {
|
|
68
|
-
|
|
85
|
+
postReady(event);
|
|
69
86
|
return;
|
|
70
87
|
}
|
|
71
88
|
if (message.type === 'APPLY_STATE') {
|
|
@@ -81,19 +98,42 @@ export function BuilderRuntimePreviewProvider({ initialSettings, children, }) {
|
|
|
81
98
|
}
|
|
82
99
|
if (message.type === 'RELOAD') {
|
|
83
100
|
postToParent(event, { type: 'APPLIED', revision: message.revision });
|
|
84
|
-
|
|
101
|
+
const previewReloadTarget = resolvePreviewReloadTarget(window.location, currentWindow.__SHOPPEX_PREVIEW_SESSION_PATH__);
|
|
102
|
+
window.setTimeout(() => {
|
|
103
|
+
if (previewReloadTarget) {
|
|
104
|
+
window.location.href = previewReloadTarget;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
window.location.reload();
|
|
108
|
+
}, 0);
|
|
85
109
|
return;
|
|
86
110
|
}
|
|
87
111
|
if (message.type === 'SELECT_ELEMENT') {
|
|
88
112
|
selectBuilderElement(message.selection.blockId);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (message.type === 'SET_INTERACTION_MODE') {
|
|
116
|
+
interactionMode = message.mode;
|
|
117
|
+
document.documentElement.setAttribute('data-builder-interaction-mode', interactionMode);
|
|
118
|
+
return;
|
|
89
119
|
}
|
|
90
120
|
};
|
|
91
121
|
const handleBuilderClick = (event) => {
|
|
92
122
|
if (!isTrustedPreviewEmbed)
|
|
93
123
|
return;
|
|
124
|
+
// In "preview" interaction mode let the storefront react to clicks
|
|
125
|
+
// naturally so the merchant can test buy-now flows or anchor links.
|
|
126
|
+
if (interactionMode === 'preview')
|
|
127
|
+
return;
|
|
94
128
|
const target = event.target;
|
|
95
129
|
if (!(target instanceof Element))
|
|
96
130
|
return;
|
|
131
|
+
// Once an element is in inline-edit mode, clicks on it should
|
|
132
|
+
// place the caret instead of re-firing block selection. The
|
|
133
|
+
// mousedown handler in installBuilderDirectManipulation has
|
|
134
|
+
// already validated this is a legitimate edit interaction.
|
|
135
|
+
if (target.closest('[data-builder-inline-edit="true"]'))
|
|
136
|
+
return;
|
|
97
137
|
const blockElement = target.closest(BUILDER_BLOCK_SELECTOR);
|
|
98
138
|
if (!blockElement)
|
|
99
139
|
return;
|
|
@@ -108,18 +148,52 @@ export function BuilderRuntimePreviewProvider({ initialSettings, children, }) {
|
|
|
108
148
|
selection,
|
|
109
149
|
});
|
|
110
150
|
};
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
151
|
+
const postPreviewError = (source, error) => {
|
|
152
|
+
if (!isTrustedPreviewEmbed)
|
|
153
|
+
return;
|
|
154
|
+
const normalized = normalizePreviewRuntimeError(error);
|
|
155
|
+
postToParent(null, {
|
|
156
|
+
type: 'PREVIEW_ERROR',
|
|
157
|
+
revision: settingsRevisionRef.current,
|
|
158
|
+
message: normalized.message,
|
|
159
|
+
...(normalized.stack ? { stack: normalized.stack } : {}),
|
|
160
|
+
source,
|
|
161
|
+
phase: 'runtime',
|
|
162
|
+
diagnostics: {
|
|
163
|
+
name: normalized.name,
|
|
164
|
+
href: window.location.href,
|
|
165
|
+
referrer: document.referrer || undefined,
|
|
166
|
+
parentOrigin: parentOrigin ?? undefined,
|
|
167
|
+
previewMode: new URLSearchParams(window.location.search).get('shoppex-preview-mode') ?? undefined,
|
|
168
|
+
userAgent: navigator.userAgent,
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
};
|
|
172
|
+
const handleRuntimeError = (event) => {
|
|
173
|
+
postPreviewError('error', event.error ?? event.message);
|
|
174
|
+
};
|
|
175
|
+
const handleUnhandledRejection = (event) => {
|
|
176
|
+
postPreviewError('unhandledrejection', event.reason);
|
|
177
|
+
};
|
|
114
178
|
window.addEventListener(BUILDER_STATE_EVENT_NAME, handleBuilderState);
|
|
115
179
|
window.addEventListener('message', handlePreviewMessage);
|
|
116
180
|
window.addEventListener('click', handleBuilderClick, true);
|
|
181
|
+
window.addEventListener('error', handleRuntimeError);
|
|
182
|
+
window.addEventListener('unhandledrejection', handleUnhandledRejection);
|
|
183
|
+
if (isTrustedPreviewEmbed) {
|
|
184
|
+
window.__SHOPPEX_PREVIEW_DISABLE_BOOTSTRAP_ERROR_BRIDGE__?.();
|
|
185
|
+
removeDirectManipulation = installBuilderDirectManipulation((message) => postToParent(null, message), () => settingsRevisionRef.current);
|
|
186
|
+
postReady(null);
|
|
187
|
+
}
|
|
117
188
|
return () => {
|
|
118
189
|
removeInspectorStyles();
|
|
119
190
|
removeHoverInspector();
|
|
191
|
+
removeDirectManipulation();
|
|
120
192
|
window.removeEventListener(BUILDER_STATE_EVENT_NAME, handleBuilderState);
|
|
121
193
|
window.removeEventListener('message', handlePreviewMessage);
|
|
122
194
|
window.removeEventListener('click', handleBuilderClick, true);
|
|
195
|
+
window.removeEventListener('error', handleRuntimeError);
|
|
196
|
+
window.removeEventListener('unhandledrejection', handleUnhandledRejection);
|
|
123
197
|
};
|
|
124
198
|
}, []);
|
|
125
199
|
return _jsx(BuilderRuntimeProvider, { settings: settings, children: children });
|
|
@@ -145,11 +219,32 @@ export function BuilderPage({ pageId, blocks, registry, context, fallback = null
|
|
|
145
219
|
return (_jsx(_Fragment, { children: pageBlocks.map((block) => {
|
|
146
220
|
const Component = registry[block.type];
|
|
147
221
|
if (!Component) {
|
|
148
|
-
return fallback;
|
|
222
|
+
return (_jsx(BuilderBlockProvider, { block: block, children: fallback ?? renderMissingBuilderBlock(pageId, block) }, block.id));
|
|
149
223
|
}
|
|
150
224
|
return (_jsx(BuilderBlockProvider, { block: block, children: _jsx(Component, { block: block, context: context }) }, block.id));
|
|
151
225
|
}) }));
|
|
152
226
|
}
|
|
227
|
+
function renderMissingBuilderBlock(pageId, block) {
|
|
228
|
+
if (!isBuilderPreviewRuntime()) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
return createElement('div', {
|
|
232
|
+
'data-page-id': pageId,
|
|
233
|
+
'data-builder-block': block.id,
|
|
234
|
+
'data-builder-block-type': block.type,
|
|
235
|
+
'data-builder-runtime-error': 'missing-block-component',
|
|
236
|
+
style: {
|
|
237
|
+
margin: '12px 0',
|
|
238
|
+
border: '1px solid #dc2626',
|
|
239
|
+
borderRadius: '8px',
|
|
240
|
+
background: '#fef2f2',
|
|
241
|
+
color: '#7f1d1d',
|
|
242
|
+
padding: '12px',
|
|
243
|
+
fontFamily: 'ui-sans-serif, system-ui, sans-serif',
|
|
244
|
+
fontSize: '13px',
|
|
245
|
+
},
|
|
246
|
+
}, `Missing Builder component for block "${block.type}".`);
|
|
247
|
+
}
|
|
153
248
|
export function useBuilderRuntime() {
|
|
154
249
|
const context = useContext(BuilderRuntimeContext);
|
|
155
250
|
if (!context) {
|
|
@@ -202,6 +297,51 @@ export function useBuilderPageBlocks(pageId) {
|
|
|
202
297
|
export function useVisibleBuilderPageBlocks(pageId) {
|
|
203
298
|
return getVisiblePageBlocks(useBuilderRuntime().settings, pageId);
|
|
204
299
|
}
|
|
300
|
+
export function useThemePageBlocks(pageId, defaultOrder) {
|
|
301
|
+
const { settings } = useBuilderRuntime();
|
|
302
|
+
const canonicalPageId = normalizeDedicatedPageId(pageId);
|
|
303
|
+
return useMemo(() => {
|
|
304
|
+
const page = settings.theme.layout[canonicalPageId];
|
|
305
|
+
if (page) {
|
|
306
|
+
return page.blocks.filter((block) => block.visible);
|
|
307
|
+
}
|
|
308
|
+
return defaultOrder.map((type) => ({ id: type, type, visible: true, settings: {} }));
|
|
309
|
+
}, [canonicalPageId, defaultOrder, settings.theme.layout]);
|
|
310
|
+
}
|
|
311
|
+
export function useThemePageBlockAttributes(pageId, defaultOrder) {
|
|
312
|
+
const canonicalPageId = normalizeDedicatedPageId(pageId);
|
|
313
|
+
const blocks = useThemePageBlocks(pageId, defaultOrder);
|
|
314
|
+
const block = blocks[0];
|
|
315
|
+
return useMemo(() => ({
|
|
316
|
+
'data-page-id': canonicalPageId,
|
|
317
|
+
...(block ? builderBlock(block.id, block.type) : {}),
|
|
318
|
+
}), [block, canonicalPageId]);
|
|
319
|
+
}
|
|
320
|
+
export function DedicatedBuilderPage({ pageId, defaultBlockOrder, as, children, ...rest }) {
|
|
321
|
+
const attrs = useThemePageBlockAttributes(pageId, defaultBlockOrder);
|
|
322
|
+
const Component = (as ?? 'section');
|
|
323
|
+
return createElement(Component, { ...attrs, ...rest }, children);
|
|
324
|
+
}
|
|
325
|
+
export function useBuilderPreviewReviews(input) {
|
|
326
|
+
const fixtures = input.fixtures ?? BUILDER_PREVIEW_REVIEWS;
|
|
327
|
+
const shouldUseFixtures = isBuilderPreviewRuntime() && Boolean(input.error);
|
|
328
|
+
return useMemo(() => {
|
|
329
|
+
if (!shouldUseFixtures) {
|
|
330
|
+
return {
|
|
331
|
+
reviews: input.reviews,
|
|
332
|
+
isLoading: input.isLoading,
|
|
333
|
+
error: input.error,
|
|
334
|
+
isUsingFixtures: false,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
reviews: fixtures,
|
|
339
|
+
isLoading: false,
|
|
340
|
+
error: null,
|
|
341
|
+
isUsingFixtures: true,
|
|
342
|
+
};
|
|
343
|
+
}, [fixtures, input.error, input.isLoading, input.reviews, shouldUseFixtures]);
|
|
344
|
+
}
|
|
205
345
|
export function useBuilderStyleSlot(slotId, input = {}) {
|
|
206
346
|
return resolveStyleSlotValue(useBuilderRuntime().settings, slotId, input);
|
|
207
347
|
}
|
|
@@ -232,6 +372,47 @@ function getNestedBuilderSetting(record, path) {
|
|
|
232
372
|
}
|
|
233
373
|
return current;
|
|
234
374
|
}
|
|
375
|
+
export function useSearchBarSettings(input) {
|
|
376
|
+
const runtime = useBuilderRuntime();
|
|
377
|
+
const headerSettings = useMemo(() => input.headerSettings ?? getNavigationHeaderSettings(runtime.settings), [input.headerSettings, runtime.settings]);
|
|
378
|
+
const heroPlaceholder = useBuilderContentValue('hero.search.placeholder');
|
|
379
|
+
const heroBackground = useBuilderContentValue('hero.search.background');
|
|
380
|
+
const heroBorderColor = useBuilderContentValue('hero.search.borderColor');
|
|
381
|
+
const heroBorderRadius = useBuilderContentValue('hero.search.borderRadius');
|
|
382
|
+
const heroMaxWidth = useBuilderContentValue('hero.search.maxWidth');
|
|
383
|
+
const heroShowShortcut = useBuilderContentValue('hero.search.showShortcut');
|
|
384
|
+
const heroShow = useBuilderContentValue('hero.search.show');
|
|
385
|
+
return useMemo(() => resolveSearchBarSettings({
|
|
386
|
+
variant: input.variant,
|
|
387
|
+
headerSettings,
|
|
388
|
+
heroValues: {
|
|
389
|
+
'hero.search.placeholder': heroPlaceholder,
|
|
390
|
+
'hero.search.background': heroBackground,
|
|
391
|
+
'hero.search.borderColor': heroBorderColor,
|
|
392
|
+
'hero.search.borderRadius': heroBorderRadius,
|
|
393
|
+
'hero.search.maxWidth': heroMaxWidth,
|
|
394
|
+
'hero.search.showShortcut': heroShowShortcut,
|
|
395
|
+
'hero.search.show': heroShow,
|
|
396
|
+
},
|
|
397
|
+
}), [
|
|
398
|
+
headerSettings,
|
|
399
|
+
heroBackground,
|
|
400
|
+
heroBorderColor,
|
|
401
|
+
heroBorderRadius,
|
|
402
|
+
heroMaxWidth,
|
|
403
|
+
heroPlaceholder,
|
|
404
|
+
heroShow,
|
|
405
|
+
heroShowShortcut,
|
|
406
|
+
input.variant,
|
|
407
|
+
]);
|
|
408
|
+
}
|
|
409
|
+
export { buildSearchShellStyle, getNavigationHeaderSettings, resolveSearchBarSettings, } from './search-bar-settings.js';
|
|
410
|
+
export function isBuilderPreviewRuntime() {
|
|
411
|
+
if (typeof window === 'undefined') {
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
return window.location.search.includes('shoppex-preview-mode=theme');
|
|
415
|
+
}
|
|
235
416
|
function parseInitialBuilderSettings(input) {
|
|
236
417
|
const parsed = BuilderSettingsSchema.safeParse(input);
|
|
237
418
|
if (parsed.success)
|
|
@@ -270,22 +451,53 @@ function parseBuilderRevision(input) {
|
|
|
270
451
|
function isRecord(value) {
|
|
271
452
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
272
453
|
}
|
|
273
|
-
function
|
|
274
|
-
if (!
|
|
454
|
+
export function resolvePreviewReloadTarget(location, sessionPath) {
|
|
455
|
+
if (typeof sessionPath !== 'string' || !sessionPath.startsWith('/s/')) {
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
return `${sessionPath}${location.search}${location.hash}`;
|
|
459
|
+
}
|
|
460
|
+
function normalizePreviewRuntimeError(error) {
|
|
461
|
+
if (error instanceof Error) {
|
|
462
|
+
return {
|
|
463
|
+
message: error.message || error.name || 'Preview runtime error',
|
|
464
|
+
name: error.name,
|
|
465
|
+
...(typeof error.stack === 'string' && error.stack ? { stack: error.stack } : {}),
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
if (typeof error === 'string') {
|
|
469
|
+
return { message: error || 'Preview runtime error' };
|
|
470
|
+
}
|
|
471
|
+
if (isRecord(error)) {
|
|
472
|
+
const message = typeof error.message === 'string' && error.message
|
|
473
|
+
? error.message
|
|
474
|
+
: 'Preview runtime error';
|
|
475
|
+
const stack = typeof error.stack === 'string' && error.stack ? error.stack : undefined;
|
|
476
|
+
const name = typeof error.name === 'string' && error.name ? error.name : undefined;
|
|
477
|
+
return { message, ...(stack ? { stack } : {}), ...(name ? { name } : {}) };
|
|
478
|
+
}
|
|
479
|
+
return { message: 'Preview runtime error' };
|
|
480
|
+
}
|
|
481
|
+
function parseOrigin(value) {
|
|
482
|
+
if (!value)
|
|
275
483
|
return null;
|
|
276
484
|
try {
|
|
277
|
-
return new URL(
|
|
485
|
+
return new URL(value).origin;
|
|
278
486
|
}
|
|
279
487
|
catch {
|
|
280
488
|
return null;
|
|
281
489
|
}
|
|
282
490
|
}
|
|
491
|
+
function getPreviewParentOrigin(location, referrer) {
|
|
492
|
+
const explicitOrigin = new URLSearchParams(location.search).get('shoppex-preview-parent-origin');
|
|
493
|
+
return parseOrigin(explicitOrigin) ?? parseOrigin(referrer);
|
|
494
|
+
}
|
|
283
495
|
function isTrustedBuilderPreviewEmbed(location, parentOrigin) {
|
|
284
496
|
if (window.parent === window || !parentOrigin)
|
|
285
497
|
return false;
|
|
286
498
|
if (!hasBuilderPreviewMode(location))
|
|
287
499
|
return false;
|
|
288
|
-
return
|
|
500
|
+
return isTrustedPreviewParentOrigin(parentOrigin);
|
|
289
501
|
}
|
|
290
502
|
function hasBuilderPreviewMode(location) {
|
|
291
503
|
const searchParams = new URLSearchParams(location.search);
|
|
@@ -293,23 +505,6 @@ function hasBuilderPreviewMode(location) {
|
|
|
293
505
|
|| searchParams.get('shoppex-preview-mode') === 'builder'
|
|
294
506
|
|| searchParams.get('shoppex-preview') === 'builder');
|
|
295
507
|
}
|
|
296
|
-
function isTrustedBuilderPreviewParentOrigin(origin) {
|
|
297
|
-
try {
|
|
298
|
-
const parsed = new URL(origin);
|
|
299
|
-
const hostname = parsed.hostname.toLowerCase();
|
|
300
|
-
return (hostname === 'dashboard.shoppex.io'
|
|
301
|
-
|| hostname === 'dashboard.shoppex.test'
|
|
302
|
-
|| hostname === 'localhost'
|
|
303
|
-
|| hostname === '127.0.0.1'
|
|
304
|
-
|| hostname === '::1'
|
|
305
|
-
|| hostname.endsWith('.localhost')
|
|
306
|
-
|| hostname.endsWith('.vercel.app')
|
|
307
|
-
|| hostname.endsWith('.vercel.run'));
|
|
308
|
-
}
|
|
309
|
-
catch {
|
|
310
|
-
return false;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
508
|
function selectBuilderElement(blockId) {
|
|
314
509
|
document
|
|
315
510
|
.querySelectorAll(BUILDER_SELECTED_SELECTOR)
|
|
@@ -320,7 +515,9 @@ function selectBuilderElement(blockId) {
|
|
|
320
515
|
if (!target)
|
|
321
516
|
return;
|
|
322
517
|
target.setAttribute('data-builder-selected', 'true');
|
|
323
|
-
target.
|
|
518
|
+
const pageId = target.getAttribute('data-page-id');
|
|
519
|
+
const scrollBlock = pageId === 'footer' ? 'end' : pageId === 'navigation' ? 'start' : 'nearest';
|
|
520
|
+
target.scrollIntoView({ behavior: 'smooth', block: scrollBlock });
|
|
324
521
|
}
|
|
325
522
|
function createBuilderSelection(blockElement, target) {
|
|
326
523
|
const contentElement = target.closest(BUILDER_CONTENT_SELECTOR);
|
|
@@ -361,48 +558,410 @@ function installBuilderPreviewInspectorStyles() {
|
|
|
361
558
|
position: relative;
|
|
362
559
|
}
|
|
363
560
|
[data-builder-block][data-builder-selected="true"] {
|
|
364
|
-
outline:
|
|
561
|
+
outline: 1px solid rgba(124, 58, 237, 0.7);
|
|
365
562
|
outline-offset: 4px;
|
|
366
563
|
}
|
|
367
564
|
[data-builder-block][data-builder-hovered="true"] {
|
|
368
|
-
outline: 1px dashed
|
|
565
|
+
outline: 1px dashed rgba(124, 58, 237, 0.5);
|
|
369
566
|
outline-offset: 4px;
|
|
370
567
|
cursor: pointer;
|
|
371
568
|
}
|
|
569
|
+
/* Block-name tooltip in the upper-left corner of the hovered block.
|
|
570
|
+
Driven by a data-builder-block-label attribute the runtime sets
|
|
571
|
+
from manifest.blocks[type].label when available, falling back to
|
|
572
|
+
the block-type slug. */
|
|
573
|
+
[data-builder-block][data-builder-hovered="true"]::before {
|
|
574
|
+
content: attr(data-builder-block-label);
|
|
575
|
+
position: absolute;
|
|
576
|
+
top: -22px;
|
|
577
|
+
left: 0;
|
|
578
|
+
padding: 2px 6px;
|
|
579
|
+
font-size: 11px;
|
|
580
|
+
font-weight: 500;
|
|
581
|
+
line-height: 1.3;
|
|
582
|
+
color: #ffffff;
|
|
583
|
+
background: #7c3aed;
|
|
584
|
+
border-radius: 4px;
|
|
585
|
+
pointer-events: none;
|
|
586
|
+
white-space: nowrap;
|
|
587
|
+
z-index: 999;
|
|
588
|
+
}
|
|
589
|
+
/* Sub-element hover: thinner outline so a hover on a text or image
|
|
590
|
+
inside a block highlights only that target instead of the whole
|
|
591
|
+
block. Suppressed while the parent block is selected so the
|
|
592
|
+
merchant doesn't see double outlines. */
|
|
593
|
+
[data-builder-content][data-builder-content-hovered="true"] {
|
|
594
|
+
outline: 1px dashed rgba(124, 58, 237, 0.5);
|
|
595
|
+
outline-offset: 2px;
|
|
596
|
+
cursor: pointer;
|
|
597
|
+
}
|
|
598
|
+
[data-builder-block][data-builder-selected="true"] [data-builder-content][data-builder-content-hovered="true"] {
|
|
599
|
+
outline: none;
|
|
600
|
+
}
|
|
601
|
+
/* Inline-edit affordance: hover over editable text content reveals a
|
|
602
|
+
text cursor + subtle brand underline so the merchant can see what
|
|
603
|
+
a double-click would target. Images, links, and buttons stay
|
|
604
|
+
outside this rule — those have their own picker affordances. */
|
|
605
|
+
[data-builder-content][data-builder-content-hovered="true"]:not(img):not(a[href]):not(:has(a[href])):not(:has(button)) {
|
|
606
|
+
text-decoration: underline;
|
|
607
|
+
text-decoration-color: rgba(124, 58, 237, 0.55);
|
|
608
|
+
text-decoration-thickness: 2px;
|
|
609
|
+
text-underline-offset: 4px;
|
|
610
|
+
cursor: text;
|
|
611
|
+
}
|
|
612
|
+
/* Active inline-edit state: replaces the dashed outline with a
|
|
613
|
+
brand-tinted ring so the merchant gets unambiguous focus
|
|
614
|
+
feedback while typing. */
|
|
615
|
+
[data-builder-content][data-builder-inline-edit="true"] {
|
|
616
|
+
outline: 2px solid #7c3aed !important;
|
|
617
|
+
outline-offset: 4px !important;
|
|
618
|
+
border-radius: 4px;
|
|
619
|
+
background: rgba(124, 58, 237, 0.06);
|
|
620
|
+
text-decoration: none !important;
|
|
621
|
+
cursor: text !important;
|
|
622
|
+
caret-color: #7c3aed;
|
|
623
|
+
}
|
|
624
|
+
[data-builder-content][data-builder-inline-edit="true"]:focus {
|
|
625
|
+
outline: 2px solid #7c3aed !important;
|
|
626
|
+
outline-offset: 4px !important;
|
|
627
|
+
}
|
|
372
628
|
`;
|
|
373
629
|
document.head.appendChild(style);
|
|
374
630
|
return () => style.remove();
|
|
375
631
|
}
|
|
376
632
|
function installBuilderPreviewHoverInspector() {
|
|
377
|
-
let
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
hovered
|
|
633
|
+
let hoveredBlock = null;
|
|
634
|
+
let hoveredContent = null;
|
|
635
|
+
const clearHoveredBlock = () => {
|
|
636
|
+
hoveredBlock?.removeAttribute('data-builder-hovered');
|
|
637
|
+
hoveredBlock = null;
|
|
638
|
+
};
|
|
639
|
+
const clearHoveredContent = () => {
|
|
640
|
+
hoveredContent?.removeAttribute('data-builder-content-hovered');
|
|
641
|
+
hoveredContent = null;
|
|
381
642
|
};
|
|
382
643
|
const handleMouseOver = (event) => {
|
|
383
644
|
const target = event.target;
|
|
384
645
|
if (!(target instanceof Element))
|
|
385
646
|
return;
|
|
386
647
|
const blockElement = target.closest(BUILDER_BLOCK_SELECTOR);
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
648
|
+
const contentElement = target.closest(BUILDER_CONTENT_SELECTOR);
|
|
649
|
+
if (blockElement !== hoveredBlock) {
|
|
650
|
+
clearHoveredBlock();
|
|
651
|
+
if (blockElement) {
|
|
652
|
+
hoveredBlock = blockElement;
|
|
653
|
+
// Drive the ::before tooltip via a label attribute. Derive
|
|
654
|
+
// it from the block-type slug — pretty manifests already use
|
|
655
|
+
// capitalised labels, but we can't read manifest from here so
|
|
656
|
+
// we humanise the slug ("hero" → "Hero").
|
|
657
|
+
if (!blockElement.hasAttribute('data-builder-block-label')) {
|
|
658
|
+
const type = blockElement.getAttribute('data-builder-block-type') ?? '';
|
|
659
|
+
// Slugs whose Title-Cased form ("Custom Html") doesn't match the
|
|
660
|
+
// manifest's display label. We can't read manifest from here so
|
|
661
|
+
// we keep a small override table for these.
|
|
662
|
+
const SLUG_LABEL_OVERRIDES = {
|
|
663
|
+
'custom-html': 'Custom Embed',
|
|
664
|
+
};
|
|
665
|
+
const override = SLUG_LABEL_OVERRIDES[type];
|
|
666
|
+
const label = override ?? type
|
|
667
|
+
.replace(/[-_]/g, ' ')
|
|
668
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
669
|
+
if (label)
|
|
670
|
+
blockElement.setAttribute('data-builder-block-label', label);
|
|
671
|
+
}
|
|
672
|
+
hoveredBlock.setAttribute('data-builder-hovered', 'true');
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
if (contentElement !== hoveredContent) {
|
|
676
|
+
clearHoveredContent();
|
|
677
|
+
if (contentElement && contentElement !== blockElement) {
|
|
678
|
+
hoveredContent = contentElement;
|
|
679
|
+
hoveredContent.setAttribute('data-builder-content-hovered', 'true');
|
|
680
|
+
}
|
|
681
|
+
}
|
|
394
682
|
};
|
|
395
683
|
const handleMouseOut = (event) => {
|
|
396
684
|
const relatedTarget = event.relatedTarget;
|
|
397
|
-
if (relatedTarget instanceof Node
|
|
398
|
-
|
|
399
|
-
|
|
685
|
+
if (relatedTarget instanceof Node) {
|
|
686
|
+
if (hoveredBlock?.contains(relatedTarget) && hoveredContent?.contains(relatedTarget))
|
|
687
|
+
return;
|
|
688
|
+
if (hoveredBlock?.contains(relatedTarget) && !hoveredContent)
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
clearHoveredBlock();
|
|
692
|
+
clearHoveredContent();
|
|
400
693
|
};
|
|
401
694
|
window.addEventListener('mouseover', handleMouseOver, true);
|
|
402
695
|
window.addEventListener('mouseout', handleMouseOut, true);
|
|
403
696
|
return () => {
|
|
404
|
-
|
|
697
|
+
clearHoveredBlock();
|
|
698
|
+
clearHoveredContent();
|
|
405
699
|
window.removeEventListener('mouseover', handleMouseOver, true);
|
|
406
700
|
window.removeEventListener('mouseout', handleMouseOut, true);
|
|
407
701
|
};
|
|
408
702
|
}
|
|
703
|
+
const INSERTER_HOVER_BAND_PX = 28;
|
|
704
|
+
const INSERTER_CLEAR_DELAY_MS = 150;
|
|
705
|
+
/**
|
|
706
|
+
* Direct-manipulation bridge: tracks the bounding rect of the selected
|
|
707
|
+
* block (for the floating toolbar), the inter-block gap currently
|
|
708
|
+
* hovered (for the "+" inserter), and inline-text-edit commits. All
|
|
709
|
+
* coordinates are reported in the iframe's viewport space; the
|
|
710
|
+
* dashboard maps them to its overlay layer.
|
|
711
|
+
*/
|
|
712
|
+
function installBuilderDirectManipulation(postMessage, getRevision) {
|
|
713
|
+
// Defensive: test environments may stub document/window without the
|
|
714
|
+
// observer APIs we rely on. In that case we silently no-op so the
|
|
715
|
+
// existing READY/APPLY handshake remains testable.
|
|
716
|
+
if (typeof MutationObserver === 'undefined' ||
|
|
717
|
+
typeof window === 'undefined' ||
|
|
718
|
+
typeof document === 'undefined') {
|
|
719
|
+
return () => { };
|
|
720
|
+
}
|
|
721
|
+
let trackedBlock = null;
|
|
722
|
+
let lastRectKey = null;
|
|
723
|
+
let lastInserterKey = null;
|
|
724
|
+
const postBlockRect = () => {
|
|
725
|
+
if (!trackedBlock || !trackedBlock.isConnected) {
|
|
726
|
+
if (trackedBlock) {
|
|
727
|
+
postMessage({
|
|
728
|
+
type: 'BLOCK_RECT',
|
|
729
|
+
revision: getRevision(),
|
|
730
|
+
blockId: trackedBlock.getAttribute('data-builder-block') ?? '',
|
|
731
|
+
rect: null,
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
trackedBlock = null;
|
|
735
|
+
lastRectKey = null;
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
const rect = trackedBlock.getBoundingClientRect();
|
|
739
|
+
const blockId = trackedBlock.getAttribute('data-builder-block') ?? '';
|
|
740
|
+
const next = `${blockId}|${rect.top}|${rect.left}|${rect.width}|${rect.height}`;
|
|
741
|
+
if (next === lastRectKey)
|
|
742
|
+
return;
|
|
743
|
+
lastRectKey = next;
|
|
744
|
+
postMessage({
|
|
745
|
+
type: 'BLOCK_RECT',
|
|
746
|
+
revision: getRevision(),
|
|
747
|
+
blockId,
|
|
748
|
+
rect: {
|
|
749
|
+
top: rect.top,
|
|
750
|
+
left: rect.left,
|
|
751
|
+
width: rect.width,
|
|
752
|
+
height: rect.height,
|
|
753
|
+
},
|
|
754
|
+
});
|
|
755
|
+
};
|
|
756
|
+
const selectionObserver = new MutationObserver(() => {
|
|
757
|
+
const selected = document.querySelector(BUILDER_SELECTED_SELECTOR);
|
|
758
|
+
if (selected instanceof HTMLElement) {
|
|
759
|
+
trackedBlock = selected;
|
|
760
|
+
postBlockRect();
|
|
761
|
+
}
|
|
762
|
+
else if (trackedBlock) {
|
|
763
|
+
// Selection cleared.
|
|
764
|
+
postMessage({
|
|
765
|
+
type: 'BLOCK_RECT',
|
|
766
|
+
revision: getRevision(),
|
|
767
|
+
blockId: trackedBlock.getAttribute('data-builder-block') ?? '',
|
|
768
|
+
rect: null,
|
|
769
|
+
});
|
|
770
|
+
trackedBlock = null;
|
|
771
|
+
lastRectKey = null;
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
selectionObserver.observe(document.body, {
|
|
775
|
+
attributes: true,
|
|
776
|
+
attributeFilter: ['data-builder-selected'],
|
|
777
|
+
subtree: true,
|
|
778
|
+
});
|
|
779
|
+
let rectFrame = 0;
|
|
780
|
+
const scheduleRectUpdate = () => {
|
|
781
|
+
if (rectFrame)
|
|
782
|
+
return;
|
|
783
|
+
rectFrame = window.requestAnimationFrame(() => {
|
|
784
|
+
rectFrame = 0;
|
|
785
|
+
postBlockRect();
|
|
786
|
+
});
|
|
787
|
+
};
|
|
788
|
+
window.addEventListener('scroll', scheduleRectUpdate, true);
|
|
789
|
+
window.addEventListener('resize', scheduleRectUpdate);
|
|
790
|
+
// Inter-block gap inserter: detect mouse near the top/bottom edge of
|
|
791
|
+
// a tracked block stack. We don't try to be clever about which gap;
|
|
792
|
+
// every direct ancestor with a list of `[data-builder-block]` children
|
|
793
|
+
// contributes one gap per pair.
|
|
794
|
+
let inserterClearTimer = null;
|
|
795
|
+
const postInserter = (index, rect) => {
|
|
796
|
+
const emitInserter = () => {
|
|
797
|
+
const key = index === null || !rect
|
|
798
|
+
? '__none__'
|
|
799
|
+
: `${index}|${rect.top}|${rect.left}|${rect.width}|${rect.height}`;
|
|
800
|
+
if (key === lastInserterKey)
|
|
801
|
+
return;
|
|
802
|
+
lastInserterKey = key;
|
|
803
|
+
postMessage({
|
|
804
|
+
type: 'INSERTER_HOVER',
|
|
805
|
+
revision: getRevision(),
|
|
806
|
+
index,
|
|
807
|
+
rect: rect === null
|
|
808
|
+
? null
|
|
809
|
+
: {
|
|
810
|
+
top: rect.top,
|
|
811
|
+
left: rect.left,
|
|
812
|
+
width: rect.width,
|
|
813
|
+
height: rect.height,
|
|
814
|
+
},
|
|
815
|
+
});
|
|
816
|
+
};
|
|
817
|
+
if (index !== null && rect !== null) {
|
|
818
|
+
if (inserterClearTimer !== null) {
|
|
819
|
+
window.clearTimeout(inserterClearTimer);
|
|
820
|
+
inserterClearTimer = null;
|
|
821
|
+
}
|
|
822
|
+
emitInserter();
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
if (inserterClearTimer !== null)
|
|
826
|
+
return;
|
|
827
|
+
inserterClearTimer = window.setTimeout(() => {
|
|
828
|
+
inserterClearTimer = null;
|
|
829
|
+
emitInserter();
|
|
830
|
+
}, INSERTER_CLEAR_DELAY_MS);
|
|
831
|
+
};
|
|
832
|
+
const handleMouseMove = (event) => {
|
|
833
|
+
const blocks = Array.from(document.querySelectorAll(BUILDER_BLOCK_SELECTOR)).filter((el) => {
|
|
834
|
+
// Only consider top-level builder blocks inside the same parent.
|
|
835
|
+
const parent = el.parentElement;
|
|
836
|
+
return parent
|
|
837
|
+
? Array.from(parent.children).some((child) => child instanceof HTMLElement &&
|
|
838
|
+
child.hasAttribute('data-builder-block'))
|
|
839
|
+
: false;
|
|
840
|
+
});
|
|
841
|
+
for (let i = 0; i < blocks.length - 1; i++) {
|
|
842
|
+
const top = blocks[i].getBoundingClientRect();
|
|
843
|
+
const bottom = blocks[i + 1].getBoundingClientRect();
|
|
844
|
+
const gapTop = top.bottom;
|
|
845
|
+
const gapBottom = bottom.top;
|
|
846
|
+
if (event.clientY >= gapTop - INSERTER_HOVER_BAND_PX / 2 &&
|
|
847
|
+
event.clientY <= gapBottom + INSERTER_HOVER_BAND_PX / 2 &&
|
|
848
|
+
event.clientX >= top.left &&
|
|
849
|
+
event.clientX <= top.right) {
|
|
850
|
+
const rect = new DOMRect(top.left, gapTop, top.width, Math.max(gapBottom - gapTop, INSERTER_HOVER_BAND_PX));
|
|
851
|
+
postInserter(i + 1, rect);
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
postInserter(null, null);
|
|
856
|
+
};
|
|
857
|
+
window.addEventListener('mousemove', handleMouseMove);
|
|
858
|
+
// Inline edit: double-click on an editable text content element flips
|
|
859
|
+
// it into contenteditable and gives the caret to the merchant.
|
|
860
|
+
//
|
|
861
|
+
// We don't bind the browser's native `dblclick` event because the
|
|
862
|
+
// dashboard's APPLY_STATE round-trip can rerender the iframe between
|
|
863
|
+
// the two clicks and the browser silently drops the dblclick.
|
|
864
|
+
// Instead we track two consecutive `mousedown` events on the same
|
|
865
|
+
// content element within a 500ms window — same behaviour as the
|
|
866
|
+
// native event but resilient to React re-mounts in between.
|
|
867
|
+
let lastDownContent = null;
|
|
868
|
+
let lastDownAt = 0;
|
|
869
|
+
const DOUBLE_DOWN_WINDOW_MS = 500;
|
|
870
|
+
const beginInlineEdit = (contentElement) => {
|
|
871
|
+
const blockElement = contentElement.closest(BUILDER_BLOCK_SELECTOR);
|
|
872
|
+
const blockId = blockElement?.getAttribute('data-builder-block');
|
|
873
|
+
const contentPath = contentElement.getAttribute('data-builder-content');
|
|
874
|
+
if (!blockId || !contentPath)
|
|
875
|
+
return false;
|
|
876
|
+
// Skip inline edit for images/links/buttons — those are picked
|
|
877
|
+
// separately in the inspector. Only run on plain text containers.
|
|
878
|
+
if (contentElement instanceof HTMLImageElement ||
|
|
879
|
+
contentElement.closest('a[href], button')) {
|
|
880
|
+
return false;
|
|
881
|
+
}
|
|
882
|
+
contentElement.setAttribute('contenteditable', 'plaintext-only');
|
|
883
|
+
contentElement.setAttribute('data-builder-inline-edit', 'true');
|
|
884
|
+
contentElement.focus();
|
|
885
|
+
const range = document.createRange();
|
|
886
|
+
range.selectNodeContents(contentElement);
|
|
887
|
+
const selection = window.getSelection();
|
|
888
|
+
selection?.removeAllRanges();
|
|
889
|
+
selection?.addRange(range);
|
|
890
|
+
const finish = (commit) => {
|
|
891
|
+
contentElement.removeAttribute('contenteditable');
|
|
892
|
+
contentElement.removeAttribute('data-builder-inline-edit');
|
|
893
|
+
contentElement.removeEventListener('blur', onBlur);
|
|
894
|
+
contentElement.removeEventListener('keydown', onKey);
|
|
895
|
+
if (commit) {
|
|
896
|
+
const value = contentElement.textContent ?? '';
|
|
897
|
+
postMessage({
|
|
898
|
+
type: 'INLINE_EDIT_COMMIT',
|
|
899
|
+
revision: getRevision(),
|
|
900
|
+
blockId,
|
|
901
|
+
contentPath,
|
|
902
|
+
value,
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
};
|
|
906
|
+
const onBlur = () => finish(true);
|
|
907
|
+
const onKey = (kev) => {
|
|
908
|
+
if (kev.key === 'Enter' && !kev.shiftKey) {
|
|
909
|
+
kev.preventDefault();
|
|
910
|
+
contentElement.blur();
|
|
911
|
+
}
|
|
912
|
+
else if (kev.key === 'Escape') {
|
|
913
|
+
kev.preventDefault();
|
|
914
|
+
finish(false);
|
|
915
|
+
contentElement.blur();
|
|
916
|
+
}
|
|
917
|
+
};
|
|
918
|
+
contentElement.addEventListener('blur', onBlur, { once: true });
|
|
919
|
+
contentElement.addEventListener('keydown', onKey);
|
|
920
|
+
return true;
|
|
921
|
+
};
|
|
922
|
+
const handleMouseDown = (event) => {
|
|
923
|
+
const target = event.target;
|
|
924
|
+
if (!(target instanceof HTMLElement)) {
|
|
925
|
+
lastDownContent = null;
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
// If we're already in an inline edit on this element, let the
|
|
929
|
+
// browser handle caret placement normally — no special handling.
|
|
930
|
+
if (target.closest('[data-builder-inline-edit="true"]'))
|
|
931
|
+
return;
|
|
932
|
+
const contentElement = target.closest(BUILDER_CONTENT_SELECTOR);
|
|
933
|
+
if (!contentElement) {
|
|
934
|
+
lastDownContent = null;
|
|
935
|
+
lastDownAt = 0;
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
const now = Date.now();
|
|
939
|
+
const isSecondDown = lastDownContent === contentElement && now - lastDownAt < DOUBLE_DOWN_WINDOW_MS;
|
|
940
|
+
if (!isSecondDown) {
|
|
941
|
+
lastDownContent = contentElement;
|
|
942
|
+
lastDownAt = now;
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
// Second mousedown on the same content element within the window —
|
|
946
|
+
// treat as the user asking to edit inline.
|
|
947
|
+
lastDownContent = null;
|
|
948
|
+
lastDownAt = 0;
|
|
949
|
+
if (!beginInlineEdit(contentElement))
|
|
950
|
+
return;
|
|
951
|
+
event.preventDefault();
|
|
952
|
+
event.stopPropagation();
|
|
953
|
+
};
|
|
954
|
+
window.addEventListener('mousedown', handleMouseDown, true);
|
|
955
|
+
return () => {
|
|
956
|
+
selectionObserver.disconnect();
|
|
957
|
+
if (rectFrame)
|
|
958
|
+
window.cancelAnimationFrame(rectFrame);
|
|
959
|
+
window.removeEventListener('scroll', scheduleRectUpdate, true);
|
|
960
|
+
window.removeEventListener('resize', scheduleRectUpdate);
|
|
961
|
+
window.removeEventListener('mousemove', handleMouseMove);
|
|
962
|
+
window.removeEventListener('mousedown', handleMouseDown, true);
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
export { getBuilderBlockSettingText, getBuilderProductBlockAttributes, getLayoutPageBlockAttributes, getProductBlockText, getProductPageBlockAttributes, } from './product-page.js';
|
|
966
|
+
export { createStandardProductBlockRegistry, splitStandardProductPageBlocks, buildStandardProductInfoTabs, resolveStandardBuyBoxLabels, resolveStandardDetailsLabels, resolveStandardRelatedProductsTitle, resolveScopedBlockSettingText, STANDARD_PRODUCT_BLOCK_TYPES, STANDARD_PRODUCT_PRIMARY_BLOCK_TYPES, STANDARD_PRODUCT_SECONDARY_BLOCK_TYPES, } from './standard-product-blocks.js';
|
|
967
|
+
export { getBuilderPreviewReviewFixtures } from './preview-fixtures.js';
|