PluginProbe
User Profile Builder – Beautiful User Registration Forms, User Profiles & User Role Editor / 4.0.0
User Profile Builder – Beautiful User Registration Forms, User Profiles & User Role Editor v4.0.0
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 / WooAddressFieldEdit.js

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

283 lines 11.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Shared editor for Woo Billing / Shipping address blocks. Each block passes
3 * a `config` (fieldsKey, csvAttr, nameAttr, sortAttr, panelTitle).
4 *
5 * UI and storage match the classic Manage Fields `woocheckbox` shape (CSV of
6 * included keys + `required_<key>` markers; empty CSV = all with defaults).
7 * `sortAttr` keeps full row order including excluded keys.
8 */
9 import { __ } from '@wordpress/i18n';
10 import { PanelBody } from '@wordpress/components';
11 import { useRef, useState } from '@wordpress/element';
12 import BaseFieldEdit from './BaseFieldEdit';
13
14 const REQ = 'required_';
15
16 function csvList( csv ) {
17 if ( ! csv ) return [];
18 return csv.split( ',' ).map( ( v ) => v.trim() ).filter( Boolean );
19 }
20
21 // Parse csvAttr → { included, required }. Tokens starting with `required_` mark required.
22 function parseFields( csv, allKeys ) {
23 const required = new Set();
24 const included = new Set();
25 for ( const t of csvList( csv ) ) {
26 if ( t.indexOf( REQ ) === 0 ) {
27 required.add( t.slice( REQ.length ) );
28 } else if ( allKeys.includes( t ) ) {
29 included.add( t );
30 }
31 }
32 return { included, required };
33 }
34
35 // sortAttr keys first, then remaining defaults. Drops `required_` / unknown keys.
36 function reconstructRowOrder( sortStr, allKeys ) {
37 const seen = new Set();
38 const out = [];
39 for ( const t of csvList( sortStr ) ) {
40 if ( t.indexOf( REQ ) === 0 ) continue;
41 if ( allKeys.includes( t ) && ! seen.has( t ) ) {
42 seen.add( t );
43 out.push( t );
44 }
45 }
46 for ( const k of allKeys ) {
47 if ( ! seen.has( k ) ) out.push( k );
48 }
49 return out;
50 }
51
52 // Included keys in row order, with `required_<key>` markers.
53 function buildCsv( order, includedSet, requiredSet ) {
54 const out = [];
55 for ( const key of order ) {
56 if ( ! includedSet.has( key ) ) continue;
57 out.push( key );
58 if ( requiredSet.has( key ) ) out.push( REQ + key );
59 }
60 return out.join( ',' );
61 }
62
63 // Classic-compatible full row order (comma-space keys).
64 function serializeOrder( order ) {
65 return order.length ? order.join( ', ' ) : '';
66 }
67
68 export default function WooAddressFieldEdit( props ) {
69 const { config, ...blockProps } = props;
70 const { attributes, setAttributes } = props;
71
72 const dragKeyRef = useRef( null );
73 const [ dragOverKey, setDragOverKey ] = useState( null );
74
75 const fb = ( typeof window !== 'undefined' && window.wppbFb ) || {};
76 const woo = fb.woocommerce || {};
77 const subFields = Array.isArray( woo[ config.fieldsKey ] ) ? woo[ config.fieldsKey ] : [];
78 const allKeys = subFields.map( ( f ) => f.key );
79 const byKey = ( key ) => subFields.find( ( f ) => f.key === key );
80
81 // Unchecking Country also unchecks State (classic parity).
82 const countryKey = allKeys.find( ( k ) => k.endsWith( '_country' ) );
83 const stateKey = allKeys.find( ( k ) => k.endsWith( '_state' ) );
84
85 const rowOrder = reconstructRowOrder( attributes[ config.sortAttr ] || '', allKeys );
86
87 const csv = attributes[ config.csvAttr ] || '';
88 const parsed = csv === ''
89 // Empty CSV = all included with default-required.
90 ? {
91 included: new Set( allKeys ),
92 required: new Set( subFields.filter( ( f ) => f.required === 'Yes' ).map( ( f ) => f.key ) ),
93 }
94 : parseFields( csv, allKeys );
95
96 let names = {};
97 try { names = JSON.parse( attributes[ config.nameAttr ] || '{}' ) || {}; } catch ( e ) { names = {}; }
98
99 const commit = ( includedSet, requiredSet, nameMap ) => setAttributes( {
100 [ config.csvAttr ]: buildCsv( rowOrder, includedSet, requiredSet ),
101 [ config.nameAttr ]: JSON.stringify( nameMap ),
102 } );
103
104 const commitOrder = ( order ) => setAttributes( {
105 [ config.sortAttr ]: serializeOrder( order ),
106 [ config.csvAttr ]: buildCsv( order, parsed.included, parsed.required ),
107 } );
108
109 const toggleInclude = ( key, on ) => {
110 const included = new Set( parsed.included );
111 const required = new Set( parsed.required );
112 if ( on ) {
113 included.add( key );
114 } else {
115 included.delete( key );
116 required.delete( key );
117 if ( key === countryKey && stateKey ) {
118 included.delete( stateKey );
119 required.delete( stateKey );
120 }
121 }
122 commit( included, required, names );
123 };
124
125 const toggleRequired = ( key, on ) => {
126 const required = new Set( parsed.required );
127 if ( on ) { required.add( key ); } else { required.delete( key ); }
128 commit( parsed.included, required, names );
129 };
130
131 const setName = ( key, val ) => {
132 const nameMap = { ...names };
133 if ( val ) { nameMap[ key ] = val; } else { delete nameMap[ key ]; }
134 commit( parsed.included, parsed.required, nameMap );
135 };
136
137 const isIncluded = ( key ) => parsed.included.has( key );
138
139 // Drag from the handle only so the label input stays selectable.
140 const onHandleDragStart = ( key ) => ( e ) => {
141 dragKeyRef.current = key;
142 e.stopPropagation();
143 try {
144 e.dataTransfer.effectAllowed = 'move';
145 e.dataTransfer.setData( 'text/plain', key );
146 } catch ( _ ) { /* older browsers */ }
147 const row = e.currentTarget.closest( '.wppb-fb-woo-fields__row' );
148 if ( row && e.dataTransfer.setDragImage ) {
149 e.dataTransfer.setDragImage( row, 10, 10 );
150 }
151 };
152
153 const onHandleDragEnd = () => {
154 dragKeyRef.current = null;
155 setDragOverKey( null );
156 };
157
158 const onRowDragOver = ( key ) => ( e ) => {
159 if ( dragKeyRef.current == null ) return;
160 e.preventDefault();
161 e.dataTransfer.dropEffect = 'move';
162 if ( dragOverKey !== key ) setDragOverKey( key );
163 };
164
165 const onRowDrop = ( targetKey ) => ( e ) => {
166 e.preventDefault();
167 e.stopPropagation();
168 const src = dragKeyRef.current;
169 dragKeyRef.current = null;
170 setDragOverKey( null );
171 if ( ! src || src === targetKey ) return;
172 const next = rowOrder.slice();
173 const from = next.indexOf( src );
174 if ( from < 0 ) return;
175 next.splice( from, 1 );
176 const to = next.indexOf( targetKey );
177 if ( to < 0 ) return;
178 next.splice( to, 0, src );
179 commitOrder( next );
180 };
181
182 const panel = (
183 <PanelBody title={ config.panelTitle }>
184 { subFields.length === 0 ? (
185 <p>{ __( 'WooCommerce field list unavailable — activate the WooCommerce integration.', 'profile-builder' ) }</p>
186 ) : (
187 <div className="wppb-fb-woo-fields">
188 <div className="wppb-fb-woo-fields__head">
189 <span className="wppb-fb-woo-fields__head-name">{ __( 'Field Name', 'profile-builder' ) }</span>
190 <span className="wppb-fb-woo-fields__head-req">{ __( 'Required', 'profile-builder' ) }</span>
191 </div>
192 { rowOrder.map( ( key ) => {
193 const f = byKey( key );
194 if ( ! f ) return null;
195 const included = isIncluded( key );
196 const rowClass = [
197 'wppb-fb-woo-fields__row',
198 included ? 'is-included' : '',
199 dragOverKey === key ? 'is-drag-over' : '',
200 ].filter( Boolean ).join( ' ' );
201 return (
202 <div
203 key={ key }
204 className={ rowClass }
205 onDragOver={ onRowDragOver( key ) }
206 onDrop={ onRowDrop( key ) }
207 >
208 <span
209 className="wppb-fb-woo-fields__handle"
210 draggable={ true }
211 onDragStart={ onHandleDragStart( key ) }
212 onDragEnd={ onHandleDragEnd }
213 role="button"
214 aria-label={ __( 'Drag to reorder', 'profile-builder' ) }
215 title={ __( 'Drag to reorder', 'profile-builder' ) }
216 >
217 <span className="dashicons dashicons-menu" aria-hidden="true"></span>
218 </span>
219 <input
220 type="checkbox"
221 className="wppb-fb-woo-fields__include"
222 checked={ included }
223 aria-label={ f.label }
224 onChange={ ( e ) => toggleInclude( key, e.target.checked ) }
225 />
226 <div className="wppb-fb-woo-fields__name">
227 <input
228 type="text"
229 value={ names[ key ] != null ? names[ key ] : '' }
230 placeholder={ f.label }
231 disabled={ ! included }
232 title={ __( 'Click to rename', 'profile-builder' ) }
233 onChange={ ( e ) => setName( key, e.target.value ) }
234 />
235 <span className="dashicons dashicons-edit" aria-hidden="true"></span>
236 </div>
237 <span className="wppb-fb-woo-fields__req-cell">
238 <input
239 type="checkbox"
240 className="wppb-fb-woo-fields__req"
241 checked={ parsed.required.has( key ) }
242 disabled={ ! included }
243 aria-label={ `${ f.label } — ${ __( 'required', 'profile-builder' ) }` }
244 onChange={ ( e ) => toggleRequired( key, e.target.checked ) }
245 />
246 </span>
247 </div>
248 );
249 } ) }
250 </div>
251 ) }
252 </PanelBody>
253 );
254
255 const orderedIncluded = rowOrder.filter( ( k ) => parsed.included.has( k ) );
256
257 return (
258 <BaseFieldEdit { ...blockProps } inspectorPanels={ panel } hideDescription>
259 <div className="wppb-fb-woo-address">
260 { orderedIncluded.length > 0 ? (
261 orderedIncluded.map( ( key ) => {
262 const f = byKey( key );
263 const label = names[ key ] || ( f ? f.label : key );
264 return (
265 <div className="wppb-fb-woo-address__field" key={ key }>
266 <label className="wppb-fb-woo-address__label">
267 { label }
268 { parsed.required.has( key ) && (
269 <span className="wppb-fb-field-required">*</span>
270 ) }
271 </label>
272 <input type="text" disabled />
273 </div>
274 );
275 } )
276 ) : (
277 <div>{ __( '(no fields selected)', 'profile-builder' ) }</div>
278 ) }
279 </div>
280 </BaseFieldEdit>
281 );
282 }
283