@shoppexio/builder-runtime 0.1.2 → 0.1.3
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/YouTubeEmbed.d.ts +13 -0
- package/dist/YouTubeEmbed.d.ts.map +1 -0
- package/dist/YouTubeEmbed.js +49 -0
- package/dist/YouTubeEmbedBuilderBlock.d.ts +7 -0
- package/dist/YouTubeEmbedBuilderBlock.d.ts.map +1 -0
- package/dist/YouTubeEmbedBuilderBlock.js +16 -0
- package/dist/block-style-settings.d.ts +5 -0
- package/dist/block-style-settings.d.ts.map +1 -0
- package/dist/block-style-settings.js +16 -0
- package/dist/builder-runtime.test.d.ts +2 -0
- package/dist/builder-runtime.test.d.ts.map +1 -0
- package/dist/builder-runtime.test.js +115 -0
- package/dist/content.d.ts +6 -0
- package/dist/content.d.ts.map +1 -1
- package/dist/content.js +31 -7
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/manifest-setting-paths.d.ts +5 -0
- package/dist/manifest-setting-paths.d.ts.map +1 -0
- package/dist/manifest-setting-paths.js +40 -0
- package/dist/merchant-custom-page.d.ts +57 -0
- package/dist/merchant-custom-page.d.ts.map +1 -0
- package/dist/merchant-custom-page.js +63 -0
- package/dist/preview-mode.d.ts +2 -0
- package/dist/preview-mode.d.ts.map +1 -0
- package/dist/preview-mode.js +7 -0
- package/dist/react-runtime.test.d.ts +2 -0
- package/dist/react-runtime.test.d.ts.map +1 -0
- package/dist/react-runtime.test.js +332 -0
- package/dist/react.d.ts +6 -0
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +16 -4
- package/dist/youtube-embed-block.d.ts +10 -0
- package/dist/youtube-embed-block.d.ts.map +1 -0
- package/dist/youtube-embed-block.js +19 -0
- package/dist/youtube.d.ts +5 -0
- package/dist/youtube.d.ts.map +1 -0
- package/dist/youtube.js +52 -0
- package/package.json +1 -1
- package/src/YouTubeEmbed.tsx +105 -0
- package/src/YouTubeEmbedBuilderBlock.tsx +49 -0
- package/src/block-style-settings.ts +24 -0
- package/src/builder-runtime.test.ts +36 -0
- package/src/content.ts +44 -9
- package/src/index.ts +4 -0
- package/src/manifest-setting-paths.test.ts +23 -0
- package/src/manifest-setting-paths.ts +55 -0
- package/src/merchant-custom-page.tsx +161 -0
- package/src/preview-mode.ts +8 -0
- package/src/react.tsx +29 -4
- package/src/youtube-embed-block.test.ts +76 -0
- package/src/youtube-embed-block.ts +28 -0
- package/src/youtube-embed-builder-block.test.tsx +166 -0
- package/src/youtube.test.ts +48 -0
- package/src/youtube.ts +66 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
3
|
+
import { createEmptyBuilderSettings } from '@shoppex/builder-contracts';
|
|
4
|
+
import { JSDOM } from 'jsdom';
|
|
5
|
+
import { act } from 'react';
|
|
6
|
+
import { createRoot } from 'react-dom/client';
|
|
7
|
+
import { BuilderBlockFrame, BuilderBlockProvider, BuilderPage, BuilderRuntimePreviewProvider, useBuilderContent, useBuilderContentRecord, } from './react.js';
|
|
8
|
+
function createSettings(revision, title) {
|
|
9
|
+
return {
|
|
10
|
+
...createEmptyBuilderSettings(revision),
|
|
11
|
+
theme: {
|
|
12
|
+
content: {
|
|
13
|
+
'hero.title': title,
|
|
14
|
+
},
|
|
15
|
+
layout: {
|
|
16
|
+
home: {
|
|
17
|
+
blocks: [
|
|
18
|
+
{
|
|
19
|
+
id: 'hero-1',
|
|
20
|
+
type: 'hero',
|
|
21
|
+
visible: true,
|
|
22
|
+
settings: {},
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
style_slots: {},
|
|
28
|
+
pages: [],
|
|
29
|
+
terms: {},
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function Probe() {
|
|
34
|
+
const title = useBuilderContent('hero.title', '');
|
|
35
|
+
return (_jsx("section", { "data-builder-block": "hero-1", "data-builder-block-type": "hero", "data-page-id": "home", children: _jsx("h1", { "data-builder-content": "hero.title", children: title }) }));
|
|
36
|
+
}
|
|
37
|
+
function FallbackProbe() {
|
|
38
|
+
const title = useBuilderContent('hero.title', 'Default title');
|
|
39
|
+
return _jsx("h1", { "data-builder-content": "hero.title", children: title });
|
|
40
|
+
}
|
|
41
|
+
function SlotButtonProbe() {
|
|
42
|
+
return (_jsx("section", { "data-builder-block": "hero-1", "data-builder-block-type": "hero", "data-page-id": "home", children: _jsx("button", { type: "button", "data-builder-slot": "button.background", children: "Buy now" }) }));
|
|
43
|
+
}
|
|
44
|
+
function ScopedProbe() {
|
|
45
|
+
return (_jsx(BuilderBlockProvider, { block: {
|
|
46
|
+
id: 'hero-1',
|
|
47
|
+
type: 'hero',
|
|
48
|
+
visible: true,
|
|
49
|
+
settings: {
|
|
50
|
+
title: 'Scoped title',
|
|
51
|
+
subtitle: 'Scoped subtitle',
|
|
52
|
+
},
|
|
53
|
+
}, children: _jsx(ScopedProbeContent, {}) }));
|
|
54
|
+
}
|
|
55
|
+
function ScopedProbeContent() {
|
|
56
|
+
const title = useBuilderContent('hero.title', '');
|
|
57
|
+
const subtitle = useBuilderContent('hero.subtitle', '');
|
|
58
|
+
const contentRecord = useBuilderContentRecord();
|
|
59
|
+
return (_jsxs(_Fragment, { children: [_jsx("h1", { "data-testid": "scoped-title", children: title }), _jsx("p", { "data-testid": "scoped-subtitle", children: subtitle }), _jsx("span", { "data-testid": "scoped-record", children: String(contentRecord.hero.title) })] }));
|
|
60
|
+
}
|
|
61
|
+
function HeroBlock({ block }) {
|
|
62
|
+
const title = useBuilderContent('hero.title', '');
|
|
63
|
+
return (_jsx(BuilderBlockFrame, { as: "section", pageId: "home", block: block, children: _jsx("h1", { "data-builder-content": "hero.title", children: title }) }));
|
|
64
|
+
}
|
|
65
|
+
describe('BuilderRuntimePreviewProvider', () => {
|
|
66
|
+
let dom;
|
|
67
|
+
let root;
|
|
68
|
+
let postedMessages;
|
|
69
|
+
let parentWindow;
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
postedMessages = [];
|
|
72
|
+
parentWindow = {
|
|
73
|
+
postMessage: (message) => {
|
|
74
|
+
postedMessages.push(message);
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
dom = new JSDOM('<!doctype html><html><body><div id="root"></div></body></html>', {
|
|
78
|
+
url: 'https://preview.shoppex.test/?shoppex-preview-mode=theme',
|
|
79
|
+
referrer: 'https://dashboard.shoppex.test/theme/editor',
|
|
80
|
+
});
|
|
81
|
+
Object.defineProperty(dom.window, 'parent', {
|
|
82
|
+
configurable: true,
|
|
83
|
+
value: parentWindow,
|
|
84
|
+
});
|
|
85
|
+
Object.assign(globalThis, {
|
|
86
|
+
window: dom.window,
|
|
87
|
+
document: dom.window.document,
|
|
88
|
+
navigator: dom.window.navigator,
|
|
89
|
+
Element: dom.window.Element,
|
|
90
|
+
HTMLElement: dom.window.HTMLElement,
|
|
91
|
+
HTMLImageElement: dom.window.HTMLImageElement,
|
|
92
|
+
Node: dom.window.Node,
|
|
93
|
+
MouseEvent: dom.window.MouseEvent,
|
|
94
|
+
MessageEvent: dom.window.MessageEvent,
|
|
95
|
+
CustomEvent: dom.window.CustomEvent,
|
|
96
|
+
CSS: {
|
|
97
|
+
escape: (value) => value.replaceAll('"', '\\"'),
|
|
98
|
+
},
|
|
99
|
+
IS_REACT_ACT_ENVIRONMENT: true,
|
|
100
|
+
});
|
|
101
|
+
root = createRoot(dom.window.document.getElementById('root'));
|
|
102
|
+
});
|
|
103
|
+
afterEach(async () => {
|
|
104
|
+
await act(async () => {
|
|
105
|
+
root.unmount();
|
|
106
|
+
});
|
|
107
|
+
dom.window.close();
|
|
108
|
+
const globals = globalThis;
|
|
109
|
+
for (const key of [
|
|
110
|
+
'window',
|
|
111
|
+
'document',
|
|
112
|
+
'navigator',
|
|
113
|
+
'Element',
|
|
114
|
+
'HTMLElement',
|
|
115
|
+
'HTMLImageElement',
|
|
116
|
+
'Node',
|
|
117
|
+
'MouseEvent',
|
|
118
|
+
'MessageEvent',
|
|
119
|
+
'CustomEvent',
|
|
120
|
+
'CSS',
|
|
121
|
+
'IS_REACT_ACT_ENVIRONMENT',
|
|
122
|
+
]) {
|
|
123
|
+
delete globals[key];
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
test('sends READY with the initial revision', async () => {
|
|
127
|
+
await act(async () => {
|
|
128
|
+
root.render(_jsx(BuilderRuntimePreviewProvider, { initialSettings: createSettings(3, 'Initial title'), children: _jsx(Probe, {}) }));
|
|
129
|
+
});
|
|
130
|
+
expect(postedMessages).toContainEqual({
|
|
131
|
+
type: 'READY',
|
|
132
|
+
revision: 3,
|
|
133
|
+
health: {
|
|
134
|
+
reactMounted: true,
|
|
135
|
+
builderRuntimeProvider: true,
|
|
136
|
+
protocolVersion: 2,
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
expect(dom.window.document.body.textContent).toContain('Initial title');
|
|
140
|
+
});
|
|
141
|
+
test('normalizes mixed initial builder settings before strict validation', async () => {
|
|
142
|
+
await act(async () => {
|
|
143
|
+
root.render(_jsx(BuilderRuntimePreviewProvider, { initialSettings: {
|
|
144
|
+
version: 2,
|
|
145
|
+
revision: 9,
|
|
146
|
+
theme: {
|
|
147
|
+
content: {
|
|
148
|
+
'hero.title': 'Mixed title',
|
|
149
|
+
},
|
|
150
|
+
layout: {
|
|
151
|
+
home: {
|
|
152
|
+
sections: [
|
|
153
|
+
{
|
|
154
|
+
id: 'hero',
|
|
155
|
+
visible: true,
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
tokens_override: {
|
|
161
|
+
colors: {
|
|
162
|
+
primary: '#111827',
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
}, children: _jsx(Probe, {}) }));
|
|
167
|
+
});
|
|
168
|
+
expect(postedMessages).toContainEqual({
|
|
169
|
+
type: 'READY',
|
|
170
|
+
revision: 9,
|
|
171
|
+
health: {
|
|
172
|
+
reactMounted: true,
|
|
173
|
+
builderRuntimeProvider: true,
|
|
174
|
+
protocolVersion: 2,
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
expect(dom.window.document.body.textContent).toContain('Mixed title');
|
|
178
|
+
});
|
|
179
|
+
test('preserves intentionally empty content strings in hooks', async () => {
|
|
180
|
+
await act(async () => {
|
|
181
|
+
root.render(_jsx(BuilderRuntimePreviewProvider, { initialSettings: createSettings(4, ''), children: _jsx(FallbackProbe, {}) }));
|
|
182
|
+
});
|
|
183
|
+
expect(dom.window.document.body.textContent).toBe('');
|
|
184
|
+
});
|
|
185
|
+
test('responds to explicit READY requests when the parent missed the initial message', async () => {
|
|
186
|
+
await act(async () => {
|
|
187
|
+
root.render(_jsx(BuilderRuntimePreviewProvider, { initialSettings: createSettings(3, 'Initial title'), children: _jsx(Probe, {}) }));
|
|
188
|
+
});
|
|
189
|
+
postedMessages.length = 0;
|
|
190
|
+
await act(async () => {
|
|
191
|
+
dom.window.dispatchEvent(new dom.window.MessageEvent('message', {
|
|
192
|
+
origin: 'https://dashboard.shoppex.test',
|
|
193
|
+
source: parentWindow,
|
|
194
|
+
data: {
|
|
195
|
+
type: 'REQUEST_READY',
|
|
196
|
+
},
|
|
197
|
+
}));
|
|
198
|
+
});
|
|
199
|
+
expect(postedMessages).toEqual([{
|
|
200
|
+
type: 'READY',
|
|
201
|
+
revision: 3,
|
|
202
|
+
health: {
|
|
203
|
+
reactMounted: true,
|
|
204
|
+
builderRuntimeProvider: true,
|
|
205
|
+
protocolVersion: 2,
|
|
206
|
+
},
|
|
207
|
+
}]);
|
|
208
|
+
});
|
|
209
|
+
test('reports iframe runtime errors to the builder parent', async () => {
|
|
210
|
+
await act(async () => {
|
|
211
|
+
root.render(_jsx(BuilderRuntimePreviewProvider, { initialSettings: createSettings(6, 'Initial title'), children: _jsx(Probe, {}) }));
|
|
212
|
+
});
|
|
213
|
+
postedMessages.length = 0;
|
|
214
|
+
dom.window.dispatchEvent(new dom.window.ErrorEvent('error', {
|
|
215
|
+
error: new Error('Preview crashed'),
|
|
216
|
+
message: 'Preview crashed',
|
|
217
|
+
}));
|
|
218
|
+
expect(postedMessages).toEqual([expect.objectContaining({
|
|
219
|
+
type: 'PREVIEW_ERROR',
|
|
220
|
+
revision: 6,
|
|
221
|
+
message: 'Preview crashed',
|
|
222
|
+
source: 'error',
|
|
223
|
+
})]);
|
|
224
|
+
});
|
|
225
|
+
test('applies preview state and acknowledges the exact revision', async () => {
|
|
226
|
+
await act(async () => {
|
|
227
|
+
root.render(_jsx(BuilderRuntimePreviewProvider, { initialSettings: createSettings(3, 'Initial title'), children: _jsx(Probe, {}) }));
|
|
228
|
+
});
|
|
229
|
+
await act(async () => {
|
|
230
|
+
dom.window.dispatchEvent(new dom.window.MessageEvent('message', {
|
|
231
|
+
origin: 'https://dashboard.shoppex.test',
|
|
232
|
+
source: parentWindow,
|
|
233
|
+
data: {
|
|
234
|
+
type: 'APPLY_STATE',
|
|
235
|
+
revision: 4,
|
|
236
|
+
state: createSettings(4, 'Updated title'),
|
|
237
|
+
},
|
|
238
|
+
}));
|
|
239
|
+
});
|
|
240
|
+
expect(postedMessages).toContainEqual({ type: 'APPLIED', revision: 4 });
|
|
241
|
+
expect(dom.window.document.body.textContent).toContain('Updated title');
|
|
242
|
+
});
|
|
243
|
+
test('renders page blocks through a typed registry and shared block frame', async () => {
|
|
244
|
+
await act(async () => {
|
|
245
|
+
root.render(_jsx(BuilderRuntimePreviewProvider, { initialSettings: createSettings(3, 'Registry title'), children: _jsx(BuilderPage, { pageId: "home", registry: {
|
|
246
|
+
hero: HeroBlock,
|
|
247
|
+
}, context: {} }) }));
|
|
248
|
+
});
|
|
249
|
+
const renderedBlock = dom.window.document.querySelector('[data-builder-block="hero-1"]');
|
|
250
|
+
expect(renderedBlock?.getAttribute('data-page-id')).toBe('home');
|
|
251
|
+
expect(renderedBlock?.getAttribute('data-builder-block-type')).toBe('hero');
|
|
252
|
+
expect(dom.window.document.body.textContent).toContain('Registry title');
|
|
253
|
+
});
|
|
254
|
+
test('prefers scoped block settings over global builder content inside a block provider', async () => {
|
|
255
|
+
await act(async () => {
|
|
256
|
+
root.render(_jsx(BuilderRuntimePreviewProvider, { initialSettings: createSettings(3, 'Global title'), children: _jsx(ScopedProbe, {}) }));
|
|
257
|
+
});
|
|
258
|
+
expect(dom.window.document.querySelector('[data-testid="scoped-title"]')?.textContent).toBe('Scoped title');
|
|
259
|
+
expect(dom.window.document.querySelector('[data-testid="scoped-subtitle"]')?.textContent).toBe('Scoped subtitle');
|
|
260
|
+
expect(dom.window.document.querySelector('[data-testid="scoped-record"]')?.textContent).toBe('Scoped title');
|
|
261
|
+
});
|
|
262
|
+
test('rejects stale preview revisions without changing rendered content', async () => {
|
|
263
|
+
await act(async () => {
|
|
264
|
+
root.render(_jsx(BuilderRuntimePreviewProvider, { initialSettings: createSettings(5, 'Current title'), children: _jsx(Probe, {}) }));
|
|
265
|
+
});
|
|
266
|
+
await act(async () => {
|
|
267
|
+
dom.window.dispatchEvent(new dom.window.MessageEvent('message', {
|
|
268
|
+
origin: 'https://dashboard.shoppex.test',
|
|
269
|
+
source: parentWindow,
|
|
270
|
+
data: {
|
|
271
|
+
type: 'APPLY_STATE',
|
|
272
|
+
revision: 4,
|
|
273
|
+
state: createSettings(4, 'Stale title'),
|
|
274
|
+
},
|
|
275
|
+
}));
|
|
276
|
+
});
|
|
277
|
+
expect(postedMessages).toContainEqual({
|
|
278
|
+
type: 'APPLY_FAILED',
|
|
279
|
+
revision: 4,
|
|
280
|
+
error: 'Stale builder revision',
|
|
281
|
+
});
|
|
282
|
+
expect(dom.window.document.body.textContent).toContain('Current title');
|
|
283
|
+
});
|
|
284
|
+
test('emits block selection details from iframe clicks', async () => {
|
|
285
|
+
await act(async () => {
|
|
286
|
+
root.render(_jsx(BuilderRuntimePreviewProvider, { initialSettings: createSettings(7, 'Clickable title'), children: _jsx(Probe, {}) }));
|
|
287
|
+
});
|
|
288
|
+
const heading = dom.window.document.querySelector('h1');
|
|
289
|
+
heading?.dispatchEvent(new dom.window.MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
290
|
+
expect(postedMessages).toContainEqual({
|
|
291
|
+
type: 'ELEMENT_CLICKED',
|
|
292
|
+
revision: 7,
|
|
293
|
+
selection: {
|
|
294
|
+
pageId: 'home',
|
|
295
|
+
blockId: 'hero-1',
|
|
296
|
+
blockType: 'hero',
|
|
297
|
+
contentPath: 'hero.title',
|
|
298
|
+
elementType: 'text',
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
test('classifies buttons before style slots for inspector clicks', async () => {
|
|
303
|
+
await act(async () => {
|
|
304
|
+
root.render(_jsx(BuilderRuntimePreviewProvider, { initialSettings: createSettings(7, 'Clickable title'), children: _jsx(SlotButtonProbe, {}) }));
|
|
305
|
+
});
|
|
306
|
+
const button = dom.window.document.querySelector('button');
|
|
307
|
+
button?.dispatchEvent(new dom.window.MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
308
|
+
expect(postedMessages).toContainEqual({
|
|
309
|
+
type: 'ELEMENT_CLICKED',
|
|
310
|
+
revision: 7,
|
|
311
|
+
selection: {
|
|
312
|
+
pageId: 'home',
|
|
313
|
+
blockId: 'hero-1',
|
|
314
|
+
blockType: 'hero',
|
|
315
|
+
slotId: 'button.background',
|
|
316
|
+
elementType: 'button',
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
test('does not intercept clicks outside trusted builder preview embeds', async () => {
|
|
321
|
+
dom.reconfigure({
|
|
322
|
+
url: 'https://preview.shoppex.test/',
|
|
323
|
+
});
|
|
324
|
+
await act(async () => {
|
|
325
|
+
root.render(_jsx(BuilderRuntimePreviewProvider, { initialSettings: createSettings(7, 'Clickable title'), children: _jsx(Probe, {}) }));
|
|
326
|
+
});
|
|
327
|
+
postedMessages.length = 0;
|
|
328
|
+
const heading = dom.window.document.querySelector('h1');
|
|
329
|
+
heading?.dispatchEvent(new dom.window.MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
330
|
+
expect(postedMessages).toEqual([]);
|
|
331
|
+
});
|
|
332
|
+
});
|
package/dist/react.d.ts
CHANGED
|
@@ -110,5 +110,11 @@ export declare function resolvePreviewReloadTarget(location: Pick<Location, 'sea
|
|
|
110
110
|
export { getBuilderBlockSettingText, getBuilderProductBlockAttributes, getLayoutPageBlockAttributes, getProductBlockText, getProductPageBlockAttributes, } from './product-page.js';
|
|
111
111
|
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';
|
|
112
112
|
export type { StandardBuyBoxLabels, StandardDetailsLabels, StandardProductBlockRegistryData, StandardProductBlockRegistryOptions, StandardProductBlockRegistrySlots, StandardProductBlockType, StandardProductSettingScope, StandardProductTabSpec, } from './standard-product-blocks.js';
|
|
113
|
+
export { YouTubeEmbed, YouTubeEmbedPreviewPlaceholder, type YouTubeEmbedProps } from './YouTubeEmbed.js';
|
|
114
|
+
export { YouTubeEmbedBuilderBlock, type YouTubeEmbedBuilderBlockProps, } from './YouTubeEmbedBuilderBlock.js';
|
|
115
|
+
export { createMerchantCustomPageRegistry, MerchantCustomPageBuilderView, useMerchantCustomPageRegistry, useMerchantCustomPageView, type MerchantCustomPageRegistryOptions, } from './merchant-custom-page.js';
|
|
116
|
+
export { isBuilderPreviewMode } from './preview-mode.js';
|
|
117
|
+
export { getYouTubeEmbedBlockStyleProps, readYouTubeEmbedBlockSettings, type YouTubeEmbedBlockInstance, } from './youtube-embed-block.js';
|
|
118
|
+
export { readManifestStyleBlockProps } from './block-style-settings.js';
|
|
113
119
|
export { getBuilderPreviewReviewFixtures } from './preview-fixtures.js';
|
|
114
120
|
//# sourceMappingURL=react.d.ts.map
|
package/dist/react.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAoB,eAAe,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAS5H,OAAO,EAQL,KAAK,wBAAwB,EAC7B,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"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"}
|
package/dist/react.js
CHANGED
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
|
2
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 { resolveManifestSettingRecordValue } from './manifest-setting-paths.js';
|
|
5
6
|
import { BUILDER_PREVIEW_REVIEWS } from './preview-fixtures.js';
|
|
6
7
|
import { createBuilderCss } from './css-vars.js';
|
|
7
8
|
import { builderBlock } from './attributes.js';
|
|
@@ -352,12 +353,16 @@ function useScopedBuilderContentValue(path) {
|
|
|
352
353
|
const block = useContext(BuilderBlockContext);
|
|
353
354
|
if (!block)
|
|
354
355
|
return undefined;
|
|
355
|
-
|
|
356
|
-
|
|
356
|
+
const resolved = resolveManifestSettingRecordValue(block.settings, path);
|
|
357
|
+
if (resolved !== undefined) {
|
|
358
|
+
return resolved;
|
|
357
359
|
}
|
|
358
360
|
const shortPath = path.startsWith(`${block.type}.`) ? path.slice(block.type.length + 1) : path.split('.').at(-1);
|
|
359
|
-
if (shortPath
|
|
360
|
-
|
|
361
|
+
if (shortPath) {
|
|
362
|
+
const shortResolved = resolveManifestSettingRecordValue(block.settings, shortPath);
|
|
363
|
+
if (shortResolved !== undefined) {
|
|
364
|
+
return shortResolved;
|
|
365
|
+
}
|
|
361
366
|
}
|
|
362
367
|
const nested = shortPath ? getNestedBuilderSetting(block.settings, shortPath) : undefined;
|
|
363
368
|
return nested ?? getNestedBuilderSetting(block.settings, path);
|
|
@@ -661,6 +666,7 @@ function installBuilderPreviewHoverInspector() {
|
|
|
661
666
|
// we keep a small override table for these.
|
|
662
667
|
const SLUG_LABEL_OVERRIDES = {
|
|
663
668
|
'custom-html': 'Custom Embed',
|
|
669
|
+
'youtube-embed': 'YouTube Video',
|
|
664
670
|
};
|
|
665
671
|
const override = SLUG_LABEL_OVERRIDES[type];
|
|
666
672
|
const label = override ?? type
|
|
@@ -964,4 +970,10 @@ function installBuilderDirectManipulation(postMessage, getRevision) {
|
|
|
964
970
|
}
|
|
965
971
|
export { getBuilderBlockSettingText, getBuilderProductBlockAttributes, getLayoutPageBlockAttributes, getProductBlockText, getProductPageBlockAttributes, } from './product-page.js';
|
|
966
972
|
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';
|
|
973
|
+
export { YouTubeEmbed, YouTubeEmbedPreviewPlaceholder } from './YouTubeEmbed.js';
|
|
974
|
+
export { YouTubeEmbedBuilderBlock, } from './YouTubeEmbedBuilderBlock.js';
|
|
975
|
+
export { createMerchantCustomPageRegistry, MerchantCustomPageBuilderView, useMerchantCustomPageRegistry, useMerchantCustomPageView, } from './merchant-custom-page.js';
|
|
976
|
+
export { isBuilderPreviewMode } from './preview-mode.js';
|
|
977
|
+
export { getYouTubeEmbedBlockStyleProps, readYouTubeEmbedBlockSettings, } from './youtube-embed-block.js';
|
|
978
|
+
export { readManifestStyleBlockProps } from './block-style-settings.js';
|
|
967
979
|
export { getBuilderPreviewReviewFixtures } from './preview-fixtures.js';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { BlockInstance } from '@shoppex/builder-contracts';
|
|
2
|
+
export type YouTubeEmbedBlockInstance = Pick<BlockInstance, 'id' | 'settings'>;
|
|
3
|
+
export declare function readYouTubeEmbedBlockSettings(block: YouTubeEmbedBlockInstance): {
|
|
4
|
+
videoUrl: string;
|
|
5
|
+
title: string;
|
|
6
|
+
height: number | undefined;
|
|
7
|
+
privacyEnhanced: boolean;
|
|
8
|
+
};
|
|
9
|
+
export declare function getYouTubeEmbedBlockStyleProps(block: YouTubeEmbedBlockInstance): import("react").CSSProperties;
|
|
10
|
+
//# sourceMappingURL=youtube-embed-block.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"youtube-embed-block.d.ts","sourceRoot":"","sources":["../src/youtube-embed-block.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAGhE,MAAM,MAAM,yBAAyB,GAAG,IAAI,CAAC,aAAa,EAAE,IAAI,GAAG,UAAU,CAAC,CAAC;AAE/E,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,yBAAyB;;;;;EAkB7E;AAED,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,yBAAyB,iCAE9E"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { readManifestStyleBlockProps } from './block-style-settings.js';
|
|
2
|
+
export function readYouTubeEmbedBlockSettings(block) {
|
|
3
|
+
const videoUrl = typeof block.settings.videoUrl === 'string' ? block.settings.videoUrl : '';
|
|
4
|
+
const title = typeof block.settings.title === 'string' && block.settings.title.trim().length > 0
|
|
5
|
+
? block.settings.title.trim()
|
|
6
|
+
: 'YouTube video';
|
|
7
|
+
const heightRaw = block.settings.height;
|
|
8
|
+
const height = typeof heightRaw === 'number' && heightRaw > 0 ? heightRaw : undefined;
|
|
9
|
+
const privacyEnhanced = block.settings.privacyEnhanced === true;
|
|
10
|
+
return {
|
|
11
|
+
videoUrl,
|
|
12
|
+
title,
|
|
13
|
+
height,
|
|
14
|
+
privacyEnhanced,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function getYouTubeEmbedBlockStyleProps(block) {
|
|
18
|
+
return readManifestStyleBlockProps(block.settings);
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"youtube.d.ts","sourceRoot":"","sources":["../src/youtube.ts"],"names":[],"mappings":"AAMA,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA+ChE;AAED,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;IAAE,eAAe,CAAC,EAAE,OAAO,CAAA;CAAE,GACtC,MAAM,CAOR"}
|
package/dist/youtube.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const YOUTUBE_VIDEO_ID_PATTERN = /^[\w-]{11}$/;
|
|
2
|
+
function isYouTubeVideoId(value) {
|
|
3
|
+
return YOUTUBE_VIDEO_ID_PATTERN.test(value);
|
|
4
|
+
}
|
|
5
|
+
export function parseYouTubeVideoId(input) {
|
|
6
|
+
const trimmed = input.trim();
|
|
7
|
+
if (!trimmed) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
if (isYouTubeVideoId(trimmed)) {
|
|
11
|
+
return trimmed;
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const url = trimmed.startsWith('http://') || trimmed.startsWith('https://')
|
|
15
|
+
? new URL(trimmed)
|
|
16
|
+
: new URL(`https://${trimmed}`);
|
|
17
|
+
const host = url.hostname.replace(/^www\./, '');
|
|
18
|
+
if (host === 'youtu.be') {
|
|
19
|
+
const candidate = url.pathname.split('/').filter(Boolean)[0] ?? '';
|
|
20
|
+
return isYouTubeVideoId(candidate) ? candidate : null;
|
|
21
|
+
}
|
|
22
|
+
if (host === 'youtube.com' || host === 'm.youtube.com' || host === 'music.youtube.com') {
|
|
23
|
+
const watchId = url.searchParams.get('v');
|
|
24
|
+
if (watchId && isYouTubeVideoId(watchId)) {
|
|
25
|
+
return watchId;
|
|
26
|
+
}
|
|
27
|
+
const embedMatch = url.pathname.match(/^\/embed\/([\w-]{11})/);
|
|
28
|
+
if (embedMatch?.[1] && isYouTubeVideoId(embedMatch[1])) {
|
|
29
|
+
return embedMatch[1];
|
|
30
|
+
}
|
|
31
|
+
const shortsMatch = url.pathname.match(/^\/shorts\/([\w-]{11})/);
|
|
32
|
+
if (shortsMatch?.[1] && isYouTubeVideoId(shortsMatch[1])) {
|
|
33
|
+
return shortsMatch[1];
|
|
34
|
+
}
|
|
35
|
+
const liveMatch = url.pathname.match(/^\/live\/([\w-]{11})/);
|
|
36
|
+
if (liveMatch?.[1] && isYouTubeVideoId(liveMatch[1])) {
|
|
37
|
+
return liveMatch[1];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
export function buildYouTubeEmbedSrc(videoId, options) {
|
|
47
|
+
if (!isYouTubeVideoId(videoId)) {
|
|
48
|
+
throw new Error(`Invalid YouTube video id: ${videoId}`);
|
|
49
|
+
}
|
|
50
|
+
const host = options?.privacyEnhanced ? 'www.youtube-nocookie.com' : 'www.youtube.com';
|
|
51
|
+
return `https://${host}/embed/${videoId}`;
|
|
52
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
import { buildYouTubeEmbedSrc, parseYouTubeVideoId } from './youtube.js';
|
|
5
|
+
|
|
6
|
+
export interface YouTubeEmbedProps {
|
|
7
|
+
videoUrl?: string;
|
|
8
|
+
title?: string;
|
|
9
|
+
height?: number;
|
|
10
|
+
privacyEnhanced?: boolean;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const DEFAULT_HEIGHT = 400;
|
|
15
|
+
const EMBED_MAX_WIDTH = 640;
|
|
16
|
+
|
|
17
|
+
export function YouTubeEmbed({
|
|
18
|
+
videoUrl = '',
|
|
19
|
+
title = 'YouTube video',
|
|
20
|
+
height,
|
|
21
|
+
privacyEnhanced = false,
|
|
22
|
+
className = '',
|
|
23
|
+
}: YouTubeEmbedProps) {
|
|
24
|
+
const videoId = useMemo(() => parseYouTubeVideoId(videoUrl), [videoUrl]);
|
|
25
|
+
const resolvedHeight =
|
|
26
|
+
typeof height === 'number' && height > 0 ? height : DEFAULT_HEIGHT;
|
|
27
|
+
|
|
28
|
+
if (!videoId) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<section className={`my-8 ${className}`} style={{ display: 'flex', justifyContent: 'center' }}>
|
|
34
|
+
<div
|
|
35
|
+
style={{
|
|
36
|
+
position: 'relative',
|
|
37
|
+
width: '100%',
|
|
38
|
+
maxWidth: `${EMBED_MAX_WIDTH}px`,
|
|
39
|
+
height: `${resolvedHeight}px`,
|
|
40
|
+
borderRadius: 'inherit',
|
|
41
|
+
}}
|
|
42
|
+
>
|
|
43
|
+
<iframe
|
|
44
|
+
src={buildYouTubeEmbedSrc(videoId, { privacyEnhanced })}
|
|
45
|
+
title={title}
|
|
46
|
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
|
47
|
+
allowFullScreen
|
|
48
|
+
loading="lazy"
|
|
49
|
+
style={{
|
|
50
|
+
display: 'block',
|
|
51
|
+
width: '100%',
|
|
52
|
+
height: '100%',
|
|
53
|
+
border: 0,
|
|
54
|
+
}}
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
</section>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function YouTubeEmbedPreviewPlaceholder({
|
|
62
|
+
className = '',
|
|
63
|
+
height,
|
|
64
|
+
}: {
|
|
65
|
+
className?: string;
|
|
66
|
+
height?: number;
|
|
67
|
+
}) {
|
|
68
|
+
const resolvedHeight =
|
|
69
|
+
typeof height === 'number' && height > 0 ? height : DEFAULT_HEIGHT;
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<section className={`my-8 ${className}`} style={{ display: 'flex', justifyContent: 'center' }}>
|
|
73
|
+
<div
|
|
74
|
+
style={{
|
|
75
|
+
position: 'relative',
|
|
76
|
+
width: '100%',
|
|
77
|
+
maxWidth: `${EMBED_MAX_WIDTH}px`,
|
|
78
|
+
height: `${resolvedHeight}px`,
|
|
79
|
+
borderRadius: 'inherit',
|
|
80
|
+
}}
|
|
81
|
+
>
|
|
82
|
+
<div
|
|
83
|
+
style={{
|
|
84
|
+
display: 'flex',
|
|
85
|
+
alignItems: 'center',
|
|
86
|
+
justifyContent: 'center',
|
|
87
|
+
width: '100%',
|
|
88
|
+
height: '100%',
|
|
89
|
+
padding: '0 24px',
|
|
90
|
+
border: '1px dashed rgba(127, 127, 127, 0.35)',
|
|
91
|
+
borderRadius: 'inherit',
|
|
92
|
+
background: 'rgba(127, 127, 127, 0.06)',
|
|
93
|
+
color: '#5f6470',
|
|
94
|
+
fontFamily: 'Inter, system-ui, sans-serif',
|
|
95
|
+
fontSize: '13px',
|
|
96
|
+
textAlign: 'center',
|
|
97
|
+
}}
|
|
98
|
+
aria-hidden="true"
|
|
99
|
+
>
|
|
100
|
+
Paste a YouTube link or video ID in the Inspector to preview the player here.
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</section>
|
|
104
|
+
);
|
|
105
|
+
}
|