PluginProbe
Booking Calendar / 11.9
Booking Calendar v11.9
11.9 11.8.4 11.8.3 11.8.2 11.8.1 11.8 11.7 11.6.1 11.6 11.5 11.4.3 11.4.2 11.4.1 11.4 11.3 11.2.1 11.2 11.1 11.0 10.15.7 10.15.6 10.1.3 10.10 10.10.1 10.10.2 All 205 releases
booking / includes / page-form-builder / _src / export / builder-exporter.js

builder-exporter.js in Booking Calendar 11.9, at includes/page-form-builder/_src/export/builder-exporter.js

1,248 lines 45.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * @file: ../includes/page-form-builder/_out/export/builder-exporter.js
3 */
4 (function () {
5 "use strict";
6
7 const core = window.WPBC_BFB_Core || {};
8
9 // == Helpers — Shared helper API for field packs ==================================================================
10 // =================================================================================================
11 // == These are generic utilities that packs can call from their own exporters:
12 // == - compute_name(), id_option(), class_options(), size_max_token(), emit_time_select(), etc.
13 // == No field-type branching should live in the core exporter.
14 // =================================================================================================
15
16 /**
17 * Default skip list (can be extended/overridden at runtime).
18 * - Only attribute NAMES here (case-insensitive). Values are removed with them.
19 */
20 const wpbc_export_skip_attrs_default = [ 'data-colstyles-active' ];
21
22 /**
23 * Remove attributes by name from an HTML-like string.
24 * Matches:
25 * - name
26 * - name="..."/name='...'/name=value
27 * with any surrounding whitespace.
28 *
29 * @param {string} html
30 * @param {string[]} attrs_lowercase attribute names (lowercase)
31 * @return {string}
32 */
33 function strip_attributes_from_markup(html, attrs_lowercase) {
34 if (!html || !attrs_lowercase?.length) return html;
35 let out = String(html);
36 for (const rawName of attrs_lowercase) {
37 if (!rawName) continue;
38 const name = String(rawName).toLowerCase().trim();
39 // Escape for regex
40 const esc = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
41 // Match full attribute name only (next char is NOT a valid name char)
42 const re = new RegExp(
43 `\\s${esc}(?![A-Za-z0-9_:\\-])(?:=(?:"[^"]*"|'[^']*'|[^\\s>]*))?`,
44 'gi'
45 );
46 out = out.replace(re, '');
47 }
48 return out;
49 }
50
51 // == Helpers – column styles parsing & CSS vars builder ===========================================================
52 var wpbc_column_overflow_values = [ 'visible', 'auto', 'hidden' ];
53
54 /**
55 * Constrain a column spacing length to units supported by the Inspector.
56 *
57 * @param {*} candidate_value Saved column-style value.
58 * @returns {string} Valid non-negative CSS length or `0px`.
59 */
60 function normalize_column_length( candidate_value ) {
61 var normalized_value = String( candidate_value == null ? '' : candidate_value ).trim();
62 if ( /^\d+(\.\d+)?$/.test( normalized_value ) ) {
63 return normalized_value + 'px';
64 }
65 return /^\d+(\.\d+)?(px|rem|em|%)$/.test( normalized_value ) ? normalized_value : '0px';
66 }
67
68 /**
69 * Constrain an optional column maximum dimension.
70 *
71 * @param {*} candidate_value Saved column-style value.
72 * @returns {string} Valid maximum dimension or `none`.
73 */
74 function normalize_column_maximum( candidate_value ) {
75 var normalized_value = String( candidate_value == null ? '' : candidate_value ).trim();
76 if ( ! normalized_value || 'none' === normalized_value ) {
77 return 'none';
78 }
79 if ( /^\d+(\.\d+)?$/.test( normalized_value ) ) {
80 return normalized_value + 'px';
81 }
82 return /^\d+(\.\d+)?(px|rem|em|%|vh|vw)$/.test( normalized_value ) ? normalized_value : 'none';
83 }
84
85 /**
86 * Constrain column overflow to the Inspector allowlist.
87 *
88 * @param {*} candidate_value Saved column-style value.
89 * @returns {string} Allowlisted overflow value.
90 */
91 function normalize_column_overflow( candidate_value ) {
92 var normalized_value = String( candidate_value || '' );
93 return wpbc_column_overflow_values.indexOf( normalized_value ) !== -1 ? normalized_value : wpbc_column_overflow_values[0];
94 }
95
96 // Known keys we treat as real per-column style overrides.
97 function has_non_default_col_styles(obj) {
98 if ( !obj || typeof obj !== 'object' ) {
99 return false;
100 }
101 var keys = [
102 'dir', 'wrap', 'jc', 'ai', 'gap', 'padding', 'margin',
103 'padding_top', 'padding_right', 'padding_bottom', 'padding_left',
104 'margin_top', 'margin_right', 'margin_bottom', 'margin_left',
105 'max_width', 'max_height', 'overflow', 'overflow_x', 'overflow_y', 'aself', 'ac'
106 ];
107 for ( var i = 0; i < keys.length; i++ ) {
108 var k = keys[i];
109 if ( obj[k] != null && String( obj[k] ).trim() !== '' ) {
110 return true;
111 }
112 }
113 return false;
114 }
115
116 /**
117 * Parse `col_styles` coming from a Section.
118 * Accepts: JSON string or array of objects.
119 *
120 * @param {string|Array|undefined|null} raw
121 * @returns {Array<Object>} array aligned to columns (may be empty)
122 */
123 function parse_col_styles_json(raw) {
124 if ( !raw ) return [];
125 if ( Array.isArray( raw ) ) return raw.filter( function (x) {
126 return x && typeof x === 'object';
127 } );
128
129 if ( typeof raw === 'string' ) {
130 try {
131 var arr = JSON.parse( raw );
132 return Array.isArray( arr ) ? arr.filter( function (x) {
133 return x && typeof x === 'object';
134 } ) : [];
135 } catch ( _e ) {
136 return [];
137 }
138 }
139 return [];
140 }
141
142 /**
143 * Build CSS variable declarations string for a column style object.
144 * Known keys -> CSS vars:
145 * - dir -> --wpbc-bfb-col-dir
146 * - wrap -> --wpbc-bfb-col-wrap
147 * - jc -> --wpbc-bfb-col-jc
148 * - ai -> --wpbc-bfb-col-ai
149 * - gap -> --wpbc-bfb-col-gap
150 * - padding -> --wpbc-bfb-col-padding
151 * - margin -> --wpbc-bfb-col-margin
152 * - padding_top/right/bottom/left -> --wpbc-bfb-col-padding-*
153 * - margin_top/right/bottom/left -> --wpbc-bfb-col-margin-*
154 * - max_width -> --wpbc-bfb-col-max-width
155 * - max_height -> --wpbc-bfb-col-max-height
156 * - overflow -> --wpbc-bfb-col-overflow
157 * - overflow_x -> --wpbc-bfb-col-overflow-x
158 * - overflow_y -> --wpbc-bfb-col-overflow-y
159 * - ac -> --wpbc-bfb-col-ac
160 * - aself-> --wpbc-bfb-col-aself
161 *
162 * Unknown keys are exported as `--wpbc-bfb-col-${key}`.
163 *
164 * @param {Object|null|undefined} obj
165 * @returns {string} e.g. "--wpbc-bfb-col-dir: row; --wpbc-bfb-col-wrap: wrap;"
166 */
167 function build_col_css_vars(obj) {
168 if ( !obj || typeof obj !== 'object' ) return '';
169
170 var map = {
171 dir : '--wpbc-bfb-col-dir',
172 wrap : '--wpbc-bfb-col-wrap',
173 jc : '--wpbc-bfb-col-jc',
174 ai : '--wpbc-bfb-col-ai',
175 gap : '--wpbc-bfb-col-gap',
176 padding: '--wpbc-bfb-col-padding',
177 margin : '--wpbc-bfb-col-margin',
178 padding_top : '--wpbc-bfb-col-padding-top',
179 padding_right : '--wpbc-bfb-col-padding-right',
180 padding_bottom: '--wpbc-bfb-col-padding-bottom',
181 padding_left : '--wpbc-bfb-col-padding-left',
182 margin_top : '--wpbc-bfb-col-margin-top',
183 margin_right : '--wpbc-bfb-col-margin-right',
184 margin_bottom : '--wpbc-bfb-col-margin-bottom',
185 margin_left : '--wpbc-bfb-col-margin-left',
186 max_width : '--wpbc-bfb-col-max-width',
187 max_height : '--wpbc-bfb-col-max-height',
188 overflow : '--wpbc-bfb-col-overflow',
189 overflow_x : '--wpbc-bfb-col-overflow-x',
190 overflow_y : '--wpbc-bfb-col-overflow-y',
191 ac : '--wpbc-bfb-col-ac',
192 aself: '--wpbc-bfb-col-aself'
193 };
194
195 var parts = [];
196
197 for ( var k in obj ) {
198 if ( !Object.prototype.hasOwnProperty.call( obj, k ) ) continue;
199 // Ignore the removed experimental child-flex setting in previously saved Builder JSON.
200 if ( 'item_flex' === k ) continue;
201 var v = obj[k];
202 if ( v == null || v === '' ) continue;
203 if ( [ 'padding_top', 'padding_right', 'padding_bottom', 'padding_left', 'margin_top', 'margin_right', 'margin_bottom', 'margin_left' ].indexOf( k ) !== -1 ) {
204 v = normalize_column_length( v );
205 } else if ( 'max_width' === k || 'max_height' === k ) {
206 v = normalize_column_maximum( v );
207 } else if ( [ 'overflow', 'overflow_x', 'overflow_y' ].indexOf( k ) !== -1 ) {
208 v = normalize_column_overflow( v );
209 }
210
211 var var_name = map[k] || ('--wpbc-bfb-col-' + String( k ).replace( /[^a-z0-9_-]/gi, '' ).toLowerCase());
212 parts.push( var_name + ': ' + String( v ) );
213 }
214
215 // Always include explicit min guard (requested): --wpbc-col-min: 0px;
216 parts.push( '--wpbc-col-min: 0px' );
217
218 return parts.join( ';' ) + (parts.length ? ';' : '');
219 }
220
221 /**
222 * Resolve numeric percent from a width token like "48.5%".
223 * Falls back to `fallback_percent` if not in percent format.
224 *
225 * @param {string|number|undefined|null} width_token
226 * @param {number} fallback_percent
227 * @returns {number}
228 */
229 function resolve_flex_basis_percent(width_token, fallback_percent) {
230 if ( typeof width_token === 'string' ) {
231 var s = width_token.trim();
232 if ( s.endsWith( '%' ) ) {
233 var p = parseFloat( s );
234 if ( isFinite( p ) ) return p;
235 }
236 }
237 if ( typeof width_token === 'number' && isFinite( width_token ) ) {
238 return width_token;
239 }
240 return fallback_percent;
241 }
242
243 /**
244 * Compute effective flex-basis values that respect inter-column gap
245 *
246 * @param columns
247 * @param gap_percent
248 * @returns {*}
249 */
250 function compute_effective_bases(columns, gap_percent = 3) {
251
252 const n = columns && columns.length ? columns.length : 1;
253
254 const raw = columns.map( (col) => {
255 const w = col && col.width != null ? String( col.width ).trim() : '';
256 const p = w.endsWith( '%' ) ? parseFloat( w ) : w ? parseFloat( w ) : NaN;
257 return Number.isFinite( p ) ? p : 100 / n;
258 } );
259
260 const sum_raw = raw.reduce( (a, b) => a + b, 0 ) || 100;
261 const gp = Number.isFinite( +gap_percent ) ? +gap_percent : 3;
262 const total_gaps = Math.max( 0, n - 1 ) * gp;
263 const available = Math.max( 0, 100 - total_gaps );
264 const scale_ratio = available / sum_raw;
265
266 return raw.map( (p) => Math.max( 0, p * scale_ratio ) );
267 }
268
269 // == adapter: builder (array-of-pages) > exporter shape { pages: [ { items:[ {kind,data} ] } ] } ==================
270 function adapt_builder_structure_to_exporter(structure) {
271
272 // if ( !Array.isArray( structure ) ) return { pages: [] };
273
274 // Ensure at least one page exists, even when Builder structure is empty `[]`.
275 // This keeps exported Advanced Form valid (wizard step #1 exists).
276 if ( ! Array.isArray( structure ) || structure.length === 0 ) {
277 return { pages: [ { items: [] } ] };
278 }
279
280 const normalize_options = (opts) => {
281 if ( !Array.isArray( opts ) ) return [];
282 return opts.map( (o) => {
283 if ( typeof o === 'string' ) return { label: o, value: o, selected: false };
284 if ( o && typeof o === 'object' ) {
285 return {
286 label : String( o.label ?? o.value ?? '' ),
287 value : String( o.value ?? o.label ?? '' ),
288 selected: !!o.selected
289 };
290 }
291 return { label: String( o ), value: String( o ), selected: false };
292 } );
293 };
294
295 // =================================================================================================
296 // == Adapter – attach parsed per-column `col_styles` from Section into each column
297 // =================================================================================================
298 const walk_section = (sec) => {
299 const section_col_styles = parse_col_styles_json( sec && sec.col_styles );
300
301 return {
302 id : sec?.id,
303 html_id : sec?.html_id || '',
304 cssclass : sec?.cssclass || '',
305 columns : (sec?.columns || []).map( (col, col_index) => {
306 const items = Array.isArray( col?.items )
307 ? col.items
308 : [
309 ...(col?.fields || []).map( (f) => ({ type: 'field', data: f }) ),
310 ...(col?.sections || []).map( (s) => ({ type: 'section', data: s }) )
311 ];
312
313 const fields = items
314 .filter( (it) => it && it.type === 'field' )
315 .map( (it) => ({ ...it.data, options: normalize_options( it.data?.options ) }) );
316
317 const sections = items
318 .filter( (it) => it && it.type === 'section' )
319 .map( (it) => walk_section( it.data ) );
320
321 return {
322 width : col?.width || '100%',
323 style : col?.style || null,
324 col_styles : section_col_styles[ col_index ] || null, // <- attach style object per column
325 fields,
326 sections
327 };
328 } )
329 };
330 };
331
332
333 const pages = structure.map( (page) => {
334 const items = [];
335 (page?.content || []).forEach( (item) => {
336 if ( !item ) return;
337 if ( item.type === 'section' && item.data ) {
338 items.push( { kind: 'section', data: walk_section( item.data ) } );
339 } else if ( item.type === 'field' && item.data ) {
340 items.push( {
341 kind: 'field',
342 data: { ...item.data, options: normalize_options( item.data.options ) }
343 } );
344 }
345 } );
346 return { items };
347 } );
348
349 return { pages };
350 }
351
352
353 // == Booking From Exporter ========================================================================================
354 class WPBC_BFB_Exporter {
355
356 /**
357 * Mutable skip-list for attribute names (lowercase).
358 * You can override it via set_skip_attrs() or add with add_skip_attrs().
359 * @type {Set<string>}
360 */
361 static skip_attrs = new Set();
362
363 /**
364 * Replace the entire skip list.
365 * @param {string[]} arr
366 */
367 static set_skip_attrs( arr ) {
368 this.skip_attrs = new Set(
369 (Array.isArray( arr ) ? arr : []).map( (n) => String( n ).toLowerCase().trim() ).filter( Boolean )
370 );
371 }
372
373 /**
374 * Add one or many attributes to the skip list.
375 * @param {string|string[]} names
376 */
377 static add_skip_attrs( names ) {
378 ( Array.isArray( names ) ? names : [ names ] )
379 .map( (n) => String( n ).toLowerCase().trim() )
380 .filter( Boolean )
381 .forEach( (n) => this.skip_attrs.add( n ) );
382 }
383
384 /**
385 * Remove one attribute from the skip list.
386 * @param {string} name
387 */
388 static remove_skip_attr( name ) {
389 if ( ! name ) { return; }
390 this.skip_attrs.delete( String( name ).toLowerCase().trim() );
391 }
392
393 /**
394 * Apply attribute skipping to a final HTML string.
395 * @param {string} html
396 * @return {string}
397 */
398 static sanitize_export( html ) {
399 return strip_attributes_from_markup( html, Array.from( this.skip_attrs ) );
400 }
401
402
403 /**
404 * Export adapted structure to advanced form text (with <r>/<c> layout and wizard wrapper).
405 *
406 * @param {Object} adapted
407 * @param {Object} [options]
408 * @param {string} [options.newline="\n"]
409 * @param {boolean} [options.addLabels=true]
410 * @param {number} [options.gapPercent=3]
411 * @returns {string}
412 */
413 static export_form(adapted, options = {}) {
414 // indent: use real TAB by default (can be overridden via options.indent)
415 const cfg = { newline: '\n', addLabels: true, gapPercent: 3, indent: '\t', ...options };
416 const IND = (typeof cfg.indent === 'string') ? cfg.indent : '\t';
417
418 let depth = 0;
419 const lines = [];
420 const push = (s = '') => lines.push( IND.repeat( depth ) + String( s ) );
421 const open = (s = '') => {
422 push( s );
423 depth++;
424 };
425 const close = (s = '') => {
426 depth = Math.max( 0, depth - 1 );
427 push( s );
428 };
429 const blank = () => {
430 lines.push( '' );
431 };
432
433 if ( !adapted || !Array.isArray( adapted.pages ) ) return '';
434
435 // Always export at least one wizard step to keep Advanced Form structure valid.
436 const pages = adapted.pages.length ? adapted.pages : [ { items: [] } ];
437
438 const ctx = { usedIds: new Set() };
439
440 open( `<div class="wpbc_bfb_form wpbc_wizard__border_container">` );
441
442 // one-per-form guards (calendar is not gated here)
443 const once = { captcha: 0, country: 0, coupon: 0, cost_corrections: 0, submit: 0 };
444
445 pages.forEach( (page, page_index) => {
446 const is_first = page_index === 0;
447 const step_num = page_index + 1;
448
449 const hidden_class = is_first ? '' : ' wpbc_wizard_step_hidden';
450 const hidden_style = is_first ? '' : ' style="display:none;clear:both;"';
451 open( `<div class="wpbc_wizard_step wpbc__form__div wpbc_wizard_step${step_num}${hidden_class}"${hidden_style}>` );
452
453 (page.items || []).forEach( (item) => {
454 if ( item.kind === 'section' ) {
455 WPBC_BFB_Exporter.render_section( item.data, { open, close, push, blank }, cfg, once, ctx );
456 // blank();
457 } else if ( item.kind === 'field' ) {
458 open( `<r>` );
459 open( `<c>` );
460 WPBC_BFB_Exporter.render_field_node( item.data, { open, close, push, blank }, cfg, once, ctx );
461 close( `</c>` );
462 close( `</r>` );
463 // blank();
464 }
465 } );
466
467 close( `</div>` );
468 } );
469
470 close( `</div>` );
471 return WPBC_BFB_Exporter.sanitize_export( lines.join( cfg.newline ) );
472 }
473
474
475 /**
476 * High-level helper: export full package from raw Builder structure.
477 *
478 * - Adapts raw Builder structure (pages/sections/columns/items) for exporters.
479 * - Builds:
480 * • advanced_form -> “Advanced Form (export)” text.
481 * • fields_data -> “Content of booking fields data (export)” text.
482 *
483 * @param {Array} structure Raw Builder structure from wpbc_bfb.get_structure().
484 * @param {Object} [options]
485 * @param {number} [options.gapPercent=3] Column gap percent for layout math.
486 *
487 * @returns {{
488 * advanced_form: string,
489 * fields_data: string,
490 * structure: Array,
491 * adapted: Object
492 * }}
493 */
494 static export_all( structure, options = {} ) {
495
496 // 1) Adapt Builder JSON to exporter shape (pages[] -> items[]).
497 const adapted = adapt_builder_structure_to_exporter( structure || [] );
498
499 // 2) Advanced Form text (same logic as debug panel).
500 const gap_percent = ( options && typeof options.gapPercent === 'number' ) ? options.gapPercent : 3;
501 const advanced_form = WPBC_BFB_Exporter.export_form(
502 adapted,
503 {
504 addLabels : true,
505 gapPercent: gap_percent
506 }
507 );
508
509 // 3) Content of booking fields data (if content exporter is available).
510 let fields_data = '';
511 if (
512 window.WPBC_BFB_ContentExporter &&
513 typeof window.WPBC_BFB_ContentExporter.export_content === 'function'
514 ) {
515 fields_data = window.WPBC_BFB_ContentExporter.export_content(
516 adapted,
517 {
518 addLabels: true,
519 sep : ': '
520 }
521 );
522 }
523
524 return {
525 advanced_form: advanced_form || '',
526 fields_data : fields_data || '',
527 structure : structure || [],
528 adapted : adapted
529 };
530 }
531
532 // =================================================================================================
533 // == Exporter – render_section() now injects per-column CSS vars from `col_styles`
534 // =================================================================================================
535 static render_section(section, io, cfg, once, ctx) {
536
537 once = once || { captcha: 0, country: 0, coupon: 0, cost_corrections: 0, submit: 0 };
538 ctx = ctx || { usedIds: new Set() };
539
540 const { open, close } = io;
541
542 const cols = Array.isArray( section.columns ) && section.columns.length
543 ? section.columns
544 : [ { width: '100%', fields: [], sections: [] } ];
545
546 // Row is active if ANY column carries styles.
547 var row_is_active = cols.some( function (col) { return has_non_default_col_styles( col && col.col_styles ); } );
548 var row_attr_active = row_is_active ? ' data-colstyles-active="1"' : '';
549 var row_custom_attrs = WPBC_BFB_Exporter.item_wrapper_attrs( section, ctx );
550
551 open( `<r${row_custom_attrs}${row_attr_active}>` );
552
553 const bases = compute_effective_bases( cols, cfg.gapPercent );
554 const esc_attr = core.WPBC_BFB_Sanitize.escape_html;
555
556 cols.forEach( (col, idx) => {
557 // (1) Resolve flex-basis.
558 var eff_basis = resolve_flex_basis_percent( col && col.width, Number.isFinite( bases[idx] ) ? +bases[idx] : 100 );
559
560 // (2) Build inline style.
561 var style_parts = [];
562
563 if ( col && typeof col.style === 'string' && col.style.trim() ) {
564 style_parts.push( col.style.trim().replace( /;+\s*$/, '' ) );
565 }
566 style_parts.push( 'flex-basis: ' + ( Number.isFinite( eff_basis ) ? eff_basis.toString() : '100' ) + '%' );
567
568 var css_vars_str = build_col_css_vars( col && col.col_styles );
569 if ( css_vars_str ) {
570 style_parts.push( css_vars_str.replace( /^;|;$/g, '' ) );
571 }
572
573 var style_attr = ` style="${esc_attr( style_parts.join( '; ' ) )}"`;
574
575 // (3) Column-level activation (more precise scoping)
576 var col_is_active = has_non_default_col_styles( col && col.col_styles );
577 var col_attr_active = col_is_active ? ' data-colstyles-active="1"' : '';
578
579 open( `<c${col_attr_active}${style_attr}>` );
580
581 // Use the shared once/ctx objects so single-per-form guards work across the whole form.
582 (col.fields || []).forEach( (node) =>
583 WPBC_BFB_Exporter.render_field_node( node, io, cfg, once, ctx )
584 );
585
586 // Recurse with the same once/ctx as well.
587 (col.sections || []).forEach( (nested) =>
588 WPBC_BFB_Exporter.render_section( nested, io, cfg, once, ctx )
589 );
590
591 close( `</c>` );
592 } );
593
594 close( `</r>` );
595 }
596
597
598 /**
599 * Build a sanitized custom CSS class and HTML ID attribute string for an exported wrapper.
600 * Used by section row wrappers and fields whose attributes belong on the <item> wrapper.
601 * Also ensures uniqueness of the html_id across the export (uses ctx.usedIds).
602 *
603 * @param {Object} wrapper_data Object containing optional cssclass and html_id properties.
604 * @param {{usedIds:Set<string>}} ctx
605 * @returns {string} e.g. ' class="x y" id="myId"'
606 */
607 static item_wrapper_attrs(wrapper_data, ctx) {
608 if ( ! wrapper_data ) {
609 return '';
610 }
611 const esc_html = core.WPBC_BFB_Sanitize.escape_html;
612 const cls_sanit = core.WPBC_BFB_Sanitize.sanitize_css_classlist;
613 const sid = core.WPBC_BFB_Sanitize.sanitize_html_id;
614
615 let out = '';
616
617 const cls_raw = String( wrapper_data.cssclass_extra || wrapper_data.cssclass || wrapper_data.class || '' );
618 const cls = cls_sanit( cls_raw );
619 let html_id = wrapper_data.html_id ? sid( String( wrapper_data.html_id ) ) : '';
620 if ( html_id && ctx?.usedIds ) {
621 let unique = html_id, i = 2;
622 while ( ctx.usedIds.has( unique ) ) {
623 unique = `${html_id}_${i++}`;
624 }
625 ctx.usedIds.add( unique );
626 html_id = unique;
627 }
628 if ( cls ) {
629 out += ` class="${esc_html( cls )}"`;
630 }
631 if ( html_id ) {
632 out += ` id="${esc_html( html_id )}"`;
633 }
634
635 return out;
636 }
637
638 // =================================================================================================
639 // == Fields – pluggable, pack-driven export
640 // == Wrap every exported field inside <item>…</item> and delegate actual shortcode export
641 // == to per-pack callbacks registered via WPBC_BFB_Exporter.register(type, fn).
642 // =================================================================================================
643 static render_field_node(field, io, cfg, once, ctx) {
644
645 const { open, close, push } = io;
646 if ( ! field || ! field.type ) {
647 return;
648 }
649
650 // Shared context (usedIds, “once-per-form” guards, etc.).
651 once = once || {};
652 ctx = ctx || { usedIds: new Set() };
653
654 const type = String( field.type ).toLowerCase();
655
656 // Optional wrapper attrs for special types (currently only used by captcha).
657 let item_attrs = '';
658 if ( type === 'captcha' ) {
659 item_attrs = WPBC_BFB_Exporter.item_wrapper_attrs( field, ctx );
660 }
661
662 open( `<item${item_attrs}>` );
663
664 try {
665 // 1) Let the corresponding field pack handle export.
666 let handled = false;
667 if ( WPBC_BFB_Exporter.has_exporter( type ) ) {
668 handled = WPBC_BFB_Exporter.run_registered_exporter( field, io, cfg, once, ctx );
669 }
670
671 // 2) Fallback: show a clear TODO comment if no exporter is registered.
672 if ( ! handled ) {
673 const name = WPBC_BFB_Exporter.compute_name( type, field );
674 push( `<!-- TODO: map field type "${type}" name="${name}" in a pack exporter -->` );
675 }
676
677 // 3) Append help text consistently (packs shouldn’t duplicate this).
678 if ( field.help ) {
679 push(
680 `<div class="wpbc_field_description">${core.WPBC_BFB_Sanitize.escape_html(
681 String( field.help )
682 )}</div>`
683 );
684 }
685 } finally {
686 // Always close wrapper.
687 close( `</item>` );
688 }
689 }
690
691 // =================================================================================================
692 // == Helpers ==
693 // =================================================================================================
694 static is_required(field) {
695 const v = field && field.required;
696 return (
697 v === true ||
698 v === 'true' ||
699 v === 1 ||
700 v === '1' ||
701 v === 'required'
702 );
703 }
704
705
706 /**
707 * Shared label emitter used by per-pack exporters.
708 *
709 * Emits optional <l>Label</l> + <br> before the provided body,
710 * respecting cfg.addLabels. Help text is emitted centrally in
711 * render_field_node(), so it is intentionally NOT handled here.
712 *
713 * @param {Object} field
714 * @param {function(string): void} emit
715 * @param {string} body
716 * @param {{addLabels?: boolean}} [cfg]
717 */
718 static emit_label_then(field, emit, body, cfg) {
719 if ( typeof emit !== 'function' ) { return; }
720
721 cfg = cfg || {};
722 const addLabels = cfg.addLabels !== false;
723
724 const raw = (field && typeof field.label === 'string') ? field.label : '';
725 const label = raw.trim();
726
727 var is_req = this.is_required( field );
728 var req_mark = is_req ? '*' : '';
729
730 if ( label && addLabels ) {
731 const esc_html = core.WPBC_BFB_Sanitize.escape_html;
732 emit( '<l>' + esc_html( label ) + req_mark + '</l>' );
733 emit( '<br>' + body );
734 } else {
735 emit( body );
736 }
737 }
738
739
740 // =================================================================================================
741 // == Helpers ==
742 // =================================================================================================
743
744 // -- Time Select Helpers --------------------------------------------------------------------------------------
745 static is_timeslot_picker_enabled() {
746 try {
747 return !!(window._wpbc && typeof window._wpbc.get_other_param === 'function'
748 && window._wpbc.get_other_param('is_enabled_booking_timeslot_picker'));
749 } catch (_) { return false; }
750 }
751
752 static time_placeholder_for(name, field) {
753 // Prefer field-specific placeholder; else sensible default per field.
754 if (typeof field.placeholder === 'string' && field.placeholder.trim()) {
755 return field.placeholder.trim();
756 }
757 if (name === 'durationtime') return '--- Select duration ---';
758 return '--- Select time ---';
759 }
760
761 /**
762 * Build tokens/default for a time-like select (start/end/range/duration).
763 * - Adds an empty-value placeholder as the first option only when:
764 * • time picker is OFF, and
765 * • no option is selected by default, and
766 * • there isn't already an empty-value option.
767 */
768 static build_time_select_tokens(field, name) {
769 let tokens_str = this.option_tokens(field);
770 let def_str = this.default_option_suffix(field, tokens_str);
771
772 if (!this.is_timeslot_picker_enabled()) {
773 const opts = Array.isArray(field.options) ? field.options : [];
774
775 const has_selected_default = opts.some(o =>
776 o && (o.selected === true || o.selected === 'true' || o.selected === 1 || o.selected === '1')
777 );
778
779 if (!has_selected_default) {
780 const has_empty_value_option = opts.some(o =>
781 o && typeof o.value !== 'undefined' && String(o.value).trim() === ''
782 );
783
784 if (!has_empty_value_option) {
785 const phText = this.time_placeholder_for(name, field);
786 const phTokenStr = '"' + core.WPBC_BFB_Sanitize.escape_for_shortcode(phText + '@@') + '"';
787
788 const other = this.option_tokens(field).trim(); // recompute, trim leading space
789 tokens_str = ' ' + phTokenStr + (other ? (' ' + other) : '');
790
791 // Ensure first option (our placeholder) becomes the default implicitly
792 def_str = '';
793 }
794 }
795 }
796 return { tokens_str, def_str };
797 }
798
799 static emit_time_select(name, field, req_mark, id_opt, cls_opts, emit_label_then) {
800 const { tokens_str, def_str } = this.build_time_select_tokens(field, name);
801 // NOTE: No size/ph tokens here to mirror rangetime behavior exactly.
802 emit_label_then(`[selectbox${req_mark} ${name}${id_opt}${cls_opts}${def_str}${tokens_str}]`);
803 }
804
805 // -- Other Helpers --------------------------------------------------------------------------------------------
806 // Return a field's default value (supports both camelCase and snake_case).
807 static get_default_value(field) {
808 const v = field?.default_value ?? field?.defaultValue ?? '';
809 return (v == null) ? '' : String( v );
810 }
811
812 // For text-like fields, the default is a final quoted token in the shortcode.
813 static default_text_suffix(field) {
814 const v = this.get_default_value( field );
815 if ( !v ) return '';
816 return ` "${core.WPBC_BFB_Sanitize.escape_for_shortcode( v )}"`;
817 }
818
819 static class_options(field) {
820 const raw = field.class || field.className || field.cssclass || '';
821 const cls = core.WPBC_BFB_Sanitize.sanitize_css_classlist( String( raw ) );
822 if ( !cls ) return '';
823 return cls
824 .split( /\s+/ )
825 .filter( Boolean )
826 .map( (c) => ` class:${core.WPBC_BFB_Sanitize.to_token( c )}` )
827 .join( '' );
828 }
829
830 static id_option(field, ctx) {
831 const raw_id = field.html_id || field.id_attr;
832 if ( !raw_id ) return '';
833 const base = core.WPBC_BFB_Sanitize.to_token( raw_id );
834 if ( !base ) return '';
835 let unique = base, i = 2;
836 while ( ctx.usedIds.has( unique ) ) unique = `${base}_${i++}`;
837 ctx.usedIds.add( unique );
838 return ` id:${unique}`;
839 }
840
841 static ph_attr(v) {
842 if ( v == null || v === '' ) return '';
843 return ` placeholder:"${core.WPBC_BFB_Sanitize.escape_for_attr_quoted( v )}"`;
844 }
845
846 // text-like size/maxlength token: "40/255" (or "40/" or "/255")
847 static size_max_token(f) {
848 const size = parseInt( f.size, 10 );
849 const max = parseInt( f.maxlength, 10 );
850 if ( Number.isFinite( size ) && Number.isFinite( max ) ) return ` ${size}/${max}`;
851 if ( Number.isFinite( size ) ) return ` ${size}/`;
852 if ( Number.isFinite( max ) ) return ` /${max}`;
853 return '';
854 }
855
856 // textarea cols/rows token: "60x4" (or "60x" or "x4")
857 static cols_rows_token(f) {
858 const cols = parseInt( f.cols, 10 );
859 const rows = parseInt( f.rows, 10 );
860 if ( Number.isFinite( cols ) && Number.isFinite( rows ) ) return ` ${cols}x${rows}`;
861 if ( Number.isFinite( cols ) ) return ` ${cols}x`;
862 if ( Number.isFinite( rows ) ) return ` x${rows}`;
863 return '';
864 }
865
866 static option_tokens(field) {
867 const options = Array.isArray( field.options ) ? field.options : [];
868 if ( options.length === 0 ) return '';
869 const parts = options.map( (o) => {
870 const title = String( o.label ?? o.value ?? '' ).trim();
871 const value = String( o.value ?? o.label ?? '' ).trim();
872 return title && value && title !== value
873 ? `"${core.WPBC_BFB_Sanitize.escape_for_shortcode( `${title}@@${value}` )}"`
874 : `"${core.WPBC_BFB_Sanitize.escape_for_shortcode( title || value )}"`;
875 } );
876 return ' ' + parts.join( ' ' );
877 }
878
879 static default_option_suffix(field, tokens) {
880 const options = Array.isArray( field.options ) ? field.options : [];
881 const selected = options.find( (o) => o.selected );
882 const def_val = selected ? (selected.value ?? selected.label) : (field.default_value ?? field.defaultValue ?? '');
883 if ( !def_val ) return '';
884 return ` default="${core.WPBC_BFB_Sanitize.escape_value_for_attr( def_val )}"`;
885 }
886
887 /**
888 * SELECTBOX / RADIO - Build the final shortcode for choice-based fields.
889 *
890 * Responsibilities:
891 * - Delegates option/default encoding to:
892 * - WPBC_BFB_Exporter.option_tokens( field )
893 * - WPBC_BFB_Exporter.default_option_suffix( field, tokens )
894 * - For `radio`:
895 * - ALWAYS appends a bare `use_label_element` token.
896 * - For `selectbox`:
897 * - Adds a bare `multiple` token when `field.multiple` is truthy
898 * (true, "true", 1, "1", "multiple") -> `[selectbox services multiple "1" "2"]`.
899 * - When single-select AND there is no `default="..."` attribute AND
900 * a non-empty `field.placeholder` is present, encodes the placeholder
901 * as the FIRST option with empty value via the `@@` syntax:
902 * placeholder "---- Select ----" -> `"---- Select ----@@"`
903 * and clears any default attribute:
904 * [selectbox* services "--- Select ---@@" "Option 1" "Option 2"]
905 * - Respects `field.use_label_element` (adds bare `use_label_element` when true).
906 * - For both kinds:
907 * - Honors `field.label_first` by appending `label_first:"1"` when truthy.
908 * - Keeps the required star, id and cssclass tokens in the canonical order.
909 *
910 * Final shortcode layout (order is important):
911 * [kind req name id cls use_label_element multiple default tokens label_first]
912 *
913 * @jDoc
914 * @param {string} kind
915 * Shortcode kind; typically "radio" or "selectbox".
916 *
917 * @param {string} req_mark
918 * Required marker used by Contact Form 7 style shortcodes:
919 * either "" (not required) or "*" (required).
920 *
921 * @param {string} name
922 * Sanitized field name as exported into the shortcode, e.g. "services".
923 * Must already be computed via WPBC_BFB_Exporter.compute_name().
924 *
925 * @param {Object} field
926 * Normalized field data object as stored in the Builder structure. Common keys:
927 * - type {string} Field type, e.g. "radio" | "select".
928 * - options {Array} Option objects: { label, value, selected }.
929 * - placeholder {string} Placeholder text (single-select only).
930 * - multiple {boolean|string|number} Enables multi-select when truthy.
931 * - use_label_element {boolean} Request bare `use_label_element` token (non-radio).
932 * - label_first {boolean} If true, appends `label_first:"1"` token.
933 * - default_value {string} Optional default value (used by default_option_suffix()).
934 * - html_id / cssclass / class / className {string} Used upstream in id_opt/cls_opts.
935 *
936 * @param {string} id_opt
937 * Optional id token built by WPBC_BFB_Exporter.id_option(field, ctx),
938 * e.g. " id:my_id" or empty string.
939 *
940 * @param {string} cls_opts
941 * Class tokens built by WPBC_BFB_Exporter.class_options(field),
942 * e.g. " class:my_class class:other".
943 *
944 * @returns {string}
945 * Complete shortcode body for the choice field, for example:
946 * "[radio* services use_label_element \"A\" \"B\"]"
947 * "[selectbox services multiple \"1\" \"2\" \"3\"]"
948 * "[selectbox* services \"--- Select ---@@\" \"Option 1\" \"Option 2\"]"
949 */
950 static choice_tag(kind, req_mark, name, field, id_opt, cls_opts) {
951 // Start from the raw options/default as before.
952 let tokens = WPBC_BFB_Exporter.option_tokens( field );
953 let def = WPBC_BFB_Exporter.default_option_suffix( field, tokens );
954
955 // For RADIO we must ALWAYS include a bare `use_label_element` token (no value/quotes).
956 // For other kinds, keep backward compatibility: include only if explicitly set.
957 let ule = '';
958 if ( kind === 'radio' ) {
959 ule = ' use_label_element';
960 } else if ( field && field.use_label_element ) {
961 ule = ' use_label_element';
962 }
963
964 // SELECTBOX-specific extras:
965 // - "multiple" flag
966 // - placeholder exported as FIRST OPTION when single-select and no default.
967 let multiple_flag = '';
968
969 if ( kind === 'selectbox' && field ) {
970 const multiple =
971 field.multiple === true ||
972 field.multiple === 'true' ||
973 field.multiple === 1 ||
974 field.multiple === '1' ||
975 field.multiple === 'multiple';
976
977 if ( multiple ) {
978 // Export bare "multiple" token as in: [selectbox services multiple "1" "2" "3"].
979 multiple_flag = ' multiple';
980 } else if ( !def ) {
981 // Single-select + NO default selected:
982 // export placeholder as the FIRST OPTION with empty value:
983 // [selectbox* services "--- Select ---@@" "Option 1" "Option 2"]
984 const rawPh = field.placeholder;
985 const ph = (typeof rawPh === 'string') ? rawPh.trim() : '';
986
987 if ( ph ) {
988 const S = core.WPBC_BFB_Sanitize;
989 const esc_sc = (S && S.escape_for_shortcode) ? S.escape_for_shortcode : (v) => String( v );
990
991 const phToken = `"${esc_sc( ph + '@@' )}"`;
992
993 if ( tokens && tokens.length ) {
994 // tokens already starts with a leading space.
995 tokens = ' ' + phToken + tokens;
996 } else {
997 tokens = ' ' + phToken;
998 }
999
1000 // Ensure there is still NO default attribute when using placeholder-as-option.
1001 def = '';
1002 }
1003 }
1004 }
1005
1006 // Optional: label_first stays as quoted flag when explicitly requested.
1007 const lf = (field && field.label_first) ? ' label_first:"1"' : '';
1008
1009 // IMPORTANT ORDER (per request):
1010 // [kind req name id cls use_label_element multiple default tokens label_first]
1011 // i.e. `use_label_element` (and select extras) come BEFORE default/tokens.
1012 return `[${kind}${req_mark} ${name}${id_opt}${cls_opts}${ule}${multiple_flag}${def}${tokens}${lf}]`;
1013 }
1014
1015 static compute_name(type, field) {
1016 // Names are fully validated when the field is added to the canvas.
1017 // The exporter must therefore preserve them (apart from idempotent sanitization), otherwise existing forms can break.
1018 const Sanit = core.WPBC_BFB_Sanitize;
1019
1020 const raw = (field && (field.name || field.id)) ? String(field.name || field.id) : String(type || 'field');
1021
1022 // Idempotent sanitization only – no auto-prefixing or renaming.
1023 const name = Sanit.sanitize_html_name( raw );
1024
1025 // In the unlikely case sanitization returns an empty string, fall back to a sanitized type-based token.
1026 return name || Sanit.sanitize_html_name( String(type || 'field') );
1027 }
1028
1029 /**
1030 * Register a per-field exporter.
1031 *
1032 * This is the ONLY place where field-specific shortcode markup should live.
1033 * Core stays generic; packs provide tiny plugins, for example:
1034 *
1035 * WPBC_BFB_Exporter.register( 'text', (field, emit, extras) => { ... } );
1036 *
1037 * @jDoc
1038 * @param {string} type Field type key, e.g. 'steps_timeline'
1039 * @param {(field:any, emit:(code:string)=>void, extras?:{io?:any,cfg?:any,once?:any,ctx?:any,core?:any})=>void}
1040 * fn
1041 * @returns {void}
1042 */
1043 static register(type, fn) {
1044 if ( ! type || typeof fn !== 'function' ) { return; }
1045 if ( ! this.__registry ) { this.__registry = new Map(); }
1046 this.__registry.set( String( type ).toLowerCase(), fn );
1047 }
1048
1049 /**
1050 * Unregister a previously registered exporter.
1051 *
1052 * @jDoc
1053 * @param {string} type
1054 * @returns {void}
1055 */
1056 static unregister(type) {
1057 if ( ! this.__registry || ! type ) { return; }
1058 this.__registry.delete( String( type ).toLowerCase() );
1059 }
1060
1061 /**
1062 * Check if an exporter exists for a given field type.
1063 *
1064 * @jDoc
1065 * @param {string} type
1066 * @returns {boolean}
1067 */
1068 static has_exporter(type) {
1069 return !!( this.__registry && this.__registry.has( String( type ).toLowerCase() ) );
1070 }
1071
1072 /**
1073 * Run a registered exporter for a field, if present.
1074 * Returns true if a registered exporter handled it.
1075 *
1076 * @jDoc
1077 * @param {any} field
1078 * @param {{open:Function,close:Function,push:Function,blank:Function}} io
1079 * @param {any} cfg
1080 * @param {any} once
1081 * @param {any} ctx
1082 * @returns {boolean}
1083 */
1084 static run_registered_exporter(field, io, cfg, once, ctx) {
1085 if ( ! field || ! field.type || ! this.__registry ) { return false; }
1086 const key = String( field.type ).toLowerCase();
1087 const fn = this.__registry.get( key );
1088 if ( typeof fn !== 'function' ) { return false; }
1089
1090 try {
1091 // Minimal, consistent emit() bridge into our line buffer:
1092 const emit = (code) => { if ( typeof code === 'string' ) { io.push( code ); } };
1093 fn( field, emit, { io, cfg, once, ctx, core } );
1094 return true;
1095 } catch (e) {
1096 _wpbc?.dev?.error?.( 'WPBC_BFB_Exporter.run_registered_exporter', e );
1097 return false;
1098 }
1099 }
1100
1101 }
1102
1103 // expose globally for packs (if not already).
1104 window.WPBC_BFB_Exporter = window.WPBC_BFB_Exporter || WPBC_BFB_Exporter;
1105 wpbc_bfb__dispatch_event_safe( 'wpbc:bfb:exporter-ready', {} );
1106
1107 // Initialize default skip list; allow a global override array before export runs.
1108 WPBC_BFB_Exporter.set_skip_attrs( window.WPBC_BFB_EXPORT_SKIP_ATTRS || wpbc_export_skip_attrs_default );
1109
1110 // == "Content of booking fields data" Exporter ====================================================================
1111
1112 // – pack-extensible generator for "Content of booking fields data" ============================================
1113 // == Produces markup like: "<div class=\"standard-content-form\"><b>Title</b>: <f>[shortcode]</f><br> ... </div>"
1114 // == Packs can override per type via: WPBC_BFB_ContentExporter.register('calendar', (field, emit, ctx)=>{...})
1115 // =================================================================================================
1116 class WPBC_BFB_ContentExporter {
1117
1118 static register(type, fn) {
1119 if ( !type || typeof fn !== 'function' ) return;
1120 if ( !this.__registry ) this.__registry = new Map();
1121 this.__registry.set( String( type ).toLowerCase(), fn );
1122 }
1123
1124 static unregister(type) {
1125 if ( !this.__registry || !type ) return;
1126 this.__registry.delete( String( type ).toLowerCase() );
1127 }
1128
1129 static has_exporter(type) {
1130 return !!(this.__registry && this.__registry.has( String( type ).toLowerCase() ));
1131 }
1132
1133 static run_registered_exporter(field, emit, ctx) {
1134 if ( !field || !field.type || !this.__registry ) return false;
1135 const key = String( field.type ).toLowerCase();
1136 const fn = this.__registry.get( key );
1137 if ( typeof fn !== 'function' ) return false;
1138 try {
1139 fn( field, emit, ctx || {} );
1140 return true;
1141 } catch ( e ) {
1142 _wpbc?.dev?.error?.( 'WPBC_BFB_ContentExporter.run_registered_exporter', e );
1143 return false;
1144 }
1145 }
1146
1147 // === NEW: shared line formatter for "Content of booking fields data" ===
1148 static emit_line_bold_field(emit, label, token, cfg) {
1149 const S = core.WPBC_BFB_Sanitize;
1150 const sep = (cfg && typeof cfg.sep === 'string') ? cfg.sep : ': ';
1151 const addLabels = (cfg && 'addLabels' in cfg) ? !!cfg.addLabels : true;
1152
1153 const title = (addLabels && label) ? `<b>${S.escape_html(label)}</b>${sep}` : '';
1154
1155 emit(`${title}<f>[${token}]</f><br>`);
1156 }
1157
1158 /**
1159 * Export adapted structure to “content of booking fields data”.
1160 * @param {{pages:Array}} adapted result of adapt_builder_structure_to_exporter()
1161 * @param {{newline?:string, addLabels?:boolean, sep?:string}} options
1162 * @returns {string}
1163 */
1164 static export_content(adapted, options = {}) {
1165
1166 const cfg = { newline: '\n', addLabels: true, sep: ': ', indent: '\t', ...options };
1167 const IND = (typeof cfg.indent === 'string') ? cfg.indent : '\t';
1168 let depth = 0;
1169 const lines = [];
1170
1171 const push = (s = '') => lines.push( IND.repeat( depth ) + String( s ) );
1172 const open = (s = '') => { push( s ); depth++; };
1173 const close = (s = '') => { depth = Math.max( 0, depth - 1 ); push( s ); };
1174
1175 const emit = (s) => {
1176 if ( typeof s !== 'string' ) { return; }
1177 String( s ).split( /\r?\n/ ).forEach( (line) => push( line ) );
1178 };
1179
1180 if ( !adapted || !Array.isArray( adapted.pages ) ) return '';
1181
1182 const skipTypes = new Set( [ 'captcha', 'submit', 'divider', 'wizard_nav', 'cost_corrections' ] );
1183
1184 const fallbackLine = (field) => {
1185 const type = String( field.type || '' ).toLowerCase();
1186 const name = WPBC_BFB_Exporter.compute_name( type, field );
1187 const label = (typeof field.label === 'string' && field.label.trim()) ? field.label.trim() : name;
1188 if ( !name ) return;
1189 WPBC_BFB_ContentExporter.emit_line_bold_field( emit, label, name, cfg );
1190 };
1191
1192 // Per-type sensible defaults (can be overridden by packs via register())
1193 const defaultContentFor = (field) => {
1194 const type = String( field.type || '' ).toLowerCase();
1195 if ( skipTypes.has( type ) ) return;
1196 // Special cases out of the box:
1197 if ( type === 'calendar' ) {
1198 const label = (typeof field.label === 'string' && field.label.trim()) ? field.label.trim() : 'Dates';
1199 WPBC_BFB_ContentExporter.emit_line_bold_field( emit, label, 'dates', cfg );
1200 return;
1201 }
1202 // time-like reserved names -> keep placeholder token equal to name
1203 const reserved = String( field.name || field.id || '' ).toLowerCase();
1204 if ( [ 'rangetime', 'starttime', 'endtime', 'durationtime' ].includes( reserved ) ) {
1205 const label = (typeof field.label === 'string' && field.label.trim()) ? field.label.trim() : reserved;
1206 // Keep your special token for duration time in content: [durationtime_val]
1207 const token = (reserved === 'durationtime') ? 'durationtime_val' : reserved;
1208 WPBC_BFB_ContentExporter.emit_line_bold_field( emit, label, token, cfg );
1209 return;
1210 }
1211 // Fallback (text/email/tel/number/textarea/select/checkbox/radio etc.)
1212 fallbackLine( field );
1213 };
1214
1215 // Walk pages/sections/columns/fields (same order as form)
1216 const walkSection = (sec) => {
1217 (sec.columns || []).forEach( (col) => {
1218 (col.fields || []).forEach( (f) => processField( f ) );
1219 (col.sections || []).forEach( (s) => walkSection( s ) );
1220 } );
1221 };
1222 const processItem = (item) => {
1223 if ( !item ) return;
1224 if ( item.kind === 'field' ) processField( item.data );
1225 if ( item.kind === 'section' ) walkSection( item.data );
1226 };
1227 const processField = (field) => {
1228 if ( !field ) return;
1229 // allow packs to override:
1230 if ( WPBC_BFB_ContentExporter.run_registered_exporter( field, emit, { cfg, core } ) ) return;
1231 defaultContentFor( field );
1232 };
1233
1234 // Wrapper first -> inner lines will be TAB-indented
1235 open( `<div class="standard-content-form">` );
1236 adapted.pages.forEach( (page) => (page.items || []).forEach( processItem ) );
1237 close( `</div>` );
1238
1239 return lines.join( cfg.newline );
1240 }
1241
1242 }
1243
1244 // expose + ready event for packs to register their content exporters.
1245 window.WPBC_BFB_ContentExporter = window.WPBC_BFB_ContentExporter || WPBC_BFB_ContentExporter;
1246 wpbc_bfb__dispatch_event_safe( 'wpbc:bfb:content-exporter-ready', {} );
1247 })();
1248