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