PluginProbe ʕ •ᴥ•ʔ
Secure Custom Fields / trunk
Secure Custom Fields vtrunk
6.9.1 6.9.0 6.8.9 6.8.7 6.8.8 6.8.6 6.8.4 6.8.5 trunk 6.4.0-beta1 6.4.0-beta2 6.4.1 6.4.1-beta3 6.4.1-beta4 6.4.1-beta5 6.4.1-beta6 6.4.1-beta7 6.4.2 6.5.0 6.5.1 6.5.2 6.5.3 6.5.4 6.5.5 6.5.6 6.5.7 6.6.0 6.7.0 6.7.1 6.8.0 6.8.1 6.8.2 6.8.3
secure-custom-fields / assets / src / js / pro / acf-datastore.js
secure-custom-fields / assets / src / js / pro Last commit date
blocks-v3 3 weeks ago _acf-blocks-v3.js 2 months ago _acf-blocks.js 2 months ago _acf-field-flexible-content.js 3 weeks ago _acf-field-gallery.js 1 month ago _acf-field-repeater.js 2 months ago _acf-jsx-names.js 1 year ago _acf-setting-clone.js 1 year ago _acf-setting-flexible-content.js 10 months ago _acf-setting-repeater.js 1 year ago _acf-ui-options-page.js 8 months ago acf-datastore.js 1 month ago acf-field-bindings.js 1 month ago acf-pro-blocks.js 8 months ago acf-pro-field-group.js 1 year ago acf-pro-input.js 1 year ago acf-pro-ui-options-page.js 1 year ago
acf-datastore.js
1345 lines
1 /* global acf, ajaxurl, jQuery */
2 ( () => {
3 'use strict';
4
5 const STORE_NAME = 'acf/fields';
6 const COMPLEX_FIELD_TYPES = [
7 'repeater',
8 'group',
9 'flexible_content',
10 'clone',
11 ];
12 const AJAX_LOOKUP_FIELD_TYPES = new Set( [
13 'post_object',
14 'page_link',
15 'relationship',
16 'taxonomy',
17 'user',
18 ] );
19 const REPEATER_ROW_STATUS_NAME_PATTERN = new RegExp(
20 `\\[acf_(${ [
21 'added',
22 'changed',
23 'deleted',
24 'reordered',
25 'inserted',
26 ].join( '|' ) })]$`
27 );
28
29 const DEFAULT_STATE = {
30 context: { postId: 0, postType: '' },
31 fields: {},
32 values: {},
33 savedValues: {},
34 nameToKey: {},
35 fieldGroups: [],
36 initialized: false,
37 syncing: false,
38 };
39
40 const cloneValue = ( value ) => {
41 if ( null === value || 'object' !== typeof value ) {
42 return value;
43 }
44
45 return JSON.parse( JSON.stringify( value ) );
46 };
47
48 const isComplexFieldType = ( type ) => COMPLEX_FIELD_TYPES.includes( type );
49
50 const buildNameToKeyMap = ( fields ) => {
51 const nameToKey = {};
52
53 for ( const fieldKey of Object.keys( fields ) ) {
54 const field = fields[ fieldKey ];
55 if ( field.name ) {
56 nameToKey[ field.name ] = fieldKey;
57 }
58 }
59
60 return nameToKey;
61 };
62
63 const resolveFieldKey = ( state, keyOrName ) => {
64 if ( ! keyOrName ) {
65 return undefined;
66 }
67
68 return state.fields[ keyOrName ]
69 ? keyOrName
70 : state.nameToKey[ keyOrName ];
71 };
72
73 const getNestedValue = ( value, path ) => {
74 let currentValue = value;
75
76 for ( const pathPart of path ) {
77 if ( null === currentValue || undefined === currentValue ) {
78 return undefined;
79 }
80 currentValue = currentValue[ pathPart ];
81 }
82
83 return currentValue;
84 };
85
86 const setNestedValue = ( value, path, nextValue ) => {
87 if ( 0 === path.length ) {
88 return nextValue;
89 }
90
91 const pathPart = path[ 0 ];
92 const currentValue = value ?? {};
93 const clonedValue = Array.isArray( currentValue )
94 ? [ ...currentValue ]
95 : { ...currentValue };
96
97 clonedValue[ pathPart ] = setNestedValue(
98 clonedValue[ pathPart ],
99 path.slice( 1 ),
100 nextValue
101 );
102
103 return clonedValue;
104 };
105
106 const reducer = ( state = DEFAULT_STATE, action ) => {
107 switch ( action.type ) {
108 case 'INITIALIZE_STORE': {
109 const fields = action.fields ?? {};
110 const values = action.values ?? {};
111
112 return {
113 ...state,
114 context: action.context ?? state.context,
115 fields,
116 values,
117 savedValues: cloneValue( values ),
118 nameToKey: buildNameToKeyMap( fields ),
119 fieldGroups: action.fieldGroups ?? [],
120 initialized: true,
121 };
122 }
123
124 case 'REGISTER_FIELD_GROUP': {
125 const fields = { ...state.fields, ...( action.fields ?? {} ) };
126 const incomingValues = action.values ?? {};
127 const values = { ...state.values };
128 const savedValues = { ...state.savedValues };
129
130 for ( const fieldKey of Object.keys( incomingValues ) ) {
131 if ( ! ( fieldKey in values ) ) {
132 values[ fieldKey ] = incomingValues[ fieldKey ];
133 }
134 if ( ! ( fieldKey in savedValues ) ) {
135 savedValues[ fieldKey ] = cloneValue(
136 incomingValues[ fieldKey ]
137 );
138 }
139 }
140
141 const existingGroupKeys = new Set(
142 state.fieldGroups.map( ( fieldGroup ) => fieldGroup.key )
143 );
144 const fieldGroups = ( action.fieldGroups ?? [] ).filter(
145 ( fieldGroup ) => ! existingGroupKeys.has( fieldGroup.key )
146 );
147 const context =
148 0 === state.context.postId && action.context
149 ? action.context
150 : state.context;
151
152 return {
153 ...state,
154 context,
155 fields,
156 values,
157 savedValues,
158 nameToKey: buildNameToKeyMap( fields ),
159 fieldGroups: [ ...state.fieldGroups, ...fieldGroups ],
160 initialized: true,
161 };
162 }
163
164 case 'SET_FIELD_VALUE': {
165 const fieldKey = resolveFieldKey( state, action.fieldKey );
166 if ( ! fieldKey ) {
167 return state;
168 }
169
170 return {
171 ...state,
172 values: { ...state.values, [ fieldKey ]: action.value },
173 };
174 }
175
176 case 'SET_VALUES': {
177 const values = {};
178
179 for ( const keyOrName of Object.keys( action.values ) ) {
180 const fieldKey = resolveFieldKey( state, keyOrName );
181 if ( fieldKey ) {
182 values[ fieldKey ] = action.values[ keyOrName ];
183 }
184 }
185
186 return { ...state, values: { ...state.values, ...values } };
187 }
188
189 case 'SET_SUB_FIELD_VALUE': {
190 const parentKey = resolveFieldKey( state, action.parentKey );
191 if ( ! parentKey ) {
192 return state;
193 }
194
195 const parentValue = state.values[ parentKey ] ?? {};
196 const nextParentValue = setNestedValue(
197 parentValue,
198 action.path,
199 action.value
200 );
201
202 return {
203 ...state,
204 values: { ...state.values, [ parentKey ]: nextParentValue },
205 };
206 }
207
208 case 'ADD_REPEATER_ROW': {
209 const fieldKey = resolveFieldKey( state, action.fieldKey );
210 if ( ! fieldKey ) {
211 return state;
212 }
213
214 const currentRows = Array.isArray( state.values[ fieldKey ] )
215 ? state.values[ fieldKey ]
216 : [];
217 const rows = [ ...currentRows ];
218 const index = Math.max(
219 0,
220 Math.min( action.index ?? rows.length, rows.length )
221 );
222
223 rows.splice( index, 0, action.rowData ?? {} );
224
225 return {
226 ...state,
227 values: { ...state.values, [ fieldKey ]: rows },
228 };
229 }
230
231 case 'REMOVE_REPEATER_ROW': {
232 const fieldKey = resolveFieldKey( state, action.fieldKey );
233 if ( ! fieldKey ) {
234 return state;
235 }
236
237 const rows = state.values[ fieldKey ];
238 if ( ! Array.isArray( rows ) ) {
239 return state;
240 }
241 if ( action.rowIndex < 0 || action.rowIndex >= rows.length ) {
242 return state;
243 }
244
245 const nextRows = [ ...rows ];
246 nextRows.splice( action.rowIndex, 1 );
247
248 return {
249 ...state,
250 values: { ...state.values, [ fieldKey ]: nextRows },
251 };
252 }
253
254 case 'MOVE_REPEATER_ROW': {
255 const fieldKey = resolveFieldKey( state, action.fieldKey );
256 if ( ! fieldKey ) {
257 return state;
258 }
259
260 const rows = state.values[ fieldKey ];
261 if ( ! Array.isArray( rows ) ) {
262 return state;
263 }
264 if (
265 action.fromIndex < 0 ||
266 action.fromIndex >= rows.length ||
267 action.toIndex < 0 ||
268 action.toIndex >= rows.length
269 ) {
270 return state;
271 }
272
273 const nextRows = [ ...rows ];
274 const [ movedRow ] = nextRows.splice( action.fromIndex, 1 );
275 nextRows.splice( action.toIndex, 0, movedRow );
276
277 return {
278 ...state,
279 values: { ...state.values, [ fieldKey ]: nextRows },
280 };
281 }
282
283 case 'MARK_AS_SAVED':
284 return { ...state, savedValues: cloneValue( state.values ) };
285
286 case 'SET_SYNCING':
287 return { ...state, syncing: action.isSyncing };
288
289 default:
290 return state;
291 }
292 };
293
294 const selectors = {
295 getFieldValue: ( state, keyOrName ) => {
296 const fieldKey = resolveFieldKey( state, keyOrName );
297 return fieldKey ? state.values[ fieldKey ] : undefined;
298 },
299 getFieldValueByKey: ( state, fieldKey ) => state.values[ fieldKey ],
300 getFieldValueByName: ( state, fieldName ) => {
301 const fieldKey = state.nameToKey[ fieldName ];
302 return fieldKey ? state.values[ fieldKey ] : undefined;
303 },
304 getAllValues: ( state ) => state.values,
305 getAllValuesByName: ( state ) => {
306 const valuesByName = {};
307
308 for ( const fieldName of Object.keys( state.nameToKey ) ) {
309 const fieldKey = state.nameToKey[ fieldName ];
310 if ( fieldKey in state.values ) {
311 valuesByName[ fieldName ] = state.values[ fieldKey ];
312 }
313 }
314
315 return valuesByName;
316 },
317 getChangedValues: ( state ) => {
318 const changedValues = {};
319
320 for ( const fieldKey of Object.keys( state.values ) ) {
321 if (
322 JSON.stringify( state.values[ fieldKey ] ) !==
323 JSON.stringify( state.savedValues[ fieldKey ] )
324 ) {
325 changedValues[ fieldKey ] = state.values[ fieldKey ];
326 }
327 }
328
329 return changedValues;
330 },
331 getField: ( state, keyOrName ) => {
332 const fieldKey = resolveFieldKey( state, keyOrName );
333 return fieldKey ? state.fields[ fieldKey ] : undefined;
334 },
335 getFields: ( state ) => state.fields,
336 getFieldsByGroup: ( state, fieldGroupKey ) => {
337 const fields = {};
338
339 for ( const fieldKey of Object.keys( state.fields ) ) {
340 if (
341 state.fields[ fieldKey ].fieldGroupKey === fieldGroupKey
342 ) {
343 fields[ fieldKey ] = state.fields[ fieldKey ];
344 }
345 }
346
347 return fields;
348 },
349 getFieldKeyByName: ( state, fieldName ) => state.nameToKey[ fieldName ],
350 getSubFieldValue: ( state, parentKey, ...path ) => {
351 const fieldKey = resolveFieldKey( state, parentKey );
352 return fieldKey
353 ? getNestedValue( state.values[ fieldKey ], path )
354 : undefined;
355 },
356 isInitialized: ( state ) => state.initialized,
357 isSyncing: ( state ) => state.syncing,
358 hasChanges: ( state ) =>
359 JSON.stringify( state.values ) !==
360 JSON.stringify( state.savedValues ),
361 isDirty: ( state, keyOrName ) => {
362 const fieldKey = resolveFieldKey( state, keyOrName );
363
364 return !! (
365 fieldKey &&
366 JSON.stringify( state.values[ fieldKey ] ) !==
367 JSON.stringify( state.savedValues[ fieldKey ] )
368 );
369 },
370 getContext: ( state ) => state.context,
371 getFieldGroups: ( state ) => state.fieldGroups,
372 };
373
374 const actions = {
375 initializeStore: ( storeData ) => ( {
376 type: 'INITIALIZE_STORE',
377 context: storeData.context,
378 fields: storeData.fields,
379 values: storeData.values,
380 fieldGroups: storeData.fieldGroups,
381 } ),
382 registerFieldGroup: ( storeData ) => ( {
383 type: 'REGISTER_FIELD_GROUP',
384 context: storeData.context,
385 fields: storeData.fields,
386 values: storeData.values,
387 fieldGroups: storeData.fieldGroups,
388 } ),
389 setFieldValue: ( fieldKey, value ) => ( {
390 type: 'SET_FIELD_VALUE',
391 fieldKey,
392 value,
393 } ),
394 setValues: ( values ) => ( { type: 'SET_VALUES', values } ),
395 setSubFieldValue: ( parentKey, ...pathAndValue ) => {
396 const value = pathAndValue.pop();
397 return {
398 type: 'SET_SUB_FIELD_VALUE',
399 parentKey,
400 path: pathAndValue,
401 value,
402 };
403 },
404 addRepeaterRow: ( fieldKey, rowData, index ) => ( {
405 type: 'ADD_REPEATER_ROW',
406 fieldKey,
407 rowData,
408 index,
409 } ),
410 removeRepeaterRow: ( fieldKey, rowIndex ) => ( {
411 type: 'REMOVE_REPEATER_ROW',
412 fieldKey,
413 rowIndex,
414 } ),
415 moveRepeaterRow: ( fieldKey, fromIndex, toIndex ) => ( {
416 type: 'MOVE_REPEATER_ROW',
417 fieldKey,
418 fromIndex,
419 toIndex,
420 } ),
421 markAsSaved: () => ( { type: 'MARK_AS_SAVED' } ),
422 setSyncing: ( isSyncing ) => ( { type: 'SET_SYNCING', isSyncing } ),
423 };
424
425 if ( window.wp?.data?.createReduxStore ) {
426 wp.data.register(
427 wp.data.createReduxStore( STORE_NAME, {
428 reducer,
429 selectors,
430 actions,
431 } )
432 );
433 }
434
435 const normalizeLookupValue = ( value ) => {
436 if (
437 null === value ||
438 undefined === value ||
439 '' === value ||
440 false === value
441 ) {
442 return [];
443 }
444
445 if ( Array.isArray( value ) ) {
446 return value.filter(
447 ( item ) => null !== item && undefined !== item && '' !== item
448 );
449 }
450
451 return [ value ];
452 };
453
454 const getMissingLookupValues = ( field, value ) => {
455 const fieldType = field.get( 'type' );
456 if ( ! AJAX_LOOKUP_FIELD_TYPES.has( fieldType ) ) {
457 return [];
458 }
459
460 const values = normalizeLookupValue( value );
461 if ( ! values.length ) {
462 return [];
463 }
464
465 if ( 'relationship' === fieldType ) {
466 const renderedIds = new Set();
467 field.$el.find( '.choices-list .acf-rel-item' ).each( function () {
468 renderedIds.add( String( jQuery( this ).data( 'id' ) ) );
469 } );
470
471 return values.filter(
472 ( item ) => ! renderedIds.has( String( item ) )
473 );
474 }
475
476 const $select = field.$el.find( 'select' ).first();
477 if ( ! $select.length ) {
478 return [];
479 }
480
481 const renderedIds = new Set();
482 $select.find( 'option' ).each( function () {
483 renderedIds.add( String( jQuery( this ).val() ) );
484 } );
485
486 return values.filter( ( item ) => ! renderedIds.has( String( item ) ) );
487 };
488
489 const appendLookupOptions = ( field, options ) => {
490 if ( ! options.length ) {
491 return;
492 }
493
494 if ( 'relationship' === field.get( 'type' ) ) {
495 const $choicesList = field.$el.find( '.choices-list' ).first();
496 if ( ! $choicesList.length ) {
497 return;
498 }
499
500 for ( const option of options ) {
501 const id = String( option.id );
502 const alreadyExists = $choicesList
503 .find( '.acf-rel-item' )
504 .toArray()
505 .some(
506 ( element ) =>
507 String( jQuery( element ).data( 'id' ) ) === id
508 );
509
510 if ( alreadyExists ) {
511 continue;
512 }
513
514 const $item = jQuery(
515 '<li><span tabindex="0" class="acf-rel-item acf-rel-item-add"></span></li>'
516 );
517 $item
518 .find( '.acf-rel-item' )
519 .attr( 'data-id', id )
520 .text( option.text );
521 $choicesList.append( $item );
522 }
523
524 return;
525 }
526
527 const $select = field.$el.find( 'select' ).first();
528 if ( ! $select.length ) {
529 return;
530 }
531
532 for ( const option of options ) {
533 const id = String( option.id );
534 const alreadyExists = $select
535 .find( 'option' )
536 .toArray()
537 .some( ( element ) => String( element.value ) === id );
538
539 if ( ! alreadyExists ) {
540 $select.append(
541 jQuery( '<option></option>' )
542 .attr( 'value', id )
543 .text( option.text )
544 );
545 }
546 }
547 };
548
549 const extractOptionFromAjaxResponse = ( response, requestedId ) => {
550 const results = response?.results;
551 if ( ! Array.isArray( results ) ) {
552 return null;
553 }
554
555 const requestedIdString = String( requestedId );
556 for ( const result of results ) {
557 if ( ! result || 'object' !== typeof result ) {
558 continue;
559 }
560
561 if ( Array.isArray( result.children ) ) {
562 for ( const child of result.children ) {
563 if ( ! child || 'object' !== typeof child ) {
564 continue;
565 }
566 if (
567 String( child.id ) === requestedIdString &&
568 'string' === typeof child.text
569 ) {
570 return { id: child.id, text: child.text };
571 }
572 }
573 } else if (
574 String( result.id ) === requestedIdString &&
575 'string' === typeof result.text
576 ) {
577 return { id: result.id, text: result.text };
578 }
579 }
580
581 return null;
582 };
583
584 const fetchLookupOption = async ( field, value ) => {
585 const fieldType = field.get( 'type' );
586 const fieldKey = field.get( 'key' );
587 const nonce = field.get( 'nonce' );
588
589 if ( ! fieldType || ! fieldKey || ! nonce || ! window.ajaxurl ) {
590 return null;
591 }
592
593 const body = new FormData();
594 body.append( 'action', `acf/fields/${ fieldType }/query` );
595 body.append( 'field_key', fieldKey );
596 body.append( 'nonce', nonce );
597 body.append( 'include', String( value ) );
598
599 try {
600 const response = await fetch( ajaxurl, {
601 method: 'POST',
602 credentials: 'same-origin',
603 body,
604 } );
605
606 if ( ! response.ok ) {
607 return null;
608 }
609
610 return extractOptionFromAjaxResponse(
611 await response.json(),
612 value
613 );
614 } catch ( error ) {
615 return null;
616 }
617 };
618
619 const readComplexValue = ( field ) => {
620 const fieldType = field.get( 'type' );
621
622 if ( 'group' === fieldType || 'clone' === fieldType ) {
623 return readGroupValue( field );
624 }
625 if ( 'repeater' === fieldType ) {
626 return readRepeaterValue( field );
627 }
628 if ( 'flexible_content' === fieldType ) {
629 return readFlexibleContentValue( field );
630 }
631
632 return undefined;
633 };
634
635 const readGroupValue = ( field ) => {
636 const value = {};
637 const childFields = acf.getFields( { parent: field.$el } );
638
639 for ( const childField of childFields ) {
640 const childKey = childField.get( 'key' );
641 const childType = childField.get( 'type' );
642 value[ childKey ] = isComplexFieldType( childType )
643 ? readComplexValue( childField )
644 : childField.val();
645 }
646
647 return value;
648 };
649
650 const readRepeaterValue = ( field ) => {
651 const value = [];
652
653 field.$el
654 .find(
655 '> .acf-input > .acf-repeater > table > tbody > tr.acf-row:not(.acf-clone)'
656 )
657 .each( function () {
658 const $row = jQuery( this );
659 const rowValue = {};
660 const childFields = acf.getFields( { parent: $row } );
661
662 for ( const childField of childFields ) {
663 const childKey = childField.get( 'key' );
664 const childType = childField.get( 'type' );
665 rowValue[ childKey ] = isComplexFieldType( childType )
666 ? readComplexValue( childField )
667 : childField.val();
668 }
669
670 value.push( rowValue );
671 } );
672
673 return value;
674 };
675
676 const readFlexibleContentValue = ( field ) => {
677 const value = [];
678
679 field.$el
680 .find(
681 '> .acf-input > .acf-flexible-content > .values > .layout:not(.acf-clone)'
682 )
683 .each( function () {
684 const $layout = jQuery( this );
685 const layoutValue = { acf_fc_layout: $layout.data( 'layout' ) };
686 const childFields = acf.getFields( { parent: $layout } );
687
688 for ( const childField of childFields ) {
689 const childKey = childField.get( 'key' );
690 const childType = childField.get( 'type' );
691 layoutValue[ childKey ] = isComplexFieldType( childType )
692 ? readComplexValue( childField )
693 : childField.val();
694 }
695
696 value.push( layoutValue );
697 } );
698
699 return value;
700 };
701
702 const removeLayoutOrRow = ( $element ) => {
703 acf.doAction( 'remove', $element );
704 $element.remove();
705 };
706
707 const writeFieldValue = ( field, value ) => {
708 const fieldType = field.get( 'type' );
709
710 if ( ! isComplexFieldType( fieldType ) ) {
711 field.val( value );
712 return;
713 }
714
715 if ( 'group' === fieldType || 'clone' === fieldType ) {
716 writeGroupValue( field, value );
717 } else if ( 'repeater' === fieldType ) {
718 writeRepeaterValue( field, value );
719 } else if ( 'flexible_content' === fieldType ) {
720 writeFlexibleContentValue( field, value );
721 }
722 };
723
724 const writeGroupValue = ( field, value ) => {
725 if ( value && 'object' === typeof value && ! Array.isArray( value ) ) {
726 writeChildValues( field.$el, value );
727 }
728 };
729
730 const writeRepeaterValue = ( field, value ) => {
731 if ( field.get( 'pagination' ) ) {
732 return;
733 }
734
735 const rows = Array.isArray( value ) ? value : [];
736 const getRows = () =>
737 field.$el.find(
738 '> .acf-input > .acf-repeater > table > tbody > tr.acf-row:not(.acf-clone)'
739 );
740 const currentRowCount = getRows().length;
741
742 for ( let index = currentRowCount - 1; index >= rows.length; index-- ) {
743 removeLayoutOrRow( getRows().eq( index ) );
744 }
745 for ( let index = currentRowCount; index < rows.length; index++ ) {
746 field.add();
747 }
748
749 getRows().each( function ( index ) {
750 const rowValue = rows[ index ];
751 if ( rowValue && 'object' === typeof rowValue ) {
752 writeChildValues( jQuery( this ), rowValue );
753 }
754 } );
755 };
756
757 const writeFlexibleContentValue = ( field, value ) => {
758 const layouts = Array.isArray( value ) ? value : [];
759 const $currentLayouts = field.$layouts();
760 const layoutShapeChanged =
761 $currentLayouts.length !== layouts.length ||
762 ! $currentLayouts.toArray().every( ( element, index ) => {
763 return (
764 jQuery( element ).data( 'layout' ) ===
765 layouts[ index ]?.acf_fc_layout
766 );
767 } );
768
769 if ( layoutShapeChanged ) {
770 $currentLayouts.each( function () {
771 removeLayoutOrRow( jQuery( this ) );
772 } );
773
774 for ( const layoutValue of layouts ) {
775 const layoutName = layoutValue?.acf_fc_layout;
776 if ( 'string' === typeof layoutName && layoutName ) {
777 field.add( { layout: layoutName } );
778 }
779 }
780 }
781
782 field.$layouts().each( function ( index ) {
783 const layoutValue = layouts[ index ];
784 if ( layoutValue && 'object' === typeof layoutValue ) {
785 writeChildValues( jQuery( this ), layoutValue );
786 }
787 } );
788 };
789
790 const writeChildValues = ( $parent, values ) => {
791 for ( const childField of acf.getFields( { parent: $parent } ) ) {
792 const childKey = childField.get( 'key' );
793 if ( childKey in values ) {
794 writeFieldValue( childField, values[ childKey ] );
795 }
796 }
797 };
798
799 if ( window.wp?.data?.select && window.wp?.data?.dispatch ) {
800 let isSyncingDomAndStore = false;
801 let previousStoreValues = null;
802
803 const getTopLevelFieldKey = ( field ) => {
804 const parents = field.parents();
805 return parents.length
806 ? parents[ parents.length - 1 ].get( 'key' )
807 : field.get( 'key' );
808 };
809
810 const writeStoreValueToField = ( fieldKey, value ) => {
811 const $field = acf.findField( fieldKey );
812 if ( ! $field.length ) {
813 return;
814 }
815
816 const field = acf.getField( $field );
817 if ( field ) {
818 writeFieldValue( field, value );
819 }
820 };
821
822 const fetchMissingLookupOptions = async ( changedKeys, values ) => {
823 const optionsByFieldKey = {};
824 const requests = [];
825
826 for ( const fieldKey of changedKeys ) {
827 const $field = acf.findField( fieldKey );
828 if ( ! $field.length ) {
829 continue;
830 }
831
832 const field = acf.getField( $field );
833 if (
834 ! field ||
835 ! AJAX_LOOKUP_FIELD_TYPES.has( field.get( 'type' ) )
836 ) {
837 continue;
838 }
839
840 const missingValues = getMissingLookupValues(
841 field,
842 values[ fieldKey ]
843 );
844 if ( ! missingValues.length ) {
845 continue;
846 }
847
848 optionsByFieldKey[ fieldKey ] = [];
849 for ( const missingValue of missingValues ) {
850 requests.push(
851 fetchLookupOption( field, missingValue ).then(
852 ( option ) => {
853 if ( option ) {
854 optionsByFieldKey[ fieldKey ].push(
855 option
856 );
857 }
858 }
859 )
860 );
861 }
862 }
863
864 await Promise.all( requests );
865 return optionsByFieldKey;
866 };
867
868 const syncDomFromStore = () => {
869 if ( isSyncingDomAndStore ) {
870 return;
871 }
872
873 const store = wp.data.select( STORE_NAME );
874 if ( ! store.isInitialized() ) {
875 return;
876 }
877
878 const values = store.getAllValues();
879 if ( values === previousStoreValues ) {
880 return;
881 }
882
883 const previousValues = previousStoreValues ?? {};
884 const changedKeys = Object.keys( values ).filter(
885 ( fieldKey ) =>
886 values[ fieldKey ] !== previousValues[ fieldKey ]
887 );
888 previousStoreValues = values;
889
890 if ( ! changedKeys.length ) {
891 return;
892 }
893
894 let needsLookupOptions = false;
895 for ( const fieldKey of changedKeys ) {
896 const $field = acf.findField( fieldKey );
897 if ( ! $field.length ) {
898 continue;
899 }
900
901 const field = acf.getField( $field );
902 if (
903 field &&
904 AJAX_LOOKUP_FIELD_TYPES.has( field.get( 'type' ) ) &&
905 getMissingLookupValues( field, values[ fieldKey ] ).length
906 ) {
907 needsLookupOptions = true;
908 break;
909 }
910 }
911
912 if ( needsLookupOptions ) {
913 isSyncingDomAndStore = true;
914 fetchMissingLookupOptions( changedKeys, values )
915 .then( ( optionsByFieldKey ) => {
916 try {
917 for ( const [ fieldKey, options ] of Object.entries(
918 optionsByFieldKey
919 ) ) {
920 const $field = acf.findField( fieldKey );
921 if ( ! $field.length ) {
922 continue;
923 }
924
925 const field = acf.getField( $field );
926 if ( field ) {
927 appendLookupOptions( field, options );
928 }
929 }
930
931 for ( const fieldKey of changedKeys ) {
932 writeStoreValueToField(
933 fieldKey,
934 values[ fieldKey ]
935 );
936 }
937 } finally {
938 isSyncingDomAndStore = false;
939 previousStoreValues = values;
940
941 if (
942 wp.data.select( STORE_NAME ).getAllValues() !==
943 values
944 ) {
945 queueMicrotask( syncDomFromStore );
946 }
947 }
948 } )
949 .catch( () => {
950 isSyncingDomAndStore = false;
951 } );
952 return;
953 }
954
955 isSyncingDomAndStore = true;
956 try {
957 for ( const fieldKey of changedKeys ) {
958 writeStoreValueToField( fieldKey, values[ fieldKey ] );
959 }
960 } finally {
961 isSyncingDomAndStore = false;
962 }
963 };
964
965 new acf.Model( {
966 id: 'datastoreSync',
967 wait: 'prepare',
968
969 initialize() {
970 if ( ! acf.isGutenbergPostEditor() ) {
971 return;
972 }
973
974 this.initializeStore();
975 this.subscribeToStore();
976 this.listenToDOM();
977 this.setupConvenienceAPI();
978 },
979
980 initializeStore() {
981 const storeData = acf.get( 'storeData' );
982 if ( ! storeData ) {
983 return;
984 }
985
986 wp.data.dispatch( STORE_NAME ).initializeStore( storeData );
987 this.reconcileWithDOM();
988 previousStoreValues = wp.data
989 .select( STORE_NAME )
990 .getAllValues();
991 },
992
993 reconcileWithDOM() {
994 const fields = acf.getFields();
995
996 if ( ! fields?.length ) {
997 return;
998 }
999
1000 const store = wp.data.select( STORE_NAME );
1001 const values = {};
1002 for ( const field of fields ) {
1003 const fieldKey = field.get( 'key' );
1004 if ( ! fieldKey || ! store.getField( fieldKey ) ) {
1005 continue;
1006 }
1007
1008 const storeValue = store.getFieldValue( fieldKey );
1009 if ( undefined === storeValue ) {
1010 continue;
1011 }
1012
1013 const fieldType = field.get( 'type' );
1014 const domValue = isComplexFieldType( fieldType )
1015 ? readComplexValue( field )
1016 : field.val();
1017
1018 if (
1019 JSON.stringify( domValue ) !==
1020 JSON.stringify( storeValue )
1021 ) {
1022 values[ fieldKey ] = domValue;
1023 }
1024 }
1025
1026 if ( Object.keys( values ).length ) {
1027 wp.data.dispatch( STORE_NAME ).setValues( values );
1028 }
1029 },
1030
1031 subscribeToStore() {
1032 this.storeUnsubscribe = wp.data.subscribe( syncDomFromStore );
1033 },
1034
1035 listenToDOM() {
1036 acf.addAction( 'change_field', this.onFieldChange, 10, this );
1037 acf.addAction(
1038 'append_field',
1039 this.onFieldStructureChange,
1040 10,
1041 this
1042 );
1043 acf.addAction(
1044 'remove_field',
1045 this.onFieldStructureChange,
1046 10,
1047 this
1048 );
1049 acf.addAction(
1050 'sortstop_field',
1051 this.onFieldStructureChange,
1052 10,
1053 this
1054 );
1055 acf.addAction(
1056 'refresh_post_screen',
1057 this.onRefreshPostScreen,
1058 10,
1059 this
1060 );
1061 },
1062
1063 onFieldChange( field ) {
1064 if ( isSyncingDomAndStore ) {
1065 return;
1066 }
1067 if ( ! wp.data.select( STORE_NAME ).isInitialized() ) {
1068 return;
1069 }
1070
1071 const fieldKey = field.get( 'key' );
1072 if ( ! fieldKey ) {
1073 return;
1074 }
1075
1076 isSyncingDomAndStore = true;
1077 try {
1078 const parentField = field.parent();
1079 if (
1080 parentField &&
1081 isComplexFieldType( parentField.get( 'type' ) )
1082 ) {
1083 const topLevelKey = getTopLevelFieldKey( field );
1084 if ( topLevelKey ) {
1085 const $topLevelField = acf.findField( topLevelKey );
1086 if ( $topLevelField.length ) {
1087 const topLevelField =
1088 acf.getField( $topLevelField );
1089 if ( topLevelField ) {
1090 wp.data
1091 .dispatch( STORE_NAME )
1092 .setFieldValue(
1093 topLevelKey,
1094 readComplexValue( topLevelField )
1095 );
1096 }
1097 }
1098 }
1099 } else if ( isComplexFieldType( field.get( 'type' ) ) ) {
1100 wp.data
1101 .dispatch( STORE_NAME )
1102 .setFieldValue(
1103 fieldKey,
1104 readComplexValue( field )
1105 );
1106 } else {
1107 wp.data
1108 .dispatch( STORE_NAME )
1109 .setFieldValue( fieldKey, field.val() );
1110 }
1111 } finally {
1112 isSyncingDomAndStore = false;
1113 previousStoreValues = wp.data
1114 .select( STORE_NAME )
1115 .getAllValues();
1116 }
1117 },
1118
1119 onFieldStructureChange( field ) {
1120 if ( isSyncingDomAndStore ) {
1121 return;
1122 }
1123 if ( ! wp.data.select( STORE_NAME ).isInitialized() ) {
1124 return;
1125 }
1126
1127 const topLevelKey = getTopLevelFieldKey( field );
1128 if ( ! topLevelKey ) {
1129 return;
1130 }
1131
1132 const $topLevelField = acf.findField( topLevelKey );
1133 if ( ! $topLevelField.length ) {
1134 return;
1135 }
1136
1137 const topLevelField = acf.getField( $topLevelField );
1138 if (
1139 ! topLevelField ||
1140 ! isComplexFieldType( topLevelField.get( 'type' ) )
1141 ) {
1142 return;
1143 }
1144
1145 isSyncingDomAndStore = true;
1146 try {
1147 wp.data
1148 .dispatch( STORE_NAME )
1149 .setFieldValue(
1150 topLevelKey,
1151 readComplexValue( topLevelField )
1152 );
1153 } finally {
1154 isSyncingDomAndStore = false;
1155 previousStoreValues = wp.data
1156 .select( STORE_NAME )
1157 .getAllValues();
1158 }
1159 },
1160
1161 onRefreshPostScreen( response ) {
1162 if ( ! response?.storeData ) {
1163 return;
1164 }
1165
1166 isSyncingDomAndStore = true;
1167 try {
1168 wp.data
1169 .dispatch( STORE_NAME )
1170 .registerFieldGroup( response.storeData );
1171 this.reconcileWithDOM();
1172 } finally {
1173 isSyncingDomAndStore = false;
1174 previousStoreValues = wp.data
1175 .select( STORE_NAME )
1176 .getAllValues();
1177 }
1178 },
1179
1180 setupConvenienceAPI() {
1181 acf.store = {
1182 get( fieldKey ) {
1183 if ( wp.data.select( STORE_NAME ).isInitialized() ) {
1184 return wp.data
1185 .select( STORE_NAME )
1186 .getFieldValue( fieldKey );
1187 }
1188
1189 return undefined;
1190 },
1191 set( fieldKey, value ) {
1192 if ( wp.data.select( STORE_NAME ).isInitialized() ) {
1193 wp.data
1194 .dispatch( STORE_NAME )
1195 .setFieldValue( fieldKey, value );
1196 }
1197 },
1198 subscribe( fieldKey, callback ) {
1199 let previousValue = wp.data
1200 .select( STORE_NAME )
1201 .getFieldValue( fieldKey );
1202
1203 return wp.data.subscribe( () => {
1204 const nextValue = wp.data
1205 .select( STORE_NAME )
1206 .getFieldValue( fieldKey );
1207 if ( nextValue !== previousValue ) {
1208 const oldValue = previousValue;
1209 previousValue = nextValue;
1210 callback( nextValue, oldValue );
1211 }
1212 } );
1213 },
1214 };
1215 },
1216 } );
1217 }
1218
1219 const collectPaginatedRepeaterRowsForSave = ( fieldKey ) => {
1220 const $field = acf.findField( fieldKey );
1221 if ( ! $field.length ) {
1222 return {};
1223 }
1224
1225 const $rows = $field.find(
1226 '> .acf-input > .acf-repeater > table > tbody > tr.acf-row:not(.acf-clone)'
1227 );
1228 const rowsById = {};
1229
1230 $rows.each( function () {
1231 const $row = jQuery( this );
1232 const rowId = $row.data( 'id' );
1233 if ( ! rowId ) {
1234 return;
1235 }
1236
1237 const rowValue = {};
1238 for ( const childField of acf.getFields( { parent: $row } ) ) {
1239 const childKey = childField.get( 'key' );
1240 const childType = childField.get( 'type' );
1241 rowValue[ childKey ] = isComplexFieldType( childType )
1242 ? readComplexValue( childField )
1243 : childField.val();
1244 }
1245
1246 $row.find( 'input.acf-row-status' ).each( function () {
1247 const matches = ( jQuery( this ).attr( 'name' ) ?? '' ).match(
1248 REPEATER_ROW_STATUS_NAME_PATTERN
1249 );
1250 if ( matches ) {
1251 rowValue[ `acf_${ matches[ 1 ] }` ] = jQuery( this ).val();
1252 }
1253 } );
1254
1255 rowsById[ rowId ] = rowValue;
1256 } );
1257
1258 return rowsById;
1259 };
1260
1261 let lastPostedAcfJson = null;
1262 let lastObservedAcfJson = null;
1263 let isApplyingEditorMetaToStore = false;
1264
1265 acf.gutenbergEditPost = function () {
1266 if ( ! acf.isGutenbergPostEditor() ) {
1267 return;
1268 }
1269
1270 const store = wp.data.select( STORE_NAME );
1271 if ( ! store || ! store.isInitialized() ) {
1272 return;
1273 }
1274
1275 const values = { ...store.getAllValues() };
1276 for ( const fieldKey of Object.keys( values ) ) {
1277 const field = store.getField( fieldKey );
1278 if ( 'repeater' === field?.type && field.pagination ) {
1279 values[ fieldKey ] =
1280 collectPaginatedRepeaterRowsForSave( fieldKey );
1281 }
1282 }
1283
1284 const acfJson = JSON.stringify( values );
1285 lastPostedAcfJson = acfJson;
1286 lastObservedAcfJson = acfJson;
1287 wp.data
1288 .dispatch( 'core/editor' )
1289 .editPost( { meta: { _acf: acfJson } } );
1290 };
1291
1292 if ( window.wp?.data?.subscribe ) {
1293 wp.data.subscribe( () => {
1294 if ( isApplyingEditorMetaToStore ) {
1295 return;
1296 }
1297
1298 const store = wp.data.select( STORE_NAME );
1299 if ( ! store?.isInitialized() ) {
1300 return;
1301 }
1302
1303 const editor = wp.data.select( 'core/editor' );
1304 if ( ! editor?.getEditedPostAttribute ) {
1305 return;
1306 }
1307
1308 const meta = editor.getEditedPostAttribute( 'meta' );
1309 const acfJson =
1310 meta && 'string' === typeof meta._acf ? meta._acf : null;
1311 if ( ! acfJson || acfJson === lastObservedAcfJson ) {
1312 return;
1313 }
1314 if ( acfJson === lastPostedAcfJson ) {
1315 lastObservedAcfJson = acfJson;
1316 return;
1317 }
1318
1319 let values;
1320 try {
1321 values = JSON.parse( acfJson );
1322 } catch ( error ) {
1323 lastObservedAcfJson = acfJson;
1324 return;
1325 }
1326
1327 if (
1328 values &&
1329 'object' === typeof values &&
1330 ! Array.isArray( values )
1331 ) {
1332 lastObservedAcfJson = acfJson;
1333 isApplyingEditorMetaToStore = true;
1334 try {
1335 wp.data.dispatch( STORE_NAME ).setValues( values );
1336 } finally {
1337 isApplyingEditorMetaToStore = false;
1338 }
1339 } else {
1340 lastObservedAcfJson = acfJson;
1341 }
1342 } );
1343 }
1344 } )();
1345