PluginProbe ʕ •ᴥ•ʔ
FrontBlocks for Gutenberg/GeneratePress / 1.5.2
FrontBlocks for Gutenberg/GeneratePress v1.5.2
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 / assets / text-animation / frontblocks-text-animation-option.jsx
frontblocks / assets / text-animation Last commit date
animations 3 months ago frontblocks-text-animation-frontend.js 3 months ago frontblocks-text-animation-option.jsx 2 months ago frontblocks-text-animation.css 3 months ago frontblocks-text-animation.js 2 months ago
frontblocks-text-animation-option.jsx
1537 lines
1 const { registerBlockType } = wp.blocks;
2 const { Fragment, useState } = wp.element;
3 const { useSelect } = wp.data;
4 const {
5 InspectorControls,
6 useBlockProps,
7 RichText,
8 PanelColorSettings,
9 } = wp.blockEditor;
10 const {
11 PanelBody,
12 SelectControl,
13 RangeControl,
14 Button,
15 ToggleControl,
16 __experimentalNumberControl: NumberControl,
17 __experimentalToggleGroupControl: ToggleGroupControl,
18 __experimentalToggleGroupControlOptionIcon: ToggleGroupControlOptionIcon,
19 } = wp.components;
20 const { __ } = wp.i18n;
21
22 const FONT_SIZE_UNITS = [
23 { label: 'px', value: 'px' },
24 { label: 'rem', value: 'rem' },
25 { label: 'em', value: 'em' },
26 { label: 'vw', value: 'vw' },
27 ];
28
29 const TAG_OPTIONS = [
30 { label: 'h1', value: 'h1' },
31 { label: 'h2', value: 'h2' },
32 { label: 'h3', value: 'h3' },
33 { label: 'h4', value: 'h4' },
34 { label: 'h5', value: 'h5' },
35 { label: 'h6', value: 'h6' },
36 { label: 'p', value: 'p' },
37 { label: 'span (inline)', value: 'span' },
38 ];
39
40 const FONT_WEIGHT_OPTIONS = [
41 { label: __( 'Thin (100)', 'frontblocks' ), value: '100' },
42 { label: __( 'Extra Light (200)', 'frontblocks' ), value: '200' },
43 { label: __( 'Light (300)', 'frontblocks' ), value: '300' },
44 { label: __( 'Normal (400)', 'frontblocks' ), value: '400' },
45 { label: __( 'Medium (500)', 'frontblocks' ), value: '500' },
46 { label: __( 'Semi Bold (600)', 'frontblocks' ), value: '600' },
47 { label: __( 'Bold (700)', 'frontblocks' ), value: '700' },
48 { label: __( 'Extra Bold (800)', 'frontblocks' ), value: '800' },
49 { label: __( 'Black (900)', 'frontblocks' ), value: '900' },
50 ];
51
52 const ALIGN_LEFT_ICON = (
53 <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" aria-hidden="true">
54 <path d="M13 15H3v2h10v-2zm0-8H3v2h10V7zM3 13h18v-2H3v2zm0 8h18v-2H3v2zM3 3v2h18V3H3z"/>
55 </svg>
56 );
57 const ALIGN_CENTER_ICON = (
58 <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" aria-hidden="true">
59 <path d="M7 15v2h10v-2H7zm-4 6h18v-2H3v2zm0-8h18v-2H3v2zm4-6v2h10V7H7zM3 3v2h18V3H3z"/>
60 </svg>
61 );
62 const ALIGN_RIGHT_ICON = (
63 <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" aria-hidden="true">
64 <path d="M3 21h18v-2H3v2zm8-4h10v-2H11v2zm-8-4h18v-2H3v2zm8-4h10V7H11v2zM3 3v2h18V3H3z"/>
65 </svg>
66 );
67 const ALIGN_JUSTIFY_ICON = (
68 <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" aria-hidden="true">
69 <path d="M3 21h18v-2H3v2zm0-4h18v-2H3v2zm0-4h18v-2H3v2zm0-4h18V7H3v2zm0-6v2h18V3H3z"/>
70 </svg>
71 );
72
73 const TRANSFORM_NONE_ICON = (
74 <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" aria-hidden="true">
75 <text x="4" y="17" fontFamily="serif" fontSize="14" fontWeight="bold" fill="currentColor">Ag</text>
76 <line x1="3" y1="21" x2="21" y2="3" stroke="currentColor" strokeWidth="1.5"/>
77 </svg>
78 );
79 const TRANSFORM_UPPERCASE_ICON = (
80 <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" aria-hidden="true">
81 <text x="2" y="17" fontFamily="serif" fontSize="15" fontWeight="bold" fill="currentColor">AA</text>
82 </svg>
83 );
84 const TRANSFORM_LOWERCASE_ICON = (
85 <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" aria-hidden="true">
86 <text x="2" y="17" fontFamily="serif" fontSize="15" fill="currentColor">aa</text>
87 </svg>
88 );
89 const TRANSFORM_CAPITALIZE_ICON = (
90 <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" aria-hidden="true">
91 <text x="2" y="17" fontFamily="serif" fontSize="15" fill="currentColor">Aa</text>
92 </svg>
93 );
94
95 const FONT_STYLE_OPTIONS = [
96 { label: __( 'Normal', 'frontblocks' ), value: 'normal' },
97 { label: __( 'Italic', 'frontblocks' ), value: 'italic' },
98 ];
99
100 const ANIMATION_OPTIONS = [
101 { label: __( 'None', 'frontblocks' ), value: 'none' },
102 { label: __( 'Fade In', 'frontblocks' ), value: 'fade-in' },
103 { label: __( 'Typewriter', 'frontblocks' ), value: 'typewriter' },
104 { label: __( 'Shuffle Text', 'frontblocks' ), value: 'shuffle-text' },
105 { label: __( 'Slide Up', 'frontblocks' ), value: 'slide-up' },
106 { label: __( 'Slide Down', 'frontblocks' ), value: 'slide-down' },
107 { label: __( 'Slide Left', 'frontblocks' ), value: 'slide-left' },
108 { label: __( 'Slide Right', 'frontblocks' ), value: 'slide-right' },
109 { label: __( 'Drop In', 'frontblocks' ), value: 'drop-in' },
110 { label: __( 'Swing', 'frontblocks' ), value: 'swing' },
111 { label: __( 'Pulse', 'frontblocks' ), value: 'pulse' },
112 { label: __( 'Flash', 'frontblocks' ), value: 'flash' },
113 { label: __( 'Rubber Band', 'frontblocks' ), value: 'rubber-band' },
114 { label: __( 'Wave', 'frontblocks' ), value: 'wave' },
115 { label: __( 'Stretch', 'frontblocks' ), value: 'stretch' },
116 { label: __( 'Squeeze', 'frontblocks' ), value: 'squeeze' },
117 { label: __( 'Roll In', 'frontblocks' ), value: 'roll-in' },
118 { label: __( 'Glitch', 'frontblocks' ), value: 'glitch' },
119 { label: __( 'Random Reveal', 'frontblocks' ), value: 'random-reveal' },
120 { label: __( 'Flicker', 'frontblocks' ), value: 'flicker' },
121 { label: __( 'Block Reveal', 'frontblocks' ), value: 'block-reveal' },
122 { label: __( 'Tracking Expand', 'frontblocks' ), value: 'tracking-expand' },
123 { label: __( 'Terminal Type', 'frontblocks' ), value: 'terminal-type' },
124 { label: __( 'Solid Outline', 'frontblocks' ), value: 'solid-outline' },
125 { label: __( 'Glitch RGB', 'frontblocks' ), value: 'glitch-rgb' },
126 { label: __( 'Water Drop', 'frontblocks' ), value: 'water-drop' },
127 { label: __( 'Pulse Neon', 'frontblocks' ), value: 'pulse-neon' },
128 { label: __( 'Shadow Pop', 'frontblocks' ), value: 'shadow-pop' },
129 { label: __( 'Scale In', 'frontblocks' ), value: 'scale-in' },
130 { label: __( 'Blur In', 'frontblocks' ), value: 'blur-in' },
131 { label: __( 'Glow In', 'frontblocks' ), value: 'glow-in' },
132 { label: __( 'Bounce In', 'frontblocks' ), value: 'bounce-in' },
133 { label: __( 'Flip In', 'frontblocks' ), value: 'flip-in' },
134 { label: __( 'Rotate In', 'frontblocks' ), value: 'rotate-in' },
135 ];
136
137 function stripHtml( html ) {
138 return html ? html.replace( /<[^>]*>/g, '' ) : '';
139 }
140
141 /* ── Animation preview registry ─────────────────────────────
142 Each entry: { loop: bool, render( text, style, Tag, key ) → JSX }
143 Add a new entry for every new animation type.
144 ──────────────────────────────────────────────────────────── */
145 const ANIMATION_PREVIEWS = {
146 'fade-in': {
147 duration: ( text ) => ( text.length * 0.05 + 0.8 ) * 1000,
148 render: function FadeInRender( { text, style, Tag, animKey } ) {
149 const CHAR_DURATION = 0.8;
150 const CHAR_DELAY = 0.05;
151 return (
152 <Tag style={ style } key={ animKey }>
153 { text.split( '' ).map( ( char, i ) => (
154 <span key={ i } style={ {
155 display: 'inline-block',
156 whiteSpace: 'pre',
157 opacity: 0,
158 animation: `frblFadeIn ${ CHAR_DURATION }s forwards`,
159 animationDelay: `${ i * CHAR_DELAY }s`,
160 } }>{ char }</span>
161 ) ) }
162 </Tag>
163 );
164 },
165 },
166 'rotate-in': {
167 duration: ( text ) => ( text.length * 0.05 + 0.5 ) * 1000,
168 render: function RotateInRender( { text, style, Tag, animKey } ) {
169 const CHAR_DURATION = 0.5;
170 const CHAR_DELAY = 0.05;
171 return (
172 <Tag style={ style } key={ animKey }>
173 { text.split( '' ).map( ( char, i ) => (
174 <span key={ i } style={ {
175 display: 'inline-block',
176 whiteSpace: 'pre',
177 opacity: 0,
178 animation: `frblRotateIn ${ CHAR_DURATION }s forwards`,
179 animationDelay: `${ i * CHAR_DELAY }s`,
180 } }>{ char }</span>
181 ) ) }
182 </Tag>
183 );
184 },
185 },
186 'flip-in': {
187 duration: ( text ) => ( text.length * 0.05 + 0.6 ) * 1000,
188 render: function FlipInRender( { text, style, Tag, animKey } ) {
189 const CHAR_DURATION = 0.6;
190 const CHAR_DELAY = 0.05;
191 return (
192 <Tag style={ style } key={ animKey }>
193 { text.split( '' ).map( ( char, i ) => (
194 <span key={ i } style={ {
195 display: 'inline-block',
196 whiteSpace: 'pre',
197 opacity: 0,
198 animation: `frblFlipIn ${ CHAR_DURATION }s forwards`,
199 animationDelay: `${ i * CHAR_DELAY }s`,
200 } }>{ char }</span>
201 ) ) }
202 </Tag>
203 );
204 },
205 },
206 'bounce-in': {
207 duration: ( text ) => ( text.length * 0.08 + 0.6 ) * 1000,
208 render: function BounceInRender( { text, style, Tag, animKey } ) {
209 const CHAR_DURATION = 0.6;
210 const CHAR_DELAY = 0.08;
211 return (
212 <Tag style={ style } key={ animKey }>
213 { text.split( '' ).map( ( char, i ) => (
214 <span key={ i } style={ {
215 display: 'inline-block',
216 whiteSpace: 'pre',
217 opacity: 0,
218 animation: `frblBounceIn ${ CHAR_DURATION }s forwards`,
219 animationDelay: `${ i * CHAR_DELAY }s`,
220 } }>{ char }</span>
221 ) ) }
222 </Tag>
223 );
224 },
225 },
226 'glow-in': {
227 duration: ( text ) => ( text.length * 0.05 + 1 ) * 1000,
228 render: function GlowInRender( { text, style, Tag, animKey } ) {
229 const CHAR_DURATION = 1;
230 const CHAR_DELAY = 0.05;
231 return (
232 <Tag style={ style } key={ animKey }>
233 { text.split( '' ).map( ( char, i ) => (
234 <span key={ i } style={ {
235 display: 'inline-block',
236 whiteSpace: 'pre',
237 opacity: 0,
238 animation: `frblGlowIn ${ CHAR_DURATION }s forwards`,
239 animationDelay: `${ i * CHAR_DELAY }s`,
240 } }>{ char }</span>
241 ) ) }
242 </Tag>
243 );
244 },
245 },
246 'blur-in': {
247 duration: ( text ) => ( text.length * 0.05 + 0.8 ) * 1000,
248 render: function BlurInRender( { text, style, Tag, animKey } ) {
249 const CHAR_DURATION = 0.8;
250 const CHAR_DELAY = 0.05;
251 return (
252 <Tag style={ style } key={ animKey }>
253 { text.split( '' ).map( ( char, i ) => (
254 <span key={ i } style={ {
255 display: 'inline-block',
256 whiteSpace: 'pre',
257 opacity: 0,
258 animation: `frblBlurIn ${ CHAR_DURATION }s forwards`,
259 animationDelay: `${ i * CHAR_DELAY }s`,
260 } }>{ char }</span>
261 ) ) }
262 </Tag>
263 );
264 },
265 },
266 'scale-in': {
267 duration: ( text ) => ( text.length * 0.05 + 0.5 ) * 1000,
268 render: function ScaleInRender( { text, style, Tag, animKey } ) {
269 const CHAR_DURATION = 0.5;
270 const CHAR_DELAY = 0.05;
271 return (
272 <Tag style={ style } key={ animKey }>
273 { text.split( '' ).map( ( char, i ) => (
274 <span key={ i } style={ {
275 display: 'inline-block',
276 whiteSpace: 'pre',
277 opacity: 0,
278 animation: `frblScaleIn ${ CHAR_DURATION }s forwards`,
279 animationDelay: `${ i * CHAR_DELAY }s`,
280 } }>{ char }</span>
281 ) ) }
282 </Tag>
283 );
284 },
285 },
286 'slide-up': {
287 duration: ( text ) => ( text.length * 0.05 + 0.5 ) * 1000,
288 render: function SlideUpRender( { text, style, Tag, animKey } ) {
289 const CHAR_DURATION = 0.5;
290 const CHAR_DELAY = 0.05;
291 return (
292 <Tag style={ { ...style, overflow: 'hidden' } } key={ animKey }>
293 { text.split( '' ).map( ( char, i ) => (
294 <span key={ i } style={ {
295 display: 'inline-block',
296 whiteSpace: 'pre',
297 opacity: 0,
298 animation: `frblSlideUp ${ CHAR_DURATION }s forwards`,
299 animationDelay: `${ i * CHAR_DELAY }s`,
300 } }>{ char }</span>
301 ) ) }
302 </Tag>
303 );
304 },
305 },
306 'shadow-pop': {
307 duration: ( text ) => ( text.length * 0.05 + 0.5 ) * 1000,
308 render: function ShadowPopRender( { text, style, Tag, animKey } ) {
309 const CHAR_DURATION = 0.5;
310 const CHAR_DELAY = 0.05;
311 return (
312 <Tag style={ style } key={ animKey }>
313 { text.split( '' ).map( ( char, i ) => (
314 <span key={ i } style={ {
315 display: 'inline-block',
316 whiteSpace: 'pre',
317 opacity: 0,
318 animation: `frblShadowPop ${ CHAR_DURATION }s forwards`,
319 animationDelay: `${ i * CHAR_DELAY }s`,
320 } }>{ char }</span>
321 ) ) }
322 </Tag>
323 );
324 },
325 },
326 'pulse-neon': {
327 duration: ( text ) => ( text.length * 0.05 + 1.5 ) * 1000,
328 render: function PulseNeonRender( { text, style, Tag, animKey } ) {
329 const CHAR_DURATION = 1.5;
330 const CHAR_DELAY = 0.05;
331 return (
332 <Tag style={ style } key={ animKey }>
333 { text.split( '' ).map( ( char, i ) => (
334 <span key={ i } style={ {
335 display: 'inline-block',
336 whiteSpace: 'pre',
337 opacity: 0,
338 animation: `frblPulseNeon ${ CHAR_DURATION }s forwards`,
339 animationDelay: `${ i * CHAR_DELAY }s`,
340 } }>{ char }</span>
341 ) ) }
342 </Tag>
343 );
344 },
345 },
346 'water-drop': {
347 duration: ( text ) => ( text.length * 0.05 + 0.6 ) * 1000,
348 render: function WaterDropRender( { text, style, Tag, animKey } ) {
349 const CHAR_DURATION = 0.6;
350 const CHAR_DELAY = 0.05;
351 return (
352 <Tag style={ style } key={ animKey }>
353 { text.split( '' ).map( ( char, i ) => (
354 <span key={ i } style={ {
355 display: 'inline-block',
356 whiteSpace: 'pre',
357 opacity: 0,
358 animation: `frblWaterDrop ${ CHAR_DURATION }s forwards`,
359 animationDelay: `${ i * CHAR_DELAY }s`,
360 } }>{ char }</span>
361 ) ) }
362 </Tag>
363 );
364 },
365 },
366 'glitch-rgb': {
367 duration: ( text ) => ( text.length * 0.1 + 0.4 ) * 1000,
368 render: function GlitchRGBRender( { text, style, Tag, animKey } ) {
369 const CHAR_DURATION = 0.4;
370 const CHAR_DELAY = 0.1;
371 return (
372 <Tag style={ style } key={ animKey }>
373 { text.split( '' ).map( ( char, i ) => (
374 <span key={ i } style={ {
375 display: 'inline-block',
376 whiteSpace: 'pre',
377 opacity: 0,
378 animation: `frblGlitchRGB ${ CHAR_DURATION }s forwards`,
379 animationDelay: `${ i * CHAR_DELAY }s`,
380 } }>{ char }</span>
381 ) ) }
382 </Tag>
383 );
384 },
385 },
386 'solid-outline': {
387 duration: ( text ) => ( text.length * 0.05 + 0.8 ) * 1000,
388 render: function SolidOutlineRender( { text, style, Tag, animKey } ) {
389 const CHAR_DURATION = 0.8;
390 const CHAR_DELAY = 0.05;
391 const strokeColor = style.color || 'currentColor';
392 return (
393 <Tag style={ { ...style, '--frbl-stroke-color': strokeColor } } key={ animKey }>
394 { text.split( '' ).map( ( char, i ) => (
395 <span key={ i } style={ {
396 display: 'inline-block',
397 whiteSpace: 'pre',
398 opacity: 0,
399 animation: `frblSolidOutline ${ CHAR_DURATION }s forwards`,
400 animationDelay: `${ i * CHAR_DELAY }s`,
401 } }>{ char }</span>
402 ) ) }
403 </Tag>
404 );
405 },
406 },
407 'terminal-type': {
408 duration: ( text ) => text.length * 100,
409 render: function TerminalTypeRender( { text, style, Tag, animKey } ) {
410 const { useEffect, useRef } = wp.element;
411 const containerRef = useRef( null );
412
413 useEffect( () => {
414 const el = containerRef.current;
415 if ( ! el ) return;
416 el.innerHTML = '';
417
418 const textSpan = document.createElement( 'span' );
419 const cursor = document.createElement( 'span' );
420 cursor.style.cssText = 'display:inline-block;width:0.6em;height:1.1em;background:currentColor;margin-left:4px;vertical-align:middle;animation:frblBlinkCursor 1s infinite;';
421
422 el.appendChild( textSpan );
423 el.appendChild( cursor );
424
425 const chars = text.split( '' );
426 let i = 0;
427 let timer;
428
429 function type() {
430 if ( i < chars.length ) {
431 textSpan.textContent += chars[ i ];
432 i++;
433 timer = setTimeout( type, 100 );
434 }
435 }
436
437 type();
438 return () => clearTimeout( timer );
439 }, [ animKey, text ] );
440
441 return <Tag ref={ containerRef } style={ style } key={ animKey } />;
442 },
443 },
444 'tracking-expand': {
445 duration: () => 1200,
446 render: function TrackingExpandRender( { text, style, Tag, animKey } ) {
447 return (
448 <Tag style={ style } key={ animKey }>
449 <span style={ {
450 display: 'inline-block',
451 opacity: 0,
452 animation: `frblTrackingExpand 1.2s forwards`,
453 } }>{ text }</span>
454 </Tag>
455 );
456 },
457 },
458 'block-reveal': {
459 duration: () => 1200,
460 render: function BlockRevealRender( { text, style, Tag, animKey } ) {
461 const { useEffect, useRef } = wp.element;
462 const containerRef = useRef( null );
463
464 useEffect( () => {
465 const el = containerRef.current;
466 if ( ! el ) return;
467
468 el.style.position = 'relative';
469 el.style.overflow = 'hidden';
470 el.innerHTML = '';
471
472 const textSpan = document.createElement( 'span' );
473 textSpan.textContent = text;
474 textSpan.style.cssText = 'display:inline-block;opacity:0;';
475
476 const block = document.createElement( 'span' );
477 block.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;background:currentColor;transform-origin:left;transform:scaleX(0);transition:transform 0.5s cubic-bezier(0.86,0,0.07,1);';
478
479 el.appendChild( textSpan );
480 el.appendChild( block );
481
482 const t1 = setTimeout( () => {
483 block.style.transform = 'scaleX(1)';
484 }, 50 );
485
486 const t2 = setTimeout( () => {
487 textSpan.style.opacity = '1';
488 block.style.transformOrigin = 'right';
489 block.style.transform = 'scaleX(0)';
490 }, 550 );
491
492 return () => { clearTimeout( t1 ); clearTimeout( t2 ); };
493 }, [ animKey, text ] );
494
495 return <Tag ref={ containerRef } style={ style } key={ animKey } />;
496 },
497 },
498 'flicker': {
499 duration: ( text ) => ( text.length * 0.05 + 1.2 ) * 1000,
500 render: function FlickerRender( { text, style, Tag, animKey } ) {
501 const CHAR_DURATION = 1.2;
502 const CHAR_DELAY = 0.05;
503 return (
504 <Tag style={ style } key={ animKey }>
505 { text.split( '' ).map( ( char, i ) => (
506 <span key={ i } style={ {
507 display: 'inline-block',
508 whiteSpace: 'pre',
509 opacity: 0,
510 animation: `frblFlicker ${ CHAR_DURATION }s forwards`,
511 animationDelay: `${ i * CHAR_DELAY }s`,
512 } }>{ char }</span>
513 ) ) }
514 </Tag>
515 );
516 },
517 },
518 'random-reveal': {
519 duration: ( text ) => text.replace( / /g, '' ).length * 50,
520 render: function RandomRevealRender( { text, style, Tag, animKey } ) {
521 const { useEffect, useRef } = wp.element;
522 const containerRef = useRef( null );
523
524 useEffect( () => {
525 const el = containerRef.current;
526 if ( ! el ) return;
527 el.innerHTML = '';
528
529 const chars = text.split( '' );
530 const indices = chars.map( ( _, i ) => i ).sort( () => Math.random() - 0.5 );
531
532 const spanList = chars.map( ( char ) => {
533 const span = document.createElement( 'span' );
534 span.className = 'frbl-char';
535 span.textContent = char;
536 span.style.cssText = 'display:inline-block;white-space:pre;opacity:0;';
537 el.appendChild( span );
538 return span;
539 } );
540
541 let i = 0;
542 let timer;
543
544 function reveal() {
545 if ( i < indices.length ) {
546 spanList[ indices[ i ] ].style.opacity = '1';
547 i++;
548 timer = setTimeout( reveal, 50 );
549 }
550 }
551
552 reveal();
553 return () => clearTimeout( timer );
554 }, [ animKey, text ] );
555
556 return <Tag ref={ containerRef } style={ style } key={ animKey } />;
557 },
558 },
559 'glitch': {
560 duration: ( text ) => ( text.length * 0.1 + 0.4 ) * 1000,
561 render: function GlitchRender( { text, style, Tag, animKey } ) {
562 const CHAR_DURATION = 0.4;
563 const CHAR_DELAY = 0.1;
564 return (
565 <Tag style={ style } key={ animKey }>
566 { text.split( '' ).map( ( char, i ) => (
567 <span key={ i } style={ {
568 display: 'inline-block',
569 whiteSpace: 'pre',
570 opacity: 0,
571 animation: `frblGlitch ${ CHAR_DURATION }s forwards`,
572 animationDelay: `${ i * CHAR_DELAY }s`,
573 } }>{ char }</span>
574 ) ) }
575 </Tag>
576 );
577 },
578 },
579 'roll-in': {
580 duration: ( text ) => ( text.length * 0.05 + 0.6 ) * 1000,
581 render: function RollInRender( { text, style, Tag, animKey } ) {
582 const CHAR_DURATION = 0.6;
583 const CHAR_DELAY = 0.05;
584 return (
585 <Tag style={ { ...style, overflow: 'hidden' } } key={ animKey }>
586 { text.split( '' ).map( ( char, i ) => (
587 <span key={ i } style={ {
588 display: 'inline-block',
589 whiteSpace: 'pre',
590 opacity: 0,
591 animation: `frblRollIn ${ CHAR_DURATION }s forwards`,
592 animationDelay: `${ i * CHAR_DELAY }s`,
593 } }>{ char }</span>
594 ) ) }
595 </Tag>
596 );
597 },
598 },
599 'squeeze': {
600 duration: ( text ) => ( text.length * 0.05 + 0.5 ) * 1000,
601 render: function SqueezeRender( { text, style, Tag, animKey } ) {
602 const CHAR_DURATION = 0.5;
603 const CHAR_DELAY = 0.05;
604 return (
605 <Tag style={ style } key={ animKey }>
606 { text.split( '' ).map( ( char, i ) => (
607 <span key={ i } style={ {
608 display: 'inline-block',
609 whiteSpace: 'pre',
610 opacity: 0,
611 animation: `frblSqueeze ${ CHAR_DURATION }s forwards`,
612 animationDelay: `${ i * CHAR_DELAY }s`,
613 } }>{ char }</span>
614 ) ) }
615 </Tag>
616 );
617 },
618 },
619 'stretch': {
620 duration: ( text ) => ( text.length * 0.05 + 0.5 ) * 1000,
621 render: function StretchRender( { text, style, Tag, animKey } ) {
622 const CHAR_DURATION = 0.5;
623 const CHAR_DELAY = 0.05;
624 return (
625 <Tag style={ style } key={ animKey }>
626 { text.split( '' ).map( ( char, i ) => (
627 <span key={ i } style={ {
628 display: 'inline-block',
629 whiteSpace: 'pre',
630 opacity: 0,
631 animation: `frblStretch ${ CHAR_DURATION }s forwards`,
632 animationDelay: `${ i * CHAR_DELAY }s`,
633 } }>{ char }</span>
634 ) ) }
635 </Tag>
636 );
637 },
638 },
639 'wave': {
640 duration: ( text ) => ( text.length * 0.08 + 0.6 ) * 1000,
641 render: function WaveRender( { text, style, Tag, animKey } ) {
642 const CHAR_DURATION = 0.6;
643 const CHAR_DELAY = 0.08;
644 return (
645 <Tag style={ style } key={ animKey }>
646 { text.split( '' ).map( ( char, i ) => (
647 <span key={ i } style={ {
648 display: 'inline-block',
649 whiteSpace: 'pre',
650 opacity: 0,
651 animation: `frblWave ${ CHAR_DURATION }s forwards`,
652 animationDelay: `${ i * CHAR_DELAY }s`,
653 } }>{ char }</span>
654 ) ) }
655 </Tag>
656 );
657 },
658 },
659 'rubber-band': {
660 duration: ( text ) => ( text.length * 0.05 + 0.8 ) * 1000,
661 render: function RubberBandRender( { text, style, Tag, animKey } ) {
662 const CHAR_DURATION = 0.8;
663 const CHAR_DELAY = 0.05;
664 return (
665 <Tag style={ style } key={ animKey }>
666 { text.split( '' ).map( ( char, i ) => (
667 <span key={ i } style={ {
668 display: 'inline-block',
669 whiteSpace: 'pre',
670 opacity: 0,
671 animation: `frblRubberBand ${ CHAR_DURATION }s forwards`,
672 animationDelay: `${ i * CHAR_DELAY }s`,
673 } }>{ char }</span>
674 ) ) }
675 </Tag>
676 );
677 },
678 },
679 'flash': {
680 duration: ( text ) => ( text.length * 0.05 + 1 ) * 1000,
681 render: function FlashRender( { text, style, Tag, animKey } ) {
682 const CHAR_DURATION = 1;
683 const CHAR_DELAY = 0.05;
684 return (
685 <Tag style={ style } key={ animKey }>
686 { text.split( '' ).map( ( char, i ) => (
687 <span key={ i } style={ {
688 display: 'inline-block',
689 whiteSpace: 'pre',
690 opacity: 0,
691 animation: `frblFlash ${ CHAR_DURATION }s forwards`,
692 animationDelay: `${ i * CHAR_DELAY }s`,
693 } }>{ char }</span>
694 ) ) }
695 </Tag>
696 );
697 },
698 },
699 'pulse': {
700 duration: ( text ) => ( text.length * 0.05 + 0.5 ) * 1000,
701 render: function PulseRender( { text, style, Tag, animKey } ) {
702 const CHAR_DURATION = 0.5;
703 const CHAR_DELAY = 0.05;
704 return (
705 <Tag style={ style } key={ animKey }>
706 { text.split( '' ).map( ( char, i ) => (
707 <span key={ i } style={ {
708 display: 'inline-block',
709 whiteSpace: 'pre',
710 opacity: 0,
711 animation: `frblPulse ${ CHAR_DURATION }s forwards`,
712 animationDelay: `${ i * CHAR_DELAY }s`,
713 } }>{ char }</span>
714 ) ) }
715 </Tag>
716 );
717 },
718 },
719 'swing': {
720 duration: ( text ) => ( text.length * 0.05 + 0.8 ) * 1000,
721 render: function SwingRender( { text, style, Tag, animKey } ) {
722 const CHAR_DURATION = 0.8;
723 const CHAR_DELAY = 0.05;
724 return (
725 <Tag style={ style } key={ animKey }>
726 { text.split( '' ).map( ( char, i ) => (
727 <span key={ i } style={ {
728 display: 'inline-block',
729 whiteSpace: 'pre',
730 opacity: 0,
731 transformOrigin: 'top center',
732 animation: `frblSwing ${ CHAR_DURATION }s forwards`,
733 animationDelay: `${ i * CHAR_DELAY }s`,
734 } }>{ char }</span>
735 ) ) }
736 </Tag>
737 );
738 },
739 },
740 'drop-in': {
741 duration: ( text ) => ( text.length * 0.06 + 0.6 ) * 1000,
742 render: function DropInRender( { text, style, Tag, animKey } ) {
743 const CHAR_DURATION = 0.6;
744 const CHAR_DELAY = 0.06;
745 return (
746 <Tag style={ { ...style, overflow: 'hidden' } } key={ animKey }>
747 { text.split( '' ).map( ( char, i ) => (
748 <span key={ i } style={ {
749 display: 'inline-block',
750 whiteSpace: 'pre',
751 opacity: 0,
752 animation: `frblDropIn ${ CHAR_DURATION }s forwards`,
753 animationDelay: `${ i * CHAR_DELAY }s`,
754 } }>{ char }</span>
755 ) ) }
756 </Tag>
757 );
758 },
759 },
760 'slide-right': {
761 duration: ( text ) => ( text.length * 0.05 + 0.5 ) * 1000,
762 render: function SlideRightRender( { text, style, Tag, animKey } ) {
763 const CHAR_DURATION = 0.5;
764 const CHAR_DELAY = 0.05;
765 return (
766 <Tag style={ { ...style, overflow: 'hidden' } } key={ animKey }>
767 { text.split( '' ).map( ( char, i ) => (
768 <span key={ i } style={ {
769 display: 'inline-block',
770 whiteSpace: 'pre',
771 opacity: 0,
772 animation: `frblSlideRight ${ CHAR_DURATION }s forwards`,
773 animationDelay: `${ i * CHAR_DELAY }s`,
774 } }>{ char }</span>
775 ) ) }
776 </Tag>
777 );
778 },
779 },
780 'slide-left': {
781 duration: ( text ) => ( text.length * 0.05 + 0.5 ) * 1000,
782 render: function SlideLeftRender( { text, style, Tag, animKey } ) {
783 const CHAR_DURATION = 0.5;
784 const CHAR_DELAY = 0.05;
785 return (
786 <Tag style={ { ...style, overflow: 'hidden' } } key={ animKey }>
787 { text.split( '' ).map( ( char, i ) => (
788 <span key={ i } style={ {
789 display: 'inline-block',
790 whiteSpace: 'pre',
791 opacity: 0,
792 animation: `frblSlideLeft ${ CHAR_DURATION }s forwards`,
793 animationDelay: `${ i * CHAR_DELAY }s`,
794 } }>{ char }</span>
795 ) ) }
796 </Tag>
797 );
798 },
799 },
800 'slide-down': {
801 duration: ( text ) => ( text.length * 0.05 + 0.5 ) * 1000,
802 render: function SlideDownRender( { text, style, Tag, animKey } ) {
803 const CHAR_DURATION = 0.5;
804 const CHAR_DELAY = 0.05;
805 return (
806 <Tag style={ { ...style, overflow: 'hidden' } } key={ animKey }>
807 { text.split( '' ).map( ( char, i ) => (
808 <span key={ i } style={ {
809 display: 'inline-block',
810 whiteSpace: 'pre',
811 opacity: 0,
812 animation: `frblSlideDown ${ CHAR_DURATION }s forwards`,
813 animationDelay: `${ i * CHAR_DELAY }s`,
814 } }>{ char }</span>
815 ) ) }
816 </Tag>
817 );
818 },
819 },
820 'shuffle-text': {
821 duration: ( text ) => ( text.replace( / /g, '' ).length * 2 + 15 ) * 30,
822 render: function ShuffleTextRender( { text, style, Tag, animKey } ) {
823 const { useEffect, useRef } = wp.element;
824 const SYMBOLS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()';
825 const containerRef = useRef( null );
826
827 useEffect( () => {
828 const el = containerRef.current;
829 if ( ! el ) return;
830 el.innerHTML = '';
831
832 const chars = text.split( '' );
833 const spanList = chars.map( ( char ) => {
834 const span = document.createElement( 'span' );
835 span.className = 'frbl-char';
836 el.appendChild( span );
837 return { el: span, char, isSpace: char === ' ' };
838 } );
839
840 let frame = 0;
841 let rafId;
842 let timerId;
843
844 function update() {
845 let allDone = true;
846 spanList.forEach( ( item, i ) => {
847 if ( item.isSpace ) { item.el.textContent = ' '; return; }
848 const startFrame = i * 2;
849 const endFrame = startFrame + 15;
850 if ( frame < startFrame ) {
851 allDone = false; item.el.textContent = '';
852 } else if ( frame < endFrame ) {
853 allDone = false;
854 item.el.textContent = SYMBOLS[ Math.floor( Math.random() * SYMBOLS.length ) ];
855 } else {
856 item.el.textContent = item.char;
857 }
858 } );
859 frame++;
860 if ( ! allDone ) {
861 timerId = setTimeout( () => { rafId = requestAnimationFrame( update ); }, 30 );
862 }
863 }
864
865 rafId = requestAnimationFrame( update );
866 return () => { cancelAnimationFrame( rafId ); clearTimeout( timerId ); };
867 }, [ animKey, text ] );
868
869 return <Tag ref={ containerRef } style={ style } key={ animKey } />;
870 },
871 },
872 'typewriter': {
873 duration: ( text ) => text.length * 80,
874 render: function TypewriterRender( { text, style, Tag, animKey } ) {
875 const { useEffect, useRef } = wp.element;
876 const containerRef = useRef( null );
877
878 useEffect( () => {
879 const el = containerRef.current;
880 if ( ! el ) return;
881 el.textContent = '';
882 const chars = text.split( '' );
883 let i = 0;
884 let timer;
885 function type() {
886 if ( i < chars.length ) {
887 el.textContent += chars[ i ];
888 i++;
889 timer = setTimeout( type, 80 );
890 }
891 }
892 type();
893 return () => clearTimeout( timer );
894 }, [ animKey, text ] );
895
896 return <Tag ref={ containerRef } style={ style } key={ animKey } />;
897 },
898 },
899 };
900
901 function AnimationPreview( { animationType, text, style, Tag } ) {
902 const [ animKey, setAnimKey ] = useState( 0 );
903 const entry = ANIMATION_PREVIEWS[ animationType ];
904
905 useState( () => {
906 if ( ! entry ) return;
907 const ms = entry.duration( text );
908 const t = setTimeout( () => setAnimKey( ( k ) => k + 1 ), ms );
909 return () => clearTimeout( t );
910 } );
911
912 if ( ! entry ) return null;
913 return entry.render( { text, style, Tag, animKey } );
914 }
915
916 function TextAnimationEdit( props ) {
917 const { attributes, setAttributes } = props;
918 const [ isHovered, setIsHovered ] = useState( false );
919 const [ isEditMode, setIsEditMode ] = useState( true );
920 const {
921 content,
922 htmlTag,
923 animationType,
924 fontSize,
925 fontSizeUnit,
926 fontFamily,
927 fontWeight,
928 fontStyle,
929 lineHeight,
930 letterSpacing,
931 textAlign,
932 textTransform,
933 textColorCustom,
934 textColorHover,
935 width,
936 widthUnit,
937 height,
938 heightUnit,
939 } = attributes;
940
941 const fontFamilyOptions = useSelect( ( select ) => {
942 try {
943 const settings = select( 'core' ).getSettings();
944 const families = settings.fontFamilies || {};
945 const all = [
946 ...( families.theme || [] ),
947 ...( families.custom || [] ),
948 ...( families.default || [] ),
949 ];
950 const options = [];
951 all.forEach( ( f ) => {
952 if ( f && f.name && f.fontFamily ) {
953 options.push( { label: f.name, value: f.fontFamily } );
954 }
955 } );
956 return options;
957 } catch ( e ) {
958 return [];
959 }
960 }, [] );
961
962 const blockProps = useBlockProps( {
963 className: 'frbl-text-animation',
964 style: {
965 fontSize: fontSize ? `${ fontSize }${ fontSizeUnit || 'px' }` : undefined,
966 fontFamily: fontFamily || undefined,
967 fontWeight: fontWeight || undefined,
968 fontStyle: fontStyle !== 'normal' ? fontStyle : undefined,
969 lineHeight: lineHeight || undefined,
970 letterSpacing: letterSpacing ? `${ letterSpacing }em` : undefined,
971 textAlign: textAlign !== 'left' ? textAlign : undefined,
972 textTransform: textTransform !== 'none' ? textTransform : undefined,
973 '--frbl-color': textColorCustom || undefined,
974 '--frbl-color-hover': textColorHover || undefined,
975 color: ( isHovered && textColorHover ) ? textColorHover : ( textColorCustom || undefined ),
976 transition: 'color 0.3s ease',
977 width: width ? `${ width }${ widthUnit || 'px' }` : undefined,
978 height: height ? `${ height }${ heightUnit || 'px' }` : undefined,
979 marginLeft: 0,
980 marginRight: 'auto',
981 },
982 onMouseEnter: () => setIsHovered( true ),
983 onMouseLeave: () => setIsHovered( false ),
984 } );
985
986 const hasAnimation = animationType && animationType !== 'none';
987 const showPreview = hasAnimation && ! isEditMode;
988
989 const previewStyle = {
990 fontSize: blockProps.style.fontSize,
991 fontFamily: blockProps.style.fontFamily,
992 fontWeight: blockProps.style.fontWeight,
993 fontStyle: blockProps.style.fontStyle,
994 lineHeight: blockProps.style.lineHeight,
995 letterSpacing: blockProps.style.letterSpacing,
996 textAlign: blockProps.style.textAlign,
997 textTransform: blockProps.style.textTransform,
998 color: blockProps.style.color || blockProps.style['--frbl-color'] || undefined,
999 width: blockProps.style.width,
1000 height: blockProps.style.height,
1001 marginLeft: 0,
1002 marginRight: 'auto',
1003 };
1004
1005 return (
1006 <Fragment>
1007 <InspectorControls>
1008
1009 <PanelBody title={ __( 'FrontBlocks Animation Text', 'frontblocks' ) } initialOpen={ true }>
1010 <SelectControl
1011 label={ __( 'Animation Type', 'frontblocks' ) }
1012 value={ animationType }
1013 options={ ANIMATION_OPTIONS }
1014 onChange={ ( value ) => {
1015 setAttributes( { animationType: value } );
1016 if ( value && value !== 'none' ) {
1017 const entry = ANIMATION_PREVIEWS[ value ];
1018 const text = stripHtml( content ) || __( 'Write your text here…', 'frontblocks' );
1019 const ms = entry ? entry.duration( text ) : 2000;
1020 setIsEditMode( false );
1021 setTimeout( () => setIsEditMode( true ), ms );
1022 } else {
1023 setIsEditMode( true );
1024 }
1025 } }
1026 />
1027 { hasAnimation && (
1028 <Button
1029 variant="secondary"
1030 size="small"
1031 onClick={ () => {
1032 const entry = ANIMATION_PREVIEWS[ animationType ];
1033 const text = stripHtml( content ) || __( 'Write your text here…', 'frontblocks' );
1034 const ms = entry ? entry.duration( text ) : 2000;
1035 setIsEditMode( false );
1036 setTimeout( () => setIsEditMode( true ), ms );
1037 } }
1038 style={ { marginTop: '8px', width: '100%', justifyContent: 'center' } }
1039 >
1040 { __( '▶ Preview animation', 'frontblocks' ) }
1041 </Button>
1042 ) }
1043 </PanelBody>
1044
1045 <PanelBody title={ __( 'Typography', 'frontblocks' ) } initialOpen={ false }>
1046
1047 <SelectControl
1048 label={ __( 'HTML Tag', 'frontblocks' ) }
1049 value={ htmlTag }
1050 options={ TAG_OPTIONS }
1051 onChange={ ( value ) => setAttributes( { htmlTag: value } ) }
1052 />
1053
1054 { fontFamilyOptions.length > 0 && (
1055 <SelectControl
1056 label={ __( 'Font Family', 'frontblocks' ) }
1057 value={ fontFamily }
1058 options={ [ { label: __( 'Default', 'frontblocks' ), value: '' }, ...fontFamilyOptions ] }
1059 onChange={ ( value ) => setAttributes( { fontFamily: value } ) }
1060 />
1061 ) }
1062
1063 <div style={ { marginBottom: '16px' } }>
1064 <p style={ { marginTop: 0, marginBottom: '8px', fontSize: '11px', fontWeight: '500', textTransform: 'uppercase', color: 'rgb(117, 117, 117)' } }>
1065 { __( 'Font Size', 'frontblocks' ) }
1066 </p>
1067 <div style={ { display: 'flex', gap: '8px' } }>
1068 <div style={ { flex: 1, marginBottom: 0 } }>
1069 <NumberControl
1070 value={ fontSize || '' }
1071 onChange={ ( value ) => setAttributes( { fontSize: value ? parseFloat( value ) : undefined } ) }
1072 min={ 1 }
1073 step={ 1 }
1074 spinControls="native"
1075 hideLabelFromVision
1076 label={ __( 'Font Size', 'frontblocks' ) }
1077 />
1078 </div>
1079 <div style={ { width: '80px', flexShrink: 0, marginBottom: 0 } }>
1080 <SelectControl
1081 value={ fontSizeUnit }
1082 options={ FONT_SIZE_UNITS }
1083 onChange={ ( value ) => setAttributes( { fontSizeUnit: value } ) }
1084 hideLabelFromVision
1085 label={ __( 'Unit', 'frontblocks' ) }
1086 __nextHasNoMarginBottom
1087 />
1088 </div>
1089 </div>
1090 </div>
1091
1092 <SelectControl
1093 label={ __( 'Font Weight', 'frontblocks' ) }
1094 value={ fontWeight }
1095 options={ [ { label: __( 'Default', 'frontblocks' ), value: '' }, ...FONT_WEIGHT_OPTIONS ] }
1096 onChange={ ( value ) => setAttributes( { fontWeight: value } ) }
1097 />
1098
1099 <SelectControl
1100 label={ __( 'Font Style', 'frontblocks' ) }
1101 value={ fontStyle }
1102 options={ FONT_STYLE_OPTIONS }
1103 onChange={ ( value ) => setAttributes( { fontStyle: value } ) }
1104 />
1105
1106 <ToggleGroupControl
1107 label={ __( 'Text Align', 'frontblocks' ) }
1108 value={ textAlign }
1109 onChange={ ( value ) => {
1110 const newAlign = value || 'left';
1111 const updates = { textAlign: newAlign };
1112 if ( newAlign !== 'left' ) {
1113 updates.width = 100;
1114 updates.widthUnit = '%';
1115 }
1116 setAttributes( updates );
1117 } }
1118 isBlock
1119 >
1120 <ToggleGroupControlOptionIcon value="left" icon={ ALIGN_LEFT_ICON } label={ __( 'Left', 'frontblocks' ) } />
1121 <ToggleGroupControlOptionIcon value="center" icon={ ALIGN_CENTER_ICON } label={ __( 'Center', 'frontblocks' ) } />
1122 <ToggleGroupControlOptionIcon value="right" icon={ ALIGN_RIGHT_ICON } label={ __( 'Right', 'frontblocks' ) } />
1123 <ToggleGroupControlOptionIcon value="justify" icon={ ALIGN_JUSTIFY_ICON } label={ __( 'Justify', 'frontblocks' ) } />
1124 </ToggleGroupControl>
1125
1126 <ToggleGroupControl
1127 label={ __( 'Text Transform', 'frontblocks' ) }
1128 value={ textTransform }
1129 onChange={ ( value ) => setAttributes( { textTransform: value || 'none' } ) }
1130 isBlock
1131 >
1132 <ToggleGroupControlOptionIcon value="none" icon={ TRANSFORM_NONE_ICON } label={ __( 'None', 'frontblocks' ) } />
1133 <ToggleGroupControlOptionIcon value="uppercase" icon={ TRANSFORM_UPPERCASE_ICON } label={ __( 'Uppercase', 'frontblocks' ) } />
1134 <ToggleGroupControlOptionIcon value="lowercase" icon={ TRANSFORM_LOWERCASE_ICON } label={ __( 'Lowercase', 'frontblocks' ) } />
1135 <ToggleGroupControlOptionIcon value="capitalize" icon={ TRANSFORM_CAPITALIZE_ICON } label={ __( 'Capitalize', 'frontblocks' ) } />
1136 </ToggleGroupControl>
1137
1138 <RangeControl
1139 label={ __( 'Line Height', 'frontblocks' ) }
1140 value={ lineHeight }
1141 onChange={ ( value ) => setAttributes( { lineHeight: value } ) }
1142 min={ 0.5 }
1143 max={ 4 }
1144 step={ 0.05 }
1145 initialPosition={ 1.5 }
1146 allowReset
1147 resetFallbackValue={ undefined }
1148 />
1149
1150 <RangeControl
1151 label={ __( 'Letter Spacing (em)', 'frontblocks' ) }
1152 value={ letterSpacing }
1153 onChange={ ( value ) => setAttributes( { letterSpacing: value } ) }
1154 min={ -0.2 }
1155 max={ 1 }
1156 step={ 0.01 }
1157 initialPosition={ 0 }
1158 allowReset
1159 resetFallbackValue={ undefined }
1160 />
1161
1162 </PanelBody>
1163
1164 <PanelBody title={ __( 'Dimensions', 'frontblocks' ) } initialOpen={ false }>
1165
1166 <div style={ { marginBottom: '16px' } }>
1167 <p style={ { marginTop: 0, marginBottom: '8px', fontSize: '11px', fontWeight: '500', textTransform: 'uppercase', color: 'rgb(117, 117, 117)' } }>
1168 { __( 'Width', 'frontblocks' ) }
1169 </p>
1170 <div style={ { display: 'flex', gap: '8px' } }>
1171 <div style={ { flex: 1 } }>
1172 <NumberControl
1173 value={ width || '' }
1174 onChange={ ( value ) => setAttributes( { width: value ? parseFloat( value ) : undefined } ) }
1175 min={ 0 }
1176 step={ 1 }
1177 spinControls="native"
1178 hideLabelFromVision
1179 label={ __( 'Width', 'frontblocks' ) }
1180 placeholder="auto"
1181 />
1182 </div>
1183 <div style={ { width: '80px', flexShrink: 0 } }>
1184 <SelectControl
1185 value={ widthUnit }
1186 options={ [ { label: 'px', value: 'px' }, { label: '%', value: '%' }, { label: 'rem', value: 'rem' }, { label: 'em', value: 'em' }, { label: 'vw', value: 'vw' }, { label: 'ch', value: 'ch' } ] }
1187 onChange={ ( value ) => setAttributes( { widthUnit: value } ) }
1188 hideLabelFromVision
1189 label={ __( 'Unit', 'frontblocks' ) }
1190 __nextHasNoMarginBottom
1191 />
1192 </div>
1193 </div>
1194 </div>
1195
1196 <div style={ { marginBottom: '8px' } }>
1197 <p style={ { marginTop: 0, marginBottom: '8px', fontSize: '11px', fontWeight: '500', textTransform: 'uppercase', color: 'rgb(117, 117, 117)' } }>
1198 { __( 'Height', 'frontblocks' ) }
1199 </p>
1200 <div style={ { display: 'flex', gap: '8px' } }>
1201 <div style={ { flex: 1 } }>
1202 <NumberControl
1203 value={ height || '' }
1204 onChange={ ( value ) => setAttributes( { height: value ? parseFloat( value ) : undefined } ) }
1205 min={ 0 }
1206 step={ 1 }
1207 spinControls="native"
1208 hideLabelFromVision
1209 label={ __( 'Height', 'frontblocks' ) }
1210 placeholder="auto"
1211 />
1212 </div>
1213 <div style={ { width: '80px', flexShrink: 0 } }>
1214 <SelectControl
1215 value={ heightUnit }
1216 options={ [ { label: 'px', value: 'px' }, { label: '%', value: '%' }, { label: 'rem', value: 'rem' }, { label: 'em', value: 'em' }, { label: 'vh', value: 'vh' } ] }
1217 onChange={ ( value ) => setAttributes( { heightUnit: value } ) }
1218 hideLabelFromVision
1219 label={ __( 'Unit', 'frontblocks' ) }
1220 __nextHasNoMarginBottom
1221 />
1222 </div>
1223 </div>
1224 </div>
1225
1226 </PanelBody>
1227
1228 <PanelColorSettings
1229 title={ __( 'Color', 'frontblocks' ) }
1230 initialOpen={ false }
1231 colorSettings={ [
1232 {
1233 label: __( 'Text Color', 'frontblocks' ),
1234 value: textColorCustom,
1235 onChange: ( value ) => setAttributes( { textColorCustom: value || '' } ),
1236 },
1237 {
1238 label: __( 'Text Color Hover', 'frontblocks' ),
1239 value: textColorHover,
1240 onChange: ( value ) => setAttributes( { textColorHover: value || '' } ),
1241 },
1242 ] }
1243 />
1244
1245 </InspectorControls>
1246
1247 { showPreview ? (
1248 <AnimationPreview
1249 animationType={ animationType }
1250 text={ stripHtml( content ) || __( 'Write your text here…', 'frontblocks' ) }
1251 style={ previewStyle }
1252 Tag={ htmlTag }
1253 />
1254 ) : (
1255 <RichText
1256 { ...blockProps }
1257 tagName={ htmlTag }
1258 value={ content }
1259 onChange={ ( value ) => setAttributes( { content: value } ) }
1260 placeholder={ __( 'Write your text here…', 'frontblocks' ) }
1261 allowedFormats={ [ 'core/bold', 'core/italic', 'core/link', 'core/underline', 'core/strikethrough', 'core/code' ] }
1262 />
1263 ) }
1264 </Fragment>
1265 );
1266 }
1267
1268 registerBlockType( 'frontblocks/text-animation', {
1269 title: __( 'FrontBlocks - Text Animation', 'frontblocks' ),
1270 description: __( 'Animated text block with typography controls. Add animation effects from the sidebar.', 'frontblocks' ),
1271 category: 'text',
1272 icon: 'editor-textcolor',
1273 keywords: [
1274 __( 'text', 'frontblocks' ),
1275 __( 'animation', 'frontblocks' ),
1276 __( 'heading', 'frontblocks' ),
1277 __( 'typography', 'frontblocks' ),
1278 ],
1279 supports: {
1280 anchor: true,
1281 className: true,
1282 color: false,
1283 spacing: {
1284 margin: true,
1285 padding: true,
1286 },
1287 },
1288 attributes: {
1289 content: {
1290 type: 'string',
1291 source: 'html',
1292 selector: '.frbl-text-animation',
1293 default: '',
1294 },
1295 htmlTag: {
1296 type: 'string',
1297 default: 'h2',
1298 },
1299 animationType: {
1300 type: 'string',
1301 default: 'none',
1302 },
1303 fontSize: {
1304 type: 'number',
1305 default: undefined,
1306 },
1307 fontSizeUnit: {
1308 type: 'string',
1309 default: 'px',
1310 },
1311 fontFamily: {
1312 type: 'string',
1313 default: '',
1314 },
1315 fontWeight: {
1316 type: 'string',
1317 default: '',
1318 },
1319 fontStyle: {
1320 type: 'string',
1321 default: 'normal',
1322 },
1323 lineHeight: {
1324 type: 'number',
1325 default: undefined,
1326 },
1327 letterSpacing: {
1328 type: 'number',
1329 default: undefined,
1330 },
1331 textAlign: {
1332 type: 'string',
1333 default: 'left',
1334 },
1335 textTransform: {
1336 type: 'string',
1337 default: 'none',
1338 },
1339 textColorCustom: {
1340 type: 'string',
1341 default: '',
1342 },
1343 textColorHover: {
1344 type: 'string',
1345 default: '',
1346 },
1347 width: {
1348 type: 'number',
1349 default: undefined,
1350 },
1351 widthUnit: {
1352 type: 'string',
1353 default: 'px',
1354 },
1355 height: {
1356 type: 'number',
1357 default: undefined,
1358 },
1359 heightUnit: {
1360 type: 'string',
1361 default: 'px',
1362 },
1363 },
1364 edit: TextAnimationEdit,
1365 save: function ( { attributes } ) {
1366 const {
1367 content,
1368 htmlTag: Tag,
1369 animationType,
1370 fontSize,
1371 fontSizeUnit,
1372 fontFamily,
1373 fontWeight,
1374 fontStyle,
1375 lineHeight,
1376 letterSpacing,
1377 textAlign,
1378 textTransform,
1379 textColorCustom,
1380 textColorHover,
1381 width,
1382 widthUnit,
1383 height,
1384 heightUnit,
1385 } = attributes;
1386
1387 const style = {};
1388 if ( fontSize ) style.fontSize = `${ fontSize }${ fontSizeUnit || 'px' }`;
1389 if ( fontFamily ) style.fontFamily = fontFamily;
1390 if ( fontWeight ) style.fontWeight = fontWeight;
1391 if ( fontStyle && fontStyle !== 'normal' ) style.fontStyle = fontStyle;
1392 if ( lineHeight ) style.lineHeight = lineHeight;
1393 if ( letterSpacing ) style.letterSpacing = `${ letterSpacing }em`;
1394 if ( textAlign && textAlign !== 'left' ) style.textAlign = textAlign;
1395 if ( textTransform && textTransform !== 'none' ) style.textTransform = textTransform;
1396 if ( textColorCustom ) style['--frbl-color'] = textColorCustom;
1397 if ( textColorHover ) style['--frbl-color-hover']= textColorHover;
1398 if ( width ) style.width = `${ width }${ widthUnit || 'px' }`;
1399 if ( height ) style.height = `${ height }${ heightUnit || 'px' }`;
1400
1401 const blockProps = wp.blockEditor.useBlockProps.save( {
1402 className: 'frbl-text-animation',
1403 style,
1404 'data-animation': ( animationType && animationType !== 'none' ) ? animationType : undefined,
1405 } );
1406
1407 return (
1408 <Tag { ...blockProps }>
1409 <RichText.Content value={ content } />
1410 </Tag>
1411 );
1412 },
1413 } );
1414
1415 // ── Extend core/paragraph and core/heading with text animation panel ─────────
1416
1417 const FRBL_TA_ATTR = {
1418 frblTextAnimation: { type: 'string', default: 'none' },
1419 frblTextAnimationLoop: { type: 'boolean', default: false },
1420 };
1421
1422 // Filter for blocks registered after our script (future-proof).
1423 wp.hooks.addFilter(
1424 'blocks.registerBlockType',
1425 'frontblocks/text-animation-native-attrs',
1426 function ( settings, name ) {
1427 if ( 'core/paragraph' !== name && 'core/heading' !== name ) {
1428 return settings;
1429 }
1430 settings.attributes = Object.assign( {}, settings.attributes, FRBL_TA_ATTR );
1431 return settings;
1432 }
1433 );
1434
1435 // Patch already-registered blocks: core blocks register before plugin scripts run,
1436 // so the filter above misses them. addBlockTypes merges into the existing block type
1437 // definition so the serializer includes frblTextAnimation when saving.
1438 wp.domReady( function () {
1439 [ 'core/paragraph', 'core/heading' ].forEach( function ( blockName ) {
1440 try {
1441 var blockType = wp.data.select( 'core/blocks' ).getBlockType( blockName );
1442 if ( blockType && ! blockType.attributes.frblTextAnimation ) {
1443 wp.data.dispatch( 'core/blocks' ).addBlockTypes(
1444 Object.assign( {}, blockType, {
1445 attributes: Object.assign( {}, blockType.attributes, FRBL_TA_ATTR ),
1446 } )
1447 );
1448 }
1449 } catch ( e ) {}
1450 } );
1451 } );
1452
1453 function addTextAnimationPanelToNativeBlocks( BlockEdit ) {
1454 return ( props ) => {
1455 if ( props.name !== 'core/paragraph' && props.name !== 'core/heading' ) {
1456 return <BlockEdit { ...props } />;
1457 }
1458
1459 const { frblTextAnimation = 'none', frblTextAnimationLoop = false } = props.attributes;
1460 const [ isEditMode, setIsEditMode ] = useState( true );
1461
1462 const hasAnimation = frblTextAnimation && 'none' !== frblTextAnimation;
1463 const showPreview = hasAnimation && ! isEditMode;
1464
1465 const Tag = 'core/heading' === props.name
1466 ? 'h' + ( props.attributes.level || 2 )
1467 : 'p';
1468 const rawText = stripHtml( props.attributes.content || '' )
1469 || __( 'Write your text here…', 'frontblocks' );
1470
1471 const playPreview = ( value ) => {
1472 const entry = ANIMATION_PREVIEWS[ value ];
1473 const ms = entry ? entry.duration( rawText ) : 2000;
1474 setIsEditMode( false );
1475 setTimeout( () => setIsEditMode( true ), ms );
1476 };
1477
1478 return (
1479 <Fragment>
1480 { showPreview
1481 ? <AnimationPreview
1482 animationType={ frblTextAnimation }
1483 text={ rawText }
1484 style={ {} }
1485 Tag={ Tag }
1486 />
1487 : <BlockEdit { ...props } />
1488 }
1489 <InspectorControls>
1490 <PanelBody
1491 title={ __( 'FrontBlocks - Text Animation', 'frontblocks' ) }
1492 initialOpen={ false }
1493 >
1494 <SelectControl
1495 label={ __( 'Animation Type', 'frontblocks' ) }
1496 value={ frblTextAnimation }
1497 options={ ANIMATION_OPTIONS }
1498 onChange={ ( value ) => {
1499 props.setAttributes( { frblTextAnimation: value } );
1500 if ( value && 'none' !== value ) {
1501 playPreview( value );
1502 } else {
1503 setIsEditMode( true );
1504 }
1505 } }
1506 />
1507 { hasAnimation && (
1508 <>
1509 <ToggleControl
1510 label={ __( 'Loop animation', 'frontblocks' ) }
1511 checked={ frblTextAnimationLoop }
1512 onChange={ ( value ) => props.setAttributes( { frblTextAnimationLoop: value } ) }
1513 style={ { marginTop: '8px' } }
1514 />
1515 <Button
1516 variant="secondary"
1517 size="small"
1518 onClick={ () => playPreview( frblTextAnimation ) }
1519 style={ { marginTop: '4px', width: '100%', justifyContent: 'center' } }
1520 >
1521 { __( '▶ Preview animation', 'frontblocks' ) }
1522 </Button>
1523 </>
1524 ) }
1525 </PanelBody>
1526 </InspectorControls>
1527 </Fragment>
1528 );
1529 };
1530 }
1531
1532 wp.hooks.addFilter(
1533 'editor.BlockEdit',
1534 'frontblocks/text-animation-native-panel',
1535 addTextAnimationPanelToNativeBlocks
1536 );
1537