cf-pagetree-parser 1.0.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.
@@ -0,0 +1,788 @@
1
+ /**
2
+ * ============================================================================
3
+ * PAGETREE PARSER - Layout Parsers
4
+ * ============================================================================
5
+ *
6
+ * ContentNode, SectionContainer, RowContainer, ColContainer, FlexContainer
7
+ *
8
+ * ============================================================================
9
+ */
10
+
11
+ import {
12
+ generateId,
13
+ generateFractionalIndex,
14
+ parseInlineStyle,
15
+ parseValueWithUnit,
16
+ parseAnimationAttrs,
17
+ } from '../utils.js';
18
+
19
+ import {
20
+ parseBackground,
21
+ backgroundToParams,
22
+ parseBorder,
23
+ borderToParams,
24
+ parseShadow,
25
+ shadowToParams,
26
+ parseBorderRadius,
27
+ parseSpacing,
28
+ spacingToAttrsAndParams,
29
+ parseFlexDirection,
30
+ parseJustifyContent,
31
+ parseAlignItems,
32
+ } from '../styles.js';
33
+
34
+ /**
35
+ * Parse ContentNode (root container)
36
+ */
37
+ export function parseContentNode(element, parseChildren) {
38
+ const styles = parseInlineStyle(element.getAttribute('style') || '');
39
+ const background = parseBackground(styles);
40
+ const overlay = element.getAttribute('data-overlay');
41
+ const bgStyleClass = element.getAttribute('data-bg-style') || 'bgCoverCenter';
42
+
43
+ const node = {
44
+ type: 'ContentNode',
45
+ id: '',
46
+ version: 0,
47
+ params: {
48
+ ...backgroundToParams(background),
49
+ '--style-foreground-color': overlay || '',
50
+ },
51
+ attrs: {
52
+ style: {
53
+ display: 'block',
54
+ 'background-position': 'center !important',
55
+ },
56
+ 'data-skip-background-settings': background.color || background.imageUrl || background.gradient ? 'false' : 'true',
57
+ className: bgStyleClass,
58
+ },
59
+ children: [],
60
+ };
61
+
62
+ // Parse children (sections) - skip the overlay div and content wrapper
63
+ const sectionChildren = element.querySelectorAll(':scope > div > section, :scope > div > [data-type="SectionContainer/V1"], :scope > section, :scope > [data-type="SectionContainer/V1"]');
64
+ sectionChildren.forEach((child, index) => {
65
+ const childNode = parseChildren(child, '', index);
66
+ if (childNode) {
67
+ node.children.push(childNode);
68
+ }
69
+ });
70
+
71
+ return node;
72
+ }
73
+
74
+ /**
75
+ * Parse SectionContainer
76
+ */
77
+ export function parseSectionContainer(element, parentId, index, parseChildren) {
78
+ const id = generateId();
79
+ const elementId = element.getAttribute('id'); // Custom element ID for scroll-to/show-hide
80
+ const styles = parseInlineStyle(element.getAttribute('style') || '');
81
+ const background = parseBackground(styles);
82
+ const border = parseBorder(styles);
83
+ const shadow = parseShadow(styles['box-shadow']);
84
+ const borderRadius = parseBorderRadius(styles);
85
+ const spacing = parseSpacing(styles);
86
+ const rowSpacingWithDefaults = {
87
+ paddingTop: spacing.paddingTop || parseValueWithUnit('0px'),
88
+ paddingBottom: spacing.paddingBottom || parseValueWithUnit('0px'),
89
+ paddingHorizontal: spacing.paddingHorizontal || parseValueWithUnit('0px'),
90
+ marginTop: spacing.marginTop || parseValueWithUnit('0px'),
91
+ };
92
+ const overlay = element.getAttribute('data-overlay');
93
+ const paintColors = element.getAttribute('data-paint-colors');
94
+
95
+ // Video background attributes
96
+ const videoBgUrl = element.getAttribute('data-video-bg-url');
97
+ const videoBgType = element.getAttribute('data-video-bg-type');
98
+ const videoBgHideMobile = element.getAttribute('data-video-bg-hide-mobile');
99
+ const videoBgOverlay = element.getAttribute('data-video-bg-overlay');
100
+
101
+ // Determine container width class
102
+ const maxWidth = styles['max-width'] || '1170px';
103
+ let containerClass = 'wideContainer';
104
+ if (maxWidth.includes('550') || maxWidth.includes('small')) containerClass = 'smallContainer';
105
+ else if (maxWidth.includes('720') || maxWidth.includes('mid')) containerClass = 'midContainer';
106
+ else if (maxWidth.includes('960') || maxWidth.includes('midWide')) containerClass = 'midWideContainer';
107
+ else if (maxWidth.includes('100%') || maxWidth.includes('full')) containerClass = 'fullContainer';
108
+
109
+ // Add background style class when there's a background image
110
+ const bgStyleClass = element.getAttribute('data-bg-style');
111
+ if (background.imageUrl) {
112
+ containerClass += ' ' + (bgStyleClass || 'bgCoverCenter');
113
+ }
114
+
115
+ const node = {
116
+ type: 'SectionContainer/V1',
117
+ id,
118
+ version: 0,
119
+ parentId,
120
+ fractionalIndex: generateFractionalIndex(index),
121
+ attrs: {
122
+ className: containerClass,
123
+ 'data-skip-background-settings': background.color || background.imageUrl || background.gradient ? 'false' : 'true',
124
+ 'data-skip-shadow-settings': shadow ? 'false' : 'true',
125
+ 'data-skip-corners-settings': borderRadius ? 'false' : 'true',
126
+ style: {},
127
+ },
128
+ params: {},
129
+ children: [],
130
+ };
131
+
132
+ // Add custom element ID for scroll-to/show-hide targeting
133
+ if (elementId) {
134
+ node.attrs.id = elementId;
135
+ }
136
+
137
+ // Add paint colors if present
138
+ if (paintColors) {
139
+ node.attrs['data-paint-colors'] = paintColors;
140
+ }
141
+
142
+ // Apply spacing
143
+ const { attrs: spacingAttrs, params: spacingParams } = spacingToAttrsAndParams(rowSpacingWithDefaults);
144
+ Object.assign(node.attrs.style, spacingAttrs.style);
145
+ Object.assign(node.params, spacingParams);
146
+
147
+ // Apply background (always include params for ClickFunnels compatibility)
148
+ Object.assign(node.params, backgroundToParams(background));
149
+
150
+ // Apply overlay
151
+ if (overlay) {
152
+ node.params['--style-foreground-color'] = overlay;
153
+ }
154
+
155
+ // Apply video background params
156
+ if (videoBgUrl && videoBgType === 'youtube') {
157
+ // Required data attributes for video background
158
+ node.attrs['data-skip-background-settings'] = 'false';
159
+ node.attrs['data-skip-background-video-settings'] = 'false';
160
+
161
+ node.params['video-bg-url'] = videoBgUrl;
162
+ node.params['video-bg-type'] = 'youtube';
163
+ node.params['video-bg-thumbnail-background'] = false;
164
+ node.params['video-bg-use-background-as-overlay'] = true;
165
+ node.params['video-bg-hide-on-mobile'] = videoBgHideMobile === 'true';
166
+
167
+ // Always use offset style with 50% vertical offset
168
+ node.params['video-bg-style-type'] = 'offset';
169
+ node.params['video-bg-offset-y'] = 50;
170
+
171
+ // Always set background-image-url to empty string (required by ClickFunnels)
172
+ node.params['--style-background-image-url'] = '';
173
+
174
+ // Set overlay color - must be rgba for video background
175
+ if (videoBgOverlay) {
176
+ node.params['--style-background-color'] = videoBgOverlay;
177
+ }
178
+ }
179
+
180
+ // Apply border
181
+ if (border.width || border.style || border.color) {
182
+ Object.assign(node.params, borderToParams(border));
183
+ }
184
+
185
+ // Apply shadow
186
+ if (shadow) {
187
+ Object.assign(node.params, shadowToParams(shadow));
188
+ }
189
+
190
+ // Apply border-radius and separate corners
191
+ const separateCorners = element.getAttribute('data-separate-corners') === 'true';
192
+ if (separateCorners) {
193
+ node.params['separate-corners'] = true;
194
+ // Parse individual corner radii
195
+ if (styles['border-top-left-radius']) {
196
+ const tl = parseValueWithUnit(styles['border-top-left-radius']);
197
+ if (tl) {
198
+ node.attrs.style['border-top-left-radius'] = tl.value;
199
+ node.params['border-top-left-radius--unit'] = tl.unit;
200
+ }
201
+ }
202
+ if (styles['border-top-right-radius']) {
203
+ const tr = parseValueWithUnit(styles['border-top-right-radius']);
204
+ if (tr) {
205
+ node.attrs.style['border-top-right-radius'] = tr.value;
206
+ node.params['border-top-right-radius--unit'] = tr.unit;
207
+ }
208
+ }
209
+ if (styles['border-bottom-left-radius']) {
210
+ const bl = parseValueWithUnit(styles['border-bottom-left-radius']);
211
+ if (bl) {
212
+ node.attrs.style['border-bottom-left-radius'] = bl.value;
213
+ node.params['border-bottom-left-radius--unit'] = bl.unit;
214
+ }
215
+ }
216
+ if (styles['border-bottom-right-radius']) {
217
+ const br = parseValueWithUnit(styles['border-bottom-right-radius']);
218
+ if (br) {
219
+ node.attrs.style['border-bottom-right-radius'] = br.value;
220
+ node.params['border-bottom-right-radius--unit'] = br.unit;
221
+ }
222
+ }
223
+ // Also set the base border-radius if present
224
+ if (borderRadius) {
225
+ node.attrs.style['border-radius'] = borderRadius.value;
226
+ node.params['border-radius--unit'] = borderRadius.unit;
227
+ }
228
+ } else if (borderRadius) {
229
+ node.attrs.style['border-radius'] = borderRadius.value;
230
+ node.params['border-radius--unit'] = borderRadius.unit;
231
+ }
232
+
233
+ // Check for visibility
234
+ const showOnly = element.getAttribute('data-show');
235
+ if (showOnly) {
236
+ node.attrs['data-show-only'] = showOnly;
237
+ }
238
+
239
+ // Parse children (rows) - skip the overlay div and content wrapper
240
+ const rowChildren = element.querySelectorAll(':scope > div > [data-type="RowContainer/V1"], :scope > [data-type="RowContainer/V1"]');
241
+ rowChildren.forEach((child, childIndex) => {
242
+ const childNode = parseChildren(child, id, childIndex);
243
+ if (childNode) {
244
+ node.children.push(childNode);
245
+ }
246
+ });
247
+
248
+ return node;
249
+ }
250
+
251
+ /**
252
+ * Parse RowContainer
253
+ */
254
+ export function parseRowContainer(element, parentId, index, parseChildren) {
255
+ const id = generateId();
256
+ const elementId = element.getAttribute('id'); // Custom element ID for scroll-to/show-hide
257
+ const styles = parseInlineStyle(element.getAttribute('style') || '');
258
+ const background = parseBackground(styles);
259
+ const border = parseBorder(styles);
260
+ const shadow = parseShadow(styles['box-shadow']);
261
+ const borderRadius = parseBorderRadius(styles);
262
+ const spacing = parseSpacing(styles);
263
+ const spacingWithDefaults = {
264
+ paddingTop: spacing.paddingTop || parseValueWithUnit('0px'),
265
+ paddingBottom: spacing.paddingBottom || parseValueWithUnit('0px'),
266
+ paddingHorizontal: spacing.paddingHorizontal || parseValueWithUnit('0px'),
267
+ marginTop: spacing.marginTop || parseValueWithUnit('0px'),
268
+ };
269
+ const overlay = element.getAttribute('data-overlay');
270
+ const paintColors = element.getAttribute('data-paint-colors');
271
+
272
+ const width = parseValueWithUnit(styles.width || '1170px');
273
+
274
+ // Determine className - add bg style class when there's a background image
275
+ const bgStyleClass = element.getAttribute('data-bg-style');
276
+ let className = '';
277
+ if (background.imageUrl) {
278
+ className = bgStyleClass || 'bgCoverCenter';
279
+ }
280
+
281
+ // Parse z-index if present
282
+ const zIndex = styles['z-index'] ? parseInt(styles['z-index'], 10) : null;
283
+
284
+ // Parse animation attributes
285
+ const { attrs: animationAttrs, params: animationParams } = parseAnimationAttrs(element);
286
+
287
+ const node = {
288
+ type: 'RowContainer/V1',
289
+ id,
290
+ version: 0,
291
+ parentId,
292
+ fractionalIndex: generateFractionalIndex(index),
293
+ attrs: {
294
+ 'data-skip-background-settings': background.color || background.imageUrl || background.gradient ? 'false' : 'true',
295
+ 'data-skip-shadow-settings': shadow ? 'false' : 'true',
296
+ 'data-skip-corners-settings': borderRadius ? 'false' : 'true',
297
+ ...animationAttrs,
298
+ style: {
299
+ width: width ? width.value : 1170,
300
+ },
301
+ },
302
+ params: {
303
+ 'width--unit': width ? width.unit : 'px',
304
+ ...animationParams,
305
+ },
306
+ selectors: {
307
+ '.col-inner': {
308
+ params: { 'height--unit': '%' },
309
+ attrs: { style: { height: 'auto' } },
310
+ },
311
+ },
312
+ children: [],
313
+ };
314
+
315
+ // Apply z-index if present
316
+ if (zIndex !== null && !isNaN(zIndex)) {
317
+ node.attrs.style['z-index'] = zIndex;
318
+ }
319
+
320
+ // Add custom element ID for scroll-to/show-hide targeting
321
+ if (elementId) {
322
+ node.attrs.id = elementId;
323
+ }
324
+
325
+ // Add paint colors if present
326
+ if (paintColors) {
327
+ node.attrs['data-paint-colors'] = paintColors;
328
+ }
329
+
330
+ // Add className if set
331
+ if (className) {
332
+ node.attrs.className = className;
333
+ }
334
+
335
+ // Apply spacing
336
+ const { attrs: spacingAttrs, params: spacingParams } = spacingToAttrsAndParams(spacingWithDefaults);
337
+ Object.assign(node.attrs.style, spacingAttrs.style);
338
+ Object.assign(node.params, spacingParams);
339
+
340
+ // Apply background (always include params for ClickFunnels compatibility)
341
+ Object.assign(node.params, backgroundToParams(background));
342
+
343
+ // Apply overlay
344
+ if (overlay) {
345
+ node.params['--style-foreground-color'] = overlay;
346
+ }
347
+
348
+ // Apply border
349
+ if (border.width || border.style || border.color) {
350
+ Object.assign(node.params, borderToParams(border));
351
+ }
352
+
353
+ // Apply shadow
354
+ if (shadow) {
355
+ Object.assign(node.params, shadowToParams(shadow));
356
+ }
357
+
358
+ // Apply border-radius and separate corners
359
+ const rowSeparateCorners = element.getAttribute('data-separate-corners') === 'true';
360
+ if (rowSeparateCorners) {
361
+ node.params['separate-corners'] = true;
362
+ // Parse individual corner radii
363
+ if (styles['border-top-left-radius']) {
364
+ const tl = parseValueWithUnit(styles['border-top-left-radius']);
365
+ if (tl) {
366
+ node.attrs.style['border-top-left-radius'] = tl.value;
367
+ node.params['border-top-left-radius--unit'] = tl.unit;
368
+ }
369
+ }
370
+ if (styles['border-top-right-radius']) {
371
+ const tr = parseValueWithUnit(styles['border-top-right-radius']);
372
+ if (tr) {
373
+ node.attrs.style['border-top-right-radius'] = tr.value;
374
+ node.params['border-top-right-radius--unit'] = tr.unit;
375
+ }
376
+ }
377
+ if (styles['border-bottom-left-radius']) {
378
+ const bl = parseValueWithUnit(styles['border-bottom-left-radius']);
379
+ if (bl) {
380
+ node.attrs.style['border-bottom-left-radius'] = bl.value;
381
+ node.params['border-bottom-left-radius--unit'] = bl.unit;
382
+ }
383
+ }
384
+ if (styles['border-bottom-right-radius']) {
385
+ const br = parseValueWithUnit(styles['border-bottom-right-radius']);
386
+ if (br) {
387
+ node.attrs.style['border-bottom-right-radius'] = br.value;
388
+ node.params['border-bottom-right-radius--unit'] = br.unit;
389
+ }
390
+ }
391
+ // Also set the base border-radius if present
392
+ if (borderRadius) {
393
+ node.attrs.style['border-radius'] = borderRadius.value;
394
+ node.params['border-radius--unit'] = borderRadius.unit;
395
+ }
396
+ } else if (borderRadius) {
397
+ node.attrs.style['border-radius'] = borderRadius.value;
398
+ node.params['border-radius--unit'] = borderRadius.unit;
399
+ }
400
+
401
+ // Parse children (columns) - skip the overlay div and content wrapper
402
+ let colChildren = element.querySelectorAll(':scope > [data-type="ColContainer/V1"]');
403
+ // If columns are nested in a z-index wrapper (overlay case), find them there
404
+ if (colChildren.length === 0) {
405
+ colChildren = element.querySelectorAll(':scope > div > [data-type="ColContainer/V1"]');
406
+ }
407
+ colChildren.forEach((child, childIndex) => {
408
+ const childNode = parseChildren(child, id, childIndex);
409
+ if (childNode) {
410
+ node.children.push(childNode);
411
+ }
412
+ });
413
+
414
+ return node;
415
+ }
416
+
417
+ /**
418
+ * Parse ColContainer
419
+ */
420
+ export function parseColContainer(element, parentId, index, parseChildren) {
421
+ const id = generateId();
422
+ const elementId = element.getAttribute('id'); // Custom element ID for scroll-to/show-hide
423
+ const styles = parseInlineStyle(element.getAttribute('style') || '');
424
+
425
+ // Calculate column span from width percentage
426
+ const widthStr = styles.width || '100%';
427
+ const widthPercent = parseFloat(widthStr);
428
+ const mdNum = Math.round((widthPercent / 100) * 12) || 12;
429
+
430
+ // Get column direction from data attribute
431
+ const colDirection = element.getAttribute('data-col-direction') || 'left';
432
+
433
+ const node = {
434
+ type: 'ColContainer/V1',
435
+ id,
436
+ version: 0,
437
+ parentId,
438
+ fractionalIndex: generateFractionalIndex(index),
439
+ attrs: {},
440
+ params: {
441
+ mdNum,
442
+ colDirection,
443
+ },
444
+ selectors: {
445
+ '& > .col-inner': {
446
+ params: {},
447
+ attrs: {
448
+ style: {},
449
+ },
450
+ },
451
+ '.col-inner': {},
452
+ },
453
+ children: [],
454
+ };
455
+
456
+ // Add custom element ID for scroll-to/show-hide targeting
457
+ if (elementId) {
458
+ node.attrs.id = elementId;
459
+ }
460
+
461
+ // Find the col-inner element
462
+ const colInner = element.querySelector(':scope > .col-inner');
463
+
464
+ if (colInner) {
465
+ const innerStyles = parseInlineStyle(colInner.getAttribute('style') || '');
466
+ const background = parseBackground(innerStyles);
467
+ const border = parseBorder(innerStyles);
468
+ const shadow = parseShadow(innerStyles['box-shadow']);
469
+ const spacing = parseSpacing(innerStyles);
470
+ const overlay = colInner.getAttribute('data-overlay');
471
+ const separateCorners = colInner.getAttribute('data-separate-corners') === 'true';
472
+ const borderRadius = parseBorderRadius(innerStyles);
473
+ const paintColors = colInner.getAttribute('data-paint-colors');
474
+
475
+ const colInnerSelector = node.selectors['& > .col-inner'];
476
+
477
+ // Check if there's any actual styling on the col-inner
478
+ const hasMargin = innerStyles['margin-left'] || innerStyles['margin-right'];
479
+ const hasBackground = background.color || background.imageUrl || background.gradient;
480
+ const hasBorder = border.width || border.style || border.color;
481
+
482
+ // Spacing - add values if explicitly set
483
+ if (spacing.paddingTop) {
484
+ colInnerSelector.attrs.style['padding-top'] = spacing.paddingTop.value;
485
+ colInnerSelector.params['padding-top--unit'] = spacing.paddingTop.unit;
486
+ }
487
+
488
+ if (spacing.paddingBottom) {
489
+ colInnerSelector.attrs.style['padding-bottom'] = spacing.paddingBottom.value;
490
+ colInnerSelector.params['padding-bottom--unit'] = spacing.paddingBottom.unit;
491
+ }
492
+
493
+ if (spacing.paddingHorizontal) {
494
+ colInnerSelector.params['--style-padding-horizontal'] = spacing.paddingHorizontal.value;
495
+ colInnerSelector.params['--style-padding-horizontal--unit'] = spacing.paddingHorizontal.unit;
496
+ }
497
+
498
+ // Margin horizontal
499
+ if (hasMargin) {
500
+ const mx = parseValueWithUnit(innerStyles['margin-left'] || innerStyles['margin-right']);
501
+ if (mx) {
502
+ colInnerSelector.params['--style-margin-horizontal'] = mx.value;
503
+ colInnerSelector.params['--style-margin-horizontal--unit'] = mx.unit;
504
+ }
505
+ }
506
+
507
+ // Background (gradient takes priority, then color)
508
+ if (background.gradient) {
509
+ colInnerSelector.params['--style-background-color'] = background.gradient;
510
+ } else if (background.color) {
511
+ colInnerSelector.params['--style-background-color'] = background.color;
512
+ }
513
+
514
+ // Always include background-image-url param (empty string if no image)
515
+ // This is required for ClickFunnels to recognize background settings
516
+ colInnerSelector.params['--style-background-image-url'] = background.imageUrl || '';
517
+
518
+ if (background.imageUrl) {
519
+ // Add bg style class when there's a background image
520
+ const colInnerBgStyle = colInner.getAttribute('data-bg-style');
521
+ colInnerSelector.attrs.className = colInnerBgStyle || 'bgCoverCenter';
522
+ }
523
+
524
+ // Overlay (foreground color)
525
+ if (overlay) {
526
+ colInnerSelector.params['--style-foreground-color'] = overlay;
527
+ }
528
+
529
+ // Border
530
+ if (hasBorder) {
531
+ Object.assign(colInnerSelector.params, borderToParams(border));
532
+ }
533
+
534
+ // Shadow
535
+ if (shadow) {
536
+ Object.assign(colInnerSelector.params, shadowToParams(shadow));
537
+ colInnerSelector.attrs['data-skip-shadow-settings'] = 'false';
538
+ } else {
539
+ colInnerSelector.attrs['data-skip-shadow-settings'] = 'true';
540
+ }
541
+
542
+ // Border radius
543
+ if (borderRadius) {
544
+ colInnerSelector.attrs.style['border-radius'] = borderRadius.value;
545
+ colInnerSelector.params['border-radius--unit'] = borderRadius.unit;
546
+ colInnerSelector.attrs['data-skip-corners-settings'] = 'false';
547
+ } else {
548
+ colInnerSelector.attrs['data-skip-corners-settings'] = 'true';
549
+ }
550
+
551
+ // Individual corner radii
552
+ if (separateCorners) {
553
+ colInnerSelector.params['separate-corners'] = true;
554
+ if (innerStyles['border-top-left-radius']) {
555
+ const tl = parseValueWithUnit(innerStyles['border-top-left-radius']);
556
+ if (tl) {
557
+ colInnerSelector.attrs.style['border-top-left-radius'] = tl.value;
558
+ colInnerSelector.params['border-top-left-radius--unit'] = tl.unit;
559
+ }
560
+ }
561
+ if (innerStyles['border-top-right-radius']) {
562
+ const tr = parseValueWithUnit(innerStyles['border-top-right-radius']);
563
+ if (tr) {
564
+ colInnerSelector.attrs.style['border-top-right-radius'] = tr.value;
565
+ colInnerSelector.params['border-top-right-radius--unit'] = tr.unit;
566
+ }
567
+ }
568
+ if (innerStyles['border-bottom-left-radius']) {
569
+ const bl = parseValueWithUnit(innerStyles['border-bottom-left-radius']);
570
+ if (bl) {
571
+ colInnerSelector.attrs.style['border-bottom-left-radius'] = bl.value;
572
+ colInnerSelector.params['border-bottom-left-radius--unit'] = bl.unit;
573
+ }
574
+ }
575
+ if (innerStyles['border-bottom-right-radius']) {
576
+ const br = parseValueWithUnit(innerStyles['border-bottom-right-radius']);
577
+ if (br) {
578
+ colInnerSelector.attrs.style['border-bottom-right-radius'] = br.value;
579
+ colInnerSelector.params['border-bottom-right-radius--unit'] = br.unit;
580
+ }
581
+ }
582
+ colInnerSelector.attrs['data-skip-corners-settings'] = 'false';
583
+ }
584
+
585
+ // Background settings flag
586
+ colInnerSelector.attrs['data-skip-background-settings'] =
587
+ (hasBackground || overlay) ? 'false' : 'true';
588
+
589
+ // Add paint colors if present
590
+ if (paintColors) {
591
+ colInnerSelector.attrs['data-paint-colors'] = paintColors;
592
+ }
593
+
594
+ // Parse children from col-inner, skipping overlay and content wrapper
595
+ let childIdx = 0;
596
+ const parseColInnerChildren = (container) => {
597
+ Array.from(container.children).forEach((child) => {
598
+ // Skip overlay divs
599
+ if (child.classList && child.classList.contains('cf-overlay')) {
600
+ return;
601
+ }
602
+ // If content wrapper (z-index div), dive into it
603
+ const childStyle = child.getAttribute('style') || '';
604
+ if (!child.getAttribute('data-type') && childStyle.includes('z-index') && child.children.length > 0) {
605
+ parseColInnerChildren(child);
606
+ } else {
607
+ const childNode = parseChildren(child, id, childIdx);
608
+ if (childNode) {
609
+ node.children.push(childNode);
610
+ childIdx++;
611
+ }
612
+ }
613
+ });
614
+ };
615
+ parseColInnerChildren(colInner);
616
+ } else {
617
+ // No col-inner found, parse direct children
618
+ let childIdx = 0;
619
+ Array.from(element.children).forEach((child) => {
620
+ if (child.classList && child.classList.contains('cf-overlay')) {
621
+ return;
622
+ }
623
+ const childNode = parseChildren(child, id, childIdx);
624
+ if (childNode) {
625
+ node.children.push(childNode);
626
+ childIdx++;
627
+ }
628
+ });
629
+ }
630
+
631
+ return node;
632
+ }
633
+
634
+ /**
635
+ * Parse FlexContainer
636
+ */
637
+ export function parseFlexContainer(element, parentId, index, parseChildren) {
638
+ const id = generateId();
639
+ const elementId = element.getAttribute('id'); // Custom element ID for scroll-to/show-hide
640
+ const styles = parseInlineStyle(element.getAttribute('style') || '');
641
+ const background = parseBackground(styles);
642
+ const border = parseBorder(styles);
643
+ const shadow = parseShadow(styles['box-shadow']);
644
+ const borderRadius = parseBorderRadius(styles);
645
+ const spacing = parseSpacing(styles);
646
+ const flexSpacingWithDefaults = {
647
+ paddingTop: spacing.paddingTop || parseValueWithUnit('0px'),
648
+ paddingBottom: spacing.paddingBottom || parseValueWithUnit('0px'),
649
+ paddingHorizontal: spacing.paddingHorizontal || parseValueWithUnit('0px'),
650
+ marginTop: spacing.marginTop || parseValueWithUnit('0px'),
651
+ };
652
+ const overlay = element.getAttribute('data-overlay');
653
+ const paintColors = element.getAttribute('data-paint-colors');
654
+
655
+ const width = parseValueWithUnit(styles.width || '100%', '%');
656
+ const height = styles.height ? parseValueWithUnit(styles.height, 'px') : null;
657
+ const gap = parseValueWithUnit(styles.gap || '1.5em', 'em');
658
+
659
+ // Determine className - add bg style class when there's a background image
660
+ const bgStyleClass = element.getAttribute('data-bg-style');
661
+ let className = 'elFlexWrap elFlexNoWrapMobile';
662
+ if (background.imageUrl) {
663
+ className += ' ' + (bgStyleClass || 'bgCoverCenter');
664
+ }
665
+
666
+ const node = {
667
+ type: 'FlexContainer/V1',
668
+ id,
669
+ version: 0,
670
+ parentId,
671
+ fractionalIndex: generateFractionalIndex(index),
672
+ attrs: {
673
+ className,
674
+ 'data-skip-background-settings': background.color || background.imageUrl || background.gradient ? 'false' : 'true',
675
+ 'data-skip-shadow-settings': shadow ? 'false' : 'true',
676
+ 'data-skip-corners-settings': borderRadius ? 'false' : 'true',
677
+ style: {
678
+ 'flex-direction': parseFlexDirection(styles['flex-direction']),
679
+ 'justify-content': parseJustifyContent(styles['justify-content']),
680
+ 'align-items': parseAlignItems(styles['align-items']),
681
+ gap: gap ? gap.value : 0,
682
+ },
683
+ },
684
+ params: {
685
+ 'gap--unit': gap ? gap.unit : 'em',
686
+ },
687
+ children: [],
688
+ };
689
+
690
+ // Add custom element ID for scroll-to/show-hide targeting
691
+ if (elementId) {
692
+ node.attrs.id = elementId;
693
+ }
694
+
695
+ // Add paint colors if present
696
+ if (paintColors) {
697
+ node.attrs['data-paint-colors'] = paintColors;
698
+ }
699
+
700
+ // Add width
701
+ node.attrs.style.width = width.value;
702
+ node.params['width--unit'] = width.unit;
703
+
704
+ // Add height if specified
705
+ if (height) {
706
+ node.attrs.style.height = height.value;
707
+ node.params['height--unit'] = height.unit;
708
+ }
709
+
710
+ // Apply spacing
711
+ const { attrs: spacingAttrs, params: spacingParams } = spacingToAttrsAndParams(flexSpacingWithDefaults);
712
+ Object.assign(node.attrs.style, spacingAttrs.style);
713
+ Object.assign(node.params, spacingParams);
714
+
715
+ // Apply background (includes gradient, color, and imageUrl)
716
+ if (background.color || background.imageUrl || background.gradient) {
717
+ Object.assign(node.params, backgroundToParams(background));
718
+ }
719
+
720
+ // Apply overlay
721
+ if (overlay) {
722
+ node.params['--style-foreground-color'] = overlay;
723
+ }
724
+
725
+ // Apply border
726
+ if (border.width || border.style || border.color) {
727
+ Object.assign(node.params, borderToParams(border));
728
+ }
729
+
730
+ // Apply shadow
731
+ if (shadow) {
732
+ Object.assign(node.params, shadowToParams(shadow));
733
+ }
734
+
735
+ // Apply border-radius
736
+ if (borderRadius) {
737
+ node.attrs.style['border-radius'] = borderRadius.value;
738
+ node.params['border-radius--unit'] = borderRadius.unit;
739
+ }
740
+
741
+ // Handle flex-wrap - modify className without losing other classes
742
+ // Default className already includes 'elFlexWrap elFlexNoWrapMobile'
743
+ // If wrap is explicitly NOT set, remove elFlexWrap and keep elFlexNoWrapMobile
744
+ if (styles['flex-wrap'] !== 'wrap') {
745
+ node.attrs.className = node.attrs.className.replace('elFlexWrap ', '');
746
+ }
747
+
748
+ // Parse children - skip the overlay div (has class cf-overlay)
749
+ const children = element.children;
750
+ let actualIndex = 0;
751
+ Array.from(children).forEach((child) => {
752
+ // Skip overlay divs
753
+ if (child.classList && child.classList.contains('cf-overlay')) {
754
+ return;
755
+ }
756
+ const childNode = parseChildren(child, id, actualIndex);
757
+ if (childNode) {
758
+ // Set width: auto on flex children to prevent them from expanding to 100%
759
+ // This is critical for proper flex layout in ClickFunnels
760
+ if (!childNode.attrs) childNode.attrs = {};
761
+ if (!childNode.attrs.style) childNode.attrs.style = {};
762
+ if (!childNode.params) childNode.params = {};
763
+
764
+ // Only set width: auto if no explicit width is already set
765
+ // Skip FlexContainer children (they have their own width handling)
766
+ if (childNode.attrs.style.width === undefined && childNode.type !== 'FlexContainer/V1') {
767
+ childNode.attrs.style.width = 'auto';
768
+ }
769
+
770
+ node.children.push(childNode);
771
+ actualIndex++;
772
+ }
773
+ });
774
+
775
+ return node;
776
+ }
777
+
778
+ /**
779
+ * Parse ColInner - wrapper element inside ColContainer
780
+ * This is typically a transparent wrapper, so we just parse its children
781
+ * and return them without creating a node for ColInner itself
782
+ */
783
+ export function parseColInner(_element, _parentId, _index, _parseChildren) {
784
+ // ColInner is a wrapper element - we don't create a node for it,
785
+ // we just return null and let the parent (ColContainer) handle it
786
+ // The ColContainer parser already handles col-inner styling via selectors
787
+ return null;
788
+ }