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

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

614 lines 19.5 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' ],
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' ),
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 return better_youtube_embed_block_render_block( $attributes );
361 }
362 } elseif ( 'boldblocks/counting-number' === $block_instance->name ) {
363 // Merge with dynamic content.
364 $attributes = array_merge( $block_instance->attributes, $computed_attributes );
365
366 // Get class and style attributes.
367 $block_reader = new \WP_HTML_Tag_Processor( $block_content );
368 $class = '';
369 $style = '';
370 $value_attrs = [];
371
372 if ( $block_reader->next_tag() ) {
373 $class = $block_reader->get_attribute( 'class' );
374 $style = $block_reader->get_attribute( 'style' );
375 }
376
377 if ( $block_reader->next_tag( [ 'class_name' => 'value' ] ) ) {
378 $value_attribute_names = $block_reader->get_attribute_names_with_prefix( '' );
379 foreach ( $value_attribute_names as $name ) {
380 $value_attrs[ $name ] = $block_reader->get_attribute( $name );
381 }
382 }
383
384 $new_content = '<div class="' . $class . '" style="' . $style . '">';
385 if ( $attributes['prefix'] ?? false ) {
386 $new_content .= '<span class="prefix">' . $attributes['prefix'] . '</span>';
387 }
388 $new_content .= '<span class="value">' . ( $attributes['startVal'] ?? '' ) . '</span>';
389 $new_content .= '<span class="sr-value">' . ( $attributes['value'] ?? '' ) . '</span>';
390 if ( $attributes['suffix'] ?? false ) {
391 $new_content .= '<span class="suffix">' . $attributes['suffix'] . '</span>';
392 }
393 $new_content .= '</div>';
394
395 $overridable_attrs = [ 'data-value', 'data-start-val', 'data-prefix', 'data-suffix' ];
396
397 $block_reader = new \WP_HTML_Tag_Processor( $new_content );
398 if ( $block_reader->next_tag( [ 'class_name' => 'value' ] ) ) {
399 foreach ( $value_attrs as $attribute_key => $attribute_value ) {
400 if ( in_array( $attribute_key, $overridable_attrs, true ) ) {
401 switch ( $attribute_key ) {
402 case 'data-value':
403 $attribute_value = $attributes['value'] ?? '';
404 break;
405
406 case 'data-start-val':
407 $attribute_value = $attributes['startVal'] ?? '';
408 break;
409
410 case 'data-prefix':
411 $attribute_value = $attributes['prefix'] ?? '';
412 break;
413
414 case 'data-suffix':
415 $attribute_value = $attributes['suffix'] ?? '';
416 break;
417
418 default:
419 break;
420 }
421 }
422
423 $block_reader->set_attribute( $attribute_key, $attribute_value );
424 }
425 }
426
427 return $block_reader->get_updated_html();
428 }
429
430 return $content;
431 }
432
433 /**
434 * Add more blocks to the supported list
435 *
436 * @param array $supported_blocks
437 * @return array
438 */
439 public function support_additional_blocks( $supported_blocks ) {
440 $supported_blocks['core/embed'] = [ 'url', 'caption', 'type', 'providerNameSlug', 'responsive', 'previewable' ];
441 $supported_blocks['core/video'] = [ 'src', 'caption' ];
442 $supported_blocks['core/audio'] = [ 'src', 'caption' ];
443 $supported_blocks['core/code'] = [ 'content' ];
444 $supported_blocks['core/verse'] = [ 'content' ];
445 $supported_blocks['core/preformatted'] = [ 'content' ];
446 $supported_blocks['core/pullquote'] = [ 'value', 'citation' ];
447
448 $supported_blocks['boldblocks/youtube-block'] = [ 'url', 'caption', 'aspectRatio', 'isMaxResThumbnail', 'customThumbnail' ];
449 $supported_blocks['boldblocks/counting-number'] = [ 'value', 'startVal', 'prefix', 'suffix' ];
450
451 return $supported_blocks;
452 }
453
454 /**
455 * Build dynamic contents
456 *
457 * @param array $parsed_block
458 * @param WP_Block $block_instance
459 * @return array
460 */
461 private function process_block_bindings( $parsed_block, $block_instance ) {
462 $computed_attributes = array();
463 $block_name = $parsed_block['blockName'] ?? '';
464 $supported_blocks = $this->get_supported_blocks();
465
466 // If the block doesn't have the bindings property, isn't one of the supported
467 // block types, or the bindings property is not an array, return the block content.
468 if (
469 ! isset( $supported_blocks[ $block_name ] ) ||
470 empty( $parsed_block['attrs']['metadata']['bindings'] ) ||
471 ! is_array( $parsed_block['attrs']['metadata']['bindings'] )
472 ) {
473 return $computed_attributes;
474 }
475
476 foreach ( $parsed_block['attrs']['metadata']['bindings'] as $attribute_name => $block_binding ) {
477 // If the attribute is not in the supported list, process next attribute.
478 if ( ! array_key_exists( $block_name, $supported_blocks ) ) {
479 continue;
480 }
481
482 // If no source is provided, or that source is not registered, process next attribute.
483 if ( ! isset( $block_binding['source'] ) || ! is_string( $block_binding['source'] ) ) {
484 continue;
485 }
486
487 $block_binding_source = get_block_bindings_source( $block_binding['source'] );
488 if ( null === $block_binding_source ) {
489 continue;
490 }
491
492 $source_args = ! empty( $block_binding['args'] ) && is_array( $block_binding['args'] ) ? $block_binding['args'] : array();
493 $source_value = $block_binding_source->get_value( $source_args, $block_instance, $attribute_name );
494
495 // If the value is not null, process the HTML based on the block and the attribute.
496 if ( ! is_null( $source_value ) ) {
497 $computed_attributes[ $attribute_name ] = $source_value;
498 }
499 }
500
501 return $computed_attributes;
502 }
503
504 /**
505 * Depending on the block attribute name, replace its value in the HTML based on the value provided.
506 *
507 * @param string $block_content Block content.
508 * @param string $attribute_name The attribute name to replace.
509 * @param mixed $source_value The value used to replace in the HTML.
510 * @param WP_Block $block_instance
511 * @param array $computed_attributes
512 * @return string The modified block content.
513 */
514 private function replace_html( string $block_content, string $attribute_name, $source_value, $block_instance, $computed_attributes ) {
515 $block_type = $block_instance->block_type;
516 $properties = $block_type->attributes[ $attribute_name ];
517
518 // Use this filter to alter source, selector for the property.
519 $properties = apply_filters( 'cbb_get_binding_attribute_properties', $properties, $attribute_name, $block_instance, $computed_attributes );
520
521 if ( ! isset( $properties['source'] ) ) {
522 return $block_content;
523 }
524
525 // Make sure the seletor is valid and don't support multiple selector.
526 if ( empty( $properties['selector'] ) || false !== stripos( $properties['selector'], ',' ) ) {
527 return $block_content;
528 }
529
530 // Depending on the attribute source, the processing will be different.
531 switch ( $properties['source'] ) {
532 case 'html':
533 case 'rich-text':
534 $parsed_selector = $this->parse_selector( $properties['selector'] );
535 $tag_name = $parsed_selector['tag_name'] ?? '';
536 $class_name = $parsed_selector['class_name'] ?? '';
537
538 // If there is no tag_name, attempt to get it from the source.
539 if ( ! $tag_name ) {
540 $block_reader = new \WP_HTML_Tag_Processor( $block_content );
541
542 // If any element matches the selector, get the tag name.
543 if ( $block_reader->next_tag( $parsed_selector['query'] ) ) {
544 $tag_name = $block_reader->get_tag();
545 }
546 }
547
548 // Bail if there is no tag name.
549 if ( ! $tag_name ) {
550 return $block_content;
551 }
552
553 $pattern = '/(<' . $tag_name . '[^>]*>).*?(<\/' . $tag_name . '>)/is';
554
555 return preg_replace( $pattern, '$1' . wp_kses_post( $source_value ) . '$2', $block_content );
556
557 case 'attribute':
558 $amended_content = new \WP_HTML_Tag_Processor( $block_content );
559 $parsed_selector = $this->parse_selector( $properties['selector'] );
560 if ( ! $amended_content->next_tag( $parsed_selector['query'] ) ) {
561 return $block_content;
562 }
563 $amended_content->set_attribute( $properties['attribute'], $source_value );
564 return $amended_content->get_updated_html();
565
566 default:
567 return $block_content;
568 }
569 }
570
571 /**
572 * Parse selector
573 *
574 * @param string $selector
575 * @return array
576 */
577 private function parse_selector( $selector ) {
578 $parsed_attrs = [];
579 $dot_pos = stripos( $selector, '.' );
580 if ( false === $dot_pos ) {
581 $parsed_attrs['tag_name'] = $selector;
582 } elseif ( $dot_pos > 0 ) {
583 $parts = explode( '.', $selector );
584 $parsed_attrs['tag_name'] = $parts[0];
585 $parsed_attrs['class_name'] = $parts[1];
586 } else {
587 $parsed_attrs['class_name'] = trim( $selector, '.' );
588 }
589
590 $query = [];
591 if ( ! empty( $parsed_attrs['tag_name'] ) ) {
592 $query['tag_name'] = $parsed_attrs['tag_name'];
593 }
594
595 if ( ! empty( $parsed_attrs['class_name'] ) ) {
596 $query['class_name'] = $parsed_attrs['class_name'];
597 }
598
599 $parsed_attrs['query'] = $query;
600
601 return $parsed_attrs;
602 }
603
604 /**
605 * Get supported blocks
606 *
607 * @return array
608 */
609 private function get_supported_blocks() {
610 return apply_filters( 'cbb_block_overrides_supported_blocks', $this->supported_blocks );
611 }
612 }
613 endif;
614