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