PluginProbe ʕ •ᴥ•ʔ
Everest Forms – Contact Form, Payment Form, Quiz, Survey & Custom Form Builder with AI / 3.6.0
Everest Forms – Contact Form, Payment Form, Quiz, Survey & Custom Form Builder with AI v3.6.0
3.6.0 3.5.3 3.5.2 3.5.1 3.5.0 3.4.8 3.4.7 3.4.6 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.5.1 1.1.6 1.1.7 1.1.8 1.1.9 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.9 1.5.0 1.5.1 1.5.10 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.6.1 1.6.7 1.7.0 1.7.0.1 1.7.0.2 1.7.0.3 1.7.1 1.7.2 1.7.2.1 1.7.2.2 1.7.3 1.7.4 1.7.5 1.7.5.1 1.7.5.2 1.7.6 1.7.7 1.7.7.1 1.7.7.2 1.7.8 1.7.9 1.8.0 1.8.0.1 1.8.1 1.8.2 1.8.2.1 1.8.2.2 1.8.2.3 1.8.3 1.8.4 1.8.5 1.8.6 1.8.7 1.8.8 1.8.9 1.9.0 1.9.0.1 1.9.1 1.9.2 1.9.3 1.9.4 1.9.4.1 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.0.1 2.0.1 2.0.2 2.0.3 2.0.3.1 2.0.4 2.0.4.1 2.0.5 2.0.6 2.0.7 2.0.8 2.0.8.1 2.0.9 3.0.0 3.0.0.1 3.0.1 3.0.2 3.0.3 3.0.3.1 3.0.4 3.0.4.1 3.0.4.2 3.0.5 3.0.5.1 3.0.5.2 3.0.6 3.0.6.1 3.0.7.1 3.0.8 3.0.8.1 3.0.9 3.0.9.1 3.0.9.2 3.0.9.3 3.0.9.4 3.0.9.5 3.1.0 3.1.1 3.1.2 3.2.0 3.2.1 3.2.2 3.2.3 3.2.4 3.2.5 3.2.6 3.3.0 3.4.0 3.4.1 3.4.2 3.4.2.1 3.4.3 3.4.4 3.4.5 trunk 1.0 1.0.1 1.0.2 1.0.3
everest-forms / src / style-customizer-v2 / index.tsx
everest-forms / src / style-customizer-v2 Last commit date
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