PluginProbe ʕ •ᴥ•ʔ
FrontBlocks for Gutenberg/GeneratePress / 1.3.2
FrontBlocks for Gutenberg/GeneratePress v1.3.2
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 4 months ago BackButton.php 7 months ago BlockPatterns.php 4 months ago Carousel.php 6 months ago ContainerEdgeAlignment.php 7 months ago Counter.php 4 months ago Events.php 6 months ago FluidTypography.php 4 months ago Gallery.php 8 months ago GravityFormsInline.php 7 months ago Headline.php 4 months ago InsertPost.php 8 months ago ProductCategories.php 8 months ago ReadingProgress.php 7 months ago ReadingTime.php 8 months ago ShapeAnimations.php 7 months ago StackedImages.php 4 months ago StickyColumn.php 8 months ago Testimonials.php 8 months ago
ShapeAnimations.php
496 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( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ), 100 );
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 * Enqueue frontend scripts and styles.
45 *
46 * @return void
47 */
48 public function enqueue_scripts() {
49 wp_enqueue_style(
50 'frontblocks-shape-animations',
51 FRBL_PLUGIN_URL . 'assets/shape-animations/frontblocks-shape-animations.css',
52 array(),
53 FRBL_VERSION
54 );
55
56 // Enqueue Lottie library from CDN.
57 wp_enqueue_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_enqueue_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 // Parse JSON data.
187 $json_data = json_decode( $attrs['frblCustomSvgAnimationJson'], true );
188
189 if ( ! $json_data || ! is_array( $json_data ) ) {
190 return $block_content;
191 }
192
193 // Detect animation type: Lottie or CSS.
194 $is_lottie = $this->is_lottie_json( $json_data );
195
196 if ( $is_lottie ) {
197 return $this->render_lottie_animation( $block_content, $json_data, $attrs );
198 } else {
199 return $this->render_css_animation( $block_content, $json_data, $attrs );
200 }
201 }
202
203 /**
204 * Detect if JSON is a Lottie animation.
205 *
206 * @param array $json_data Parsed JSON data.
207 * @return bool True if Lottie format detected.
208 */
209 private function is_lottie_json( $json_data ) {
210 // Lottie JSON typically has these properties.
211 return isset( $json_data['v'] ) && isset( $json_data['fr'] ) && isset( $json_data['layers'] );
212 }
213
214 /**
215 * Render Lottie animation.
216 *
217 * @param string $block_content Original block content.
218 * @param array $json_data Lottie JSON data.
219 * @param array $attrs Block attributes.
220 * @return string Modified block content.
221 */
222 private function render_lottie_animation( $block_content, $json_data, $attrs ) {
223 // Generate unique ID for this Lottie instance.
224 $unique_id = 'frbl-lottie-' . wp_generate_password( 8, false );
225
226 // Get optional settings.
227 $loop = true; // Default to loop for Lottie.
228 $autoplay = true; // Default to autoplay.
229 $speed = 1; // Default speed.
230
231 // Override with animation settings if provided.
232 if ( isset( $json_data['animation'] ) ) {
233 $loop = isset( $json_data['animation']['loop'] ) ? (bool) $json_data['animation']['loop'] : true;
234 $autoplay = isset( $json_data['animation']['autoplay'] ) ? (bool) $json_data['animation']['autoplay'] : true;
235 $speed = isset( $json_data['animation']['speed'] ) ? (float) $json_data['animation']['speed'] : 1;
236 }
237
238 // Get Shape block styles (width, height, colors from GenerateBlocks).
239 $styles = isset( $attrs['styles'] ) ? $attrs['styles'] : array();
240 $svg_styles = isset( $styles['svg'] ) ? $styles['svg'] : array();
241 $width = isset( $svg_styles['width'] ) ? $svg_styles['width'] : '';
242 $height = isset( $svg_styles['height'] ) ? $svg_styles['height'] : '';
243 $fill_color = isset( $svg_styles['fill'] ) ? $svg_styles['fill'] : '';
244 $stroke_color = isset( $svg_styles['color'] ) ? $svg_styles['color'] : '';
245
246 // Build inline styles.
247 $inline_styles = 'width: ' . ( ! empty( $width ) ? esc_attr( $width ) : '100%' ) . ';';
248 $inline_styles .= 'height: ' . ( ! empty( $height ) ? esc_attr( $height ) : '100%' ) . ';';
249 if ( ! empty( $fill_color ) ) {
250 $inline_styles .= '--lottie-color: ' . esc_attr( $fill_color ) . ';';
251 }
252
253 // Encode Lottie JSON for data attribute.
254 $lottie_json_encoded = htmlspecialchars( wp_json_encode( $json_data ), ENT_QUOTES, 'UTF-8' );
255
256 // Create Lottie container.
257 $lottie_container = '<div ';
258 $lottie_container .= 'id="' . esc_attr( $unique_id ) . '" ';
259 $lottie_container .= 'class="frbl-lottie-animation" ';
260 $lottie_container .= 'data-lottie-json="' . $lottie_json_encoded . '" ';
261 $lottie_container .= 'data-loop="' . esc_attr( $loop ? 'true' : 'false' ) . '" ';
262 $lottie_container .= 'data-autoplay="' . esc_attr( $autoplay ? 'true' : 'false' ) . '" ';
263 $lottie_container .= 'data-speed="' . esc_attr( $speed ) . '" ';
264 $lottie_container .= 'style="' . $inline_styles . '"';
265 $lottie_container .= '></div>';
266
267 // Replace SVG with Lottie container.
268 $block_content = preg_replace(
269 '/<svg[^>]*>.*?<\/svg>/is',
270 $lottie_container,
271 $block_content
272 );
273
274 // Add Lottie class to wrapper.
275 $block_content = preg_replace_callback(
276 '/^<([a-z][a-z0-9]*)\s*((?:[^>]|\\n)*?)(?:class="([^"]*?)")?([^>]*?)>/i',
277 function ( $matches ) {
278 $tag = $matches[1] ?? 'div';
279 $beginning = $matches[2] ?? '';
280 $existing_class = $matches[3] ?? '';
281 $ending = $matches[4] ?? '';
282
283 // Add Lottie wrapper class.
284 if ( ! empty( $existing_class ) ) {
285 $new_class = $existing_class . ' frbl-has-lottie-animation';
286 } else {
287 $new_class = 'frbl-has-lottie-animation';
288 }
289
290 $result = '<' . $tag . ' ' . $beginning;
291 $result .= ' class="' . $new_class . '"';
292 $result .= $ending . '>';
293
294 return $result;
295 },
296 $block_content,
297 1
298 );
299
300 return $block_content;
301 }
302
303 /**
304 * Render CSS animation (original functionality).
305 *
306 * @param string $block_content Original block content.
307 * @param array $json_data JSON data with SVG and animation.
308 * @param array $attrs Block attributes (unused, kept for consistent signature).
309 * @return string Modified block content.
310 */
311 private function render_css_animation( $block_content, $json_data, $attrs ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
312 // Extract SVG and animation data.
313 $custom_svg = isset( $json_data['svg'] ) ? $json_data['svg'] : '';
314 $animation_data = isset( $json_data['animation'] ) ? $json_data['animation'] : array();
315 $animation_name = isset( $animation_data['name'] ) ? sanitize_key( $animation_data['name'] ) : 'customAnimation';
316 $animation_trigger = isset( $animation_data['trigger'] ) ? sanitize_text_field( $animation_data['trigger'] ) : 'load';
317 $animation_keyframes = isset( $animation_data['keyframes'] ) ? $animation_data['keyframes'] : '';
318 $animation_duration = isset( $animation_data['duration'] ) ? sanitize_text_field( $animation_data['duration'] ) : '1s';
319 $animation_delay = isset( $animation_data['delay'] ) ? sanitize_text_field( $animation_data['delay'] ) : '0s';
320 $animation_infinite = isset( $animation_data['infinite'] ) ? (bool) $animation_data['infinite'] : false;
321
322 // Generate unique ID for this block's animation.
323 $unique_id = 'frbl-anim-' . md5( $animation_keyframes );
324
325 // Inject custom keyframes and SVG.
326 if ( ! empty( $animation_keyframes ) ) {
327 // Add inline style with keyframes.
328 $inline_style = '<style>';
329 $inline_style .= esc_html( $animation_keyframes );
330 $inline_style .= ' .frbl-custom-svg-animation.' . esc_attr( $unique_id ) . ' { ';
331 $inline_style .= 'animation-name: ' . esc_attr( $animation_name ) . '; ';
332 $inline_style .= 'animation-duration: ' . esc_attr( $animation_duration ) . '; ';
333 $inline_style .= 'animation-delay: ' . esc_attr( $animation_delay ) . '; ';
334 $inline_style .= 'animation-fill-mode: both; ';
335 $inline_style .= 'animation-timing-function: ease-in-out; ';
336 if ( $animation_infinite ) {
337 $inline_style .= 'animation-iteration-count: infinite; ';
338 }
339 $inline_style .= '} ';
340 if ( 'hover' === $animation_trigger ) {
341 $inline_style .= ' .frbl-custom-svg-animation.' . esc_attr( $unique_id ) . ':hover { ';
342 $inline_style .= 'animation-play-state: running; ';
343 $inline_style .= '} ';
344 }
345 $inline_style .= '</style>';
346
347 $block_content = $inline_style . $block_content;
348 }
349
350 // Replace SVG content if provided.
351 if ( ! empty( $custom_svg ) ) {
352 // Sanitize SVG.
353 $custom_svg = wp_kses( $custom_svg, $this->get_svg_allowed_tags() );
354
355 // Replace the SVG content in the block.
356 $block_content = preg_replace(
357 '/<svg[^>]*>.*?<\/svg>/is',
358 $custom_svg,
359 $block_content
360 );
361 }
362
363 // Add custom animation class.
364 $animation_class = 'frbl-custom-svg-animation ' . $unique_id;
365 $animation_class .= ' frbl-shape-trigger-' . esc_attr( $animation_trigger );
366
367 // Add class to the wrapper.
368 $block_content = preg_replace_callback(
369 '/^<([a-z][a-z0-9]*)\s*((?:[^>]|\\n)*?)(?:class="([^"]*?)")?([^>]*?)>/i',
370 function ( $matches ) use ( $animation_class, $animation_trigger, $animation_name ) {
371 $tag = $matches[1] ?? 'div';
372 $beginning = $matches[2] ?? '';
373 $existing_class = $matches[3] ?? '';
374 $ending = $matches[4] ?? '';
375
376 // Add classes.
377 if ( ! empty( $existing_class ) ) {
378 $new_class = $existing_class . ' ' . $animation_class;
379 } else {
380 $new_class = $animation_class;
381 }
382
383 // Add data attributes.
384 $data_attrs = ' data-shape-animation="' . esc_attr( $animation_name ) . '"';
385 $data_attrs .= ' data-shape-trigger="' . esc_attr( $animation_trigger ) . '"';
386
387 // Build the opening tag.
388 $result = '<' . $tag . ' ' . $beginning;
389 $result .= ' class="' . $new_class . '"';
390 $result .= $data_attrs;
391 $result .= $ending . '>';
392
393 return $result;
394 },
395 $block_content,
396 1
397 );
398
399 return $block_content;
400 }
401
402 /**
403 * Get allowed SVG tags for wp_kses.
404 *
405 * @return array Allowed tags and attributes.
406 */
407 private function get_svg_allowed_tags() {
408 return array(
409 'svg' => array(
410 'xmlns' => array(),
411 'viewbox' => array(),
412 'width' => array(),
413 'height' => array(),
414 'fill' => array(),
415 'class' => array(),
416 'aria-hidden' => array(),
417 'role' => array(),
418 ),
419 'path' => array(
420 'd' => array(),
421 'fill' => array(),
422 'stroke' => array(),
423 'stroke-width' => array(),
424 'class' => array(),
425 ),
426 'circle' => array(
427 'cx' => array(),
428 'cy' => array(),
429 'r' => array(),
430 'fill' => array(),
431 'stroke' => array(),
432 'stroke-width' => array(),
433 'class' => array(),
434 ),
435 'rect' => array(
436 'x' => array(),
437 'y' => array(),
438 'width' => array(),
439 'height' => array(),
440 'fill' => array(),
441 'stroke' => array(),
442 'stroke-width' => array(),
443 'rx' => array(),
444 'ry' => array(),
445 'class' => array(),
446 ),
447 'line' => array(
448 'x1' => array(),
449 'y1' => array(),
450 'x2' => array(),
451 'y2' => array(),
452 'stroke' => array(),
453 'stroke-width' => array(),
454 'class' => array(),
455 ),
456 'polygon' => array(
457 'points' => array(),
458 'fill' => array(),
459 'stroke' => array(),
460 'stroke-width' => array(),
461 'class' => array(),
462 ),
463 'polyline' => array(
464 'points' => array(),
465 'fill' => array(),
466 'stroke' => array(),
467 'stroke-width' => array(),
468 'class' => array(),
469 ),
470 'ellipse' => array(
471 'cx' => array(),
472 'cy' => array(),
473 'rx' => array(),
474 'ry' => array(),
475 'fill' => array(),
476 'stroke' => array(),
477 'stroke-width' => array(),
478 'class' => array(),
479 ),
480 'g' => array(
481 'fill' => array(),
482 'class' => array(),
483 'transform' => array(),
484 ),
485 'defs' => array(),
486 'clippath' => array(
487 'id' => array(),
488 ),
489 'use' => array(
490 'xlink:href' => array(),
491 'href' => array(),
492 ),
493 );
494 }
495 }
496