/**
* Style Customizer v2 — builder panel entry. Mounts into the "Style" builder tab and renders
* the two-pane panel (controls + live preview), initializing the store from the localized
* payload (`evfStyleV2.payload`) with a REST `apiFetch` fallback.
*/
import React from 'react';
import { createRoot } from 'react-dom/client';
import { createPortal } from 'react-dom';
import './style.scss';
import { App } from './App';
import { PreviewSkeleton } from './PreviewPane';
import { initStore, getStore } from './store';
import { BootstrapSettings, StylePayload } from './types';
const apiFetch = ( window as any ).wp?.apiFetch;
const __ = ( window as any ).wp?.i18n?.__ || ( ( s: string ) => s );
const rawSettings: BootstrapSettings | undefined = ( window as any ).evfStyleV2;
const settings: BootstrapSettings = rawSettings || {
restBase: '',
formId: 0,
formTitle: '',
previewUrl: '',
frontendCssUrl: '',
wrapperId: '',
markerClass: 'evf-style-v2',
previewSession: '',
aiEnabled: false,
aiDisabled: false,
ajaxUrl: '',
aiNonce: '',
aiHintDismissed: true,
};
// Bridge so the (legacy jQuery) Fields tab can flag unsaved changes without importing the bundle.
( window as any ).evfScv2SetFormDirty = ( dirty: boolean ) => {
try {
getStore().setUnsavedFieldChanges( !! dirty );
} catch ( e ) {
// Store not initialized yet — the builder will call again on the next interaction.
}
};
// Reverse bridge: lets the (legacy jQuery) tab-switcher ask "would leaving the Style tab
// right now lose anything?" before it switches away — the existing `beforeunload` guard
// only protects a real page close/reload, not this in-app SPA tab switch.
( window as any ).evfScv2IsDirty = (): boolean => {
try {
return getStore().isDirty();
} catch ( e ) {
return false; // Store not initialized yet — nothing to lose.
}
};
const MIGRATION_SEEN_PREFIX = 'evf_scv2_migration_seen_';
/** Whether this browser has already sat through the migrating transition for this form once. */
function hasSeenMigration( formId: number ): boolean {
try {
return window.localStorage.getItem( MIGRATION_SEEN_PREFIX + formId ) === '1';
} catch ( e ) {
return false;
}
}
function markMigrationSeen( formId: number ) {
try {
window.localStorage.setItem( MIGRATION_SEEN_PREFIX + formId, '1' );
} catch ( e ) {
// Best-effort only — worst case the transition shows again next visit.
}
}
// Synchronous init from the localized payload, at module load — before Bootstrap even mounts.
// Except the very first time this form is seen freshly migrated, which shows a one-time
// "Migrating your styles…" transition and re-fetches via REST instead.
let readyFromPayload = false;
let migrationPending = false;
if ( rawSettings && rawSettings.payload && settings.formId ) {
const justMigrated = !! ( rawSettings.payload.migration && rawSettings.payload.migration.just_migrated );
if ( justMigrated && ! hasSeenMigration( settings.formId ) ) {
migrationPending = true;
} else {
try {
initStore( rawSettings.payload, settings );
readyFromPayload = true;
} catch ( e ) {
readyFromPayload = false;
}
}
}
/** Long enough to read as a deliberate, reassuring step rather than a flash. */
const MIGRATION_MIN_VISIBLE_MS = 900;
function Bootstrap() {
const [ state, setState ] = React.useState< 'loading' | 'migrating' | 'ready' | 'error' >(
readyFromPayload ? 'ready' : migrationPending ? 'migrating' : 'loading'
);
const [ message, setMessage ] = React.useState( '' );
React.useEffect( () => {
if ( readyFromPayload ) {
return; // Already initialized synchronously above — no fetch needed.
}
const finish = ( payload: StylePayload ) => {
try {
initStore( payload, settings );
} catch ( e ) {
setMessage( __( 'The style customizer could not start.', 'everest-forms' ) );
setState( 'error' );
return;
}
setState( 'ready' );
};
if ( migrationPending ) {
markMigrationSeen( settings.formId ); // Never show this transition again for this form.
const startedAt = Date.now();
const fresh = apiFetch && settings.formId
? apiFetch( { path: `${ settings.restBase }/${ settings.formId }` } )
: Promise.reject( new Error( 'apiFetch unavailable' ) );
// On failure, fall back to the already-embedded payload rather than blocking the panel.
fresh
.catch( () => ( rawSettings as { payload: StylePayload } ).payload )
.then( ( payload: StylePayload ) => {
const remaining = MIGRATION_MIN_VISIBLE_MS - ( Date.now() - startedAt );
if ( remaining > 0 ) {
window.setTimeout( () => finish( payload ), remaining );
} else {
finish( payload );
}
} );
return;
}
if ( ! apiFetch || ! settings.formId ) {
setMessage( __( 'The style customizer could not start.', 'everest-forms' ) );
setState( 'error' );
return;
}
apiFetch( { path: `${ settings.restBase }/${ settings.formId }` } )
.then( ( payload: StylePayload ) => finish( payload ) )
.catch( ( e: any ) => {
setMessage( ( e && e.message ) || __( 'Failed to load styles.', 'everest-forms' ) );
setState( 'error' );
} );
}, [] );
if ( state === 'loading' ) {
return
{ message }