PluginProbe
Booking Calendar / 11.7
Booking Calendar v11.7
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 10.11 All 204 releases
booking / includes / page-form-builder / advanced-mode / _src / bfb-advanced-form-editor.js

bfb-advanced-form-editor.js in Booking Calendar 11.7, at includes/page-form-builder/advanced-mode/_src/bfb-advanced-form-editor.js

1,077 lines 28.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /*
2 * Advanced Booking form mode.
3 *
4 * Refactored: module-style (DOM / State / Editor / Sync / Clipboard / Events / UI)
5 *
6 * @file ../includes/page-form-builder/advanced-mode/_out/bfb-advanced-form-editor.js
7 */
8 (function (w, d) {
9 'use strict';
10
11 // True after wpbc:bfb:structure:loaded fired at least once.
12 var structure_loaded_once = false;
13
14 function adapted_has_any_field(adapted) {
15 if ( ! adapted || ! Array.isArray( adapted.pages ) ) {
16 return false;
17 }
18
19 var has = false;
20
21 function walk_section(sec) {
22 if ( has || ! sec ) { return; }
23 var cols = Array.isArray( sec.columns ) ? sec.columns : [];
24 for ( var i = 0; i < cols.length; i++ ) {
25 var col = cols[i] || {};
26 if ( Array.isArray( col.fields ) && col.fields.length ) { has = true; return; }
27 var nested = Array.isArray( col.sections ) ? col.sections : [];
28 for ( var j = 0; j < nested.length; j++ ) {
29 walk_section( nested[j] );
30 if ( has ) { return; }
31 }
32 }
33 }
34
35 for ( var p = 0; p < adapted.pages.length; p++ ) {
36 var page = adapted.pages[p] || {};
37 var items = Array.isArray( page.items ) ? page.items : [];
38 for ( var k = 0; k < items.length; k++ ) {
39 var it = items[k] || {};
40 if ( it.kind === 'field' ) { return true; }
41 if ( it.kind === 'section' ) {
42 walk_section( it.data );
43 if ( has ) { return true; }
44 }
45 }
46 }
47 return has;
48 }
49
50 // == Constants ==================================================================================================
51 var IDS = {
52 panels : 'wpbc_bfb__advanced_mode_panels',
53 ta_form : 'wpbc_bfb__advanced_form_editor',
54 ta_content : 'wpbc_bfb__content_form_editor',
55 btn_regen : 'wpbc_bfb__advanced_regenerate_btn',
56 btn_copy_form: 'wpbc_bfb__advanced_copy_form_btn',
57 btn_copy_cnt : 'wpbc_bfb__advanced_copy_content_btn',
58 cb_autosync : 'wpbc_bfb__advanced_autosync',
59 dirty_hint : 'wpbc_bfb__advanced_dirty_hint'
60 };
61
62 var KEY = {
63 FORM : 'advanced_form',
64 CONTENT: 'content_form'
65 };
66
67 var TA_ID_BY_KEY = {};
68 TA_ID_BY_KEY[KEY.FORM] = IDS.ta_form;
69 TA_ID_BY_KEY[KEY.CONTENT] = IDS.ta_content;
70
71 // == DOM helpers ================================================================================================
72 var DOM = (function () {
73
74 function get_by_id(id) {
75 return d.getElementById( id );
76 }
77
78 function on(el, type, fn) {
79 if ( el ) {
80 el.addEventListener( type, fn );
81 }
82 }
83
84 function is_advanced_ui_present() {
85 return !! d.querySelector( '#' + IDS.panels );
86 }
87
88 return {
89 get : get_by_id,
90 on : on,
91 has_ui: is_advanced_ui_present
92 };
93 })();
94
95 // == State ======================================================================================================
96 var State = (function () {
97
98 var state = {
99 editors : {}, // key -> wp.codeEditor instance (or null)
100 autosync_user_value : null, // null = not decided by user, boolean = user explicitly set
101 sync_state_bootstrapped: false,
102 is_dirty : false,
103 is_programmatic_update : false,
104 is_inited : false,
105 textarea_fallback_bound: false
106 };
107
108 state.editors[KEY.FORM] = null;
109 state.editors[KEY.CONTENT] = null;
110
111 function set_live_badges() {
112 var cb = DOM.get( IDS.cb_autosync );
113 var sync_on = !! (cb && cb.checked);
114
115 var live = (sync_on && ! state.is_dirty) ? 'builder' : 'advanced';
116
117 var root = d.documentElement;
118 if ( ! root ) {
119 return;
120 }
121
122 root.setAttribute( 'data-wpbc-bfb-live-source', live === 'builder' ? 'builder' : 'advanced' );
123 root.setAttribute( 'data-wpbc-bfb-sync-mode', sync_on ? 'on' : 'off' );
124 }
125
126 function set_dirty(on) {
127 state.is_dirty = !! on;
128
129 var hint = DOM.get( IDS.dirty_hint );
130 if ( hint ) {
131 hint.style.display = state.is_dirty ? 'inline' : 'none';
132 }
133
134 var autosync = DOM.get( IDS.cb_autosync );
135
136 if ( state.is_dirty ) {
137 // User edited Advanced => explicit opt-out of autosync for this session.
138 state.autosync_user_value = false;
139 state.sync_state_bootstrapped = true;
140
141 if ( autosync ) {
142 autosync.checked = false;
143 }
144 } else {
145 // IMPORTANT:
146 // When clearing dirty state, do NOT force autosync checkbox ON.
147 // Checkbox state is controlled by:
148 // - user action (cb change)
149 // - Sync.apply_autosync_state() when autosync_user_value === null
150 }
151
152 set_live_badges();
153 }
154
155
156 function is_autosync_on() {
157 var cb = DOM.get( IDS.cb_autosync );
158 return !! (cb && cb.checked && ! state.is_dirty);
159 }
160
161 return {
162 raw : state,
163 set_dirty : set_dirty,
164 is_autosync_on: is_autosync_on,
165 update_badges : set_live_badges
166 };
167 })();
168
169 // == Editor (CodeMirror + textarea fallback + shortcode highlighting mode) ======================================
170 var Editor = (function () {
171
172 var is_oshortcode_defined = false;
173
174 function can_init_codemirror() {
175 var wpns = w.wp || null;
176 return !! (
177 wpns &&
178 wpns.codeEditor &&
179 typeof wpns.codeEditor.initialize === 'function' &&
180 w.wpbc_bfb_code_editor_settings
181 );
182 }
183
184 function ensure_oshortcode_mode() {
185 if ( is_oshortcode_defined ) {
186 return;
187 }
188
189 var CM = w.wp && w.wp.CodeMirror ? w.wp.CodeMirror : null;
190 if ( ! CM || typeof CM.defineMode !== 'function' ) {
191 return;
192 }
193
194 CM.defineMode( 'oshortcode', function (config, parserConfig) {
195
196 var overlay = {
197 token: function (stream) {
198 var ch;
199
200 // [name ...] or [name* ...]
201 if ( stream.match( /^\[([a-zA-Z0-9_]+)\*?\s?/ ) ) {
202 while ( (ch = stream.next()) != null ) {
203 if ( ch === ']' ) {
204 return 'oshortcode';
205 }
206 }
207 }
208
209 while ( stream.next() != null && ! stream.match( /^\[([a-zA-Z0-9_]+)\*?\s?/, false ) ) {}
210 return null;
211 }
212 };
213
214 var base = CM.getMode( config, (parserConfig && parserConfig.backdrop) || 'htmlmixed' );
215
216 // Fallback if overlay addon is missing.
217 if ( typeof CM.overlayMode !== 'function' ) {
218 return base;
219 }
220
221 return CM.overlayMode( base, overlay );
222 } );
223
224 is_oshortcode_defined = true;
225 }
226
227 function bind_textarea_dirty_fallback() {
228 if ( State.raw.textarea_fallback_bound ) {
229 return;
230 }
231
232 var ta_form = DOM.get( IDS.ta_form );
233 var ta_cnt = DOM.get( IDS.ta_content );
234 if ( ! ta_form || ! ta_cnt ) {
235 return;
236 }
237
238 function on_change() {
239 if ( State.raw.is_programmatic_update ) {
240 return;
241 }
242 State.set_dirty( true );
243 }
244
245 ta_form.addEventListener( 'input', on_change );
246 ta_form.addEventListener( 'change', on_change );
247 ta_cnt.addEventListener( 'input', on_change );
248 ta_cnt.addEventListener( 'change', on_change );
249
250 State.raw.textarea_fallback_bound = true;
251 }
252
253 function init_editor(textarea_el, key) {
254 var wpns = w.wp || null;
255
256 if ( ! textarea_el || ! wpns || ! wpns.codeEditor || typeof wpns.codeEditor.initialize !== 'function' ) {
257 return null;
258 }
259
260 var base = w.wpbc_bfb_code_editor_settings || null;
261 if ( ! base ) {
262 return null;
263 }
264
265 ensure_oshortcode_mode();
266
267 // Clone so we don't mutate localized shared settings object.
268 var settings = Object.assign( {}, base );
269 settings.codemirror = Object.assign( {}, base.codemirror || {} );
270 settings.codemirror.mode = 'oshortcode';
271
272 var inst = wpns.codeEditor.initialize( textarea_el, settings );
273
274 if ( inst && inst.codemirror ) {
275 inst.codemirror.on( 'change', function () {
276 if ( State.raw.is_programmatic_update ) {
277 return;
278 }
279 State.set_dirty( true );
280 } );
281 }
282
283 State.raw.editors[key] = inst;
284 return inst;
285 }
286
287 function ensure_inited() {
288 if ( State.raw.is_inited ) {
289 return true;
290 }
291
292 var ta_form = DOM.get( IDS.ta_form );
293 var ta_cnt = DOM.get( IDS.ta_content );
294 if ( ! ta_form || ! ta_cnt ) {
295 return false;
296 }
297
298 bind_textarea_dirty_fallback();
299
300 if ( can_init_codemirror() ) {
301 var i1 = init_editor( ta_form, KEY.FORM );
302 var i2 = init_editor( ta_cnt, KEY.CONTENT );
303
304 if ( ! i1 ) {
305 State.raw.editors[KEY.FORM] = null;
306 }
307 if ( ! i2 ) {
308 State.raw.editors[KEY.CONTENT] = null;
309 }
310 }
311
312 State.raw.is_inited = true;
313 return true;
314 }
315
316 function refresh_all() {
317 var keys = Object.keys( State.raw.editors );
318 for ( var i = 0; i < keys.length; i++ ) {
319 var inst = State.raw.editors[keys[i]];
320 try {
321 if ( inst && inst.codemirror && typeof inst.codemirror.refresh === 'function' ) {
322 inst.codemirror.refresh();
323 }
324 } catch ( e ) {}
325 }
326 }
327
328 function textarea_id_for(key) {
329 return TA_ID_BY_KEY[key] || '';
330 }
331
332 function get_value(key) {
333 var inst = State.raw.editors[key];
334 if ( inst && inst.codemirror && typeof inst.codemirror.getValue === 'function' ) {
335 return String( inst.codemirror.getValue() || '' );
336 }
337 var ta = DOM.get( textarea_id_for( key ) );
338 return ta ? String( ta.value || '' ) : '';
339 }
340
341 function set_value(key, value) {
342 value = (value == null) ? '' : String( value );
343
344 var ta = DOM.get( textarea_id_for( key ) );
345
346 State.raw.is_programmatic_update = true;
347 try {
348 if ( ta ) {
349 ta.value = value;
350 }
351
352 var inst = State.raw.editors[key];
353 if ( inst && inst.codemirror && typeof inst.codemirror.setValue === 'function' ) {
354 inst.codemirror.setValue( value );
355 if ( typeof inst.codemirror.save === 'function' ) {
356 inst.codemirror.save();
357 }
358 }
359 } finally {
360 State.raw.is_programmatic_update = false;
361 }
362 }
363
364 function focus_and_select(key) {
365 var ta = DOM.get( textarea_id_for( key ) );
366 if ( ! ta ) {
367 return;
368 }
369 try {
370 ta.focus();
371 ta.select();
372 } catch ( e ) {}
373 }
374
375 function save_all_to_textareas() {
376 var keys = Object.keys( State.raw.editors );
377 for ( var i = 0; i < keys.length; i++ ) {
378 var inst = State.raw.editors[keys[i]];
379 try {
380 if ( inst && inst.codemirror && typeof inst.codemirror.save === 'function' ) {
381 inst.codemirror.save();
382 }
383 } catch ( e ) {}
384 }
385 }
386
387 return {
388 ensure_inited: ensure_inited,
389 refresh_all : refresh_all,
390 get_value : get_value,
391 set_value : set_value,
392 focus_select : focus_and_select,
393 save_all : save_all_to_textareas
394 };
395 })();
396
397 // == Builder export + Sync ======================================================================================
398 var Sync = (function () {
399
400 var poll_timer_id = null;
401 var debounce_timer_id = null;
402
403 function can_export_from_builder() {
404 return !! (
405 w.wpbc_bfb &&
406 typeof w.wpbc_bfb.get_structure === 'function' &&
407 w.WPBC_BFB_Exporter &&
408 typeof w.WPBC_BFB_Exporter.export_all === 'function'
409 );
410 }
411
412 function get_current_structure() {
413 return (w.wpbc_bfb && typeof w.wpbc_bfb.get_structure === 'function') ? w.wpbc_bfb.get_structure() : [];
414 }
415
416 function export_all_from_builder() {
417 if ( ! w.WPBC_BFB_Exporter || typeof w.WPBC_BFB_Exporter.export_all !== 'function' ) {
418 return null;
419 }
420 try {
421 return w.WPBC_BFB_Exporter.export_all( get_current_structure(), { gapPercent: 3 } );
422 } catch ( e ) {
423 return null;
424 }
425 }
426
427 function export_output_looks_ready(out) {
428 if ( ! out ) {
429 return false;
430 }
431
432 var af = (out.advanced_form == null) ? '' : String( out.advanced_form );
433 var cf = (out.fields_data == null) ? '' : String( out.fields_data );
434
435 af = af.trim();
436 cf = cf.trim();
437
438 if ( ! af && ! cf ) {
439 return false;
440 }
441
442 // If structure is loaded and there are NO fields, accept empty export.
443 if ( structure_loaded_once && ! adapted_has_any_field( out.adapted ) ) {
444 return true;
445 }
446
447 // Real BFB export includes layout tags (<r>, <c>, <item>).
448 var has_layout_tags = /<\s*(r|c|item)\b/i.test( af ) || /<\s*(r|c|item)\b/i.test( cf );
449 var has_any_item = /<\s*item\b/i.test( af ) || /<\s*item\b/i.test( cf );
450
451 return has_layout_tags && has_any_item;
452 }
453
454 function get_builder_export_if_ready() {
455 if ( ! can_export_from_builder() ) {
456 return null;
457 }
458
459 var out = export_all_from_builder();
460 if ( ! out ) {
461 return null;
462 }
463
464 if ( ! export_output_looks_ready( out ) ) {
465 return null;
466 }
467
468 return out;
469 }
470
471 // --- Normalization for compare -------------------------------------------------------
472 function normalize_style_value(css) {
473 css = (css == null) ? '' : String( css );
474
475 css = css.replace( /\r\n/g, '\n' ).replace( /\r/g, '\n' );
476 css = css.replace( /\t/g, ' ' );
477 css = css.replace( /\s+/g, ' ' );
478
479 css = css.replace( /\s*;\s*/g, ';' );
480 css = css.replace( /\s*:\s*/g, ':' );
481
482 css = css.trim().replace( /;+\s*$/g, '' );
483 if ( css !== '' ) {
484 css += ';';
485 }
486
487 return css;
488 }
489
490 function normalize_inline_styles_in_markup(html) {
491 html = (html == null) ? '' : String( html );
492
493 return html.replace( /\bstyle=(["'])(.*?)\1/gi, function (_m, quote, css) {
494 return 'style=' + quote + normalize_style_value( css ) + quote;
495 } );
496 }
497
498 function normalize_text(s) {
499 s = (s == null) ? '' : String( s );
500
501 s = s.replace( /\r\n/g, '\n' ).replace( /\r/g, '\n' );
502 s = normalize_inline_styles_in_markup( s );
503
504 s = s.replace( /[ \t]+$/gm, '' );
505 s = s.replace( /[ ]{2,}/g, ' ' );
506 s = s.replace( /^\s+/gm, '' );
507 s = s.replace( /\n{3,}/g, '\n\n' );
508
509 return s.trim();
510 }
511
512 function get_current_advanced_texts() {
513 return {
514 advanced_form: Editor.get_value( KEY.FORM ),
515 content_form : Editor.get_value( KEY.CONTENT )
516 };
517 }
518
519 function detect_sync_state_with_export() {
520 var out = get_builder_export_if_ready();
521 if ( ! out ) {
522 return null;
523 }
524
525 var cur = get_current_advanced_texts();
526
527 var a1 = normalize_text( cur.advanced_form );
528 var a2 = normalize_text( cur.content_form );
529
530 var b1 = normalize_text( out.advanced_form || '' );
531 var b2 = normalize_text( out.fields_data || '' );
532
533 return {
534 is_synced: (a1 === b1) && (a2 === b2),
535 out : out
536 };
537 }
538
539 function apply_autosync_from_sync_state(is_synced) {
540 var cb = DOM.get( IDS.cb_autosync );
541 if ( ! cb ) {
542 return;
543 }
544
545 if ( State.raw.is_dirty ) {
546 State.raw.autosync_user_value = false;
547 State.raw.sync_state_bootstrapped = true;
548 cb.checked = false;
549 State.update_badges();
550 return;
551 }
552
553 // Only auto-set checkbox if user never explicitly touched it.
554 if ( State.raw.autosync_user_value === null ) {
555 cb.checked = !! is_synced;
556 }
557
558 State.raw.sync_state_bootstrapped = true;
559 State.update_badges();
560 }
561
562 function regenerate_from_builder(out_opt) {
563 var out = out_opt || get_builder_export_if_ready();
564 if ( ! out ) {
565 return false;
566 }
567
568 Editor.set_value( KEY.FORM, out.advanced_form || '' );
569 Editor.set_value( KEY.CONTENT, out.fields_data || '' );
570
571 State.set_dirty( false );
572 State.update_badges();
573
574 return true;
575 }
576
577 function sync_detect_and_apply() {
578 var res = detect_sync_state_with_export();
579 if ( res === null ) {
580 return false;
581 }
582
583 var cb = DOM.get( IDS.cb_autosync );
584 var sync_on = !! (cb && cb.checked);
585
586 State.raw.sync_state_bootstrapped = true;
587
588 if ( State.raw.is_dirty ) {
589 apply_autosync_from_sync_state( false );
590 return true;
591 }
592
593 if ( sync_on ) {
594 if ( ! res.is_synced ) {
595 regenerate_from_builder( res.out );
596 } else {
597 State.update_badges();
598 }
599 return true;
600 }
601
602 apply_autosync_from_sync_state( res.is_synced );
603 return true;
604 }
605
606 // --- Poll / debounce -------------------------------------------------------
607 function schedule_sync_detect(reason) {
608
609 if ( poll_timer_id ) {
610 clearTimeout( poll_timer_id );
611 poll_timer_id = null;
612 }
613
614 var started_ms = Date.now();
615 var max_total_ms = 12000;
616 var delay_ms = 150;
617
618 (function tick() {
619
620 if ( ! DOM.has_ui() ) {
621 poll_timer_id = null;
622 return;
623 }
624
625 Editor.ensure_inited();
626
627 if ( State.raw.is_dirty ) {
628 apply_autosync_from_sync_state( false );
629 poll_timer_id = null;
630 return;
631 }
632
633 if ( sync_detect_and_apply() ) {
634 poll_timer_id = null;
635 return;
636 }
637
638 if ( (Date.now() - started_ms) < max_total_ms ) {
639 delay_ms = Math.min( 600, Math.floor( delay_ms * 1.25 ) );
640 poll_timer_id = setTimeout( tick, delay_ms );
641 return;
642 }
643
644 poll_timer_id = null;
645 })();
646 }
647
648 function schedule_sync_detect_debounced(reason) {
649 if ( debounce_timer_id ) {
650 clearTimeout( debounce_timer_id );
651 }
652 debounce_timer_id = setTimeout( function () {
653 debounce_timer_id = null;
654 schedule_sync_detect( reason );
655 }, 250 );
656 }
657
658 return {
659 schedule_detect : schedule_sync_detect,
660 schedule_detect_debounced: schedule_sync_detect_debounced,
661 regenerate : regenerate_from_builder,
662 apply_autosync_state : apply_autosync_from_sync_state
663 };
664 })();
665
666 // == Clipboard ==================================================================================================
667 var Clipboard = (function () {
668
669 async function copy_text(text) {
670 text = (text == null) ? '' : String( text );
671
672 if ( typeof w.wpbc_copy_to_clipboard === 'function' ) {
673 try { return await w.wpbc_copy_to_clipboard( text ); } catch ( e ) {}
674 }
675
676 try {
677 if ( w.isSecureContext && navigator.clipboard && navigator.clipboard.writeText ) {
678 await navigator.clipboard.writeText( text );
679 return true;
680 }
681 } catch ( e ) {}
682
683 try {
684 var ta = d.createElement( 'textarea' );
685 ta.value = text;
686 ta.setAttribute( 'readonly', '' );
687 ta.style.position = 'fixed';
688 ta.style.top = '-9999px';
689 ta.style.opacity = '0';
690 d.body.appendChild( ta );
691 ta.focus();
692 ta.select();
693 var ok = d.execCommand( 'copy' );
694 d.body.removeChild( ta );
695 return !! ok;
696 } catch ( e ) {
697 return false;
698 }
699 }
700
701 function feedback_button(btn, ok) {
702 if ( ! btn ) {
703 return;
704 }
705
706 var original = btn.getAttribute( 'data-wpbc-original-text' ) || btn.textContent;
707 btn.setAttribute( 'data-wpbc-original-text', original );
708
709 btn.textContent = ok ? 'Copied!' : 'Press Ctrl/Cmd+C to copy';
710 setTimeout( function () {
711 btn.textContent = original;
712 }, 1500 );
713 }
714
715 async function copy_editor_value(key, btn) {
716 Editor.ensure_inited();
717
718 var ok = await copy_text( Editor.get_value( key ) );
719 if ( ! ok ) {
720 Editor.focus_select( key );
721 }
722
723 feedback_button( btn, ok );
724 }
725
726 return {
727 copy_editor_value: copy_editor_value
728 };
729 })();
730
731 // == UI bindings
732 var UI = (function () {
733
734 function bind_ui() {
735
736 DOM.on( DOM.get( IDS.btn_regen ), 'click', function (e) {
737 e.preventDefault();
738 Editor.ensure_inited();
739 Sync.regenerate();
740 Editor.refresh_all();
741
742 // Safe default until we can confirm sync state.
743 var cb = DOM.get( IDS.cb_autosync );
744 if ( cb ) {
745 cb.checked = true;
746 State.update_badges();
747 }
748 } );
749
750 DOM.on( DOM.get( IDS.cb_autosync ), 'change', function () {
751
752 var cb = DOM.get( IDS.cb_autosync );
753 if ( ! cb ) {
754 return;
755 }
756
757 State.raw.autosync_user_value = !! cb.checked; // explicit user choice
758 State.raw.sync_state_bootstrapped = true;
759
760 if ( cb.checked ) {
761 State.set_dirty( false );
762 Editor.ensure_inited();
763
764 if ( ! Sync.regenerate() ) {
765 Sync.schedule_detect( 'autosync_user_on_wait_ready' );
766 }
767
768 Editor.refresh_all();
769 }
770
771 State.update_badges();
772 } );
773
774 DOM.on( DOM.get( IDS.btn_copy_form ), 'click', function (e) {
775 e.preventDefault();
776 Clipboard.copy_editor_value( KEY.FORM, DOM.get( IDS.btn_copy_form ) );
777 } );
778
779 DOM.on( DOM.get( IDS.btn_copy_cnt ), 'click', function (e) {
780 e.preventDefault();
781 Clipboard.copy_editor_value( KEY.CONTENT, DOM.get( IDS.btn_copy_cnt ) );
782 } );
783 }
784
785 return {
786 bind: bind_ui
787 };
788 })();
789
790 // == WP / Builder events ========================================================================================
791 var Events = (function () {
792
793 function hook_events() {
794
795 /**
796 * Infer apply source without relying on any "advanced_source" key.
797 *
798 * Returns:
799 * - 'builder' : if texts match current Builder export (when export is ready)
800 * - 'advanced' : if texts do not match Builder export (but have any content)
801 * - 'auto' : if Builder export is not available/ready (safe default)
802 *
803 * @param {string} af
804 * @param {string} cf
805 * @return {'builder'|'advanced'|'auto'}
806 */
807 function infer_apply_source(af, cf) {
808
809 af = (af == null) ? '' : String(af);
810 cf = (cf == null) ? '' : String(cf);
811
812 function has_text(v) {
813 return !! (v && String(v).trim());
814 }
815
816 function normalize_style_value(css) {
817 css = (css == null) ? '' : String( css );
818
819 css = css.replace( /\r\n/g, '\n' ).replace( /\r/g, '\n' );
820 css = css.replace( /\t/g, ' ' );
821 css = css.replace( /\s+/g, ' ' );
822
823 // Normalize spacing around separators.
824 css = css.replace( /\s*;\s*/g, ';' );
825 css = css.replace( /\s*:\s*/g, ':' );
826
827 // Ensure stable trailing semicolon (important for compare).
828 css = css.trim().replace( /;+\s*$/g, '' );
829 if ( css !== '' ) {
830 css += ';';
831 }
832 return css;
833 }
834
835 function normalize_inline_styles_in_markup(html) {
836 html = (html == null) ? '' : String( html );
837
838 // Handles: style="..." and style='...' (with optional spaces around '=')
839 return html.replace( /\bstyle\s*=\s*(["'])(.*?)\1/gi, function (_m, quote, css) {
840 return 'style=' + quote + normalize_style_value( css ) + quote;
841 } );
842 }
843
844 function normalize_text(s) {
845 s = (s == null) ? '' : String(s);
846 s = s.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
847
848 // IMPORTANT: normalize inline styles so "flex:1" == "flex:1;"
849 s = normalize_inline_styles_in_markup(s);
850
851 s = s.replace(/[ \t]+$/gm, '');
852 s = s.replace(/[ ]{2,}/g, ' ');
853 s = s.replace(/^\s+/gm, '');
854 s = s.replace(/\n{3,}/g, '\n\n');
855 return s.trim();
856 }
857
858 function export_output_looks_ready(out) {
859 if (!out) return false;
860 var a = normalize_text(out.advanced_form || '');
861 var b = normalize_text(out.fields_data || '');
862 if (!a && !b) return false;
863 // If structure is loaded and there are NO fields, accept empty export.
864 if ( structure_loaded_once && ! adapted_has_any_field( out.adapted ) ) {
865 return true;
866 }
867 // Expect layout tags to exist in real export.
868 var has_layout_tags = /<\s*(r|c|item)\b/i.test(a) || /<\s*(r|c|item)\b/i.test(b);
869 var has_any_item = /<\s*item\b/i.test(a) || /<\s*item\b/i.test(b);
870 return has_layout_tags && has_any_item;
871 }
872
873 function try_get_builder_export() {
874 try {
875 if (
876 !w.wpbc_bfb ||
877 typeof w.wpbc_bfb.get_structure !== 'function' ||
878 !w.WPBC_BFB_Exporter ||
879 typeof w.WPBC_BFB_Exporter.export_all !== 'function'
880 ) {
881 return null;
882 }
883 var out = w.WPBC_BFB_Exporter.export_all(w.wpbc_bfb.get_structure(), { gapPercent: 3 });
884 if (!export_output_looks_ready(out)) {
885 return null;
886 }
887 return out;
888 } catch (_e) {
889 return null;
890 }
891 }
892
893 var out = try_get_builder_export();
894 if (!out) {
895 return 'auto';
896 }
897
898 var in_af = normalize_text(af);
899 var in_cf = normalize_text(cf);
900 var ex_af = normalize_text(out.advanced_form || '');
901 var ex_cf = normalize_text(out.fields_data || '');
902
903 if (in_af === ex_af && in_cf === ex_cf) {
904 return 'builder';
905 }
906
907 if (has_text(af) || has_text(cf)) {
908 return 'advanced';
909 }
910
911 return 'auto';
912 }
913
914
915 d.addEventListener( 'wpbc:bfb:structure:change', function () {
916 if ( ! Editor.ensure_inited() ) {
917 return;
918 }
919
920 if ( State.raw.is_dirty ) {
921 return;
922 }
923
924 if ( State.raw.autosync_user_value === false ) {
925 return;
926 }
927
928 var cb = DOM.get( IDS.cb_autosync );
929 if ( ! cb ) {
930 return;
931 }
932
933 if ( ! State.raw.sync_state_bootstrapped ) {
934 Sync.schedule_detect_debounced( 'structure_change_pre_bootstrap' );
935 return;
936 }
937
938 Sync.schedule_detect_debounced( 'structure_change' );
939 } );
940
941 d.addEventListener( 'wpbc:bfb:structure:loaded', function () {
942 structure_loaded_once = true;
943 Sync.schedule_detect( 'structure_loaded' );
944 } );
945
946 d.addEventListener( 'wpbc:bfb:top-tab', function (ev) {
947
948 var tab_id = (ev && ev.detail && ev.detail.tab) ? String( ev.detail.tab ) : '';
949 var is_inner = (tab_id === 'advanced_mode__booking_form' || tab_id === 'advanced_mode__booking_data');
950
951 // Entering Advanced Mode (root tab) or switching inner tabs.
952 if ( tab_id !== 'advanced_tab' && ! is_inner ) {
953 return;
954 }
955
956 if ( ! Editor.ensure_inited() ) {
957 return;
958 }
959
960 if ( State.is_autosync_on() ) {
961 if ( ! Sync.regenerate() ) {
962 Sync.schedule_detect( 'top_tab_wait_ready' );
963 }
964 }
965
966
967 setTimeout( Editor.refresh_all, 60 );
968 if ( tab_id === 'advanced_mode__booking_data' ) {
969 setTimeout( Editor.refresh_all, 120 );
970 }
971 } );
972
973 d.addEventListener( 'wpbc:bfb:advanced_text:apply', function (ev) {
974
975 var det = (ev && ev.detail) ? ev.detail : {};
976 var af = (det.advanced_form == null) ? '' : String( det.advanced_form );
977 var cf = (det.content_form == null) ? '' : String( det.content_form );
978
979 var src = String( det.advanced_mode_source || 'auto' ).toLowerCase(); // builder|advanced|auto.
980 if ( src !== 'builder' && src !== 'advanced' && src !== 'auto' ) {
981 src = 'auto';
982 }
983 if ( src === 'auto' ) {
984 src = infer_apply_source( af, cf ); // builder|advanced|auto.
985 }
986
987
988 var cb = DOM.get( IDS.cb_autosync );
989
990 // Always use Editor API (keeps textarea + CodeMirror in sync).
991 Editor.ensure_inited();
992 Editor.set_value( KEY.FORM, af );
993 Editor.set_value( KEY.CONTENT, cf );
994 setTimeout( Editor.refresh_all, 60 );
995
996 if ( src === 'advanced' ) {
997 // Advanced is authoritative => dirty + autosync OFF.
998 if ( cb ) { cb.checked = false; }
999 State.set_dirty( true ); // also sets autosync_user_value=false and bootstrapped=true .
1000
1001 } else if ( src === 'builder' ) {
1002 // Builder is authoritative => autosync ON.
1003 if ( cb ) { cb.checked = true; }
1004 State.raw.autosync_user_value = true;
1005 State.raw.sync_state_bootstrapped = true;
1006 State.set_dirty( false );
1007
1008 } else {
1009 // auto/unknown: Safe default = autosync OFF until we confirm real sync state. This prevents Preview using Builder before export is ready / comparison is done.
1010 if ( cb ) { cb.checked = false; }
1011 State.raw.autosync_user_value = null;
1012 State.raw.sync_state_bootstrapped = false;
1013 State.set_dirty( false );
1014 }
1015
1016 State.update_badges();
1017 Sync.schedule_detect( 'advanced_text_apply' );
1018 } );
1019
1020
1021 }
1022
1023 return {
1024 hook: hook_events
1025 };
1026 })();
1027
1028 // == Public API (unchanged) ======================================================================================
1029 w.wpbc_bfb_advanced_editor_api = w.wpbc_bfb_advanced_editor_api || {};
1030
1031 w.wpbc_bfb_advanced_editor_api.get_values = function () {
1032
1033 // Ensure CodeMirror is ready (if enabled).
1034 Editor.ensure_inited();
1035
1036 // Push CodeMirror -> textarea (no-op if not inited).
1037 Editor.save_all();
1038
1039 // In your system, "manual mode" (autosync OFF) must be treated as "use Advanced".
1040 var use_advanced = !! State.raw.is_dirty || ! State.is_autosync_on();
1041
1042 return {
1043 advanced_form: Editor.get_value( KEY.FORM ),
1044 content_form : Editor.get_value( KEY.CONTENT ),
1045 is_dirty : use_advanced
1046 };
1047 };
1048
1049
1050 w.wpbc_bfb_advanced_editor_api.set_dirty = function (state) {
1051 State.set_dirty( !! state );
1052 };
1053
1054 // == Boot =======================================================================================================
1055 d.addEventListener( 'DOMContentLoaded', function () {
1056
1057 UI.bind();
1058 Events.hook();
1059
1060 if ( DOM.has_ui() ) {
1061 Editor.ensure_inited();
1062 setTimeout( Editor.refresh_all, 60 );
1063
1064 // Safe default until we can confirm sync state.
1065 var cb = DOM.get( IDS.cb_autosync );
1066 if ( cb && State.raw.autosync_user_value === null && ! State.raw.sync_state_bootstrapped ) {
1067 cb.checked = false;
1068 State.update_badges();
1069 }
1070 }
1071
1072 // Initial autosync checkbox state (based on real sync status).
1073 Sync.schedule_detect( 'boot' );
1074 } );
1075
1076 })( window, document );
1077