PluginProbe
Content Blocks Builder – Create blocks, repeater blocks with carousel, grid, popup layouts / 2.8.13
Content Blocks Builder – Create blocks, repeater blocks with carousel, grid, popup layouts v2.8.13
2.8.14 2.8.13 2.8.12 2.7.5 2.7.6 2.7.7 2.7.8 2.7.9 2.8.0 2.8.1 2.8.10 2.8.11 2.8.2 2.8.3 2.8.4 2.8.5 2.8.6 2.8.7 2.8.8 2.8.9 trunk 1.0.1 1.0.2 1.1.0 1.1.1 All 147 releases
content-blocks-builder / includes / custom-blocks.php

custom-blocks.php in Content Blocks Builder – Create blocks, repeater blocks with carousel, grid, popup layouts 2.8.13, at includes/custom-blocks.php

2,720 lines 82.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Register custom blocks
4 *
5 * @package BoldBlocks
6 * @author Phi Phan <mrphipv@gmail.com>
7 * @copyright Copyright (c) 2022, Phi Phan
8 */
9
10 namespace BoldBlocks;
11
12 // Exit if accessed directly.
13 defined( 'ABSPATH' ) || exit;
14
15 if ( ! class_exists( CustomBlocks::class ) ) :
16 /**
17 * Create/edit custom content blocks.
18 */
19 class CustomBlocks extends CoreComponent {
20 /**
21 * Post type
22 *
23 * @var string
24 */
25 protected $post_type = 'boldblocks_block';
26
27 /**
28 * Post type helper instance
29 *
30 * @var PostType
31 */
32 protected $post_type_helper;
33
34 /**
35 * Store all custom blocks
36 *
37 * @var array
38 */
39 protected $all_custom_blocks = [];
40
41 /**
42 * Store all names of custom blocks
43 *
44 * @var array
45 */
46 protected $custom_blocks = [];
47
48 /**
49 * Store all names of repeater blocks
50 *
51 * @var array
52 */
53 protected $repeater_blocks = [];
54
55 /**
56 * Max items that can query
57 *
58 * @var integer
59 */
60 protected $max_items = 200;
61
62 /**
63 * The handle for custom blocks scripts
64 *
65 * @var string
66 */
67 public $custom_blocks_handle = 'boldblocks-custom-blocks';
68
69 /**
70 * The handle for custom blocks frontend scripts
71 *
72 * @var string
73 */
74 public $custom_blocks_frontend_handle = 'boldblocks-custom-blocks-frontend';
75
76 /**
77 * The handle for custom blocks editor scripts
78 *
79 * @var string
80 */
81 public $custom_blocks_editor_handle = 'boldblocks-custom-blocks-editor';
82
83 /**
84 * The handle for carousel blocks frontend scripts
85 *
86 * @var string
87 */
88 public $carousel_blocks_frontend_handle = 'boldblocks-carousel-frontend';
89
90 /**
91 * Store preview style for html wrapper blocks
92 *
93 * @var string
94 */
95 private $html_editor_style = '';
96
97 /**
98 * Constructor
99 *
100 * @param ContentBlocksBuilder $the_plugin_instance
101 */
102 public function __construct( $the_plugin_instance ) {
103 parent::__construct( $the_plugin_instance );
104
105 $this->post_type_helper = PostType::get_instance();
106 }
107
108 /**
109 * Run main hooks
110 *
111 * @return void
112 */
113 public function run() {
114 // Register custom post type.
115 add_action( 'init', [ $this, 'register_custom_post_type' ], 5 );
116
117 // Custom admin columns.
118 add_filter( 'manage_edit-' . $this->post_type . '_columns', [ $this, 'add_block_custom_columns' ] );
119 add_action( 'manage_' . $this->post_type . '_posts_custom_column', [ $this, 'manage_block_columns' ], 10, 2 );
120 add_action( 'admin_head', [ $this, 'add_block_custom_columns_style' ] );
121
122 // Register scripts, make sure it is placed before load_custom_blocks.
123 add_action( 'init', [ $this, 'register_scripts' ] );
124
125 // Load custom blocks.
126 add_action( 'init', [ $this, 'load_custom_blocks' ] );
127
128 // Allow creating patterns from custom blocks.
129 add_filter( 'boldblocks_get_pattern_allowed_blocks', [ $this, 'allow_creating_patterns' ] );
130
131 // Add meta fields to meta revisioning.
132 add_filter( 'boldblocks_post_revision_meta_keys', [ $this, 'add_fields_to_revision_meta_keys' ] );
133
134 // Add theme name to body class.
135 add_filter( 'body_class', [ $this, 'add_body_class' ] );
136
137 // Add block version to custom blocks.
138 add_action( 'save_post_' . $this->post_type, [ $this, 'save_version_to_block' ], 10, 3 );
139
140 // Prevent users from deleting the core blocks.
141 add_filter( 'user_has_cap', [ $this, 'prevent_core_blocks_from_deletion' ], 10, 3 );
142
143 // Change the placeholder text.
144 add_filter( 'enter_title_here', [ $this, 'enter_title_here' ] );
145
146 // Register codemirror.
147 add_action( 'admin_enqueue_scripts', [ $this, 'register_code_editor_scripts' ] );
148
149 // Change the script src to use new version of esprima.
150 add_filter( 'script_loader_src', [ $this, 'adjust_esprima_script' ], 10, 2 );
151
152 // Handle save post.
153 add_action( 'save_post_' . $this->post_type, [ $this, 'save_post' ], 10, 2 );
154
155 // Clear the transient cache on upgraded.
156 add_action( 'cbb/version_upgraded', [ $this, 'clear_transient_cache' ] );
157
158 // Enqueue frontend scripts for non-cbb blocks.
159 add_filter( 'render_block', [ $this, 'enqueue_frontend_block_scripts' ], 10, 3 );
160
161 // Enqueue frontend scripts for carousel layout.
162 add_filter( 'render_block', [ $this, 'enqueue_frontend_carousel_scripts' ], 10, 3 );
163
164 // Register rest fields.
165 add_action( 'rest_api_init', [ $this, 'register_rest_fields' ] );
166
167 // Render inline script module.
168 add_filter( 'wp_inline_script_attributes', [ $this, 'add_inline_script_module' ] );
169
170 // Hide on the frontend.
171 add_filter( 'render_block', [ $this, 'hide_on_frontend' ], 10, 3 );
172
173 // Enqueue custom scripts & styles for custom blocks.
174 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_custom_block_scripts' ] );
175
176 // Customize the Query Loop block.
177 add_action( 'init', [ $this, 'query_loop_extended_filters_and_sorting' ], PHP_INT_MAX );
178
179 // Enqueue styles for the html preview iframe.
180 add_filter( 'block_editor_settings_all', [ $this, 'enqueue_html_style_for_the_editor' ] );
181
182 // Strip all cbb meta fields.
183 add_filter( 'postmeta_form_keys', [ $this, 'strip_cbb_meta_keys' ] );
184
185 // Force to remove custom fields.
186 add_action( 'add_meta_boxes', [ $this, 'remove_meta_boxes' ] );
187 }
188
189 /**
190 * Register custom post type
191 *
192 * @return void
193 */
194 public function register_custom_post_type() {
195 // Register block type.
196 $this->post_type_helper->register_post_type(
197 $this->post_type,
198 [
199 'rest_base' => 'boldblocks-blocks',
200 'menu_icon' => 'dashicons-block-default',
201 'menu_position' => 80, // Right after Settings.
202 'capabilities' => [
203 'read_post' => 'manage_options',
204 'edit_post' => 'manage_options',
205 'delete_post' => 'manage_options',
206 'edit_posts' => 'manage_options',
207 'delete_posts' => 'manage_options',
208 ],
209 ],
210 [
211 'name' => _x( 'Custom Blocks', 'post type general name', 'content-blocks-builder' ),
212 'singular_name' => _x( 'Custom Block', 'post type singular name', 'content-blocks-builder' ),
213 'name_admin_bar' => _x( 'Block', 'add new on admin bar', 'content-blocks-builder' ),
214 'all_items' => __( 'All Blocks', 'content-blocks-builder' ),
215 'add_new' => __( 'Add New Block', 'content-blocks-builder' ),
216 'add_new_item' => __( 'Add New Block', 'content-blocks-builder' ),
217 ]
218 );
219
220 // Block keywords.
221 $this->post_type_helper->register_taxonomy(
222 'boldblocks_block_keywords',
223 $this->post_type,
224 [],
225 [
226 'name' => _x( 'Keywords', 'Taxonomy General Name', 'content-blocks-builder' ),
227 'singular_name' => _x( 'Keyword', 'Taxonomy Singular Name', 'content-blocks-builder' ),
228 ]
229 );
230
231 // Meta fields.
232 $this->register_meta(
233 'boldblocks_block_blocks',
234 [
235 'default' => '[]',
236 ]
237 );
238
239 $this->register_meta(
240 'boldblocks_block_dependent_blocks',
241 [
242 'show_in_rest' => array(
243 'schema' => array(
244 'items' => array(
245 'type' => 'string',
246 ),
247 ),
248 ),
249 'type' => 'array',
250 'default' => [],
251 ]
252 );
253
254 $this->register_meta( 'boldblocks_block_class' );
255
256 $this->register_meta( 'boldblocks_block_description' );
257
258 $this->register_meta( 'boldblocks_block_icon' );
259
260 $this->register_meta(
261 'boldblocks_block_supports',
262 [
263 'show_in_rest' => [
264 'schema' => [
265 'type' => 'object',
266 'additionalProperties' => array(
267 'type' => [ 'boolean' ], // can be ['boolean', 'object'...].
268 ),
269 ],
270 ],
271 'type' => 'object',
272 ]
273 );
274
275 $this->register_meta(
276 'boldblocks_template_lock',
277 [
278 'default' => 'false',
279 ]
280 );
281
282 $this->register_meta(
283 'boldblocks_enable_variation_picker',
284 [
285 'type' => 'boolean',
286 'default' => false,
287 ]
288 );
289
290 $this->register_meta(
291 'boldblocks_enable_repeater',
292 [
293 'type' => 'boolean',
294 'default' => false,
295 ]
296 );
297
298 $this->register_meta(
299 'boldblocks_parent_layout_type',
300 [
301 'default' => 'vstack',
302 ]
303 );
304
305 $this->register_meta( 'boldblocks_parent_block_icon' );
306
307 $this->register_meta(
308 'boldblocks_parent_block_supports',
309 [
310 'show_in_rest' => [
311 'schema' => [
312 'type' => 'object',
313 'additionalProperties' => array(
314 'type' => [ 'boolean' ], // can be ['boolean', 'object'...].
315 ),
316 ],
317 ],
318 'type' => 'object',
319 ]
320 );
321
322 $this->register_meta(
323 'boldblocks_parent_enable_variation_picker',
324 [
325 'type' => 'boolean',
326 'default' => false,
327 ]
328 );
329
330 $this->register_meta(
331 'boldblocks_disable_standalone',
332 [
333 'type' => 'boolean',
334 'default' => false,
335 ]
336 );
337
338 $this->register_meta(
339 'boldblocks_is_fixed_nested_item_count',
340 [
341 'type' => 'boolean',
342 'default' => false,
343 ]
344 );
345
346 $this->register_meta(
347 'boldblocks_nested_item_count',
348 [
349 'type' => 'integer',
350 'default' => 1,
351 ]
352 );
353
354 $this->register_meta( 'boldblocks_parent_block_name' );
355
356 $this->register_meta( 'boldblocks_parent_block_title' );
357
358 $this->register_meta( 'boldblocks_parent_block_description' );
359
360 $this->register_meta( 'boldblocks_parent_block_class' );
361
362 $this->register_meta( 'boldblocks_block_version' );
363
364 $this->register_meta(
365 'boldblocks_block_dependencies',
366 [
367 'type' => 'array',
368 'show_in_rest' => array(
369 'schema' => array(
370 'items' => array(
371 'type' => 'object',
372 'properties' => array(
373 'slug' => array(
374 'type' => 'string',
375 ),
376 'name' => array(
377 'type' => 'string',
378 ),
379 ),
380 ),
381 ),
382 ),
383 'default' => [],
384 ]
385 );
386
387 $this->register_meta(
388 'boldblocks_block_external_scripts',
389 [
390 'type' => 'array',
391 'show_in_rest' => array(
392 'schema' => array(
393 'items' => array(
394 'type' => 'object',
395 'properties' => array(
396 'handle' => array(
397 'type' => 'string',
398 'default' => '',
399 ),
400 'src' => array(
401 'type' => 'string',
402 'default' => '',
403 ),
404 'deps' => array(
405 'type' => 'string',
406 'default' => '',
407 ),
408 'version' => array(
409 'type' => 'string',
410 'default' => '',
411 ),
412 'in_footer' => array(
413 'type' => 'boolean',
414 'default' => false,
415 ),
416 'loading_strategy' => array(
417 'type' => 'string',
418 'default' => 'none',
419 ),
420 'in_backend' => array(
421 'type' => 'boolean',
422 'default' => false,
423 ),
424 ),
425 ),
426 ),
427 ),
428 'default' => [],
429 ]
430 );
431
432 $this->register_meta(
433 'boldblocks_block_custom_scripts',
434 [
435 'type' => 'array',
436 'show_in_rest' => array(
437 'schema' => array(
438 'items' => array(
439 'type' => 'object',
440 'properties' => array(
441 'value' => array(
442 'type' => 'string',
443 ),
444 'script_type' => array(
445 'type' => 'string',
446 'default' => '', // 'module', 'custom', 'default'.
447 ),
448 'handle' => array(
449 'type' => 'string',
450 ),
451 // is_module is deprecated. it will be removed in the future.
452 'is_module' => array(
453 'type' => 'boolean',
454 'default' => false,
455 ),
456 'deps' => array(
457 'type' => 'string',
458 'default' => '',
459 ),
460 ),
461 ),
462 ),
463 ),
464 'default' => [],
465 ]
466 );
467
468 $this->register_meta(
469 'boldblocks_block_external_styles',
470 [
471 'type' => 'array',
472 'show_in_rest' => array(
473 'schema' => array(
474 'items' => array(
475 'type' => 'object',
476 'properties' => array(
477 'handle' => array(
478 'type' => 'string',
479 'default' => '',
480 ),
481 'src' => array(
482 'type' => 'string',
483 'default' => '',
484 ),
485 'deps' => array(
486 'type' => 'string',
487 'default' => '',
488 ),
489 'version' => array(
490 'type' => 'string',
491 'default' => '',
492 ),
493 'media' => array(
494 'type' => 'string',
495 'default' => 'all',
496 ),
497 'in_backend' => array(
498 'type' => 'boolean',
499 'default' => false,
500 ),
501 ),
502 ),
503 ),
504 ),
505 'default' => [],
506 ]
507 );
508
509 $this->register_meta(
510 'boldblocks_block_custom_styles',
511 [
512 'type' => 'array',
513 'show_in_rest' => array(
514 'schema' => array(
515 'items' => array(
516 'type' => 'object',
517 'properties' => array(
518 'handle' => array(
519 'type' => 'string',
520 ),
521 'type' => array(
522 'type' => 'string',
523 ),
524 'value' => array(
525 'type' => 'string',
526 ),
527 ),
528 ),
529 ),
530 ),
531 'default' => [],
532 ]
533 );
534
535 $this->register_meta(
536 'boldblocks_block_attributes',
537 [
538 'type' => 'array',
539 'show_in_rest' => array(
540 'schema' => array(
541 'items' => array(
542 'type' => 'object',
543 'properties' => array(
544 'name' => array(
545 'type' => 'string',
546 ),
547 'type' => array(
548 'type' => 'string',
549 ),
550 ),
551 'additionalProperties' => array(
552 'type' => [ 'integer', 'number', 'boolean', 'string', 'object', 'array' ], // The order of data type matters.
553 ),
554 ),
555 ),
556 ),
557 'default' => [],
558 ]
559 );
560
561 $this->register_meta(
562 'boldblocks_parent_block_attributes',
563 [
564 'type' => 'array',
565 'show_in_rest' => array(
566 'schema' => array(
567 'items' => array(
568 'type' => 'object',
569 'properties' => array(
570 'name' => array(
571 'type' => 'string',
572 ),
573 'type' => array(
574 'type' => 'string',
575 ),
576 ),
577 'additionalProperties' => array(
578 'type' => [ 'integer', 'number', 'boolean', 'string', 'object', 'array' ],
579 ),
580 ),
581 ),
582 ),
583 'default' => [],
584 ]
585 );
586
587 $this->register_meta(
588 'boldblocks_enable_custom_attributes',
589 [
590 'type' => 'boolean',
591 'default' => false,
592 ]
593 );
594
595 $this->register_meta(
596 'boldblocks_parent_enable_custom_attributes',
597 [
598 'type' => 'boolean',
599 'default' => false,
600 ]
601 );
602
603 $this->register_meta(
604 'boldblocks_hide_on_frontend',
605 [
606 'type' => 'boolean',
607 'default' => false,
608 ]
609 );
610
611 $this->register_meta(
612 'boldblocks_is_readonly',
613 [
614 'type' => 'boolean',
615 'default' => false,
616 ]
617 );
618
619 $this->register_meta(
620 'boldblocks_is_transformable',
621 [
622 'type' => 'boolean',
623 'default' => false,
624 ]
625 );
626
627 $this->register_meta(
628 'boldblocks_is_hidden',
629 [
630 'type' => 'boolean',
631 'default' => false,
632 ]
633 );
634
635 $this->register_meta( 'boldblocks_block_parent' );
636
637 $this->register_meta( 'boldblocks_block_ancestor' );
638
639 $this->register_meta( 'boldblocks_block_allowed_blocks' );
640 }
641
642 /**
643 * Register meta field
644 *
645 * @param string $meta_key
646 * @param array $args
647 * @return void
648 */
649 private function register_meta( $meta_key, $args = [] ) {
650 $this->post_type_helper->register_meta( $this->post_type, $meta_key, $args );
651 }
652
653 /**
654 * Don't show meta fields created CBB
655 *
656 * @param array $keys
657 * @return array
658 */
659 public function strip_cbb_meta_keys( $keys ) {
660 global $wpdb;
661
662 /**
663 * Filters the number of custom fields to retrieve for the drop-down
664 * in the Custom Fields meta box.
665 *
666 * @since 2.1.0
667 *
668 * @param int $limit Number of custom fields to retrieve. Default 30.
669 */
670 $limit = apply_filters( 'postmeta_form_limit', 30 );
671
672 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
673 $keys = $wpdb->get_col(
674 $wpdb->prepare(
675 "SELECT DISTINCT meta_key
676 FROM $wpdb->postmeta
677 WHERE meta_key NOT BETWEEN '_' AND '_z'
678 AND meta_key NOT LIKE %s
679 HAVING meta_key NOT LIKE %s
680 ORDER BY meta_key
681 LIMIT %d",
682 $wpdb->esc_like( 'boldblocks_' ) . '%',
683 $wpdb->esc_like( '_' ) . '%',
684 $limit
685 )
686 );
687
688 return $keys;
689 }
690
691 /**
692 * Remove custom meta boxes
693 *
694 * @param string $post_type
695 * @return void
696 */
697 public function remove_meta_boxes( $post_type ) {
698 if ( $post_type === $this->post_type ) {
699 remove_meta_box( 'postcustom', false, 'normal' );
700 }
701 }
702
703 /**
704 * Load all custom blocks
705 *
706 * @return void
707 */
708 public function load_custom_blocks() {
709 // Load all custom blocks.
710 $all_custom_blocks = $this->get_all_custom_blocks();
711
712 // Build an array of custom block name.
713 $this->custom_blocks = array_column( $all_custom_blocks, null, 'name' );
714
715 // Build an array of repeater blocks.
716 $this->repeater_blocks = array_filter(
717 array_map(
718 function ( $block_args ) {
719 return isset( $block_args['parent'] ) ? $block_args['parent'] : false;
720 },
721 $all_custom_blocks
722 )
723 );
724
725 // Make name as the key.
726 $this->repeater_blocks = array_column( $this->repeater_blocks, null, 'name' );
727
728 // Get current post.
729 $post = $this->get_current_post();
730 $block_name = '';
731 $repeater_block_name = '';
732
733 if ( $post && $this->post_type === $post->post_type ) {
734 $block_name = sprintf( 'boldblocks/%s', $this->sanitize_block_name( $post->post_name ) );
735
736 $enable_repeater = get_post_meta( $post->ID, 'boldblocks_enable_repeater', true );
737 if ( $enable_repeater ) {
738 $parent_name = get_post_meta( $post->ID, 'boldblocks_parent_block_name', true );
739 if ( ! $parent_name ) {
740 $parent_name = $post->post_name;
741 }
742 $repeater_block_name = sprintf( 'boldblocks/%s-repeater', $this->sanitize_block_name( $parent_name ) );
743 }
744 }
745
746 // Register all blocks.
747 foreach ( array_merge( $this->custom_blocks, $this->repeater_blocks ) as $name => $args ) {
748 // Build block attributes.
749 $block_atts = [
750 'api_version' => 3,
751 'supports' => [
752 'cbb' => true,
753 'layout' => true,
754 'migrateClass' => ! empty( $args['migrateClass'] ) ? ( isset( $args['id'] ) ? 'wp-block-boldblocks-custom' : 'wp-block-boldblocks-custom-parent' ) : '',
755 ],
756 'uses_context' => [ 'postId', 'postType', 'cbb/block-overrides' ],
757 ];
758
759 if ( $args['blockSupports']['background'] ?? false ) {
760 // New duotone in WP 6.3.
761 if ( ! ( $block_atts['supports']['filter'] ?? false ) ) {
762 $block_atts['supports']['filter'] = [];
763 }
764 $block_atts['supports']['filter']['duotone'] = true;
765
766 if ( ! ( $block_atts['selectors'] ?? false ) ) {
767 $block_atts['selectors'] = [];
768 }
769
770 if ( ! ( $block_atts['selectors']['filter'] ?? false ) ) {
771 $block_atts['selectors']['filter'] = [];
772 }
773
774 $block_atts['selectors']['filter']['duotone'] = '> .bb\\:block-background';
775 }
776
777 if ( ! isset( $args['parent'] ) ) {
778 if ( $args['blockParent'] ?? false ) {
779 $block_parent = $args['blockParent'];
780 if ( $block_name ) {
781 $index = array_search( $block_name, $block_parent, true );
782 if ( $index !== false ) {
783 unset( $block_parent[ $index ] );
784 $block_parent = array_values( $block_parent );
785 }
786 }
787
788 if ( $repeater_block_name ) {
789 $index = array_search( $repeater_block_name, $block_parent, true );
790 if ( $index !== false ) {
791 unset( $block_parent[ $index ] );
792 $block_parent = array_values( $block_parent );
793 }
794 }
795
796 if ( $block_parent ) {
797 $block_atts['parent'] = $block_parent;
798 }
799 }
800
801 if ( $args['blockAncestor'] ?? false ) {
802 $block_ancestor = $args['blockAncestor'];
803 if ( $block_name ) {
804 $index = array_search( $block_name, $block_ancestor, true );
805 if ( $index !== false ) {
806 unset( $block_ancestor[ $index ] );
807 $block_ancestor = array_values( $block_ancestor );
808 }
809 }
810
811 if ( $repeater_block_name ) {
812 $index = array_search( $repeater_block_name, $block_ancestor, true );
813 if ( $index !== false ) {
814 unset( $block_ancestor[ $index ] );
815 $block_ancestor = array_values( $block_ancestor );
816 }
817 }
818
819 if ( $block_ancestor ) {
820 $block_atts['ancestor'] = $block_ancestor;
821 }
822 }
823 }
824
825 // Styles and scripts.
826 $handles = [
827 'style_handles' => [ $this->custom_blocks_handle ],
828 'editor_style_handles' => [ $this->custom_blocks_editor_handle ],
829 'script_handles' => [],
830 'editor_script_handles' => [ $this->custom_blocks_handle ],
831 'view_script_handles' => [ $this->custom_blocks_frontend_handle ],
832 ];
833
834 $parent_layout_type = $args['layoutType'] ?? '';
835
836 if ( empty( $parent_layout_type ) && ! isset( $args['parent'] ) ) {
837 $block_atts['supports']['layoutType'] = 'standalone';
838 } else {
839 $block_layout_type = '';
840 if ( $parent_layout_type ) {
841 // Is parent block.
842 $block_layout_type = $parent_layout_type;
843 } else {
844 // Is child block.
845 $parent_layout = $args['parent']['layoutType'] ?? '';
846 $block_layout_type = $parent_layout . 'Item';
847 }
848 $block_atts['supports']['layoutType'] = $block_layout_type;
849 }
850
851 // Don't allow layout on repeater block except vstack layout.
852 if ( $parent_layout_type && 'vstack' !== $parent_layout_type ) {
853 $block_atts['supports']['layout'] = false;
854 }
855
856 // Don't allow layout on accordion item.
857 if ( 'accordion' === ( $args['parent']['layoutType'] ?? '' ) ) {
858 $block_atts['supports']['layout'] = false;
859 }
860
861 // Enqueue scripts for specific block types.
862 if ( 'carousel' === $parent_layout_type ) {
863 // Mark it as carousel item block.
864 $block_atts['supports']['parentLayoutType'] = 'carousel';
865
866 $handles['style_handles'][] = $this->carousel_blocks_frontend_handle;
867 $handles['script_handles'][] = $this->carousel_blocks_frontend_handle;
868 }
869
870 // Scripts & styles.
871 $is_backend = is_admin();
872 $name_handle = str_replace( '/', '-', $name );
873 $block_inline_handle = 'cbb-' . $name_handle;
874 $block_module_inline_handle = 'cbb-script-module-' . $name_handle;
875
876 if ( ! empty( $args['external_scripts'] ) ) {
877 $external_scripts = array_filter(
878 $args['external_scripts'],
879 function ( $script ) {
880 return $script['handle'] ?? false;
881 }
882 );
883
884 foreach ( $external_scripts as $script ) {
885 if ( $script['deps'] ?? false ) {
886 $deps = $this->refine_script_handle( $script['deps'], $is_backend );
887 $deps = \explode( ',', $deps );
888 } else {
889 $deps = [];
890 }
891 $version = $style['version'] ?? null;
892 if ( $version ) {
893 if ( 'WP' === $version ) {
894 $version = '';
895 } elseif ( 'CBB' === $version ) {
896 $version = BOLDBLOCKS_CBB_VERSION;
897 }
898 }
899
900 // Don't load external resources on admin side for some cases.
901 if ( ! $is_backend || ( $script['in_backend'] ?? false ) ) {
902 $loading_strategy_args = [
903 'in_footer' => isset( $script['in_footer'] ) ? $script['in_footer'] : true,
904 ];
905
906 $loading_strategy = $script['loading_strategy'] ?? '';
907 if ( in_array( $loading_strategy, [ 'defer', 'async' ], true ) ) {
908 $loading_strategy_args['strategy'] = $loading_strategy;
909 }
910
911 wp_register_script( $script['handle'], $script['src'] ?? '', $deps, $version, $loading_strategy_args );
912
913 // Add the handle to the block.
914 $handles['view_script_handles'][] = $script['handle'];
915
916 if ( $is_backend ) {
917 $handles['editor_script_handles'][] = $script['handle'];
918 }
919 }
920 }
921 }
922
923 if ( ! empty( $args['custom_scripts'] ) && ! $is_backend ) {
924 $custom_scripts = $args['custom_scripts'];
925
926 // Refine custom scripts.
927 $custom_scripts = array_reduce(
928 $custom_scripts,
929 function ( $carry, $script ) {
930 // Skip if script is empty.
931 if ( ! $script || ! is_array( $script ) || empty( $script['value'] ) ) {
932 return $carry;
933 }
934
935 // Refine script type.
936 $script_type = $script['script_type'] ?? '';
937
938 // Legacy support for script_type.
939 if ( ! $script_type ) {
940 if ( $script['is_module'] ?? false ) {
941 $script_type = 'module';
942 } elseif ( ! empty( $script['handle'] ) ) {
943 $script_type = 'custom';
944 } else {
945 $script_type = 'default';
946 }
947
948 $script['script_type'] = $script_type;
949 }
950
951 // If custom type but no handle, change to default.
952 if ( 'custom' === $script_type && empty( $script['handle'] ) ) {
953 $script['script_type'] = 'default';
954 }
955
956 $carry[] = $script;
957
958 return $carry;
959 },
960 []
961 );
962
963 $default_handle_scripts = array_filter(
964 $custom_scripts,
965 function ( $script ) {
966 return 'custom' !== $script['script_type'];
967 }
968 );
969
970 if ( $default_handle_scripts ) {
971 $dep_handles = array_reduce(
972 $default_handle_scripts,
973 function ( $carry, $script ) {
974 if ( 'module' === $script['script_type'] ) {
975 $carry['has_module'] = true;
976 } elseif ( ! empty( $script['deps'] ) ) {
977 $deps = explode( ',', $script['deps'] );
978 foreach ( $deps as $dep ) {
979 $dep = trim( $dep );
980 if ( ! $dep || in_array( $dep, $carry['deps'], true ) ) {
981 continue;
982 }
983
984 $carry['deps'][] = $dep;
985 }
986 }
987
988 return $carry;
989 },
990 [
991 'has_module' => false,
992 'deps' => [],
993 ]
994 );
995
996 if ( $dep_handles['has_module'] ) {
997 // Register the default handle for module scripts.
998 wp_register_script( $block_module_inline_handle, '', [], BOLDBLOCKS_CBB_VERSION, [ 'in_footer' => true ] );
999 }
1000
1001 if ( ! in_array( 'CBB_BLOCK_API', $dep_handles['deps'], true ) ) {
1002 array_unshift( $dep_handles['deps'], 'CBB_BLOCK_API' );
1003 }
1004
1005 // Register the default handle for inline scripts.
1006 wp_register_script( $block_inline_handle, '', explode( ',', $this->refine_script_handle( implode( ',', $dep_handles['deps'] ), $is_backend ) ), BOLDBLOCKS_CBB_VERSION, [ 'in_footer' => true ] );
1007 }
1008
1009 foreach ( $custom_scripts as $script ) {
1010 $script_type = $script['script_type'];
1011
1012 // Determine if a custom handle should be used.
1013 if ( 'custom' === $script_type ) {
1014 $handle = $this->refine_script_handle( $script['handle'], $is_backend );
1015 } else {
1016 $handle = 'module' === $script_type ? $block_module_inline_handle : $block_inline_handle;
1017
1018 // Add handle to view scripts and editor scripts if backend.
1019 if ( ! in_array( $handle, $handles['view_script_handles'], true ) ) {
1020 $handles['view_script_handles'][] = $handle;
1021 }
1022 if ( $is_backend ) {
1023 if ( ! in_array( $handle, $handles['editor_script_handles'], true ) ) {
1024 $handles['editor_script_handles'][] = $handle;
1025 }
1026 }
1027 }
1028
1029 // Add inline script.
1030 wp_add_inline_script( $handle, $script['value'] );
1031 }
1032 }
1033
1034 if ( ! empty( $args['external_styles'] ) ) {
1035 $external_styles = array_filter(
1036 $args['external_styles'],
1037 function ( $style ) {
1038 return $style['handle'] ?? false;
1039 }
1040 );
1041
1042 foreach ( $external_styles as $style ) {
1043 if ( $style['deps'] ?? false ) {
1044 $deps = $this->refine_style_handle( $style['deps'], $is_backend );
1045 $deps = \explode( ',', $deps );
1046 } else {
1047 $deps = [];
1048 }
1049 $version = $style['version'] ?? null;
1050 if ( $version ) {
1051 if ( 'WP' === $version ) {
1052 $version = '';
1053 } elseif ( 'CBB' === $version ) {
1054 $version = BOLDBLOCKS_CBB_VERSION;
1055 }
1056 }
1057 // Don't load external resources on admin side for some cases.
1058 if ( ! $is_backend || ! ( $style['src'] ?? '' ) || ( $style['in_backend'] ?? false ) ) {
1059 wp_register_style( $style['handle'], $style['src'] ?? '', $deps, $version, $style['media'] ?? 'all' );
1060
1061 // Add the handle to the block.
1062 $handles['style_handles'][] = $style['handle'];
1063 }
1064 }
1065 }
1066
1067 if ( ! empty( $args['custom_styles'] ) ) {
1068 $custom_styles = $args['custom_styles'];
1069
1070 // Hanle preview style for wrapper blocks of core/HTML.
1071 $is_preview = ! empty( $args['dependentBlocks'] ) && is_array( $args['dependentBlocks'] ) && count( $args['dependentBlocks'] ) === 1 && 'core/html' === ( $args['dependentBlocks'][0] ?? '' );
1072
1073 foreach ( $custom_styles as $style ) {
1074 if ( $style && is_array( $style ) && $style['value'] ) {
1075 $snippet_type = $style['type'] ?? 'all';
1076 if ( ( 'view' === $snippet_type && $is_backend ) || ( 'editor' === $snippet_type && ! $is_backend ) ) {
1077 continue;
1078 }
1079
1080 $value = $style['value'];
1081 if ( $is_preview ) {
1082 $this->html_editor_style .= $value;
1083 }
1084
1085 if ( empty( $style['handle'] ) ) {
1086 $handle = $block_inline_handle;
1087 // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
1088 wp_register_style( $handle, '' );
1089
1090 // Add the default handle to the block.
1091 $handles['style_handles'][] = $handle;
1092 } else {
1093 $handle = $this->refine_style_handle( $style['handle'], $is_backend );
1094 }
1095 wp_add_inline_style( $handle, $value );
1096 }
1097 }
1098 }
1099
1100 // Hide on frontend.
1101 $block_atts['supports']['hideOnFrontend'] = $args['hideOnFrontend'] ?? false;
1102
1103 // Block keywords.
1104 if ( $args['keywords'] ?? false ) {
1105 $block_atts['keywords'] = $args['keywords'];
1106 }
1107
1108 // Synced block overrides.
1109 if ( $args['isSyncedBlock'] ?? false ) {
1110 $block_atts['supports']['block_id'] = $args['id'];
1111 $block_atts['supports']['block_content'] = $args['postContent'];
1112 $block_atts['render_callback'] = [ $this, 'render_readonly_block' ];
1113 }
1114
1115 // Enable interactivity.
1116 $block_atts['supports']['interactivity'] = [ 'clientNavigation' => true ];
1117
1118 register_block_type( $name, array_merge( $block_atts, $handles ) );
1119 }
1120 }
1121
1122 /**
1123 * Make sure the block name is valid
1124 *
1125 * @param string $name
1126 * @return string
1127 */
1128 private function sanitize_block_name( $name ) {
1129 return sanitize_key( str_replace( '_', '-', $name ) );
1130 }
1131
1132 /**
1133 * Handle script handle
1134 *
1135 * @param string $handle
1136 * @param boolean $is_backend
1137 * @return string
1138 */
1139 private function refine_script_handle( $handle, $is_backend = false ) {
1140 if ( $handle ) {
1141 $handle = str_replace( ' ', '', $handle );
1142 $handle = str_replace( 'CBB_BLOCK_API', $is_backend ? $this->custom_blocks_handle : $this->custom_blocks_frontend_handle, $handle );
1143 $handle = str_replace( 'CBB_CAROUSEL_API', $is_backend ? $this->custom_blocks_handle : $this->carousel_blocks_frontend_handle, $handle );
1144 }
1145
1146 return $handle;
1147 }
1148
1149 /**
1150 * Handle style handle
1151 *
1152 * @param string $handle
1153 * @param boolean $is_backend
1154 * @return string
1155 */
1156 private function refine_style_handle( $handle, $is_backend = false ) {
1157 if ( $handle ) {
1158 $handle = str_replace( 'CBB_BLOCK_STYLE', $this->custom_blocks_handle, $handle );
1159 $handle = str_replace( 'CBB_CAROUSEL_STYLE', $is_backend ? $this->custom_blocks_handle : $this->carousel_blocks_frontend_handle, $handle );
1160 }
1161
1162 return $handle;
1163 }
1164
1165 /**
1166 * Register code editor scripts
1167 *
1168 * @return void
1169 */
1170 public function register_code_editor_scripts() {
1171 $screen = get_current_screen();
1172 if ( $screen && $screen->is_block_editor ) {
1173 wp_enqueue_code_editor( array( 'type' => 'text/css' ) );
1174 wp_enqueue_code_editor( array( 'type' => 'javascript' ) );
1175 wp_enqueue_code_editor( array( 'type' => 'application/json' ) );
1176 }
1177 }
1178
1179 /**
1180 * Adjust the src of the esprima script.
1181 *
1182 * @param string $src The source URL of the script.
1183 * @param string $handle The handle of the script.
1184 * @return string The adjusted source URL of the script.
1185 */
1186 public function adjust_esprima_script( $src, $handle ) {
1187 if ( 'esprima' === $handle ) {
1188 $src = 'https://cdn.jsdelivr.net/npm/esprima-next@5.8.4/dist/esprima.js';
1189 }
1190
1191 return $src;
1192 }
1193
1194 /**
1195 * Register scripts
1196 *
1197 * @return void
1198 */
1199 public function register_scripts() {
1200 // Custom blocks asset file.
1201 $custom_blocks_asset = $this->the_plugin_instance->include_file( "build/custom-blocks{$this->the_plugin_instance->get_script_suffix()}.asset.php" );
1202
1203 // Scripts.
1204 wp_register_script(
1205 $this->custom_blocks_handle,
1206 $this->the_plugin_instance->get_file_uri( "build/custom-blocks{$this->the_plugin_instance->get_script_suffix()}.js" ),
1207 array_merge( $custom_blocks_asset['dependencies'] ?? [], array( 'code-editor', 'csslint', 'jshint' ) ),
1208 $this->the_plugin_instance->get_script_version( $custom_blocks_asset ),
1209 [ 'in_footer' => true ]
1210 );
1211
1212 // Add translation.
1213 wp_set_script_translations( $this->custom_blocks_handle, 'content-blocks-builder' );
1214
1215 // Get all blocks.
1216 $all_custom_blocks = $this->get_all_custom_blocks();
1217 $current_post = $this->get_current_post();
1218 if ( $current_post && $this->post_type === $current_post->post_type ) {
1219 // Hide the current block.
1220 $all_custom_blocks = array_map(
1221 function ( $block ) use ( $current_post ) {
1222 if ( $block['id'] === $current_post->ID ) {
1223 $block['isHidden'] = true;
1224 }
1225 return $block;
1226 },
1227 $all_custom_blocks
1228 );
1229 }
1230
1231 // Define data.
1232 $blocks_scripts = [
1233 'blocks' => $all_custom_blocks,
1234 'variations' => $this->the_plugin_instance->get_component( Variations::class )->get_all_variations(),
1235 'variationNonce' => wp_create_nonce( 'cbb_variation_nonce' ),
1236 'animations' => $this->get_animated_blocks(),
1237 'canEditCSS' => $this->the_plugin_instance->get_component( CustomStyle::class )->user_can_edit_css(),
1238 ];
1239
1240 // Get block name for current variation.
1241 $block_name = $this->the_plugin_instance->get_component( Variations::class )->get_block_name();
1242 if ( $block_name ) {
1243 $blocks_scripts['variationBlockName'] = $block_name;
1244 }
1245
1246 // Load all custom blocks for registration.
1247 wp_add_inline_script( $this->custom_blocks_handle, 'var CBBBlocks=' . wp_json_encode( $blocks_scripts ), 'before' );
1248
1249 // Frontend styles.
1250 wp_register_style(
1251 $this->custom_blocks_handle,
1252 $this->the_plugin_instance->get_file_uri( "build/custom-blocks{$this->the_plugin_instance->get_script_suffix()}.css" ),
1253 [],
1254 $this->the_plugin_instance->get_script_version( $custom_blocks_asset )
1255 );
1256
1257 // Editor styles.
1258 wp_register_style(
1259 $this->custom_blocks_editor_handle,
1260 $this->the_plugin_instance->get_file_uri( 'build/custom-blocks-editor.css' ),
1261 [],
1262 $this->the_plugin_instance->get_script_version( $custom_blocks_asset )
1263 );
1264
1265 // Custom blocks asset file.
1266 $custom_blocks_frontend_asset = $this->the_plugin_instance->include_file( "build/custom-blocks-frontend{$this->the_plugin_instance->get_script_suffix()}.asset.php" );
1267
1268 // Frontend script.
1269 wp_register_script(
1270 $this->custom_blocks_frontend_handle,
1271 $this->the_plugin_instance->get_file_uri( "build/custom-blocks-frontend{$this->the_plugin_instance->get_script_suffix()}.js" ),
1272 $custom_blocks_frontend_asset['dependencies'] ?? [],
1273 $this->the_plugin_instance->get_script_version( $custom_blocks_frontend_asset ),
1274 [ 'in_footer' => true ]
1275 );
1276
1277 // Add translation.
1278 wp_set_script_translations( $this->custom_blocks_frontend_handle, 'content-blocks-builder' );
1279
1280 // Carousel asset file.
1281 $caousel_frontend_asset = $this->the_plugin_instance->include_file( 'build/carousel-frontend.asset.php' );
1282
1283 // Carousel styles.
1284 wp_register_style(
1285 $this->carousel_blocks_frontend_handle,
1286 $this->the_plugin_instance->get_file_uri( 'build/carousel-frontend.css' ),
1287 [],
1288 $this->the_plugin_instance->get_script_version( $caousel_frontend_asset )
1289 );
1290
1291 // Carousel frontend script.
1292 wp_register_script(
1293 $this->carousel_blocks_frontend_handle,
1294 $this->the_plugin_instance->get_file_uri( 'build/carousel-frontend.js' ),
1295 $caousel_frontend_asset['dependencies'] ?? [],
1296 $this->the_plugin_instance->get_script_version( $caousel_frontend_asset ),
1297 [ 'in_footer' => true ]
1298 );
1299
1300 // Add translation.
1301 wp_set_script_translations( $this->carousel_blocks_frontend_handle, 'content-blocks-builder' );
1302 }
1303
1304 /**
1305 * Get all custom blocks.
1306 *
1307 * @return array
1308 */
1309 public function get_all_custom_blocks() {
1310 if ( ! $this->all_custom_blocks ) {
1311 $this->all_custom_blocks = $this->get_posts( $this->the_plugin_instance->is_debug_mode() );
1312 }
1313
1314 return $this->all_custom_blocks;
1315 }
1316
1317 /**
1318 * Query posts and cache the results.
1319 *
1320 * @param bool $force_refresh Optional. Whether to force the cache to be refreshed. Default false.
1321 * @return array Array of WP_Post objects.
1322 */
1323 public function get_posts( $force_refresh = false ) {
1324 if ( $force_refresh ) {
1325 return $this->query_posts();
1326 }
1327
1328 $cache_key = 'bb_block_posts';
1329 $posts = get_transient( $cache_key );
1330
1331 // If nothing is found, build the object.
1332 if ( false === $posts ) {
1333 // Query posts.
1334 $posts = $this->query_posts();
1335
1336 if ( ! is_wp_error( $posts ) && count( $posts ) > 0 ) {
1337 set_transient( $cache_key, $posts, HOUR_IN_SECONDS );
1338 }
1339 }
1340
1341 return $posts;
1342 }
1343
1344 /**
1345 * Query and parse blocks
1346 *
1347 * @return array
1348 */
1349 public function query_posts() {
1350 $post_args = apply_filters(
1351 'cbb_block_query_args',
1352 [
1353 'post_type' => $this->post_type,
1354 'posts_per_page' => $this->get_max_items(),
1355 'orderby' => [
1356 'menu_order' => 'DESC',
1357 'date' => 'DESC',
1358 ],
1359 ]
1360 );
1361
1362 $raw_posts = get_posts( $post_args );
1363
1364 $block_posts = array_map(
1365 function ( $item ) {
1366 // Template lock.
1367 $template_lock = get_post_meta( $item->ID, 'boldblocks_template_lock', true );
1368
1369 // Convert boolean string to boolean.
1370 $template_lock = in_array( $template_lock, [ 'false', 'inherit' ], true ) ? false : $template_lock;
1371
1372 // Scripts & styles.
1373 $block_external_scripts = get_post_meta( $item->ID, 'boldblocks_block_external_scripts', true );
1374 $block_custom_scripts = get_post_meta( $item->ID, 'boldblocks_block_custom_scripts', true );
1375 $block_external_styles = get_post_meta( $item->ID, 'boldblocks_block_external_styles', true );
1376 $block_custom_styles = get_post_meta( $item->ID, 'boldblocks_block_custom_styles', true );
1377
1378 // Hide on frontend.
1379 $hide_on_frontend = (bool) get_post_meta( $item->ID, 'boldblocks_hide_on_frontend', true );
1380
1381 // Is a readonly block.
1382 $is_readonly = (bool) get_post_meta( $item->ID, 'boldblocks_is_readonly', true );
1383
1384 // Is block transformable.
1385 $is_transformable = (bool) get_post_meta( $item->ID, 'boldblocks_is_transformable', true );
1386
1387 // Is block hidden.
1388 $is_hidden = (bool) get_post_meta( $item->ID, 'boldblocks_is_hidden', true );
1389
1390 // Parent & ancestor.
1391 $block_parent = get_post_meta( $item->ID, 'boldblocks_block_parent', true );
1392 $block_parent = $block_parent ? array_filter( explode( ',', $block_parent ) ) : [];
1393 $block_ancestor = get_post_meta( $item->ID, 'boldblocks_block_ancestor', true );
1394 $block_ancestor = $block_ancestor ? array_filter( explode( ',', $block_ancestor ) ) : [];
1395 $allowed_blocks = get_post_meta( $item->ID, 'boldblocks_block_allowed_blocks', true );
1396 $allowed_blocks = $allowed_blocks ? array_filter( explode( ',', $allowed_blocks ) ) : [];
1397
1398 $keywords = get_the_terms( $item->ID, 'boldblocks_block_keywords' );
1399 if ( $keywords && ! is_wp_error( $keywords ) ) {
1400 $keywords = wp_list_pluck( $keywords, 'name' );
1401 } else {
1402 $keywords = false;
1403 }
1404
1405 $block_name = sprintf( 'boldblocks/%s', $this->sanitize_block_name( $item->post_name ) );
1406 $block_version = get_post_meta( $item->ID, 'boldblocks_block_version', true );
1407 $migrateClass = version_compare( $block_version, '2.7.14', '<=' );
1408
1409 $block_args = [
1410 'id' => $item->ID,
1411 'name' => $block_name,
1412 'title' => wp_strip_all_tags( $item->post_title ),
1413 'postContent' => $item->post_content,
1414 'blocks' => get_post_meta( $item->ID, 'boldblocks_block_blocks', true ),
1415 'description' => get_post_meta( $item->ID, 'boldblocks_block_description', true ),
1416 'templateLock' => $template_lock,
1417 'className' => get_post_meta( $item->ID, 'boldblocks_block_class', true ),
1418 'blockIcon' => get_post_meta( $item->ID, 'boldblocks_block_icon', true ),
1419 'blockSupports' => get_post_meta( $item->ID, 'boldblocks_block_supports', true ),
1420 'blockAttributes' => [
1421 'attributes' => get_post_meta( $item->ID, 'boldblocks_block_attributes', true ),
1422 'enableCustomAttributes' => (bool) get_post_meta( $item->ID, 'boldblocks_enable_custom_attributes', true ),
1423 ],
1424 'enableVariationPicker' => (bool) get_post_meta( $item->ID, 'boldblocks_enable_variation_picker', true ),
1425 'blockVersion' => $block_version,
1426 'migrateClass' => $migrateClass,
1427 'dependentBlocks' => get_post_meta( $item->ID, 'boldblocks_block_dependent_blocks', true ),
1428 'hideOnFrontend' => $hide_on_frontend,
1429 'isTransformable' => $is_transformable,
1430 'isHidden' => $is_hidden,
1431 'isSyncedBlock' => $is_readonly, // Only for standalone blocks.
1432 'allowedBlocks' => $allowed_blocks,
1433 ];
1434
1435 // Get parent block parameters.
1436 $enable_repeater = get_post_meta( $item->ID, 'boldblocks_enable_repeater', true );
1437 if ( $enable_repeater ) {
1438 $parent_name = get_post_meta( $item->ID, 'boldblocks_parent_block_name', true );
1439 if ( ! $parent_name ) {
1440 $parent_name = $item->post_name;
1441 }
1442
1443 $parent_block_name = sprintf( 'boldblocks/%s-repeater', $this->sanitize_block_name( $parent_name ) );
1444
1445 $parent_layout_type = get_post_meta( $item->ID, 'boldblocks_parent_layout_type', true );
1446
1447 $parent_title = get_post_meta( $item->ID, 'boldblocks_parent_block_title', true );
1448 if ( ! $parent_title ) {
1449 // translators: Repeater block title.
1450 $parent_title = sprintf( __( 'Repeater: %s', 'content-blocks-builder' ), $item->post_title );
1451 }
1452
1453 $parent_args = [
1454 'name' => $parent_block_name,
1455 'title' => wp_strip_all_tags( $parent_title ),
1456 'description' => get_post_meta( $item->ID, 'boldblocks_parent_block_description', true ),
1457 'className' => get_post_meta( $item->ID, 'boldblocks_parent_block_class', true ) ?? '',
1458 'blockIcon' => get_post_meta( $item->ID, 'boldblocks_parent_block_icon', true ),
1459 'blockSupports' => get_post_meta( $item->ID, 'boldblocks_parent_block_supports', true ),
1460 'blockAttributes' => [
1461 'attributes' => get_post_meta( $item->ID, 'boldblocks_parent_block_attributes', true ),
1462 'enableCustomAttributes' => (bool) get_post_meta( $item->ID, 'boldblocks_parent_enable_custom_attributes', true ),
1463 ],
1464 'enableVariationPicker' => (bool) get_post_meta( $item->ID, 'boldblocks_parent_enable_variation_picker', true ),
1465 'external_scripts' => $block_external_scripts,
1466 'custom_scripts' => $this->refine_custom_scripts( $block_custom_scripts, 'JS', $parent_block_name, $block_name ),
1467 'external_styles' => $block_external_styles,
1468 'custom_styles' => $this->refine_custom_scripts( $block_custom_styles, 'CSS', $parent_block_name, $block_name ),
1469 'hideOnFrontend' => $hide_on_frontend,
1470 'blockParent' => $block_parent,
1471 'blockAncestor' => $block_ancestor,
1472 'migrateClass' => $migrateClass,
1473
1474 ];
1475
1476 if ( $keywords ) {
1477 $parent_args['keywords'] = $keywords;
1478 }
1479
1480 $is_fixed_nested_item_count = get_post_meta( $item->ID, 'boldblocks_is_fixed_nested_item_count', true );
1481 if ( $is_fixed_nested_item_count ) {
1482 $nested_item_count = get_post_meta( $item->ID, 'boldblocks_nested_item_count', true );
1483 $nested_item_count = $nested_item_count ? absint( $nested_item_count ) : 1;
1484 $parent_args['nestedItemCount'] = $nested_item_count ? $nested_item_count : 1;
1485 }
1486
1487 $parent_args['layoutType'] = $parent_layout_type;
1488 $block_args['parent'] = $parent_args;
1489 $block_args['parentName'] = $parent_block_name;
1490
1491 $block_args['standalone'] = ! get_post_meta( $item->ID, 'boldblocks_disable_standalone', true );
1492 } else {
1493 if ( $keywords ) {
1494 $block_args['keywords'] = $keywords;
1495 }
1496
1497 $block_args = array_merge(
1498 $block_args,
1499 [
1500 'external_scripts' => $block_external_scripts,
1501 'custom_scripts' => $this->refine_custom_scripts( $block_custom_scripts, 'JS', $block_name ),
1502 'external_styles' => $block_external_styles,
1503 'custom_styles' => $this->refine_custom_scripts( $block_custom_styles, 'CSS', $block_name ),
1504 'blockParent' => $block_parent,
1505 'blockAncestor' => $block_ancestor,
1506 ]
1507 );
1508 }
1509
1510 return $block_args;
1511 },
1512 $raw_posts
1513 );
1514
1515 return $block_posts;
1516 }
1517
1518 /**
1519 * Refine custom scripts & styles
1520 *
1521 * @param array $custom_scripts
1522 * @param string $type
1523 * @param string $block_name
1524 * @return array
1525 */
1526 private function refine_custom_scripts( $custom_scripts, $type, $block_name, $child_block_name = '' ) {
1527 if ( $custom_scripts && is_array( $custom_scripts ) ) {
1528 $custom_style_handler = $this->the_plugin_instance->get_component( CustomStyle::class );
1529 $block_class = '.wp-block-' . str_replace( '/', '-', $block_name );
1530 $child_block_class = $child_block_name ? '.wp-block-' . str_replace( '/', '-', $child_block_name ) : '';
1531
1532 $custom_scripts = array_filter(
1533 $custom_scripts,
1534 function ( $script ) {
1535 return ! empty( $script['value'] );
1536 }
1537 );
1538
1539 $custom_scripts = array_map(
1540 function ( $script ) use ( $custom_style_handler, $block_class, $child_block_class, $type ) {
1541 $tokens = [];
1542 if ( 'CSS' === $type ) {
1543 if ( $child_block_class ) {
1544 $tokens['childblockselector'] = $child_block_class;
1545 }
1546 $tokens['selector'] = $block_class;
1547 } else {
1548 if ( $child_block_class ) {
1549 $tokens['CHILDBLOCKSELECTOR'] = $child_block_class;
1550 }
1551 $tokens['BLOCKSELECTOR'] = $block_class;
1552 }
1553
1554 $script['value'] = $custom_style_handler->refine_custom_value( $script['value'], $tokens, $type );
1555
1556 return $script;
1557 },
1558 $custom_scripts
1559 );
1560 }
1561
1562 return $custom_scripts;
1563 }
1564
1565 /**
1566 * Enqueue styles for html blocks in the editor
1567 *
1568 * @param array $editor_settings
1569 * @return void
1570 */
1571 public function enqueue_html_style_for_the_editor( $editor_settings ) {
1572 if ( $this->html_editor_style ) {
1573 $editor_settings['styles'][] = [ 'css' => $this->html_editor_style ];
1574 }
1575
1576 return $editor_settings;
1577 }
1578
1579 /**
1580 * Handle save block post
1581 *
1582 * @param int $post_id
1583 * @param WP_Post $post
1584 * @return void
1585 */
1586 public function save_post( $post_id, $post ) {
1587 // Ignore auto-draft status.
1588 if ( 'auto-draft' === $post->post_status ) {
1589 return;
1590 }
1591
1592 // Ignore autosave.
1593 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
1594 return;
1595 }
1596
1597 $this->clear_transient_cache();
1598 }
1599
1600 /**
1601 * Clear transient cache
1602 *
1603 * @return void
1604 */
1605 public function clear_transient_cache() {
1606 delete_transient( 'bb_block_posts' );
1607 }
1608
1609 /**
1610 * Add custom blocks to the list of blocks that can create patterns from.
1611 *
1612 * @param array $allowed_blocks
1613 * @return array
1614 */
1615 public function allow_creating_patterns( $allowed_blocks ) {
1616 return array_merge( $allowed_blocks, array_keys( $this->custom_blocks ), array_keys( $this->repeater_blocks ) );
1617 }
1618
1619 /**
1620 * Add custom columns to custom post type
1621 *
1622 * @param array $columns The array of columns.
1623 * @return array
1624 */
1625 public function add_block_custom_columns( $columns ) {
1626 $custom_columns = array(
1627 'block_icon' => esc_html__( 'Block icon', 'content-blocks-builder' ),
1628 'block_name' => esc_html__( 'Block name', 'content-blocks-builder' ),
1629 'parent_block_icon' => esc_html__( 'Parent icon', 'content-blocks-builder' ),
1630 'parent_block_name' => esc_html__( 'Parent block name', 'content-blocks-builder' ),
1631 );
1632
1633 if ( apply_filters( 'cbb_display_block_version', $this->the_plugin_instance->is_debug_mode() ) ) {
1634 $custom_columns['block_version'] = esc_html__( 'Block version', 'content-blocks-builder' );
1635 }
1636
1637 // Add after title column.
1638 $return = array_slice( $columns, 0, 2, true ) + $custom_columns + array_slice( $columns, 2, count( $columns ) - 2, true );
1639
1640 return $return;
1641 }
1642
1643 /**
1644 * Manage custom columns.
1645 *
1646 * @param string $column the column name.
1647 * @param int $post_id the post id.
1648 */
1649 public function manage_block_columns( $column, $post_id ) {
1650 switch ( $column ) {
1651 case 'block_icon':
1652 $block_icon = get_post_meta( $post_id, 'boldblocks_block_icon', true );
1653 if ( empty( $block_icon ) ) {
1654 $block_icon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="bb-icon bb-icon--block-default"><path d="M19 8h-1V6h-5v2h-2V6H6v2H5c-1.1 0-2 .9-2 2v8c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-8c0-1.1-.9-2-2-2zm.5 10c0 .3-.2.5-.5.5H5c-.3 0-.5-.2-.5-.5v-8c0-.3.2-.5.5-.5h14c.3 0 .5.2.5.5v8z"/></svg>';
1655 }
1656 echo $block_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1657 break;
1658
1659 case 'block_name':
1660 echo esc_html( 'boldblocks/' . $this->sanitize_block_name( get_post_field( 'post_name', $post_id ) ) );
1661 break;
1662
1663 case 'parent_block_icon':
1664 if ( get_post_meta( $post_id, 'boldblocks_enable_repeater', true ) ) {
1665 $block_icon = get_post_meta( $post_id, 'boldblocks_parent_block_icon', true );
1666 if ( empty( $block_icon ) ) {
1667 $block_icon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="bb-icon bb-icon--block-default"><path d="M19 8h-1V6h-5v2h-2V6H6v2H5c-1.1 0-2 .9-2 2v8c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-8c0-1.1-.9-2-2-2zm.5 10c0 .3-.2.5-.5.5H5c-.3 0-.5-.2-.5-.5v-8c0-.3.2-.5.5-.5h14c.3 0 .5.2.5.5v8z"/></svg>';
1668 }
1669 echo $block_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1670 } else {
1671 esc_html_e( 'NA', 'content-blocks-builder' );
1672 }
1673 break;
1674
1675 case 'parent_block_name':
1676 $enable_repeater = get_post_meta( $post_id, 'boldblocks_enable_repeater', true );
1677 if ( $enable_repeater ) {
1678 echo esc_html( 'boldblocks/' . $this->sanitize_block_name( get_post_field( 'post_name', $post_id ) ) . '-repeater' );
1679 } else {
1680 esc_html_e( 'NA', 'content-blocks-builder' );
1681 }
1682 break;
1683
1684 case 'block_version':
1685 $block_version = get_post_meta( $post_id, 'boldblocks_block_version', true );
1686 if ( $block_version ) {
1687 echo esc_html( $block_version );
1688 } else {
1689 echo esc_html( '1.x' );
1690 }
1691 break;
1692
1693 // Just break out of the switch statement for everything else.
1694 default:
1695 break;
1696 }
1697 }
1698
1699 /**
1700 * Add style to custom columns
1701 *
1702 * @return void
1703 */
1704 public function add_block_custom_columns_style() {
1705 echo '<style>.column-block_icon, .column-parent_block_icon {width: 70px;}.column-block_icon svg,.column-parent_block_icon svg {max-width: 32px;max-height: 32px;}</style>';
1706 }
1707
1708 /**
1709 * Add custom block's meta fields to meta revisioning.
1710 *
1711 * @param array $fields
1712 * @return array
1713 */
1714 public function add_fields_to_revision_meta_keys( $fields ) {
1715 return array_merge(
1716 $fields,
1717 [
1718 'boldblocks_block_blocks',
1719 'boldblocks_block_class',
1720 'boldblocks_block_description',
1721 'boldblocks_block_icon',
1722 'boldblocks_block_supports',
1723 'boldblocks_template_lock',
1724 'boldblocks_enable_variation_picker',
1725 'boldblocks_enable_repeater',
1726 'boldblocks_parent_layout_type',
1727 'boldblocks_parent_block_icon',
1728 'boldblocks_parent_block_supports',
1729 'boldblocks_parent_enable_variation_picker',
1730 'boldblocks_disable_standalone',
1731 'boldblocks_is_fixed_nested_item_count',
1732 'boldblocks_nested_item_count',
1733 'boldblocks_parent_block_name',
1734 'boldblocks_parent_block_title',
1735 'boldblocks_parent_block_description',
1736 'boldblocks_parent_block_class',
1737 'boldblocks_block_custom_scripts',
1738 'boldblocks_block_custom_styles',
1739 'boldblocks_block_attributes',
1740 'boldblocks_parent_block_attributes',
1741 ]
1742 );
1743 }
1744
1745 /**
1746 * Add theme name to body class
1747 *
1748 * @param array $classes
1749 * @return array
1750 */
1751 public function add_body_class( $classes ) {
1752 // Add a frontend CSS class.
1753 $classes[] = 'cbb-frontend';
1754
1755 // Add theme slug.
1756 $classes[] = get_stylesheet();
1757
1758 return $classes;
1759 }
1760
1761 /**
1762 * Save the plugin version to custom blocks
1763 *
1764 * @param int $post_id
1765 * @param WP_Post $post
1766 * @param boolean $update
1767 * @return void
1768 */
1769 public function save_version_to_block( $post_id, $post, $update ) {
1770 // Bail if it is a update action.
1771 if ( $update ) {
1772 return;
1773 }
1774
1775 // Save the plugin version to blocks.
1776 update_post_meta( $post_id, 'boldblocks_block_version', $this->the_plugin_instance->get_plugin_version() );
1777 }
1778
1779 /**
1780 * Prevent users from deleting core blocks.
1781 *
1782 * @param array $allcaps
1783 * @param array $caps
1784 * @param array $args
1785 * @return array
1786 */
1787 public function prevent_core_blocks_from_deletion( $allcaps, $caps, $args ) {
1788 // Bail out if we're not asking about deleting a post.
1789 if ( 'delete_post' !== $args[0] ) {
1790 return $allcaps;
1791 }
1792
1793 // Bail out for users who can't delete posts.
1794 if ( empty( $allcaps['delete_posts'] ) ) {
1795 return $allcaps;
1796 }
1797
1798 // Load the post data.
1799 $post = get_post( $args[2] );
1800
1801 // Bail out if the post type is not boldblocks_block.
1802 if ( $this->post_type !== $post->post_type ) {
1803 return $allcaps;
1804 }
1805
1806 // Don't allow deleting core blocks.
1807 if ( \in_array( $post->post_name, [ 'group', 'carousel-item', 'grid-item', 'stack-item', 'accordion-item' ], true ) ) {
1808 $allcaps[ $caps[0] ] = false;
1809 }
1810
1811 return $allcaps;
1812 }
1813
1814 /**
1815 * Placeholder text. Default 'Add title'.
1816 *
1817 * @param string $text
1818 * @return string
1819 */
1820 public function enter_title_here( $text ) {
1821 $post_type = get_post_type();
1822 if ( $this->post_type === $post_type ) {
1823 $text = __( 'Add block title', 'content-blocks-builder' );
1824 }
1825
1826 return $text;
1827 }
1828
1829 /**
1830 * Enqueue frontend scripts for non-cbb blocks
1831 *
1832 * @param string $block_content
1833 * @param array $block
1834 * @param WP_Block $block_instance
1835 * @return string
1836 */
1837 public function enqueue_frontend_block_scripts( $block_content, $block, $block_instance ) {
1838 // Ignore admin side.
1839 if ( is_admin() ) {
1840 return $block_content;
1841 }
1842
1843 if ( isset( $GLOBALS['boldblocks_frontend_loaded'] ) ) {
1844 return $block_content;
1845 }
1846
1847 if ( $block_instance->block_type->supports['cbb'] ?? false ) {
1848 $GLOBALS['boldblocks_frontend_loaded'] = true;
1849 } elseif ( $this->is_enqueue_frontend_script_for_non_cbb_blocks( $block, $block_instance ) ) {
1850 wp_enqueue_script( $this->custom_blocks_frontend_handle );
1851 wp_enqueue_style( $this->custom_blocks_handle );
1852
1853 $GLOBALS['boldblocks_frontend_loaded'] = true;
1854 }
1855
1856 return $block_content;
1857 }
1858
1859 /**
1860 * Whether to load the frontend scripts for non-cbb blocks.
1861 *
1862 * @param array $block
1863 * @param WP_Block $block_instance
1864 * @return boolean
1865 */
1866 private function is_enqueue_frontend_script_for_non_cbb_blocks( $block, $block_instance ) {
1867 $block_names = $this->the_plugin_instance->get_component( CustomStyle::class )->non_cbb_blocks;
1868 if ( ! in_array( $block['blockName'] ?? '', $block_names, true ) ) {
1869 return false;
1870 }
1871
1872 // Has sticky enabling.
1873 if ( $block['attrs']['boldblocks']['sticky']['enable'] ?? false ) {
1874 return true;
1875 }
1876
1877 return false;
1878 }
1879
1880 /**
1881 * Enqueue frontend scripts for carousel layout in the query loop block
1882 *
1883 * @param string $block_content
1884 * @param array $block
1885 * @param WP_Block $block_instance
1886 * @return string
1887 */
1888 public function enqueue_frontend_carousel_scripts( $block_content, $block, $block_instance ) {
1889 // Ignore admin side.
1890 if ( is_admin() ) {
1891 return $block_content;
1892 }
1893
1894 if ( isset( $GLOBALS['boldblocks_carousel_loaded'] ) ) {
1895 return $block_content;
1896 }
1897
1898 // Make sure we have the blockName.
1899 if ( empty( $block['blockName'] ) ) {
1900 return $block_content;
1901 }
1902
1903 if ( 'core/query' === $block['blockName'] ) {
1904 // If this is a core/query block, enqueue the carousel script.
1905 if ( $this->is_enqueue_carousel_script_for_query_loop( $block, $block_instance ) ) {
1906 wp_enqueue_script( $this->carousel_blocks_frontend_handle );
1907 wp_enqueue_style( $this->carousel_blocks_frontend_handle );
1908
1909 $GLOBALS['boldblocks_carousel_loaded'] = true;
1910 }
1911 } elseif ( strpos( $block['blockName'], 'boldblocks/' ) === 0 ) {
1912 if ( isset( $block_instance->block_type->supports['parentLayoutType'] ) && 'carousel' === $block_instance->block_type->supports['parentLayoutType'] ) {
1913 $GLOBALS['boldblocks_carousel_loaded'] = true;
1914 }
1915 }
1916
1917 return $block_content;
1918 }
1919
1920 /**
1921 * Whether to load the carousel scripts for core query block.
1922 *
1923 * @param array $block
1924 * @param WP_Block $block_instance
1925 * @return boolean
1926 */
1927 private function is_enqueue_carousel_script_for_query_loop( $block, $block_instance ) {
1928 if ( 'carousel' === ( $block['attrs']['displayLayout']['type'] ?? '' ) ) {
1929 return true;
1930 }
1931
1932 if ( $block_instance->block_type->api_version >= 3 ) {
1933 $post_template = $this->the_plugin_instance->get_component( CustomStyle::class )->find_nested_block( $block_instance, 'core/post-template' );
1934
1935 if ( $post_template ) {
1936 if ( 'carousel' === ( $post_template->attributes['boldblocks']['layout']['type'] ?? '' ) ) {
1937 return true;
1938 }
1939 }
1940 }
1941
1942 return false;
1943 }
1944
1945 /**
1946 * Register custom rest fields for easier getting, updating data.
1947 *
1948 * @return void
1949 */
1950 public function register_rest_fields() {
1951 register_rest_field(
1952 $this->post_type,
1953 'keywords',
1954 array(
1955 'get_callback' => function ( $params ) {
1956 $keywords = wp_list_pluck( wp_get_object_terms( $params['id'], 'boldblocks_block_keywords' ), 'name' );
1957
1958 return \implode( ',', $keywords );
1959 },
1960 'update_callback' => function ( $value, $post ) {
1961 wp_set_post_terms( $post->ID, $value, 'boldblocks_block_keywords' );
1962 },
1963 'schema' => array(
1964 'type' => 'string',
1965 ),
1966 )
1967 );
1968 }
1969
1970 /**
1971 * Render the inline script as inline script module.
1972 *
1973 * @param array $attributes
1974 * @return array
1975 */
1976 public function add_inline_script_module( $attributes ) {
1977 if ( $attributes['id'] ?? false ) {
1978 if ( strpos( $attributes['id'], 'cbb-script-module-' ) === 0 ) {
1979 $attributes['type'] = 'module';
1980 }
1981 }
1982
1983 return $attributes;
1984 }
1985
1986 /**
1987 * Hide the block on the frontend.
1988 *
1989 * @param string $block_content
1990 * @param array $block
1991 * @param WP_Block $block_instance
1992 * @return string
1993 */
1994 public function hide_on_frontend( $block_content, $block, $block_instance ) {
1995 // Ignore admin side.
1996 if ( is_admin() ) {
1997 return $block_content;
1998 }
1999
2000 if ( $block_instance->block_type->supports['hideOnFrontend'] ?? false ) {
2001 return '<!-- ' . esc_html( $block_instance->name ) . ' -->';
2002 }
2003
2004 return $block_content;
2005 }
2006
2007 /**
2008 * Enqueue scripts for custom blocks when editing them
2009 *
2010 * @param string $hook_suffix
2011 * @return void
2012 */
2013 public function enqueue_custom_block_scripts( $hook_suffix ) {
2014 global $post;
2015 $screen = get_current_screen();
2016 if ( 'post.php' === $hook_suffix && $this->post_type === $screen->post_type ) {
2017 $custom_style_handler = $this->the_plugin_instance->get_component( CustomStyle::class );
2018 // Styles.
2019 $external_styles = get_post_meta( $post->ID, 'boldblocks_block_external_styles', true );
2020 $custom_styles = get_post_meta( $post->ID, 'boldblocks_block_custom_styles', true );
2021
2022 $block_class = '.wp-block-post-content';
2023
2024 if ( ! empty( $external_styles ) ) {
2025 $external_styles = array_filter(
2026 $external_styles,
2027 function ( $style ) {
2028 return $style['handle'] ?? false;
2029 }
2030 );
2031
2032 foreach ( $external_styles as $style ) {
2033 if ( $style['deps'] ?? false ) {
2034 $deps = $this->refine_style_handle( $style['deps'], $is_backend );
2035 $deps = \explode( ',', $deps );
2036 } else {
2037 $deps = [];
2038 }
2039 $version = $style['version'] ?? null;
2040 if ( $version ) {
2041 if ( 'WP' === $version ) {
2042 $version = '';
2043 } elseif ( 'CBB' === $version ) {
2044 $version = BOLDBLOCKS_CBB_VERSION;
2045 }
2046 }
2047 // Don't load external resources on admin side.
2048 if ( ! ( $style['src'] ?? '' ) ) {
2049 // Add 'edit-' prefix to separate it from the handle registered by other hook.
2050 wp_register_style( 'edit-' . $style['handle'], $style['src'] ?? '', $deps, $version, $style['media'] ?? 'all' );
2051 }
2052 }
2053 }
2054
2055 if ( ! empty( $custom_styles ) ) {
2056 $custom_styles = array_filter(
2057 $custom_styles,
2058 function ( $style ) {
2059 return ! empty( $style['value'] ) && 'view' !== ( $style['type'] ?? '' );
2060 }
2061 );
2062
2063 $dynamic_style = '';
2064 foreach ( $custom_styles as $style ) {
2065 $dynamic_style .= $custom_style_handler->refine_custom_value( $style['value'], [ 'selector' => $block_class ], 'CSS' );
2066 }
2067
2068 if ( $dynamic_style ) {
2069 $dynamic_style .= '.wp-block-post-content{position:relative!important;top:unset!important;right:unset!important;bottom:unset!important;left:unset!important;display:block!important;width:auto!important;max-width:none!important;height:auto!important;max-height:none!important;}.is-selected,.has-child-selected,.has-child-selected * {animation:none!important;}';
2070 $handle = 'edit-' . $this->custom_blocks_handle;
2071 if ( ! wp_style_is( $handle, 'registered' ) ) {
2072 // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
2073 wp_register_style( $handle, '' );
2074 }
2075
2076 wp_add_inline_style( $handle, $dynamic_style );
2077 wp_enqueue_style( $handle );
2078 }
2079 }
2080 }
2081 }
2082
2083 /**
2084 * Renders the 'readonly' block on the server.
2085 *
2086 * @param array $attributes Block attributes.
2087 * @param string $content Block default content.
2088 * @param WP_Block $block Block instance.
2089 * @return string Returns the block content on the frontend.
2090 */
2091 public function render_readonly_block( $attributes, $content, $block ) {
2092 $raw_block_content = $block->block_type->supports['block_content'] ?? '';
2093
2094 // Ignore empty block.
2095 if ( empty( $raw_block_content ) ) {
2096 return $content;
2097 }
2098
2099 if ( ! empty( $attributes['innerContents'] ) ) {
2100 // Convert to bindings.
2101 $raw_block_content = str_replace( 'cbbBindings', 'bindings', $raw_block_content );
2102
2103 // Handle embeds.
2104 global $wp_embed;
2105 $raw_block_content = $wp_embed->run_shortcode( $raw_block_content );
2106 $raw_block_content = $wp_embed->autoembed( $raw_block_content );
2107
2108 $filter_block_context = static function ( $context ) use ( $attributes ) {
2109 $context['cbb/block-overrides'] = $attributes['innerContents'];
2110 return $context;
2111 };
2112 add_filter( 'render_block_context', $filter_block_context, 1 );
2113
2114 $block_content = do_blocks( $raw_block_content );
2115
2116 remove_filter( 'render_block_context', $filter_block_context, 1 );
2117 } else {
2118 $block_content = apply_filters( 'the_content', $raw_block_content );
2119 }
2120
2121 if ( $block_content ) {
2122 $wrapper_content = trim( $block->parsed_block['innerHTML'] ?? '' );
2123 if ( $wrapper_content ) {
2124 $closing_div = substr( $wrapper_content, -6 );
2125
2126 if ( $closing_div === '</div>' ) {
2127 $opening_div = substr( $wrapper_content, 0, strlen( $wrapper_content ) - 6 );
2128 $block_content = $opening_div . $block_content . $closing_div;
2129
2130 return $block_content;
2131 }
2132 }
2133 }
2134
2135 return $content;
2136 }
2137
2138 /**
2139 * Customize Query Loop
2140 *
2141 * @return void
2142 */
2143 public function query_loop_extended_filters_and_sorting() {
2144 // Get public post types.
2145 $post_types = get_post_types(
2146 [
2147 'public' => true,
2148 'show_in_rest' => true,
2149 ]
2150 );
2151
2152 // For backend.
2153 foreach ( $post_types as $post_type ) {
2154 add_filter( 'rest_' . $post_type . '_collection_params', [ $this, 'query_loop_support_advanced_sorting' ], 10 );
2155 add_filter( 'rest_' . $post_type . '_query', [ $this, 'query_loop_add_custom_query_params' ], 10, 2 );
2156 }
2157
2158 // For frontend.
2159 add_filter( 'query_loop_block_query_vars', [ $this, 'query_loop_customize_frontend' ], 10, 2 );
2160
2161 // Add custom query vars.
2162 add_filter( 'query_vars', [ $this, 'query_loop_query_vars' ], 1 );
2163
2164 // Alter the result of query loop.
2165 add_filter( 'the_posts', [ $this, 'query_loop_the_posts' ], 10, 2 );
2166 }
2167
2168 /**
2169 * Support advanced sorting for the Query Loop block.
2170 *
2171 * @param array $query_params
2172 * @return array
2173 */
2174 public function query_loop_support_advanced_sorting( $query_params ) {
2175 $query_params['orderby']['enum'][] = 'meta_value';
2176 $query_params['orderby']['enum'][] = 'meta_value_num';
2177 $query_params['orderby']['enum'][] = 'menu_order';
2178 $query_params['orderby']['enum'][] = 'rand';
2179
2180 return $query_params;
2181 }
2182
2183 /**
2184 * Customize query args for frontend.
2185 *
2186 * @param array $query_args
2187 * @param WP_Block $block
2188 * @return array
2189 */
2190 public function query_loop_customize_frontend( $query_args, $block ) {
2191 $query_context = $block->context['query'] ?? [];
2192 $queried_object = get_queried_object();
2193
2194 return $this->query_loop_alter_query_params( $query_args, $query_context['cbb'] ?? [], $query_context, $queried_object );
2195 }
2196
2197 /**
2198 * Add custom query parameters to the Query Loop.
2199 *
2200 * @param array $query_args
2201 * @param WP_REST_Request $request
2202 * @return array
2203 */
2204 public function query_loop_add_custom_query_params( $query_args, $request ) {
2205 return $this->query_loop_alter_query_params( $query_args, $request->get_param( 'cbb' ) ?? [] );
2206 }
2207
2208 /**
2209 * Change query args by CBB.
2210 *
2211 * @param array $query_args
2212 * @param array $cbb_params
2213 * @param mixed $query_context
2214 * @param mixed $queried_object
2215 * @return array
2216 */
2217 private function query_loop_alter_query_params( $query_args, $cbb_params, $query_context = null, $queried_object = null ) {
2218 $cbb_args = [];
2219 if ( $cbb_params && is_array( $cbb_params ) ) {
2220 // It's a cbb query.
2221 $cbb_args = [ 'cbb' => $cbb_params ];
2222
2223 // Query additional post types.
2224 $post_type_args = $this->query_loop_filter_by_post_types( $cbb_params['postTypes'] ?? '' );
2225 if ( $post_type_args ) {
2226 $post_type = (array) ( $query_args['post_type'] ?? 'post' );
2227 $post_types = array_merge( $post_type, $post_type_args );
2228 $cbb_args['post_type'] = $post_types;
2229 // Set query args here for the context filter below.
2230 $query_args['post_type'] = $post_types;
2231 }
2232
2233 // Query by custom post ids.
2234 $post_in_args = $this->query_loop_filter_by_post_ids( $cbb_params['include'] ?? '' );
2235 if ( $post_in_args ) {
2236 $cbb_args = array_merge( $cbb_args, $post_in_args );
2237 }
2238
2239 // Query by meta fields.
2240 $meta_query_args = $this->query_loop_filter_by_meta_queries( $cbb_params );
2241 if ( $meta_query_args ) {
2242 $cbb_args = array_merge( $cbb_args, $meta_query_args );
2243 }
2244
2245 // Query by parent.
2246 $parent_args = $this->query_loop_filter_by_parent( $cbb_params['parent'] ?? '' );
2247 if ( $parent_args ) {
2248 $cbb_args = array_merge( $cbb_args, $parent_args );
2249 }
2250
2251 // Query by context.
2252 if ( $queried_object && $queried_object instanceof \WP_Post ) {
2253 $query_post_type = $query_args['post_type'] ?? '';
2254 if ( $queried_object->post_type === $query_post_type || ( is_array( $query_post_type ) && in_array( $queried_object->post_type, $query_post_type, true ) ) ) {
2255 $context_args = $this->query_loop_filter_by_context( $cbb_params, $queried_object );
2256
2257 if ( $context_args['post__not_in'] ?? false ) {
2258 // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in
2259 $cbb_args['post__not_in'] = $context_args['post__not_in'];
2260 }
2261
2262 if ( $context_args['tax_query'] ?? false ) {
2263 if ( ( $query_args['tax_query'] ?? false ) && is_array( $query_args['tax_query'] ) ) {
2264 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
2265 $cbb_args['tax_query'] = array_merge( $query_args['tax_query'], [ $context_args['tax_query'] ] );
2266 } else {
2267 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
2268 $cbb_args['tax_query'] = $context_args['tax_query'];
2269 }
2270 }
2271 }
2272
2273 // ignoreStickyPosts is depreated since WP 6.8.
2274 // Ignore sticky posts not exlude them.
2275 if ( ( $cbb_params['ignoreStickyPosts'] ?? false ) && ( 'exclude' === $query_context['sticky'] ?? '' ) ) {
2276 $cbb_args['ignore_sticky_posts'] = true;
2277 $sticky = get_option( 'sticky_posts' );
2278 if ( ! empty( $sticky ) && ! empty( $query_args['post__not_in'] ) ) {
2279 // Remove sticky from the post__not_in list.
2280 // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in
2281 $query_args['post__not_in'] = array_diff( $query_args['post__not_in'], $sticky );
2282 }
2283 }
2284 }
2285
2286 // Custom sorting.
2287 $orderby = [];
2288 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
2289 if ( ( $cbb_params['orderbyFromQueryString'] ?? false ) && isset( $_GET['orderby'] ) ) {
2290 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Recommended
2291 $orderby = wp_unslash( $_GET['orderby'] );
2292 // orderby will be sanitized below.
2293 if ( $orderby ) {
2294 if ( ! is_array( $orderby ) ) {
2295 $orderby = [ $orderby ];
2296 }
2297
2298 $orderby = array_filter(
2299 array_map(
2300 function ( $item ) {
2301 return $item ? sanitize_text_field( $item ) : false;
2302 },
2303 $orderby
2304 )
2305 );
2306 }
2307 }
2308
2309 $sort_args = $this->query_loop_custom_sort( array_merge( $orderby, $cbb_params['sorting'] ?? [] ) );
2310 if ( $sort_args ) {
2311 $cbb_args = array_merge( $cbb_args, $sort_args );
2312
2313 if ( isset( $query_args['order'] ) ) {
2314 unset( $query_args['order'] );
2315 }
2316 }
2317
2318 // Combine post__not_in value.
2319 // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in
2320 $cbb_args['post__not_in'] = array_merge( $query_args['post__not_in'], $cbb_args['post__not_in'] ?? [] );
2321
2322 // Don't need pagination.
2323 if ( $cbb_params['disablePagination'] ?? false ) {
2324 $cbb_args['no_found_rows'] = true;
2325 }
2326 }
2327
2328 // Allow third-party to alter the final result.
2329 $query_args = apply_filters( 'cbb_query_loop_block_query_vars', array_merge( $query_args, $cbb_args ), $query_args, $cbb_params, $query_context, $queried_object );
2330
2331 return $query_args;
2332 }
2333
2334 /**
2335 * Filter by additional post types.
2336 *
2337 * @param string $post_types
2338 * @return array
2339 */
2340 private function query_loop_filter_by_post_types( $post_types ) {
2341 $post_types_args = [];
2342 $post_types = trim( $post_types );
2343 if ( $post_types ) {
2344 $post_types_args = explode( ',', $post_types );
2345 }
2346
2347 return $post_types_args;
2348 }
2349
2350 /**
2351 * Filter by post ids.
2352 *
2353 * @param string $include
2354 * @return array
2355 */
2356 private function query_loop_filter_by_post_ids( $include ) {
2357 $post_in_args = [];
2358 if ( $include ) {
2359 $post_in = explode( ',', $include );
2360 if ( $post_in ) {
2361 $post_in_args['post__in'] = $post_in;
2362 $post_in_args['orderby'] = 'post__in';
2363 $post_in_args['ignore_sticky_posts'] = true;
2364 }
2365 }
2366
2367 return $post_in_args;
2368 }
2369
2370 /**
2371 * Filter by parent.
2372 *
2373 * @param string $parent
2374 * @return array
2375 */
2376 private function query_loop_filter_by_parent( $parent ) {
2377 $parent_args = [];
2378 if ( $parent ) {
2379 $parent_ids = explode( ',', $parent );
2380 if ( count( $parent_ids ) > 1 ) {
2381 $parent_args['post_parent__in'] = $parent_ids;
2382 } else {
2383 $parent_args['post_parent'] = $parent;
2384 }
2385 }
2386
2387 return $parent_args;
2388 }
2389
2390 /**
2391 * Filter by meta queries
2392 *
2393 * @param array $params
2394 * @return array
2395 */
2396 private function query_loop_filter_by_meta_queries( $params ) {
2397 $meta_query_args = [];
2398 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
2399 $queries = $params['metaQuery']['queries'] ?? [];
2400 if ( $queries && count( $queries ) > 0 ) {
2401 $queries = array_map(
2402 function ( $query ) {
2403 $refined_query = [
2404 'type' => strtoupper( $query['type'] ?? 'char' ),
2405 'compare' => $query['compare'] ?? '=',
2406 'key' => $query['key'] ?? '',
2407 'value' => $query['value'] ?? '',
2408 ];
2409
2410 if ( 'NUMERIC' === $refined_query['type'] ) {
2411 $refined_query['type'] = 'DECIMAL(10,3)';
2412 }
2413
2414 // No key.
2415 if ( ! $refined_query['key'] ) {
2416 return false;
2417 }
2418
2419 // Don't need a value.
2420 if ( in_array( $refined_query['compare'], [ 'EXISTS', 'NOT EXISTS' ], true ) ) {
2421 unset( $refined_query['value'] );
2422
2423 return $refined_query;
2424 }
2425
2426 $compare = $refined_query['compare'];
2427 $refined_value = $refined_query['value'];
2428
2429 // Get query string.
2430 $query_string = $query['query_string'] ?? '';
2431 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
2432 if ( $query_string && isset( $_GET[ $query_string ] ) ) {
2433 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
2434 $refined_value = sanitize_text_field( wp_unslash( $_GET[ $query_string ] ) );
2435 }
2436
2437 // No value.
2438 if ( $refined_value === '' ) {
2439 return false;
2440 }
2441
2442 if ( in_array( $compare, [ 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ], true ) ) {
2443 $refined_value = preg_split( '/,\s*(?=(?:[^"]*"[^"]*")*[^"]*$)/', $refined_value, -1, PREG_SPLIT_NO_EMPTY );
2444 $refined_value = array_filter(
2445 array_map(
2446 function ( $value ) {
2447 return trim( $value, '"' );
2448 },
2449 $refined_value
2450 )
2451 );
2452 }
2453
2454 if ( 'DATE' === $refined_query['type'] ) {
2455 $format = ! empty( $query['date_format'] ) ? $query['date_format'] : 'Y-m-d';
2456
2457 if ( is_array( $refined_value ) ) {
2458 $refined_value = array_filter(
2459 array_map(
2460 function ( $value ) {
2461 $formated_value = strtotime( $value );
2462
2463 // No valid date string.
2464 if ( ! $formated_value ) {
2465 return false;
2466 }
2467
2468 if ( 'timestamp' !== $format ) {
2469 $formated_value = gmdate( $format, $formated_value );
2470 }
2471
2472 return $formated_value;
2473 },
2474 $refined_value
2475 )
2476 );
2477 } else {
2478 $refined_value = strtotime( $refined_value );
2479
2480 if ( 'timestamp' !== $format ) {
2481 $refined_value = gmdate( $format, $refined_value );
2482 }
2483 }
2484
2485 // No valid value.
2486 if ( ! $refined_value ) {
2487 return false;
2488 }
2489
2490 if ( 'timestamp' === $format ) {
2491 $refined_query['type'] = 'NUMERIC';
2492 }
2493 }
2494
2495 // Update value.
2496 $refined_query['value'] = $refined_value;
2497
2498 return $refined_query;
2499 },
2500 $queries
2501 );
2502
2503 $queries = array_filter( $queries );
2504
2505 if ( count( $queries ) > 0 ) {
2506 // Maximize 5 items.
2507 $queries = array_slice( $queries, 0, 5 );
2508
2509 $refined_queries = [];
2510 $instances = [];
2511 foreach ( $queries as $query ) {
2512 $key = $query['key'];
2513 if ( ! isset( $refined_queries[ $key ] ) ) {
2514 $refined_queries[ $key ] = $query;
2515 $instances[ $key ] = 1;
2516 } else {
2517 $refined_queries[ $key . '_' . $instances[ $key ] ] = $query;
2518 $instances[ $key ] = absint( $instances[ $key ] ) + 1;
2519 }
2520 }
2521
2522 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
2523 $meta_query_args['meta_query'] = $refined_queries;
2524 if ( count( $queries ) > 1 ) {
2525 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
2526 $meta_query_args['meta_query']['relation'] = $params['metaQuery']['relation'] ?? 'AND';
2527 }
2528 }
2529 }
2530
2531 return $meta_query_args;
2532 }
2533
2534 /**
2535 * Filter by context.
2536 *
2537 * @param array $params
2538 * @return array
2539 */
2540 private function query_loop_filter_by_context( $params, $post ) {
2541 $context_args = [];
2542
2543 if ( $params['ignoreCurrentPost'] ?? false ) {
2544 // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in
2545 $context_args['post__not_in'] = [ $post->ID ];
2546 }
2547
2548 if ( $params['queryRelatedPosts'] ?? false ) {
2549 $taxonomies = \get_object_taxonomies( $post, 'names' );
2550
2551 $tax_queries = [];
2552 foreach ( $taxonomies as $taxonomy ) {
2553 $terms = \get_the_terms( $post->ID, $taxonomy );
2554 if ( empty( $terms ) ) {
2555 continue;
2556 }
2557 $term_ids = \wp_list_pluck( $terms, 'term_id' );
2558 $tax_queries[] = array(
2559 'taxonomy' => $taxonomy,
2560 'terms' => $term_ids,
2561 );
2562 }
2563
2564 if ( count( $tax_queries ) > 1 ) {
2565 $tax_queries['relation'] = 'OR';
2566 }
2567
2568 if ( $tax_queries ) {
2569 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
2570 $context_args['tax_query'] = $tax_queries;
2571 }
2572 }
2573
2574 return $context_args;
2575 }
2576
2577 /**
2578 * Custom sorting.
2579 *
2580 * @param array $sorting
2581 * @return array
2582 */
2583 private function query_loop_custom_sort( $sorting ) {
2584 $sort_args = [];
2585
2586 if ( $sorting && is_array( $sorting ) ) {
2587 if ( 'rand' === $sorting[0] ) {
2588 $sort_args['orderby'] = 'rand';
2589 } else {
2590 $order_by = [];
2591 foreach ( $sorting as $item ) {
2592 if ( ! $item || 'rand' === $item ) {
2593 continue;
2594 }
2595
2596 $item_array = explode( '/', $item );
2597
2598 if ( count( $item_array ) !== 2 ) {
2599 continue;
2600 }
2601
2602 if ( isset( $order_by[ $item_array[0] ] ) || ! in_array( strtoupper( $item_array[1] ), [ 'ASC', 'DESC' ], true ) ) {
2603 continue;
2604 }
2605
2606 $order_by[ $item_array[0] ] = strtoupper( $item_array[1] );
2607 }
2608
2609 if ( $order_by ) {
2610 $sort_args['orderby'] = $order_by;
2611 }
2612 }
2613 }
2614
2615 return $sort_args;
2616 }
2617
2618 /**
2619 * Add custom query vars.
2620 *
2621 * @param array $vars
2622 * @return array
2623 */
2624 public function query_loop_query_vars( $vars ) {
2625 $vars[] = 'cbb';
2626
2627 return $vars;
2628 }
2629
2630 /**
2631 * Alter the result of the query loop
2632 *
2633 * @param array $posts
2634 * @param WP_Query $query
2635 * @return array
2636 */
2637 public function query_loop_the_posts( $posts, $query ) {
2638 if ( $query->query_vars['cbb'] ?? false ) {
2639 $cbb_args = $query->query_vars['cbb'] ?? [];
2640 $posts_per_page = $query->query_vars['posts_per_page'] ?? 1;
2641 $found_posts = $query->found_posts;
2642
2643 if ( ( $cbb_args['queryFallbackPosts'] ?? false ) && $found_posts < $posts_per_page ) {
2644 $ignore_posts = [];
2645 if ( $found_posts > 0 ) {
2646 $ignore_posts = wp_list_pluck( $query->posts, 'ID' );
2647 }
2648
2649 $fallback_args = [
2650 'post_type' => $query->query_vars['post_type'] ?? 'any',
2651 'post_status' => 'publish',
2652 'posts_per_page' => $posts_per_page - $found_posts,
2653 'ignore_sticky_posts' => 1,
2654 // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in
2655 'post__not_in' => array_merge( $query->query_vars['post__not_in'] ?? [], $ignore_posts ),
2656 'no_found_rows' => true,
2657 ];
2658
2659 $fallback_posts = get_posts( $fallback_args );
2660
2661 if ( $fallback_posts ) {
2662 $posts = array_merge( $posts, $fallback_posts );
2663 }
2664 }
2665 }
2666
2667 return $posts;
2668 }
2669
2670 /**
2671 * Get the current post
2672 *
2673 * @return string
2674 */
2675 public function get_current_post() {
2676 global $pagenow;
2677 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
2678 $post_id = 'post.php' === $pagenow && isset( $_GET['post'] ) ? absint( $_GET['post'] ) : 0;
2679 $post = $post_id ? get_post( $post_id ) : false;
2680
2681 return $post instanceof \WP_Post ? $post : false;
2682 }
2683
2684 /**
2685 * Get animated blocks
2686 */
2687 public function get_animated_blocks() {
2688 return apply_filters(
2689 'cbb_get_animated_blocks',
2690 [
2691 'core/heading',
2692 'core/paragraph',
2693 'core/image',
2694 'core/button',
2695 'core/buttons',
2696 'core/list-item',
2697 'core/list',
2698 'core/group',
2699 'core/columns',
2700 'core/column',
2701 'core/cover',
2702 'core/media-text',
2703 'core/post-title',
2704 'core/post-excerpt',
2705 'core/post-terms',
2706 'core/readmore',
2707 'boldblocks/svg-block',
2708 ]
2709 );
2710 }
2711
2712 /**
2713 * Get max items
2714 */
2715 public function get_max_items() {
2716 return apply_filters( 'cbb_get_max_items', $this->max_items, $this->post_type );
2717 }
2718 }
2719 endif;
2720