PluginProbe
Booking Calendar / 11.1
Booking Calendar v11.1
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 10.11.2 All 203 releases
booking / includes / page-form-builder / preview / _src / bfb-preview.js

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

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