PluginProbe
Booking Calendar / 11.6.1
Booking Calendar v11.6.1
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 / preview / _src / bfb-preview.js

bfb-preview.js in Booking Calendar 11.6.1, at includes/page-form-builder/preview/_src/bfb-preview.js

1,163 lines 33.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/preview/_out/bfb-preview.js
3 // == BFB Preview Client — sends current structure via AJAX and loads preview URL into iframe
4 // ---------------------------------------------------------------------------------------------------------------------
5 (function (w, d) {
6 'use strict';
7
8 /**
9 * Simple logger alias (optional).
10 *
11 * @type {{log:Function, error:Function}}
12 */
13 var dev = (w._wpbc && w._wpbc.dev) ? w._wpbc.dev : {
14 log: function () {},
15 error: function () {}
16 };
17
18 /**
19 * Preview client class.
20 * Binds to a preview panel root that contains button + iframe.
21 */
22 class wpbc_bfb_preview_client {
23
24 /**
25 * @param {HTMLElement} root_el Root element of the preview panel.
26 */
27 constructor(root_el) {
28 this.root_el = root_el;
29 this.iframe = root_el.querySelector( '[data-wpbc-bfb-preview-iframe="1"]' );
30 this.loader = root_el.querySelector( '[data-wpbc-bfb-preview-loader="1"]' ); // NEW
31 this.button = null; // No local "Update Preview" button in panel anymore.
32
33 this.nonce = root_el.getAttribute( 'data-preview-nonce' ) || '';
34 this.is_busy = false;
35
36 if ( !this.iframe ) {
37 dev.error( 'wpbc_bfb_preview_client', 'Missing iframe element' );
38 return;
39 }
40
41 this.bind_events();
42 }
43
44
45 /**
46 * Show/hide the overlay loader over the iframe.
47 *
48 * @param {boolean} is_visible
49 */
50 set_loader_visible(is_visible) {
51 if ( !this.loader ) {
52 return;
53 }
54 if ( is_visible ) {
55 this.loader.classList.add( 'is-visible' );
56 } else {
57 this.loader.classList.remove( 'is-visible' );
58 }
59 }
60
61 /**
62 * Bind UI events (panel-local button, if present).
63 */
64 bind_events() {
65 var self = this;
66
67 if ( ! this.button ) {
68 return;
69 }
70
71 this.button.addEventListener( 'click', function () {
72 self.update_preview();
73 } );
74 }
75
76 /**
77 * Get current BFB structure from global builder.
78 *
79 * @returns {Object|null}
80 */
81 get_current_structure() {
82 if ( ! w.wpbc_bfb || typeof w.wpbc_bfb.get_structure !== 'function' ) {
83 dev.error( 'wpbc_bfb_preview_client', 'wpbc_bfb.get_structure() is not available' );
84 return null;
85 }
86
87 try {
88 return w.wpbc_bfb.get_structure();
89 } catch (e) {
90 dev.error( 'wpbc_bfb_preview_client.get_current_structure', e );
91 return null;
92 }
93 }
94
95 /**
96 * Parse combined length value like "100%", "320px", "12.5rem".
97 *
98 * @param {string} value
99 * @return {{num:string, unit:string}}
100 */
101 parse_length_value(value) {
102 var raw = String(value == null ? '' : value).trim();
103 var m = raw.match(/^\s*(-?\d+(?:\.\d+)?)\s*([a-z%]*)\s*$/i);
104 if (!m) {
105 return { num: raw, unit: '' };
106 }
107 return { num: (m[1] || ''), unit: (m[2] || '') };
108 }
109
110 /**
111 * Build current form settings payload (same structure as real Save):
112 * {
113 * options: { ...source-of-truth... },
114 * css_vars: { ...compiled... }
115 * }
116 *
117 * @param {string} form_name
118 * @returns {{options:Object, css_vars:Object}}
119 */
120 get_current_form_settings(form_name) {
121
122 var form_settings = {
123 options : {},
124 css_vars: {}
125 };
126
127 // --- same event contract as ajax/_out/bfb-ajax.js ----------------------------------------------
128 wpbc_bfb__dispatch_event_safe( 'wpbc:bfb:form_settings:collect', {
129 settings : form_settings,
130 form_name: form_name || 'standard'
131 } );
132
133 // Strict: require correct shape.
134 if ( !form_settings || typeof form_settings !== 'object' ) {
135 form_settings = { options: {}, css_vars: {} };
136 }
137 if ( !form_settings.options || typeof form_settings.options !== 'object' ) {
138 form_settings.options = {};
139 }
140 if ( !form_settings.css_vars || typeof form_settings.css_vars !== 'object' ) {
141 form_settings.css_vars = {};
142 }
143
144 return form_settings;
145 }
146
147 /**
148 * Read a Form Style control directly from DOM.
149 *
150 * @param {string} key
151 * @returns {string|null}
152 */
153 read_form_style_control_value(key) {
154 var control = null;
155 var checked = null;
156
157 if ( key === 'booking_form_style' ) {
158 checked = d.querySelector( 'input[type="radio"][name="booking_form_style"]:checked' );
159 return checked ? String( checked.value || '' ) : null;
160 }
161
162 control = d.querySelector( '[data-wpbc-bfb-fs-key="' + key + '"]' );
163 if ( ! control ) {
164 control = d.getElementById( key );
165 }
166 if ( ! control ) {
167 return null;
168 }
169
170 if ( control.matches && control.matches( 'input[type="radio"]' ) ) {
171 checked = d.querySelector( 'input[type="radio"][name="' + control.name + '"]:checked' );
172 return checked ? String( checked.value || '' ) : null;
173 }
174 if ( control.matches && control.matches( 'input[type="checkbox"]' ) ) {
175 return control.checked ? 'On' : 'Off';
176 }
177
178 if ( typeof control.value !== 'undefined' ) {
179 return String( control.value == null ? '' : control.value );
180 }
181
182 return null;
183 }
184
185 /**
186 * Build the current global Form Style override for preview only.
187 *
188 * @returns {Object}
189 */
190 get_current_form_style_settings() {
191 var style_keys = [
192 'booking_form_style',
193 'booking_form_accent_enabled',
194 'booking_form_accent_color',
195 'booking_form_custom_background_color',
196 'booking_form_custom_border_color',
197 'booking_form_custom_border_width',
198 'booking_form_custom_border_radius',
199 'booking_form_custom_padding_vertical',
200 'booking_form_custom_padding_horizontal',
201 'booking_form_custom_text_color',
202 'booking_form_custom_field_background_color',
203 'booking_form_custom_field_text_color',
204 'booking_form_custom_field_border_color',
205 'booking_form_custom_button_background_color',
206 'booking_form_custom_button_text_color',
207 'booking_form_custom_button_border_color',
208 'booking_form_custom_button_hover_background_color',
209 'booking_form_custom_button_hover_text_color',
210 'booking_form_custom_button_hover_border_color',
211 'booking_form_custom_secondary_button_background_color',
212 'booking_form_custom_secondary_button_text_color',
213 'booking_form_custom_secondary_button_border_color',
214 'booking_form_custom_secondary_button_hover_background_color',
215 'booking_form_custom_secondary_button_hover_text_color',
216 'booking_form_custom_secondary_button_hover_border_color'
217 ];
218 var out = {};
219 var collected = {};
220 var self = this;
221 var localized = w.wpbc_bfb_settings_vars && w.wpbc_bfb_settings_vars.global_form_style
222 ? w.wpbc_bfb_settings_vars.global_form_style
223 : {};
224
225 if ( w.WPBC_BFB_FormSettings && typeof w.WPBC_BFB_FormSettings.collect === 'function' ) {
226 collected = w.WPBC_BFB_FormSettings.collect( 'global' ) || {};
227 }
228
229 style_keys.forEach( function (key) {
230 var dom_value = self.read_form_style_control_value( key );
231 if ( null !== dom_value ) {
232 out[key] = dom_value;
233 } else if ( Object.prototype.hasOwnProperty.call( collected, key ) ) {
234 out[key] = collected[key];
235 } else if ( Object.prototype.hasOwnProperty.call( localized, key ) ) {
236 out[key] = localized[key];
237 }
238 } );
239
240 return out;
241 }
242
243 /**
244 * Get the currently selected calendar skin URL from the Builder page.
245 *
246 * @returns {string}
247 */
248 get_selected_calendar_skin_url() {
249 var select_el = d.querySelector( '.js-wpbc-bfb-calendar-skin' );
250 if ( select_el && select_el.options ) {
251 var selected_option = select_el.options[ select_el.selectedIndex ];
252 if ( selected_option ) {
253 var selected_url = selected_option.getAttribute( 'data-wpbc-calendar-skin-url' );
254 if ( selected_url ) {
255 return String( selected_url );
256 }
257 }
258 }
259
260 var stylesheet = d.getElementById( 'wpbc-calendar-skin-css' );
261 if ( stylesheet && stylesheet.href ) {
262 return String( stylesheet.href );
263 }
264
265 return '';
266 }
267
268 /**
269 * Apply the unsaved Builder calendar skin inside the loaded Preview iframe.
270 *
271 * @param {string} skin_url
272 * @param {Function} done
273 * @param {number} tries
274 */
275 apply_calendar_skin_to_iframe(skin_url, done, tries) {
276 var self = this;
277 var attempt_num = Number( tries || 0 );
278
279 if ( ! skin_url || ! this.iframe || ! this.iframe.contentWindow ) {
280 if ( typeof done === 'function' ) {
281 done();
282 }
283 return;
284 }
285
286 try {
287 if ( typeof this.iframe.contentWindow.wpbc__calendar__change_skin === 'function' ) {
288 this.iframe.contentWindow.wpbc__calendar__change_skin( skin_url );
289 if ( typeof done === 'function' ) {
290 done();
291 }
292 return;
293 }
294 } catch ( e ) {
295 dev.error( 'wpbc_bfb_preview_client.apply_calendar_skin_to_iframe', e );
296 }
297
298 if ( attempt_num >= 20 ) {
299 if ( typeof done === 'function' ) {
300 done();
301 }
302 return;
303 }
304
305 w.setTimeout( function () {
306 self.apply_calendar_skin_to_iframe( skin_url, done, attempt_num + 1 );
307 }, 100 );
308 }
309
310 /**
311 * Send snapshot to server and update iframe src with returned preview URL.
312 *
313 * @param {{source_button?:HTMLElement}} [opts]
314 */
315 update_preview( opts ) {
316 if ( this.is_busy ) {
317 return;
318 }
319
320 var structure = this.get_current_structure();
321 if ( ! structure ) {
322 return;
323 }
324
325 this.set_loader_visible( true );
326
327 var options = opts || {};
328 var source_btn = options.source_button || null;
329
330 var $source_btn = null;
331 if ( source_btn && w.jQuery && typeof w.wpbc_bfb__button_busy_start === 'function' ) {
332 $source_btn = w.jQuery( source_btn );
333 if ( $source_btn && $source_btn.length ) {
334 w.wpbc_bfb__button_busy_start( $source_btn );
335 }
336 }
337
338 this.is_busy = true;
339 this.set_button_busy( true ); // local "Update Preview" button in the panel
340
341 var payload = new w.FormData();
342 var cfg = w.WPBC_BFB_Ajax || {};
343
344 // Build settings payload (same as real saving).
345 var form_settings = this.get_current_form_settings( cfg.form_name || 'standard' );
346 var preview_form_style = this.get_current_form_style_settings();
347
348 payload.append( 'action', 'WPBC_AJX_BFB_SAVE_FORM_CONFIG' );
349 payload.append( 'nonce', ( cfg.nonce_save || this.nonce || '' ) );
350
351 // IMPORTANT:
352 // - form_name is the logical form key (usually 'standard' or selected form)
353 payload.append( 'form_name', ( cfg.form_name || 'standard' ) );
354 payload.append( 'status', 'preview' );
355 payload.append( 'return_preview_url', '1' );
356
357 payload.append( 'engine', ( cfg.engine || 'bfb' ) );
358 payload.append( 'engine_version', ( cfg.engine_version || '1.0' ) );
359
360 payload.append( 'structure', JSON.stringify( structure ) );
361
362 // Send real settings (options + compiled css_vars).
363 payload.append( 'settings', JSON.stringify( form_settings ) );
364 payload.append( 'preview_form_style', JSON.stringify( preview_form_style ) );
365
366 // ----------------------------------------------------------------------------
367 // Choose where advanced_form + content_form are taken from (auto|builder|advanced)
368 // ----------------------------------------------------------------------------
369 var preview_source = wpbc_bfb_preview__get_source( cfg, source_btn );
370 payload.append( 'preview_source', preview_source );
371
372 var adv = null;
373
374 // 1) Try Advanced Mode text (if selected / auto+dirty)
375 if ( preview_source === 'advanced' || preview_source === 'auto' ) {
376
377 adv = wpbc_bfb_preview__read_advanced_mode_payload( d, w );
378
379 var can_use_advanced =
380 (preview_source === 'advanced') ||
381 (preview_source === 'auto' && adv && adv.is_dirty);
382
383 if ( can_use_advanced ) {
384
385 // Forced "advanced" but empty -> fallback to builder export.
386 if ( ! wpbc_bfb_preview__has_text( adv.advanced_form ) && ! wpbc_bfb_preview__has_text( adv.content_form ) ) {
387
388 if ( typeof w.wpbc_admin_show_message === 'function' ) {
389 w.wpbc_admin_show_message(
390 'Advanced Mode is selected, but editors are empty. Using Builder export.',
391 'warning',
392 6000
393 );
394 }
395
396 } else {
397
398 if ( wpbc_bfb_preview__has_text( adv.advanced_form ) ) {
399 payload.append( 'advanced_form', adv.advanced_form );
400 }
401 if ( wpbc_bfb_preview__has_text( adv.content_form ) ) {
402 payload.append( 'content_form', adv.content_form );
403 }
404
405 }
406 }
407 }
408
409 // 2) If not taken from Advanced Mode -> export from Builder structure (current behavior)
410 var already_has_adv =
411 payload.has && (payload.has( 'advanced_form' ) || payload.has( 'content_form' )); // some browsers
412 // support
413 // FormData.has
414
415 // Fallback for old browsers (no FormData.has)
416 if ( typeof payload.has !== 'function' ) {
417 already_has_adv = false; // just try exporter if needed
418 }
419
420 if ( ! already_has_adv ) {
421
422 if ( w.WPBC_BFB_Exporter && typeof w.WPBC_BFB_Exporter.export_all === 'function' ) {
423 try {
424
425 var width_combined = form_settings.options.booking_form_layout_width || '';
426 var parsed_width = this.parse_length_value( width_combined );
427 var width_unit = parsed_width.unit ? parsed_width.unit : '%';
428
429 var export_options = {
430 gapPercent : 3,
431 form_slug : (cfg.form_name || 'standard'),
432 form_width_value: parsed_width.num,
433 form_width_unit : width_unit
434 };
435
436 var export_result = w.WPBC_BFB_Exporter.export_all( structure || [], export_options );
437
438 if ( export_result ) {
439 if ( export_result.advanced_form ) {
440 payload.append( 'advanced_form', export_result.advanced_form );
441 }
442 if ( export_result.fields_data ) {
443 payload.append( 'content_form', export_result.fields_data );
444 }
445 }
446
447 } catch ( e ) {
448 console.error( 'WPBC BFB: export_all error', e );
449 }
450 }
451 }
452
453
454 // Use shared helper if available; fallback to ajaxurl.
455 var ajax_url = '';
456 if ( typeof w.wpbc_bfb__get_ajax_url === 'function' ) {
457 ajax_url = w.wpbc_bfb__get_ajax_url();
458 } else if ( typeof w.ajaxurl !== 'undefined' && w.ajaxurl ) {
459 ajax_url = w.ajaxurl;
460 }
461
462 var self = this;
463 var preview_calendar_skin_url = this.get_selected_calendar_skin_url();
464
465 function end_busy_now() {
466 self._end_busy( $source_btn );
467 }
468
469 if ( ! ajax_url ) {
470 dev.error( 'wpbc_bfb_preview_client', 'ajax URL is not defined' );
471 end_busy_now();
472 return;
473 }
474
475 w.fetch( ajax_url, {
476 method: 'POST',
477 body: payload
478 } )
479 .then( function (response) {
480 return response.json();
481 } )
482 .then( function (data) {
483 if ( ! data || ! data.success || ! data.data || ! data.data.preview_url ) {
484 dev.error( 'wpbc_bfb_preview_client', 'Preview AJAX error', data );
485 end_busy_now();
486 return;
487 }
488
489 if ( ! self.iframe ) {
490 dev.error( 'wpbc_bfb_preview_client', 'Missing iframe element' );
491 end_busy_now();
492 return;
493 }
494
495 // Wait until iframe has really loaded the preview URL.
496 var on_load = function () {
497 self.iframe.removeEventListener( 'load', on_load );
498 self.apply_calendar_skin_to_iframe( preview_calendar_skin_url, end_busy_now );
499 };
500
501 self.iframe.addEventListener( 'load', on_load );
502 self.iframe.setAttribute( 'src', data.data.preview_url );
503 } )
504 .catch( function (err) {
505 dev.error( 'wpbc_bfb_preview_client', 'Preview AJAX failed', err );
506 end_busy_now();
507 } );
508 }
509
510 /**
511 * Internal helper: stop busy state on panel button + optional top toolbar button.
512 *
513 * @param {jQuery|null} $source_btn
514 * @private
515 */
516 _end_busy($source_btn) {
517 this.is_busy = false;
518 this.set_button_busy( false );
519 this.set_loader_visible( false ); // NEW
520
521 if ( $source_btn && typeof w.wpbc_bfb__button_busy_end === 'function' ) {
522 w.wpbc_bfb__button_busy_end( $source_btn );
523 }
524 }
525
526 /**
527 * Force-reset any busy states related to preview.
528 * This is called when switching back to Builder mode (soft-cancel UX).
529 */
530 reset_busy_state() {
531 this.is_busy = false;
532 this.set_button_busy( false ); // panel button, if any
533 this.set_loader_visible( false ); // NEW
534
535 if ( w.jQuery && typeof w.wpbc_bfb__button_busy_end === 'function' ) {
536 // Top toolbar buttons that may show "Loading preview..."
537 var $btns = w.jQuery(
538 '[data-wpbc-bfb-top-preview-btn="1"],' +
539 '[data-wpbc-bfb-top-refresh-btn="1"]'
540 );
541
542 $btns.each( function () {
543 var $b = w.jQuery( this );
544 if ( $b.hasClass( 'wpbc-is-busy' ) ) {
545 w.wpbc_bfb__button_busy_end( $b );
546 }
547 } );
548 }
549 }
550
551 /**
552 * Simple busy indicator for the panel "Update Preview" button.
553 * Reuses global WPBC busy helpers if available, so the spinner matches Save/Load buttons.
554 *
555 * @param {boolean} is_busy
556 */
557 set_button_busy( is_busy ) {
558 if ( ! this.button ) {
559 return;
560 }
561
562 // If jQuery + global helpers exist, use same spinner as other toolbar buttons.
563 if ( w.jQuery && typeof w.wpbc_bfb__button_busy_start === 'function' && typeof w.wpbc_bfb__button_busy_end === 'function' ) {
564 var $btn = w.jQuery( this.button );
565
566 if ( is_busy ) {
567 w.wpbc_bfb__button_busy_start( $btn );
568 } else {
569 w.wpbc_bfb__button_busy_end( $btn );
570 }
571
572 return;
573 }
574
575 // Fallback: simple disabled state without spinner.
576 if ( is_busy ) {
577 this.button.setAttribute( 'disabled', 'disabled' );
578 this.button.classList.add( 'wpbc_bfb__preview_btn_busy' );
579 } else {
580 this.button.removeAttribute( 'disabled' );
581 this.button.classList.remove( 'wpbc_bfb__preview_btn_busy' );
582 }
583 }
584
585 /**
586 * Set current view mode: "builder" or "preview".
587 * Applies/removes CSS class on <body>.
588 *
589 * @param {('builder'|'preview')} mode
590 */
591 set_mode( mode ) {
592 var body = d.body;
593 if ( ! body ) {
594 return;
595 }
596
597 if ( mode === 'preview' ) {
598 body.classList.add( 'wpbc_bfb__mode_preview' );
599 } else {
600 body.classList.remove( 'wpbc_bfb__mode_preview' );
601 }
602 }
603
604 }
605
606 // -- Helpers ------------------------------------------------------------------------------------------------------
607 /**
608 * Is Preview mode currently active?
609 * We prefer body class (set by client.set_mode), fallback to active tab id.
610 *
611 * @return {boolean}
612 */
613 function wpbc_bfb_preview__is_preview_mode_active() {
614 var body = d.body;
615 if ( body && body.classList && body.classList.contains( 'wpbc_bfb__mode_preview' ) ) {
616 return true;
617 }
618 return (wpbc_bfb_preview__get_active_top_tab_id() === 'preview_tab');
619 }
620
621 /**
622 * When a form is loaded via AJAX while Preview tab is active,
623 * refresh the iframe so it shows the newly loaded form.
624 *
625 * @param {wpbc_bfb_preview_client} client
626 */
627 function wpbc_bfb_preview__bind_form_ajax_loaded_events(client) {
628
629 if ( ! client ) {
630 return;
631 }
632
633 // Prevent double binding if this script is injected twice.
634 if ( w.__wpbc_bfb_preview__form_ajax_loaded_bound === '1' ) {
635 return;
636 }
637 w.__wpbc_bfb_preview__form_ajax_loaded_bound = '1';
638
639 var debounce_id = null;
640
641 d.addEventListener(
642 'wpbc:bfb:form:ajax_loaded',
643 function (ev) {
644
645 // Only do anything if Preview is currently active.
646 if ( ! wpbc_bfb_preview__is_preview_mode_active() ) {
647 return;
648 }
649
650 var det = (ev && ev.detail) ? ev.detail : {};
651 var fn = det.form_name ? String( det.form_name ) : '';
652
653 // Best-effort: keep cfg.form_name in sync for payload building.
654 if ( fn ) {
655 w.WPBC_BFB_Ajax = w.WPBC_BFB_Ajax || {};
656 w.WPBC_BFB_Ajax.form_name = fn;
657 }
658
659 // Debounce multiple rapid loads (or secondary events).
660 if ( debounce_id ) {
661 clearTimeout( debounce_id );
662 }
663
664 debounce_id = setTimeout( function () {
665 debounce_id = null;
666
667 // Preview might have been left while waiting.
668 if ( ! wpbc_bfb_preview__is_preview_mode_active() ) {
669 return;
670 }
671
672 // If we are already sending preview, don't stack another request.
673 if ( client.is_busy ) {
674 // Try once more shortly (safe).
675 setTimeout( function () {
676 if ( wpbc_bfb_preview__is_preview_mode_active() && ! client.is_busy ) {
677 client.update_preview( { source_button: null } );
678 }
679 }, 250 );
680 return;
681 }
682
683 // Wait until builder API is present (form load can be async).
684 var tries = 0;
685 (function wait_for_builder() {
686
687 if ( ! wpbc_bfb_preview__is_preview_mode_active() ) {
688 return;
689 }
690
691 if ( w.wpbc_bfb && typeof w.wpbc_bfb.get_structure === 'function' ) {
692 client.update_preview( { source_button: null } );
693 return;
694 }
695
696 tries++;
697 if ( tries < 15 ) {
698 setTimeout( wait_for_builder, 200 );
699 }
700 })();
701 }, 180 );
702
703 },
704 { passive: true }
705 );
706 }
707
708 function wpbc_bfb_preview__get_source(cfg, btn) {
709
710 // priority: button attr -> global cfg -> default
711 var v = '';
712 try {
713 if ( btn && btn.getAttribute ) {
714 v = btn.getAttribute( 'data-wpbc-bfb-preview-source' ) || btn.getAttribute( 'data-wpbc-bfb-save-source' ) || '';
715 }
716 } catch ( e ) {}
717
718 if ( ! v && cfg && (cfg.preview_source || cfg.save_source) ) {
719 v = cfg.preview_source || cfg.save_source;
720 }
721
722 v = String( v || 'auto' ).toLowerCase();
723 if ( [ 'builder', 'advanced', 'auto' ].indexOf( v ) === -1 ) {
724 v = 'builder';
725 }
726 return v;
727 }
728
729 function wpbc_bfb_preview__has_text(v) {
730 return !! (v && String( v ).trim());
731 }
732
733 function wpbc_bfb_preview__read_advanced_mode_payload(d, w) {
734
735 // best: API (syncs CodeMirror -> textarea)
736 if ( w.wpbc_bfb_advanced_editor_api && typeof w.wpbc_bfb_advanced_editor_api.get_values === 'function' ) {
737 try {
738 return w.wpbc_bfb_advanced_editor_api.get_values();
739 } catch ( e ) {}
740 }
741
742 // fallback: read textareas directly
743 var ta_form = d.getElementById( 'wpbc_bfb__advanced_form_editor' );
744 var ta_content = d.getElementById( 'wpbc_bfb__content_form_editor' );
745
746 return {
747 advanced_form: ta_form ? String( ta_form.value || '' ) : '',
748 content_form : ta_content ? String( ta_content.value || '' ) : '',
749 is_dirty : false
750 };
751 }
752
753 /**
754 * Is this element the Preview TAB link in top tabs nav?
755 *
756 * @param {HTMLElement|null} el
757 * @return {boolean}
758 */
759 function wpbc_bfb_preview__is_preview_tab_link(el) {
760 if ( !el || !el.getAttribute ) {
761 return false;
762 }
763 return (
764 el.getAttribute( 'data-wpbc-bfb-action' ) === 'panel' &&
765 el.getAttribute( 'data-wpbc-bfb-tab' ) === 'preview_tab'
766 );
767 }
768
769 /**
770 * Is this element the Builder TAB link in top tabs nav?
771 *
772 * @param {HTMLElement|null} el
773 * @return {boolean}
774 */
775 function wpbc_bfb_preview__is_builder_tab_link(el) {
776 if ( !el || !el.getAttribute ) {
777 return false;
778 }
779 return (
780 el.getAttribute( 'data-wpbc-bfb-action' ) === 'panel' &&
781 el.getAttribute( 'data-wpbc-bfb-tab' ) === 'builder_tab'
782 );
783 }
784
785 /**
786 * Activate a "panel" tab and show its panel section (scoped).
787 *
788 * Strategy:
789 * 1) Try to click the real top-tab link (best).
790 * 2) If link does not exist (e.g. Preview tab removed from nav), do a scoped manual switch:
791 * - use nav's data-wpbc-bfb-panels-root + data-wpbc-bfb-panel-class
792 * - hide/show ONLY those outer panels
793 * - DO NOT touch inner panels
794 *
795 * @param {string} tab_id
796 * @return {boolean}
797 */
798 function wpbc_bfb__activate_panel_tab(tab_id) {
799
800 tab_id = String( tab_id || '' ).trim();
801 if ( ! tab_id ) {
802 return false;
803 }
804
805 // 1) Best: trigger existing top-tabs switching by clicking the tab link (if it exists).
806 var tab_link = d.querySelector( '[data-wpbc-bfb-action="panel"][data-wpbc-bfb-tab="' + tab_id + '"]' );
807 if ( tab_link && typeof tab_link.click === 'function' ) {
808 try {
809 tab_link.click();
810 return true;
811 } catch ( _e ) {}
812 }
813
814 // 2) Fallback: scoped manual toggle (outer panels only).
815 var nav = d.getElementById( 'wpbc_bfb__top_horisontal_nav' );
816
817 // Determine outer panels root.
818 var panels_root = d;
819 var panels_root_selector = '';
820
821 if ( nav ) {
822 panels_root_selector = nav.getAttribute( 'data-wpbc-bfb-panels-root' ) || '';
823 panels_root_selector = String( panels_root_selector || '' ).trim();
824 }
825
826 // Fallback if attribute missing: prefer #wpbc_bfb__top_panels if present.
827 if ( ! panels_root_selector ) {
828 panels_root_selector = '#wpbc_bfb__top_panels';
829 }
830
831 if ( panels_root_selector ) {
832 try {
833 var root_el = d.querySelector( panels_root_selector );
834 if ( root_el ) {
835 panels_root = root_el;
836 }
837 } catch ( _e2 ) {}
838 }
839
840 // Determine outer base class (IMPORTANT: must be the OUTER base, not wpbc_bfb__tab_section).
841 var panel_base_class = 'wpbc_bfb__top_tab_section';
842 if ( nav ) {
843 var from_nav = nav.getAttribute( 'data-wpbc-bfb-panel-class' ) || '';
844 from_nav = String( from_nav || '' ).trim();
845 if ( from_nav ) {
846 panel_base_class = from_nav;
847 }
848 }
849
850 // Update active tab on nav (source of truth).
851 if ( nav ) {
852 nav.setAttribute( 'data-active-tab', tab_id );
853 }
854
855 // Hide ONLY outer panels (scoped by base class).
856 var selector_all = '.' + panel_base_class;
857 var panel_nodes = panels_root.querySelectorAll( selector_all );
858
859 for ( var i = 0; i < panel_nodes.length; i++ ) {
860 panel_nodes[i].style.display = 'none';
861 }
862
863 // Show requested outer panel.
864 var selector_active = '.' + panel_base_class + '__' + tab_id;
865 var active_panel = panels_root.querySelector( selector_active );
866
867 if ( ! active_panel ) {
868 // Backward-compatible fallback (if someone forgot to add outer classes).
869 active_panel = panels_root.querySelector( '.wpbc_bfb__tab_section__' + tab_id );
870 }
871
872 if ( active_panel ) {
873 active_panel.style.display = '';
874 }
875
876 // Mark active item in outer nav if it exists (Preview can be missing).
877 if ( nav ) {
878 var items = nav.querySelectorAll( '.wpbc_ui_el__horis_nav_item' );
879 for ( var j = 0; j < items.length; j++ ) {
880 items[j].classList.remove( 'active' );
881 }
882
883 var active_item = nav.querySelector( '.wpbc_ui_el__horis_nav_item__' + tab_id );
884 if ( active_item ) {
885 active_item.classList.add( 'active' );
886 }
887 }
888
889 // Emit same event as bfb-top-tabs.js so other modules stay in sync.
890 try {
891 d.dispatchEvent(
892 new CustomEvent( 'wpbc:bfb:top-tab', {
893 detail: {
894 tab : tab_id,
895 nav_id: (nav && nav.id) ? String( nav.id ) : ''
896 }
897 } )
898 );
899 } catch ( _e3 ) {}
900
901 return true;
902 }
903
904
905 /**
906 * Get currently active top tab id from the nav container.
907 *
908 * @return {string}
909 */
910 function wpbc_bfb_preview__get_active_top_tab_id() {
911 var nav = d.getElementById( 'wpbc_bfb__top_horisontal_nav' );
912 if ( !nav ) {
913 return '';
914 }
915 return nav.getAttribute( 'data-active-tab' ) || '';
916 }
917
918 /**
919 * Sync preview "mode" with currently active top tab.
920 *
921 * Rules:
922 * - preview_tab => enable preview mode (buttons make sense)
923 * - any other tab => disable preview mode + reset busy states
924 *
925 * IMPORTANT:
926 * - We do NOT change the active top tab here (no panel switching).
927 * We only sync preview UI mode.
928 *
929 * @param {string} tab_id
930 * @param {wpbc_bfb_preview_client} client
931 */
932 function wpbc_bfb_preview__sync_mode_to_tab(tab_id, client) {
933
934 tab_id = String( tab_id || '' );
935
936 if ( !client ) {
937 return;
938 }
939
940 if ( 'preview_tab' === tab_id ) {
941 client.set_mode( 'preview' );
942 return;
943 }
944
945 // Leaving preview -> soft-cancel busy state and hide preview-specific toolbar.
946 if ( typeof client.reset_busy_state === 'function' ) {
947 client.reset_busy_state();
948 }
949 client.set_mode( 'builder' );
950 }
951
952 /**
953 * Bind to top-tab switch event emitted by bfb-top-tabs.js.
954 *
955 * @param {wpbc_bfb_preview_client} client
956 */
957 function wpbc_bfb_preview__bind_top_tab_events(client) {
958
959 // Sync immediately (in case event already fired before this script init).
960 wpbc_bfb_preview__sync_mode_to_tab( wpbc_bfb_preview__get_active_top_tab_id(), client );
961
962 // React to future tab changes.
963 d.addEventListener( 'wpbc:bfb:top-tab', function (ev) {
964 var det = (ev && ev.detail) ? ev.detail : {};
965 var tab_id = det.tab ? String( det.tab ) : '';
966 var nav_id = det.nav_id ? String( det.nav_id ) : '';
967 var prev_id = det.prev_tab ? String( det.prev_tab ) : '';
968
969 // If user switched to preview_tab in the MAIN top nav,
970 // remember what tab they came from.
971 if ( nav_id === TOP_NAV_ID && tab_id === 'preview_tab' && prev_id && prev_id !== 'preview_tab' ) {
972 return_tab_id = prev_id;
973 }
974
975 wpbc_bfb_preview__sync_mode_to_tab( tab_id, client );
976 } );
977
978 }
979
980
981 // -- Bind | Init on Load ------------------------------------------------------------------------------------------
982 var TOP_NAV_ID = 'wpbc_bfb__top_horisontal_nav';
983 var return_tab_id = 'builder_tab';
984
985 function remember_return_tab_from_dom() {
986 var cur = wpbc_bfb_preview__get_active_top_tab_id();
987 if (cur && cur !== 'preview_tab') {
988 return_tab_id = cur;
989 }
990 }
991
992 function get_return_tab_id() {
993 var t = String(return_tab_id || '').trim();
994 if (!t || t === 'preview_tab') t = 'builder_tab';
995 return t;
996 }
997
998 /**
999 * Auto-init preview client on Builder page and wire top toolbar buttons.
1000 */
1001 function wpbc_bfb_init_preview_client() {
1002 var root = d.querySelector( '[data-wpbc-bfb-preview-root="1"]' );
1003
1004 if ( !root ) {
1005 return;
1006 }
1007
1008 var client = new wpbc_bfb_preview_client( root );
1009
1010 if ( !w.WPBC_BFB_Preview ) {
1011 w.WPBC_BFB_Preview = {
1012 client: client,
1013
1014 show_preview: function (opts) {
1015 var opt = opts || {};
1016 var src = opt.source_button || null;
1017
1018 // Remember where we were BEFORE switching to preview.
1019 remember_return_tab_from_dom();
1020
1021 if ( ! wpbc_bfb_preview__is_preview_tab_link( src ) ) {
1022 wpbc_bfb__activate_panel_tab( 'preview_tab' );
1023 }
1024
1025 // Enable preview mode UI (shows refresh/back buttons etc).
1026 client.set_mode( 'preview' );
1027
1028 // IMPORTANT: Always regenerate when show_preview() is called (Preview tab click included).
1029 client.update_preview( { source_button: src } );
1030 },
1031
1032
1033 show_builder: function (opts) {
1034 var opt = opts || {};
1035 var src = opt.source_button || null;
1036
1037 if ( client && typeof client.reset_busy_state === 'function' ) {
1038 client.reset_busy_state();
1039 }
1040
1041 var back_to = get_return_tab_id();
1042
1043 // If Back button itself is not a real tab link -> just activate remembered tab.
1044 // If activation fails (tab removed), fallback to builder_tab.
1045 if ( ! wpbc_bfb_preview__is_builder_tab_link( src ) ) {
1046 if ( ! wpbc_bfb__activate_panel_tab( back_to ) ) {
1047 wpbc_bfb__activate_panel_tab( 'builder_tab' );
1048 }
1049 }
1050
1051 client.set_mode( 'builder' );
1052 },
1053
1054 show_advanced_tab: function (opts) {
1055 var opt = opts || {};
1056
1057 if ( client && typeof client.reset_busy_state === 'function' ) {
1058 client.reset_busy_state();
1059 }
1060 wpbc_bfb__activate_panel_tab( 'advanced_tab' );
1061 client.set_mode( 'builder' );
1062 }
1063
1064
1065 };
1066 }
1067
1068 // Listen for top-tab changes and sync preview mode automatically.
1069 wpbc_bfb_preview__bind_top_tab_events( client );
1070
1071 // When a form is loaded via AJAX, refresh preview (only if preview mode is active).
1072 wpbc_bfb_preview__bind_form_ajax_loaded_events( client );
1073
1074 wpbc_bfb_bind_top_toolbar_buttons( client );
1075 }
1076
1077
1078 /**
1079 * Bind top toolbar Builder / Preview / Refresh buttons.
1080 * Supports multiple elements for each action (toolbar buttons, top tabs, etc.)
1081 * via data-wpbc-bfb-top-*-btn="1".
1082 */
1083 function wpbc_bfb_bind_top_toolbar_buttons(client) {
1084
1085 var btn_preview_list = d.querySelectorAll( '[data-wpbc-bfb-top-preview-btn="1"]' );
1086 var btn_builder_list = d.querySelectorAll( '[data-wpbc-bfb-top-builder-btn="1"]' );
1087 var btn_refresh_list = d.querySelectorAll( '[data-wpbc-bfb-top-refresh-btn="1"]' );
1088
1089 function for_each_node(list, cb) {
1090 if ( !list || !list.length ) {
1091 return;
1092 }
1093 Array.prototype.forEach.call( list, function (el) {
1094 if ( el && typeof cb === 'function' ) {
1095 cb( el );
1096 }
1097 } );
1098 }
1099
1100 for_each_node( btn_preview_list, function (btn_preview) {
1101
1102 // Prevent double binding if this script runs twice.
1103 if ( btn_preview.getAttribute( 'data-wpbc-bfb-bound' ) === '1' ) {
1104 return;
1105 }
1106 btn_preview.setAttribute( 'data-wpbc-bfb-bound', '1' );
1107
1108 btn_preview.addEventListener( 'click', function (e) {
1109 e.preventDefault();
1110
1111 if ( w.WPBC_BFB_Preview && typeof w.WPBC_BFB_Preview.show_preview === 'function' ) {
1112 w.WPBC_BFB_Preview.show_preview( {
1113 source_button: btn_preview
1114 } );
1115 }
1116 } );
1117 } );
1118
1119 for_each_node( btn_refresh_list, function (btn_refresh) {
1120
1121 if ( btn_refresh.getAttribute( 'data-wpbc-bfb-bound' ) === '1' ) {
1122 return;
1123 }
1124 btn_refresh.setAttribute( 'data-wpbc-bfb-bound', '1' );
1125
1126 btn_refresh.addEventListener( 'click', function (e) {
1127 e.preventDefault();
1128
1129 if ( w.WPBC_BFB_Preview && typeof w.WPBC_BFB_Preview.show_preview === 'function' ) {
1130 w.WPBC_BFB_Preview.show_preview( {
1131 update : true,
1132 source_button: btn_refresh
1133 } );
1134 }
1135 } );
1136 } );
1137
1138 for_each_node( btn_builder_list, function (btn_builder) {
1139
1140 if ( btn_builder.getAttribute( 'data-wpbc-bfb-bound' ) === '1' ) {
1141 return;
1142 }
1143 btn_builder.setAttribute( 'data-wpbc-bfb-bound', '1' );
1144
1145 btn_builder.addEventListener( 'click', function (e) {
1146 e.preventDefault();
1147
1148 if ( w.WPBC_BFB_Preview && typeof w.WPBC_BFB_Preview.show_builder === 'function' ) {
1149 w.WPBC_BFB_Preview.show_builder( { source_button: btn_builder } );
1150 }
1151 } );
1152 } );
1153 }
1154
1155
1156 if ( d.readyState === 'complete' || d.readyState === 'interactive' ) {
1157 setTimeout( wpbc_bfb_init_preview_client, 0 );
1158 } else {
1159 d.addEventListener( 'DOMContentLoaded', wpbc_bfb_init_preview_client );
1160 }
1161
1162 })( window, document );
1163