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 / BlockAttributeMirror.js

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

219 lines 9.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Subscribe to core/block-editor and drive per-edit REST mirror for PB fields.
3 * First sighting is capture-only; later changes schedule debounced PUTs.
4 * Write-back applies sanitized server values when the user hasn't typed past them.
5 */
6
7 import { useEffect, useRef } from '@wordpress/element';
8 import { useSelect, useDispatch, useRegistry } from '@wordpress/data';
9 import { registerPlugin } from '@wordpress/plugins';
10 import {
11 scheduleAttributePersist,
12 captureInitial,
13 forget,
14 migrate,
15 registerWriteBack,
16 registerSnapshotSync,
17 } from '../lib/persistFieldAttributes';
18 import { makeWriteBackHandler } from '../lib/writeBackHandler';
19 import { REPEATER_BLOCK_NAME as REPEATER_BLOCK } from '../lib/repeaterBlockName';
20
21 /** Collect PB field blocks with scope (`top` or `sub:<parentMetaName>`). */
22 function collectPbBlocks( blocks, scope, parentMetaName, out, fieldTypeMap ) {
23 for ( const b of blocks ) {
24 if ( ! b || ! b.name ) continue;
25 const isPb = b.name.startsWith( 'profile-builder/' );
26 if ( isPb && b.attributes && b.attributes.id ) {
27 const fieldType = fieldTypeMap[ b.name ] || '';
28 out.push( {
29 clientId: b.clientId,
30 name: b.name,
31 id: Number( b.attributes.id ),
32 attributes: b.attributes,
33 scope,
34 fieldType,
35 } );
36 }
37 if ( b.innerBlocks && b.innerBlocks.length ) {
38 if ( b.name === REPEATER_BLOCK && b.attributes ) {
39 const childScope = 'sub:' + ( b.attributes[ 'meta-name' ] || '' );
40 // Skip until Repeater has a meta-name (parent option key).
41 if ( b.attributes[ 'meta-name' ] ) {
42 collectPbBlocks( b.innerBlocks, childScope, b.attributes[ 'meta-name' ], out, fieldTypeMap );
43 }
44 } else {
45 collectPbBlocks( b.innerBlocks, scope, parentMetaName, out, fieldTypeMap );
46 }
47 }
48 }
49 return out;
50 }
51
52 const BlockAttributeMirror = () => {
53 const registry = useRegistry();
54 const { updateBlockAttributes, __unstableMarkNextChangeAsNotPersistent } = useDispatch( 'core/block-editor' );
55
56 const postType = useSelect(
57 ( select ) => select( 'core/editor' ).getCurrentPostType(),
58 []
59 );
60 const isOurPostType = [ 'wppb-rf-cpt', 'wppb-epf-cpt' ].includes( postType );
61
62 const seenKeys = useRef( new Set() );
63 const writeBackUnregs = useRef( new Map() ); // clientId → unregister fn
64
65 useEffect( () => {
66 if ( ! isOurPostType ) return undefined;
67
68 const fieldTypeMap = ( window.wppbFb && window.wppbFb.conditionalFields && window.wppbFb.conditionalFields.blockToFieldType ) || {};
69
70 const buildWriteBack = makeWriteBackHandler( {
71 getBlock: ( clientId ) => registry.select( 'core/block-editor' ).getBlock( clientId ),
72 updateBlockAttributes,
73 markNonPersistent: typeof __unstableMarkNextChangeAsNotPersistent === 'function'
74 ? __unstableMarkNextChangeAsNotPersistent
75 : undefined,
76 } );
77
78 let prevBlocks = null;
79 // Skip persist when attributes ref unchanged (array rebuild false positives).
80 const prevAttrsRef = new Map(); // key (scope:id) → attributes object reference
81
82 const runTick = () => {
83 const blocks = registry.select( 'core/block-editor' ).getBlocks();
84 if ( blocks === prevBlocks ) return;
85 prevBlocks = blocks;
86
87 const collected = collectPbBlocks( blocks, 'top', null, [], fieldTypeMap );
88
89 // Duplicate block copies id — reset dupes to 0 for re-allocation.
90 const seenIdInTick = new Set();
91 const dedupClientIds = [];
92 for ( const item of collected ) {
93 if ( seenIdInTick.has( item.id ) ) {
94 dedupClientIds.push( item.clientId );
95 } else {
96 seenIdInTick.add( item.id );
97 }
98 }
99 if ( dedupClientIds.length > 0 ) {
100 if ( typeof __unstableMarkNextChangeAsNotPersistent === 'function' ) {
101 __unstableMarkNextChangeAsNotPersistent();
102 }
103 for ( const clientId of dedupClientIds ) {
104 updateBlockAttributes( clientId, { id: 0 } );
105 }
106 return;
107 }
108
109 const presentKeys = new Set();
110 for ( const item of collected ) presentKeys.add( item.scope + ':' + item.id );
111
112 // Re-scope persister state when Repeater meta-name renames a sub key.
113 const vanished = [];
114 for ( const key of seenKeys.current ) {
115 if ( ! presentKeys.has( key ) ) vanished.push( key );
116 }
117 if ( vanished.length > 0 ) {
118 const appearedById = new Map();
119 for ( const item of collected ) {
120 const key = item.scope + ':' + item.id;
121 if ( ! seenKeys.current.has( key ) ) appearedById.set( item.id, item );
122 }
123 for ( const oldKey of vanished ) {
124 const [ oldScope, oldIdStr ] = oldKey.split( /:(?=[^:]+$)/ );
125 const oldId = Number( oldIdStr );
126 const successor = appearedById.get( oldId );
127 if ( successor && successor.scope !== oldScope ) {
128 const newKey = successor.scope + ':' + successor.id;
129 migrate( oldScope, oldId, {
130 scope: successor.scope,
131 id: successor.id,
132 fieldType: successor.fieldType,
133 clientId: successor.clientId,
134 } );
135 seenKeys.current.delete( oldKey );
136 seenKeys.current.add( newKey );
137 if ( prevAttrsRef.has( oldKey ) ) {
138 prevAttrsRef.set( newKey, prevAttrsRef.get( oldKey ) );
139 prevAttrsRef.delete( oldKey );
140 }
141 appearedById.delete( oldId );
142 }
143 }
144 }
145
146 for ( const item of collected ) {
147 const key = item.scope + ':' + item.id;
148
149 if ( ! writeBackUnregs.current.has( item.clientId ) ) {
150 const unreg = registerWriteBack( item.clientId, buildWriteBack( item.clientId ) );
151 writeBackUnregs.current.set( item.clientId, unreg );
152 }
153
154 if ( ! seenKeys.current.has( key ) ) {
155 captureInitial( item.scope, item.id, item.attributes );
156 seenKeys.current.add( key );
157 prevAttrsRef.set( key, item.attributes );
158 continue;
159 }
160
161 if ( prevAttrsRef.get( key ) === item.attributes ) continue;
162 prevAttrsRef.set( key, item.attributes );
163
164 scheduleAttributePersist( {
165 scope: item.scope,
166 id: item.id,
167 fieldType: item.fieldType,
168 clientId: item.clientId,
169 attributes: item.attributes,
170 } );
171 }
172
173 for ( const key of Array.from( seenKeys.current ) ) {
174 if ( ! presentKeys.has( key ) ) {
175 const [ scope, idStr ] = key.split( /:(?=[^:]+$)/ );
176 forget( scope, Number( idStr ) );
177 seenKeys.current.delete( key );
178 prevAttrsRef.delete( key );
179 }
180 }
181 const liveClientIds = new Set( collected.map( ( c ) => c.clientId ) );
182 for ( const [ clientId, unreg ] of Array.from( writeBackUnregs.current.entries() ) ) {
183 if ( ! liveClientIds.has( clientId ) ) {
184 unreg();
185 writeBackUnregs.current.delete( clientId );
186 }
187 }
188 };
189
190 // Coalesce store bursts into one microtask (works when tab is backgrounded).
191 let pendingTick = false;
192 let disposed = false;
193 const scheduleTick = () => {
194 if ( pendingTick ) return;
195 pendingTick = true;
196 Promise.resolve().then( () => {
197 pendingTick = false;
198 if ( ! disposed ) runTick();
199 } );
200 };
201
202 runTick();
203 const unsubscribe = registry.subscribe( scheduleTick );
204 return () => {
205 disposed = true;
206 unsubscribe();
207 for ( const unreg of writeBackUnregs.current.values() ) unreg();
208 writeBackUnregs.current.clear();
209 seenKeys.current.clear();
210 };
211 }, [ isOurPostType, registry, updateBlockAttributes, __unstableMarkNextChangeAsNotPersistent ] );
212
213 return null;
214 };
215
216 registerPlugin( 'wppb-fb-block-attribute-mirror', { render: BlockAttributeMirror } );
217
218 export { registerSnapshotSync };
219