mcp-magento-cms 1.0.3 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +173 -82
- package/package.json +4 -3
- package/src/assignStores.js +150 -0
- package/src/index.js +840 -89
- package/src/magentoClient.js +95 -4
- package/src/pageBuilder.js +66 -68
- package/src/pageBuilderElements.js +789 -0
- package/src/pageBuilderRepair.js +266 -0
- package/src/pageBuilderRules.js +127 -0
- package/src/pageBuilderValidate.js +164 -0
- package/src/projectRules.js +95 -0
|
@@ -0,0 +1,789 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Magento Page Builder native content-type composers (project-agnostic).
|
|
3
|
+
* Covers core types from Magento_PageBuilder master templates.
|
|
4
|
+
* Theme CSS class names are passed via css_classes — never hardcoded here.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const ROW_ATTRS =
|
|
8
|
+
'data-enable-parallax="0" data-parallax-speed="0.5" data-background-images="{}" ' +
|
|
9
|
+
'data-background-type="image" data-video-loop="true" data-video-play-only-visible="true" ' +
|
|
10
|
+
'data-video-lazy-load="true" data-video-fallback-src=""';
|
|
11
|
+
|
|
12
|
+
const FLEX_COL =
|
|
13
|
+
'justify-content:flex-start;display:flex;flex-direction:column;' +
|
|
14
|
+
'background-position:left top;background-size:cover;background-repeat:no-repeat;background-attachment:scroll';
|
|
15
|
+
|
|
16
|
+
const WIDTH_FOR_COUNT = {
|
|
17
|
+
1: '100%',
|
|
18
|
+
2: '50%',
|
|
19
|
+
3: '33.3333%',
|
|
20
|
+
4: '25%',
|
|
21
|
+
5: '20%',
|
|
22
|
+
6: '16.6667%',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** All Magento_PageBuilder core content types this package can compose. */
|
|
26
|
+
export const PAGEBUILDER_ELEMENT_TYPES = [
|
|
27
|
+
'row',
|
|
28
|
+
'column-group',
|
|
29
|
+
'column-line',
|
|
30
|
+
'column',
|
|
31
|
+
'columns', // convenience: column-group + line + equal columns
|
|
32
|
+
'text',
|
|
33
|
+
'heading',
|
|
34
|
+
'image',
|
|
35
|
+
'buttons',
|
|
36
|
+
'button-item',
|
|
37
|
+
'banner',
|
|
38
|
+
'html',
|
|
39
|
+
'divider',
|
|
40
|
+
'video',
|
|
41
|
+
'map',
|
|
42
|
+
'tabs',
|
|
43
|
+
'tab-item',
|
|
44
|
+
'slider',
|
|
45
|
+
'slide',
|
|
46
|
+
'products',
|
|
47
|
+
'block',
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
export class StyleBag {
|
|
51
|
+
constructor() {
|
|
52
|
+
/** @type {Record<string, string>} */
|
|
53
|
+
this.styles = {};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** @param {string} css */
|
|
57
|
+
add(css) {
|
|
58
|
+
const id = genStyleId();
|
|
59
|
+
this.styles[id] = css;
|
|
60
|
+
return id;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
toHtml() {
|
|
64
|
+
const entries = Object.entries(this.styles);
|
|
65
|
+
if (!entries.length) return '';
|
|
66
|
+
const rules = entries
|
|
67
|
+
.map(([id, css]) => `#html-body [data-pb-style=${id}]{${css}}`)
|
|
68
|
+
.join('');
|
|
69
|
+
return `<style>${rules}</style>`;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function genStyleId() {
|
|
74
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
75
|
+
let id = '';
|
|
76
|
+
for (let i = 0; i < 7; i++) {
|
|
77
|
+
id += chars[Math.floor(Math.random() * chars.length)];
|
|
78
|
+
}
|
|
79
|
+
return id;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function escapeHtml(value) {
|
|
83
|
+
return String(value)
|
|
84
|
+
.replace(/&/g, '&')
|
|
85
|
+
.replace(/</g, '<')
|
|
86
|
+
.replace(/>/g, '>')
|
|
87
|
+
.replace(/"/g, '"')
|
|
88
|
+
.replace(/'/g, ''');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function looksLikeHtml(value) {
|
|
92
|
+
return /<[^>]+>/.test(String(value || ''));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function normalizeHtml(htmlContent) {
|
|
96
|
+
const t = String(htmlContent || '').trim();
|
|
97
|
+
if (!t) return '<p></p>';
|
|
98
|
+
if (!looksLikeHtml(t)) {
|
|
99
|
+
return String(t)
|
|
100
|
+
.split(/\r\n|\r|\n/)
|
|
101
|
+
.map((l) => l.trim())
|
|
102
|
+
.filter(Boolean)
|
|
103
|
+
.map((l) => `<p>${escapeHtml(l)}</p>`)
|
|
104
|
+
.join('') || '<p></p>';
|
|
105
|
+
}
|
|
106
|
+
return t;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function classAttr(cssClasses) {
|
|
110
|
+
const c = String(cssClasses || '').trim();
|
|
111
|
+
return c ? ` class="${escapeHtml(c)}"` : '';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function mergeClass(...parts) {
|
|
115
|
+
return parts
|
|
116
|
+
.map((p) => String(p || '').trim())
|
|
117
|
+
.filter(Boolean)
|
|
118
|
+
.join(' ');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function childrenHtml(node, bag) {
|
|
122
|
+
const kids = node.children || node.elements || [];
|
|
123
|
+
if (!Array.isArray(kids) || !kids.length) return '';
|
|
124
|
+
return kids.map((c) => composeElement(c, bag)).join('');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Compose one Page Builder element (and nested children).
|
|
129
|
+
* @param {object} node
|
|
130
|
+
* @param {StyleBag} bag
|
|
131
|
+
*/
|
|
132
|
+
export function composeElement(node, bag) {
|
|
133
|
+
if (!node || typeof node !== 'object') {
|
|
134
|
+
throw new Error('composeElement: node must be an object');
|
|
135
|
+
}
|
|
136
|
+
const type = String(node.type || node.content_type || '')
|
|
137
|
+
.toLowerCase()
|
|
138
|
+
.trim();
|
|
139
|
+
if (!type) throw new Error('composeElement: missing type');
|
|
140
|
+
|
|
141
|
+
switch (type) {
|
|
142
|
+
case 'row':
|
|
143
|
+
return composeRow(node, bag);
|
|
144
|
+
case 'columns':
|
|
145
|
+
case 'column_group':
|
|
146
|
+
case 'column-group':
|
|
147
|
+
return composeColumnGroup(node, bag);
|
|
148
|
+
case 'column-line':
|
|
149
|
+
case 'column_line':
|
|
150
|
+
return composeColumnLine(node, bag);
|
|
151
|
+
case 'column':
|
|
152
|
+
return composeColumn(node, bag);
|
|
153
|
+
case 'text':
|
|
154
|
+
return composeText(node, bag);
|
|
155
|
+
case 'heading':
|
|
156
|
+
return composeHeading(node, bag);
|
|
157
|
+
case 'image':
|
|
158
|
+
return composeImage(node, bag);
|
|
159
|
+
case 'buttons':
|
|
160
|
+
return composeButtons(node, bag);
|
|
161
|
+
case 'button-item':
|
|
162
|
+
case 'button_item':
|
|
163
|
+
case 'button':
|
|
164
|
+
return composeButtonItem(node, bag);
|
|
165
|
+
case 'banner':
|
|
166
|
+
return composeBanner(node, bag);
|
|
167
|
+
case 'html':
|
|
168
|
+
return composeHtml(node, bag);
|
|
169
|
+
case 'divider':
|
|
170
|
+
return composeDivider(node, bag);
|
|
171
|
+
case 'video':
|
|
172
|
+
return composeVideo(node, bag);
|
|
173
|
+
case 'map':
|
|
174
|
+
return composeMap(node, bag);
|
|
175
|
+
case 'tabs':
|
|
176
|
+
return composeTabs(node, bag);
|
|
177
|
+
case 'tab-item':
|
|
178
|
+
case 'tab_item':
|
|
179
|
+
case 'tab':
|
|
180
|
+
return composeTabItem(node, bag);
|
|
181
|
+
case 'slider':
|
|
182
|
+
return composeSlider(node, bag);
|
|
183
|
+
case 'slide':
|
|
184
|
+
return composeSlide(node, bag);
|
|
185
|
+
case 'products':
|
|
186
|
+
return composeProducts(node, bag);
|
|
187
|
+
case 'block':
|
|
188
|
+
case 'cms_block':
|
|
189
|
+
return composeBlock(node, bag);
|
|
190
|
+
default:
|
|
191
|
+
throw new Error(
|
|
192
|
+
`Unsupported Page Builder type "${type}". Supported: ${PAGEBUILDER_ELEMENT_TYPES.join(', ')}`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* @param {object[]} sections
|
|
199
|
+
* @returns {{ html: string, used_types: string[], allow_full_bleed: boolean }}
|
|
200
|
+
*/
|
|
201
|
+
export function composeDocument(sections) {
|
|
202
|
+
if (!Array.isArray(sections) || !sections.length) {
|
|
203
|
+
throw new Error('composeDocument: sections must be a non-empty array');
|
|
204
|
+
}
|
|
205
|
+
const bag = new StyleBag();
|
|
206
|
+
const used = new Set();
|
|
207
|
+
const walk = (n) => {
|
|
208
|
+
if (!n || typeof n !== 'object') return;
|
|
209
|
+
const t = String(n.type || n.content_type || '').toLowerCase();
|
|
210
|
+
if (t) used.add(t);
|
|
211
|
+
for (const c of n.children || n.elements || n.columns || n.tabs || n.slides || n.items || []) {
|
|
212
|
+
walk(c);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
sections.forEach(walk);
|
|
216
|
+
|
|
217
|
+
const body = sections.map((s) => composeElement(s, bag)).join('');
|
|
218
|
+
const html = body + bag.toHtml();
|
|
219
|
+
const allowFullBleed = sections.some(
|
|
220
|
+
(s) =>
|
|
221
|
+
String(s.type || '').toLowerCase() === 'row' &&
|
|
222
|
+
String(s.appearance || '').toLowerCase() === 'full-bleed'
|
|
223
|
+
);
|
|
224
|
+
return {
|
|
225
|
+
html,
|
|
226
|
+
used_types: [...used],
|
|
227
|
+
allow_full_bleed: allowFullBleed,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// —— Row ——
|
|
232
|
+
|
|
233
|
+
function composeRow(node, bag) {
|
|
234
|
+
const appearance = String(node.appearance || 'contained').toLowerCase();
|
|
235
|
+
const allowed = ['contained', 'full-width', 'full-bleed'];
|
|
236
|
+
if (!allowed.includes(appearance)) {
|
|
237
|
+
throw new Error(`row appearance must be one of: ${allowed.join(', ')}`);
|
|
238
|
+
}
|
|
239
|
+
const styleId = bag.add(FLEX_COL);
|
|
240
|
+
const css = classAttr(node.css_classes);
|
|
241
|
+
const inner = childrenHtml(node, bag);
|
|
242
|
+
|
|
243
|
+
if (appearance === 'full-bleed') {
|
|
244
|
+
return (
|
|
245
|
+
`<div${css} data-content-type="row" data-appearance="full-bleed" ${ROW_ATTRS} ` +
|
|
246
|
+
`data-element="main" data-pb-style="${styleId}">${inner}</div>`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
if (appearance === 'full-width') {
|
|
250
|
+
const innerId = bag.add('');
|
|
251
|
+
return (
|
|
252
|
+
`<div${css} data-content-type="row" data-appearance="full-width" ${ROW_ATTRS} ` +
|
|
253
|
+
`data-element="main" data-pb-style="${styleId}">` +
|
|
254
|
+
`<div class="row-full-width-inner" data-element="inner" data-pb-style="${innerId}">${inner}</div></div>`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
// contained
|
|
258
|
+
return (
|
|
259
|
+
`<div${css} data-content-type="row" data-appearance="contained" ${ROW_ATTRS} ` +
|
|
260
|
+
`data-element="main" data-pb-style="${styleId}">` +
|
|
261
|
+
`<div data-element="inner">${inner}</div></div>`
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// —— Columns ——
|
|
266
|
+
|
|
267
|
+
function composeColumnGroup(node, bag) {
|
|
268
|
+
// Convenience: { type:'columns', columns:[{width?, children}] }
|
|
269
|
+
if (Array.isArray(node.columns) && node.columns.length) {
|
|
270
|
+
const n = node.columns.length;
|
|
271
|
+
const defaultW = WIDTH_FOR_COUNT[n] || `${(100 / n).toFixed(4)}%`;
|
|
272
|
+
const cols = node.columns.map((col) =>
|
|
273
|
+
composeColumn(
|
|
274
|
+
{
|
|
275
|
+
type: 'column',
|
|
276
|
+
width: col.width || defaultW,
|
|
277
|
+
css_classes: col.css_classes,
|
|
278
|
+
children: col.children || col.elements || [],
|
|
279
|
+
},
|
|
280
|
+
bag
|
|
281
|
+
)
|
|
282
|
+
);
|
|
283
|
+
const gId = bag.add(
|
|
284
|
+
'display:flex;background-position:left top;background-size:cover;background-repeat:no-repeat;background-attachment:scroll;'
|
|
285
|
+
);
|
|
286
|
+
const lineId = bag.add('display:flex;justify-content:flex-start;');
|
|
287
|
+
const css = classAttr(mergeClass('pagebuilder-column-group', node.css_classes));
|
|
288
|
+
return (
|
|
289
|
+
`<div${css} data-content-type="column-group" data-grid-size="12" data-background-images="{}" ` +
|
|
290
|
+
`data-element="main" data-pb-style="${gId}">` +
|
|
291
|
+
`<div class="pagebuilder-column-line" data-content-type="column-line" data-element="main" data-pb-style="${lineId}">` +
|
|
292
|
+
`${cols.join('')}</div></div>`
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
const gId = bag.add(
|
|
296
|
+
'display:flex;background-position:left top;background-size:cover;background-repeat:no-repeat;background-attachment:scroll;'
|
|
297
|
+
);
|
|
298
|
+
const css = classAttr(mergeClass('pagebuilder-column-group', node.css_classes));
|
|
299
|
+
return (
|
|
300
|
+
`<div${css} data-content-type="column-group" data-grid-size="12" data-background-images="{}" ` +
|
|
301
|
+
`data-element="main" data-pb-style="${gId}">${childrenHtml(node, bag)}</div>`
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function composeColumnLine(node, bag) {
|
|
306
|
+
const lineId = bag.add('display:flex;justify-content:flex-start;');
|
|
307
|
+
const css = classAttr(mergeClass('pagebuilder-column-line', node.css_classes));
|
|
308
|
+
return (
|
|
309
|
+
`<div${css} data-content-type="column-line" data-element="main" data-pb-style="${lineId}">` +
|
|
310
|
+
`${childrenHtml(node, bag)}</div>`
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function composeColumn(node, bag) {
|
|
315
|
+
const width = String(node.width || '100%');
|
|
316
|
+
const styleId = bag.add(
|
|
317
|
+
`${FLEX_COL};width:${width};align-self:stretch`
|
|
318
|
+
);
|
|
319
|
+
const css = classAttr(mergeClass('pagebuilder-column', node.css_classes));
|
|
320
|
+
return (
|
|
321
|
+
`<div${css} data-content-type="column" data-appearance="full-height" data-background-images="{}" ` +
|
|
322
|
+
`data-element="main" data-pb-style="${styleId}">${childrenHtml(node, bag)}</div>`
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// —— Text / Heading ——
|
|
327
|
+
|
|
328
|
+
function composeText(node, bag) {
|
|
329
|
+
const styleId = bag.add('border-style:none');
|
|
330
|
+
const html = normalizeHtml(node.html ?? node.content ?? node.text ?? '');
|
|
331
|
+
const css = classAttr(mergeClass('bypass-html-filter', node.css_classes));
|
|
332
|
+
return (
|
|
333
|
+
`<div${css} data-content-type="text" data-appearance="default" data-element="main" ` +
|
|
334
|
+
`data-pb-style="${styleId}">${html}</div>`
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function composeHeading(node, bag) {
|
|
339
|
+
const level = String(node.heading_type || node.level || 'h2').toLowerCase();
|
|
340
|
+
const tag = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(level) ? level : 'h2';
|
|
341
|
+
const text = node.html != null ? String(node.html) : escapeHtml(String(node.text || node.content || ''));
|
|
342
|
+
const styleId = bag.add('text-align:left;border-style:none');
|
|
343
|
+
const css = classAttr(node.css_classes);
|
|
344
|
+
// Magento master: attributes on the heading element itself
|
|
345
|
+
return (
|
|
346
|
+
`<${tag}${css} data-content-type="heading" data-appearance="default" data-element="main" ` +
|
|
347
|
+
`data-pb-style="${styleId}">${text}</${tag}>`
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// —— Image ——
|
|
352
|
+
|
|
353
|
+
function composeImage(node, bag) {
|
|
354
|
+
const src = String(node.src || node.desktop_image || node.url || '').trim();
|
|
355
|
+
if (!src) throw new Error('image requires src (or desktop_image)');
|
|
356
|
+
const mobile = String(node.mobile_src || node.mobile_image || src).trim();
|
|
357
|
+
const alt = escapeHtml(String(node.alt || ''));
|
|
358
|
+
const caption = node.caption != null ? String(node.caption) : '';
|
|
359
|
+
const href = String(node.link_url || node.href || '').trim();
|
|
360
|
+
const mainId = bag.add('border-style:none');
|
|
361
|
+
const imgId = bag.add('max-width:100%;height:auto');
|
|
362
|
+
const imgMId = bag.add('max-width:100%;height:auto');
|
|
363
|
+
const css = classAttr(node.css_classes);
|
|
364
|
+
const appearance = String(node.appearance || 'full-width');
|
|
365
|
+
|
|
366
|
+
const imgs =
|
|
367
|
+
`<img class="pagebuilder-mobile-hidden" src="${escapeHtml(src)}" alt="${alt}" title="${alt}" ` +
|
|
368
|
+
`loading="lazy" data-element="desktop_image" data-pb-style="${imgId}">` +
|
|
369
|
+
`<img class="pagebuilder-mobile-only" src="${escapeHtml(mobile)}" alt="${alt}" title="${alt}" ` +
|
|
370
|
+
`loading="lazy" data-element="mobile_image" data-pb-style="${imgMId}">`;
|
|
371
|
+
|
|
372
|
+
const body = href
|
|
373
|
+
? `<a href="${escapeHtml(href)}" target="" data-link-type="default" data-element="link">${imgs}</a>`
|
|
374
|
+
: imgs;
|
|
375
|
+
|
|
376
|
+
const cap = caption
|
|
377
|
+
? `<figcaption data-element="caption">${caption}</figcaption>`
|
|
378
|
+
: `<figcaption data-element="caption"></figcaption>`;
|
|
379
|
+
|
|
380
|
+
return (
|
|
381
|
+
`<figure${css} data-content-type="image" data-appearance="${escapeHtml(appearance)}" ` +
|
|
382
|
+
`data-element="main" data-pb-style="${mainId}">${body}${cap}</figure>`
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// —— Buttons ——
|
|
387
|
+
|
|
388
|
+
function composeButtons(node, bag) {
|
|
389
|
+
const items = Array.isArray(node.items)
|
|
390
|
+
? node.items
|
|
391
|
+
: Array.isArray(node.children)
|
|
392
|
+
? node.children
|
|
393
|
+
: [];
|
|
394
|
+
const itemsHtml = items
|
|
395
|
+
.map((it) =>
|
|
396
|
+
composeButtonItem(
|
|
397
|
+
{ type: 'button-item', ...it, text: it.text || it.label, url: it.url || it.href || it.link_url },
|
|
398
|
+
bag
|
|
399
|
+
)
|
|
400
|
+
)
|
|
401
|
+
.join('');
|
|
402
|
+
const appearance = String(node.appearance || 'inline');
|
|
403
|
+
const css = classAttr(node.css_classes);
|
|
404
|
+
return (
|
|
405
|
+
`<div${css} data-content-type="buttons" data-appearance="${escapeHtml(appearance)}" ` +
|
|
406
|
+
`data-element="main">${itemsHtml}</div>`
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function composeButtonItem(node, bag) {
|
|
411
|
+
const label = String(node.text || node.label || node.link_text || 'Button');
|
|
412
|
+
const href = String(node.url || node.href || node.link_url || '#').trim();
|
|
413
|
+
const btnType = String(node.button_type || node.style || 'primary').replace(/^pagebuilder-button-/, '');
|
|
414
|
+
const btnClass = `pagebuilder-button-${btnType}`;
|
|
415
|
+
const styleId = bag.add('display:inline-block');
|
|
416
|
+
const extra = String(node.css_classes || '').trim();
|
|
417
|
+
const cls = mergeClass(btnClass, extra);
|
|
418
|
+
return (
|
|
419
|
+
`<div class="${escapeHtml(cls)}" data-content-type="button-item" data-appearance="default" ` +
|
|
420
|
+
`data-element="main" data-pb-style="${styleId}">` +
|
|
421
|
+
`<a class="${escapeHtml(btnClass)}" href="${escapeHtml(href)}" target="" data-link-type="default" data-element="link">` +
|
|
422
|
+
`<span data-element="link_text">${escapeHtml(label)}</span></a></div>`
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// —— Banner ——
|
|
427
|
+
|
|
428
|
+
function composeBanner(node, bag) {
|
|
429
|
+
const appearance = String(node.appearance || 'poster');
|
|
430
|
+
const bg = String(node.background_image || node.src || '').trim();
|
|
431
|
+
const contentHtml = normalizeHtml(node.content_html || node.html || node.content || '');
|
|
432
|
+
const showButton = String(node.show_button || (node.button_text ? 'always' : 'never')).toLowerCase();
|
|
433
|
+
const buttonText = String(node.button_text || '');
|
|
434
|
+
const linkUrl = String(node.link_url || node.href || '').trim();
|
|
435
|
+
const minHeight = String(node.min_height || '300px');
|
|
436
|
+
const overlayColor = node.overlay_color != null ? String(node.overlay_color) : '';
|
|
437
|
+
|
|
438
|
+
const mainId = bag.add('border-style:none');
|
|
439
|
+
const wrapId = bag.add(`min-height:${minHeight};background-position:center center;background-size:cover;`);
|
|
440
|
+
const overlayId = bag.add(
|
|
441
|
+
`min-height:${minHeight};padding:0;background-color:${overlayColor || 'transparent'};`
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
const bgAttr = bg
|
|
445
|
+
? `{"desktop_image":"${String(bg).replace(/"/g, '')}"}`
|
|
446
|
+
: '{}';
|
|
447
|
+
|
|
448
|
+
const buttonHtml =
|
|
449
|
+
showButton === 'never'
|
|
450
|
+
? `<div data-element="button" style="display:none"></div>`
|
|
451
|
+
: `<button type="button" class="pagebuilder-banner-button" data-element="button">${escapeHtml(buttonText || 'Button')}</button>`;
|
|
452
|
+
|
|
453
|
+
const posterInner =
|
|
454
|
+
`<div class="pagebuilder-banner-wrapper" data-background-images="${bgAttr}" data-background-type="image" ` +
|
|
455
|
+
`data-video-loop="true" data-video-play-only-visible="true" data-video-lazy-load="true" data-video-fallback-src="" ` +
|
|
456
|
+
`data-element="wrapper" data-pb-style="${wrapId}">` +
|
|
457
|
+
`<div class="pagebuilder-overlay pagebuilder-poster-overlay" data-overlay-color="${escapeHtml(overlayColor)}" ` +
|
|
458
|
+
`data-element="overlay" data-pb-style="${overlayId}">` +
|
|
459
|
+
`<div class="pagebuilder-poster-content"><div data-element="content">${contentHtml}</div>${buttonHtml}` +
|
|
460
|
+
`</div></div></div>`;
|
|
461
|
+
|
|
462
|
+
const css = classAttr(node.css_classes);
|
|
463
|
+
const showBtnAttr = showButton === 'hover' ? 'hover' : showButton === 'always' ? 'always' : 'never';
|
|
464
|
+
|
|
465
|
+
if (linkUrl && showBtnAttr !== 'never') {
|
|
466
|
+
return (
|
|
467
|
+
`<div${css} data-content-type="banner" data-appearance="${escapeHtml(appearance)}" ` +
|
|
468
|
+
`data-show-button="${showBtnAttr}" data-show-overlay="always" data-element="main" data-pb-style="${mainId}">` +
|
|
469
|
+
`<a href="${escapeHtml(linkUrl)}" target="" data-link-type="default" data-element="link" ` +
|
|
470
|
+
`title="${escapeHtml(buttonText || '')}">${posterInner}</a></div>`
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return (
|
|
475
|
+
`<div${css} data-content-type="banner" data-appearance="${escapeHtml(appearance)}" ` +
|
|
476
|
+
`data-show-button="${showBtnAttr}" data-show-overlay="always" data-element="main" data-pb-style="${mainId}">` +
|
|
477
|
+
`<div data-element="empty_link">${posterInner}</div></div>`
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// —— HTML / Divider / Video / Map ——
|
|
482
|
+
|
|
483
|
+
function composeHtml(node, bag) {
|
|
484
|
+
// Magento HTML content type stores markup as text content (escaped when saved via stage);
|
|
485
|
+
// for master format we put raw HTML inside and rely on bypass-html-filter on storefront.
|
|
486
|
+
const styleId = bag.add('border-style:none');
|
|
487
|
+
const html = String(node.html ?? node.content ?? '');
|
|
488
|
+
const css = classAttr(mergeClass('bypass-html-filter', node.css_classes));
|
|
489
|
+
return (
|
|
490
|
+
`<div${css} data-content-type="html" data-appearance="default" data-element="main" ` +
|
|
491
|
+
`data-pb-style="${styleId}">${html}</div>`
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function composeDivider(node, bag) {
|
|
496
|
+
const mainId = bag.add('border-style:none;text-align:center');
|
|
497
|
+
const thickness = String(node.line_thickness || node.thickness || '1px');
|
|
498
|
+
const color = String(node.line_color || node.color || '#cccccc');
|
|
499
|
+
const width = String(node.line_width || node.width || '100%');
|
|
500
|
+
const lineId = bag.add(
|
|
501
|
+
`border-bottom-width:${thickness};border-bottom-style:solid;border-bottom-color:${color};width:${width};`
|
|
502
|
+
);
|
|
503
|
+
const css = classAttr(node.css_classes);
|
|
504
|
+
return (
|
|
505
|
+
`<div${css} data-content-type="divider" data-appearance="default" data-element="main" data-pb-style="${mainId}">` +
|
|
506
|
+
`<hr data-element="line" data-pb-style="${lineId}"></div>`
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function composeVideo(node, bag) {
|
|
511
|
+
const src = String(node.src || node.video_source || node.url || '').trim();
|
|
512
|
+
if (!src) throw new Error('video requires src');
|
|
513
|
+
const mainId = bag.add('border-style:none');
|
|
514
|
+
const innerId = bag.add('');
|
|
515
|
+
const wrapId = bag.add('');
|
|
516
|
+
const css = classAttr(node.css_classes);
|
|
517
|
+
const hosted = /youtube|youtu\.be|vimeo|player\./i.test(src);
|
|
518
|
+
const media = hosted
|
|
519
|
+
? `<iframe frameborder="0" allowfullscreen src="${escapeHtml(src)}" data-element="video"></iframe>`
|
|
520
|
+
: `<video controls src="${escapeHtml(src)}" data-element="video"></video>`;
|
|
521
|
+
return (
|
|
522
|
+
`<div${css} data-content-type="video" data-appearance="default" data-element="main" data-pb-style="${mainId}">` +
|
|
523
|
+
`<div class="pagebuilder-video-inner" data-element="inner" data-pb-style="${innerId}">` +
|
|
524
|
+
`<div class="pagebuilder-video-wrapper" data-element="wrapper" data-pb-style="${wrapId}">` +
|
|
525
|
+
`<div class="pagebuilder-video-container">${media}</div></div></div></div>`
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function composeMap(node, bag) {
|
|
530
|
+
const locations = node.locations || [
|
|
531
|
+
{
|
|
532
|
+
latitude: node.latitude ?? node.lat ?? 0,
|
|
533
|
+
longitude: node.longitude ?? node.lng ?? 0,
|
|
534
|
+
location_name: node.location_name || node.name || '',
|
|
535
|
+
},
|
|
536
|
+
];
|
|
537
|
+
const locJson = escapeHtml(JSON.stringify(locations));
|
|
538
|
+
const height = String(node.height || '300px');
|
|
539
|
+
const styleId = bag.add(`height:${height};border-style:none`);
|
|
540
|
+
const css = classAttr(node.css_classes);
|
|
541
|
+
return (
|
|
542
|
+
`<div${css} data-content-type="map" data-appearance="default" data-show-controls="true" ` +
|
|
543
|
+
`data-locations="${locJson}" data-element="main" data-pb-style="${styleId}"></div>`
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// —— Tabs ——
|
|
548
|
+
|
|
549
|
+
function composeTabs(node, bag) {
|
|
550
|
+
const tabs = Array.isArray(node.tabs) ? node.tabs : node.children || [];
|
|
551
|
+
if (!tabs.length) throw new Error('tabs requires tabs[]');
|
|
552
|
+
const mainId = bag.add('border-style:none');
|
|
553
|
+
const navId = bag.add('');
|
|
554
|
+
const contentId = bag.add('');
|
|
555
|
+
const css = classAttr(node.css_classes);
|
|
556
|
+
const defaultActive = Number(node.default_active ?? 0);
|
|
557
|
+
|
|
558
|
+
const nav = tabs
|
|
559
|
+
.map((tab, i) => {
|
|
560
|
+
const id = tab.id || `tab${genStyleId()}`;
|
|
561
|
+
tab._id = id;
|
|
562
|
+
const active = i === defaultActive ? ' true' : '';
|
|
563
|
+
const name = escapeHtml(String(tab.name || tab.title || `Tab ${i + 1}`));
|
|
564
|
+
return (
|
|
565
|
+
`<li role="tab" class="tab-header" data-element="headers" data-active="${active.trim() || 'false'}">` +
|
|
566
|
+
`<a href="#${id}" class="tab-title"><span class="tab-title">${name}</span></a></li>`
|
|
567
|
+
);
|
|
568
|
+
})
|
|
569
|
+
.join('');
|
|
570
|
+
|
|
571
|
+
const panels = tabs
|
|
572
|
+
.map((tab) =>
|
|
573
|
+
composeTabItem(
|
|
574
|
+
{
|
|
575
|
+
type: 'tab-item',
|
|
576
|
+
id: tab._id,
|
|
577
|
+
name: tab.name || tab.title,
|
|
578
|
+
css_classes: tab.css_classes,
|
|
579
|
+
children: tab.children || tab.elements || [],
|
|
580
|
+
},
|
|
581
|
+
bag
|
|
582
|
+
)
|
|
583
|
+
)
|
|
584
|
+
.join('');
|
|
585
|
+
|
|
586
|
+
return (
|
|
587
|
+
`<div${css} data-content-type="tabs" data-appearance="default" data-element="main" data-pb-style="${mainId}">` +
|
|
588
|
+
`<ul role="tablist" class="tabs-navigation" data-element="navigation" data-pb-style="${navId}">${nav}</ul>` +
|
|
589
|
+
`<div class="tabs-content" data-element="content" data-pb-style="${contentId}">${panels}</div></div>`
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function composeTabItem(node, bag) {
|
|
594
|
+
const id = node.id || `tab${genStyleId()}`;
|
|
595
|
+
const styleId = bag.add(FLEX_COL);
|
|
596
|
+
const css = classAttr(node.css_classes);
|
|
597
|
+
return (
|
|
598
|
+
`<div id="${escapeHtml(id)}"${css} data-content-type="tab-item" data-appearance="default" ` +
|
|
599
|
+
`data-element="main" data-pb-style="${styleId}">${childrenHtml(node, bag)}</div>`
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// —— Slider / Slide ——
|
|
604
|
+
|
|
605
|
+
function composeSlider(node, bag) {
|
|
606
|
+
const slides = Array.isArray(node.slides) ? node.slides : node.children || [];
|
|
607
|
+
const mainId = bag.add(`min-height:${node.min_height || '300px'};border-style:none`);
|
|
608
|
+
const css = classAttr(mergeClass('pagebuilder-slider', node.css_classes));
|
|
609
|
+
const showDots = node.show_dots === false ? 'false' : 'true';
|
|
610
|
+
const showArrows = node.show_arrows === false ? 'false' : 'true';
|
|
611
|
+
const autoplay = node.autoplay ? 'true' : 'false';
|
|
612
|
+
const slidesHtml = slides
|
|
613
|
+
.map((s) => composeSlide({ type: 'slide', ...s }, bag))
|
|
614
|
+
.join('');
|
|
615
|
+
return (
|
|
616
|
+
`<div${css} data-content-type="slider" data-appearance="default" data-autoplay="${autoplay}" ` +
|
|
617
|
+
`data-autoplay-speed="${escapeHtml(String(node.autoplay_speed || '4000'))}" ` +
|
|
618
|
+
`data-fade="false" data-infinite-loop="true" data-show-arrows="${showArrows}" data-show-dots="${showDots}" ` +
|
|
619
|
+
`data-element="main" data-pb-style="${mainId}">${slidesHtml}</div>`
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function composeSlide(node, bag) {
|
|
624
|
+
const bg = String(node.background_image || node.src || '').trim();
|
|
625
|
+
const contentHtml = normalizeHtml(node.content_html || node.html || node.content || '');
|
|
626
|
+
const showButton = String(node.show_button || (node.button_text ? 'always' : 'never')).toLowerCase();
|
|
627
|
+
const buttonText = String(node.button_text || '');
|
|
628
|
+
const linkUrl = String(node.link_url || node.href || '').trim();
|
|
629
|
+
const minHeight = String(node.min_height || '300px');
|
|
630
|
+
const appearance = String(node.appearance || 'poster');
|
|
631
|
+
|
|
632
|
+
const mainId = bag.add('border-style:none');
|
|
633
|
+
const wrapId = bag.add(`min-height:${minHeight};background-position:center center;background-size:cover;`);
|
|
634
|
+
const overlayId = bag.add(`min-height:${minHeight};padding:0;background-color:transparent;`);
|
|
635
|
+
const bgAttr = bg
|
|
636
|
+
? `{"desktop_image":"${String(bg).replace(/"/g, '')}"}`
|
|
637
|
+
: '{}';
|
|
638
|
+
|
|
639
|
+
const buttonHtml =
|
|
640
|
+
showButton === 'never'
|
|
641
|
+
? ''
|
|
642
|
+
: `<button type="button" class="pagebuilder-slide-button" data-element="button">${escapeHtml(buttonText || 'Button')}</button>`;
|
|
643
|
+
|
|
644
|
+
const inner =
|
|
645
|
+
`<div class="pagebuilder-slide-wrapper" data-background-images="${bgAttr}" data-background-type="image" ` +
|
|
646
|
+
`data-element="wrapper" data-pb-style="${wrapId}">` +
|
|
647
|
+
`<div class="pagebuilder-overlay pagebuilder-poster-overlay" data-overlay-color="" data-element="overlay" ` +
|
|
648
|
+
`data-pb-style="${overlayId}"><div class="pagebuilder-poster-content">` +
|
|
649
|
+
`<div data-element="content">${contentHtml}</div>${buttonHtml}</div></div></div>`;
|
|
650
|
+
|
|
651
|
+
const css = classAttr(node.css_classes);
|
|
652
|
+
const showBtnAttr = showButton === 'always' ? 'always' : showButton === 'hover' ? 'hover' : 'never';
|
|
653
|
+
|
|
654
|
+
if (linkUrl) {
|
|
655
|
+
return (
|
|
656
|
+
`<div${css} data-content-type="slide" data-appearance="${escapeHtml(appearance)}" ` +
|
|
657
|
+
`data-show-button="${showBtnAttr}" data-show-overlay="always" data-element="main" data-pb-style="${mainId}">` +
|
|
658
|
+
`<a href="${escapeHtml(linkUrl)}" target="" data-link-type="default" data-element="link">${inner}</a></div>`
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
return (
|
|
662
|
+
`<div${css} data-content-type="slide" data-appearance="${escapeHtml(appearance)}" ` +
|
|
663
|
+
`data-show-button="${showBtnAttr}" data-show-overlay="always" data-element="main" data-pb-style="${mainId}">` +
|
|
664
|
+
`<div data-element="empty_link">${inner}</div></div>`
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// —— Products / Block (widget wrappers) ——
|
|
669
|
+
|
|
670
|
+
function composeProducts(node, bag) {
|
|
671
|
+
const appearance = String(node.appearance || 'grid');
|
|
672
|
+
const styleId = bag.add('border-style:none');
|
|
673
|
+
const css = classAttr(node.css_classes);
|
|
674
|
+
let inner = String(node.html || node.widget_html || '').trim();
|
|
675
|
+
|
|
676
|
+
if (!inner) {
|
|
677
|
+
const skus = Array.isArray(node.skus) ? node.skus : [];
|
|
678
|
+
const count = Number(node.products_count || (skus.length ? skus.length : 5));
|
|
679
|
+
const template =
|
|
680
|
+
appearance === 'carousel'
|
|
681
|
+
? 'Magento_PageBuilder::catalog/product/widget/content/carousel.phtml'
|
|
682
|
+
: 'Magento_PageBuilder::catalog/product/widget/content/grid.phtml';
|
|
683
|
+
|
|
684
|
+
if (skus.length) {
|
|
685
|
+
// Magento widget conditions_encoded uses a compact substitute alphabet.
|
|
686
|
+
const raw = JSON.stringify({
|
|
687
|
+
'1': {
|
|
688
|
+
type: 'Magento\\CatalogWidget\\Model\\Rule\\Condition\\Combine',
|
|
689
|
+
aggregator: 'all',
|
|
690
|
+
value: '1',
|
|
691
|
+
new_child: '',
|
|
692
|
+
},
|
|
693
|
+
'1--1': {
|
|
694
|
+
type: 'Magento\\CatalogWidget\\Model\\Rule\\Condition\\Product',
|
|
695
|
+
attribute: 'sku',
|
|
696
|
+
operator: '()',
|
|
697
|
+
value: skus.join(','),
|
|
698
|
+
},
|
|
699
|
+
});
|
|
700
|
+
const encoded = encodeWidgetConditions(raw);
|
|
701
|
+
inner =
|
|
702
|
+
`{{widget type="Magento\\CatalogWidget\\Block\\Product\\ProductsList" ` +
|
|
703
|
+
`template="${template}" show_pager="0" products_count="${count}" ` +
|
|
704
|
+
`conditions_encoded="${encoded}" type_name="Catalog Products List"}}`;
|
|
705
|
+
} else if (node.category_id) {
|
|
706
|
+
const raw = JSON.stringify({
|
|
707
|
+
'1': {
|
|
708
|
+
type: 'Magento\\CatalogWidget\\Model\\Rule\\Condition\\Combine',
|
|
709
|
+
aggregator: 'all',
|
|
710
|
+
value: '1',
|
|
711
|
+
new_child: '',
|
|
712
|
+
},
|
|
713
|
+
'1--1': {
|
|
714
|
+
type: 'Magento\\CatalogWidget\\Model\\Rule\\Condition\\Product',
|
|
715
|
+
attribute: 'category_ids',
|
|
716
|
+
operator: '==',
|
|
717
|
+
value: String(node.category_id),
|
|
718
|
+
},
|
|
719
|
+
});
|
|
720
|
+
const encoded = encodeWidgetConditions(raw);
|
|
721
|
+
inner =
|
|
722
|
+
`{{widget type="Magento\\CatalogWidget\\Block\\Product\\ProductsList" ` +
|
|
723
|
+
`template="${template}" show_pager="0" products_count="${count}" ` +
|
|
724
|
+
`conditions_encoded="${encoded}" type_name="Catalog Products List"}}`;
|
|
725
|
+
} else {
|
|
726
|
+
inner = '<!-- Pass skus[], category_id, or widget_html — or configure Products in Admin -->';
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
return (
|
|
731
|
+
`<div${css} data-content-type="products" data-appearance="${escapeHtml(appearance)}" ` +
|
|
732
|
+
`data-element="main" data-pb-style="${styleId}">${inner}</div>`
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/** Magento \Magento\Widget\Helper\Conditions-style encoding for widget directives. */
|
|
737
|
+
function encodeWidgetConditions(json) {
|
|
738
|
+
return String(json)
|
|
739
|
+
.replace(/\[/g, '^[')
|
|
740
|
+
.replace(/\]/g, '^]')
|
|
741
|
+
.replace(/{/g, '`{')
|
|
742
|
+
.replace(/}/g, '`}')
|
|
743
|
+
.replace(/"/g, '``')
|
|
744
|
+
.replace(/\\/g, '^\\')
|
|
745
|
+
.replace(/</g, '^<')
|
|
746
|
+
.replace(/>/g, '^>');
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
function composeBlock(node, bag) {
|
|
751
|
+
const styleId = bag.add('border-style:none');
|
|
752
|
+
const css = classAttr(node.css_classes);
|
|
753
|
+
let inner = String(node.html || node.widget_html || '').trim();
|
|
754
|
+
if (!inner) {
|
|
755
|
+
const blockId = node.block_id || node.identifier;
|
|
756
|
+
if (!blockId) throw new Error('block requires block_id or identifier (or html)');
|
|
757
|
+
inner =
|
|
758
|
+
`{{widget type="Magento\\Cms\\Block\\Widget\\Block" ` +
|
|
759
|
+
`template="widget/static_block/default.phtml" ` +
|
|
760
|
+
`block_id="${escapeHtml(String(blockId))}" type_name="CMS Static Block"}}`;
|
|
761
|
+
}
|
|
762
|
+
return (
|
|
763
|
+
`<div${css} data-content-type="block" data-appearance="default" data-element="main" ` +
|
|
764
|
+
`data-pb-style="${styleId}">${inner}</div>`
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/** Machine-readable catalog for list_section_types / agents. */
|
|
769
|
+
export function describeElements() {
|
|
770
|
+
return [
|
|
771
|
+
{ type: 'row', params: ['appearance=contained|full-width|full-bleed', 'css_classes?', 'children[]'], notes: 'full-bleed only when explicitly requested; then allow_full_bleed=true on save' },
|
|
772
|
+
{ type: 'columns', params: ['columns[{width?, css_classes?, children[]}]', 'css_classes?'], notes: 'Convenience for column-group + column-line + equal widths' },
|
|
773
|
+
{ type: 'column-group', params: ['children[] (column-line or columns)', 'css_classes?'] },
|
|
774
|
+
{ type: 'column', params: ['width%', 'css_classes?', 'children[]'] },
|
|
775
|
+
{ type: 'text', params: ['html|content|text', 'css_classes?'] },
|
|
776
|
+
{ type: 'heading', params: ['text|html', 'heading_type=h1..h6', 'css_classes?'] },
|
|
777
|
+
{ type: 'image', params: ['src', 'alt?', 'mobile_src?', 'link_url?', 'caption?', 'css_classes?'] },
|
|
778
|
+
{ type: 'buttons', params: ['items[{text,url,button_type?}]', 'appearance?', 'css_classes?'] },
|
|
779
|
+
{ type: 'banner', params: ['background_image', 'content_html', 'show_button?', 'button_text?', 'link_url?', 'min_height?', 'css_classes?'], notes: 'Prefer native Banner button over separate Buttons in hero' },
|
|
780
|
+
{ type: 'html', params: ['html', 'css_classes?'], notes: 'Use sparingly; prefer native types' },
|
|
781
|
+
{ type: 'divider', params: ['line_color?', 'line_thickness?', 'line_width?', 'css_classes?'] },
|
|
782
|
+
{ type: 'video', params: ['src', 'css_classes?'] },
|
|
783
|
+
{ type: 'map', params: ['locations[]|{latitude,longitude}', 'height?', 'css_classes?'] },
|
|
784
|
+
{ type: 'tabs', params: ['tabs[{name, children[]}]', 'default_active?', 'css_classes?'] },
|
|
785
|
+
{ type: 'slider', params: ['slides[{background_image, content_html, button_text?, link_url?}]', 'autoplay?', 'css_classes?'] },
|
|
786
|
+
{ type: 'products', params: ['skus[]|category_id|html', 'appearance=grid|carousel', 'products_count?', 'css_classes?'] },
|
|
787
|
+
{ type: 'block', params: ['block_id|identifier|html', 'css_classes?'] },
|
|
788
|
+
];
|
|
789
|
+
}
|