PluginProbe
Booking Calendar / 10.15.6
Booking Calendar v10.15.6
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 10.15.6, at includes/page-form-builder/preview/_src/bfb-preview.js

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