PluginProbe
Content Blocks Builder – Create blocks, repeater blocks with carousel, grid, popup layouts / 2.8.5
Content Blocks Builder – Create blocks, repeater blocks with carousel, grid, popup layouts v2.8.5
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.5, at includes/custom-blocks.php

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