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

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