PluginProbe
Content Blocks Builder – Create blocks, repeater blocks with carousel, grid, popup layouts / 2.8.8
Content Blocks Builder – Create blocks, repeater blocks with carousel, grid, popup layouts v2.8.8
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 / block-overrides.php

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

631 lines 20.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Block Overrides
4 *
5 * @package BoldBlocks
6 * @author Phi Phan <mrphipv@gmail.com>
7 * @copyright Copyright (c) 2024, Phi Phan
8 */
9
10 namespace BoldBlocks;
11
12 // Exit if accessed directly.
13 defined( 'ABSPATH' ) || exit;
14
15 if ( ! class_exists( BlockOverrides::class ) ) :
16 /**
17 * Create/edit custom content blocks.
18 */
19 class BlockOverrides extends CoreComponent {
20 /**
21 * Define a list of supported core blocks
22 *
23 * @var array
24 */
25 protected $supported_core_blocks = [
26 'core/paragraph' => [ 'content' ],
27 'core/heading' => [ 'content' ],
28 'core/button' => [ 'text', 'url', 'linkTarget', 'rel' ],
29 'core/image' => [ 'id', 'url', 'title', 'alt', 'caption' ],
30 ];
31
32 /**
33 * Define a list of supported block binding
34 *
35 * @var array
36 */
37 protected $supported_blocks = [
38 'boldblocks/svg-block' => [
39 'content',
40 'title',
41 'description',
42 'linkUrl',
43 'linkTarget',
44 'linkRel',
45 'buttonText',
46 ],
47 ];
48
49 /**
50 * Run main hooks
51 *
52 * @return void
53 */
54 public function run() {
55 // Register block bindings.
56 add_action( 'init', [ $this, 'register_block_bindings' ] );
57
58 // Register block binding contexts for supported blocks.
59 add_filter( 'register_block_type_args', [ $this, 'register_custom_args' ], 10, 2 );
60
61 // Register scripts.
62 add_action( 'init', [ $this, 'register_scripts' ] );
63
64 // Replace the dynamic values.
65 add_action( 'render_block', [ $this, 'render_block_replace_dynamic_content' ], 10, 3 );
66
67 // Render specific blocks.
68 add_filter( 'cbb_pre_render_dynamic_content', [ $this, 'pre_render_dynamic_content' ], 10, 4 );
69
70 // Support more blocks.
71 add_filter( 'cbb_block_overrides_supported_blocks', [ $this, 'support_additional_blocks' ] );
72 }
73
74 /**
75 * Register custom block bindings
76 *
77 * @return void
78 */
79 public function register_block_bindings() {
80 if ( function_exists( 'register_block_bindings_source' ) ) {
81 register_block_bindings_source(
82 'cbb/block-overrides',
83 array(
84 'label' => _x( 'Block Overrides', 'block bindings source', 'content-blocks-builder' ),
85 'get_value_callback' => [ $this, 'block_bindings_block_overrides_get_value' ],
86 'uses_context' => array( 'cbb/block-overrides' ),
87 )
88 );
89 }
90 }
91
92 /**
93 * Get value callback for block binding attributes
94 *
95 * @param array $source_args
96 * @param WP_Block $block_instance
97 * @param string $attribute_name
98 * @return mixed
99 */
100 public function block_bindings_block_overrides_get_value( array $source_args, $block_instance, string $attribute_name ) {
101 if ( empty( $block_instance->attributes['metadata']['name'] ) ) {
102 return null;
103 }
104 $metadata_name = $block_instance->attributes['metadata']['name'];
105 return _wp_array_get( $block_instance->context, array( 'cbb/block-overrides', $metadata_name, $attribute_name ), null );
106 }
107
108 /**
109 * Register block overrides context for blocks
110 *
111 * @param array $args
112 * @param string $block_name
113 * @return void
114 */
115 public function register_custom_args( $args, $block_name ) {
116 if ( array_key_exists( $block_name, $this->get_supported_blocks() ) ) {
117 if ( empty( $args['uses_context'] ) ) {
118 $args['uses_context'] = [];
119 }
120
121 $args['uses_context'][] = 'cbb/block-overrides';
122 }
123
124 return $args;
125 }
126
127 /**
128 * Register scripts
129 *
130 * @return void
131 */
132 public function register_scripts() {
133 // Custom blocks handle.
134 $custom_blocks_handler = $this->the_plugin_instance->get_component( CustomBlocks::class );
135
136 // Load all custom blocks for registration.
137 wp_add_inline_script( $custom_blocks_handler->custom_blocks_handle, 'var CBB_SYNCING_SUPPORTED_BLOCKS=' . wp_json_encode( apply_filters( 'cbb_block_overrides_get_all_supported_blocks', array_merge( $this->supported_core_blocks, $this->get_supported_blocks() ) ) ), 'before' );
138 }
139
140 /**
141 * Replace original content with content from the context
142 *
143 * @param string $block_content
144 * @param array $block
145 * @param WP_Block $block_instance
146 * @return void
147 */
148 public function render_block_replace_dynamic_content( $block_content, $block, $block_instance ) {
149 if ( ! function_exists( 'register_block_bindings_source' ) ) {
150 return $block_content;
151 }
152
153 // Get dynamic data.
154 $computed_attributes = $this->process_block_bindings( $block, $block_instance );
155
156 if ( empty( $computed_attributes ) || empty( $block_content ) ) {
157 return $block_content;
158 }
159
160 // Allow render blocks in a different way.
161 $pre_render = apply_filters( 'cbb_pre_render_dynamic_content', null, $block_content, $computed_attributes, $block_instance );
162
163 if ( ! is_null( $pre_render ) ) {
164 $block_content = $pre_render;
165 } else {
166 foreach ( $computed_attributes as $attribute_name => $source_value ) {
167 $block_content = $this->replace_html( $block_content, $attribute_name, $source_value, $block_instance, $computed_attributes );
168 }
169 }
170
171 return $block_content;
172 }
173
174 /**
175 * Render special blocks.
176 *
177 * @param null $content
178 * @param string $block_content
179 * @param array $computed_attributes
180 * @param WP_Block $block_instance
181 * @return null|string
182 */
183 public function pre_render_dynamic_content( $content, $block_content, $computed_attributes, $block_instance ) {
184 if ( 'boldblocks/svg-block' === $block_instance->name ) {
185 // Merge with dynamic content.
186 $attributes = array_merge( $block_instance->attributes, $computed_attributes );
187
188 // Get class and style attributes.
189 $block_reader = new \WP_HTML_Tag_Processor( $block_content );
190 $class = '';
191 $inner_class = '';
192 $style = '';
193 $inner_style = '';
194
195 if ( $block_reader->next_tag() ) {
196 $class = $block_reader->get_attribute( 'class' );
197 $style = $block_reader->get_attribute( 'style' );
198 }
199
200 if ( $block_reader->next_tag( [ 'class_name' => 'wp-block-boldblocks-svg-block__inner' ] ) ) {
201 $inner_class = $block_reader->get_attribute( 'class' );
202 $inner_style = $block_reader->get_attribute( 'style' );
203 }
204
205 // Don't display empty icon.
206 if ( empty( $attributes['content'] ) ) {
207 return '';
208 }
209
210 $svg = $attributes['content'];
211
212 $id = \uniqid();
213 $title = '';
214 $desc = '';
215 $texts = '';
216 if ( ! empty( $attributes['title'] ) ) {
217 $title = "<title id=\"svg_block_{$id}_title\">{$attributes['title']}</title>";
218 $texts .= $title;
219 }
220 if ( ! empty( $attributes['description'] ) ) {
221 $desc = "<desc id=\"svg_block_{$id}_desc\">{$attributes['description']}</desc>";
222 $texts .= $desc;
223 }
224
225 // Add title & desc.
226 $svg = \preg_replace(
227 '/' . \preg_quote( '>', '/' ) . '/',
228 '>' . $texts,
229 $svg,
230 1
231 );
232
233 $is_link = ! empty( $attributes['linkUrl'] ) || ! empty( $attributes['linkToPost'] );
234 $inner_tag = $is_link ? 'a' : 'div';
235 $inner_attributes = '';
236 $button = '';
237 if ( $is_link ) {
238 if ( ! empty( $attributes['linkToPost'] ) ) {
239 if ( isset( $block_instance->context['postId'] ) ) {
240 // Get post_id from the context first.
241 $post_id = $block_instance->context['postId'];
242 } else {
243 // Fallback to the current post id.
244 $post_id = get_queried_object_id();
245 }
246
247 $link_url = get_permalink( $post_id );
248 } else {
249 $link_url = $attributes['linkUrl'] ?? '';
250 }
251
252 if ( $link_url ) {
253 $inner_attributes .= ' href="' . $link_url . '"';
254
255 if ( ! empty( $attributes['linkTarget'] ) ) {
256 $inner_attributes .= ' target="' . $attributes['linkTarget'] . '"';
257 }
258
259 if ( ! empty( $attributes['linkRel'] ) ) {
260 $inner_attributes .= ' rel="' . $attributes['linkRel'] . '"';
261 }
262
263 if ( ! empty( $attributes['useAsButton'] ) && ! empty( $attributes['buttonText'] ) ) {
264 $button = '<span class="button-text">' . $attributes['buttonText'] . '</span>';
265 }
266 }
267 }
268
269 $new_content = "<div><{$inner_tag}{$inner_attributes}>{$svg}{$button}</{$inner_tag}></div>";
270 $block_reader = new \WP_HTML_Tag_Processor( $new_content );
271 $block_reader->next_tag();
272 $block_reader->set_attribute( 'class', $class );
273 if ( $style ) {
274 $block_reader->set_attribute( 'style', $style );
275 }
276
277 $block_reader->next_tag();
278 $block_reader->set_attribute( 'class', $inner_class );
279 if ( $inner_style ) {
280 $block_reader->set_attribute( 'style', $inner_style );
281 }
282
283 if ( $block_reader->next_tag( 'svg' ) ) {
284 $block_reader->set_attribute( 'role', 'img' );
285
286 if ( $texts ) {
287 if ( $title ) {
288 $block_reader->set_attribute( 'aria-labelledby', "svg_block_{$id}_title" );
289 }
290 if ( $desc ) {
291 $block_reader->set_attribute( 'aria-describedby', "svg_block_{$id}_desc" );
292 }
293 }
294 }
295
296 return $block_reader->get_updated_html();
297 } elseif ( 'core/embed' === $block_instance->name ) {
298 // Merge with dynamic content.
299 $attributes = array_merge( $block_instance->attributes, $computed_attributes );
300
301 if ( empty( $attributes['url'] ) ) {
302 return '';
303 }
304
305 // Get class and style attributes.
306 $block_reader = new \WP_HTML_Tag_Processor( $block_content );
307 $class = '';
308 $style = '';
309
310 if ( $block_reader->next_tag( 'figure' ) ) {
311 $class = $block_reader->get_attribute( 'class' );
312 if ( $class ) {
313 $class = explode( ' ', $class );
314 $class = array_filter(
315 $class,
316 function ( $item ) {
317 if ( 0 === stripos( $item, 'is-type-' ) || 0 === stripos( $item, 'is-provider-' ) || 0 === stripos( $item, 'wp-block-embed-' ) ) {
318 return false;
319 }
320
321 return $item;
322 }
323 );
324
325 $class = implode( ' ', $class );
326 }
327
328 $style = $block_reader->get_attribute( 'style' );
329 if ( $style ) {
330 $style = " style=\"{$style}\"";
331 }
332 }
333
334 $classes = [ 'wp-block-embed' ];
335 if ( $class ) {
336 $classes[] = $class;
337 }
338 if ( $attributes['type'] ?? '' ) {
339 $classes[] = "is-type-{$attributes['type']}";
340 }
341 if ( $attributes['providerNameSlug'] ?? '' ) {
342 $classes[] = "is-provider-{$attributes['providerNameSlug']}";
343 $classes[] = "wp-block-embed-{$attributes['providerNameSlug']}";
344 }
345 $markup = '<figure class="' . implode( ' ', $classes ) . '"' . $style . '><div class="wp-block-embed__wrapper">' . PHP_EOL;
346 $markup .= $attributes['url'] . PHP_EOL;
347 $markup .= '</div>';
348 if ( $attributes['caption'] ?? '' ) {
349 $markup .= '<figcaption class="wp-element-caption">' . $attributes['caption'] . '</figcaption>';
350 }
351 $markup .= '</figure>';
352
353 global $wp_embed;
354 return $wp_embed->autoembed( $markup );
355 } elseif ( 'boldblocks/youtube-block' === $block_instance->name ) {
356 if ( function_exists( 'better_youtube_embed_block_render_block' ) ) {
357 // Merge with dynamic content.
358 $attributes = array_merge( $block_instance->attributes, $computed_attributes );
359
360 // Get caption from the markup if there is no binding value.
361 if ( ! isset( $computed_attributes['caption'] ) ) {
362 $block_reader = new \WP_HTML_Tag_Processor( $block_content );
363 if ( $block_reader->next_tag( 'figcaption' ) ) {
364 $caption = '';
365 while ( $block_reader->next_token() ) {
366 if ( '#text' === $block_reader->get_token_name() ) {
367 $caption .= $block_reader->get_modifiable_text();
368 }
369 }
370
371 if ( $caption ) {
372 $attributes['caption'] = $caption;
373 }
374 }
375 }
376
377 return better_youtube_embed_block_render_block( $attributes );
378 }
379 } elseif ( 'boldblocks/counting-number' === $block_instance->name ) {
380 // Merge with dynamic content.
381 $attributes = array_merge( $block_instance->attributes, $computed_attributes );
382
383 // Get class and style attributes.
384 $block_reader = new \WP_HTML_Tag_Processor( $block_content );
385 $class = '';
386 $style = '';
387 $value_attrs = [];
388
389 if ( $block_reader->next_tag() ) {
390 $class = $block_reader->get_attribute( 'class' );
391 $style = $block_reader->get_attribute( 'style' );
392 }
393
394 if ( $block_reader->next_tag( [ 'class_name' => 'value' ] ) ) {
395 $value_attribute_names = $block_reader->get_attribute_names_with_prefix( '' );
396 foreach ( $value_attribute_names as $name ) {
397 $value_attrs[ $name ] = $block_reader->get_attribute( $name );
398 }
399 }
400
401 $new_content = '<div class="' . $class . '" style="' . $style . '">';
402 if ( $attributes['prefix'] ?? false ) {
403 $new_content .= '<span class="prefix">' . $attributes['prefix'] . '</span>';
404 }
405 $new_content .= '<span class="value">' . ( $attributes['startVal'] ?? '' ) . '</span>';
406 $new_content .= '<span class="sr-value">' . ( $attributes['value'] ?? '' ) . '</span>';
407 if ( $attributes['suffix'] ?? false ) {
408 $new_content .= '<span class="suffix">' . $attributes['suffix'] . '</span>';
409 }
410 $new_content .= '</div>';
411
412 $overridable_attrs = [ 'data-value', 'data-start-val', 'data-prefix', 'data-suffix' ];
413
414 $block_reader = new \WP_HTML_Tag_Processor( $new_content );
415 if ( $block_reader->next_tag( [ 'class_name' => 'value' ] ) ) {
416 foreach ( $value_attrs as $attribute_key => $attribute_value ) {
417 if ( in_array( $attribute_key, $overridable_attrs, true ) ) {
418 switch ( $attribute_key ) {
419 case 'data-value':
420 $attribute_value = $attributes['value'] ?? '';
421 break;
422
423 case 'data-start-val':
424 $attribute_value = $attributes['startVal'] ?? '';
425 break;
426
427 case 'data-prefix':
428 $attribute_value = $attributes['prefix'] ?? '';
429 break;
430
431 case 'data-suffix':
432 $attribute_value = $attributes['suffix'] ?? '';
433 break;
434
435 default:
436 break;
437 }
438 }
439
440 $block_reader->set_attribute( $attribute_key, $attribute_value );
441 }
442 }
443
444 return $block_reader->get_updated_html();
445 }
446
447 return $content;
448 }
449
450 /**
451 * Add more blocks to the supported list
452 *
453 * @param array $supported_blocks
454 * @return array
455 */
456 public function support_additional_blocks( $supported_blocks ) {
457 $supported_blocks['core/embed'] = [ 'url', 'caption', 'type', 'providerNameSlug', 'responsive', 'previewable' ];
458 $supported_blocks['core/video'] = [ 'src', 'caption' ];
459 $supported_blocks['core/audio'] = [ 'src', 'caption' ];
460 $supported_blocks['core/code'] = [ 'content' ];
461 $supported_blocks['core/verse'] = [ 'content' ];
462 $supported_blocks['core/preformatted'] = [ 'content' ];
463 $supported_blocks['core/pullquote'] = [ 'value', 'citation' ];
464
465 $supported_blocks['boldblocks/youtube-block'] = [ 'url', 'caption', 'aspectRatio', 'isMaxResThumbnail', 'customThumbnail' ];
466 $supported_blocks['boldblocks/counting-number'] = [ 'value', 'startVal', 'prefix', 'suffix' ];
467
468 return $supported_blocks;
469 }
470
471 /**
472 * Build dynamic contents
473 *
474 * @param array $parsed_block
475 * @param WP_Block $block_instance
476 * @return array
477 */
478 private function process_block_bindings( $parsed_block, $block_instance ) {
479 $computed_attributes = array();
480 $block_name = $parsed_block['blockName'] ?? '';
481 $supported_blocks = $this->get_supported_blocks();
482
483 // If the block doesn't have the bindings property, isn't one of the supported
484 // block types, or the bindings property is not an array, return the block content.
485 if (
486 ! isset( $supported_blocks[ $block_name ] ) ||
487 empty( $parsed_block['attrs']['metadata']['bindings'] ) ||
488 ! is_array( $parsed_block['attrs']['metadata']['bindings'] )
489 ) {
490 return $computed_attributes;
491 }
492
493 foreach ( $parsed_block['attrs']['metadata']['bindings'] as $attribute_name => $block_binding ) {
494 // If the attribute is not in the supported list, process next attribute.
495 if ( ! array_key_exists( $block_name, $supported_blocks ) ) {
496 continue;
497 }
498
499 // If no source is provided, or that source is not registered, process next attribute.
500 if ( ! isset( $block_binding['source'] ) || ! is_string( $block_binding['source'] ) ) {
501 continue;
502 }
503
504 $block_binding_source = get_block_bindings_source( $block_binding['source'] );
505 if ( null === $block_binding_source ) {
506 continue;
507 }
508
509 $source_args = ! empty( $block_binding['args'] ) && is_array( $block_binding['args'] ) ? $block_binding['args'] : array();
510 $source_value = $block_binding_source->get_value( $source_args, $block_instance, $attribute_name );
511
512 // If the value is not null, process the HTML based on the block and the attribute.
513 if ( ! is_null( $source_value ) ) {
514 $computed_attributes[ $attribute_name ] = $source_value;
515 }
516 }
517
518 return $computed_attributes;
519 }
520
521 /**
522 * Depending on the block attribute name, replace its value in the HTML based on the value provided.
523 *
524 * @param string $block_content Block content.
525 * @param string $attribute_name The attribute name to replace.
526 * @param mixed $source_value The value used to replace in the HTML.
527 * @param WP_Block $block_instance
528 * @param array $computed_attributes
529 * @return string The modified block content.
530 */
531 private function replace_html( string $block_content, string $attribute_name, $source_value, $block_instance, $computed_attributes ) {
532 $block_type = $block_instance->block_type;
533 $properties = $block_type->attributes[ $attribute_name ];
534
535 // Use this filter to alter source, selector for the property.
536 $properties = apply_filters( 'cbb_get_binding_attribute_properties', $properties, $attribute_name, $block_instance, $computed_attributes );
537
538 if ( ! isset( $properties['source'] ) ) {
539 return $block_content;
540 }
541
542 // Make sure the seletor is valid and don't support multiple selector.
543 if ( empty( $properties['selector'] ) || false !== stripos( $properties['selector'], ',' ) ) {
544 return $block_content;
545 }
546
547 // Depending on the attribute source, the processing will be different.
548 switch ( $properties['source'] ) {
549 case 'html':
550 case 'rich-text':
551 $parsed_selector = $this->parse_selector( $properties['selector'] );
552 $tag_name = $parsed_selector['tag_name'] ?? '';
553 $class_name = $parsed_selector['class_name'] ?? '';
554
555 // If there is no tag_name, attempt to get it from the source.
556 if ( ! $tag_name ) {
557 $block_reader = new \WP_HTML_Tag_Processor( $block_content );
558
559 // If any element matches the selector, get the tag name.
560 if ( $block_reader->next_tag( $parsed_selector['query'] ) ) {
561 $tag_name = $block_reader->get_tag();
562 }
563 }
564
565 // Bail if there is no tag name.
566 if ( ! $tag_name ) {
567 return $block_content;
568 }
569
570 $pattern = '/(<' . $tag_name . '[^>]*>).*?(<\/' . $tag_name . '>)/is';
571
572 return preg_replace( $pattern, '$1' . wp_kses_post( $source_value ) . '$2', $block_content );
573
574 case 'attribute':
575 $amended_content = new \WP_HTML_Tag_Processor( $block_content );
576 $parsed_selector = $this->parse_selector( $properties['selector'] );
577 if ( ! $amended_content->next_tag( $parsed_selector['query'] ) ) {
578 return $block_content;
579 }
580 $amended_content->set_attribute( $properties['attribute'], $source_value );
581 return $amended_content->get_updated_html();
582
583 default:
584 return $block_content;
585 }
586 }
587
588 /**
589 * Parse selector
590 *
591 * @param string $selector
592 * @return array
593 */
594 private function parse_selector( $selector ) {
595 $parsed_attrs = [];
596 $dot_pos = stripos( $selector, '.' );
597 if ( false === $dot_pos ) {
598 $parsed_attrs['tag_name'] = $selector;
599 } elseif ( $dot_pos > 0 ) {
600 $parts = explode( '.', $selector );
601 $parsed_attrs['tag_name'] = $parts[0];
602 $parsed_attrs['class_name'] = $parts[1];
603 } else {
604 $parsed_attrs['class_name'] = trim( $selector, '.' );
605 }
606
607 $query = [];
608 if ( ! empty( $parsed_attrs['tag_name'] ) ) {
609 $query['tag_name'] = $parsed_attrs['tag_name'];
610 }
611
612 if ( ! empty( $parsed_attrs['class_name'] ) ) {
613 $query['class_name'] = $parsed_attrs['class_name'];
614 }
615
616 $parsed_attrs['query'] = $query;
617
618 return $parsed_attrs;
619 }
620
621 /**
622 * Get supported blocks
623 *
624 * @return array
625 */
626 private function get_supported_blocks() {
627 return apply_filters( 'cbb_block_overrides_supported_blocks', $this->supported_blocks );
628 }
629 }
630 endif;
631