PluginProbe
Booking Calendar / 11.4.3
Booking Calendar v11.4.3
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.4.3, at includes/page-form-builder/preview/_src/bfb-preview.js

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