PluginProbe
User Profile Builder – Beautiful User Registration Forms, User Profiles & User Role Editor / 4.0.3
User Profile Builder – Beautiful User Registration Forms, User Profiles & User Role Editor v4.0.3
4.0.3 4.0.2 4.0.1 4.0.0 3.16.6 3.16.5 3.16.4 3.16.3 3.16.2 3.16.1 3.16.0 3.15.9 3.9.9 3.9.5 3.9.6 3.9.7 3.9.8 1.1.7 1.1.8 1.1.9 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 All 341 releases
profile-builder / form-builder / blocks / src / components / ProgressBarPanel.js

ProgressBarPanel.js in User Profile Builder – Beautiful User Registration Forms, User Profiles & User Role Editor 4.0.3, at form-builder/blocks/src/components/ProgressBarPanel.js

265 lines 10.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Progress Bar document sidebar panel + live canvas reconciler.
3 * Gated on `window.wppbFb.progressBar.active`.
4 */
5
6 import { __ } from '@wordpress/i18n';
7 import { registerPlugin } from '@wordpress/plugins';
8 import { PluginDocumentSettingPanel } from '@wordpress/edit-post';
9 import { SelectControl, ToggleControl } from '@wordpress/components';
10 import { useEntityProp } from '@wordpress/core-data';
11 import { useSelect, useDispatch, select as dataSelect } from '@wordpress/data';
12 import { useEffect } from '@wordpress/element';
13 import { createBlock } from '@wordpress/blocks';
14
15 import {
16 PROGRESS_BAR_BLOCK,
17 isStepBoundary,
18 withoutProgressBars,
19 } from '../lib/stepBoundaries';
20
21 /**
22 * Desired bar slots for position setting. Uses shared `isStepBoundary()`.
23 * Returns `{ context, before }` (`before` null = append).
24 */
25 function computeDesiredPlan( nonBarBlocks, position ) {
26 const wantTop = position === 'Top of Form' || position === 'Both';
27 const wantBottom = position === 'Bottom of Form' || position === 'Both';
28 const plan = [];
29
30 if ( wantTop ) {
31 const beforeId = nonBarBlocks.length > 0 ? nonBarBlocks[ 0 ].clientId : null;
32 plan.push( { context: 'top', before: beforeId } );
33 }
34
35 for ( let i = 0; i < nonBarBlocks.length; i++ ) {
36 const b = nonBarBlocks[ i ];
37 if ( ! isStepBoundary( nonBarBlocks, i ) ) continue;
38 if ( wantBottom ) {
39 plan.push( { context: 'bottom', before: b.clientId } );
40 }
41 if ( wantTop ) {
42 const nextId = ( i + 1 < nonBarBlocks.length ) ? nonBarBlocks[ i + 1 ].clientId : null;
43 plan.push( { context: 'top', before: nextId } );
44 }
45 }
46
47 if ( wantBottom ) {
48 plan.push( { context: 'bottom', before: null } );
49 }
50
51 return plan;
52 }
53
54 /**
55 * Pair existing bars to plan slots by matching `position-context`.
56 * Context changes are remove + insert. Returns `{ removes, inserts }`.
57 */
58 function diffPlan( allBlocks, plan ) {
59 const removes = [];
60 const inserts = [];
61 let planIdx = 0;
62
63 const canvasItems = allBlocks.map( ( b ) => ( {
64 clientId: b.clientId,
65 name: b.name,
66 context: b.attributes && b.attributes[ 'position-context' ],
67 } ) );
68
69 const findBarsBefore = ( startIdx ) => {
70 const out = [];
71 for ( let j = startIdx - 1; j >= 0; j-- ) {
72 if ( canvasItems[ j ].name !== PROGRESS_BAR_BLOCK ) break;
73 out.unshift( canvasItems[ j ] );
74 }
75 return out;
76 };
77
78 const consumedBars = new Set();
79
80 for ( ; planIdx < plan.length; planIdx++ ) {
81 const slot = plan[ planIdx ];
82
83 if ( slot.before === null ) break;
84
85 const anchorIdx = canvasItems.findIndex( ( c ) => c.clientId === slot.before );
86 if ( anchorIdx < 0 ) {
87 inserts.push( { context: slot.context, beforeNonBarClientId: slot.before } );
88 continue;
89 }
90
91 const candidateBars = findBarsBefore( anchorIdx ).filter( ( c ) => ! consumedBars.has( c.clientId ) );
92 const match = candidateBars.find( ( c ) => c.context === slot.context );
93 if ( match ) {
94 consumedBars.add( match.clientId );
95 } else {
96 inserts.push( { context: slot.context, beforeNonBarClientId: slot.before } );
97 }
98 }
99
100 const endSlots = plan.slice( planIdx );
101 if ( endSlots.length > 0 ) {
102 let lastNonBarIdx = -1;
103 for ( let i = canvasItems.length - 1; i >= 0; i-- ) {
104 if ( canvasItems[ i ].name !== PROGRESS_BAR_BLOCK ) { lastNonBarIdx = i; break; }
105 }
106 const trailingBars = [];
107 for ( let i = lastNonBarIdx + 1; i < canvasItems.length; i++ ) {
108 if ( canvasItems[ i ].name === PROGRESS_BAR_BLOCK && ! consumedBars.has( canvasItems[ i ].clientId ) ) {
109 trailingBars.push( canvasItems[ i ] );
110 }
111 }
112 for ( const slot of endSlots ) {
113 const match = trailingBars.find( ( c ) => c.context === slot.context && ! consumedBars.has( c.clientId ) );
114 if ( match ) {
115 consumedBars.add( match.clientId );
116 } else {
117 inserts.push( { context: slot.context, beforeNonBarClientId: null } );
118 }
119 }
120 }
121
122 for ( const c of canvasItems ) {
123 if ( c.name === PROGRESS_BAR_BLOCK && ! consumedBars.has( c.clientId ) ) {
124 removes.push( c.clientId );
125 }
126 }
127
128 return { removes, inserts };
129 }
130
131 /**
132 * Keep canvas bars in sync with settings via positional diff.
133 * Fingerprint ignores bars so our own mutations don't loop.
134 */
135 function useProgressBarReconciler( enabled, position ) {
136 const treeFingerprint = useSelect(
137 ( select ) => withoutProgressBars( select( 'core/block-editor' ).getBlocks() )
138 .map( ( b ) => `${ b.name }:${ b.clientId }` )
139 .join( '|' ),
140 []
141 );
142 const { insertBlock, removeBlock, updateBlockAttributes } = useDispatch( 'core/block-editor' );
143
144 useEffect( () => {
145 // Unlock first — `lock.remove` makes plain removeBlock a no-op.
146 const removeLockedBlock = ( clientId ) => {
147 updateBlockAttributes( clientId, { lock: {} } );
148 removeBlock( clientId, false );
149 };
150
151 const allBlocks = dataSelect( 'core/block-editor' ).getBlocks();
152 const nonBar = withoutProgressBars( allBlocks );
153
154 if ( ! enabled ) {
155 for ( const b of allBlocks ) {
156 if ( b.name === PROGRESS_BAR_BLOCK ) removeLockedBlock( b.clientId );
157 }
158 return;
159 }
160
161 const plan = computeDesiredPlan( nonBar, position );
162 const { removes, inserts } = diffPlan( allBlocks, plan );
163
164 for ( const clientId of removes ) {
165 removeLockedBlock( clientId );
166 }
167
168 for ( const slot of inserts ) {
169 const currentBlocks = dataSelect( 'core/block-editor' ).getBlocks();
170 let idx = currentBlocks.length;
171 if ( slot.beforeNonBarClientId !== null ) {
172 idx = currentBlocks.findIndex( ( b ) => b.clientId === slot.beforeNonBarClientId );
173 if ( idx < 0 ) idx = currentBlocks.length;
174 }
175 insertBlock(
176 createBlock( PROGRESS_BAR_BLOCK, {
177 'position-context': slot.context,
178 lock: { move: true, remove: true },
179 } ),
180 idx,
181 undefined,
182 false
183 );
184 }
185 }, [ enabled, position, treeFingerprint, insertBlock, removeBlock, updateBlockAttributes ] );
186 }
187
188 /**
189 * Split across components so hook count never changes when meta resolves late.
190 */
191 const ProgressBarPanel = () => {
192 const postType = useSelect( ( select ) => select( 'core/editor' ).getCurrentPostType(), [] );
193 if ( ! [ 'wppb-rf-cpt', 'wppb-epf-cpt' ].includes( postType ) ) return null;
194 if ( ! ( window.wppbFb && window.wppbFb.progressBar && window.wppbFb.progressBar.active ) ) return null;
195
196 return <ProgressBarPanelInner postType={ postType } />;
197 };
198
199 const ProgressBarPanelInner = ( { postType } ) => {
200 const [ meta, setMeta ] = useEntityProp( 'postType', postType, 'meta' );
201 if ( ! meta ) return null;
202
203 return <ProgressBarPanelBody meta={ meta } setMeta={ setMeta } />;
204 };
205
206 const ProgressBarPanelBody = ( { meta, setMeta } ) => {
207 const enabled = meta.wppb_fb_progress_bar_enable === 'Yes';
208 const position = meta.wppb_fb_progress_bar_position || 'Top of Form';
209
210 useProgressBarReconciler( enabled, position );
211
212 return (
213 <PluginDocumentSettingPanel
214 name="wppb-progress-bar-settings"
215 title={ __( 'Progress Bar', 'profile-builder' ) }
216 >
217 <ToggleControl
218 label={ __( 'Enable Progress Bar', 'profile-builder' ) }
219 checked={ enabled }
220 onChange={ ( val ) => setMeta( { wppb_fb_progress_bar_enable: val ? 'Yes' : 'No' } ) }
221 help={ __( 'Show a completion progress bar on the form.', 'profile-builder' ) }
222 />
223
224 { enabled && (
225 <>
226 <SelectControl
227 label={ __( 'Calculation Mode', 'profile-builder' ) }
228 value={ meta.wppb_fb_progress_bar_calculation_mode || 'All Fields' }
229 options={ [
230 { label: __( 'All Fields', 'profile-builder' ), value: 'All Fields' },
231 { label: __( 'Required Fields', 'profile-builder' ), value: 'Required Fields' },
232 ] }
233 onChange={ ( val ) => setMeta( { wppb_fb_progress_bar_calculation_mode: val } ) }
234 help={ __( 'Choose how progress is calculated.', 'profile-builder' ) }
235 />
236 <SelectControl
237 label={ __( 'Style / Display Options', 'profile-builder' ) }
238 value={ meta.wppb_fb_progress_bar_display_options || 'Bar Only' }
239 options={ [
240 { label: __( 'Bar Only', 'profile-builder' ), value: 'Bar Only' },
241 { label: __( 'Bar and Percentage Label', 'profile-builder' ), value: 'Bar and Percentage Label' },
242 ] }
243 onChange={ ( val ) => setMeta( { wppb_fb_progress_bar_display_options: val } ) }
244 />
245 <SelectControl
246 label={ __( 'Position', 'profile-builder' ) }
247 value={ meta.wppb_fb_progress_bar_position || 'Top of Form' }
248 options={ [
249 { label: __( 'Top of Form', 'profile-builder' ), value: 'Top of Form' },
250 { label: __( 'Bottom of Form', 'profile-builder' ), value: 'Bottom of Form' },
251 { label: __( 'Both', 'profile-builder' ), value: 'Both' },
252 ] }
253 onChange={ ( val ) => setMeta( { wppb_fb_progress_bar_position: val } ) }
254 help={ __( 'Where to display the progress bar.', 'profile-builder' ) }
255 />
256 </>
257 ) }
258 </PluginDocumentSettingPanel>
259 );
260 };
261
262 registerPlugin( 'wppb-progress-bar-settings-plugin', {
263 render: ProgressBarPanel,
264 } );
265