PluginProbe ʕ •ᴥ•ʔ
FrontBlocks for Gutenberg/GeneratePress / 1.3.6
FrontBlocks for Gutenberg/GeneratePress v1.3.6
trunk 0.2.0 0.2.1 0.2.2 0.2.3 0.2.4 0.2.5 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.1.0 1.2.0 1.2.1 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 ci-artifacts
frontblocks / includes / Frontend / ShapeAnimations.php
frontblocks / includes / Frontend Last commit date
Animations.php 1 month ago BackButton.php 7 months ago BeforeAfter.php 1 month ago BlockPatterns.php 4 months ago Carousel.php 1 month ago ContainerEdgeAlignment.php 1 month ago Counter.php 1 month ago Events.php 1 month ago FaqSchema.php 1 month ago FluidTypography.php 1 month ago Gallery.php 8 months ago GravityFormsInline.php 1 month ago Headline.php 1 month ago InsertPost.php 1 month ago ProductCategories.php 1 month ago ReadingProgress.php 7 months ago ReadingTime.php 8 months ago ShapeAnimations.php 1 month ago StackedImages.php 4 months ago StickyColumn.php 1 month ago SvgUpload.php 1 month ago Testimonials.php 8 months ago TextAnimation.php 1 month ago UserText.php 1 month ago
ShapeAnimations.php
537 lines
1 <?php
2 /**
3 * Shape Animations module for FrontBlocks.
4 *
5 * Adds animation controls to GenerateBlocks Shape block.
6 *
7 * @package FrontBlocks
8 * @author David Perez <david@close.technology>
9 * @copyright 2023 Closemarketing
10 * @version 1.0
11 */
12
13 namespace FrontBlocks\Frontend;
14
15 defined( 'ABSPATH' ) || exit;
16
17 /**
18 * ShapeAnimations class.
19 *
20 * @since 1.0.0
21 */
22 class ShapeAnimations {
23
24 /**
25 * CSS animation styles queued for wp_footer output.
26 *
27 * @var array
28 */
29 private static $queued_css = array();
30
31 /**
32 * Constructor.
33 */
34 public function __construct() {
35 $this->init_hooks();
36 }
37
38 /**
39 * Initialize hooks.
40 *
41 * @return void
42 */
43 private function init_hooks() {
44 add_action( 'init', array( $this, 'register_scripts' ) );
45 add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_block_editor_assets' ), 5 );
46 add_action( 'enqueue_block_editor_assets', array( $this, 'register_shape_animation_attributes' ), 15 );
47 add_filter( 'render_block', array( $this, 'add_animation_classes_to_shape' ), 10, 2 );
48 add_action( 'wp_footer', array( $this, 'output_queued_styles' ), 5 );
49 }
50
51 /**
52 * Register frontend scripts and styles for conditional enqueueing.
53 *
54 * @return void
55 */
56 public function register_scripts() {
57 wp_register_style(
58 'frontblocks-shape-animations',
59 FRBL_PLUGIN_URL . 'assets/shape-animations/frontblocks-shape-animations.css',
60 array(),
61 FRBL_VERSION
62 );
63
64 // Register Lottie library from CDN.
65 wp_register_script(
66 'lottie-player',
67 'https://cdnjs.cloudflare.com/ajax/libs/lottie-web/5.12.2/lottie.min.js',
68 array(),
69 '5.12.2',
70 true
71 );
72
73 wp_register_script(
74 'frontblocks-shape-animations',
75 FRBL_PLUGIN_URL . 'assets/shape-animations/frontblocks-shape-animations.js',
76 array( 'lottie-player' ),
77 FRBL_VERSION,
78 true
79 );
80 }
81
82 /**
83 * Enqueue block editor assets.
84 *
85 * @return void
86 */
87 public function enqueue_block_editor_assets() {
88 // Enqueue CSS for editor preview.
89 wp_enqueue_style(
90 'frontblocks-shape-animations-editor',
91 FRBL_PLUGIN_URL . 'assets/shape-animations/frontblocks-shape-animations.css',
92 array(),
93 FRBL_VERSION
94 );
95
96 // Enqueue Lottie library for editor preview.
97 wp_enqueue_script(
98 'lottie-player-editor',
99 'https://cdnjs.cloudflare.com/ajax/libs/lottie-web/5.12.2/lottie.min.js',
100 array(),
101 '5.12.2',
102 true
103 );
104
105 // Enqueue editor controls script.
106 wp_enqueue_script(
107 'frontblocks-shape-animation-editor',
108 FRBL_PLUGIN_URL . 'assets/shape-animations/frontblocks-shape-animation-option.js',
109 array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-i18n', 'wp-hooks', 'wp-block-editor', 'lottie-player-editor' ),
110 FRBL_VERSION,
111 true
112 );
113
114 wp_set_script_translations(
115 'frontblocks-shape-animation-editor',
116 'frontblocks'
117 );
118 }
119
120 /**
121 * Register animation attributes for Shape and Icon blocks.
122 *
123 * @return void
124 */
125 public function register_shape_animation_attributes() {
126 wp_add_inline_script(
127 'wp-blocks',
128 "
129 wp.hooks.addFilter(
130 'blocks.registerBlockType',
131 'frontblocks/shape-animation-attributes',
132 function( settings, name ) {
133 if ( 'generateblocks/shape' !== name && 'core/icon' !== name ) {
134 return settings;
135 }
136
137 if ( ! settings || typeof settings !== 'object' ) {
138 return settings;
139 }
140
141 if ( ! settings.attributes || typeof settings.attributes !== 'object' ) {
142 settings.attributes = {};
143 }
144
145 try {
146 settings.attributes = {
147 ...settings.attributes,
148 frblCustomSvgAnimationEnabled: {
149 type: 'boolean',
150 default: false
151 },
152 frblCustomSvgAnimationJson: {
153 type: 'string',
154 default: ''
155 }
156 };
157 } catch( error ) {
158 return settings;
159 }
160
161 return settings;
162 }
163 );
164 "
165 );
166 }
167
168 /**
169 * Add animation classes to shape blocks on frontend render.
170 *
171 * @param string $block_content Block content.
172 * @param array $block Block data.
173 * @return string Modified block content.
174 */
175 public function add_animation_classes_to_shape( $block_content, $block ) {
176 if ( ! isset( $block['blockName'] ) ||
177 ( 'generateblocks/shape' !== $block['blockName'] && 'core/icon' !== $block['blockName'] ) ) {
178 return $block_content;
179 }
180
181 if ( empty( $block['attrs'] ) ) {
182 return $block_content;
183 }
184
185 $attrs = $block['attrs'];
186
187 if ( ! isset( $attrs['frblCustomSvgAnimationEnabled'] ) || ! $attrs['frblCustomSvgAnimationEnabled'] ) {
188 return $block_content;
189 }
190
191 if ( empty( $attrs['frblCustomSvgAnimationJson'] ) ) {
192 return $block_content;
193 }
194
195 // Enqueue frontend assets only when a shape animation block is detected.
196 if ( ! wp_style_is( 'frontblocks-shape-animations', 'enqueued' ) ) {
197 wp_enqueue_style( 'frontblocks-shape-animations' );
198 }
199 if ( ! wp_script_is( 'frontblocks-shape-animations', 'enqueued' ) ) {
200 wp_enqueue_script( 'frontblocks-shape-animations' );
201 }
202
203 // Parse JSON data.
204 $json_data = json_decode( $attrs['frblCustomSvgAnimationJson'], true );
205
206 if ( ! $json_data || ! is_array( $json_data ) ) {
207 return $block_content;
208 }
209
210 // Detect animation type: Lottie or CSS.
211 $is_lottie = $this->is_lottie_json( $json_data );
212
213 $block_name = $block['blockName'];
214
215 if ( $is_lottie ) {
216 return $this->render_lottie_animation( $block_content, $json_data, $attrs, $block_name );
217 } else {
218 return $this->render_css_animation( $block_content, $json_data, $attrs );
219 }
220 }
221
222 /**
223 * Detect if JSON is a Lottie animation.
224 *
225 * @param array $json_data Parsed JSON data.
226 * @return bool True if Lottie format detected.
227 */
228 private function is_lottie_json( $json_data ) {
229 // Lottie JSON typically has these properties.
230 return isset( $json_data['v'] ) && isset( $json_data['fr'] ) && isset( $json_data['layers'] );
231 }
232
233 /**
234 * Render Lottie animation.
235 *
236 * @param string $block_content Original block content.
237 * @param array $json_data Lottie JSON data.
238 * @param array $attrs Block attributes.
239 * @param string $block_name Block name.
240 * @return string Modified block content.
241 */
242 private function render_lottie_animation( $block_content, $json_data, $attrs, $block_name = 'generateblocks/shape' ) {
243 // Generate unique ID for this Lottie instance.
244 $unique_id = 'frbl-lottie-' . wp_generate_password( 8, false );
245
246 // Get optional settings.
247 $loop = true; // Default to loop for Lottie.
248 $autoplay = true; // Default to autoplay.
249 $speed = 1; // Default speed.
250
251 // Override with animation settings if provided.
252 if ( isset( $json_data['animation'] ) ) {
253 $loop = isset( $json_data['animation']['loop'] ) ? (bool) $json_data['animation']['loop'] : true;
254 $autoplay = isset( $json_data['animation']['autoplay'] ) ? (bool) $json_data['animation']['autoplay'] : true;
255 $speed = isset( $json_data['animation']['speed'] ) ? (float) $json_data['animation']['speed'] : 1;
256 }
257
258 // Extract size and color based on block type.
259 if ( 'core/icon' === $block_name ) {
260 $icon_width = isset( $attrs['width'] ) ? absint( $attrs['width'] ) : 0;
261 $width = $icon_width > 0 ? $icon_width . 'px' : '';
262 $height = $width;
263 $fill_color = isset( $attrs['style']['color']['text'] ) ? sanitize_text_field( $attrs['style']['color']['text'] ) : '';
264 } else {
265 // Get Shape block styles (width, height, colors from GenerateBlocks).
266 $styles = isset( $attrs['styles'] ) ? $attrs['styles'] : array();
267 $svg_styles = isset( $styles['svg'] ) ? $styles['svg'] : array();
268 $width = isset( $svg_styles['width'] ) ? $svg_styles['width'] : '';
269 $height = isset( $svg_styles['height'] ) ? $svg_styles['height'] : '';
270 $fill_color = isset( $svg_styles['fill'] ) ? $svg_styles['fill'] : '';
271 }
272
273 // Build inline styles.
274 $inline_styles = 'width: ' . ( ! empty( $width ) ? esc_attr( $width ) : '100%' ) . ';';
275 $inline_styles .= 'height: ' . ( ! empty( $height ) ? esc_attr( $height ) : '100%' ) . ';';
276 if ( ! empty( $fill_color ) ) {
277 $inline_styles .= '--lottie-color: ' . esc_attr( $fill_color ) . ';';
278 }
279
280 // Encode Lottie JSON for data attribute.
281 $lottie_json_encoded = htmlspecialchars( wp_json_encode( $json_data ), ENT_QUOTES, 'UTF-8' );
282
283 // Create Lottie container.
284 $lottie_container = '<div ';
285 $lottie_container .= 'id="' . esc_attr( $unique_id ) . '" ';
286 $lottie_container .= 'class="frbl-lottie-animation" ';
287 $lottie_container .= 'data-lottie-json="' . $lottie_json_encoded . '" ';
288 $lottie_container .= 'data-loop="' . esc_attr( $loop ? 'true' : 'false' ) . '" ';
289 $lottie_container .= 'data-autoplay="' . esc_attr( $autoplay ? 'true' : 'false' ) . '" ';
290 $lottie_container .= 'data-speed="' . esc_attr( $speed ) . '" ';
291 $lottie_container .= 'style="' . $inline_styles . '"';
292 $lottie_container .= '></div>';
293
294 // Replace SVG with Lottie container.
295 $block_content = preg_replace(
296 '/<svg[^>]*>.*?<\/svg>/is',
297 $lottie_container,
298 $block_content
299 );
300
301 // Add Lottie class to wrapper.
302 $block_content = preg_replace_callback(
303 '/^<([a-z][a-z0-9]*)\s*((?:[^>]|\\n)*?)(?:class="([^"]*?)")?([^>]*?)>/i',
304 function ( $matches ) {
305 $tag = $matches[1] ?? 'div';
306 $beginning = $matches[2] ?? '';
307 $existing_class = $matches[3] ?? '';
308 $ending = $matches[4] ?? '';
309
310 // Add Lottie wrapper class.
311 if ( ! empty( $existing_class ) ) {
312 $new_class = $existing_class . ' frbl-has-lottie-animation';
313 } else {
314 $new_class = 'frbl-has-lottie-animation';
315 }
316
317 $result = '<' . $tag . ' ' . $beginning;
318 $result .= ' class="' . $new_class . '"';
319 $result .= $ending . '>';
320
321 return $result;
322 },
323 $block_content,
324 1
325 );
326
327 return $block_content;
328 }
329
330 /**
331 * Output all queued CSS animation keyframes in the footer.
332 *
333 * @return void
334 */
335 public function output_queued_styles() {
336 if ( empty( self::$queued_css ) ) {
337 return;
338 }
339 echo '<style id="frbl-shape-animation-keyframes">';
340 foreach ( self::$queued_css as $css ) {
341 echo $css; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- sanitized in render_css_animation.
342 }
343 echo '</style>';
344 }
345
346 /**
347 * Render CSS animation (original functionality).
348 *
349 * @param string $block_content Original block content.
350 * @param array $json_data JSON data with SVG and animation.
351 * @param array $attrs Block attributes (unused, kept for consistent signature).
352 * @return string Modified block content.
353 */
354 private function render_css_animation( $block_content, $json_data, $attrs ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
355 // Extract SVG and animation data.
356 $custom_svg = isset( $json_data['svg'] ) ? $json_data['svg'] : '';
357 $animation_data = isset( $json_data['animation'] ) ? $json_data['animation'] : array();
358 $animation_name = isset( $animation_data['name'] ) ? sanitize_text_field( $animation_data['name'] ) : 'customAnimation';
359 $animation_trigger = isset( $animation_data['trigger'] ) ? sanitize_text_field( $animation_data['trigger'] ) : 'load';
360 $animation_keyframes = isset( $animation_data['keyframes'] ) ? $animation_data['keyframes'] : '';
361 $animation_duration = isset( $animation_data['duration'] ) ? sanitize_text_field( $animation_data['duration'] ) : '1s';
362 $animation_delay = isset( $animation_data['delay'] ) ? sanitize_text_field( $animation_data['delay'] ) : '0s';
363 $animation_infinite = isset( $animation_data['infinite'] ) ? (bool) $animation_data['infinite'] : false;
364
365 // Generate unique ID for this block's animation.
366 $unique_id = 'frbl-anim-' . md5( $animation_keyframes );
367
368 // Queue CSS keyframes for footer output (avoids inline style tag issues).
369 if ( ! empty( $animation_keyframes ) && ! isset( self::$queued_css[ $unique_id ] ) ) {
370 // Strip HTML tags to prevent injection; CSS does not use < > & characters.
371 $css = wp_strip_all_tags( $animation_keyframes );
372 $css .= ' .frbl-custom-svg-animation.' . esc_attr( $unique_id ) . ' { ';
373 $css .= 'animation-name: ' . esc_attr( $animation_name ) . '; ';
374 $css .= 'animation-duration: ' . esc_attr( $animation_duration ) . '; ';
375 $css .= 'animation-delay: ' . esc_attr( $animation_delay ) . '; ';
376 $css .= 'animation-fill-mode: both; ';
377 $css .= 'animation-timing-function: ease-in-out; ';
378 if ( $animation_infinite ) {
379 $css .= 'animation-iteration-count: infinite; ';
380 }
381 $css .= '} ';
382 if ( 'hover' === $animation_trigger ) {
383 $css .= ' .frbl-custom-svg-animation.' . esc_attr( $unique_id ) . ':hover { ';
384 $css .= 'animation-play-state: running; ';
385 $css .= '} ';
386 }
387
388 self::$queued_css[ $unique_id ] = $css;
389 }
390
391 // Replace SVG content if provided.
392 if ( ! empty( $custom_svg ) ) {
393 // Sanitize SVG.
394 $custom_svg = wp_kses( $custom_svg, $this->get_svg_allowed_tags() );
395
396 // Replace the SVG content in the block.
397 $block_content = preg_replace(
398 '/<svg[^>]*>.*?<\/svg>/is',
399 $custom_svg,
400 $block_content
401 );
402 }
403
404 // Add custom animation class.
405 $animation_class = 'frbl-custom-svg-animation ' . $unique_id;
406 $animation_class .= ' frbl-shape-trigger-' . esc_attr( $animation_trigger );
407
408 // Add class to the wrapper.
409 $block_content = preg_replace_callback(
410 '/^<([a-z][a-z0-9]*)\s*((?:[^>]|\\n)*?)(?:class="([^"]*?)")?([^>]*?)>/i',
411 function ( $matches ) use ( $animation_class, $animation_trigger, $animation_name ) {
412 $tag = $matches[1] ?? 'div';
413 $beginning = $matches[2] ?? '';
414 $existing_class = $matches[3] ?? '';
415 $ending = $matches[4] ?? '';
416
417 // Add classes.
418 if ( ! empty( $existing_class ) ) {
419 $new_class = $existing_class . ' ' . $animation_class;
420 } else {
421 $new_class = $animation_class;
422 }
423
424 // Add data attributes.
425 $data_attrs = ' data-shape-animation="' . esc_attr( $animation_name ) . '"';
426 $data_attrs .= ' data-shape-trigger="' . esc_attr( $animation_trigger ) . '"';
427
428 // Build the opening tag.
429 $result = '<' . $tag . ' ' . $beginning;
430 $result .= ' class="' . $new_class . '"';
431 $result .= $data_attrs;
432 $result .= $ending . '>';
433
434 return $result;
435 },
436 $block_content,
437 1
438 );
439
440 return $block_content;
441 }
442
443 /**
444 * Get allowed SVG tags for wp_kses.
445 *
446 * @return array Allowed tags and attributes.
447 */
448 private function get_svg_allowed_tags() {
449 return array(
450 'svg' => array(
451 'xmlns' => array(),
452 'viewbox' => array(),
453 'width' => array(),
454 'height' => array(),
455 'fill' => array(),
456 'class' => array(),
457 'aria-hidden' => array(),
458 'role' => array(),
459 ),
460 'path' => array(
461 'd' => array(),
462 'fill' => array(),
463 'stroke' => array(),
464 'stroke-width' => array(),
465 'class' => array(),
466 ),
467 'circle' => array(
468 'cx' => array(),
469 'cy' => array(),
470 'r' => array(),
471 'fill' => array(),
472 'stroke' => array(),
473 'stroke-width' => array(),
474 'class' => array(),
475 ),
476 'rect' => array(
477 'x' => array(),
478 'y' => array(),
479 'width' => array(),
480 'height' => array(),
481 'fill' => array(),
482 'stroke' => array(),
483 'stroke-width' => array(),
484 'rx' => array(),
485 'ry' => array(),
486 'class' => array(),
487 ),
488 'line' => array(
489 'x1' => array(),
490 'y1' => array(),
491 'x2' => array(),
492 'y2' => array(),
493 'stroke' => array(),
494 'stroke-width' => array(),
495 'class' => array(),
496 ),
497 'polygon' => array(
498 'points' => array(),
499 'fill' => array(),
500 'stroke' => array(),
501 'stroke-width' => array(),
502 'class' => array(),
503 ),
504 'polyline' => array(
505 'points' => array(),
506 'fill' => array(),
507 'stroke' => array(),
508 'stroke-width' => array(),
509 'class' => array(),
510 ),
511 'ellipse' => array(
512 'cx' => array(),
513 'cy' => array(),
514 'rx' => array(),
515 'ry' => array(),
516 'fill' => array(),
517 'stroke' => array(),
518 'stroke-width' => array(),
519 'class' => array(),
520 ),
521 'g' => array(
522 'fill' => array(),
523 'class' => array(),
524 'transform' => array(),
525 ),
526 'defs' => array(),
527 'clippath' => array(
528 'id' => array(),
529 ),
530 'use' => array(
531 'xlink:href' => array(),
532 'href' => array(),
533 ),
534 );
535 }
536 }
537