added multiply-effects functionality
This commit is contained in:
parent
b4a42c7e3e
commit
f3cee3c393
@ -10,9 +10,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import UiElementRenderer from '../UiElements/UiElementRenderer';
|
import UiElementRenderer from '../UiElements/UiElementRenderer';
|
||||||
import { useElementEffects } from '../../hooks/useElementEffects';
|
import { useElementEffects } from '../../hooks/useElementEffects';
|
||||||
|
import { useAppearAnimation } from '../../hooks/useAppearAnimation';
|
||||||
import {
|
import {
|
||||||
buildTransitionStyle,
|
buildTransitionStyle,
|
||||||
buildAppearAnimationStyle,
|
|
||||||
hasAnyEffects,
|
hasAnyEffects,
|
||||||
type ElementEffectProperties,
|
type ElementEffectProperties,
|
||||||
} from '../../lib/elementEffects';
|
} from '../../lib/elementEffects';
|
||||||
@ -82,6 +82,12 @@ const CanvasElement: React.FC<CanvasElementProps> = ({
|
|||||||
isEditMode ? {} : effectProperties,
|
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%)
|
// Clamp position to canvas bounds (0-100%)
|
||||||
const clamp = (value: number, min: number, max: number) =>
|
const clamp = (value: number, min: number, max: number) =>
|
||||||
Math.min(Math.max(value, min), max);
|
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
|
// Handle keyboard interaction for accessibility
|
||||||
const handleKeyDown = (event: React.KeyboardEvent) => {
|
const handleKeyDown = (event: React.KeyboardEvent) => {
|
||||||
if (event.key === 'Enter' || event.key === ' ') {
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
role='button'
|
role='button'
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
data-constructor-element-id={element.id}
|
data-constructor-element-id={element.id}
|
||||||
className='absolute cursor-pointer'
|
className='absolute cursor-pointer'
|
||||||
style={positionStyle}
|
style={combinedStyle}
|
||||||
onMouseDown={isEditMode ? onMouseDown : undefined}
|
onMouseDown={isEditMode ? onMouseDown : undefined}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
|
onAnimationEnd={effectProperties.appearAnimation ? onAnimationEnd : undefined}
|
||||||
{...(!isEditMode ? eventHandlers : {})}
|
{...(!isEditMode ? eventHandlers : {})}
|
||||||
>
|
>
|
||||||
<UiElementRenderer
|
{content}
|
||||||
element={element}
|
|
||||||
resolveUrl={resolveUrl}
|
|
||||||
isSelected={isSelected}
|
|
||||||
isEditMode={isEditMode}
|
|
||||||
onGalleryCardClick={onGalleryCardClick}
|
|
||||||
onCarouselButtonPositionChange={onCarouselButtonPositionChange}
|
|
||||||
letterboxStyles={letterboxStyles}
|
|
||||||
pageTransitionSettings={pageTransitionSettings}
|
|
||||||
preloadCache={preloadCache}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -9,9 +9,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import UiElementRenderer from './UiElements/UiElementRenderer';
|
import UiElementRenderer from './UiElements/UiElementRenderer';
|
||||||
import { useElementEffects } from '../hooks/useElementEffects';
|
import { useElementEffects } from '../hooks/useElementEffects';
|
||||||
|
import { useAppearAnimation } from '../hooks/useAppearAnimation';
|
||||||
import {
|
import {
|
||||||
buildTransitionStyle,
|
buildTransitionStyle,
|
||||||
buildAppearAnimationStyle,
|
|
||||||
hasAnyEffects,
|
hasAnyEffects,
|
||||||
type ElementEffectProperties,
|
type ElementEffectProperties,
|
||||||
} from '../lib/elementEffects';
|
} from '../lib/elementEffects';
|
||||||
@ -75,6 +75,12 @@ const RuntimeElement: React.FC<RuntimeElementProps> = ({
|
|||||||
// Use effects hook for interactive states
|
// Use effects hook for interactive states
|
||||||
const { effectStyle, eventHandlers } = useElementEffects(effectProperties);
|
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
|
// Build base position style
|
||||||
let positionStyle: React.CSSProperties = {
|
let positionStyle: React.CSSProperties = {
|
||||||
left: `${xPercent}%`,
|
left: `${xPercent}%`,
|
||||||
@ -99,28 +105,56 @@ const RuntimeElement: React.FC<RuntimeElementProps> = ({
|
|||||||
positionStyle = { ...positionStyle, ...transitionStyle };
|
positionStyle = { ...positionStyle, ...transitionStyle };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add appear animation if configured
|
// Check if appear animation uses transform (slide/scale need separate wrapper)
|
||||||
if (effectProperties.appearAnimation) {
|
const needsAnimationWrapper =
|
||||||
const animationStyle = buildAppearAnimationStyle(effectProperties);
|
effectProperties.appearAnimation &&
|
||||||
positionStyle = { ...positionStyle, ...animationStyle };
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className='absolute cursor-pointer'
|
className='absolute cursor-pointer'
|
||||||
style={positionStyle}
|
style={combinedStyle}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
|
onAnimationEnd={effectProperties.appearAnimation ? onAnimationEnd : undefined}
|
||||||
{...eventHandlers}
|
{...eventHandlers}
|
||||||
>
|
>
|
||||||
<UiElementRenderer
|
{content}
|
||||||
element={element}
|
|
||||||
resolveUrl={resolveUrl}
|
|
||||||
onGalleryCardClick={onGalleryCardClick}
|
|
||||||
letterboxStyles={letterboxStyles}
|
|
||||||
pageTransitionSettings={pageTransitionSettings}
|
|
||||||
preloadCache={preloadCache}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -290,11 +290,9 @@
|
|||||||
@keyframes element-fade-in {
|
@keyframes element-fade-in {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translate3d(0, 0, 0);
|
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
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