PluginProbe ʕ •ᴥ•ʔ
Secure Custom Fields / 6.5.4
Secure Custom Fields v6.5.4
6.9.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
1901 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 document.querySelectorAll( 'iframe[name="editor-canvas"]' ).length > 0;
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 ) {
763 restrictMode( [ 'preview' ] );
764 } else {
765 switch ( blockType.mode ) {
766 case 'edit':
767 restrictMode( [ 'edit', 'preview' ] );
768 break;
769 case 'preview':
770 restrictMode( [ 'preview', 'edit' ] );
771 break;
772 default:
773 restrictMode( [ 'auto' ] );
774 break;
775 }
776 }
777 }
778
779 render() {
780 const { name, attributes, setAttributes, clientId } = this.props;
781 const blockType = getBlockType( name );
782 const forcePreview =
783 isBlockInQueryLoop( clientId ) ||
784 isSiteEditor();
785 let { mode } = attributes;
786
787 if ( forcePreview ) {
788 mode = 'preview';
789 }
790
791 // Show toggle only for edit/preview modes and for blocks not in a query loop/FSE.
792 let showToggle = blockType.supports.mode;
793 if ( mode === 'auto' || forcePreview ) {
794 showToggle = false;
795 }
796
797 // Configure toggle variables.
798 const toggleText = mode === 'preview' ? acf.__( 'Switch to Edit' ) : acf.__( 'Switch to Preview' );
799 const toggleIcon = mode === 'preview' ? 'edit' : 'welcome-view-site';
800 function toggleMode() {
801 setAttributes( {
802 mode: mode === 'preview' ? 'edit' : 'preview',
803 } );
804 }
805
806 // Return template.
807 return (
808 <Fragment>
809 <BlockControls>
810 { showToggle && (
811 <ToolbarGroup>
812 <ToolbarButton
813 className="components-icon-button components-toolbar__control"
814 label={ toggleText }
815 icon={ toggleIcon }
816 onClick={ toggleMode }
817 />
818 </ToolbarGroup>
819 ) }
820 </BlockControls>
821
822 <InspectorControls>
823 { mode === 'preview' && (
824 <div className="acf-block-component acf-block-panel">
825 <BlockForm { ...this.props } />
826 </div>
827 ) }
828 </InspectorControls>
829
830 <BlockBody { ...this.props } />
831 </Fragment>
832 );
833 }
834 }
835
836 /**
837 * The BlockBody functional component.
838 *
839 * @since ACF 5.7.12
840 */
841 function BlockBody( props ) {
842 const { attributes, isSelected, name, clientId } = props;
843 const { mode } = attributes;
844
845 const index = useSelect( ( select ) => {
846 const rootClientId = select( 'core/block-editor' ).getBlockRootClientId( clientId );
847 return select( 'core/block-editor' ).getBlockIndex( clientId, rootClientId );
848 } );
849
850 let showForm = true;
851 let additionalClasses = 'acf-block-component acf-block-body';
852
853 if ( ( mode === 'auto' && ! isSelected ) || mode === 'preview' ) {
854 additionalClasses += ' acf-block-preview';
855 showForm = false;
856 }
857
858 // Setup block cache if required, and update mode.
859 if ( ! ( clientId in acf.blockInstances ) ) {
860 acf.blockInstances[ clientId ] = {
861 validation_errors: false,
862 mode: mode,
863 };
864 }
865 acf.blockInstances[ clientId ].mode = mode;
866
867 if ( ! isSelected ) {
868 if ( blockSupportsValidation( name ) && acf.blockInstances[ clientId ].validation_errors ) {
869 additionalClasses += ' acf-block-has-validation-error';
870 }
871 acf.blockInstances[ clientId ].has_been_deselected = true;
872 }
873
874 if ( getBlockVersion( name ) > 1 ) {
875 return (
876 <div { ...useBlockProps( { className: additionalClasses } ) }>
877 { showForm ? (
878 <BlockForm { ...props } index={ index } />
879 ) : (
880 <BlockPreview { ...props } index={ index } />
881 ) }
882 </div>
883 );
884 } else {
885 return (
886 <div { ...useBlockProps() }>
887 <div className="acf-block-component acf-block-body">
888 { showForm ? (
889 <BlockForm { ...props } index={ index } />
890 ) : (
891 <BlockPreview { ...props } index={ index } />
892 ) }
893 </div>
894 </div>
895 );
896 }
897 }
898
899 /**
900 * A react component to append HTMl.
901 *
902 * @date 19/2/19
903 * @since ACF 5.7.12
904 *
905 * @param string children The html to insert.
906 * @return void
907 */
908 class Div extends Component {
909 render() {
910 return <div dangerouslySetInnerHTML={ { __html: this.props.children } } />;
911 }
912 }
913
914 /**
915 * A react Component for inline scripts.
916 *
917 * This Component uses a combination of React references and jQuery to append the
918 * inline <script> HTML each time the component is rendered.
919 *
920 * @date 29/05/2020
921 * @since ACF 5.9.0
922 *
923 * @param type Var Description.
924 * @return type Description.
925 */
926 class Script extends Component {
927 render() {
928 return <div ref={ ( el ) => ( this.el = el ) } />;
929 }
930 setHTML( html ) {
931 $( this.el ).html( `<script>${ html }</script>` );
932 }
933 componentDidUpdate() {
934 this.setHTML( this.props.children );
935 }
936 componentDidMount() {
937 this.setHTML( this.props.children );
938 }
939 }
940
941 /**
942 * DynamicHTML Class.
943 *
944 * A react componenet to load and insert dynamic HTML.
945 *
946 * @date 19/2/19
947 * @since ACF 5.7.12
948 *
949 * @param void
950 * @return void
951 */
952 class DynamicHTML extends Component {
953 constructor( props ) {
954 super( props );
955
956 // Bind callbacks.
957 this.setRef = this.setRef.bind( this );
958
959 // Define default props and call setup().
960 this.id = '';
961 this.el = false;
962 this.subscribed = true;
963 this.renderMethod = 'jQuery';
964 this.passedValidation = false;
965 this.setup( props );
966
967 // Load state.
968 this.loadState();
969 }
970
971 setup( props ) {
972 const constructor = this.constructor.name;
973 const clientId = props.clientId;
974 if ( ! ( clientId in acf.blockInstances ) ) {
975 acf.blockInstances[ clientId ] = {
976 validation_errors: false,
977 mode: props.mode,
978 };
979 }
980 if ( ! ( constructor in acf.blockInstances[ clientId ] ) ) {
981 acf.blockInstances[ clientId ][ constructor ] = {};
982 }
983 }
984
985 fetch() {
986 // Do nothing.
987 }
988
989 maybePreload( blockId, clientId, form ) {
990 acf.debug( 'Preload check', blockId, clientId, form );
991 if ( ! isBlockInQueryLoop( this.props.clientId ) ) {
992 const preloadedBlocks = acf.get( 'preloadedBlocks' );
993 const modeText = form ? 'form' : 'preview';
994
995 if ( preloadedBlocks && preloadedBlocks[ blockId ] ) {
996 // Ensure we only preload the correct block state (form or preview).
997 if (
998 ( form && ! preloadedBlocks[ blockId ].form ) ||
999 ( ! form && preloadedBlocks[ blockId ].form )
1000 ) {
1001 acf.debug( 'Preload failed: state not preloaded.' );
1002 return false;
1003 }
1004
1005 // Set HTML to the preloaded version.
1006 preloadedBlocks[ blockId ].html = preloadedBlocks[ blockId ].html.replaceAll( blockId, clientId );
1007
1008 // Replace blockId in errors.
1009 if ( preloadedBlocks[ blockId ].validation && preloadedBlocks[ blockId ].validation.errors ) {
1010 preloadedBlocks[ blockId ].validation.errors = preloadedBlocks[ blockId ].validation.errors.map(
1011 ( error ) => {
1012 error.input = error.input.replaceAll( blockId, clientId );
1013 return error;
1014 }
1015 );
1016 }
1017
1018 // Return preloaded object.
1019 acf.debug( 'Preload successful', preloadedBlocks[ blockId ] );
1020 return preloadedBlocks[ blockId ];
1021 }
1022 }
1023 acf.debug( 'Preload failed: not preloaded.' );
1024 return false;
1025 }
1026
1027 loadState() {
1028 const client = acf.blockInstances[ this.props.clientId ] || {};
1029 this.state = client[ this.constructor.name ] || {};
1030 }
1031
1032 setState( state ) {
1033 acf.blockInstances[ this.props.clientId ][ this.constructor.name ] = {
1034 ...this.state,
1035 ...state,
1036 };
1037
1038 // Update component state if subscribed.
1039 // - Allows AJAX callback to update store without modifying state of an unmounted component.
1040 if ( this.subscribed ) {
1041 super.setState( state );
1042 }
1043
1044 acf.debug(
1045 'SetState',
1046 Object.assign( {}, this ),
1047 this.props.clientId,
1048 this.constructor.name,
1049 Object.assign( {}, acf.blockInstances[ this.props.clientId ][ this.constructor.name ] )
1050 );
1051 }
1052
1053 setHtml( html ) {
1054 html = html ? html.trim() : '';
1055
1056 // Bail early if html has not changed.
1057 if ( html === this.state.html ) {
1058 return;
1059 }
1060
1061 // Update state.
1062 const state = {
1063 html,
1064 };
1065
1066 if ( this.renderMethod === 'jsx' ) {
1067 state.jsx = acf.parseJSX( html, getBlockVersion( this.props.name ) );
1068
1069 // Handle templates which don't contain any valid JSX parsable elements.
1070 if ( ! state.jsx ) {
1071 console.warn(
1072 'Your ACF block template contains no valid HTML elements. Appending a empty div to prevent React JS errors.'
1073 );
1074 state.html += '<div></div>';
1075 state.jsx = acf.parseJSX( state.html, getBlockVersion( this.props.name ) );
1076 }
1077
1078 // If we've got an object (as an array) find the first valid React ref.
1079 if ( Array.isArray( state.jsx ) ) {
1080 let refElement = state.jsx.find( ( element ) => React.isValidElement( element ) );
1081 state.ref = refElement.ref;
1082 } else {
1083 state.ref = state.jsx.ref;
1084 }
1085 state.$el = $( this.el );
1086 } else {
1087 state.$el = $( html );
1088 }
1089 this.setState( state );
1090 }
1091
1092 setRef( el ) {
1093 this.el = el;
1094 }
1095
1096 render() {
1097 // Render JSX.
1098 if ( this.state.jsx ) {
1099 // If we're a v2+ block, use the jsx element itself as our ref.
1100 if ( getBlockVersion( this.props.name ) > 1 ) {
1101 this.setRef( this.state.jsx );
1102 return this.state.jsx;
1103 } else {
1104 return <div ref={ this.setRef }>{ this.state.jsx }</div>;
1105 }
1106 }
1107
1108 // Return HTML.
1109 return (
1110 <div ref={ this.setRef }>
1111 <Placeholder>
1112 <Spinner />
1113 </Placeholder>
1114 </div>
1115 );
1116 }
1117
1118 shouldComponentUpdate( { index }, { html } ) {
1119 if ( index !== this.props.index ) {
1120 this.componentWillMove();
1121 }
1122 return html !== this.state.html;
1123 }
1124
1125 display( context ) {
1126 // This method is called after setting new HTML and the Component render.
1127 // The jQuery render method simply needs to move $el into place.
1128 if ( this.renderMethod === 'jQuery' ) {
1129 const $el = this.state.$el;
1130 const $prevParent = $el.parent();
1131 const $thisParent = $( this.el );
1132
1133 // Move $el into place.
1134 $thisParent.html( $el );
1135
1136 // Special case for reusable blocks.
1137 // Multiple instances of the same reusable block share the same block id.
1138 // This causes all instances to share the same state (cool), which unfortunately
1139 // pulls $el back and forth between the last rendered reusable block.
1140 // This simple fix leaves a "clone" behind :)
1141 if ( $prevParent.length && $prevParent[ 0 ] !== $thisParent[ 0 ] ) {
1142 $prevParent.html( $el.clone() );
1143 }
1144 }
1145
1146 // Lock block if required.
1147 if ( this.getValidationErrors() && this.isNotNewlyAdded() ) {
1148 this.lockBlockForSaving();
1149 } else {
1150 this.unlockBlockForSaving();
1151 }
1152
1153 // Call context specific method.
1154 switch ( context ) {
1155 case 'append':
1156 this.componentDidAppend();
1157 break;
1158 case 'remount':
1159 this.componentDidRemount();
1160 break;
1161 }
1162 }
1163
1164 validate() {
1165 // Do nothing.
1166 }
1167
1168 componentDidMount() {
1169 // Fetch on first load.
1170 if ( this.state.html === undefined ) {
1171 this.fetch();
1172
1173 // Or remount existing HTML.
1174 } else {
1175 this.display( 'remount' );
1176 }
1177 }
1178
1179 componentDidUpdate( prevProps, prevState ) {
1180 // HTML has changed.
1181 this.display( 'append' );
1182 }
1183
1184 componentDidAppend() {
1185 acf.doAction( 'append', this.state.$el );
1186 }
1187
1188 componentWillUnmount() {
1189 acf.doAction( 'unmount', this.state.$el );
1190
1191 // Unsubscribe this component from state.
1192 this.subscribed = false;
1193 }
1194
1195 componentDidRemount() {
1196 this.subscribed = true;
1197
1198 // Use setTimeout to avoid incorrect timing of events.
1199 // React will unmount and mount components in DOM order.
1200 // This means a new component can be mounted before an old one is unmounted.
1201 // ACF shares $el across new/old components which is un-React-like.
1202 // This timout ensures that unmounting occurs before remounting.
1203 setTimeout( () => {
1204 acf.doAction( 'remount', this.state.$el );
1205 } );
1206 }
1207
1208 componentWillMove() {
1209 acf.doAction( 'unmount', this.state.$el );
1210 setTimeout( () => {
1211 acf.doAction( 'remount', this.state.$el );
1212 } );
1213 }
1214
1215 isNotNewlyAdded() {
1216 return acf.blockInstances[ this.props.clientId ].has_been_deselected || false;
1217 }
1218
1219 hasShownValidation() {
1220 return acf.blockInstances[ this.props.clientId ].shown_validation || false;
1221 }
1222
1223 setShownValidation() {
1224 acf.blockInstances[ this.props.clientId ].shown_validation = true;
1225 }
1226
1227 setValidationErrors( errors ) {
1228 acf.blockInstances[ this.props.clientId ].validation_errors = errors;
1229 }
1230
1231 getValidationErrors() {
1232 return acf.blockInstances[ this.props.clientId ].validation_errors;
1233 }
1234
1235 getMode() {
1236 return acf.blockInstances[ this.props.clientId ].mode;
1237 }
1238
1239 lockBlockForSaving() {
1240 if ( ! wp.data.dispatch( 'core/editor' ) ) return;
1241 wp.data.dispatch( 'core/editor' ).lockPostSaving( 'acf/block/' + this.props.clientId );
1242 }
1243
1244 unlockBlockForSaving() {
1245 if ( ! wp.data.dispatch( 'core/editor' ) ) return;
1246 wp.data.dispatch( 'core/editor' ).unlockPostSaving( 'acf/block/' + this.props.clientId );
1247 }
1248
1249 displayValidation( $formEl ) {
1250 if ( ! blockSupportsValidation( this.props.name ) ) {
1251 acf.debug( 'Block does not support validation' );
1252 return;
1253 }
1254 if ( ! $formEl || $formEl.hasClass( 'acf-empty-block-fields' ) ) {
1255 acf.debug( 'There is no edit form available to validate.' );
1256 return;
1257 }
1258
1259 const errors = this.getValidationErrors();
1260 acf.debug( 'Starting handle validation', Object.assign( {}, this ), Object.assign( {}, $formEl ), errors );
1261
1262 this.setShownValidation();
1263
1264 let validator = acf.getBlockFormValidator( $formEl );
1265 validator.clearErrors();
1266
1267 acf.doAction( 'blocks/validation/pre_apply', errors );
1268 if ( errors ) {
1269 validator.addErrors( errors );
1270 validator.showErrors( 'after' );
1271
1272 this.lockBlockForSaving();
1273 } else {
1274 // remove previous error message
1275 if ( validator.has( 'notice' ) ) {
1276 validator.get( 'notice' ).update( {
1277 type: 'success',
1278 text: acf.__( 'Validation successful' ),
1279 timeout: 1000,
1280 } );
1281 validator.set( 'notice', null );
1282 }
1283
1284 this.unlockBlockForSaving();
1285 }
1286 acf.doAction( 'blocks/validation/post_apply', errors );
1287 }
1288 }
1289
1290 /**
1291 * BlockForm Class.
1292 *
1293 * A react componenet to handle the block form.
1294 *
1295 * @date 19/2/19
1296 * @since ACF 5.7.12
1297 *
1298 * @param string id the block id.
1299 * @return void
1300 */
1301 class BlockForm extends DynamicHTML {
1302 setup( props ) {
1303 this.id = `BlockForm-${ props.clientId }`;
1304 super.setup( props );
1305 }
1306
1307 fetch( validate_only = false, data = false ) {
1308 // Extract props.
1309 const { context, clientId, name } = this.props;
1310 let { attributes } = this.props;
1311
1312 let query = { form: true };
1313 if ( validate_only ) {
1314 query = { validate: true };
1315 attributes.data = data;
1316 }
1317
1318 const hash = createBlockAttributesHash( attributes, context );
1319
1320 acf.debug( 'BlockForm fetch', attributes, query );
1321
1322 // Try preloaded data first.
1323 const preloaded = this.maybePreload( hash, clientId, true );
1324
1325 if ( preloaded ) {
1326 this.setHtml( acf.applyFilters( 'blocks/form/render', preloaded.html, true ) );
1327 if ( preloaded.validation ) this.setValidationErrors( preloaded.validation.errors );
1328 return;
1329 }
1330
1331 if ( ! blockSupportsValidation( name ) ) {
1332 query.validate = false;
1333 }
1334
1335 // Request AJAX and update HTML on complete.
1336 fetchBlock( {
1337 attributes,
1338 context,
1339 clientId,
1340 query,
1341 } ).done( ( { data } ) => {
1342 acf.debug( 'fetch block form promise' );
1343
1344 if ( ! data ) {
1345 this.setHtml( `<div class="acf-block-fields acf-fields acf-empty-block-fields">${acf.__( 'Error loading block form' )}</div>` );
1346 return;
1347 }
1348
1349 if ( data.form ) {
1350 this.setHtml(
1351 acf.applyFilters( 'blocks/form/render', data.form.replaceAll( data.clientId, clientId ), false )
1352 );
1353 }
1354
1355 if ( data.validation ) this.setValidationErrors( data.validation.errors );
1356
1357 if ( this.isNotNewlyAdded() ) {
1358 acf.debug( "Block has already shown it's invalid. The form needs to show validation errors" );
1359 this.validate();
1360 }
1361 } );
1362 }
1363
1364 validate( loadState = true ) {
1365 if ( loadState ) {
1366 this.loadState();
1367 }
1368
1369 acf.debug( 'BlockForm calling validate with state', Object.assign( {}, this ) );
1370 super.displayValidation( this.state.$el );
1371 }
1372
1373 shouldComponentUpdate( nextProps, nextState ) {
1374 if (
1375 blockSupportsValidation( this.props.name ) &&
1376 this.state.$el &&
1377 this.isNotNewlyAdded() &&
1378 ! this.hasShownValidation()
1379 ) {
1380 this.validate( false ); // Shouldn't update state in shouldComponentUpdate.
1381 }
1382
1383 return super.shouldComponentUpdate( nextProps, nextState );
1384 }
1385
1386 componentWillUnmount() {
1387 super.componentWillUnmount();
1388
1389 //TODO: either delete this, or clear validations here (if that's a sensible idea)
1390
1391 acf.debug( 'BlockForm Component did unmount' );
1392 }
1393
1394 componentDidRemount() {
1395 super.componentDidRemount();
1396
1397 acf.debug( 'BlockForm component did remount' );
1398
1399 const { $el } = this.state;
1400
1401 if ( blockSupportsValidation( this.props.name ) && this.isNotNewlyAdded() ) {
1402 acf.debug( "Block has already shown it's invalid. The form needs to show validation errors" );
1403 this.validate();
1404 }
1405
1406 // Make sure our on append events are registered.
1407 if ( $el.data( 'acf-events-added' ) !== true ) {
1408 this.componentDidAppend();
1409 }
1410 }
1411
1412 componentDidAppend() {
1413 super.componentDidAppend();
1414
1415 acf.debug( 'BlockForm component did append' );
1416
1417 // Extract props.
1418 const { attributes, setAttributes, clientId, name } = this.props;
1419 const thisBlockForm = this;
1420 const { $el } = this.state;
1421
1422 // Callback for updating block data and validation status if we're in an edit only mode.
1423 function serializeData( silent = false ) {
1424 const data = acf.serialize( $el, `acf-block_${ clientId }` );
1425
1426 if ( silent ) {
1427 attributes.data = data;
1428 } else {
1429 setAttributes( {
1430 data,
1431 } );
1432 }
1433
1434 if ( blockSupportsValidation( name ) && ! silent && thisBlockForm.getMode() === 'edit' ) {
1435 acf.debug( 'No block preview currently available. Need to trigger a validation only fetch.' );
1436 thisBlockForm.fetch( true, data );
1437 }
1438 }
1439
1440 // Add events.
1441 let timeout = false;
1442 $el.on( 'change keyup', () => {
1443 clearTimeout( timeout );
1444 timeout = setTimeout( serializeData, 300 );
1445 } );
1446
1447 // Log initialization for remount check on the persistent element.
1448 $el.data( 'acf-events-added', true );
1449
1450 // Ensure newly added block is saved with data.
1451 // Do it silently to avoid triggering a preview render.
1452 if ( ! attributes.data ) {
1453 serializeData( true );
1454 }
1455 }
1456 }
1457
1458 /**
1459 * BlockPreview Class.
1460 *
1461 * A react componenet to handle the block preview.
1462 *
1463 * @date 19/2/19
1464 * @since ACF 5.7.12
1465 *
1466 * @param string id the block id.
1467 * @return void
1468 */
1469 class BlockPreview extends DynamicHTML {
1470 setup( props ) {
1471 const blockType = getBlockType( props.name );
1472 const contextPostId = acf.isget( this.props, 'context', 'postId' );
1473
1474 this.id = `BlockPreview-${ props.clientId }`;
1475
1476 super.setup( props );
1477
1478 // Apply the contextPostId to the ID if set to stop query loop ID duplication.
1479 if ( contextPostId ) {
1480 this.id = `BlockPreview-${ props.clientId }-${ contextPostId }`;
1481 }
1482
1483 if ( blockType.supports.jsx ) {
1484 this.renderMethod = 'jsx';
1485 }
1486 }
1487
1488 fetch( args = {} ) {
1489 const {
1490 attributes = this.props.attributes,
1491 clientId = this.props.clientId,
1492 context = this.props.context,
1493 delay = 0,
1494 } = args;
1495
1496 const { name } = this.props;
1497
1498 // Remember attributes used to fetch HTML.
1499 this.setState( {
1500 prevAttributes: attributes,
1501 prevContext: context,
1502 } );
1503
1504 const hash = createBlockAttributesHash( attributes, context );
1505
1506 // Try preloaded data first.
1507 let preloaded = this.maybePreload( hash, clientId, false );
1508
1509 if ( preloaded ) {
1510 if ( getBlockVersion( name ) == 1 ) {
1511 preloaded.html = '<div class="acf-block-preview">' + preloaded.html + '</div>';
1512 }
1513 this.setHtml( acf.applyFilters( 'blocks/preview/render', preloaded.html, true ) );
1514 if ( preloaded.validation ) this.setValidationErrors( preloaded.validation.errors );
1515 return;
1516 }
1517
1518 let query = { preview: true };
1519
1520 if ( ! blockSupportsValidation( name ) ) {
1521 query.validate = false;
1522 }
1523
1524 // Request AJAX and update HTML on complete.
1525 fetchBlock( {
1526 attributes,
1527 context,
1528 clientId,
1529 query,
1530 delay,
1531 } ).done( ( { data } ) => {
1532 if ( ! data ) {
1533 this.setHtml( `<div class="acf-block-fields acf-fields acf-empty-block-fields">${acf.__( 'Error previewing block' )}</div>` );
1534 return;
1535 }
1536
1537 let replaceHtml = data.preview.replaceAll( data.clientId, clientId );
1538 if ( getBlockVersion( name ) == 1 ) {
1539 replaceHtml = '<div class="acf-block-preview">' + replaceHtml + '</div>';
1540 }
1541 acf.debug( 'fetch block render promise' );
1542 this.setHtml( acf.applyFilters( 'blocks/preview/render', replaceHtml, false ) );
1543 if ( data.validation ) {
1544 this.setValidationErrors( data.validation.errors );
1545 }
1546 if ( this.isNotNewlyAdded() ) {
1547 this.validate();
1548 }
1549 } );
1550 }
1551
1552 validate() {
1553 // Check we've got a block form for this instance.
1554 const client = acf.blockInstances[ this.props.clientId ] || {};
1555 const blockFormState = client.BlockForm || false;
1556 if ( blockFormState ) {
1557 super.displayValidation( blockFormState.$el );
1558 }
1559 }
1560
1561 componentDidAppend() {
1562 super.componentDidAppend();
1563 this.renderBlockPreviewEvent();
1564 }
1565
1566 shouldComponentUpdate( nextProps, nextState ) {
1567 const nextAttributes = nextProps.attributes;
1568 const thisAttributes = this.props.attributes;
1569
1570 // Update preview if block data has changed.
1571 if (
1572 ! compareObjects( nextAttributes, thisAttributes ) ||
1573 ! compareObjects( nextProps.context, this.props.context )
1574 ) {
1575 let delay = 0;
1576
1577 // Delay fetch when editing className or anchor to simulate consistent logic to custom fields.
1578 if ( nextAttributes.className !== thisAttributes.className ) {
1579 delay = 300;
1580 }
1581 if ( nextAttributes.anchor !== thisAttributes.anchor ) {
1582 delay = 300;
1583 }
1584
1585 acf.debug( 'Triggering fetch from block preview shouldComponentUpdate' );
1586
1587 this.fetch( {
1588 attributes: nextAttributes,
1589 context: nextProps.context,
1590 delay,
1591 } );
1592 }
1593 return super.shouldComponentUpdate( nextProps, nextState );
1594 }
1595
1596 renderBlockPreviewEvent() {
1597 // Extract props.
1598 const { attributes, name } = this.props;
1599 const { $el, ref } = this.state;
1600 var blockElement;
1601
1602 // Generate action friendly type.
1603 const type = attributes.name.replace( 'acf/', '' );
1604
1605 if ( ref && ref.current ) {
1606 // We've got a react ref from a JSX container. Use the parent as the blockElement
1607 blockElement = $( ref.current ).parent();
1608 } else if ( getBlockVersion( name ) == 1 ) {
1609 blockElement = $el;
1610 } else {
1611 blockElement = $el.parents( '.acf-block-preview' );
1612 }
1613
1614 // Do action.
1615 acf.doAction( 'render_block_preview', blockElement, attributes );
1616 acf.doAction( `render_block_preview/type=${ type }`, blockElement, attributes );
1617 }
1618
1619 componentDidRemount() {
1620 super.componentDidRemount();
1621
1622 acf.debug(
1623 'Checking if fetch is required in BlockPreview componentDidRemount',
1624 Object.assign( {}, this.state.prevAttributes ),
1625 Object.assign( {}, this.props.attributes ),
1626 Object.assign( {}, this.state.prevContext ),
1627 Object.assign( {}, this.props.context )
1628 );
1629
1630 // Update preview if data has changed since last render (changing from "edit" to "preview").
1631 if (
1632 ! compareObjects( this.state.prevAttributes, this.props.attributes ) ||
1633 ! compareObjects( this.state.prevContext, this.props.context )
1634 ) {
1635 acf.debug( 'Triggering block preview fetch from componentDidRemount' );
1636 this.fetch();
1637 }
1638
1639 // Fire the block preview event so blocks can reinit JS elements.
1640 // React reusing DOM elements covers any potential race condition from the above fetch.
1641 this.renderBlockPreviewEvent();
1642 }
1643 }
1644
1645 /**
1646 * Initializes ACF Blocks logic and registration.
1647 *
1648 * @since ACF 5.9.0
1649 */
1650 function initialize() {
1651 // Add support for WordPress versions before 5.2.
1652 if ( ! wp.blockEditor ) {
1653 wp.blockEditor = wp.editor;
1654 }
1655
1656 // Register block types.
1657 const blockTypes = acf.get( 'blockTypes' );
1658 if ( blockTypes ) {
1659 blockTypes.map( registerBlockType );
1660 }
1661 }
1662
1663 // Run the initialize callback during the "prepare" action.
1664 // This ensures that all localized data is available and that blocks are registered before the WP editor has been instantiated.
1665 acf.addAction( 'prepare', initialize );
1666
1667 /**
1668 * Returns a valid vertical alignment.
1669 *
1670 * @date 07/08/2020
1671 * @since ACF 5.9.0
1672 *
1673 * @param string align A vertical alignment.
1674 * @return string
1675 */
1676 function validateVerticalAlignment( align ) {
1677 const ALIGNMENTS = [ 'top', 'center', 'bottom' ];
1678 const DEFAULT = 'top';
1679 return ALIGNMENTS.includes( align ) ? align : DEFAULT;
1680 }
1681
1682 /**
1683 * Returns a valid horizontal alignment.
1684 *
1685 * @date 07/08/2020
1686 * @since ACF 5.9.0
1687 *
1688 * @param string align A horizontal alignment.
1689 * @return string
1690 */
1691 function validateHorizontalAlignment( align ) {
1692 const ALIGNMENTS = [ 'left', 'center', 'right' ];
1693 const DEFAULT = acf.get( 'rtl' ) ? 'right' : 'left';
1694 return ALIGNMENTS.includes( align ) ? align : DEFAULT;
1695 }
1696
1697 /**
1698 * Returns a valid matrix alignment.
1699 *
1700 * Written for "upgrade-path" compatibility from vertical alignment to matrix alignment.
1701 *
1702 * @date 07/08/2020
1703 * @since ACF 5.9.0
1704 *
1705 * @param string align A matrix alignment.
1706 * @return string
1707 */
1708 function validateMatrixAlignment( align ) {
1709 const DEFAULT = 'center center';
1710 if ( align ) {
1711 const [ y, x ] = align.split( ' ' );
1712 return `${ validateVerticalAlignment( y ) } ${ validateHorizontalAlignment( x ) }`;
1713 }
1714 return DEFAULT;
1715 }
1716
1717 /**
1718 * A higher order component adding alignContent editing functionality.
1719 *
1720 * @date 08/07/2020
1721 * @since ACF 5.9.0
1722 *
1723 * @param component OriginalBlockEdit The original BlockEdit component.
1724 * @param object blockType The block type settings.
1725 * @return component
1726 */
1727 function withAlignContentComponent( OriginalBlockEdit, blockType ) {
1728 // Determine alignment vars
1729 let type = blockType.supports.align_content || blockType.supports.alignContent;
1730 let AlignmentComponent;
1731 let validateAlignment;
1732 switch ( type ) {
1733 case 'matrix':
1734 AlignmentComponent = BlockAlignmentMatrixControl || BlockAlignmentMatrixToolbar;
1735 validateAlignment = validateMatrixAlignment;
1736 break;
1737 default:
1738 AlignmentComponent = BlockVerticalAlignmentToolbar;
1739 validateAlignment = validateVerticalAlignment;
1740 break;
1741 }
1742
1743 // Ensure alignment component exists.
1744 if ( AlignmentComponent === undefined ) {
1745 console.warn( `The "${ type }" alignment component was not found.` );
1746 return OriginalBlockEdit;
1747 }
1748
1749 // Ensure correct block attribute data is sent in intial preview AJAX request.
1750 blockType.alignContent = validateAlignment( blockType.alignContent );
1751
1752 // Return wrapped component.
1753 return class WrappedBlockEdit extends Component {
1754 render() {
1755 const { attributes, setAttributes } = this.props;
1756 const { alignContent } = attributes;
1757 function onChangeAlignContent( alignContent ) {
1758 setAttributes( {
1759 alignContent: validateAlignment( alignContent ),
1760 } );
1761 }
1762 return (
1763 <Fragment>
1764 <BlockControls group="block">
1765 <AlignmentComponent
1766 label={ acf.__( 'Change content alignment' ) }
1767 value={ validateAlignment( alignContent ) }
1768 onChange={ onChangeAlignContent }
1769 />
1770 </BlockControls>
1771 <OriginalBlockEdit { ...this.props } />
1772 </Fragment>
1773 );
1774 }
1775 };
1776 }
1777
1778 /**
1779 * A higher order component adding alignText editing functionality.
1780 *
1781 * @date 08/07/2020
1782 * @since ACF 5.9.0
1783 *
1784 * @param component OriginalBlockEdit The original BlockEdit component.
1785 * @param object blockType The block type settings.
1786 * @return component
1787 */
1788 function withAlignTextComponent( OriginalBlockEdit, blockType ) {
1789 const validateAlignment = validateHorizontalAlignment;
1790
1791 // Ensure correct block attribute data is sent in intial preview AJAX request.
1792 blockType.alignText = validateAlignment( blockType.alignText );
1793
1794 // Return wrapped component.
1795 return class WrappedBlockEdit extends Component {
1796 render() {
1797 const { attributes, setAttributes } = this.props;
1798 const { alignText } = attributes;
1799
1800 function onChangeAlignText( alignText ) {
1801 setAttributes( {
1802 alignText: validateAlignment( alignText ),
1803 } );
1804 }
1805
1806 return (
1807 <Fragment>
1808 <BlockControls group="block">
1809 <AlignmentToolbar value={ validateAlignment( alignText ) } onChange={ onChangeAlignText } />
1810 </BlockControls>
1811 <OriginalBlockEdit { ...this.props } />
1812 </Fragment>
1813 );
1814 }
1815 };
1816 }
1817
1818 /**
1819 * A higher order component adding full height support.
1820 *
1821 * @date 19/07/2021
1822 * @since ACF 5.10.0
1823 *
1824 * @param component OriginalBlockEdit The original BlockEdit component.
1825 * @param object blockType The block type settings.
1826 * @return component
1827 */
1828 function withFullHeightComponent( OriginalBlockEdit, blockType ) {
1829 if ( ! BlockFullHeightAlignmentControl ) return OriginalBlockEdit;
1830
1831 // Return wrapped component.
1832 return class WrappedBlockEdit extends Component {
1833 render() {
1834 const { attributes, setAttributes } = this.props;
1835 const { fullHeight } = attributes;
1836
1837 function onToggleFullHeight( fullHeight ) {
1838 setAttributes( {
1839 fullHeight,
1840 } );
1841 }
1842
1843 return (
1844 <Fragment>
1845 <BlockControls group="block">
1846 <BlockFullHeightAlignmentControl isActive={ fullHeight } onToggle={ onToggleFullHeight } />
1847 </BlockControls>
1848 <OriginalBlockEdit { ...this.props } />
1849 </Fragment>
1850 );
1851 }
1852 };
1853 }
1854
1855 /**
1856 * Appends a backwards compatibility attribute for conversion.
1857 *
1858 * @since ACF 6.0
1859 *
1860 * @param object attributes The block type attributes.
1861 * @return object
1862 */
1863 function addBackCompatAttribute( attributes, new_attribute, type ) {
1864 attributes[ new_attribute ] = {
1865 type: type,
1866 };
1867 return attributes;
1868 }
1869
1870 /**
1871 * Create a block hash from attributes
1872 *
1873 * @since ACF 6.0
1874 *
1875 * @param object attributes The block type attributes.
1876 * @param object context The current block context object.
1877 * @return string
1878 */
1879 function createBlockAttributesHash( attributes, context ) {
1880 attributes[ '_acf_context' ] = sortObjectByKey( context );
1881 return md5( JSON.stringify( sortObjectByKey( attributes ) ) );
1882 }
1883
1884 /**
1885 * Key sort an object
1886 *
1887 * @since ACF 6.3.1
1888 *
1889 * @param object toSort The object to be sorted
1890 * @return object
1891 */
1892 function sortObjectByKey( toSort ) {
1893 return Object.keys( toSort )
1894 .sort()
1895 .reduce( ( acc, currValue ) => {
1896 acc[ currValue ] = toSort[ currValue ];
1897 return acc;
1898 }, {} );
1899 }
1900 } )( jQuery );
1901