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

2,716 lines 82.6 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 ( ! empty( $args['custom_scripts'] ) && ! $is_backend ) {
922 $custom_scripts = $args['custom_scripts'];
923
924 // Refine custom scripts.
925 $custom_scripts = array_reduce(
926 $custom_scripts,
927 function ( $carry, $script ) {
928 // Skip if script is empty.
929 if ( ! $script || ! is_array( $script ) || empty( $script['value'] ) ) {
930 return $carry;
931 }
932
933 // Refine script type.
934 $script_type = $script['script_type'] ?? '';
935
936 // Legacy support for script_type.
937 if ( ! $script_type ) {
938 if ( $script['is_module'] ?? false ) {
939 $script_type = 'module';
940 } elseif ( ! empty( $script['handle'] ) ) {
941 $script_type = 'custom';
942 } else {
943 $script_type = 'default';
944 }
945
946 $script['script_type'] = $script_type;
947 }
948
949 // If custom type but no handle, change to default.
950 if ( 'custom' === $script_type && empty( $script['handle'] ) ) {
951 $script['script_type'] = 'default';
952 }
953
954 $carry[] = $script;
955
956 return $carry;
957 },
958 []
959 );
960
961 $default_handle_scripts = array_filter(
962 $custom_scripts,
963 function ( $script ) {
964 return 'custom' !== $script['script_type'];
965 }
966 );
967
968 if ( $default_handle_scripts ) {
969 $dep_handles = array_reduce(
970 $default_handle_scripts,
971 function ( $carry, $script ) {
972 if ( 'module' === $script['script_type'] ) {
973 $carry['has_module'] = true;
974 } elseif ( ! empty( $script['deps'] ) ) {
975 $deps = explode( ',', $script['deps'] );
976 foreach ( $deps as $dep ) {
977 $dep = trim( $dep );
978 if ( ! $dep || in_array( $dep, $carry['deps'], true ) ) {
979 continue;
980 }
981
982 $carry['deps'][] = $dep;
983 }
984 }
985
986 return $carry;
987 },
988 [
989 'has_module' => false,
990 'deps' => [],
991 ]
992 );
993
994 if ( $dep_handles['has_module'] ) {
995 // Register the default handle for module scripts.
996 wp_register_script( $block_module_inline_handle, '', [], BOLDBLOCKS_CBB_VERSION, [ 'in_footer' => true ] );
997 }
998
999 if ( ! in_array( 'CBB_BLOCK_API', $dep_handles['deps'], true ) ) {
1000 array_unshift( $dep_handles['deps'], 'CBB_BLOCK_API' );
1001 }
1002
1003 // Register the default handle for inline scripts.
1004 wp_register_script( $block_inline_handle, '', explode( ',', $this->refine_script_handle( implode( ',', $dep_handles['deps'] ), $is_backend ) ), BOLDBLOCKS_CBB_VERSION, [ 'in_footer' => true ] );
1005 }
1006
1007 foreach ( $custom_scripts as $script ) {
1008 $script_type = $script['script_type'];
1009
1010 // Determine if a custom handle should be used.
1011 if ( 'custom' === $script_type ) {
1012 $handle = $this->refine_script_handle( $script['handle'], $is_backend );
1013 } else {
1014 $handle = 'module' === $script_type ? $block_module_inline_handle : $block_inline_handle;
1015
1016 // Add handle to view scripts and editor scripts if backend.
1017 if ( ! in_array( $handle, $handles['view_script_handles'], true ) ) {
1018 $handles['view_script_handles'][] = $handle;
1019 }
1020 if ( $is_backend ) {
1021 if ( ! in_array( $handle, $handles['editor_script_handles'], true ) ) {
1022 $handles['editor_script_handles'][] = $handle;
1023 }
1024 }
1025 }
1026
1027 // Add inline script.
1028 wp_add_inline_script( $handle, $script['value'] );
1029 }
1030 }
1031
1032 if ( ! empty( $args['external_styles'] ) ) {
1033 $external_styles = array_filter(
1034 $args['external_styles'],
1035 function ( $style ) {
1036 return $style['handle'] ?? false;
1037 }
1038 );
1039
1040 foreach ( $external_styles as $style ) {
1041 if ( $style['deps'] ?? false ) {
1042 $deps = $this->refine_style_handle( $style['deps'], $is_backend );
1043 $deps = \explode( ',', $deps );
1044 } else {
1045 $deps = [];
1046 }
1047 $version = $style['version'] ?? null;
1048 if ( $version ) {
1049 if ( 'WP' === $version ) {
1050 $version = '';
1051 } elseif ( 'CBB' === $version ) {
1052 $version = BOLDBLOCKS_CBB_VERSION;
1053 }
1054 }
1055 // Don't load external resources on admin side for some cases.
1056 if ( ! $is_backend || ! ( $style['src'] ?? '' ) || ( $style['in_backend'] ?? false ) ) {
1057 wp_register_style( $style['handle'], $style['src'] ?? '', $deps, $version, $style['media'] ?? 'all' );
1058
1059 // Add the handle to the block.
1060 $handles['style_handles'][] = $style['handle'];
1061 }
1062 }
1063 }
1064
1065 if ( ! empty( $args['custom_styles'] ) ) {
1066 $custom_styles = $args['custom_styles'];
1067
1068 // Hanle preview style for wrapper blocks of core/HTML.
1069 $is_preview = ! empty( $args['dependentBlocks'] ) && is_array( $args['dependentBlocks'] ) && count( $args['dependentBlocks'] ) === 1 && 'core/html' === ( $args['dependentBlocks'][0] ?? '' );
1070
1071 foreach ( $custom_styles as $style ) {
1072 if ( $style && is_array( $style ) && $style['value'] ) {
1073 $snippet_type = $style['type'] ?? 'all';
1074 if ( ( 'view' === $snippet_type && $is_backend ) || ( 'editor' === $snippet_type && ! $is_backend ) ) {
1075 continue;
1076 }
1077
1078 $value = $style['value'];
1079 if ( $is_preview ) {
1080 $this->html_editor_style .= $value;
1081 }
1082
1083 if ( empty( $style['handle'] ) ) {
1084 $handle = $block_inline_handle;
1085 // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
1086 wp_register_style( $handle, '' );
1087
1088 // Add the default handle to the block.
1089 $handles['style_handles'][] = $handle;
1090 } else {
1091 $handle = $this->refine_style_handle( $style['handle'], $is_backend );
1092 }
1093 wp_add_inline_style( $handle, $value );
1094 }
1095 }
1096 }
1097
1098 // Hide on frontend.
1099 $block_atts['supports']['hideOnFrontend'] = $args['hideOnFrontend'] ?? false;
1100
1101 // Block keywords.
1102 if ( $args['keywords'] ?? false ) {
1103 $block_atts['keywords'] = $args['keywords'];
1104 }
1105
1106 // Synced block overrides.
1107 if ( $args['isSyncedBlock'] ?? false ) {
1108 $block_atts['supports']['block_id'] = $args['id'];
1109 $block_atts['supports']['block_content'] = $args['postContent'];
1110 $block_atts['render_callback'] = [ $this, 'render_readonly_block' ];
1111 }
1112
1113 // Enable interactivity.
1114 $block_atts['supports']['interactivity'] = [ 'clientNavigation' => true ];
1115
1116 register_block_type( $name, array_merge( $block_atts, $handles ) );
1117 }
1118 }
1119
1120 /**
1121 * Make sure the block name is valid
1122 *
1123 * @param string $name
1124 * @return string
1125 */
1126 private function sanitize_block_name( $name ) {
1127 return sanitize_key( str_replace( '_', '-', $name ) );
1128 }
1129
1130 /**
1131 * Handle script handle
1132 *
1133 * @param string $handle
1134 * @param boolean $is_backend
1135 * @return string
1136 */
1137 private function refine_script_handle( $handle, $is_backend = false ) {
1138 if ( $handle ) {
1139 $handle = str_replace( ' ', '', $handle );
1140 $handle = str_replace( 'CBB_BLOCK_API', $is_backend ? $this->custom_blocks_handle : $this->custom_blocks_frontend_handle, $handle );
1141 $handle = str_replace( 'CBB_CAROUSEL_API', $is_backend ? $this->custom_blocks_handle : $this->carousel_blocks_frontend_handle, $handle );
1142 }
1143
1144 return $handle;
1145 }
1146
1147 /**
1148 * Handle style handle
1149 *
1150 * @param string $handle
1151 * @param boolean $is_backend
1152 * @return string
1153 */
1154 private function refine_style_handle( $handle, $is_backend = false ) {
1155 if ( $handle ) {
1156 $handle = str_replace( 'CBB_BLOCK_STYLE', $this->custom_blocks_handle, $handle );
1157 $handle = str_replace( 'CBB_CAROUSEL_STYLE', $is_backend ? $this->custom_blocks_handle : $this->carousel_blocks_frontend_handle, $handle );
1158 }
1159
1160 return $handle;
1161 }
1162
1163 /**
1164 * Register code editor scripts
1165 *
1166 * @return void
1167 */
1168 public function register_code_editor_scripts() {
1169 $screen = get_current_screen();
1170 if ( $screen && $screen->is_block_editor ) {
1171 wp_enqueue_code_editor( array( 'type' => 'text/css' ) );
1172 wp_enqueue_code_editor( array( 'type' => 'javascript' ) );
1173 }
1174 }
1175
1176 /**
1177 * Adjust the src of the esprima script.
1178 *
1179 * @param string $src The source URL of the script.
1180 * @param string $handle The handle of the script.
1181 * @return string The adjusted source URL of the script.
1182 */
1183 public function adjust_esprima_script( $src, $handle ) {
1184 if ( 'esprima' === $handle ) {
1185 $src = 'https://cdn.jsdelivr.net/npm/esprima-next@5.8.4/dist/esprima.js';
1186 }
1187
1188 return $src;
1189 }
1190
1191 /**
1192 * Register scripts
1193 *
1194 * @return void
1195 */
1196 public function register_scripts() {
1197 // Custom blocks asset file.
1198 $custom_blocks_asset = $this->the_plugin_instance->include_file( "build/custom-blocks{$this->the_plugin_instance->get_script_suffix()}.asset.php" );
1199
1200 // Scripts.
1201 wp_register_script(
1202 $this->custom_blocks_handle,
1203 $this->the_plugin_instance->get_file_uri( "build/custom-blocks{$this->the_plugin_instance->get_script_suffix()}.js" ),
1204 array_merge( $custom_blocks_asset['dependencies'] ?? [], array( 'code-editor', 'csslint', 'jshint' ) ),
1205 $this->the_plugin_instance->get_script_version( $custom_blocks_asset ),
1206 [ 'in_footer' => true ]
1207 );
1208
1209 // Add translation.
1210 wp_set_script_translations( $this->custom_blocks_handle, 'content-blocks-builder' );
1211
1212 // Get all blocks.
1213 $all_custom_blocks = $this->get_all_custom_blocks();
1214 $current_post = $this->get_current_post();
1215 if ( $current_post && $this->post_type === $current_post->post_type ) {
1216 // Hide the current block.
1217 $all_custom_blocks = array_map(
1218 function ( $block ) use ( $current_post ) {
1219 if ( $block['id'] === $current_post->ID ) {
1220 $block['isHidden'] = true;
1221 }
1222 return $block;
1223 },
1224 $all_custom_blocks
1225 );
1226 }
1227
1228 // Define data.
1229 $blocks_scripts = [
1230 'blocks' => $all_custom_blocks,
1231 'variations' => $this->the_plugin_instance->get_component( Variations::class )->get_all_variations(),
1232 'variationNonce' => wp_create_nonce( 'cbb_variation_nonce' ),
1233 'animations' => $this->get_animated_blocks(),
1234 ];
1235
1236 // Get block name for current variation.
1237 $block_name = $this->the_plugin_instance->get_component( Variations::class )->get_block_name();
1238 if ( $block_name ) {
1239 $blocks_scripts['variationBlockName'] = $block_name;
1240 }
1241
1242 // Load all custom blocks for registration.
1243 wp_add_inline_script( $this->custom_blocks_handle, 'var CBBBlocks=' . wp_json_encode( $blocks_scripts ), 'before' );
1244
1245 // Frontend styles.
1246 wp_register_style(
1247 $this->custom_blocks_handle,
1248 $this->the_plugin_instance->get_file_uri( "build/custom-blocks{$this->the_plugin_instance->get_script_suffix()}.css" ),
1249 [],
1250 $this->the_plugin_instance->get_script_version( $custom_blocks_asset )
1251 );
1252
1253 // Editor styles.
1254 wp_register_style(
1255 $this->custom_blocks_editor_handle,
1256 $this->the_plugin_instance->get_file_uri( 'build/custom-blocks-editor.css' ),
1257 [],
1258 $this->the_plugin_instance->get_script_version( $custom_blocks_asset )
1259 );
1260
1261 // Custom blocks asset file.
1262 $custom_blocks_frontend_asset = $this->the_plugin_instance->include_file( "build/custom-blocks-frontend{$this->the_plugin_instance->get_script_suffix()}.asset.php" );
1263
1264 // Frontend script.
1265 wp_register_script(
1266 $this->custom_blocks_frontend_handle,
1267 $this->the_plugin_instance->get_file_uri( "build/custom-blocks-frontend{$this->the_plugin_instance->get_script_suffix()}.js" ),
1268 $custom_blocks_frontend_asset['dependencies'] ?? [],
1269 $this->the_plugin_instance->get_script_version( $custom_blocks_frontend_asset ),
1270 [ 'in_footer' => true ]
1271 );
1272
1273 // Add translation.
1274 wp_set_script_translations( $this->custom_blocks_frontend_handle, 'content-blocks-builder' );
1275
1276 // Carousel asset file.
1277 $caousel_frontend_asset = $this->the_plugin_instance->include_file( 'build/carousel-frontend.asset.php' );
1278
1279 // Carousel styles.
1280 wp_register_style(
1281 $this->carousel_blocks_frontend_handle,
1282 $this->the_plugin_instance->get_file_uri( 'build/carousel-frontend.css' ),
1283 [],
1284 $this->the_plugin_instance->get_script_version( $caousel_frontend_asset )
1285 );
1286
1287 // Carousel frontend script.
1288 wp_register_script(
1289 $this->carousel_blocks_frontend_handle,
1290 $this->the_plugin_instance->get_file_uri( 'build/carousel-frontend.js' ),
1291 $caousel_frontend_asset['dependencies'] ?? [],
1292 $this->the_plugin_instance->get_script_version( $caousel_frontend_asset ),
1293 [ 'in_footer' => true ]
1294 );
1295
1296 // Add translation.
1297 wp_set_script_translations( $this->carousel_blocks_frontend_handle, 'content-blocks-builder' );
1298 }
1299
1300 /**
1301 * Get all custom blocks.
1302 *
1303 * @return array
1304 */
1305 public function get_all_custom_blocks() {
1306 if ( ! $this->all_custom_blocks ) {
1307 $this->all_custom_blocks = $this->get_posts( $this->the_plugin_instance->is_debug_mode() );
1308 }
1309
1310 return $this->all_custom_blocks;
1311 }
1312
1313 /**
1314 * Query posts and cache the results.
1315 *
1316 * @param bool $force_refresh Optional. Whether to force the cache to be refreshed. Default false.
1317 * @return array Array of WP_Post objects.
1318 */
1319 public function get_posts( $force_refresh = false ) {
1320 if ( $force_refresh ) {
1321 return $this->query_posts();
1322 }
1323
1324 $cache_key = 'bb_block_posts';
1325 $posts = get_transient( $cache_key );
1326
1327 // If nothing is found, build the object.
1328 if ( false === $posts ) {
1329 // Query posts.
1330 $posts = $this->query_posts();
1331
1332 if ( ! is_wp_error( $posts ) && count( $posts ) > 0 ) {
1333 set_transient( $cache_key, $posts, HOUR_IN_SECONDS );
1334 }
1335 }
1336
1337 return $posts;
1338 }
1339
1340 /**
1341 * Query and parse blocks
1342 *
1343 * @return array
1344 */
1345 public function query_posts() {
1346 $post_args = apply_filters(
1347 'cbb_block_query_args',
1348 [
1349 'post_type' => $this->post_type,
1350 'posts_per_page' => $this->get_max_items(),
1351 'orderby' => [
1352 'menu_order' => 'DESC',
1353 'date' => 'DESC',
1354 ],
1355 ]
1356 );
1357
1358 $raw_posts = get_posts( $post_args );
1359
1360 $block_posts = array_map(
1361 function ( $item ) {
1362 // Template lock.
1363 $template_lock = get_post_meta( $item->ID, 'boldblocks_template_lock', true );
1364
1365 // Convert boolean string to boolean.
1366 $template_lock = in_array( $template_lock, [ 'false', 'inherit' ], true ) ? false : $template_lock;
1367
1368 // Scripts & styles.
1369 $block_external_scripts = get_post_meta( $item->ID, 'boldblocks_block_external_scripts', true );
1370 $block_custom_scripts = get_post_meta( $item->ID, 'boldblocks_block_custom_scripts', true );
1371 $block_external_styles = get_post_meta( $item->ID, 'boldblocks_block_external_styles', true );
1372 $block_custom_styles = get_post_meta( $item->ID, 'boldblocks_block_custom_styles', true );
1373
1374 // Hide on frontend.
1375 $hide_on_frontend = (bool) get_post_meta( $item->ID, 'boldblocks_hide_on_frontend', true );
1376
1377 // Is a readonly block.
1378 $is_readonly = (bool) get_post_meta( $item->ID, 'boldblocks_is_readonly', true );
1379
1380 // Is block transformable.
1381 $is_transformable = (bool) get_post_meta( $item->ID, 'boldblocks_is_transformable', true );
1382
1383 // Is block hidden.
1384 $is_hidden = (bool) get_post_meta( $item->ID, 'boldblocks_is_hidden', true );
1385
1386 // Parent & ancestor.
1387 $block_parent = get_post_meta( $item->ID, 'boldblocks_block_parent', true );
1388 $block_parent = $block_parent ? array_filter( explode( ',', $block_parent ) ) : [];
1389 $block_ancestor = get_post_meta( $item->ID, 'boldblocks_block_ancestor', true );
1390 $block_ancestor = $block_ancestor ? array_filter( explode( ',', $block_ancestor ) ) : [];
1391 $allowed_blocks = get_post_meta( $item->ID, 'boldblocks_block_allowed_blocks', true );
1392 $allowed_blocks = $allowed_blocks ? array_filter( explode( ',', $allowed_blocks ) ) : [];
1393
1394 $keywords = get_the_terms( $item->ID, 'boldblocks_block_keywords' );
1395 if ( $keywords && ! is_wp_error( $keywords ) ) {
1396 $keywords = wp_list_pluck( $keywords, 'name' );
1397 } else {
1398 $keywords = false;
1399 }
1400
1401 $block_name = sprintf( 'boldblocks/%s', $this->sanitize_block_name( $item->post_name ) );
1402 $block_version = get_post_meta( $item->ID, 'boldblocks_block_version', true );
1403 $migrateClass = version_compare( $block_version, '2.7.14', '<=' );
1404
1405 $block_args = [
1406 'id' => $item->ID,
1407 'name' => $block_name,
1408 'title' => wp_strip_all_tags( $item->post_title ),
1409 'postContent' => $item->post_content,
1410 'blocks' => get_post_meta( $item->ID, 'boldblocks_block_blocks', true ),
1411 'description' => get_post_meta( $item->ID, 'boldblocks_block_description', true ),
1412 'templateLock' => $template_lock,
1413 'className' => get_post_meta( $item->ID, 'boldblocks_block_class', true ),
1414 'blockIcon' => get_post_meta( $item->ID, 'boldblocks_block_icon', true ),
1415 'blockSupports' => get_post_meta( $item->ID, 'boldblocks_block_supports', true ),
1416 'blockAttributes' => [
1417 'attributes' => get_post_meta( $item->ID, 'boldblocks_block_attributes', true ),
1418 'enableCustomAttributes' => (bool) get_post_meta( $item->ID, 'boldblocks_enable_custom_attributes', true ),
1419 ],
1420 'enableVariationPicker' => (bool) get_post_meta( $item->ID, 'boldblocks_enable_variation_picker', true ),
1421 'blockVersion' => $block_version,
1422 'migrateClass' => $migrateClass,
1423 'dependentBlocks' => get_post_meta( $item->ID, 'boldblocks_block_dependent_blocks', true ),
1424 'hideOnFrontend' => $hide_on_frontend,
1425 'isTransformable' => $is_transformable,
1426 'isHidden' => $is_hidden,
1427 'isSyncedBlock' => $is_readonly, // Only for standalone blocks.
1428 'allowedBlocks' => $allowed_blocks,
1429 ];
1430
1431 // Get parent block parameters.
1432 $enable_repeater = get_post_meta( $item->ID, 'boldblocks_enable_repeater', true );
1433 if ( $enable_repeater ) {
1434 $parent_name = get_post_meta( $item->ID, 'boldblocks_parent_block_name', true );
1435 if ( ! $parent_name ) {
1436 $parent_name = $item->post_name;
1437 }
1438
1439 $parent_block_name = sprintf( 'boldblocks/%s-repeater', $this->sanitize_block_name( $parent_name ) );
1440
1441 $parent_layout_type = get_post_meta( $item->ID, 'boldblocks_parent_layout_type', true );
1442
1443 $parent_title = get_post_meta( $item->ID, 'boldblocks_parent_block_title', true );
1444 if ( ! $parent_title ) {
1445 // translators: Repeater block title.
1446 $parent_title = sprintf( __( 'Repeater: %s', 'content-blocks-builder' ), $item->post_title );
1447 }
1448
1449 $parent_args = [
1450 'name' => $parent_block_name,
1451 'title' => wp_strip_all_tags( $parent_title ),
1452 'description' => get_post_meta( $item->ID, 'boldblocks_parent_block_description', true ),
1453 'className' => get_post_meta( $item->ID, 'boldblocks_parent_block_class', true ) ?? '',
1454 'blockIcon' => get_post_meta( $item->ID, 'boldblocks_parent_block_icon', true ),
1455 'blockSupports' => get_post_meta( $item->ID, 'boldblocks_parent_block_supports', true ),
1456 'blockAttributes' => [
1457 'attributes' => get_post_meta( $item->ID, 'boldblocks_parent_block_attributes', true ),
1458 'enableCustomAttributes' => (bool) get_post_meta( $item->ID, 'boldblocks_parent_enable_custom_attributes', true ),
1459 ],
1460 'enableVariationPicker' => (bool) get_post_meta( $item->ID, 'boldblocks_parent_enable_variation_picker', true ),
1461 'external_scripts' => $block_external_scripts,
1462 'custom_scripts' => $this->refine_custom_scripts( $block_custom_scripts, 'JS', $parent_block_name, $block_name ),
1463 'external_styles' => $block_external_styles,
1464 'custom_styles' => $this->refine_custom_scripts( $block_custom_styles, 'CSS', $parent_block_name, $block_name ),
1465 'hideOnFrontend' => $hide_on_frontend,
1466 'blockParent' => $block_parent,
1467 'blockAncestor' => $block_ancestor,
1468 'migrateClass' => $migrateClass,
1469
1470 ];
1471
1472 if ( $keywords ) {
1473 $parent_args['keywords'] = $keywords;
1474 }
1475
1476 $is_fixed_nested_item_count = get_post_meta( $item->ID, 'boldblocks_is_fixed_nested_item_count', true );
1477 if ( $is_fixed_nested_item_count ) {
1478 $nested_item_count = get_post_meta( $item->ID, 'boldblocks_nested_item_count', true );
1479 $nested_item_count = $nested_item_count ? absint( $nested_item_count ) : 1;
1480 $parent_args['nestedItemCount'] = $nested_item_count ? $nested_item_count : 1;
1481 }
1482
1483 $parent_args['layoutType'] = $parent_layout_type;
1484 $block_args['parent'] = $parent_args;
1485 $block_args['parentName'] = $parent_block_name;
1486
1487 $block_args['standalone'] = ! get_post_meta( $item->ID, 'boldblocks_disable_standalone', true );
1488 } else {
1489 if ( $keywords ) {
1490 $block_args['keywords'] = $keywords;
1491 }
1492
1493 $block_args = array_merge(
1494 $block_args,
1495 [
1496 'external_scripts' => $block_external_scripts,
1497 'custom_scripts' => $this->refine_custom_scripts( $block_custom_scripts, 'JS', $block_name ),
1498 'external_styles' => $block_external_styles,
1499 'custom_styles' => $this->refine_custom_scripts( $block_custom_styles, 'CSS', $block_name ),
1500 'blockParent' => $block_parent,
1501 'blockAncestor' => $block_ancestor,
1502 ]
1503 );
1504 }
1505
1506 return $block_args;
1507 },
1508 $raw_posts
1509 );
1510
1511 return $block_posts;
1512 }
1513
1514 /**
1515 * Refine custom scripts & styles
1516 *
1517 * @param array $custom_scripts
1518 * @param string $type
1519 * @param string $block_name
1520 * @return array
1521 */
1522 private function refine_custom_scripts( $custom_scripts, $type, $block_name, $child_block_name = '' ) {
1523 if ( $custom_scripts && is_array( $custom_scripts ) ) {
1524 $custom_style_handler = $this->the_plugin_instance->get_component( CustomStyle::class );
1525 $block_class = '.wp-block-' . str_replace( '/', '-', $block_name );
1526 $child_block_class = $child_block_name ? '.wp-block-' . str_replace( '/', '-', $child_block_name ) : '';
1527
1528 $custom_scripts = array_filter(
1529 $custom_scripts,
1530 function ( $script ) {
1531 return ! empty( $script['value'] );
1532 }
1533 );
1534
1535 $custom_scripts = array_map(
1536 function ( $script ) use ( $custom_style_handler, $block_class, $child_block_class, $type ) {
1537 $tokens = [];
1538 if ( 'CSS' === $type ) {
1539 if ( $child_block_class ) {
1540 $tokens['childblockselector'] = $child_block_class;
1541 }
1542 $tokens['selector'] = $block_class;
1543 } else {
1544 if ( $child_block_class ) {
1545 $tokens['CHILDBLOCKSELECTOR'] = $child_block_class;
1546 }
1547 $tokens['BLOCKSELECTOR'] = $block_class;
1548 }
1549
1550 $script['value'] = $custom_style_handler->refine_custom_value( $script['value'], $tokens, $type );
1551
1552 return $script;
1553 },
1554 $custom_scripts
1555 );
1556 }
1557
1558 return $custom_scripts;
1559 }
1560
1561 /**
1562 * Enqueue styles for html blocks in the editor
1563 *
1564 * @param array $editor_settings
1565 * @return void
1566 */
1567 public function enqueue_html_style_for_the_editor( $editor_settings ) {
1568 if ( $this->html_editor_style ) {
1569 $editor_settings['styles'][] = [ 'css' => $this->html_editor_style ];
1570 }
1571
1572 return $editor_settings;
1573 }
1574
1575 /**
1576 * Handle save block post
1577 *
1578 * @param int $post_id
1579 * @param WP_Post $post
1580 * @return void
1581 */
1582 public function save_post( $post_id, $post ) {
1583 // Ignore auto-draft status.
1584 if ( 'auto-draft' === $post->post_status ) {
1585 return;
1586 }
1587
1588 // Ignore autosave.
1589 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
1590 return;
1591 }
1592
1593 $this->clear_transient_cache();
1594 }
1595
1596 /**
1597 * Clear transient cache
1598 *
1599 * @return void
1600 */
1601 public function clear_transient_cache() {
1602 delete_transient( 'bb_block_posts' );
1603 }
1604
1605 /**
1606 * Add custom blocks to the list of blocks that can create patterns from.
1607 *
1608 * @param array $allowed_blocks
1609 * @return array
1610 */
1611 public function allow_creating_patterns( $allowed_blocks ) {
1612 return array_merge( $allowed_blocks, array_keys( $this->custom_blocks ), array_keys( $this->repeater_blocks ) );
1613 }
1614
1615 /**
1616 * Add custom columns to custom post type
1617 *
1618 * @param array $columns The array of columns.
1619 * @return array
1620 */
1621 public function add_block_custom_columns( $columns ) {
1622 $custom_columns = array(
1623 'block_icon' => esc_html__( 'Block icon', 'content-blocks-builder' ),
1624 'block_name' => esc_html__( 'Block name', 'content-blocks-builder' ),
1625 'parent_block_icon' => esc_html__( 'Parent icon', 'content-blocks-builder' ),
1626 'parent_block_name' => esc_html__( 'Parent block name', 'content-blocks-builder' ),
1627 );
1628
1629 if ( apply_filters( 'cbb_display_block_version', $this->the_plugin_instance->is_debug_mode() ) ) {
1630 $custom_columns['block_version'] = esc_html__( 'Block version', 'content-blocks-builder' );
1631 }
1632
1633 // Add after title column.
1634 $return = array_slice( $columns, 0, 2, true ) + $custom_columns + array_slice( $columns, 2, count( $columns ) - 2, true );
1635
1636 return $return;
1637 }
1638
1639 /**
1640 * Manage custom columns.
1641 *
1642 * @param string $column the column name.
1643 * @param int $post_id the post id.
1644 */
1645 public function manage_block_columns( $column, $post_id ) {
1646 switch ( $column ) {
1647 case 'block_icon':
1648 $block_icon = get_post_meta( $post_id, 'boldblocks_block_icon', true );
1649 if ( empty( $block_icon ) ) {
1650 $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>';
1651 }
1652 echo $block_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1653 break;
1654
1655 case 'block_name':
1656 echo esc_html( 'boldblocks/' . $this->sanitize_block_name( get_post_field( 'post_name', $post_id ) ) );
1657 break;
1658
1659 case 'parent_block_icon':
1660 if ( get_post_meta( $post_id, 'boldblocks_enable_repeater', true ) ) {
1661 $block_icon = get_post_meta( $post_id, 'boldblocks_parent_block_icon', true );
1662 if ( empty( $block_icon ) ) {
1663 $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>';
1664 }
1665 echo $block_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1666 } else {
1667 esc_html_e( 'NA', 'content-blocks-builder' );
1668 }
1669 break;
1670
1671 case 'parent_block_name':
1672 $enable_repeater = get_post_meta( $post_id, 'boldblocks_enable_repeater', true );
1673 if ( $enable_repeater ) {
1674 echo esc_html( 'boldblocks/' . $this->sanitize_block_name( get_post_field( 'post_name', $post_id ) ) . '-repeater' );
1675 } else {
1676 esc_html_e( 'NA', 'content-blocks-builder' );
1677 }
1678 break;
1679
1680 case 'block_version':
1681 $block_version = get_post_meta( $post_id, 'boldblocks_block_version', true );
1682 if ( $block_version ) {
1683 echo esc_html( $block_version );
1684 } else {
1685 echo esc_html( '1.x' );
1686 }
1687 break;
1688
1689 // Just break out of the switch statement for everything else.
1690 default:
1691 break;
1692 }
1693 }
1694
1695 /**
1696 * Add style to custom columns
1697 *
1698 * @return void
1699 */
1700 public function add_block_custom_columns_style() {
1701 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>';
1702 }
1703
1704 /**
1705 * Add custom block's meta fields to meta revisioning.
1706 *
1707 * @param array $fields
1708 * @return array
1709 */
1710 public function add_fields_to_revision_meta_keys( $fields ) {
1711 return array_merge(
1712 $fields,
1713 [
1714 'boldblocks_block_blocks',
1715 'boldblocks_block_class',
1716 'boldblocks_block_description',
1717 'boldblocks_block_icon',
1718 'boldblocks_block_supports',
1719 'boldblocks_template_lock',
1720 'boldblocks_enable_variation_picker',
1721 'boldblocks_enable_repeater',
1722 'boldblocks_parent_layout_type',
1723 'boldblocks_parent_block_icon',
1724 'boldblocks_parent_block_supports',
1725 'boldblocks_parent_enable_variation_picker',
1726 'boldblocks_disable_standalone',
1727 'boldblocks_is_fixed_nested_item_count',
1728 'boldblocks_nested_item_count',
1729 'boldblocks_parent_block_name',
1730 'boldblocks_parent_block_title',
1731 'boldblocks_parent_block_description',
1732 'boldblocks_parent_block_class',
1733 'boldblocks_block_custom_scripts',
1734 'boldblocks_block_custom_styles',
1735 'boldblocks_block_attributes',
1736 'boldblocks_parent_block_attributes',
1737 ]
1738 );
1739 }
1740
1741 /**
1742 * Add theme name to body class
1743 *
1744 * @param array $classes
1745 * @return array
1746 */
1747 public function add_body_class( $classes ) {
1748 // Add a frontend CSS class.
1749 $classes[] = 'cbb-frontend';
1750
1751 // Add theme slug.
1752 $classes[] = get_stylesheet();
1753
1754 return $classes;
1755 }
1756
1757 /**
1758 * Save the plugin version to custom blocks
1759 *
1760 * @param int $post_id
1761 * @param WP_Post $post
1762 * @param boolean $update
1763 * @return void
1764 */
1765 public function save_version_to_block( $post_id, $post, $update ) {
1766 // Bail if it is a update action.
1767 if ( $update ) {
1768 return;
1769 }
1770
1771 // Save the plugin version to blocks.
1772 update_post_meta( $post_id, 'boldblocks_block_version', $this->the_plugin_instance->get_plugin_version() );
1773 }
1774
1775 /**
1776 * Prevent users from deleting core blocks.
1777 *
1778 * @param array $allcaps
1779 * @param array $caps
1780 * @param array $args
1781 * @return array
1782 */
1783 public function prevent_core_blocks_from_deletion( $allcaps, $caps, $args ) {
1784 // Bail out if we're not asking about deleting a post.
1785 if ( 'delete_post' !== $args[0] ) {
1786 return $allcaps;
1787 }
1788
1789 // Bail out for users who can't delete posts.
1790 if ( empty( $allcaps['delete_posts'] ) ) {
1791 return $allcaps;
1792 }
1793
1794 // Load the post data.
1795 $post = get_post( $args[2] );
1796
1797 // Bail out if the post type is not boldblocks_block.
1798 if ( $this->post_type !== $post->post_type ) {
1799 return $allcaps;
1800 }
1801
1802 // Don't allow deleting core blocks.
1803 if ( \in_array( $post->post_name, [ 'group', 'carousel-item', 'grid-item', 'stack-item', 'accordion-item' ], true ) ) {
1804 $allcaps[ $caps[0] ] = false;
1805 }
1806
1807 return $allcaps;
1808 }
1809
1810 /**
1811 * Placeholder text. Default 'Add title'.
1812 *
1813 * @param string $text
1814 * @return string
1815 */
1816 public function enter_title_here( $text ) {
1817 $post_type = get_post_type();
1818 if ( $this->post_type === $post_type ) {
1819 $text = __( 'Add block title', 'content-blocks-builder' );
1820 }
1821
1822 return $text;
1823 }
1824
1825 /**
1826 * Enqueue frontend scripts for non-cbb blocks
1827 *
1828 * @param string $block_content
1829 * @param array $block
1830 * @param WP_Block $block_instance
1831 * @return string
1832 */
1833 public function enqueue_frontend_block_scripts( $block_content, $block, $block_instance ) {
1834 // Ignore admin side.
1835 if ( is_admin() ) {
1836 return $block_content;
1837 }
1838
1839 if ( isset( $GLOBALS['boldblocks_frontend_loaded'] ) ) {
1840 return $block_content;
1841 }
1842
1843 if ( $block_instance->block_type->supports['cbb'] ?? false ) {
1844 $GLOBALS['boldblocks_frontend_loaded'] = true;
1845 } elseif ( $this->is_enqueue_frontend_script_for_non_cbb_blocks( $block, $block_instance ) ) {
1846 wp_enqueue_script( $this->custom_blocks_frontend_handle );
1847 wp_enqueue_style( $this->custom_blocks_handle );
1848
1849 $GLOBALS['boldblocks_frontend_loaded'] = true;
1850 }
1851
1852 return $block_content;
1853 }
1854
1855 /**
1856 * Whether to load the frontend scripts for non-cbb blocks.
1857 *
1858 * @param array $block
1859 * @param WP_Block $block_instance
1860 * @return boolean
1861 */
1862 private function is_enqueue_frontend_script_for_non_cbb_blocks( $block, $block_instance ) {
1863 $block_names = $this->the_plugin_instance->get_component( CustomStyle::class )->non_cbb_blocks;
1864 if ( ! in_array( $block['blockName'] ?? '', $block_names, true ) ) {
1865 return false;
1866 }
1867
1868 // Has sticky enabling.
1869 if ( $block['attrs']['boldblocks']['sticky']['enable'] ?? false ) {
1870 return true;
1871 }
1872
1873 return false;
1874 }
1875
1876 /**
1877 * Enqueue frontend scripts for carousel layout in the query loop block
1878 *
1879 * @param string $block_content
1880 * @param array $block
1881 * @param WP_Block $block_instance
1882 * @return string
1883 */
1884 public function enqueue_frontend_carousel_scripts( $block_content, $block, $block_instance ) {
1885 // Ignore admin side.
1886 if ( is_admin() ) {
1887 return $block_content;
1888 }
1889
1890 if ( isset( $GLOBALS['boldblocks_carousel_loaded'] ) ) {
1891 return $block_content;
1892 }
1893
1894 // Make sure we have the blockName.
1895 if ( empty( $block['blockName'] ) ) {
1896 return $block_content;
1897 }
1898
1899 if ( 'core/query' === $block['blockName'] ) {
1900 // If this is a core/query block, enqueue the carousel script.
1901 if ( $this->is_enqueue_carousel_script_for_query_loop( $block, $block_instance ) ) {
1902 wp_enqueue_script( $this->carousel_blocks_frontend_handle );
1903 wp_enqueue_style( $this->carousel_blocks_frontend_handle );
1904
1905 $GLOBALS['boldblocks_carousel_loaded'] = true;
1906 }
1907 } elseif ( strpos( $block['blockName'], 'boldblocks/' ) === 0 ) {
1908 if ( isset( $block_instance->block_type->supports['parentLayoutType'] ) && 'carousel' === $block_instance->block_type->supports['parentLayoutType'] ) {
1909 $GLOBALS['boldblocks_carousel_loaded'] = true;
1910 }
1911 }
1912
1913 return $block_content;
1914 }
1915
1916 /**
1917 * Whether to load the carousel scripts for core query block.
1918 *
1919 * @param array $block
1920 * @param WP_Block $block_instance
1921 * @return boolean
1922 */
1923 private function is_enqueue_carousel_script_for_query_loop( $block, $block_instance ) {
1924 if ( 'carousel' === ( $block['attrs']['displayLayout']['type'] ?? '' ) ) {
1925 return true;
1926 }
1927
1928 if ( $block_instance->block_type->api_version >= 3 ) {
1929 $post_template = $this->the_plugin_instance->get_component( CustomStyle::class )->find_nested_block( $block_instance, 'core/post-template' );
1930
1931 if ( $post_template ) {
1932 if ( 'carousel' === ( $post_template->attributes['boldblocks']['layout']['type'] ?? '' ) ) {
1933 return true;
1934 }
1935 }
1936 }
1937
1938 return false;
1939 }
1940
1941 /**
1942 * Register custom rest fields for easier getting, updating data.
1943 *
1944 * @return void
1945 */
1946 public function register_rest_fields() {
1947 register_rest_field(
1948 $this->post_type,
1949 'keywords',
1950 array(
1951 'get_callback' => function ( $params ) {
1952 $keywords = wp_list_pluck( wp_get_object_terms( $params['id'], 'boldblocks_block_keywords' ), 'name' );
1953
1954 return \implode( ',', $keywords );
1955 },
1956 'update_callback' => function ( $value, $post ) {
1957 wp_set_post_terms( $post->ID, $value, 'boldblocks_block_keywords' );
1958 },
1959 'schema' => array(
1960 'type' => 'string',
1961 ),
1962 )
1963 );
1964 }
1965
1966 /**
1967 * Render the inline script as inline script module.
1968 *
1969 * @param array $attributes
1970 * @return array
1971 */
1972 public function add_inline_script_module( $attributes ) {
1973 if ( $attributes['id'] ?? false ) {
1974 if ( strpos( $attributes['id'], 'cbb-script-module-' ) === 0 ) {
1975 $attributes['type'] = 'module';
1976 }
1977 }
1978
1979 return $attributes;
1980 }
1981
1982 /**
1983 * Hide the block on the frontend.
1984 *
1985 * @param string $block_content
1986 * @param array $block
1987 * @param WP_Block $block_instance
1988 * @return string
1989 */
1990 public function hide_on_frontend( $block_content, $block, $block_instance ) {
1991 // Ignore admin side.
1992 if ( is_admin() ) {
1993 return $block_content;
1994 }
1995
1996 if ( $block_instance->block_type->supports['hideOnFrontend'] ?? false ) {
1997 return '';
1998 }
1999
2000 return $block_content;
2001 }
2002
2003 /**
2004 * Enqueue scripts for custom blocks when editing them
2005 *
2006 * @param string $hook_suffix
2007 * @return void
2008 */
2009 public function enqueue_custom_block_scripts( $hook_suffix ) {
2010 global $post;
2011 $screen = get_current_screen();
2012 if ( 'post.php' === $hook_suffix && $this->post_type === $screen->post_type ) {
2013 $custom_style_handler = $this->the_plugin_instance->get_component( CustomStyle::class );
2014 // Styles.
2015 $external_styles = get_post_meta( $post->ID, 'boldblocks_block_external_styles', true );
2016 $custom_styles = get_post_meta( $post->ID, 'boldblocks_block_custom_styles', true );
2017
2018 $block_class = '.wp-block-post-content';
2019
2020 if ( ! empty( $external_styles ) ) {
2021 $external_styles = array_filter(
2022 $external_styles,
2023 function ( $style ) {
2024 return $style['handle'] ?? false;
2025 }
2026 );
2027
2028 foreach ( $external_styles as $style ) {
2029 if ( $style['deps'] ?? false ) {
2030 $deps = $this->refine_style_handle( $style['deps'], $is_backend );
2031 $deps = \explode( ',', $deps );
2032 } else {
2033 $deps = [];
2034 }
2035 $version = $style['version'] ?? null;
2036 if ( $version ) {
2037 if ( 'WP' === $version ) {
2038 $version = '';
2039 } elseif ( 'CBB' === $version ) {
2040 $version = BOLDBLOCKS_CBB_VERSION;
2041 }
2042 }
2043 // Don't load external resources on admin side.
2044 if ( ! ( $style['src'] ?? '' ) ) {
2045 // Add 'edit-' prefix to separate it from the handle registered by other hook.
2046 wp_register_style( 'edit-' . $style['handle'], $style['src'] ?? '', $deps, $version, $style['media'] ?? 'all' );
2047 }
2048 }
2049 }
2050
2051 if ( ! empty( $custom_styles ) ) {
2052 $custom_styles = array_filter(
2053 $custom_styles,
2054 function ( $style ) {
2055 return ! empty( $style['value'] ) && 'view' !== ( $style['type'] ?? '' );
2056 }
2057 );
2058
2059 $dynamic_style = '';
2060 foreach ( $custom_styles as $style ) {
2061 $dynamic_style .= $custom_style_handler->refine_custom_value( $style['value'], [ 'selector' => $block_class ], 'CSS' );
2062 }
2063
2064 if ( $dynamic_style ) {
2065 $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;}';
2066 $handle = 'edit-' . $this->custom_blocks_handle;
2067 if ( ! wp_style_is( $handle, 'registered' ) ) {
2068 // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
2069 wp_register_style( $handle, '' );
2070 }
2071
2072 wp_add_inline_style( $handle, $dynamic_style );
2073 wp_enqueue_style( $handle );
2074 }
2075 }
2076 }
2077 }
2078
2079 /**
2080 * Renders the 'readonly' block on the server.
2081 *
2082 * @param array $attributes Block attributes.
2083 * @param string $content Block default content.
2084 * @param WP_Block $block Block instance.
2085 * @return string Returns the block content on the frontend.
2086 */
2087 public function render_readonly_block( $attributes, $content, $block ) {
2088 $raw_block_content = $block->block_type->supports['block_content'] ?? '';
2089
2090 // Ignore empty block.
2091 if ( empty( $raw_block_content ) ) {
2092 return $content;
2093 }
2094
2095 if ( ! empty( $attributes['innerContents'] ) ) {
2096 // Convert to bindings.
2097 $raw_block_content = str_replace( 'cbbBindings', 'bindings', $raw_block_content );
2098
2099 // Handle embeds.
2100 global $wp_embed;
2101 $raw_block_content = $wp_embed->run_shortcode( $raw_block_content );
2102 $raw_block_content = $wp_embed->autoembed( $raw_block_content );
2103
2104 $filter_block_context = static function ( $context ) use ( $attributes ) {
2105 $context['cbb/block-overrides'] = $attributes['innerContents'];
2106 return $context;
2107 };
2108 add_filter( 'render_block_context', $filter_block_context, 1 );
2109
2110 $block_content = do_blocks( $raw_block_content );
2111
2112 remove_filter( 'render_block_context', $filter_block_context, 1 );
2113 } else {
2114 $block_content = apply_filters( 'the_content', $raw_block_content );
2115 }
2116
2117 if ( $block_content ) {
2118 $wrapper_content = trim( $block->parsed_block['innerHTML'] ?? '' );
2119 if ( $wrapper_content ) {
2120 $closing_div = substr( $wrapper_content, -6 );
2121
2122 if ( $closing_div === '</div>' ) {
2123 $opening_div = substr( $wrapper_content, 0, strlen( $wrapper_content ) - 6 );
2124 $block_content = $opening_div . $block_content . $closing_div;
2125
2126 return $block_content;
2127 }
2128 }
2129 }
2130
2131 return $content;
2132 }
2133
2134 /**
2135 * Customize Query Loop
2136 *
2137 * @return void
2138 */
2139 public function query_loop_extended_filters_and_sorting() {
2140 // Get public post types.
2141 $post_types = get_post_types(
2142 [
2143 'public' => true,
2144 'show_in_rest' => true,
2145 ]
2146 );
2147
2148 // For backend.
2149 foreach ( $post_types as $post_type ) {
2150 add_filter( 'rest_' . $post_type . '_collection_params', [ $this, 'query_loop_support_advanced_sorting' ], 10 );
2151 add_filter( 'rest_' . $post_type . '_query', [ $this, 'query_loop_add_custom_query_params' ], 10, 2 );
2152 }
2153
2154 // For frontend.
2155 add_filter( 'query_loop_block_query_vars', [ $this, 'query_loop_customize_frontend' ], 10, 2 );
2156
2157 // Add custom query vars.
2158 add_filter( 'query_vars', [ $this, 'query_loop_query_vars' ], 1 );
2159
2160 // Alter the result of query loop.
2161 add_filter( 'the_posts', [ $this, 'query_loop_the_posts' ], 10, 2 );
2162 }
2163
2164 /**
2165 * Support advanced sorting for the Query Loop block.
2166 *
2167 * @param array $query_params
2168 * @return array
2169 */
2170 public function query_loop_support_advanced_sorting( $query_params ) {
2171 $query_params['orderby']['enum'][] = 'meta_value';
2172 $query_params['orderby']['enum'][] = 'meta_value_num';
2173 $query_params['orderby']['enum'][] = 'menu_order';
2174 $query_params['orderby']['enum'][] = 'rand';
2175
2176 return $query_params;
2177 }
2178
2179 /**
2180 * Customize query args for frontend.
2181 *
2182 * @param array $query_args
2183 * @param WP_Block $block
2184 * @return array
2185 */
2186 public function query_loop_customize_frontend( $query_args, $block ) {
2187 $query_context = $block->context['query'] ?? [];
2188 $queried_object = get_queried_object();
2189
2190 return $this->query_loop_alter_query_params( $query_args, $query_context['cbb'] ?? [], $query_context, $queried_object );
2191 }
2192
2193 /**
2194 * Add custom query parameters to the Query Loop.
2195 *
2196 * @param array $query_args
2197 * @param WP_REST_Request $request
2198 * @return array
2199 */
2200 public function query_loop_add_custom_query_params( $query_args, $request ) {
2201 return $this->query_loop_alter_query_params( $query_args, $request->get_param( 'cbb' ) ?? [] );
2202 }
2203
2204 /**
2205 * Change query args by CBB.
2206 *
2207 * @param array $query_args
2208 * @param array $cbb_params
2209 * @param mixed $query_context
2210 * @param mixed $queried_object
2211 * @return array
2212 */
2213 private function query_loop_alter_query_params( $query_args, $cbb_params, $query_context = null, $queried_object = null ) {
2214 $cbb_args = [];
2215 if ( $cbb_params && is_array( $cbb_params ) ) {
2216 // It's a cbb query.
2217 $cbb_args = [ 'cbb' => $cbb_params ];
2218
2219 // Query additional post types.
2220 $post_type_args = $this->query_loop_filter_by_post_types( $cbb_params['postTypes'] ?? '' );
2221 if ( $post_type_args ) {
2222 $post_type = (array) ( $query_args['post_type'] ?? 'post' );
2223 $post_types = array_merge( $post_type, $post_type_args );
2224 $cbb_args['post_type'] = $post_types;
2225 // Set query args here for the context filter below.
2226 $query_args['post_type'] = $post_types;
2227 }
2228
2229 // Query by custom post ids.
2230 $post_in_args = $this->query_loop_filter_by_post_ids( $cbb_params['include'] ?? '' );
2231 if ( $post_in_args ) {
2232 $cbb_args = array_merge( $cbb_args, $post_in_args );
2233 }
2234
2235 // Query by meta fields.
2236 $meta_query_args = $this->query_loop_filter_by_meta_queries( $cbb_params );
2237 if ( $meta_query_args ) {
2238 $cbb_args = array_merge( $cbb_args, $meta_query_args );
2239 }
2240
2241 // Query by parent.
2242 $parent_args = $this->query_loop_filter_by_parent( $cbb_params['parent'] ?? '' );
2243 if ( $parent_args ) {
2244 $cbb_args = array_merge( $cbb_args, $parent_args );
2245 }
2246
2247 // Query by context.
2248 if ( $queried_object && $queried_object instanceof \WP_Post ) {
2249 $query_post_type = $query_args['post_type'] ?? '';
2250 if ( $queried_object->post_type === $query_post_type || ( is_array( $query_post_type ) && in_array( $queried_object->post_type, $query_post_type, true ) ) ) {
2251 $context_args = $this->query_loop_filter_by_context( $cbb_params, $queried_object );
2252
2253 if ( $context_args['post__not_in'] ?? false ) {
2254 // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in
2255 $cbb_args['post__not_in'] = $context_args['post__not_in'];
2256 }
2257
2258 if ( $context_args['tax_query'] ?? false ) {
2259 if ( ( $query_args['tax_query'] ?? false ) && is_array( $query_args['tax_query'] ) ) {
2260 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
2261 $cbb_args['tax_query'] = array_merge( $query_args['tax_query'], [ $context_args['tax_query'] ] );
2262 } else {
2263 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
2264 $cbb_args['tax_query'] = $context_args['tax_query'];
2265 }
2266 }
2267 }
2268
2269 // ignoreStickyPosts is depreated since WP 6.8.
2270 // Ignore sticky posts not exlude them.
2271 if ( ( $cbb_params['ignoreStickyPosts'] ?? false ) && ( 'exclude' === $query_context['sticky'] ?? '' ) ) {
2272 $cbb_args['ignore_sticky_posts'] = true;
2273 $sticky = get_option( 'sticky_posts' );
2274 if ( ! empty( $sticky ) && ! empty( $query_args['post__not_in'] ) ) {
2275 // Remove sticky from the post__not_in list.
2276 // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in
2277 $query_args['post__not_in'] = array_diff( $query_args['post__not_in'], $sticky );
2278 }
2279 }
2280 }
2281
2282 // Custom sorting.
2283 $orderby = [];
2284 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
2285 if ( ( $cbb_params['orderbyFromQueryString'] ?? false ) && isset( $_GET['orderby'] ) ) {
2286 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Recommended
2287 $orderby = wp_unslash( $_GET['orderby'] );
2288 // orderby will be sanitized below.
2289 if ( $orderby ) {
2290 if ( ! is_array( $orderby ) ) {
2291 $orderby = [ $orderby ];
2292 }
2293
2294 $orderby = array_filter(
2295 array_map(
2296 function ( $item ) {
2297 return $item ? sanitize_text_field( $item ) : false;
2298 },
2299 $orderby
2300 )
2301 );
2302 }
2303 }
2304
2305 $sort_args = $this->query_loop_custom_sort( array_merge( $orderby, $cbb_params['sorting'] ?? [] ) );
2306 if ( $sort_args ) {
2307 $cbb_args = array_merge( $cbb_args, $sort_args );
2308
2309 if ( isset( $query_args['order'] ) ) {
2310 unset( $query_args['order'] );
2311 }
2312 }
2313
2314 // Combine post__not_in value.
2315 // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in
2316 $cbb_args['post__not_in'] = array_merge( $query_args['post__not_in'], $cbb_args['post__not_in'] ?? [] );
2317
2318 // Don't need pagination.
2319 if ( $cbb_params['disablePagination'] ?? false ) {
2320 $cbb_args['no_found_rows'] = true;
2321 }
2322 }
2323
2324 // Allow third-party to alter the final result.
2325 $query_args = apply_filters( 'cbb_query_loop_block_query_vars', array_merge( $query_args, $cbb_args ), $query_args, $cbb_params, $query_context, $queried_object );
2326
2327 return $query_args;
2328 }
2329
2330 /**
2331 * Filter by additional post types.
2332 *
2333 * @param string $post_types
2334 * @return array
2335 */
2336 private function query_loop_filter_by_post_types( $post_types ) {
2337 $post_types_args = [];
2338 $post_types = trim( $post_types );
2339 if ( $post_types ) {
2340 $post_types_args = explode( ',', $post_types );
2341 }
2342
2343 return $post_types_args;
2344 }
2345
2346 /**
2347 * Filter by post ids.
2348 *
2349 * @param string $include
2350 * @return array
2351 */
2352 private function query_loop_filter_by_post_ids( $include ) {
2353 $post_in_args = [];
2354 if ( $include ) {
2355 $post_in = explode( ',', $include );
2356 if ( $post_in ) {
2357 $post_in_args['post__in'] = $post_in;
2358 $post_in_args['orderby'] = 'post__in';
2359 $post_in_args['ignore_sticky_posts'] = true;
2360 }
2361 }
2362
2363 return $post_in_args;
2364 }
2365
2366 /**
2367 * Filter by parent.
2368 *
2369 * @param string $parent
2370 * @return array
2371 */
2372 private function query_loop_filter_by_parent( $parent ) {
2373 $parent_args = [];
2374 if ( $parent ) {
2375 $parent_ids = explode( ',', $parent );
2376 if ( count( $parent_ids ) > 1 ) {
2377 $parent_args['post_parent__in'] = $parent_ids;
2378 } else {
2379 $parent_args['post_parent'] = $parent;
2380 }
2381 }
2382
2383 return $parent_args;
2384 }
2385
2386 /**
2387 * Filter by meta queries
2388 *
2389 * @param array $params
2390 * @return array
2391 */
2392 private function query_loop_filter_by_meta_queries( $params ) {
2393 $meta_query_args = [];
2394 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
2395 $queries = $params['metaQuery']['queries'] ?? [];
2396 if ( $queries && count( $queries ) > 0 ) {
2397 $queries = array_map(
2398 function ( $query ) {
2399 $refined_query = [
2400 'type' => strtoupper( $query['type'] ?? 'char' ),
2401 'compare' => $query['compare'] ?? '=',
2402 'key' => $query['key'] ?? '',
2403 'value' => $query['value'] ?? '',
2404 ];
2405
2406 if ( 'NUMERIC' === $refined_query['type'] ) {
2407 $refined_query['type'] = 'DECIMAL(10,3)';
2408 }
2409
2410 // No key.
2411 if ( ! $refined_query['key'] ) {
2412 return false;
2413 }
2414
2415 // Don't need a value.
2416 if ( in_array( $refined_query['compare'], [ 'EXISTS', 'NOT EXISTS' ], true ) ) {
2417 unset( $refined_query['value'] );
2418
2419 return $refined_query;
2420 }
2421
2422 $compare = $refined_query['compare'];
2423 $refined_value = $refined_query['value'];
2424
2425 // Get query string.
2426 $query_string = $query['query_string'] ?? '';
2427 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
2428 if ( $query_string && isset( $_GET[ $query_string ] ) ) {
2429 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
2430 $refined_value = sanitize_text_field( wp_unslash( $_GET[ $query_string ] ) );
2431 }
2432
2433 // No value.
2434 if ( $refined_value === '' ) {
2435 return false;
2436 }
2437
2438 if ( in_array( $compare, [ 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ], true ) ) {
2439 $refined_value = preg_split( '/,\s*(?=(?:[^"]*"[^"]*")*[^"]*$)/', $refined_value, -1, PREG_SPLIT_NO_EMPTY );
2440 $refined_value = array_filter(
2441 array_map(
2442 function ( $value ) {
2443 return trim( $value, '"' );
2444 },
2445 $refined_value
2446 )
2447 );
2448 }
2449
2450 if ( 'DATE' === $refined_query['type'] ) {
2451 $format = ! empty( $query['date_format'] ) ? $query['date_format'] : 'Y-m-d';
2452
2453 if ( is_array( $refined_value ) ) {
2454 $refined_value = array_filter(
2455 array_map(
2456 function ( $value ) {
2457 $formated_value = strtotime( $value );
2458
2459 // No valid date string.
2460 if ( ! $formated_value ) {
2461 return false;
2462 }
2463
2464 if ( 'timestamp' !== $format ) {
2465 $formated_value = gmdate( $format, $formated_value );
2466 }
2467
2468 return $formated_value;
2469 },
2470 $refined_value
2471 )
2472 );
2473 } else {
2474 $refined_value = strtotime( $refined_value );
2475
2476 if ( 'timestamp' !== $format ) {
2477 $refined_value = gmdate( $format, $refined_value );
2478 }
2479 }
2480
2481 // No valid value.
2482 if ( ! $refined_value ) {
2483 return false;
2484 }
2485
2486 if ( 'timestamp' === $format ) {
2487 $refined_query['type'] = 'NUMERIC';
2488 }
2489 }
2490
2491 // Update value.
2492 $refined_query['value'] = $refined_value;
2493
2494 return $refined_query;
2495 },
2496 $queries
2497 );
2498
2499 $queries = array_filter( $queries );
2500
2501 if ( count( $queries ) > 0 ) {
2502 // Maximize 5 items.
2503 $queries = array_slice( $queries, 0, 5 );
2504
2505 $refined_queries = [];
2506 $instances = [];
2507 foreach ( $queries as $query ) {
2508 $key = $query['key'];
2509 if ( ! isset( $refined_queries[ $key ] ) ) {
2510 $refined_queries[ $key ] = $query;
2511 $instances[ $key ] = 1;
2512 } else {
2513 $refined_queries[ $key . '_' . $instances[ $key ] ] = $query;
2514 $instances[ $key ] = absint( $instances[ $key ] ) + 1;
2515 }
2516 }
2517
2518 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
2519 $meta_query_args['meta_query'] = $refined_queries;
2520 if ( count( $queries ) > 1 ) {
2521 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
2522 $meta_query_args['meta_query']['relation'] = $params['metaQuery']['relation'] ?? 'AND';
2523 }
2524 }
2525 }
2526
2527 return $meta_query_args;
2528 }
2529
2530 /**
2531 * Filter by context.
2532 *
2533 * @param array $params
2534 * @return array
2535 */
2536 private function query_loop_filter_by_context( $params, $post ) {
2537 $context_args = [];
2538
2539 if ( $params['ignoreCurrentPost'] ?? false ) {
2540 // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in
2541 $context_args['post__not_in'] = [ $post->ID ];
2542 }
2543
2544 if ( $params['queryRelatedPosts'] ?? false ) {
2545 $taxonomies = \get_object_taxonomies( $post, 'names' );
2546
2547 $tax_queries = [];
2548 foreach ( $taxonomies as $taxonomy ) {
2549 $terms = \get_the_terms( $post->ID, $taxonomy );
2550 if ( empty( $terms ) ) {
2551 continue;
2552 }
2553 $term_ids = \wp_list_pluck( $terms, 'term_id' );
2554 $tax_queries[] = array(
2555 'taxonomy' => $taxonomy,
2556 'terms' => $term_ids,
2557 );
2558 }
2559
2560 if ( count( $tax_queries ) > 1 ) {
2561 $tax_queries['relation'] = 'OR';
2562 }
2563
2564 if ( $tax_queries ) {
2565 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
2566 $context_args['tax_query'] = $tax_queries;
2567 }
2568 }
2569
2570 return $context_args;
2571 }
2572
2573 /**
2574 * Custom sorting.
2575 *
2576 * @param array $sorting
2577 * @return array
2578 */
2579 private function query_loop_custom_sort( $sorting ) {
2580 $sort_args = [];
2581
2582 if ( $sorting && is_array( $sorting ) ) {
2583 if ( 'rand' === $sorting[0] ) {
2584 $sort_args['orderby'] = 'rand';
2585 } else {
2586 $order_by = [];
2587 foreach ( $sorting as $item ) {
2588 if ( ! $item || 'rand' === $item ) {
2589 continue;
2590 }
2591
2592 $item_array = explode( '/', $item );
2593
2594 if ( count( $item_array ) !== 2 ) {
2595 continue;
2596 }
2597
2598 if ( isset( $order_by[ $item_array[0] ] ) || ! in_array( strtoupper( $item_array[1] ), [ 'ASC', 'DESC' ], true ) ) {
2599 continue;
2600 }
2601
2602 $order_by[ $item_array[0] ] = strtoupper( $item_array[1] );
2603 }
2604
2605 if ( $order_by ) {
2606 $sort_args['orderby'] = $order_by;
2607 }
2608 }
2609 }
2610
2611 return $sort_args;
2612 }
2613
2614 /**
2615 * Add custom query vars.
2616 *
2617 * @param array $vars
2618 * @return array
2619 */
2620 public function query_loop_query_vars( $vars ) {
2621 $vars[] = 'cbb';
2622
2623 return $vars;
2624 }
2625
2626 /**
2627 * Alter the result of the query loop
2628 *
2629 * @param array $posts
2630 * @param WP_Query $query
2631 * @return array
2632 */
2633 public function query_loop_the_posts( $posts, $query ) {
2634 if ( $query->query_vars['cbb'] ?? false ) {
2635 $cbb_args = $query->query_vars['cbb'] ?? [];
2636 $posts_per_page = $query->query_vars['posts_per_page'] ?? 1;
2637 $found_posts = $query->found_posts;
2638
2639 if ( ( $cbb_args['queryFallbackPosts'] ?? false ) && $found_posts < $posts_per_page ) {
2640 $ignore_posts = [];
2641 if ( $found_posts > 0 ) {
2642 $ignore_posts = wp_list_pluck( $query->posts, 'ID' );
2643 }
2644
2645 $fallback_args = [
2646 'post_type' => $query->query_vars['post_type'] ?? 'any',
2647 'post_status' => 'publish',
2648 'posts_per_page' => $posts_per_page - $found_posts,
2649 'ignore_sticky_posts' => 1,
2650 // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in
2651 'post__not_in' => array_merge( $query->query_vars['post__not_in'] ?? [], $ignore_posts ),
2652 'no_found_rows' => true,
2653 ];
2654
2655 $fallback_posts = get_posts( $fallback_args );
2656
2657 if ( $fallback_posts ) {
2658 $posts = array_merge( $posts, $fallback_posts );
2659 }
2660 }
2661 }
2662
2663 return $posts;
2664 }
2665
2666 /**
2667 * Get the current post
2668 *
2669 * @return string
2670 */
2671 public function get_current_post() {
2672 global $pagenow;
2673 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
2674 $post_id = 'post.php' === $pagenow && isset( $_GET['post'] ) ? absint( $_GET['post'] ) : 0;
2675 $post = $post_id ? get_post( $post_id ) : false;
2676
2677 return $post instanceof \WP_Post ? $post : false;
2678 }
2679
2680 /**
2681 * Get animated blocks
2682 */
2683 public function get_animated_blocks() {
2684 return apply_filters(
2685 'cbb_get_animated_blocks',
2686 [
2687 'core/heading',
2688 'core/paragraph',
2689 'core/image',
2690 'core/button',
2691 'core/buttons',
2692 'core/list-item',
2693 'core/list',
2694 'core/group',
2695 'core/columns',
2696 'core/column',
2697 'core/cover',
2698 'core/media-text',
2699 'core/post-title',
2700 'core/post-excerpt',
2701 'core/post-terms',
2702 'core/readmore',
2703 'boldblocks/svg-block',
2704 ]
2705 );
2706 }
2707
2708 /**
2709 * Get max items
2710 */
2711 public function get_max_items() {
2712 return apply_filters( 'cbb_get_max_items', $this->max_items, $this->post_type );
2713 }
2714 }
2715 endif;
2716