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-blocks.js
secure-custom-fields / assets / src / js / pro Last commit date
blocks-v3 2 weeks ago _acf-blocks-v3.js 2 months ago _acf-blocks.js 2 months ago _acf-field-flexible-content.js 2 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-blocks.js
2145 lines
1 const md5 = require( 'md5' );
2
3 ( ( $, undefined ) => {
4 // Dependencies.
5 const {
6 BlockControls,
7 InspectorControls,
8 InnerBlocks,
9 useBlockProps,
10 AlignmentToolbar,
11 BlockVerticalAlignmentToolbar,
12 } = wp.blockEditor;
13
14 const { ToolbarGroup, ToolbarButton, Placeholder, Spinner } = wp.components;
15 const { Fragment } = wp.element;
16 const { Component } = React;
17 const { useSelect } = wp.data;
18 const { createHigherOrderComponent } = wp.compose;
19
20 // Potentially experimental dependencies.
21 const BlockAlignmentMatrixToolbar =
22 wp.blockEditor.__experimentalBlockAlignmentMatrixToolbar ||
23 wp.blockEditor.BlockAlignmentMatrixToolbar;
24 // Gutenberg v10.x begins transition from Toolbar components to Control components.
25 const BlockAlignmentMatrixControl =
26 wp.blockEditor.__experimentalBlockAlignmentMatrixControl ||
27 wp.blockEditor.BlockAlignmentMatrixControl;
28 const BlockFullHeightAlignmentControl =
29 wp.blockEditor.__experimentalBlockFullHeightAligmentControl ||
30 wp.blockEditor.__experimentalBlockFullHeightAlignmentControl ||
31 wp.blockEditor.BlockFullHeightAlignmentControl;
32 const useInnerBlocksProps =
33 wp.blockEditor.__experimentalUseInnerBlocksProps ||
34 wp.blockEditor.useInnerBlocksProps;
35
36 /**
37 * Storage for registered block types.
38 *
39 * @since ACF 5.8.0
40 * @var object
41 */
42 const blockTypes = {};
43
44 /**
45 * Data storage for Block Instances and their DynamicHTML components.
46 * This is temporarily stored on the ACF object, but this will be replaced later.
47 * Developers should not rely on reading or using any aspect of acf.blockInstances.
48 *
49 * @since ACF 6.3
50 */
51 acf.blockInstances = {};
52
53 /**
54 * Returns a block type for the given name.
55 *
56 * @date 20/2/19
57 * @since ACF 5.8.0
58 *
59 * @param string name The block name.
60 * @return (object|false)
61 */
62 function getBlockType( name ) {
63 return blockTypes[ name ] || false;
64 }
65
66 /**
67 * Returns a block version for a given block name
68 *
69 * @date 8/6/22
70 * @since ACF 6.0
71 *
72 * @param string name The block name
73 * @return int
74 */
75 function getBlockVersion( name ) {
76 const blockType = getBlockType( name );
77 return blockType.acf_block_version || 1;
78 }
79
80 /**
81 * Returns a block's validate property. Default true.
82 *
83 * @since ACF 6.3
84 *
85 * @param string name The block name
86 * @return boolean
87 */
88 function blockSupportsValidation( name ) {
89 const blockType = getBlockType( name );
90 return blockType.validate;
91 }
92
93 /**
94 * Returns true if a block (identified by client ID) is nested in a query loop block.
95 *
96 * @date 17/1/22
97 * @since ACF 5.12
98 *
99 * @param {string} clientId A block client ID
100 * @return boolean
101 */
102 function isBlockInQueryLoop( clientId ) {
103 const parents = wp.data
104 .select( 'core/block-editor' )
105 .getBlockParents( clientId );
106 const parentsData = wp.data
107 .select( 'core/block-editor' )
108 .getBlocksByClientId( parents );
109 return parentsData.filter( ( block ) => block.name === 'core/query' )
110 .length;
111 }
112
113 /**
114 * Returns true if we're currently inside the WP 5.9+ site editor.
115 *
116 * @date 08/02/22
117 * @since ACF 5.12
118 *
119 * @return boolean
120 */
121 function isSiteEditor() {
122 return (
123 document.querySelectorAll( 'iframe[name="editor-canvas"]' ).length >
124 0
125 );
126 }
127
128 /**
129 * Returns true if the block editor is currently showing the desktop device type preview.
130 *
131 * This function will always return true in the site editor as it uses the
132 * edit-post store rather than the edit-site store.
133 *
134 * @date 15/02/22
135 * @since ACF 5.12
136 *
137 * @return boolean
138 */
139 function isDesktopPreviewDeviceType() {
140 const editPostStore = select( 'core/edit-post' );
141
142 // Return true if the edit post store isn't available (such as in the widget editor)
143 if ( ! editPostStore ) return true;
144
145 // Check if function exists (experimental or not) and return true if it's Desktop, or doesn't exist.
146 if ( editPostStore.__experimentalGetPreviewDeviceType ) {
147 return (
148 'Desktop' === editPostStore.__experimentalGetPreviewDeviceType()
149 );
150 } else if ( editPostStore.getPreviewDeviceType ) {
151 return 'Desktop' === editPostStore.getPreviewDeviceType();
152 } else {
153 return true;
154 }
155 }
156
157 /**
158 * Returns true if the block editor is currently in template edit mode.
159 *
160 * @date 16/02/22
161 * @since ACF 5.12
162 *
163 * @return boolean
164 */
165 function isEditingTemplate() {
166 const editPostStore = select( 'core/edit-post' );
167
168 // Return false if the edit post store isn't available (such as in the widget editor)
169 if ( ! editPostStore ) return false;
170
171 // Return false if the function doesn't exist
172 if ( ! editPostStore.isEditingTemplate ) return false;
173
174 return editPostStore.isEditingTemplate();
175 }
176
177 /**
178 * Returns true if we're currently inside an iFramed non-desktop device preview type (WP5.9+)
179 *
180 * @date 15/02/22
181 * @since ACF 5.12
182 *
183 * @return boolean
184 */
185 function isiFramedMobileDevicePreview() {
186 return (
187 $( 'iframe[name=editor-canvas]' ).length &&
188 ! isDesktopPreviewDeviceType()
189 );
190 }
191
192 /**
193 * Registers a block type.
194 *
195 * @date 19/2/19
196 * @since ACF 5.8.0
197 *
198 * @param object blockType The block type settings localized from PHP.
199 * @return object The result from wp.blocks.registerBlockType().
200 */
201 function registerBlockType( blockType ) {
202 // Bail early if is excluded post_type.
203 const allowedTypes = blockType.post_types || [];
204 if ( allowedTypes.length ) {
205 // Always allow block to appear on "Edit reusable Block" screen.
206 allowedTypes.push( 'wp_block' );
207
208 // Check post type.
209 const postType = acf.get( 'postType' );
210 if ( ! allowedTypes.includes( postType ) ) {
211 return false;
212 }
213 }
214
215 // Handle svg HTML.
216 if (
217 typeof blockType.icon === 'string' &&
218 blockType.icon.substr( 0, 4 ) === '<svg'
219 ) {
220 const iconHTML = blockType.icon;
221 blockType.icon = <Div>{ iconHTML }</Div>;
222 }
223
224 // Remove icon if empty to allow for default "block".
225 // Avoids JS error preventing block from being registered.
226 if ( ! blockType.icon ) {
227 delete blockType.icon;
228 }
229
230 // Check category exists and fallback to "common".
231 const category = wp.blocks
232 .getCategories()
233 .filter( ( { slug } ) => slug === blockType.category )
234 .pop();
235 if ( ! category ) {
236 //console.warn( `The block "${blockType.name}" is registered with an unknown category "${blockType.category}".` );
237 blockType.category = 'common';
238 }
239
240 // Merge in block settings before local additions.
241 blockType = acf.parseArgs( blockType, {
242 title: '',
243 name: '',
244 category: '',
245 api_version: 2,
246 acf_block_version: 1,
247 } );
248
249 // Remove all empty attribute defaults from PHP values to allow serialisation.
250 // https://github.com/WordPress/gutenberg/issues/7342
251 for ( const key in blockType.attributes ) {
252 if (
253 'default' in blockType.attributes[ key ] &&
254 blockType.attributes[ key ].default.length === 0
255 ) {
256 delete blockType.attributes[ key ].default;
257 }
258 }
259
260 // Apply anchor supports to avoid block editor default writing to ID.
261 if ( blockType.supports.anchor ) {
262 blockType.attributes.anchor = {
263 type: 'string',
264 };
265 }
266
267 // Append edit and save functions.
268 let ThisBlockEdit = BlockEdit;
269 let ThisBlockSave = BlockSave;
270
271 // Apply alignText functionality.
272 if ( blockType.supports.alignText || blockType.supports.align_text ) {
273 blockType.attributes = addBackCompatAttribute(
274 blockType.attributes,
275 'align_text',
276 'string'
277 );
278 ThisBlockEdit = withAlignTextComponent( ThisBlockEdit, blockType );
279 }
280
281 // Apply alignContent functionality.
282 if (
283 blockType.supports.alignContent ||
284 blockType.supports.align_content
285 ) {
286 blockType.attributes = addBackCompatAttribute(
287 blockType.attributes,
288 'align_content',
289 'string'
290 );
291 ThisBlockEdit = withAlignContentComponent(
292 ThisBlockEdit,
293 blockType
294 );
295 }
296
297 // Apply fullHeight functionality.
298 if ( blockType.supports.fullHeight || blockType.supports.full_height ) {
299 blockType.attributes = addBackCompatAttribute(
300 blockType.attributes,
301 'full_height',
302 'boolean'
303 );
304 ThisBlockEdit = withFullHeightComponent(
305 ThisBlockEdit,
306 blockType.blockType
307 );
308 }
309
310 // Set edit and save functions.
311 blockType.edit = ( props ) => {
312 // Ensure we remove our save lock if a block is removed.
313 wp.element.useEffect( () => {
314 return () => {
315 if ( ! wp.data.dispatch( 'core/editor' ) ) return;
316 wp.data
317 .dispatch( 'core/editor' )
318 .unlockPostSaving( 'acf/block/' + props.clientId );
319 };
320 }, [] );
321
322 return <ThisBlockEdit { ...props } />;
323 };
324 blockType.save = () => <ThisBlockSave />;
325
326 // Add to storage.
327 blockTypes[ blockType.name ] = blockType;
328
329 // Register with WP.
330 const result = wp.blocks.registerBlockType( blockType.name, blockType );
331
332 // Fix bug in 'core/anchor/attribute' filter overwriting attribute.
333 // Required for < WP5.9
334 // See https://github.com/WordPress/gutenberg/issues/15240
335 if ( result.attributes.anchor ) {
336 result.attributes.anchor = {
337 type: 'string',
338 };
339 }
340
341 // Return result.
342 return result;
343 }
344
345 /**
346 * Returns the wp.data.select() response with backwards compatibility.
347 *
348 * @date 17/06/2020
349 * @since ACF 5.9.0
350 *
351 * @param string selector The selector name.
352 * @return mixed
353 */
354 function select( selector ) {
355 if ( selector === 'core/block-editor' ) {
356 return (
357 wp.data.select( 'core/block-editor' ) ||
358 wp.data.select( 'core/editor' )
359 );
360 }
361 return wp.data.select( selector );
362 }
363
364 /**
365 * Returns the wp.data.dispatch() response with backwards compatibility.
366 *
367 * @date 17/06/2020
368 * @since ACF 5.9.0
369 *
370 * @param string selector The selector name.
371 * @return mixed
372 */
373 function dispatch( selector ) {
374 return wp.data.dispatch( selector );
375 }
376
377 /**
378 * Returns an array of all blocks for the given args.
379 *
380 * @date 27/2/19
381 * @since ACF 5.7.13
382 *
383 * @param {object} args An object of key=>value pairs used to filter results.
384 * @return array.
385 */
386 function getBlocks( args ) {
387 let blocks = [];
388
389 // Local function to recurse through all child blocks and add to the blocks array.
390 const recurseBlocks = ( block ) => {
391 blocks.push( block );
392 select( 'core/block-editor' )
393 .getBlocks( block.clientId )
394 .forEach( recurseBlocks );
395 };
396
397 // Trigger initial recursion for parent level blocks.
398 select( 'core/block-editor' ).getBlocks().forEach( recurseBlocks );
399
400 // Loop over args and filter.
401 for ( const k in args ) {
402 blocks = blocks.filter(
403 ( { attributes } ) => attributes[ k ] === args[ k ]
404 );
405 }
406
407 // Return results.
408 return blocks;
409 }
410
411 /**
412 * Storage for the AJAX queue.
413 *
414 * @const {array}
415 */
416 const ajaxQueue = {};
417
418 /**
419 * Storage for cached AJAX requests for block content.
420 *
421 * @since ACF 5.12
422 * @const {array}
423 */
424 const fetchCache = {};
425
426 /**
427 * Fetches a JSON result from the AJAX API.
428 *
429 * @date 28/2/19
430 * @since ACF 5.7.13
431 *
432 * @param object block The block props.
433 * @query object The query args used in AJAX callback.
434 * @return object The AJAX promise.
435 */
436 function fetchBlock( args ) {
437 const {
438 attributes = {},
439 context = {},
440 query = {},
441 clientId = null,
442 delay = 0,
443 } = args;
444
445 // Build a unique queue ID from block data, including the clientId for edit forms.
446 const queueId = md5(
447 JSON.stringify( { ...attributes, ...context, ...query } )
448 );
449
450 const data = ajaxQueue[ queueId ] || {
451 query: {},
452 timeout: false,
453 promise: $.Deferred(),
454 started: false,
455 };
456
457 // Append query args to storage.
458 data.query = { ...data.query, ...query };
459
460 if ( data.started ) return data.promise;
461
462 // Set fresh timeout.
463 clearTimeout( data.timeout );
464 data.timeout = setTimeout( () => {
465 data.started = true;
466 if ( fetchCache[ queueId ] ) {
467 ajaxQueue[ queueId ] = null;
468 data.promise.resolve.apply(
469 fetchCache[ queueId ][ 0 ],
470 fetchCache[ queueId ][ 1 ]
471 );
472 } else {
473 $.ajax( {
474 url: acf.get( 'ajaxurl' ),
475 dataType: 'json',
476 type: 'post',
477 cache: false,
478 data: acf.prepareForAjax( {
479 action: 'acf/ajax/fetch-block',
480 block: JSON.stringify( attributes ),
481 clientId: clientId,
482 context: JSON.stringify( context ),
483 query: data.query,
484 } ),
485 } )
486 .always( () => {
487 // Clean up queue after AJAX request is complete.
488 ajaxQueue[ queueId ] = null;
489 } )
490 .done( function () {
491 fetchCache[ queueId ] = [ this, arguments ];
492 data.promise.resolve.apply( this, arguments );
493 } )
494 .fail( function () {
495 data.promise.reject.apply( this, arguments );
496 } );
497 }
498 }, delay );
499
500 // Update storage.
501 ajaxQueue[ queueId ] = data;
502
503 // Return promise.
504 return data.promise;
505 }
506
507 /**
508 * Returns true if both object are the same.
509 *
510 * @date 19/05/2020
511 * @since ACF 5.9.0
512 *
513 * @param object obj1
514 * @param object obj2
515 * @return bool
516 */
517 function compareObjects( obj1, obj2 ) {
518 return JSON.stringify( obj1 ) === JSON.stringify( obj2 );
519 }
520
521 /**
522 * Converts HTML into a React element.
523 *
524 * @date 19/05/2020
525 * @since ACF 5.9.0
526 *
527 * @param string html The HTML to convert.
528 * @param int acfBlockVersion The ACF block version number.
529 * @return object Result of React.createElement().
530 */
531 acf.parseJSX = ( html, acfBlockVersion ) => {
532 // Apply a temporary wrapper for the jQuery parse to prevent text nodes triggering errors.
533 html = '<div>' + html + '</div>';
534 // Correctly balance InnerBlocks tags for jQuery's initial parse.
535 html = html.replace(
536 /<InnerBlocks([^>]+)?\/>/,
537 '<InnerBlocks$1></InnerBlocks>'
538 );
539 return parseNode( $( html )[ 0 ], acfBlockVersion, 0 ).props.children;
540 };
541
542 /**
543 * Converts a DOM node into a React element.
544 *
545 * @date 19/05/2020
546 * @since ACF 5.9.0
547 *
548 * @param DOM node The DOM node.
549 * @param int acfBlockVersion The ACF block version number.
550 * @param int level The recursion level.
551 * @return object Result of React.createElement().
552 */
553 function parseNode( node, acfBlockVersion, level = 0 ) {
554 // Get node name.
555 const nodeName = parseNodeName(
556 node.nodeName.toLowerCase(),
557 acfBlockVersion
558 );
559 if ( ! nodeName ) {
560 return null;
561 }
562
563 // Get node attributes in React friendly format.
564 const nodeAttrs = {};
565
566 if ( level === 1 && nodeName !== 'ACFInnerBlocks' ) {
567 // Top level (after stripping away the container div), create a ref for passing through to ACF's JS API.
568 nodeAttrs.ref = React.createRef();
569 }
570
571 acf.arrayArgs( node.attributes )
572 .map( parseNodeAttr )
573 .forEach( ( { name, value } ) => {
574 nodeAttrs[ name ] = value;
575 } );
576
577 if ( 'ACFInnerBlocks' === nodeName ) {
578 return <ACFInnerBlocks { ...nodeAttrs } />;
579 }
580
581 // Define args for React.createElement().
582 const args = [ nodeName, nodeAttrs ];
583 acf.arrayArgs( node.childNodes ).forEach( ( child ) => {
584 if ( child instanceof Text ) {
585 const text = child.textContent;
586 if ( text ) {
587 args.push( text );
588 }
589 } else {
590 args.push( parseNode( child, acfBlockVersion, level + 1 ) );
591 }
592 } );
593
594 // Return element.
595 return React.createElement.apply( this, args );
596 }
597
598 /**
599 * Converts a node or attribute name into it's JSX compliant name
600 *
601 * @date 05/07/2021
602 * @since ACF 5.9.8
603 *
604 * @param string name The node or attribute name.
605 * @return string
606 */
607 function getJSXName( name ) {
608 const replacement = acf.isget( acf, 'jsxNameReplacements', name );
609 if ( replacement ) return replacement;
610 return name;
611 }
612
613 /**
614 * A react Component for inline scripts.
615 *
616 * This Component uses a combination of React references and jQuery to append the
617 * inline <script> HTML each time the component is rendered.
618 *
619 * @date 29/05/2020
620 * @since ACF 5.9.0
621 *
622 * @param type Var Description.
623 * @return type Description.
624 */
625 class Script extends Component {
626 render() {
627 return <div ref={ ( el ) => ( this.el = el ) } />;
628 }
629 setHTML( html ) {
630 $( this.el ).html( `<script>${ html }</script>` );
631 }
632 componentDidUpdate() {
633 this.setHTML( this.props.children );
634 }
635 componentDidMount() {
636 this.setHTML( this.props.children );
637 }
638 }
639
640 /**
641 * Converts the given name into a React friendly name or component.
642 *
643 * @date 19/05/2020
644 * @since ACF 5.9.0
645 *
646 * @param string name The node name in lowercase.
647 * @param int acfBlockVersion The ACF block version number.
648 * @return mixed
649 */
650 function parseNodeName( name, acfBlockVersion ) {
651 switch ( name ) {
652 case 'innerblocks':
653 if ( acfBlockVersion < 2 ) {
654 return InnerBlocks;
655 }
656 return 'ACFInnerBlocks';
657 case 'script':
658 return Script;
659 case '#comment':
660 return null;
661 default:
662 // Replace names for JSX counterparts.
663 name = getJSXName( name );
664 }
665 return name;
666 }
667
668 /**
669 * Functional component for ACFInnerBlocks.
670 *
671 * @since ACF 6.0.0
672 *
673 * @param obj props element properties.
674 * @return DOM element
675 */
676 function ACFInnerBlocks( props ) {
677 const { className = 'acf-innerblocks-container' } = props;
678 const innerBlockProps = useInnerBlocksProps(
679 { className: className },
680 props
681 );
682
683 return <div { ...innerBlockProps }>{ innerBlockProps.children }</div>;
684 }
685
686 /**
687 * Converts the given attribute into a React friendly name and value object.
688 *
689 * @date 19/05/2020
690 * @since ACF 5.9.0
691 *
692 * @param obj nodeAttr The node attribute.
693 * @return obj
694 */
695 function parseNodeAttr( nodeAttr ) {
696 let name = nodeAttr.name;
697 let value = nodeAttr.value;
698
699 // Allow overrides for third party libraries who might use specific attributes.
700 let shortcut = acf.applyFilters(
701 'acf_blocks_parse_node_attr',
702 false,
703 nodeAttr
704 );
705
706 if ( shortcut ) return shortcut;
707
708 switch ( name ) {
709 // Class.
710 case 'class':
711 name = 'className';
712 break;
713
714 // Style.
715 case 'style':
716 const css = {};
717 value.split( ';' ).forEach( ( s ) => {
718 const pos = s.indexOf( ':' );
719 if ( pos > 0 ) {
720 let ruleName = s.substr( 0, pos ).trim();
721 const ruleValue = s.substr( pos + 1 ).trim();
722
723 // Rename core properties, but not CSS variables.
724 if ( ruleName.charAt( 0 ) !== '-' ) {
725 ruleName = acf.strCamelCase( ruleName );
726 }
727 css[ ruleName ] = ruleValue;
728 }
729 } );
730 value = css;
731 break;
732
733 // Default.
734 default:
735 // No formatting needed for "data-x" attributes.
736 if ( name.indexOf( 'data-' ) === 0 ) {
737 break;
738 }
739
740 // Replace names for JSX counterparts.
741 name = getJSXName( name );
742
743 // Convert JSON values.
744 const c1 = value.charAt( 0 );
745 if ( c1 === '[' || c1 === '{' ) {
746 value = JSON.parse( value );
747 }
748
749 // Convert bool values.
750 if ( value === 'true' || value === 'false' ) {
751 value = value === 'true';
752 }
753 break;
754 }
755 return {
756 name,
757 value,
758 };
759 }
760
761 /**
762 * Higher Order Component used to set default block attribute values.
763 *
764 * By modifying block attributes directly, instead of defining defaults in registerBlockType(),
765 * WordPress will include them always within the saved block serialized JSON.
766 *
767 * @date 31/07/2020
768 * @since ACF 5.9.0
769 *
770 * @param Component BlockListBlock The BlockListBlock Component.
771 * @return Component
772 */
773 const withDefaultAttributes = createHigherOrderComponent(
774 ( BlockListBlock ) =>
775 class WrappedBlockEdit extends Component {
776 constructor( props ) {
777 super( props );
778
779 // Extract vars.
780 const { name, attributes } = this.props;
781
782 // Only run on ACF Blocks.
783 const blockType = getBlockType( name );
784 if ( ! blockType ) {
785 return;
786 }
787
788 // Check and remove any empty string attributes to match PHP behaviour.
789 Object.keys( attributes ).forEach( ( key ) => {
790 if ( attributes[ key ] === '' ) {
791 delete attributes[ key ];
792 }
793 } );
794
795 // Backward compatibility attribute replacement.
796 const upgrades = {
797 full_height: 'fullHeight',
798 align_content: 'alignContent',
799 align_text: 'alignText',
800 };
801
802 Object.keys( upgrades ).forEach( ( key ) => {
803 if ( attributes[ key ] !== undefined ) {
804 attributes[ upgrades[ key ] ] = attributes[ key ];
805 } else if (
806 attributes[ upgrades[ key ] ] === undefined
807 ) {
808 //Check for a default
809 if ( blockType[ key ] !== undefined ) {
810 attributes[ upgrades[ key ] ] =
811 blockType[ key ];
812 }
813 }
814 delete blockType[ key ];
815 delete attributes[ key ];
816 } );
817
818 // Set default attributes for those undefined.
819 for ( let attribute in blockType.attributes ) {
820 if (
821 attributes[ attribute ] === undefined &&
822 blockType[ attribute ] !== undefined
823 ) {
824 attributes[ attribute ] = blockType[ attribute ];
825 }
826 }
827 }
828 render() {
829 return <BlockListBlock { ...this.props } />;
830 }
831 },
832 'withDefaultAttributes'
833 );
834 wp.hooks.addFilter(
835 'editor.BlockListBlock',
836 'acf/with-default-attributes',
837 withDefaultAttributes
838 );
839
840 /**
841 * The BlockSave functional component.
842 *
843 * @date 08/07/2020
844 * @since ACF 5.9.0
845 */
846 function BlockSave() {
847 return <InnerBlocks.Content />;
848 }
849
850 /**
851 * The BlockEdit component.
852 *
853 * @date 19/2/19
854 * @since ACF 5.7.12
855 */
856 class BlockEdit extends Component {
857 constructor( props ) {
858 super( props );
859 this.setup();
860 }
861
862 setup() {
863 const { name, attributes, clientId } = this.props;
864 const blockType = getBlockType( name );
865
866 // Restrict current mode.
867 function restrictMode( modes ) {
868 if ( ! modes.includes( attributes.mode ) ) {
869 attributes.mode = modes[ 0 ];
870 }
871 }
872
873 if ( isBlockInQueryLoop( clientId ) || isSiteEditor() ) {
874 restrictMode( [ 'preview' ] );
875 } else {
876 switch ( blockType.mode ) {
877 case 'edit':
878 restrictMode( [ 'edit', 'preview' ] );
879 break;
880 case 'preview':
881 restrictMode( [ 'preview', 'edit' ] );
882 break;
883 default:
884 restrictMode( [ 'auto' ] );
885 break;
886 }
887 }
888 }
889
890 render() {
891 const { name, attributes, setAttributes, clientId } = this.props;
892 const blockType = getBlockType( name );
893 const forcePreview =
894 isBlockInQueryLoop( clientId ) || isSiteEditor();
895 let { mode } = attributes;
896
897 if ( forcePreview ) {
898 mode = 'preview';
899 }
900
901 // Show toggle only for edit/preview modes and for blocks not in a query loop/FSE.
902 let showToggle = blockType.supports.mode;
903 if ( mode === 'auto' || forcePreview ) {
904 showToggle = false;
905 }
906
907 // Configure toggle variables.
908 const toggleText =
909 mode === 'preview'
910 ? acf.__( 'Switch to Edit' )
911 : acf.__( 'Switch to Preview' );
912 const toggleIcon =
913 mode === 'preview' ? 'edit' : 'welcome-view-site';
914 function toggleMode() {
915 setAttributes( {
916 mode: mode === 'preview' ? 'edit' : 'preview',
917 } );
918 }
919
920 // Return template.
921 return (
922 <Fragment>
923 <BlockControls>
924 { showToggle && (
925 <ToolbarGroup>
926 <ToolbarButton
927 className="components-icon-button components-toolbar__control"
928 label={ toggleText }
929 icon={ toggleIcon }
930 onClick={ toggleMode }
931 />
932 </ToolbarGroup>
933 ) }
934 </BlockControls>
935
936 <InspectorControls>
937 { mode === 'preview' && (
938 <div className="acf-block-component acf-block-panel">
939 <BlockForm { ...this.props } />
940 </div>
941 ) }
942 </InspectorControls>
943
944 <BlockBody { ...this.props } />
945 </Fragment>
946 );
947 }
948 }
949
950 /**
951 * The BlockBody functional component.
952 *
953 * @since ACF 5.7.12
954 */
955 function BlockBody( props ) {
956 const { attributes, isSelected, name, clientId } = props;
957 const { mode } = attributes;
958
959 const index = useSelect( ( select ) => {
960 const rootClientId =
961 select( 'core/block-editor' ).getBlockRootClientId( clientId );
962 return select( 'core/block-editor' ).getBlockIndex(
963 clientId,
964 rootClientId
965 );
966 } );
967
968 let showForm = true;
969 let additionalClasses = 'acf-block-component acf-block-body';
970
971 if ( ( mode === 'auto' && ! isSelected ) || mode === 'preview' ) {
972 additionalClasses += ' acf-block-preview';
973 showForm = false;
974 }
975
976 // Setup block cache if required, and update mode.
977 if ( ! ( clientId in acf.blockInstances ) ) {
978 acf.blockInstances[ clientId ] = {
979 validation_errors: false,
980 mode: mode,
981 };
982 }
983 acf.blockInstances[ clientId ].mode = mode;
984
985 if ( ! isSelected ) {
986 if (
987 blockSupportsValidation( name ) &&
988 acf.blockInstances[ clientId ].validation_errors
989 ) {
990 additionalClasses += ' acf-block-has-validation-error';
991 }
992 acf.blockInstances[ clientId ].has_been_deselected = true;
993 }
994
995 if ( getBlockVersion( name ) > 1 ) {
996 return (
997 <div { ...useBlockProps( { className: additionalClasses } ) }>
998 { showForm ? (
999 <BlockForm { ...props } index={ index } />
1000 ) : (
1001 <BlockPreview { ...props } index={ index } />
1002 ) }
1003 </div>
1004 );
1005 } else {
1006 return (
1007 <div { ...useBlockProps() }>
1008 <div className="acf-block-component acf-block-body">
1009 { showForm ? (
1010 <BlockForm { ...props } index={ index } />
1011 ) : (
1012 <BlockPreview { ...props } index={ index } />
1013 ) }
1014 </div>
1015 </div>
1016 );
1017 }
1018 }
1019
1020 /**
1021 * A react component to append HTMl.
1022 *
1023 * @date 19/2/19
1024 * @since ACF 5.7.12
1025 *
1026 * @param string children The html to insert.
1027 * @return void
1028 */
1029 class Div extends Component {
1030 render() {
1031 return (
1032 <div
1033 dangerouslySetInnerHTML={ { __html: this.props.children } }
1034 />
1035 );
1036 }
1037 }
1038
1039 /**
1040 * DynamicHTML Class.
1041 *
1042 * A react componenet to load and insert dynamic HTML.
1043 *
1044 * @date 19/2/19
1045 * @since ACF 5.7.12
1046 *
1047 * @param void
1048 * @return void
1049 */
1050 class DynamicHTML extends Component {
1051 constructor( props ) {
1052 super( props );
1053
1054 // Bind callbacks.
1055 this.setRef = this.setRef.bind( this );
1056
1057 // Define default props and call setup().
1058 this.id = '';
1059 this.el = false;
1060 this.subscribed = true;
1061 this.renderMethod = 'jQuery';
1062 this.passedValidation = false;
1063 this.setup( props );
1064
1065 // Load state.
1066 this.loadState();
1067 }
1068
1069 setup( props ) {
1070 const constructor = this.constructor.name;
1071 const clientId = props.clientId;
1072 if ( ! ( clientId in acf.blockInstances ) ) {
1073 acf.blockInstances[ clientId ] = {
1074 validation_errors: false,
1075 mode: props.mode,
1076 };
1077 }
1078 if ( ! ( constructor in acf.blockInstances[ clientId ] ) ) {
1079 acf.blockInstances[ clientId ][ constructor ] = {};
1080 }
1081 }
1082
1083 fetch() {
1084 // Do nothing.
1085 }
1086
1087 maybePreload( blockId, clientId, form ) {
1088 acf.debug( 'Preload check', blockId, clientId, form );
1089
1090 // Early return if block is in query loop.
1091 if ( isBlockInQueryLoop( this.props.clientId ) ) {
1092 acf.debug( 'Preload failed: Block is in query loop.' );
1093 return false;
1094 }
1095
1096 const preloadedBlocks = acf.get( 'preloadedBlocks' );
1097
1098 // Early return if no preloaded blocks exist.
1099 if ( ! preloadedBlocks || ! preloadedBlocks[ blockId ] ) {
1100 acf.debug( 'Preload failed: Block not preloaded.' );
1101 return false;
1102 }
1103
1104 // Create a copy to avoid mutating the original preloaded data.
1105 const preloadedBlock = { ...preloadedBlocks[ blockId ] };
1106
1107 // Ensure we only preload the correct block state (form or preview).
1108 if (
1109 ( form && ! preloadedBlock.form ) ||
1110 ( ! form && preloadedBlock.form )
1111 ) {
1112 acf.debug(
1113 'Preload failed: Correct state not preloaded.',
1114 form ? 'form' : 'preview'
1115 );
1116 return false;
1117 }
1118
1119 // Replace blockId with clientId in HTML.
1120 preloadedBlock.html = preloadedBlock.html.replaceAll(
1121 blockId,
1122 clientId
1123 );
1124
1125 // Replace blockId in validation errors.
1126 if (
1127 preloadedBlock.validation &&
1128 preloadedBlock.validation.errors
1129 ) {
1130 preloadedBlock.validation.errors =
1131 preloadedBlock.validation.errors.map( ( error ) => {
1132 error.input = error.input.replaceAll( blockId, clientId );
1133 return error;
1134 } );
1135 }
1136
1137 // Return preloaded object.
1138 acf.debug( 'Preload successful', preloadedBlock );
1139 return preloadedBlock;
1140 }
1141
1142 loadState() {
1143 const client = acf.blockInstances[ this.props.clientId ] || {};
1144 this.state = client[ this.constructor.name ] || {};
1145 }
1146 setState( state ) {
1147 acf.blockInstances[ this.props.clientId ][ this.constructor.name ] =
1148 {
1149 ...this.state,
1150 ...state,
1151 };
1152
1153 // Update component state if subscribed.
1154 // - Allows AJAX callback to update store without modifying state of an unmounted component.
1155 if ( this.subscribed || acf.get( 'StrictMode' ) ) {
1156 super.setState( state );
1157 }
1158
1159 acf.debug(
1160 'SetState',
1161 Object.assign( {}, this ),
1162 this.props.clientId,
1163 this.constructor.name,
1164 Object.assign(
1165 {},
1166 acf.blockInstances[ this.props.clientId ][
1167 this.constructor.name
1168 ]
1169 )
1170 );
1171 }
1172
1173 setHtml( html ) {
1174 html = html ? html.trim() : '';
1175
1176 // Bail early if html has not changed.
1177 if ( html === this.state.html ) {
1178 return;
1179 }
1180
1181 // Update state.
1182 const state = {
1183 html,
1184 };
1185
1186 if ( this.renderMethod === 'jsx' ) {
1187 state.jsx = acf.parseJSX(
1188 html,
1189 getBlockVersion( this.props.name )
1190 );
1191
1192 // Handle templates which don't contain any valid JSX parsable elements.
1193 if ( ! state.jsx ) {
1194 console.warn(
1195 'Your ACF block template contains no valid HTML elements. Appending a empty div to prevent React JS errors.'
1196 );
1197 state.html += '<div></div>';
1198 state.jsx = acf.parseJSX(
1199 state.html,
1200 getBlockVersion( this.props.name )
1201 );
1202 }
1203
1204 // If we've got an object (as an array) find the first valid React ref.
1205 if ( Array.isArray( state.jsx ) ) {
1206 let refElement = state.jsx.find( ( element ) =>
1207 React.isValidElement( element )
1208 );
1209 state.ref = refElement.ref;
1210 } else {
1211 state.ref = state.jsx.ref;
1212 }
1213 state.$el = $( this.el );
1214 } else {
1215 state.$el = $( html );
1216 }
1217 this.setState( state );
1218 }
1219
1220 setRef( el ) {
1221 this.el = el;
1222 }
1223
1224 render() {
1225 // Render JSX.
1226 if ( this.state.jsx ) {
1227 // If we're a v2+ block, use the jsx element itself as our ref.
1228 if ( getBlockVersion( this.props.name ) > 1 ) {
1229 this.setRef( this.state.jsx );
1230 return this.state.jsx;
1231 } else {
1232 return <div ref={ this.setRef }>{ this.state.jsx }</div>;
1233 }
1234 }
1235
1236 // Return HTML.
1237 return (
1238 <div ref={ this.setRef }>
1239 <Placeholder>
1240 <Spinner />
1241 </Placeholder>
1242 </div>
1243 );
1244 }
1245
1246 shouldComponentUpdate( { index }, { html } ) {
1247 if ( index !== this.props.index ) {
1248 this.componentWillMove();
1249 }
1250 return html !== this.state.html;
1251 }
1252
1253 display( context ) {
1254 // This method is called after setting new HTML and the Component render.
1255 // The jQuery render method simply needs to move $el into place.
1256 if ( this.renderMethod === 'jQuery' ) {
1257 const $el = this.state.$el;
1258 const $prevParent = $el.parent();
1259 const $thisParent = $( this.el );
1260
1261 // Move $el into place.
1262 // Skip this in StrictMode unless context is 'append' or component is subscribed.
1263 if (
1264 ! (
1265 acf.get( 'StrictMode' ) &&
1266 context !== 'append' &&
1267 ! this.subscribed
1268 )
1269 ) {
1270 $thisParent.html( $el );
1271 }
1272
1273 // Special case for reusable blocks.
1274 // Multiple instances of the same reusable block share the same block id.
1275 // This causes all instances to share the same state (cool), which unfortunately
1276 // pulls $el back and forth between the last rendered reusable block.
1277 // This simple fix leaves a "clone" behind :)
1278 if (
1279 $prevParent.length &&
1280 $prevParent[ 0 ] !== $thisParent[ 0 ]
1281 ) {
1282 $prevParent.html( $el.clone() );
1283 }
1284 }
1285
1286 // Lock block if required.
1287 if ( this.getValidationErrors() && this.isNotNewlyAdded() ) {
1288 this.lockBlockForSaving();
1289 } else {
1290 this.unlockBlockForSaving();
1291 }
1292
1293 // Call context specific method.
1294 switch ( context ) {
1295 case 'append':
1296 this.componentDidAppend();
1297 break;
1298 case 'remount':
1299 this.componentDidRemount();
1300 break;
1301 }
1302 }
1303
1304 validate() {
1305 // Do nothing.
1306 }
1307
1308 componentDidMount() {
1309 // Fetch on first load.
1310 if ( this.state.html === undefined ) {
1311 this.fetch();
1312
1313 // Or remount existing HTML.
1314 } else {
1315 this.display( 'remount' );
1316 }
1317 }
1318
1319 componentDidUpdate( prevProps, prevState ) {
1320 // HTML has changed.
1321 this.display( 'append' );
1322 }
1323
1324 componentDidAppend() {
1325 acf.doAction( 'append', this.state.$el );
1326 }
1327
1328 componentWillUnmount() {
1329 // Only skip unmount action if in StrictMode AND component is not subscribed
1330 if ( ! acf.get( 'StrictMode' ) || this.subscribed ) {
1331 acf.doAction( 'unmount', this.state.$el );
1332 }
1333
1334 // Unsubscribe this component from state
1335 this.subscribed = false;
1336 }
1337
1338 componentDidRemount() {
1339 this.subscribed = true;
1340
1341 // Use setTimeout to avoid incorrect timing of events.
1342 // React will unmount and mount components in DOM order.
1343 // This means a new component can be mounted before an old one is unmounted.
1344 // ACF shares $el across new/old components which is un-React-like.
1345 // This timout ensures that unmounting occurs before remounting.
1346 setTimeout( () => {
1347 acf.doAction( 'remount', this.state.$el );
1348 } );
1349 }
1350
1351 componentWillMove() {
1352 acf.doAction( 'unmount', this.state.$el );
1353 setTimeout( () => {
1354 acf.doAction( 'remount', this.state.$el );
1355 } );
1356 }
1357
1358 isNotNewlyAdded() {
1359 return (
1360 acf.blockInstances[ this.props.clientId ].has_been_deselected ||
1361 false
1362 );
1363 }
1364
1365 hasShownValidation() {
1366 return (
1367 acf.blockInstances[ this.props.clientId ].shown_validation ||
1368 false
1369 );
1370 }
1371
1372 setShownValidation() {
1373 acf.blockInstances[ this.props.clientId ].shown_validation = true;
1374 }
1375
1376 setValidationErrors( errors ) {
1377 acf.blockInstances[ this.props.clientId ].validation_errors =
1378 errors;
1379 }
1380
1381 getValidationErrors() {
1382 return acf.blockInstances[ this.props.clientId ].validation_errors;
1383 }
1384
1385 getMode() {
1386 return acf.blockInstances[ this.props.clientId ].mode;
1387 }
1388
1389 lockBlockForSaving() {
1390 if ( ! wp.data.dispatch( 'core/editor' ) ) return;
1391 wp.data
1392 .dispatch( 'core/editor' )
1393 .lockPostSaving( 'acf/block/' + this.props.clientId );
1394 }
1395
1396 unlockBlockForSaving() {
1397 if ( ! wp.data.dispatch( 'core/editor' ) ) return;
1398 wp.data
1399 .dispatch( 'core/editor' )
1400 .unlockPostSaving( 'acf/block/' + this.props.clientId );
1401 }
1402
1403 displayValidation( $formEl ) {
1404 if ( ! blockSupportsValidation( this.props.name ) ) {
1405 acf.debug( 'Block does not support validation' );
1406 return;
1407 }
1408 if ( ! $formEl || $formEl.hasClass( 'acf-empty-block-fields' ) ) {
1409 acf.debug( 'There is no edit form available to validate.' );
1410 return;
1411 }
1412
1413 const errors = this.getValidationErrors();
1414 acf.debug(
1415 'Starting handle validation',
1416 Object.assign( {}, this ),
1417 Object.assign( {}, $formEl ),
1418 errors
1419 );
1420
1421 this.setShownValidation();
1422
1423 let validator = acf.getBlockFormValidator( $formEl );
1424 validator.clearErrors();
1425
1426 acf.doAction( 'blocks/validation/pre_apply', errors );
1427 if ( errors ) {
1428 validator.addErrors( errors );
1429 validator.showErrors( 'after' );
1430
1431 this.lockBlockForSaving();
1432 } else {
1433 // remove previous error message
1434 if ( validator.has( 'notice' ) ) {
1435 validator.get( 'notice' ).update( {
1436 type: 'success',
1437 text: acf.__( 'Validation successful' ),
1438 timeout: 1000,
1439 } );
1440 validator.set( 'notice', null );
1441 }
1442
1443 this.unlockBlockForSaving();
1444 }
1445 acf.doAction( 'blocks/validation/post_apply', errors );
1446 }
1447 }
1448
1449 /**
1450 * BlockForm Class.
1451 *
1452 * A react componenet to handle the block form.
1453 *
1454 * @date 19/2/19
1455 * @since ACF 5.7.12
1456 *
1457 * @param string id the block id.
1458 * @return void
1459 */
1460 class BlockForm extends DynamicHTML {
1461 setup( props ) {
1462 this.id = `BlockForm-${ props.clientId }`;
1463 super.setup( props );
1464 }
1465
1466 fetch( validate_only = false, data = false ) {
1467 // Extract props.
1468 const { context, clientId, name } = this.props;
1469 let { attributes } = this.props;
1470
1471 let query = { form: true };
1472 if ( validate_only ) {
1473 query = { validate: true };
1474 attributes.data = data;
1475 }
1476
1477 const hash = createBlockAttributesHash( attributes, context );
1478
1479 acf.debug( 'BlockForm fetch', attributes, query );
1480
1481 // Try preloaded data first.
1482 const preloaded = this.maybePreload( hash, clientId, true );
1483
1484 if ( preloaded ) {
1485 this.setHtml(
1486 acf.applyFilters(
1487 'blocks/form/render',
1488 preloaded.html,
1489 true
1490 )
1491 );
1492 if ( preloaded.validation )
1493 this.setValidationErrors( preloaded.validation.errors );
1494 return;
1495 }
1496
1497 if ( ! blockSupportsValidation( name ) ) {
1498 query.validate = false;
1499 }
1500
1501 // Request AJAX and update HTML on complete.
1502 fetchBlock( {
1503 attributes,
1504 context,
1505 clientId,
1506 query,
1507 } ).done( ( { data } ) => {
1508 acf.debug( 'fetch block form promise' );
1509
1510 if ( ! data ) {
1511 this.setHtml(
1512 `<div class="acf-block-fields acf-fields acf-empty-block-fields">${ acf.__(
1513 'Error loading block form'
1514 ) }</div>`
1515 );
1516 return;
1517 }
1518
1519 if ( data.form ) {
1520 this.setHtml(
1521 acf.applyFilters(
1522 'blocks/form/render',
1523 data.form.replaceAll( data.clientId, clientId ),
1524 false
1525 )
1526 );
1527 }
1528
1529 if ( data.validation )
1530 this.setValidationErrors( data.validation.errors );
1531
1532 if ( this.isNotNewlyAdded() ) {
1533 acf.debug(
1534 "Block has already shown it's invalid. The form needs to show validation errors"
1535 );
1536 this.validate();
1537 }
1538 } );
1539 }
1540
1541 validate( loadState = true ) {
1542 if ( loadState ) {
1543 this.loadState();
1544 }
1545
1546 acf.debug(
1547 'BlockForm calling validate with state',
1548 Object.assign( {}, this )
1549 );
1550 super.displayValidation( this.state.$el );
1551 }
1552
1553 shouldComponentUpdate( nextProps, nextState ) {
1554 if (
1555 blockSupportsValidation( this.props.name ) &&
1556 this.state.$el &&
1557 this.isNotNewlyAdded() &&
1558 ! this.hasShownValidation()
1559 ) {
1560 this.validate( false ); // Shouldn't update state in shouldComponentUpdate.
1561 }
1562
1563 return super.shouldComponentUpdate( nextProps, nextState );
1564 }
1565
1566 componentWillUnmount() {
1567 super.componentWillUnmount();
1568
1569 //TODO: either delete this, or clear validations here (if that's a sensible idea)
1570
1571 acf.debug( 'BlockForm Component did unmount' );
1572 }
1573
1574 componentDidRemount() {
1575 super.componentDidRemount();
1576
1577 acf.debug( 'BlockForm component did remount' );
1578
1579 const { $el } = this.state;
1580
1581 if (
1582 blockSupportsValidation( this.props.name ) &&
1583 this.isNotNewlyAdded()
1584 ) {
1585 acf.debug(
1586 "Block has already shown it's invalid. The form needs to show validation errors"
1587 );
1588 this.validate();
1589 }
1590
1591 // Make sure our on append events are registered.
1592 if ( $el.data( 'acf-events-added' ) !== true ) {
1593 this.componentDidAppend();
1594 }
1595 }
1596
1597 componentDidAppend() {
1598 super.componentDidAppend();
1599
1600 acf.debug( 'BlockForm component did append' );
1601
1602 // Extract props.
1603 const { attributes, setAttributes, clientId, name } = this.props;
1604 const thisBlockForm = this;
1605 const { $el } = this.state;
1606
1607 // Callback for updating block data and validation status if we're in an edit only mode.
1608 function serializeData( silent = false ) {
1609 const data = acf.serialize( $el, `acf-block_${ clientId }` );
1610
1611 if ( silent ) {
1612 attributes.data = data;
1613 } else {
1614 setAttributes( {
1615 data,
1616 } );
1617 }
1618
1619 if (
1620 blockSupportsValidation( name ) &&
1621 ! silent &&
1622 thisBlockForm.getMode() === 'edit'
1623 ) {
1624 acf.debug(
1625 'No block preview currently available. Need to trigger a validation only fetch.'
1626 );
1627 thisBlockForm.fetch( true, data );
1628 }
1629 }
1630
1631 // Add events.
1632 let timeout = false;
1633 $el.on( 'change keyup', () => {
1634 clearTimeout( timeout );
1635 timeout = setTimeout( serializeData, 300 );
1636 } );
1637
1638 // Log initialization for remount check on the persistent element.
1639 $el.data( 'acf-events-added', true );
1640
1641 // Ensure newly added block is saved with data.
1642 // Do it silently to avoid triggering a preview render.
1643 if ( ! attributes.data ) {
1644 serializeData( true );
1645 }
1646 }
1647 }
1648
1649 /**
1650 * BlockPreview Class.
1651 *
1652 * A react componenet to handle the block preview.
1653 *
1654 * @date 19/2/19
1655 * @since ACF 5.7.12
1656 *
1657 * @param string id the block id.
1658 * @return void
1659 */
1660 class BlockPreview extends DynamicHTML {
1661 setup( props ) {
1662 const blockType = getBlockType( props.name );
1663 const contextPostId = acf.isget( this.props, 'context', 'postId' );
1664
1665 this.id = `BlockPreview-${ props.clientId }`;
1666
1667 super.setup( props );
1668
1669 // Apply the contextPostId to the ID if set to stop query loop ID duplication.
1670 if ( contextPostId ) {
1671 this.id = `BlockPreview-${ props.clientId }-${ contextPostId }`;
1672 }
1673
1674 if ( blockType.supports.jsx ) {
1675 this.renderMethod = 'jsx';
1676 }
1677 }
1678
1679 fetch( args = {} ) {
1680 const {
1681 attributes = this.props.attributes,
1682 clientId = this.props.clientId,
1683 context = this.props.context,
1684 delay = 0,
1685 } = args;
1686
1687 const { name } = this.props;
1688
1689 // Remember attributes used to fetch HTML.
1690 this.setState( {
1691 prevAttributes: attributes,
1692 prevContext: context,
1693 } );
1694
1695 const hash = createBlockAttributesHash( attributes, context );
1696
1697 // Try preloaded data first.
1698 let preloaded = this.maybePreload( hash, clientId, false );
1699
1700 if ( preloaded ) {
1701 if ( getBlockVersion( name ) == 1 ) {
1702 preloaded.html =
1703 '<div class="acf-block-preview">' +
1704 preloaded.html +
1705 '</div>';
1706 }
1707 this.setHtml(
1708 acf.applyFilters(
1709 'blocks/preview/render',
1710 preloaded.html,
1711 true
1712 )
1713 );
1714 if ( preloaded.validation )
1715 this.setValidationErrors( preloaded.validation.errors );
1716 return;
1717 }
1718
1719 let query = { preview: true };
1720
1721 if ( ! blockSupportsValidation( name ) ) {
1722 query.validate = false;
1723 }
1724
1725 // Request AJAX and update HTML on complete.
1726 fetchBlock( {
1727 attributes,
1728 context,
1729 clientId,
1730 query,
1731 delay,
1732 } ).done( ( { data } ) => {
1733 if ( ! data ) {
1734 this.setHtml(
1735 `<div class="acf-block-fields acf-fields acf-empty-block-fields">${ acf.__(
1736 'Error previewing block'
1737 ) }</div>`
1738 );
1739 return;
1740 }
1741
1742 let replaceHtml = data.preview.replaceAll(
1743 data.clientId,
1744 clientId
1745 );
1746 if ( getBlockVersion( name ) == 1 ) {
1747 replaceHtml =
1748 '<div class="acf-block-preview">' +
1749 replaceHtml +
1750 '</div>';
1751 }
1752 acf.debug( 'fetch block render promise' );
1753 this.setHtml(
1754 acf.applyFilters(
1755 'blocks/preview/render',
1756 replaceHtml,
1757 false
1758 )
1759 );
1760 if ( data.validation ) {
1761 this.setValidationErrors( data.validation.errors );
1762 }
1763 if ( this.isNotNewlyAdded() ) {
1764 this.validate();
1765 }
1766 } );
1767 }
1768
1769 validate() {
1770 // Check we've got a block form for this instance.
1771 const client = acf.blockInstances[ this.props.clientId ] || {};
1772 const blockFormState = client.BlockForm || false;
1773 if ( blockFormState ) {
1774 super.displayValidation( blockFormState.$el );
1775 }
1776 }
1777
1778 componentDidAppend() {
1779 super.componentDidAppend();
1780 this.renderBlockPreviewEvent();
1781 }
1782
1783 shouldComponentUpdate( nextProps, nextState ) {
1784 const nextAttributes = nextProps.attributes;
1785 const thisAttributes = this.props.attributes;
1786
1787 // Update preview if block data has changed.
1788 if (
1789 ! compareObjects( nextAttributes, thisAttributes ) ||
1790 ! compareObjects( nextProps.context, this.props.context )
1791 ) {
1792 let delay = 0;
1793
1794 // Delay fetch when editing className or anchor to simulate consistent logic to custom fields.
1795 if ( nextAttributes.className !== thisAttributes.className ) {
1796 delay = 300;
1797 }
1798 if ( nextAttributes.anchor !== thisAttributes.anchor ) {
1799 delay = 300;
1800 }
1801
1802 acf.debug(
1803 'Triggering fetch from block preview shouldComponentUpdate'
1804 );
1805
1806 this.fetch( {
1807 attributes: nextAttributes,
1808 context: nextProps.context,
1809 delay,
1810 } );
1811 }
1812 return super.shouldComponentUpdate( nextProps, nextState );
1813 }
1814
1815 renderBlockPreviewEvent() {
1816 // Extract props.
1817 const { attributes, name } = this.props;
1818 const { $el, ref } = this.state;
1819 var blockElement;
1820
1821 // Generate action friendly type.
1822 const type = attributes.name.replace( 'acf/', '' );
1823
1824 if ( ref && ref.current ) {
1825 // We've got a react ref from a JSX container. Use the parent as the blockElement
1826 blockElement = $( ref.current ).parent();
1827 } else if ( getBlockVersion( name ) == 1 ) {
1828 blockElement = $el;
1829 } else {
1830 blockElement = $el.parents( '.acf-block-preview' );
1831 }
1832
1833 // Do action.
1834 acf.doAction( 'render_block_preview', blockElement, attributes );
1835 acf.doAction(
1836 `render_block_preview/type=${ type }`,
1837 blockElement,
1838 attributes
1839 );
1840 }
1841
1842 componentDidRemount() {
1843 super.componentDidRemount();
1844
1845 acf.debug(
1846 'Checking if fetch is required in BlockPreview componentDidRemount',
1847 Object.assign( {}, this.state.prevAttributes ),
1848 Object.assign( {}, this.props.attributes ),
1849 Object.assign( {}, this.state.prevContext ),
1850 Object.assign( {}, this.props.context )
1851 );
1852
1853 // Update preview if data has changed since last render (changing from "edit" to "preview").
1854 if (
1855 ! compareObjects(
1856 this.state.prevAttributes,
1857 this.props.attributes
1858 ) ||
1859 ! compareObjects( this.state.prevContext, this.props.context )
1860 ) {
1861 acf.debug(
1862 'Triggering block preview fetch from componentDidRemount'
1863 );
1864 this.fetch();
1865 }
1866
1867 // Fire the block preview event so blocks can reinit JS elements.
1868 // React reusing DOM elements covers any potential race condition from the above fetch.
1869 this.renderBlockPreviewEvent();
1870 }
1871 }
1872
1873 /**
1874 * Initializes ACF Blocks logic and registration.
1875 *
1876 * @since ACF 5.9.0
1877 */
1878 function initialize() {
1879 // Add support for WordPress versions before 5.2.
1880 if ( ! wp.blockEditor ) {
1881 wp.blockEditor = wp.editor;
1882 }
1883
1884 // Register block types.
1885 const blockTypes = acf.get( 'blockTypes' );
1886 if ( blockTypes ) {
1887 // Only register blocks with version <= 2 (v3+ blocks are registered separately).
1888 blockTypes.forEach( ( blockType ) => {
1889 parseInt( blockType.acf_block_version ) <= 2 &&
1890 registerBlockType( blockType );
1891 } );
1892 }
1893 }
1894
1895 // Run the initialize callback during the "prepare" action.
1896 // This ensures that all localized data is available and that blocks are registered before the WP editor has been instantiated.
1897 acf.addAction( 'prepare', initialize );
1898
1899 /**
1900 * Returns a valid vertical alignment.
1901 *
1902 * @date 07/08/2020
1903 * @since ACF 5.9.0
1904 *
1905 * @param string align A vertical alignment.
1906 * @return string
1907 */
1908 function validateVerticalAlignment( align ) {
1909 const ALIGNMENTS = [ 'top', 'center', 'bottom' ];
1910 const DEFAULT = 'top';
1911 return ALIGNMENTS.includes( align ) ? align : DEFAULT;
1912 }
1913
1914 /**
1915 * Returns a valid horizontal alignment.
1916 *
1917 * @date 07/08/2020
1918 * @since ACF 5.9.0
1919 *
1920 * @param string align A horizontal alignment.
1921 * @return string
1922 */
1923 function validateHorizontalAlignment( align ) {
1924 const ALIGNMENTS = [ 'left', 'center', 'right' ];
1925 const DEFAULT = acf.get( 'rtl' ) ? 'right' : 'left';
1926 return ALIGNMENTS.includes( align ) ? align : DEFAULT;
1927 }
1928
1929 /**
1930 * Returns a valid matrix alignment.
1931 *
1932 * Written for "upgrade-path" compatibility from vertical alignment to matrix alignment.
1933 *
1934 * @date 07/08/2020
1935 * @since ACF 5.9.0
1936 *
1937 * @param string align A matrix alignment.
1938 * @return string
1939 */
1940 function validateMatrixAlignment( align ) {
1941 const DEFAULT = 'center center';
1942 if ( align ) {
1943 const [ y, x ] = align.split( ' ' );
1944 return `${ validateVerticalAlignment(
1945 y
1946 ) } ${ validateHorizontalAlignment( x ) }`;
1947 }
1948 return DEFAULT;
1949 }
1950
1951 /**
1952 * A higher order component adding alignContent editing functionality.
1953 *
1954 * @date 08/07/2020
1955 * @since ACF 5.9.0
1956 *
1957 * @param component OriginalBlockEdit The original BlockEdit component.
1958 * @param object blockType The block type settings.
1959 * @return component
1960 */
1961 function withAlignContentComponent( OriginalBlockEdit, blockType ) {
1962 // Determine alignment vars
1963 let type =
1964 blockType.supports.align_content || blockType.supports.alignContent;
1965 let AlignmentComponent;
1966 let validateAlignment;
1967 switch ( type ) {
1968 case 'matrix':
1969 AlignmentComponent =
1970 BlockAlignmentMatrixControl || BlockAlignmentMatrixToolbar;
1971 validateAlignment = validateMatrixAlignment;
1972 break;
1973 default:
1974 AlignmentComponent = BlockVerticalAlignmentToolbar;
1975 validateAlignment = validateVerticalAlignment;
1976 break;
1977 }
1978
1979 // Ensure alignment component exists.
1980 if ( AlignmentComponent === undefined ) {
1981 console.warn(
1982 `The "${ type }" alignment component was not found.`
1983 );
1984 return OriginalBlockEdit;
1985 }
1986
1987 // Ensure correct block attribute data is sent in intial preview AJAX request.
1988 blockType.alignContent = validateAlignment( blockType.alignContent );
1989
1990 // Return wrapped component.
1991 return class WrappedBlockEdit extends Component {
1992 render() {
1993 const { attributes, setAttributes } = this.props;
1994 const { alignContent } = attributes;
1995 function onChangeAlignContent( alignContent ) {
1996 setAttributes( {
1997 alignContent: validateAlignment( alignContent ),
1998 } );
1999 }
2000 return (
2001 <Fragment>
2002 <BlockControls group="block">
2003 <AlignmentComponent
2004 label={ acf.__( 'Change content alignment' ) }
2005 value={ validateAlignment( alignContent ) }
2006 onChange={ onChangeAlignContent }
2007 />
2008 </BlockControls>
2009 <OriginalBlockEdit { ...this.props } />
2010 </Fragment>
2011 );
2012 }
2013 };
2014 }
2015
2016 /**
2017 * A higher order component adding alignText editing functionality.
2018 *
2019 * @date 08/07/2020
2020 * @since ACF 5.9.0
2021 *
2022 * @param component OriginalBlockEdit The original BlockEdit component.
2023 * @param object blockType The block type settings.
2024 * @return component
2025 */
2026 function withAlignTextComponent( OriginalBlockEdit, blockType ) {
2027 const validateAlignment = validateHorizontalAlignment;
2028
2029 // Ensure correct block attribute data is sent in intial preview AJAX request.
2030 blockType.alignText = validateAlignment( blockType.alignText );
2031
2032 // Return wrapped component.
2033 return class WrappedBlockEdit extends Component {
2034 render() {
2035 const { attributes, setAttributes } = this.props;
2036 const { alignText } = attributes;
2037
2038 function onChangeAlignText( alignText ) {
2039 setAttributes( {
2040 alignText: validateAlignment( alignText ),
2041 } );
2042 }
2043
2044 return (
2045 <Fragment>
2046 <BlockControls group="block">
2047 <AlignmentToolbar
2048 value={ validateAlignment( alignText ) }
2049 onChange={ onChangeAlignText }
2050 />
2051 </BlockControls>
2052 <OriginalBlockEdit { ...this.props } />
2053 </Fragment>
2054 );
2055 }
2056 };
2057 }
2058
2059 /**
2060 * A higher order component adding full height support.
2061 *
2062 * @date 19/07/2021
2063 * @since ACF 5.10.0
2064 *
2065 * @param component OriginalBlockEdit The original BlockEdit component.
2066 * @param object blockType The block type settings.
2067 * @return component
2068 */
2069 function withFullHeightComponent( OriginalBlockEdit, blockType ) {
2070 if ( ! BlockFullHeightAlignmentControl ) return OriginalBlockEdit;
2071
2072 // Return wrapped component.
2073 return class WrappedBlockEdit extends Component {
2074 render() {
2075 const { attributes, setAttributes } = this.props;
2076 const { fullHeight } = attributes;
2077
2078 function onToggleFullHeight( fullHeight ) {
2079 setAttributes( {
2080 fullHeight,
2081 } );
2082 }
2083
2084 return (
2085 <Fragment>
2086 <BlockControls group="block">
2087 <BlockFullHeightAlignmentControl
2088 isActive={ fullHeight }
2089 onToggle={ onToggleFullHeight }
2090 />
2091 </BlockControls>
2092 <OriginalBlockEdit { ...this.props } />
2093 </Fragment>
2094 );
2095 }
2096 };
2097 }
2098
2099 /**
2100 * Appends a backwards compatibility attribute for conversion.
2101 *
2102 * @since ACF 6.0
2103 *
2104 * @param object attributes The block type attributes.
2105 * @return object
2106 */
2107 function addBackCompatAttribute( attributes, new_attribute, type ) {
2108 attributes[ new_attribute ] = {
2109 type: type,
2110 };
2111 return attributes;
2112 }
2113
2114 /**
2115 * Create a block hash from attributes
2116 *
2117 * @since ACF 6.0
2118 *
2119 * @param object attributes The block type attributes.
2120 * @param object context The current block context object.
2121 * @return string
2122 */
2123 function createBlockAttributesHash( attributes, context ) {
2124 attributes[ '_acf_context' ] = sortObjectByKey( context );
2125 return md5( JSON.stringify( sortObjectByKey( attributes ) ) );
2126 }
2127
2128 /**
2129 * Key sort an object
2130 *
2131 * @since ACF 6.3.1
2132 *
2133 * @param object toSort The object to be sorted
2134 * @return object
2135 */
2136 function sortObjectByKey( toSort ) {
2137 return Object.keys( toSort )
2138 .sort()
2139 .reduce( ( acc, currValue ) => {
2140 acc[ currValue ] = toSort[ currValue ];
2141 return acc;
2142 }, {} );
2143 }
2144 } )( jQuery );
2145