AiAssistant.tsx
2 days ago
App.tsx
2 days ago
BuilderSync.ts
2 days ago
ControlRenderer.tsx
2 days ago
HoverTip.tsx
2 days ago
Popover.tsx
2 days ago
PreviewBridge.ts
2 days ago
PreviewPane.tsx
2 days ago
constants.ts
2 days ago
cssVars.ts
2 days ago
globals.d.ts
2 days ago
index.tsx
2 days ago
panes.tsx
2 days ago
store.ts
2 days ago
style.scss
2 days ago
types.ts
2 days ago
index.tsx
224 lines
| 1 | /** |
| 2 | * Style Customizer v2 — builder panel entry. Mounts into the "Style" builder tab and renders |
| 3 | * the two-pane panel (controls + live preview), initializing the store from the localized |
| 4 | * payload (`evfStyleV2.payload`) with a REST `apiFetch` fallback. |
| 5 | */ |
| 6 | import React from 'react'; |
| 7 | import { createRoot } from 'react-dom/client'; |
| 8 | import { createPortal } from 'react-dom'; |
| 9 | import './style.scss'; |
| 10 | import { App } from './App'; |
| 11 | import { PreviewSkeleton } from './PreviewPane'; |
| 12 | import { initStore, getStore } from './store'; |
| 13 | import { BootstrapSettings, StylePayload } from './types'; |
| 14 | |
| 15 | const apiFetch = ( window as any ).wp?.apiFetch; |
| 16 | const __ = ( window as any ).wp?.i18n?.__ || ( ( s: string ) => s ); |
| 17 | |
| 18 | const rawSettings: BootstrapSettings | undefined = ( window as any ).evfStyleV2; |
| 19 | |
| 20 | const settings: BootstrapSettings = rawSettings || { |
| 21 | restBase: '', |
| 22 | formId: 0, |
| 23 | formTitle: '', |
| 24 | previewUrl: '', |
| 25 | frontendCssUrl: '', |
| 26 | wrapperId: '', |
| 27 | markerClass: 'evf-style-v2', |
| 28 | previewSession: '', |
| 29 | aiEnabled: false, |
| 30 | aiDisabled: false, |
| 31 | ajaxUrl: '', |
| 32 | aiNonce: '', |
| 33 | aiHintDismissed: true, |
| 34 | }; |
| 35 | |
| 36 | // Bridge so the (legacy jQuery) Fields tab can flag unsaved changes without importing the bundle. |
| 37 | ( window as any ).evfScv2SetFormDirty = ( dirty: boolean ) => { |
| 38 | try { |
| 39 | getStore().setUnsavedFieldChanges( !! dirty ); |
| 40 | } catch ( e ) { |
| 41 | // Store not initialized yet — the builder will call again on the next interaction. |
| 42 | } |
| 43 | }; |
| 44 | |
| 45 | // Reverse bridge: lets the (legacy jQuery) tab-switcher ask "would leaving the Style tab |
| 46 | // right now lose anything?" before it switches away — the existing `beforeunload` guard |
| 47 | // only protects a real page close/reload, not this in-app SPA tab switch. |
| 48 | ( window as any ).evfScv2IsDirty = (): boolean => { |
| 49 | try { |
| 50 | return getStore().isDirty(); |
| 51 | } catch ( e ) { |
| 52 | return false; // Store not initialized yet — nothing to lose. |
| 53 | } |
| 54 | }; |
| 55 | |
| 56 | const MIGRATION_SEEN_PREFIX = 'evf_scv2_migration_seen_'; |
| 57 | |
| 58 | /** Whether this browser has already sat through the migrating transition for this form once. */ |
| 59 | function hasSeenMigration( formId: number ): boolean { |
| 60 | try { |
| 61 | return window.localStorage.getItem( MIGRATION_SEEN_PREFIX + formId ) === '1'; |
| 62 | } catch ( e ) { |
| 63 | return false; |
| 64 | } |
| 65 | } |
| 66 | |
| 67 | function markMigrationSeen( formId: number ) { |
| 68 | try { |
| 69 | window.localStorage.setItem( MIGRATION_SEEN_PREFIX + formId, '1' ); |
| 70 | } catch ( e ) { |
| 71 | // Best-effort only — worst case the transition shows again next visit. |
| 72 | } |
| 73 | } |
| 74 | |
| 75 | // Synchronous init from the localized payload, at module load — before Bootstrap even mounts. |
| 76 | // Except the very first time this form is seen freshly migrated, which shows a one-time |
| 77 | // "Migrating your styles…" transition and re-fetches via REST instead. |
| 78 | let readyFromPayload = false; |
| 79 | let migrationPending = false; |
| 80 | if ( rawSettings && rawSettings.payload && settings.formId ) { |
| 81 | const justMigrated = !! ( rawSettings.payload.migration && rawSettings.payload.migration.just_migrated ); |
| 82 | if ( justMigrated && ! hasSeenMigration( settings.formId ) ) { |
| 83 | migrationPending = true; |
| 84 | } else { |
| 85 | try { |
| 86 | initStore( rawSettings.payload, settings ); |
| 87 | readyFromPayload = true; |
| 88 | } catch ( e ) { |
| 89 | readyFromPayload = false; |
| 90 | } |
| 91 | } |
| 92 | } |
| 93 | |
| 94 | /** Long enough to read as a deliberate, reassuring step rather than a flash. */ |
| 95 | const MIGRATION_MIN_VISIBLE_MS = 900; |
| 96 | |
| 97 | function Bootstrap() { |
| 98 | const [ state, setState ] = React.useState< 'loading' | 'migrating' | 'ready' | 'error' >( |
| 99 | readyFromPayload ? 'ready' : migrationPending ? 'migrating' : 'loading' |
| 100 | ); |
| 101 | const [ message, setMessage ] = React.useState( '' ); |
| 102 | |
| 103 | React.useEffect( () => { |
| 104 | if ( readyFromPayload ) { |
| 105 | return; // Already initialized synchronously above — no fetch needed. |
| 106 | } |
| 107 | |
| 108 | const finish = ( payload: StylePayload ) => { |
| 109 | try { |
| 110 | initStore( payload, settings ); |
| 111 | } catch ( e ) { |
| 112 | setMessage( __( 'The style customizer could not start.', 'everest-forms' ) ); |
| 113 | setState( 'error' ); |
| 114 | return; |
| 115 | } |
| 116 | setState( 'ready' ); |
| 117 | }; |
| 118 | |
| 119 | if ( migrationPending ) { |
| 120 | markMigrationSeen( settings.formId ); // Never show this transition again for this form. |
| 121 | const startedAt = Date.now(); |
| 122 | const fresh = apiFetch && settings.formId |
| 123 | ? apiFetch( { path: `${ settings.restBase }/${ settings.formId }` } ) |
| 124 | : Promise.reject( new Error( 'apiFetch unavailable' ) ); |
| 125 | |
| 126 | // On failure, fall back to the already-embedded payload rather than blocking the panel. |
| 127 | fresh |
| 128 | .catch( () => ( rawSettings as { payload: StylePayload } ).payload ) |
| 129 | .then( ( payload: StylePayload ) => { |
| 130 | const remaining = MIGRATION_MIN_VISIBLE_MS - ( Date.now() - startedAt ); |
| 131 | if ( remaining > 0 ) { |
| 132 | window.setTimeout( () => finish( payload ), remaining ); |
| 133 | } else { |
| 134 | finish( payload ); |
| 135 | } |
| 136 | } ); |
| 137 | return; |
| 138 | } |
| 139 | |
| 140 | if ( ! apiFetch || ! settings.formId ) { |
| 141 | setMessage( __( 'The style customizer could not start.', 'everest-forms' ) ); |
| 142 | setState( 'error' ); |
| 143 | return; |
| 144 | } |
| 145 | apiFetch( { path: `${ settings.restBase }/${ settings.formId }` } ) |
| 146 | .then( ( payload: StylePayload ) => finish( payload ) ) |
| 147 | .catch( ( e: any ) => { |
| 148 | setMessage( ( e && e.message ) || __( 'Failed to load styles.', 'everest-forms' ) ); |
| 149 | setState( 'error' ); |
| 150 | } ); |
| 151 | }, [] ); |
| 152 | |
| 153 | if ( state === 'loading' ) { |
| 154 | return <BootLoading />; |
| 155 | } |
| 156 | if ( state === 'migrating' ) { |
| 157 | return <BootLoading migrating />; |
| 158 | } |
| 159 | if ( state === 'error' ) { |
| 160 | return ( |
| 161 | <div className="evfscv2-boot notice notice-error" style={ { margin: 20 } }> |
| 162 | <p>{ message }</p> |
| 163 | </div> |
| 164 | ); |
| 165 | } |
| 166 | return <App />; |
| 167 | } |
| 168 | |
| 169 | /** Sidebar controls skeleton (subtabs + a few shimmering rows), shown while the schema loads. */ |
| 170 | function SidebarSkeleton() { |
| 171 | return ( |
| 172 | <div className="scv2-skel-side" aria-hidden="true"> |
| 173 | <div className="scv2-skel-tabs"> |
| 174 | <span /> |
| 175 | <span /> |
| 176 | <span /> |
| 177 | </div> |
| 178 | <div className="scv2-skel-body"> |
| 179 | <div className="skel-bar" style={ { width: '40%' } } /> |
| 180 | <div className="skel-bar" style={ { width: '100%', height: 46 } } /> |
| 181 | <div className="skel-bar" style={ { width: '52%', marginTop: 18 } } /> |
| 182 | <div className="skel-bar" style={ { width: '100%', height: 56 } } /> |
| 183 | <div className="skel-bar" style={ { width: '100%', height: 56 } } /> |
| 184 | <div className="skel-bar" style={ { width: '100%', height: 56 } } /> |
| 185 | </div> |
| 186 | </div> |
| 187 | ); |
| 188 | } |
| 189 | |
| 190 | /** Loading state: sidebar skeleton in the controls mount + the preview loader in the content mount. */ |
| 191 | function BootLoading( { migrating = false }: { migrating?: boolean } ) { |
| 192 | const host = document.getElementById( 'evf-scv2-preview' ); |
| 193 | return ( |
| 194 | <> |
| 195 | <SidebarSkeleton /> |
| 196 | { host && |
| 197 | createPortal( |
| 198 | <section |
| 199 | className="preview" |
| 200 | aria-label={ migrating ? __( 'Migrating your styles', 'everest-forms' ) : __( 'Live preview', 'everest-forms' ) } |
| 201 | > |
| 202 | <PreviewSkeleton note={ migrating ? __( 'Migrating your styles…', 'everest-forms' ) : undefined } /> |
| 203 | </section>, |
| 204 | host |
| 205 | ) } |
| 206 | </> |
| 207 | ); |
| 208 | } |
| 209 | |
| 210 | const mount = () => { |
| 211 | // The controls root lives in the builder's native sidebar; <App/> portals the preview into #evf-scv2-preview. |
| 212 | const container = document.getElementById( 'evf-scv2-controls' ); |
| 213 | if ( ! container ) { |
| 214 | return; |
| 215 | } |
| 216 | createRoot( container ).render( <Bootstrap /> ); |
| 217 | }; |
| 218 | |
| 219 | if ( document.readyState === 'loading' ) { |
| 220 | document.addEventListener( 'DOMContentLoaded', mount ); |
| 221 | } else { |
| 222 | mount(); |
| 223 | } |
| 224 |