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