# booking/11.9/includes/page-setup-wizard/_src/setup-wizard.js

Booking Calendar, version 11.9. 1,152 lines.

- Page: https://pluginprobe.com/plugins/booking/11.9/code/includes/page-setup-wizard/_src/setup-wizard.js
- Raw: https://pluginprobe.com/plugins/booking/11.9/raw/includes/page-setup-wizard/_src/setup-wizard.js
- Modified: 2026-10-03T20:32:36+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.9/code/includes/page-setup-wizard/_src/setup-wizard.js#L10-L20`.

```javascript
/**
 * Coordinate Setup Wizard checkpoint saves, navigation, and dialogs.
 *
 * The server remains authoritative for field allow-lists, step order,
 * revisions, access, and persistence. This adapter performs accessible early
 * validation, ignores stale responses, and never applies canonical settings.
 *
 * @package Booking Calendar
 */
( function ( $, window, document ) {
	'use strict';

	var config = window.wpbc_setup_wizard;
	var root = document.querySelector( '[data-wpbc-setup-wizard]' );
	var statusNode;
	var transition_message_node;
	var request_error_node;
	var request_error_message_node;
	var request_error_reference_node;
	var request_error_request_id_node;
	var activeDialog = null;
	var dialogTrigger = null;
	var activeRequest = null;
	var requestSequence = 0;
	var mode_toolbar_show_timer = null;
	var mode_toolbar_hide_timer = null;
	var save_operation_state = null;
	var request_phases = {
		IDLE: 'idle',
		REQUESTING: 'requesting',
		REDIRECTING: 'redirecting'
	};
	var request_phase = request_phases.IDLE;
	var browser_ready = false;
	var step_adapters = [];

	if ( ! config || ! root ) {
		return;
	}

	statusNode = root.querySelector( '[data-wpbc-setup-wizard-status]' );
	transition_message_node = root.querySelector( '[data-wpbc-setup-wizard-transition-message]' );
	request_error_node = root.querySelector( '[data-wpbc-setup-wizard-request-error]' );
	request_error_message_node = root.querySelector( '[data-wpbc-setup-wizard-request-error-message]' );
	request_error_reference_node = root.querySelector( '[data-wpbc-setup-wizard-request-error-reference]' );
	request_error_request_id_node = root.querySelector( '[data-wpbc-setup-wizard-request-error-request-id]' );

	/**
	 * Set the live status message exposed to assistive technology.
	 *
	 * @param {string} message Status text.
	 * @return {void}
	 */
	function setStatus( message ) {
		if ( statusNode ) {
			statusNode.textContent = message || '';
		}
	}

	/**
	 * Return the normalized data object from a direct or jQuery AJAX response.
	 *
	 * @param {Object} response_or_xhr WordPress response or jQuery request object.
	 * @return {Object} Normalized response data.
	 */
	function get_response_data( response_or_xhr ) {
		var response = response_or_xhr && response_or_xhr.responseJSON
			? response_or_xhr.responseJSON
			: response_or_xhr;

		return response && response.data && 'object' === typeof response.data
			? response.data
			: {};
	}

	/**
	 * Hide and reset the shared request error notice.
	 *
	 * @return {void}
	 */
	function clear_request_error() {
		if ( ! request_error_node ) {
			return;
		}

		request_error_node.hidden = true;
		if ( request_error_message_node ) {
			request_error_message_node.textContent = '';
		}
		if ( request_error_reference_node ) {
			request_error_reference_node.hidden = true;
		}
		if ( request_error_request_id_node ) {
			request_error_request_id_node.textContent = '';
		}
	}

	/**
	 * Show one server or transport failure in the common wizard alert region.
	 *
	 * Text is assigned through textContent so an error returned by any step cannot
	 * inject markup. The request reference helps support staff correlate a visible error
	 * with its server response without exposing internal stack details.
	 *
	 * @param {Object} response_or_xhr WordPress response or jQuery request object.
	 * @return {void}
	 */
	function show_request_error( response_or_xhr ) {
		var response_data = get_response_data( response_or_xhr );
		var request_id = response_data.request_id ? String( response_data.request_id ) : '';

		if ( ! request_error_node || ! request_error_message_node ) {
			return;
		}

		request_error_message_node.textContent = getErrorMessage( response_or_xhr );
		if ( request_error_reference_node && request_error_request_id_node ) {
			request_error_request_id_node.textContent = request_id;
			request_error_reference_node.hidden = ! request_id;
		}

		request_error_node.hidden = false;
		try {
			request_error_node.focus( { preventScroll: true } );
		} catch ( focus_error ) {
			request_error_node.focus();
		}
		try {
			request_error_node.scrollIntoView( {
				behavior: window.matchMedia && window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches ? 'auto' : 'smooth',
				block: 'start'
			} );
		} catch ( scroll_error ) {
			request_error_node.scrollIntoView();
		}
	}

	/**
	 * Release the server-locked forward action after browser initialization.
	 *
	 * The primary action is rendered as a disabled non-submit button so a click
	 * cannot fall through to the browser's native GET form submission before the
	 * shared handlers exist. Form-backed steps recover submit behavior only after
	 * the shared controller and dependent step scripts have finished loading.
	 *
	 * @return {void}
	 */
	function enable_continue_when_ready() {
		var continue_button = root.querySelector( '[data-wpbc-setup-wizard-enable-when-ready]' );

		if ( ! browser_ready || request_phases.IDLE !== request_phase || ! continue_button ) {
			return;
		}

		if ( continue_button.hasAttribute( 'data-wpbc-setup-wizard-submit-when-ready' ) ) {
			continue_button.type = 'submit';
		}
		continue_button.disabled = false;
	}

	/**
	 * Mark the shared shell ready after dependent footer scripts have executed.
	 *
	 * @return {void}
	 */
	function mark_browser_ready() {
		browser_ready = true;
		root.dataset.wpbcSetupWizardBrowserReady = 'true';
		enable_continue_when_ready();
	}

	/**
	 * Defer readiness until parsing and all DOMContentLoaded listeners complete.
	 *
	 * The zero-delay task also covers optimized installations that execute the
	 * footer bundle after DOMContentLoaded while preserving dependency order.
	 *
	 * @return {void}
	 */
	function schedule_browser_ready() {
		var defer_ready = function () {
			window.setTimeout( mark_browser_ready, 0 );
		};

		if ( 'loading' === document.readyState ) {
			document.addEventListener( 'DOMContentLoaded', defer_ready, { once: true } );
			return;
		}

		defer_ready();
	}

	/**
	 * Lock or unlock interactive wizard controls around one active request.
	 *
	 * Repeated calls with the current state are ignored so the original disabled
	 * state of every control remains available for restoration.
	 *
	 * @param {boolean} isBusy Whether a request is active.
	 * @return {void}
	 */
	function setBusy( isBusy ) {
		var current_busy_state = 'true' === root.getAttribute( 'aria-busy' );

		if ( current_busy_state === isBusy ) {
			return;
		}

		root.classList.toggle( 'wpbc_setup_wizard--busy', isBusy );
		root.setAttribute( 'aria-busy', isBusy ? 'true' : 'false' );

		root.querySelectorAll( 'button, input, select, textarea' ).forEach( function ( control ) {
			if ( isBusy ) {
				control.dataset.wpbcWasDisabled = control.disabled ? 'true' : 'false';
				control.disabled = true;
			} else {
				control.disabled = 'true' === control.dataset.wpbcWasDisabled;
				delete control.dataset.wpbcWasDisabled;
			}
		} );

		if ( ! isBusy ) {
			enable_continue_when_ready();
		}
	}

	/**
	 * Update the visible message inside the blocking transition overlay.
	 *
	 * The separate live status node remains responsible for announcements to
	 * assistive technology.
	 *
	 * @param {string} message Visible transition text.
	 * @return {void}
	 */
	function set_transition_message( message ) {
		if ( transition_message_node ) {
			transition_message_node.textContent = message || '';
		}
	}

	/**
	 * Move the request controller between idle, requesting, and redirecting.
	 *
	 * Redirecting deliberately remains busy after the AJAX promise settles. The
	 * old document must stay locked until the browser replaces it.
	 *
	 * @param {string} phase   One value from request_phases.
	 * @param {string} message Visible and announced progress text.
	 * @return {void}
	 */
	function set_request_phase( phase, message ) {
		request_phase = phase;
		root.dataset.requestPhase = phase;

		if ( request_phases.IDLE === phase ) {
			set_transition_message( '' );
			setBusy( false );
			return;
		}

		set_transition_message( message );
		setBusy( true );
		setStatus( message );
	}

	/**
	 * Prevent links or delegated controls from acting outside the idle phase.
	 *
	 * Native form controls are disabled by setBusy(). This capture-phase guard
	 * also blocks the header links and shared product menu before their own click
	 * handlers run while the old document is waiting to be replaced.
	 *
	 * @param {MouseEvent} event Captured click event inside the wizard shell.
	 * @return {void}
	 */
	function prevent_interaction_while_busy( event ) {
		if ( request_phases.IDLE === request_phase ) {
			return;
		}

		event.preventDefault();
		event.stopImmediatePropagation();
	}

	/**
	 * Return every draft field control rendered by the current step.
	 *
	 * @return {HTMLElement[]} Ordered controls.
	 */
	function getFieldControls() {
		return Array.prototype.slice.call( root.querySelectorAll( '[data-wpbc-setup-wizard-field]' ) );
	}

	/**
	 * Find all controls sharing one stable field name.
	 *
	 * @param {string} fieldName Draft field name.
	 * @return {HTMLElement[]} Matching controls.
	 */
	function findFieldControls( fieldName ) {
		return getFieldControls().filter( function ( control ) {
			return control.name === fieldName;
		} );
	}

	/**
	 * Find a field-level error container without interpolating a CSS selector.
	 *
	 * @param {string} fieldName Draft field name.
	 * @return {HTMLElement|null} Matching error node.
	 */
	function findFieldError( fieldName ) {
		var errorNodes = root.querySelectorAll( '[data-wpbc-setup-wizard-error-for]' );
		var matchingNode = null;

		errorNodes.forEach( function ( errorNode ) {
			if ( errorNode.dataset.wpbcSetupWizardErrorFor === fieldName ) {
				matchingNode = errorNode;
			}
		} );

		return matchingNode;
	}

	/**
	 * Clear an existing field error and invalid state.
	 *
	 * @param {string} fieldName Draft field name.
	 * @return {void}
	 */
	function clearFieldError( fieldName ) {
		var errorNode = findFieldError( fieldName );
		var alternateControl = root.querySelector( '[data-wpbc-setup-wizard-error-control-for="' + fieldName + '"]' );

		findFieldControls( fieldName ).forEach( function ( control ) {
			control.removeAttribute( 'aria-invalid' );
		} );
		if ( alternateControl ) {
			alternateControl.removeAttribute( 'aria-invalid' );
		}

		if ( errorNode ) {
			errorNode.textContent = '';
		}
	}

	/**
	 * Render one client- or server-provided field error.
	 *
	 * @param {string} fieldName Draft field name.
	 * @param {string} message Human-readable validation message.
	 * @return {HTMLElement|null} First invalid control.
	 */
	function setFieldError( fieldName, message ) {
		var controls = findFieldControls( fieldName );
		var errorNode = findFieldError( fieldName );
		var alternateControl = root.querySelector( '[data-wpbc-setup-wizard-error-control-for="' + fieldName + '"]' );

		controls.forEach( function ( control ) {
			control.setAttribute( 'aria-invalid', 'true' );
		} );

		if ( errorNode ) {
			errorNode.textContent = message;
		}

		if ( alternateControl ) {
			alternateControl.setAttribute( 'aria-invalid', 'true' );
		}

		return alternateControl || ( controls.length ? controls[ 0 ] : null );
	}


	/**
	 * Lock or unlock the consumer shell for an explicit module operation.
	 *
	 * @param {boolean} is_busy Whether the module operation is active.
	 * @param {string}  message Accessible operation status.
	 * @return {void}
	 */
	function set_step_adapter_busy( is_busy, message ) {
		set_request_phase( is_busy ? request_phases.REQUESTING : request_phases.IDLE, message || '' );
	}

	/**
	 * Register one current-page editor adapter with the shared wizard shell.
	 *
	 * A step module owns its rendering, draft synchronization, and client-side
	 * validation. The shell exposes only navigation-safe services and never
	 * assumes which domain fields are present.
	 *
	 * @param {Object} step_adapter Step-owned adapter contract.
	 * @return {void}
	 */
	function register_step_adapter( step_adapter ) {
		if ( ! step_adapter || 'function' !== typeof step_adapter.initialize ) {
			return;
		}

		step_adapters.push( step_adapter );
		step_adapter.initialize( {
			root: root,
			config: config,
			set_status: setStatus,
			show_error: show_request_error,
			clear_error: clear_request_error,
			set_busy: set_step_adapter_busy,
			set_field_error: setFieldError,
			clear_field_error: clearFieldError,
			open_dialog: openDialog
		} );
	}

	/**
	 * Synchronize every registered step adapter before draft collection.
	 *
	 * @return {void}
	 */
	function sync_step_adapters() {
		step_adapters.forEach( function ( step_adapter ) {
			if ( 'function' === typeof step_adapter.sync ) {
				step_adapter.sync();
			}
		} );
	}

	/**
	 * Validate registered step modules in registration order.
	 *
	 * @return {HTMLElement|null} First invalid control, or null.
	 */
	function validate_step_adapters() {
		var first_invalid_control = null;

		step_adapters.some( function ( step_adapter ) {
			if ( 'function' !== typeof step_adapter.validate ) {
				return false;
			}

			first_invalid_control = step_adapter.validate();
			return Boolean( first_invalid_control );
		} );

		return first_invalid_control;
	}

	window.wpbc_setup_wizard_api = {
		register_step_adapter: register_step_adapter
	};

	/**
	 * Remove every visible validation error from the current step.
	 *
	 * @return {void}
	 */
	function clearFieldErrors() {
		getFieldControls().forEach( function ( control ) {
			clearFieldError( control.name );
		} );

		root.querySelectorAll( '[data-wpbc-review-step].has-error' ).forEach( function ( review_row ) {
			review_row.classList.remove( 'has-error' );
		} );
	}

	/**
	 * Validate required and email fields before forward navigation.
	 *
	 * Server validation remains authoritative. This check provides immediate
	 * keyboard focus and avoids a needless request for obvious omissions.
	 *
	 * @return {boolean} True when client-visible constraints pass.
	 */
	function validateCurrentStep() {
		var controls = getFieldControls();
		var handledFields = {};
		var firstInvalidControl = null;

		clearFieldErrors();

		firstInvalidControl = validate_step_adapters();
		if ( firstInvalidControl ) {
			setStatus( config.i18n.validation_error );
			firstInvalidControl.focus();
			return false;
		}

		controls.forEach( function ( control ) {
			var fieldControls;
			var hasValue;
			var invalidMessage = '';

			if ( handledFields[ control.name ] ) {
				return;
			}
			handledFields[ control.name ] = true;
			fieldControls = findFieldControls( control.name );

			if ( 'radio' === control.type ) {
				hasValue = fieldControls.some( function ( radioControl ) {
					return radioControl.checked;
				} );
			} else if ( 'checkbox' === control.type ) {
				hasValue = control.checked;
			} else {
				hasValue = '' !== String( control.value ).trim();
			}

			if ( control.required && ! hasValue ) {
				invalidMessage = config.i18n.required;
			} else if ( 'email' === control.type && hasValue && control.validity && control.validity.typeMismatch ) {
				invalidMessage = config.i18n.invalid_email;
			}

			if ( invalidMessage ) {
				firstInvalidControl = firstInvalidControl || setFieldError( control.name, invalidMessage );
			}
		} );

		if ( firstInvalidControl ) {
			setStatus( config.i18n.validation_error );
			firstInvalidControl.focus();
			return false;
		}

		return true;
	}

	/**
	 * Collect scalar values from the current allow-listed form controls.
	 *
	 * @return {Object} Draft field values keyed by stable field name.
	 */
	function collectFields() {
		var fieldValues = {};

		sync_step_adapters();

		getFieldControls().forEach( function ( control ) {
			if ( 'radio' === control.type ) {
				if ( control.checked ) {
					fieldValues[ control.name ] = control.value;
				}
				return;
			}

			if ( 'checkbox' === control.type ) {
				fieldValues[ control.name ] = control.checked ? '1' : '0';
				return;
			}

			fieldValues[ control.name ] = control.value;
		} );

		return fieldValues;
	}

	/**
	 * Extract a safe message from a failed WordPress AJAX response.
	 *
	 * @param {Object} response_or_xhr WordPress response or jQuery request object.
	 * @return {string} Human-readable error text.
	 */
	function getErrorMessage( response_or_xhr ) {
		var response_data = get_response_data( response_or_xhr );

		if ( response_data.message ) {
			return String( response_data.message );
		}

		return config.i18n.error;
	}

	/**
	 * Render server-side field errors and focus the first rejected control.
	 *
	 * @param {Object} xhr jQuery request object.
	 * @return {void}
	 */
	function renderServerErrors( xhr ) {
		var responseData = xhr && xhr.responseJSON ? xhr.responseJSON.data : null;
		var firstInvalidControl = null;
		var invalid_step_row = null;

		if ( ! responseData || ( ! responseData.field_errors && ! responseData.invalid_step_id ) ) {
			return;
		}

		Object.keys( responseData.field_errors || {} ).forEach( function ( fieldName ) {
			firstInvalidControl = firstInvalidControl || setFieldError( fieldName, String( responseData.field_errors[ fieldName ] ) );
		} );

		if ( responseData.invalid_step_id ) {
			root.querySelectorAll( '[data-wpbc-review-step]' ).forEach( function ( review_row ) {
				if ( review_row.dataset.wpbcReviewStep === responseData.invalid_step_id ) {
					invalid_step_row = review_row;
					review_row.classList.add( 'has-error' );
				}
			} );
		}

		if ( firstInvalidControl ) {
			firstInvalidControl.focus();
		} else if ( invalid_step_row ) {
			firstInvalidControl = invalid_step_row.querySelector( '[data-wpbc-setup-wizard-edit-step]' );
			if ( firstInvalidControl ) {
				firstInvalidControl.focus();
			}
		}
	}

	/**
	 * Build a stable idempotency key for one page, revision, and field snapshot.
	 *
	 * Two browser tabs receive different entropy, while an unchanged retry after
	 * an ambiguous response reuses the same identifier. Editing a field creates
	 * a new logical operation rather than replaying the previous result.
	 *
	 * @param {Object} field_values Current scalar field snapshot.
	 * @return {string} Stable operation identifier.
	 */
	function get_save_operation_id( field_values ) {
		var normalized_fields = {};
		var field_signature;
		var entropy_values;
		var entropy;
		var operation_id;

		Object.keys( field_values || {} ).sort().forEach( function ( field_name ) {
			normalized_fields[ field_name ] = field_values[ field_name ];
		} );
		field_signature = JSON.stringify( normalized_fields );

		if (
			save_operation_state
			&& save_operation_state.step_id === config.current_step
			&& save_operation_state.revision === config.revision
			&& save_operation_state.field_signature === field_signature
		) {
			return save_operation_state.operation_id;
		}

		if ( window.crypto && 'function' === typeof window.crypto.getRandomValues ) {
			entropy_values = new Uint32Array( 2 );
			window.crypto.getRandomValues( entropy_values );
			entropy = entropy_values[ 0 ].toString( 36 ) + entropy_values[ 1 ].toString( 36 );
		} else {
			entropy = Date.now().toString( 36 ) + Math.random().toString( 36 ).slice( 2, 12 );
		}

		operation_id = (
			'save_' +
			String( config.current_step || '' ).replace( /[^a-z0-9_-]/gi, '' ) + '_' +
			String( config.revision ) + '_' +
			entropy
		).slice( 0, 100 );
		save_operation_state = {
			step_id: config.current_step,
			revision: config.revision,
			field_signature: field_signature,
			operation_id: operation_id
		};

		return operation_id;
	}

	/**
	 * Send one revision-bound action and ignore aborted or stale responses.
	 *
	 * @param {string} action WordPress AJAX action.
	 * @param {Object} payload Action-specific scalar or field values.
	 * @return {void}
	 */
	function sendRequest( action, payload ) {
		var sequence;
		var requestId;

		requestSequence += 1;
		sequence = requestSequence;
		requestId = 'request_' + Date.now() + '_' + sequence;

		if ( activeRequest ) {
			activeRequest.abort();
		}

		clear_request_error();
		set_request_phase( request_phases.REQUESTING, config.i18n.working );

		activeRequest = $.ajax( {
			url: config.ajax_url,
			method: 'POST',
			dataType: 'json',
			data: $.extend( {}, payload, {
				action: action,
				nonce: config.nonce,
				current_step: config.current_step,
				expected_revision: config.revision,
				request_id: requestId
			} )
		} );

		activeRequest.done( function ( response ) {
			if ( sequence !== requestSequence ) {
				return;
			}

			if (
				! response ||
				! response.success ||
				! response.data ||
				response.data.request_id !== requestId
			) {
				set_request_phase( request_phases.IDLE );
				show_request_error( response );
				setStatus( getErrorMessage( response ) );
				return;
			}

			if ( response.data.checkpoint ) {
				config.current_step = response.data.checkpoint.current_step;
				config.revision = response.data.checkpoint.revision;
				root.dataset.revision = String( response.data.checkpoint.revision );
			}

			if ( 'string' !== typeof response.data.redirect_url || '' === response.data.redirect_url.trim() ) {
				set_request_phase( request_phases.IDLE );
				show_request_error( response );
				setStatus( getErrorMessage( response ) );
				return;
			}

			set_request_phase( request_phases.REDIRECTING, config.i18n.loading );

			try {
				window.location.assign( response.data.redirect_url );
			} catch ( redirectError ) {
				set_request_phase( request_phases.IDLE );
				show_request_error( {} );
				setStatus( config.i18n.error );
			}
		} );

		activeRequest.fail( function ( xhr, textStatus ) {
			if ( 'abort' === textStatus || sequence !== requestSequence ) {
				return;
			}

			set_request_phase( request_phases.IDLE );
			show_request_error( xhr );
			renderServerErrors( xhr );
			setStatus( getErrorMessage( xhr ) );
		} );

		activeRequest.always( function () {
			if ( sequence === requestSequence ) {
				activeRequest = null;
				if ( request_phases.REDIRECTING !== request_phase ) {
					set_request_phase( request_phases.IDLE );
				}
			}
		} );
	}

	/**
	 * Return focusable controls currently available in a dialog.
	 *
	 * @param {HTMLElement} dialog Dialog element.
	 * @return {HTMLElement[]} Ordered focusable elements.
	 */
	function getFocusableElements( dialog ) {
		return Array.prototype.slice.call(
			dialog.querySelectorAll( 'button:not([disabled]), a[href], [tabindex]:not([tabindex="-1"])' )
		);
	}

	/**
	 * Open a registered confirmation dialog and transfer focus inside it.
	 *
	 * @param {string} action Dialog action identifier.
	 * @param {HTMLElement} trigger Control that opened the dialog.
	 * @return {boolean} Whether the requested dialog was opened.
	 */
	function openDialog( action, trigger ) {
		var backdrop = root.querySelector( '[data-wpbc-setup-wizard-dialog="' + action + '"]' );

		if ( ! backdrop ) {
			return false;
		}

		dialogTrigger = trigger;
		activeDialog = backdrop;
		backdrop.hidden = false;
		document.body.classList.add( 'wpbc_setup_wizard_dialog_open' );

		window.setTimeout( function () {
			var cancelButton = backdrop.querySelector( '[data-wpbc-setup-wizard-dialog-cancel]' );
			if ( cancelButton ) {
				cancelButton.focus();
			}
		}, 0 );

		return true;
	}

	/**
	 * Close the active dialog, announce its outcome, and restore trigger focus.
	 *
	 * The lifecycle event lets a domain adapter commit or roll back provisional
	 * browser state without adding domain branches to the shared shell.
	 *
	 * @param {string} outcome Either confirm or cancel.
	 * @return {void}
	 */
	function closeDialog( outcome ) {
		var closed_dialog;
		var closed_action;
		var closed_trigger;

		if ( ! activeDialog ) {
			return;
		}

		closed_dialog = activeDialog;
		closed_action = String( closed_dialog.dataset.wpbcSetupWizardDialog || '' );
		closed_trigger = dialogTrigger;
		closed_dialog.hidden = true;
		activeDialog = null;
		document.body.classList.remove( 'wpbc_setup_wizard_dialog_open' );

		if ( closed_trigger ) {
			closed_trigger.focus();
		}
		dialogTrigger = null;

		root.dispatchEvent( new window.CustomEvent( 'wpbc:setup-wizard-dialog-closed', {
			detail: {
				action: closed_action,
				outcome: 'confirm' === outcome ? 'confirm' : 'cancel',
				trigger: closed_trigger
			}
		} ) );
	}

	/**
	 * Synchronize visual selection state with native radio controls.
	 *
	 * Native radio behavior supplies Tab, Space, and arrow-key operation. The
	 * card styling is presentation only and does not determine authorization.
	 *
	 * @return {void}
	 */
	function updateExperienceCards() {
		root.querySelectorAll( '.wpbc_setup_wizard__experience-card' ).forEach( function ( card ) {
			var radioControl = card.querySelector( 'input[type="radio"]' );
			var selectedBadge = card.querySelector( '.wpbc_setup_wizard__selected-badge' );
			var isSelected = radioControl && radioControl.checked;

			card.classList.toggle( 'is-selected', Boolean( isSelected ) );
			if ( selectedBadge ) {
				selectedBadge.hidden = ! isSelected;
			}
		} );
	}

	/**
	 * Hide the Step 4 Booking Mode learning tooltip and cancel pending timers.
	 *
	 * @return {void}
	 */
	function hide_mode_toolbar_tooltip() {
		var toolbar = root.querySelector( '[data-wpbc-setup-wizard-mode-toolbar]' );
		var tooltip = root.querySelector( '[data-wpbc-setup-wizard-mode-toolbar-tooltip]' );

		window.clearTimeout( mode_toolbar_show_timer );
		window.clearTimeout( mode_toolbar_hide_timer );
		mode_toolbar_show_timer = null;
		mode_toolbar_hide_timer = null;

		if ( toolbar ) {
			toolbar.classList.remove( 'is-previewing' );
		}

		if ( tooltip ) {
			tooltip.classList.remove( 'is-visible' );
			tooltip.setAttribute( 'aria-hidden', 'true' );
		}
	}

	/**
	 * Synchronize the preview-only toolbar selector with the Step 4 radio draft.
	 *
	 * The preview deliberately avoids the released `.wpbc_booking_mode_option`
	 * hook because that control performs the canonical Booking Mode mutation.
	 *
	 * @param {string}  mode_id        Validated mode identifier from a radio.
	 * @param {boolean} should_animate Whether to teach the toolbar location.
	 * @return {void}
	 */
	function update_mode_toolbar_preview( mode_id, should_animate ) {
		var toolbar = root.querySelector( '[data-wpbc-setup-wizard-mode-toolbar]' );
		var toolbar_label;
		var tooltip;
		var selected_option = null;
		var reduce_motion;

		if ( ! toolbar ) {
			return;
		}

		toolbar_label = toolbar.querySelector( '[data-wpbc-setup-wizard-mode-toolbar-label]' );
		tooltip = toolbar.querySelector( '[data-wpbc-setup-wizard-mode-toolbar-tooltip]' );
		toolbar.querySelectorAll( '[data-wpbc-setup-wizard-mode-toolbar-option]' ).forEach( function ( option ) {
			var is_selected = option.dataset.wpbcSetupWizardModeToolbarOption === mode_id;

			option.classList.toggle( 'is-current', is_selected );
			option.setAttribute( 'aria-checked', is_selected ? 'true' : 'false' );
			if ( is_selected ) {
				selected_option = option;
			}
		} );

		if ( ! selected_option || ! toolbar_label ) {
			return;
		}

		toolbar_label.textContent = selected_option.dataset.wpbcSetupWizardModeToolbarTitle || selected_option.textContent.trim();
		if ( ! should_animate ) {
			return;
		}

		hide_mode_toolbar_tooltip();
		toolbar.classList.remove( 'is-previewing' );
		void toolbar.offsetWidth;
		toolbar.classList.add( 'is-previewing' );

		reduce_motion = window.matchMedia && window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches;
		mode_toolbar_show_timer = window.setTimeout( function () {
			toolbar.classList.remove( 'is-previewing' );
			if ( ! tooltip ) {
				return;
			}

			tooltip.classList.add( 'is-visible' );
			tooltip.setAttribute( 'aria-hidden', 'false' );
			setStatus( tooltip.textContent.trim() );
			mode_toolbar_hide_timer = window.setTimeout( hide_mode_toolbar_tooltip, 6500 );
		}, reduce_motion ? 0 : 1900 );
	}

	root.addEventListener( 'click', prevent_interaction_while_busy, true );

	root.addEventListener( 'click', function ( event ) {
		var directionButton = event.target.closest( '[data-wpbc-setup-wizard-direction]' );
		var edit_step_button = event.target.closest( '[data-wpbc-setup-wizard-edit-step]' );
		var dialogButton = event.target.closest( '[data-wpbc-setup-wizard-open-dialog]' );
		var cancelButton = event.target.closest( '[data-wpbc-setup-wizard-dialog-cancel]' );
		var confirmButton = event.target.closest( '[data-wpbc-setup-wizard-confirm]' );
		var mode_toolbar_option = event.target.closest( '[data-wpbc-setup-wizard-mode-toolbar-option]' );
		var dismiss_error_button = event.target.closest( '[data-wpbc-setup-wizard-dismiss-error]' );
		var mode_radio = null;
		var direction;
		var action;
		var submitted_fields;

		if ( dismiss_error_button ) {
			event.preventDefault();
			clear_request_error();
			return;
		}

		if ( edit_step_button ) {
			event.preventDefault();
			sendRequest( config.action_edit, {
				target_step: edit_step_button.dataset.wpbcSetupWizardEditStep
			} );
			return;
		}

		if ( mode_toolbar_option ) {
			event.preventDefault();
			root.querySelectorAll( 'input[name="booking_experience"]' ).forEach( function ( radio_control ) {
				if ( radio_control.value === mode_toolbar_option.dataset.wpbcSetupWizardModeToolbarOption ) {
					mode_radio = radio_control;
				}
			} );

			if ( mode_radio && ! mode_radio.checked ) {
				$( mode_radio ).prop( 'checked', true ).trigger( 'change' );
			}
			return;
		}

		if ( directionButton ) {
			event.preventDefault();
			direction = directionButton.dataset.wpbcSetupWizardDirection;
			if ( 'back' === direction ) {
				clearFieldErrors();
				sendRequest( config.action_back, {} );
				return;
			}

			if ( 'next' !== direction || ! validateCurrentStep() ) {
				return;
			}
			clearFieldErrors();
			submitted_fields = collectFields();
			sendRequest( config.action_save_continue, {
				fields: submitted_fields,
				operation_id: get_save_operation_id( submitted_fields )
			} );
			return;
		}

		if ( dialogButton ) {
			event.preventDefault();
			openDialog( dialogButton.dataset.wpbcSetupWizardOpenDialog, dialogButton );
			return;
		}

		if ( cancelButton ) {
			event.preventDefault();
			closeDialog( 'cancel' );
			return;
		}

		if ( confirmButton ) {
			event.preventDefault();
			action = confirmButton.dataset.wpbcSetupWizardConfirm;
			closeDialog( 'confirm' );
			if ( 'restart' === action || 'skip' === action ) {
				sendRequest( 'restart' === action ? config.action_restart : config.action_skip, {
					confirmation: action
				} );
			}
			return;
		}

		if ( activeDialog && event.target === activeDialog ) {
			closeDialog( 'cancel' );
		}
	} );

	root.addEventListener( 'submit', function ( event ) {
		var continueButton;

		if ( 'wpbc-setup-wizard-step-form' !== event.target.id ) {
			return;
		}

		event.preventDefault();
		continueButton = root.querySelector( '[data-wpbc-setup-wizard-direction="next"]' );
		if ( continueButton && ! continueButton.disabled ) {
			continueButton.click();
		}
	} );

	root.addEventListener( 'change', function ( event ) {
		if ( event.target.matches( '[data-wpbc-setup-wizard-field]' ) ) {
			clearFieldError( event.target.name );
		}

		if ( event.target.matches( 'input[name="booking_experience"]' ) ) {
			updateExperienceCards();
			update_mode_toolbar_preview( event.target.value, true );
		}

		if ( event.target.matches( 'input[name="customer_journey"]' ) ) {
			updateExperienceCards();
		}

	} );

	root.addEventListener( 'input', function ( event ) {
		if ( event.target.matches( '[data-wpbc-setup-wizard-field]' ) ) {
			clearFieldError( event.target.name );
		}
	} );

	document.addEventListener( 'keydown', function ( event ) {
		var dialog;
		var focusable;
		var first;
		var last;

		if ( ! activeDialog ) {
			return;
		}

		if ( 'Escape' === event.key ) {
			event.preventDefault();
			closeDialog( 'cancel' );
			return;
		}

		if ( 'Tab' !== event.key ) {
			return;
		}

		dialog = activeDialog.querySelector( '[role="dialog"]' );
		focusable = getFocusableElements( dialog );
		if ( ! focusable.length ) {
			event.preventDefault();
			dialog.focus();
			return;
		}

		first = focusable[ 0 ];
		last = focusable[ focusable.length - 1 ];
		if ( event.shiftKey && document.activeElement === first ) {
			event.preventDefault();
			last.focus();
		} else if ( ! event.shiftKey && document.activeElement === last ) {
			event.preventDefault();
			first.focus();
		}
	} );

	/**
	 * Restore transient request state when the browser revives this document.
	 *
	 * A page restored from the back-forward cache must not retain the disabled
	 * controls or transition overlay from a navigation that already completed.
	 *
	 * @param {PageTransitionEvent} event Browser page-show event.
	 * @return {void}
	 */
	function restore_transient_request_state( event ) {
		if ( ! event.persisted ) {
			return;
		}

		requestSequence += 1;
		if ( activeRequest ) {
			activeRequest.abort();
			activeRequest = null;
		}

		set_request_phase( request_phases.IDLE );
		setStatus( '' );
	}

	window.addEventListener( 'pageshow', restore_transient_request_state );

	updateExperienceCards();
	if ( root.querySelector( 'input[name="booking_experience"]:checked' ) ) {
		update_mode_toolbar_preview( root.querySelector( 'input[name="booking_experience"]:checked' ).value, false );
	}
	schedule_browser_ready();
}( jQuery, window, document ) );

```
