added multiply-effects functionality
This commit is contained in:
parent
b4a42c7e3e
commit
f3cee3c393
@ -10,9 +10,9 @@
|
||||
import React from 'react';
|
||||
import UiElementRenderer from '../UiElements/UiElementRenderer';
|
||||
import { useElementEffects } from '../../hooks/useElementEffects';
|
||||
import { useAppearAnimation } from '../../hooks/useAppearAnimation';
|
||||
import {
|
||||
buildTransitionStyle,
|
||||
buildAppearAnimationStyle,
|
||||
hasAnyEffects,
|
||||
type ElementEffectProperties,
|
||||
} from '../../lib/elementEffects';
|
||||
@ -82,6 +82,12 @@ const CanvasElement: React.FC<CanvasElementProps> = ({
|
||||
isEditMode ? {} : effectProperties,
|
||||
);
|
||||
|
||||
// Use appear animation hook (removes animation after completion to unlock properties)
|
||||
const { animationStyle, onAnimationEnd } = useAppearAnimation(
|
||||
effectProperties,
|
||||
element.id, // Reset animation when element changes
|
||||
);
|
||||
|
||||
// Clamp position to canvas bounds (0-100%)
|
||||
const clamp = (value: number, min: number, max: number) =>
|
||||
Math.min(Math.max(value, min), max);
|
||||
@ -114,14 +120,6 @@ const CanvasElement: React.FC<CanvasElementProps> = ({
|
||||
};
|
||||
}
|
||||
|
||||
// Add appear animation (ALWAYS - for WYSIWYG)
|
||||
if (effectProperties.appearAnimation) {
|
||||
positionStyle = {
|
||||
...positionStyle,
|
||||
...buildAppearAnimationStyle(effectProperties),
|
||||
};
|
||||
}
|
||||
|
||||
// Handle keyboard interaction for accessibility
|
||||
const handleKeyDown = (event: React.KeyboardEvent) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
@ -130,29 +128,66 @@ const CanvasElement: React.FC<CanvasElementProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
// Check if appear animation uses transform (slide/scale need separate wrapper)
|
||||
const needsAnimationWrapper =
|
||||
effectProperties.appearAnimation &&
|
||||
effectProperties.appearAnimation !== 'fade';
|
||||
|
||||
// Render content
|
||||
const content = (
|
||||
<UiElementRenderer
|
||||
element={element}
|
||||
resolveUrl={resolveUrl}
|
||||
isSelected={isSelected}
|
||||
isEditMode={isEditMode}
|
||||
onGalleryCardClick={onGalleryCardClick}
|
||||
onCarouselButtonPositionChange={onCarouselButtonPositionChange}
|
||||
letterboxStyles={letterboxStyles}
|
||||
pageTransitionSettings={pageTransitionSettings}
|
||||
preloadCache={preloadCache}
|
||||
/>
|
||||
);
|
||||
|
||||
// For slide/scale (uses transform), use separate wrapper to avoid conflict with positioning
|
||||
if (needsAnimationWrapper) {
|
||||
return (
|
||||
<div
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
data-constructor-element-id={element.id}
|
||||
className='absolute cursor-pointer'
|
||||
style={positionStyle}
|
||||
onMouseDown={isEditMode ? onMouseDown : undefined}
|
||||
onClick={onClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
{...(!isEditMode ? eventHandlers : {})}
|
||||
>
|
||||
<div style={animationStyle} onAnimationEnd={onAnimationEnd}>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Fade or no animation: apply animation to positioning div
|
||||
const combinedStyle = effectProperties.appearAnimation
|
||||
? { ...positionStyle, ...animationStyle }
|
||||
: positionStyle;
|
||||
|
||||
return (
|
||||
<div
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
data-constructor-element-id={element.id}
|
||||
className='absolute cursor-pointer'
|
||||
style={positionStyle}
|
||||
style={combinedStyle}
|
||||
onMouseDown={isEditMode ? onMouseDown : undefined}
|
||||
onClick={onClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
onAnimationEnd={effectProperties.appearAnimation ? onAnimationEnd : undefined}
|
||||
{...(!isEditMode ? eventHandlers : {})}
|
||||
>
|
||||
<UiElementRenderer
|
||||
element={element}
|
||||
resolveUrl={resolveUrl}
|
||||
isSelected={isSelected}
|
||||
isEditMode={isEditMode}
|
||||
onGalleryCardClick={onGalleryCardClick}
|
||||
onCarouselButtonPositionChange={onCarouselButtonPositionChange}
|
||||
letterboxStyles={letterboxStyles}
|
||||
pageTransitionSettings={pageTransitionSettings}
|
||||
preloadCache={preloadCache}
|
||||
/>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -9,9 +9,9 @@
|
||||
import React from 'react';
|
||||
import UiElementRenderer from './UiElements/UiElementRenderer';
|
||||
import { useElementEffects } from '../hooks/useElementEffects';
|
||||
import { useAppearAnimation } from '../hooks/useAppearAnimation';
|
||||
import {
|
||||
buildTransitionStyle,
|
||||
buildAppearAnimationStyle,
|
||||
hasAnyEffects,
|
||||
type ElementEffectProperties,
|
||||
} from '../lib/elementEffects';
|
||||
@ -75,6 +75,12 @@ const RuntimeElement: React.FC<RuntimeElementProps> = ({
|
||||
// Use effects hook for interactive states
|
||||
const { effectStyle, eventHandlers } = useElementEffects(effectProperties);
|
||||
|
||||
// Use appear animation hook (removes animation after completion to unlock properties)
|
||||
const { animationStyle, onAnimationEnd } = useAppearAnimation(
|
||||
effectProperties,
|
||||
element.id, // Reset animation when element changes
|
||||
);
|
||||
|
||||
// Build base position style
|
||||
let positionStyle: React.CSSProperties = {
|
||||
left: `${xPercent}%`,
|
||||
@ -99,28 +105,56 @@ const RuntimeElement: React.FC<RuntimeElementProps> = ({
|
||||
positionStyle = { ...positionStyle, ...transitionStyle };
|
||||
}
|
||||
|
||||
// Add appear animation if configured
|
||||
if (effectProperties.appearAnimation) {
|
||||
const animationStyle = buildAppearAnimationStyle(effectProperties);
|
||||
positionStyle = { ...positionStyle, ...animationStyle };
|
||||
// Check if appear animation uses transform (slide/scale need separate wrapper)
|
||||
const needsAnimationWrapper =
|
||||
effectProperties.appearAnimation &&
|
||||
effectProperties.appearAnimation !== 'fade';
|
||||
|
||||
// Render content (with or without animation wrapper)
|
||||
const content = (
|
||||
<UiElementRenderer
|
||||
element={element}
|
||||
resolveUrl={resolveUrl}
|
||||
onGalleryCardClick={onGalleryCardClick}
|
||||
letterboxStyles={letterboxStyles}
|
||||
pageTransitionSettings={pageTransitionSettings}
|
||||
preloadCache={preloadCache}
|
||||
/>
|
||||
);
|
||||
|
||||
// For fade animation (opacity only), apply to positioning div
|
||||
// For slide/scale (uses transform), use separate wrapper to avoid conflict
|
||||
if (needsAnimationWrapper) {
|
||||
return (
|
||||
<div
|
||||
className='absolute cursor-pointer'
|
||||
style={positionStyle}
|
||||
onClick={onClick}
|
||||
tabIndex={0}
|
||||
{...eventHandlers}
|
||||
>
|
||||
<div style={animationStyle} onAnimationEnd={onAnimationEnd}>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Fade or no animation: apply animation to positioning div
|
||||
const combinedStyle = effectProperties.appearAnimation
|
||||
? { ...positionStyle, ...animationStyle }
|
||||
: positionStyle;
|
||||
|
||||
return (
|
||||
<div
|
||||
className='absolute cursor-pointer'
|
||||
style={positionStyle}
|
||||
style={combinedStyle}
|
||||
onClick={onClick}
|
||||
tabIndex={0}
|
||||
onAnimationEnd={effectProperties.appearAnimation ? onAnimationEnd : undefined}
|
||||
{...eventHandlers}
|
||||
>
|
||||
<UiElementRenderer
|
||||
element={element}
|
||||
resolveUrl={resolveUrl}
|
||||
onGalleryCardClick={onGalleryCardClick}
|
||||
letterboxStyles={letterboxStyles}
|
||||
pageTransitionSettings={pageTransitionSettings}
|
||||
preloadCache={preloadCache}
|
||||
/>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -290,11 +290,9 @@
|
||||
@keyframes element-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
105
frontend/src/hooks/useAppearAnimation.ts
Normal file
105
frontend/src/hooks/useAppearAnimation.ts
Normal file
@ -0,0 +1,105 @@
|
||||
/**
|
||||
* useAppearAnimation Hook
|
||||
*
|
||||
* Manages appear animation lifecycle to prevent conflicts with interactive effects.
|
||||
*
|
||||
* Problem: CSS animations with `animation-fill-mode: forwards` lock animated
|
||||
* properties (opacity, transform), preventing CSS transitions from working
|
||||
* on hover/focus/active states.
|
||||
*
|
||||
* Solution: Track animation completion via `animationend` event and remove
|
||||
* the animation style after it completes, unlocking properties for transitions.
|
||||
*
|
||||
* Cross-browser: Uses standard `animationend` event supported by all modern
|
||||
* browsers (Chrome, Firefox, Safari, Edge, iOS Safari, Android Chrome).
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import type { CSSProperties, AnimationEventHandler } from 'react';
|
||||
import {
|
||||
buildAppearAnimationStyle,
|
||||
type ElementEffectProperties,
|
||||
} from '../lib/elementEffects';
|
||||
|
||||
interface UseAppearAnimationResult {
|
||||
/** Animation style to apply (empty after animation completes) */
|
||||
animationStyle: CSSProperties;
|
||||
/** Handler to attach to element's onAnimationEnd */
|
||||
onAnimationEnd: AnimationEventHandler<HTMLElement>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for managing appear animation with proper cleanup.
|
||||
*
|
||||
* @param effects - Element effect properties containing appear animation config
|
||||
* @param key - Optional key to reset animation (e.g., page slug for re-triggering on navigation)
|
||||
* @returns Object with animationStyle and onAnimationEnd handler
|
||||
*
|
||||
* @example
|
||||
* const { animationStyle, onAnimationEnd } = useAppearAnimation(effectProperties);
|
||||
* return (
|
||||
* <div
|
||||
* style={{ ...baseStyle, ...animationStyle }}
|
||||
* onAnimationEnd={onAnimationEnd}
|
||||
* >
|
||||
* {content}
|
||||
* </div>
|
||||
* );
|
||||
*/
|
||||
export function useAppearAnimation(
|
||||
effects: Partial<ElementEffectProperties>,
|
||||
key?: string | number,
|
||||
): UseAppearAnimationResult {
|
||||
// Track whether animation has completed
|
||||
const [hasAnimationEnded, setHasAnimationEnded] = useState(false);
|
||||
|
||||
// Track the animation name to identify our animation in the event
|
||||
const animationNameRef = useRef<string | null>(null);
|
||||
|
||||
// Reset animation state when key changes (e.g., navigating to new page)
|
||||
useEffect(() => {
|
||||
setHasAnimationEnded(false);
|
||||
}, [key]);
|
||||
|
||||
// Build animation style and capture animation name
|
||||
const animationStyle = (() => {
|
||||
// If no appear animation or animation already ended, return empty
|
||||
if (!effects.appearAnimation || hasAnimationEnded) {
|
||||
animationNameRef.current = null;
|
||||
return {};
|
||||
}
|
||||
|
||||
const style = buildAppearAnimationStyle(effects);
|
||||
|
||||
// Extract animation name from the style for event matching
|
||||
if (style.animation) {
|
||||
const match = String(style.animation).match(/^([\w-]+)/);
|
||||
animationNameRef.current = match ? match[1] : null;
|
||||
}
|
||||
|
||||
return style;
|
||||
})();
|
||||
|
||||
// Handler for animationend event
|
||||
const onAnimationEnd: AnimationEventHandler<HTMLElement> = useCallback(
|
||||
(event) => {
|
||||
// Only handle our animation (ignore child animations)
|
||||
// Check if the animation name matches our appear animation
|
||||
const targetAnimationName = animationNameRef.current;
|
||||
|
||||
if (
|
||||
targetAnimationName &&
|
||||
event.animationName === targetAnimationName &&
|
||||
event.target === event.currentTarget
|
||||
) {
|
||||
setHasAnimationEnded(true);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return {
|
||||
animationStyle,
|
||||
onAnimationEnd,
|
||||
};
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user