# booking/11.4/includes/page-form-builder/preview/_src/bfb-preview.js

Booking Calendar, version 11.4. 1,158 lines.

- Page: https://pluginprobe.com/plugins/booking/11.4/code/includes/page-form-builder/preview/_src/bfb-preview.js
- Raw: https://pluginprobe.com/plugins/booking/11.4/raw/includes/page-form-builder/preview/_src/bfb-preview.js
- Modified: 2026-07-13T07:52:42+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/booking/11.4/code/includes/page-form-builder/preview/_src/bfb-preview.js#L10-L20`.

```javascript
// ---------------------------------------------------------------------------------------------------------------------
// == File  /includes/page-form-builder/preview/_out/bfb-preview.js
// == BFB Preview Client — sends current structure via AJAX and loads preview URL into iframe
// ---------------------------------------------------------------------------------------------------------------------
(function (w, d) {
	'use strict';

	/**
	 * Simple logger alias (optional).
	 *
	 * @type {{log:Function, error:Function}}
	 */
	var dev = (w._wpbc && w._wpbc.dev) ? w._wpbc.dev : {
		log: function () {},
		error: function () {}
	};

	/**
	 * Preview client class.
	 * Binds to a preview panel root that contains button + iframe.
	 */
	class wpbc_bfb_preview_client {

		/**
		 * @param {HTMLElement} root_el Root element of the preview panel.
		 */
		constructor(root_el) {
			this.root_el = root_el;
			this.iframe  = root_el.querySelector( '[data-wpbc-bfb-preview-iframe="1"]' );
			this.loader  = root_el.querySelector( '[data-wpbc-bfb-preview-loader="1"]' ); // NEW
			this.button  = null; // No local "Update Preview" button in panel anymore.

			this.nonce   = root_el.getAttribute( 'data-preview-nonce' ) || '';
			this.is_busy = false;

			if ( !this.iframe ) {
				dev.error( 'wpbc_bfb_preview_client', 'Missing iframe element' );
				return;
			}

			this.bind_events();
		}


		/**
		 * Show/hide the overlay loader over the iframe.
		 *
		 * @param {boolean} is_visible
		 */
		set_loader_visible(is_visible) {
			if ( !this.loader ) {
				return;
			}
			if ( is_visible ) {
				this.loader.classList.add( 'is-visible' );
			} else {
				this.loader.classList.remove( 'is-visible' );
			}
		}

		/**
		 * Bind UI events (panel-local button, if present).
		 */
		bind_events() {
			var self = this;

			if ( ! this.button ) {
				return;
			}

			this.button.addEventListener( 'click', function () {
				self.update_preview();
			} );
		}

		/**
		 * Get current BFB structure from global builder.
		 *
		 * @returns {Object|null}
		 */
		get_current_structure() {
			if ( ! w.wpbc_bfb || typeof w.wpbc_bfb.get_structure !== 'function' ) {
				dev.error( 'wpbc_bfb_preview_client', 'wpbc_bfb.get_structure() is not available' );
				return null;
			}

			try {
				return w.wpbc_bfb.get_structure();
			} catch (e) {
				dev.error( 'wpbc_bfb_preview_client.get_current_structure', e );
				return null;
			}
		}

		/**
		 * Parse combined length value like "100%", "320px", "12.5rem".
		 *
		 * @param {string} value
		 * @return {{num:string, unit:string}}
		 */
		parse_length_value(value) {
			var raw = String(value == null ? '' : value).trim();
			var m = raw.match(/^\s*(-?\d+(?:\.\d+)?)\s*([a-z%]*)\s*$/i);
			if (!m) {
				return { num: raw, unit: '' };
			}
			return { num: (m[1] || ''), unit: (m[2] || '') };
		}

		/**
		 * Build current form settings payload (same structure as real Save):
		 * {
		 *   options: { ...source-of-truth... },
		 *   css_vars: { ...compiled... }
		 * }
		 *
		 * @param {string} form_name
		 * @returns {{options:Object, css_vars:Object}}
		 */
		get_current_form_settings(form_name) {

			var form_settings = {
				options : {},
				css_vars: {}
			};

			// --- same event contract as ajax/_out/bfb-ajax.js ----------------------------------------------
			wpbc_bfb__dispatch_event_safe( 'wpbc:bfb:form_settings:collect', {
				settings : form_settings,
				form_name: form_name || 'standard'
			} );

			// Strict: require correct shape.
			if ( !form_settings || typeof form_settings !== 'object' ) {
				form_settings = { options: {}, css_vars: {} };
			}
			if ( !form_settings.options || typeof form_settings.options !== 'object' ) {
				form_settings.options = {};
			}
			if ( !form_settings.css_vars || typeof form_settings.css_vars !== 'object' ) {
				form_settings.css_vars = {};
			}

			return form_settings;
		}

		/**
		 * Read a Form Style control directly from DOM.
		 *
		 * @param {string} key
		 * @returns {string|null}
		 */
		read_form_style_control_value(key) {
			var control = null;
			var checked = null;

			if ( key === 'booking_form_style' ) {
				checked = d.querySelector( 'input[type="radio"][name="booking_form_style"]:checked' );
				return checked ? String( checked.value || '' ) : null;
			}

			control = d.querySelector( '[data-wpbc-bfb-fs-key="' + key + '"]' );
			if ( ! control ) {
				control = d.getElementById( key );
			}
			if ( ! control ) {
				return null;
			}

			if ( control.matches && control.matches( 'input[type="radio"]' ) ) {
				checked = d.querySelector( 'input[type="radio"][name="' + control.name + '"]:checked' );
				return checked ? String( checked.value || '' ) : null;
			}

			if ( typeof control.value !== 'undefined' ) {
				return String( control.value == null ? '' : control.value );
			}

			return null;
		}

		/**
		 * Build the current global Form Style override for preview only.
		 *
		 * @returns {Object}
		 */
		get_current_form_style_settings() {
			var style_keys = [
				'booking_form_style',
				'booking_form_custom_background_color',
				'booking_form_custom_border_color',
				'booking_form_custom_border_width',
				'booking_form_custom_border_radius',
				'booking_form_custom_padding_vertical',
				'booking_form_custom_padding_horizontal',
				'booking_form_custom_text_color',
				'booking_form_custom_field_background_color',
				'booking_form_custom_field_text_color',
				'booking_form_custom_field_border_color',
				'booking_form_custom_button_background_color',
				'booking_form_custom_button_text_color',
				'booking_form_custom_button_border_color',
				'booking_form_custom_button_hover_background_color',
				'booking_form_custom_button_hover_text_color',
				'booking_form_custom_button_hover_border_color',
				'booking_form_custom_secondary_button_background_color',
				'booking_form_custom_secondary_button_text_color',
				'booking_form_custom_secondary_button_border_color',
				'booking_form_custom_secondary_button_hover_background_color',
				'booking_form_custom_secondary_button_hover_text_color',
				'booking_form_custom_secondary_button_hover_border_color'
			];
			var out = {};
			var collected = {};
			var self = this;
			var localized = w.wpbc_bfb_settings_vars && w.wpbc_bfb_settings_vars.global_form_style
				? w.wpbc_bfb_settings_vars.global_form_style
				: {};

			if ( w.WPBC_BFB_FormSettings && typeof w.WPBC_BFB_FormSettings.collect === 'function' ) {
				collected = w.WPBC_BFB_FormSettings.collect( 'global' ) || {};
			}

			style_keys.forEach( function (key) {
				var dom_value = self.read_form_style_control_value( key );
				if ( null !== dom_value ) {
					out[key] = dom_value;
				} else if ( Object.prototype.hasOwnProperty.call( collected, key ) ) {
					out[key] = collected[key];
				} else if ( Object.prototype.hasOwnProperty.call( localized, key ) ) {
					out[key] = localized[key];
				}
			} );

			return out;
		}

		/**
		 * Get the currently selected calendar skin URL from the Builder page.
		 *
		 * @returns {string}
		 */
		get_selected_calendar_skin_url() {
			var select_el = d.querySelector( '.js-wpbc-bfb-calendar-skin' );
			if ( select_el && select_el.options ) {
				var selected_option = select_el.options[ select_el.selectedIndex ];
				if ( selected_option ) {
					var selected_url = selected_option.getAttribute( 'data-wpbc-calendar-skin-url' );
					if ( selected_url ) {
						return String( selected_url );
					}
				}
			}

			var stylesheet = d.getElementById( 'wpbc-calendar-skin-css' );
			if ( stylesheet && stylesheet.href ) {
				return String( stylesheet.href );
			}

			return '';
		}

		/**
		 * Apply the unsaved Builder calendar skin inside the loaded Preview iframe.
		 *
		 * @param {string} skin_url
		 * @param {Function} done
		 * @param {number} tries
		 */
		apply_calendar_skin_to_iframe(skin_url, done, tries) {
			var self        = this;
			var attempt_num = Number( tries || 0 );

			if ( ! skin_url || ! this.iframe || ! this.iframe.contentWindow ) {
				if ( typeof done === 'function' ) {
					done();
				}
				return;
			}

			try {
				if ( typeof this.iframe.contentWindow.wpbc__calendar__change_skin === 'function' ) {
					this.iframe.contentWindow.wpbc__calendar__change_skin( skin_url );
					if ( typeof done === 'function' ) {
						done();
					}
					return;
				}
			} catch ( e ) {
				dev.error( 'wpbc_bfb_preview_client.apply_calendar_skin_to_iframe', e );
			}

			if ( attempt_num >= 20 ) {
				if ( typeof done === 'function' ) {
					done();
				}
				return;
			}

			w.setTimeout( function () {
				self.apply_calendar_skin_to_iframe( skin_url, done, attempt_num + 1 );
			}, 100 );
		}

		/**
		 * Send snapshot to server and update iframe src with returned preview URL.
		 *
		 * @param {{source_button?:HTMLElement}} [opts]
		 */
		update_preview( opts ) {
			if ( this.is_busy ) {
				return;
			}

			var structure = this.get_current_structure();
			if ( ! structure ) {
				return;
			}

			this.set_loader_visible( true );

			var options    = opts || {};
			var source_btn = options.source_button || null;

			var $source_btn = null;
			if ( source_btn && w.jQuery && typeof w.wpbc_bfb__button_busy_start === 'function' ) {
				$source_btn = w.jQuery( source_btn );
				if ( $source_btn && $source_btn.length ) {
					w.wpbc_bfb__button_busy_start( $source_btn );
				}
			}

			this.is_busy = true;
			this.set_button_busy( true ); // local "Update Preview" button in the panel

			var payload = new w.FormData();
			var cfg     = w.WPBC_BFB_Ajax || {};

			// Build settings payload (same as real saving).
			var form_settings = this.get_current_form_settings( cfg.form_name || 'standard' );
			var preview_form_style = this.get_current_form_style_settings();

			payload.append( 'action', 'WPBC_AJX_BFB_SAVE_FORM_CONFIG' );
			payload.append( 'nonce', ( cfg.nonce_save || this.nonce || '' ) );

			// IMPORTANT:
			// - form_name is the logical form key (usually 'standard' or selected form)
			payload.append( 'form_name', ( cfg.form_name || 'standard' ) );
			payload.append( 'status', 'preview' );
			payload.append( 'return_preview_url', '1' );

			payload.append( 'engine', ( cfg.engine || 'bfb' ) );
			payload.append( 'engine_version', ( cfg.engine_version || '1.0' ) );

			payload.append( 'structure', JSON.stringify( structure ) );

			// Send real settings (options + compiled css_vars).
			payload.append( 'settings', JSON.stringify( form_settings ) );
			payload.append( 'preview_form_style', JSON.stringify( preview_form_style ) );

			// ----------------------------------------------------------------------------
			// Choose where advanced_form + content_form are taken from (auto|builder|advanced)
			// ----------------------------------------------------------------------------
			var preview_source = wpbc_bfb_preview__get_source( cfg, source_btn );
			payload.append( 'preview_source', preview_source );

			var adv = null;

			// 1) Try Advanced Mode text (if selected / auto+dirty)
			if ( preview_source === 'advanced' || preview_source === 'auto' ) {

				adv = wpbc_bfb_preview__read_advanced_mode_payload( d, w );

				var can_use_advanced =
						(preview_source === 'advanced') ||
						(preview_source === 'auto' && adv && adv.is_dirty);

				if ( can_use_advanced ) {

					// Forced "advanced" but empty -> fallback to builder export.
					if ( ! wpbc_bfb_preview__has_text( adv.advanced_form ) && ! wpbc_bfb_preview__has_text( adv.content_form ) ) {

						if ( typeof w.wpbc_admin_show_message === 'function' ) {
							w.wpbc_admin_show_message(
								'Advanced Mode is selected, but editors are empty. Using Builder export.',
								'warning',
								6000
							);
						}

					} else {

						if ( wpbc_bfb_preview__has_text( adv.advanced_form ) ) {
							payload.append( 'advanced_form', adv.advanced_form );
						}
						if ( wpbc_bfb_preview__has_text( adv.content_form ) ) {
							payload.append( 'content_form', adv.content_form );
						}

					}
				}
			}

			// 2) If not taken from Advanced Mode -> export from Builder structure (current behavior)
			var already_has_adv =
					payload.has && (payload.has( 'advanced_form' ) || payload.has( 'content_form' )); // some browsers
																									  // support
																									  // FormData.has

			// Fallback for old browsers (no FormData.has)
			if ( typeof payload.has !== 'function' ) {
				already_has_adv = false; // just try exporter if needed
			}

			if ( ! already_has_adv ) {

				if ( w.WPBC_BFB_Exporter && typeof w.WPBC_BFB_Exporter.export_all === 'function' ) {
					try {

						var width_combined = form_settings.options.booking_form_layout_width || '';
						var parsed_width   = this.parse_length_value( width_combined );
						var width_unit     = parsed_width.unit ? parsed_width.unit : '%';

						var export_options = {
							gapPercent      : 3,
							form_slug       : (cfg.form_name || 'standard'),
							form_width_value: parsed_width.num,
							form_width_unit : width_unit
						};

						var export_result = w.WPBC_BFB_Exporter.export_all( structure || [], export_options );

						if ( export_result ) {
							if ( export_result.advanced_form ) {
								payload.append( 'advanced_form', export_result.advanced_form );
							}
							if ( export_result.fields_data ) {
								payload.append( 'content_form', export_result.fields_data );
							}
						}

					} catch ( e ) {
						console.error( 'WPBC BFB: export_all error', e );
					}
				}
			}


			// Use shared helper if available; fallback to ajaxurl.
			var ajax_url = '';
			if ( typeof w.wpbc_bfb__get_ajax_url === 'function' ) {
				ajax_url = w.wpbc_bfb__get_ajax_url();
			} else if ( typeof w.ajaxurl !== 'undefined' && w.ajaxurl ) {
				ajax_url = w.ajaxurl;
			}

			var self = this;
			var preview_calendar_skin_url = this.get_selected_calendar_skin_url();

			function end_busy_now() {
				self._end_busy( $source_btn );
			}

			if ( ! ajax_url ) {
				dev.error( 'wpbc_bfb_preview_client', 'ajax URL is not defined' );
				end_busy_now();
				return;
			}

			w.fetch( ajax_url, {
				method: 'POST',
				body:   payload
			} )
			.then( function (response) {
				return response.json();
			} )
			.then( function (data) {
				if ( ! data || ! data.success || ! data.data || ! data.data.preview_url ) {
					dev.error( 'wpbc_bfb_preview_client', 'Preview AJAX error', data );
					end_busy_now();
					return;
				}

				if ( ! self.iframe ) {
					dev.error( 'wpbc_bfb_preview_client', 'Missing iframe element' );
					end_busy_now();
					return;
				}

				// Wait until iframe has really loaded the preview URL.
				var on_load = function () {
					self.iframe.removeEventListener( 'load', on_load );
					self.apply_calendar_skin_to_iframe( preview_calendar_skin_url, end_busy_now );
				};

				self.iframe.addEventListener( 'load', on_load );
				self.iframe.setAttribute( 'src', data.data.preview_url );
			} )
			.catch( function (err) {
				dev.error( 'wpbc_bfb_preview_client', 'Preview AJAX failed', err );
				end_busy_now();
			} );
		}

		/**
		 * Internal helper: stop busy state on panel button + optional top toolbar button.
		 *
		 * @param {jQuery|null} $source_btn
		 * @private
		 */
		_end_busy($source_btn) {
			this.is_busy = false;
			this.set_button_busy( false );
			this.set_loader_visible( false ); // NEW

			if ( $source_btn && typeof w.wpbc_bfb__button_busy_end === 'function' ) {
				w.wpbc_bfb__button_busy_end( $source_btn );
			}
		}

		/**
		 * Force-reset any busy states related to preview.
		 * This is called when switching back to Builder mode (soft-cancel UX).
		 */
		reset_busy_state() {
			this.is_busy = false;
			this.set_button_busy( false ); // panel button, if any
			this.set_loader_visible( false ); // NEW

			if ( w.jQuery && typeof w.wpbc_bfb__button_busy_end === 'function' ) {
				// Top toolbar buttons that may show "Loading preview..."
				var $btns = w.jQuery(
					'[data-wpbc-bfb-top-preview-btn="1"],' +
					'[data-wpbc-bfb-top-refresh-btn="1"]'
				);

				$btns.each( function () {
					var $b = w.jQuery( this );
					if ( $b.hasClass( 'wpbc-is-busy' ) ) {
						w.wpbc_bfb__button_busy_end( $b );
					}
				} );
			}
		}

		/**
		 * Simple busy indicator for the panel "Update Preview" button.
		 * Reuses global WPBC busy helpers if available, so the spinner matches Save/Load buttons.
		 *
		 * @param {boolean} is_busy
		 */
		set_button_busy( is_busy ) {
			if ( ! this.button ) {
				return;
			}

			// If jQuery + global helpers exist, use same spinner as other toolbar buttons.
			if ( w.jQuery && typeof w.wpbc_bfb__button_busy_start === 'function' && typeof w.wpbc_bfb__button_busy_end === 'function' ) {
				var $btn = w.jQuery( this.button );

				if ( is_busy ) {
					w.wpbc_bfb__button_busy_start( $btn );
				} else {
					w.wpbc_bfb__button_busy_end( $btn );
				}

				return;
			}

			// Fallback: simple disabled state without spinner.
			if ( is_busy ) {
				this.button.setAttribute( 'disabled', 'disabled' );
				this.button.classList.add( 'wpbc_bfb__preview_btn_busy' );
			} else {
				this.button.removeAttribute( 'disabled' );
				this.button.classList.remove( 'wpbc_bfb__preview_btn_busy' );
			}
		}

		/**
		 * Set current view mode: "builder" or "preview".
		 * Applies/removes CSS class on <body>.
		 *
		 * @param {('builder'|'preview')} mode
		 */
		set_mode( mode ) {
			var body = d.body;
			if ( ! body ) {
				return;
			}

			if ( mode === 'preview' ) {
				body.classList.add( 'wpbc_bfb__mode_preview' );
			} else {
				body.classList.remove( 'wpbc_bfb__mode_preview' );
			}
		}

	}

	// -- Helpers ------------------------------------------------------------------------------------------------------
	/**
	 * Is Preview mode currently active?
	 * We prefer body class (set by client.set_mode), fallback to active tab id.
	 *
	 * @return {boolean}
	 */
	function wpbc_bfb_preview__is_preview_mode_active() {
		var body = d.body;
		if ( body && body.classList && body.classList.contains( 'wpbc_bfb__mode_preview' ) ) {
			return true;
		}
		return (wpbc_bfb_preview__get_active_top_tab_id() === 'preview_tab');
	}

	/**
	 * When a form is loaded via AJAX while Preview tab is active,
	 * refresh the iframe so it shows the newly loaded form.
	 *
	 * @param {wpbc_bfb_preview_client} client
	 */
	function wpbc_bfb_preview__bind_form_ajax_loaded_events(client) {

		if ( ! client ) {
			return;
		}

		// Prevent double binding if this script is injected twice.
		if ( w.__wpbc_bfb_preview__form_ajax_loaded_bound === '1' ) {
			return;
		}
		w.__wpbc_bfb_preview__form_ajax_loaded_bound = '1';

		var debounce_id = null;

		d.addEventListener(
			'wpbc:bfb:form:ajax_loaded',
			function (ev) {

				// Only do anything if Preview is currently active.
				if ( ! wpbc_bfb_preview__is_preview_mode_active() ) {
					return;
				}

				var det = (ev && ev.detail) ? ev.detail : {};
				var fn  = det.form_name ? String( det.form_name ) : '';

				// Best-effort: keep cfg.form_name in sync for payload building.
				if ( fn ) {
					w.WPBC_BFB_Ajax           = w.WPBC_BFB_Ajax || {};
					w.WPBC_BFB_Ajax.form_name = fn;
				}

				// Debounce multiple rapid loads (or secondary events).
				if ( debounce_id ) {
					clearTimeout( debounce_id );
				}

				debounce_id = setTimeout( function () {
					debounce_id = null;

					// Preview might have been left while waiting.
					if ( ! wpbc_bfb_preview__is_preview_mode_active() ) {
						return;
					}

					// If we are already sending preview, don't stack another request.
					if ( client.is_busy ) {
						// Try once more shortly (safe).
						setTimeout( function () {
							if ( wpbc_bfb_preview__is_preview_mode_active() && ! client.is_busy ) {
								client.update_preview( { source_button: null } );
							}
						}, 250 );
						return;
					}

					// Wait until builder API is present (form load can be async).
					var tries = 0;
					(function wait_for_builder() {

						if ( ! wpbc_bfb_preview__is_preview_mode_active() ) {
							return;
						}

						if ( w.wpbc_bfb && typeof w.wpbc_bfb.get_structure === 'function' ) {
							client.update_preview( { source_button: null } );
							return;
						}

						tries++;
						if ( tries < 15 ) {
							setTimeout( wait_for_builder, 200 );
						}
					})();
				}, 180 );

			},
			{ passive: true }
		);
	}

	function wpbc_bfb_preview__get_source(cfg, btn) {

		// priority: button attr -> global cfg -> default
		var v = '';
		try {
			if ( btn && btn.getAttribute ) {
				v = btn.getAttribute( 'data-wpbc-bfb-preview-source' ) || btn.getAttribute( 'data-wpbc-bfb-save-source' ) || '';
			}
		} catch ( e ) {}

		if ( ! v && cfg && (cfg.preview_source || cfg.save_source) ) {
			v = cfg.preview_source || cfg.save_source;
		}

		v = String( v || 'auto' ).toLowerCase();
		if ( [ 'builder', 'advanced', 'auto' ].indexOf( v ) === -1 ) {
			v = 'builder';
		}
		return v;
	}

	function wpbc_bfb_preview__has_text(v) {
		return !! (v && String( v ).trim());
	}

	function wpbc_bfb_preview__read_advanced_mode_payload(d, w) {

		// best: API (syncs CodeMirror -> textarea)
		if ( w.wpbc_bfb_advanced_editor_api && typeof w.wpbc_bfb_advanced_editor_api.get_values === 'function' ) {
			try {
				return w.wpbc_bfb_advanced_editor_api.get_values();
			} catch ( e ) {}
		}

		// fallback: read textareas directly
		var ta_form    = d.getElementById( 'wpbc_bfb__advanced_form_editor' );
		var ta_content = d.getElementById( 'wpbc_bfb__content_form_editor' );

		return {
			advanced_form: ta_form ? String( ta_form.value || '' ) : '',
			content_form : ta_content ? String( ta_content.value || '' ) : '',
			is_dirty     : false
		};
	}

	/**
	 * Is this element the Preview TAB link in top tabs nav?
	 *
	 * @param {HTMLElement|null} el
	 * @return {boolean}
	 */
	function wpbc_bfb_preview__is_preview_tab_link(el) {
		if ( !el || !el.getAttribute ) {
			return false;
		}
		return (
			el.getAttribute( 'data-wpbc-bfb-action' ) === 'panel' &&
			el.getAttribute( 'data-wpbc-bfb-tab' ) === 'preview_tab'
		);
	}

	/**
	 * Is this element the Builder TAB link in top tabs nav?
	 *
	 * @param {HTMLElement|null} el
	 * @return {boolean}
	 */
	function wpbc_bfb_preview__is_builder_tab_link(el) {
		if ( !el || !el.getAttribute ) {
			return false;
		}
		return (
			el.getAttribute( 'data-wpbc-bfb-action' ) === 'panel' &&
			el.getAttribute( 'data-wpbc-bfb-tab' ) === 'builder_tab'
		);
	}

	/**
	 * Activate a "panel" tab and show its panel section (scoped).
	 *
	 * Strategy:
	 * 1) Try to click the real top-tab link (best).
	 * 2) If link does not exist (e.g. Preview tab removed from nav), do a scoped manual switch:
	 *    - use nav's data-wpbc-bfb-panels-root + data-wpbc-bfb-panel-class
	 *    - hide/show ONLY those outer panels
	 *    - DO NOT touch inner panels
	 *
	 * @param {string} tab_id
	 * @return {boolean}
	 */
	function wpbc_bfb__activate_panel_tab(tab_id) {

		tab_id = String( tab_id || '' ).trim();
		if ( ! tab_id ) {
			return false;
		}

		// 1) Best: trigger existing top-tabs switching by clicking the tab link (if it exists).
		var tab_link = d.querySelector( '[data-wpbc-bfb-action="panel"][data-wpbc-bfb-tab="' + tab_id + '"]' );
		if ( tab_link && typeof tab_link.click === 'function' ) {
			try {
				tab_link.click();
				return true;
			} catch ( _e ) {}
		}

		// 2) Fallback: scoped manual toggle (outer panels only).
		var nav = d.getElementById( 'wpbc_bfb__top_horisontal_nav' );

		// Determine outer panels root.
		var panels_root          = d;
		var panels_root_selector = '';

		if ( nav ) {
			panels_root_selector = nav.getAttribute( 'data-wpbc-bfb-panels-root' ) || '';
			panels_root_selector = String( panels_root_selector || '' ).trim();
		}

		// Fallback if attribute missing: prefer #wpbc_bfb__top_panels if present.
		if ( ! panels_root_selector ) {
			panels_root_selector = '#wpbc_bfb__top_panels';
		}

		if ( panels_root_selector ) {
			try {
				var root_el = d.querySelector( panels_root_selector );
				if ( root_el ) {
					panels_root = root_el;
				}
			} catch ( _e2 ) {}
		}

		// Determine outer base class (IMPORTANT: must be the OUTER base, not wpbc_bfb__tab_section).
		var panel_base_class = 'wpbc_bfb__top_tab_section';
		if ( nav ) {
			var from_nav = nav.getAttribute( 'data-wpbc-bfb-panel-class' ) || '';
			from_nav     = String( from_nav || '' ).trim();
			if ( from_nav ) {
				panel_base_class = from_nav;
			}
		}

		// Update active tab on nav (source of truth).
		if ( nav ) {
			nav.setAttribute( 'data-active-tab', tab_id );
		}

		// Hide ONLY outer panels (scoped by base class).
		var selector_all = '.' + panel_base_class;
		var panel_nodes  = panels_root.querySelectorAll( selector_all );

		for ( var i = 0; i < panel_nodes.length; i++ ) {
			panel_nodes[i].style.display = 'none';
		}

		// Show requested outer panel.
		var selector_active = '.' + panel_base_class + '__' + tab_id;
		var active_panel    = panels_root.querySelector( selector_active );

		if ( ! active_panel ) {
			// Backward-compatible fallback (if someone forgot to add outer classes).
			active_panel = panels_root.querySelector( '.wpbc_bfb__tab_section__' + tab_id );
		}

		if ( active_panel ) {
			active_panel.style.display = '';
		}

		// Mark active item in outer nav if it exists (Preview can be missing).
		if ( nav ) {
			var items = nav.querySelectorAll( '.wpbc_ui_el__horis_nav_item' );
			for ( var j = 0; j < items.length; j++ ) {
				items[j].classList.remove( 'active' );
			}

			var active_item = nav.querySelector( '.wpbc_ui_el__horis_nav_item__' + tab_id );
			if ( active_item ) {
				active_item.classList.add( 'active' );
			}
		}

		// Emit same event as bfb-top-tabs.js so other modules stay in sync.
		try {
			d.dispatchEvent(
				new CustomEvent( 'wpbc:bfb:top-tab', {
					detail: {
						tab   : tab_id,
						nav_id: (nav && nav.id) ? String( nav.id ) : ''
					}
				} )
			);
		} catch ( _e3 ) {}

		return true;
	}


	/**
	 * Get currently active top tab id from the nav container.
	 *
	 * @return {string}
	 */
	function wpbc_bfb_preview__get_active_top_tab_id() {
		var nav = d.getElementById( 'wpbc_bfb__top_horisontal_nav' );
		if ( !nav ) {
			return '';
		}
		return nav.getAttribute( 'data-active-tab' ) || '';
	}

	/**
	 * Sync preview "mode" with currently active top tab.
	 *
	 * Rules:
	 * - preview_tab => enable preview mode (buttons make sense)
	 * - any other tab => disable preview mode + reset busy states
	 *
	 * IMPORTANT:
	 * - We do NOT change the active top tab here (no panel switching).
	 *   We only sync preview UI mode.
	 *
	 * @param {string} tab_id
	 * @param {wpbc_bfb_preview_client} client
	 */
	function wpbc_bfb_preview__sync_mode_to_tab(tab_id, client) {

		tab_id = String( tab_id || '' );

		if ( !client ) {
			return;
		}

		if ( 'preview_tab' === tab_id ) {
			client.set_mode( 'preview' );
			return;
		}

		// Leaving preview -> soft-cancel busy state and hide preview-specific toolbar.
		if ( typeof client.reset_busy_state === 'function' ) {
			client.reset_busy_state();
		}
		client.set_mode( 'builder' );
	}

	/**
	 * Bind to top-tab switch event emitted by bfb-top-tabs.js.
	 *
	 * @param {wpbc_bfb_preview_client} client
	 */
	function wpbc_bfb_preview__bind_top_tab_events(client) {

		// Sync immediately (in case event already fired before this script init).
		wpbc_bfb_preview__sync_mode_to_tab( wpbc_bfb_preview__get_active_top_tab_id(), client );

		// React to future tab changes.
		d.addEventListener( 'wpbc:bfb:top-tab', function (ev) {
			var det     = (ev && ev.detail) ? ev.detail : {};
			var tab_id  = det.tab ? String( det.tab ) : '';
			var nav_id  = det.nav_id ? String( det.nav_id ) : '';
			var prev_id = det.prev_tab ? String( det.prev_tab ) : '';

			// If user switched to preview_tab in the MAIN top nav,
			// remember what tab they came from.
			if ( nav_id === TOP_NAV_ID && tab_id === 'preview_tab' && prev_id && prev_id !== 'preview_tab' ) {
				return_tab_id = prev_id;
			}

			wpbc_bfb_preview__sync_mode_to_tab( tab_id, client );
		} );

	}


	// -- Bind | Init on Load ------------------------------------------------------------------------------------------
	var TOP_NAV_ID = 'wpbc_bfb__top_horisontal_nav';
	var return_tab_id = 'builder_tab';

	function remember_return_tab_from_dom() {
		var cur = wpbc_bfb_preview__get_active_top_tab_id();
		if (cur && cur !== 'preview_tab') {
			return_tab_id = cur;
		}
	}

	function get_return_tab_id() {
		var t = String(return_tab_id || '').trim();
		if (!t || t === 'preview_tab') t = 'builder_tab';
		return t;
	}

	/**
	 * Auto-init preview client on Builder page and wire top toolbar buttons.
	 */
	function wpbc_bfb_init_preview_client() {
		var root = d.querySelector( '[data-wpbc-bfb-preview-root="1"]' );

		if ( !root ) {
			return;
		}

		var client = new wpbc_bfb_preview_client( root );

		if ( !w.WPBC_BFB_Preview ) {
			w.WPBC_BFB_Preview = {
				client: client,

				show_preview: function (opts) {
					var opt = opts || {};
					var src = opt.source_button || null;

					// Remember where we were BEFORE switching to preview.
					remember_return_tab_from_dom();

					if ( ! wpbc_bfb_preview__is_preview_tab_link( src ) ) {
						wpbc_bfb__activate_panel_tab( 'preview_tab' );
					}

					// Enable preview mode UI (shows refresh/back buttons etc).
					client.set_mode( 'preview' );

					// IMPORTANT: Always regenerate when show_preview() is called (Preview tab click included).
					client.update_preview( { source_button: src } );
				},


				show_builder: function (opts) {
					var opt = opts || {};
					var src = opt.source_button || null;

					if ( client && typeof client.reset_busy_state === 'function' ) {
						client.reset_busy_state();
					}

					var back_to = get_return_tab_id();

					// If Back button itself is not a real tab link -> just activate remembered tab.
					// If activation fails (tab removed), fallback to builder_tab.
					if ( ! wpbc_bfb_preview__is_builder_tab_link( src ) ) {
						if ( ! wpbc_bfb__activate_panel_tab( back_to ) ) {
							wpbc_bfb__activate_panel_tab( 'builder_tab' );
						}
					}

					client.set_mode( 'builder' );
				},

				show_advanced_tab: function (opts) {
					var opt = opts || {};

					if ( client && typeof client.reset_busy_state === 'function' ) {
						client.reset_busy_state();
					}
					wpbc_bfb__activate_panel_tab( 'advanced_tab' );
					client.set_mode( 'builder' );
				}


			};
		}

		// Listen for top-tab changes and sync preview mode automatically.
		wpbc_bfb_preview__bind_top_tab_events( client );

		// When a form is loaded via AJAX, refresh preview (only if preview mode is active).
		wpbc_bfb_preview__bind_form_ajax_loaded_events( client );

		wpbc_bfb_bind_top_toolbar_buttons( client );
	}


	/**
	 * Bind top toolbar Builder / Preview / Refresh buttons.
	 * Supports multiple elements for each action (toolbar buttons, top tabs, etc.)
	 * via data-wpbc-bfb-top-*-btn="1".
	 */
	function wpbc_bfb_bind_top_toolbar_buttons(client) {

		var btn_preview_list = d.querySelectorAll( '[data-wpbc-bfb-top-preview-btn="1"]' );
		var btn_builder_list = d.querySelectorAll( '[data-wpbc-bfb-top-builder-btn="1"]' );
		var btn_refresh_list = d.querySelectorAll( '[data-wpbc-bfb-top-refresh-btn="1"]' );

		function for_each_node(list, cb) {
			if ( !list || !list.length ) {
				return;
			}
			Array.prototype.forEach.call( list, function (el) {
				if ( el && typeof cb === 'function' ) {
					cb( el );
				}
			} );
		}

		for_each_node( btn_preview_list, function (btn_preview) {

			// Prevent double binding if this script runs twice.
			if ( btn_preview.getAttribute( 'data-wpbc-bfb-bound' ) === '1' ) {
				return;
			}
			btn_preview.setAttribute( 'data-wpbc-bfb-bound', '1' );

			btn_preview.addEventListener( 'click', function (e) {
				e.preventDefault();

				if ( w.WPBC_BFB_Preview && typeof w.WPBC_BFB_Preview.show_preview === 'function' ) {
					w.WPBC_BFB_Preview.show_preview( {
						source_button: btn_preview
					} );
				}
			} );
		} );

		for_each_node( btn_refresh_list, function (btn_refresh) {

			if ( btn_refresh.getAttribute( 'data-wpbc-bfb-bound' ) === '1' ) {
				return;
			}
			btn_refresh.setAttribute( 'data-wpbc-bfb-bound', '1' );

			btn_refresh.addEventListener( 'click', function (e) {
				e.preventDefault();

				if ( w.WPBC_BFB_Preview && typeof w.WPBC_BFB_Preview.show_preview === 'function' ) {
					w.WPBC_BFB_Preview.show_preview( {
						update       : true,
						source_button: btn_refresh
					} );
				}
			} );
		} );

		for_each_node( btn_builder_list, function (btn_builder) {

			if ( btn_builder.getAttribute( 'data-wpbc-bfb-bound' ) === '1' ) {
				return;
			}
			btn_builder.setAttribute( 'data-wpbc-bfb-bound', '1' );

			btn_builder.addEventListener( 'click', function (e) {
				e.preventDefault();

				if ( w.WPBC_BFB_Preview && typeof w.WPBC_BFB_Preview.show_builder === 'function' ) {
					w.WPBC_BFB_Preview.show_builder( { source_button: btn_builder } );
				}
			} );
		} );
	}


	if ( d.readyState === 'complete' || d.readyState === 'interactive' ) {
		setTimeout( wpbc_bfb_init_preview_client, 0 );
	} else {
		d.addEventListener( 'DOMContentLoaded', wpbc_bfb_init_preview_client );
	}

})( window, document );

```
