fixed positioning

This commit is contained in:
Dmitri 2026-05-28 10:45:18 +02:00
parent 08365ca09e
commit 7dd3384a46
2 changed files with 83 additions and 95 deletions

View File

@ -10,11 +10,11 @@
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,
extractEffectProperties, type ElementEffectProperties,
} from '../../lib/elementEffects'; } from '../../lib/elementEffects';
import type { CanvasElement as CanvasElementType } from '../../types/constructor'; import type { CanvasElement as CanvasElementType } from '../../types/constructor';
import type { ResolvedTransitionSettings } from '../../types/transition'; import type { ResolvedTransitionSettings } from '../../types/transition';
@ -57,66 +57,67 @@ const CanvasElement: React.FC<CanvasElementProps> = ({
pageTransitionSettings, pageTransitionSettings,
preloadCache, preloadCache,
}) => { }) => {
// Extract effect properties from element using helper // Extract effect properties from element
const effectProperties = extractEffectProperties( const effectProperties: Partial<ElementEffectProperties> = {
element as unknown as Record<string, unknown>, appearAnimation: element.appearAnimation,
); appearAnimationDuration: element.appearAnimationDuration,
appearAnimationEasing: element.appearAnimationEasing,
// Use appear animation hook (removes animation after completion to unlock properties) hoverScale: element.hoverScale,
const { animationStyle, onAnimationEnd, hasAnimationEnded } = hoverOpacity: element.hoverOpacity,
useAppearAnimation( hoverBackgroundColor: element.hoverBackgroundColor,
effectProperties, hoverColor: element.hoverColor,
element.id, // Reset animation when element changes hoverBoxShadow: element.hoverBoxShadow,
); hoverTransitionDuration: element.hoverTransitionDuration,
focusScale: element.focusScale,
focusOpacity: element.focusOpacity,
focusOutline: element.focusOutline,
focusBoxShadow: element.focusBoxShadow,
activeScale: element.activeScale,
activeOpacity: element.activeOpacity,
activeBackgroundColor: element.activeBackgroundColor,
// Hover reveal effects
hoverReveal: element.hoverReveal,
hoverRevealInitialOpacity: element.hoverRevealInitialOpacity,
hoverRevealTargetOpacity: element.hoverRevealTargetOpacity,
hoverRevealDuration: element.hoverRevealDuration,
hoverRevealDelay: element.hoverRevealDelay,
hoverRevealPersist: element.hoverRevealPersist,
hoverPersistOnClick: element.hoverPersistOnClick,
};
// Use effects hook - disabled in edit mode to avoid interfering with dragging // Use effects hook - disabled in edit mode to avoid interfering with dragging
const { effectStyle, eventHandlers, onPersistClick } = useElementEffects( const { effectStyle, eventHandlers } = useElementEffects(
isEditMode ? {} : effectProperties, isEditMode ? {} : effectProperties,
isEditMode
? undefined
: {
resetKey: element.id,
appearAnimationCompleted:
hasAnimationEnded || !effectProperties.appearAnimation,
},
); );
// Combined click handler (only persist click in preview mode)
const handleClick = () => {
if (!isEditMode) {
onPersistClick();
}
onClick();
};
// 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);
const xClamped = clamp(element.xPercent ?? 50, 0, 100); const xClamped = clamp(element.xPercent ?? 50, 0, 100);
const yClamped = clamp(element.yPercent ?? 50, 0, 100); const yClamped = clamp(element.yPercent ?? 50, 0, 100);
// Build base position style // Build base position style (outer div - handles positioning + animation)
let positionStyle: React.CSSProperties = { let positionStyle: React.CSSProperties = {
left: `${xClamped}%`, left: `${xClamped}%`,
top: `${yClamped}%`, top: `${yClamped}%`,
transform: 'translate(-50%, -50%)', transform: 'translate(-50%, -50%)',
}; };
// Merge interactive effects (preview mode only) // Add appear animation to outer div (ALWAYS - for WYSIWYG)
if (!isEditMode && effectStyle.transform) { // Animation is applied to outer div to keep positioning hack working
// Preserve the translate, add effect transform if (effectProperties.appearAnimation) {
positionStyle.transform = `translate(-50%, -50%) ${effectStyle.transform}`;
// Remove transform from effectStyle to avoid double application
const { transform, ...restEffectStyle } = effectStyle;
positionStyle = { ...positionStyle, ...restEffectStyle };
} else if (!isEditMode) {
positionStyle = { ...positionStyle, ...effectStyle };
}
// Add transition for interactive effects (preview mode only)
if (!isEditMode && hasAnyEffects(effectProperties)) {
positionStyle = { positionStyle = {
...positionStyle, ...positionStyle,
...buildAppearAnimationStyle(effectProperties),
};
}
// Build inner wrapper style for hover/focus/active effects (preview mode only)
// Using separate div so animation on outer div doesn't block these effects
let innerEffectStyle: React.CSSProperties = {};
if (!isEditMode && hasAnyEffects(effectProperties)) {
innerEffectStyle = {
...effectStyle,
...buildTransitionStyle(effectProperties), ...buildTransitionStyle(effectProperties),
}; };
} }
@ -129,51 +130,8 @@ const CanvasElement: React.FC<CanvasElementProps> = ({
} }
}; };
// Check if appear animation uses transform (slide/scale need separate wrapper) // Check if we need the inner wrapper for effects
const needsAnimationWrapper = const needsEffectWrapper = !isEditMode && hasAnyEffects(effectProperties);
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={handleClick}
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
@ -181,16 +139,40 @@ const CanvasElement: React.FC<CanvasElementProps> = ({
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={combinedStyle} style={positionStyle}
onMouseDown={isEditMode ? onMouseDown : undefined} onMouseDown={isEditMode ? onMouseDown : undefined}
onClick={handleClick} onClick={onClick}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
onAnimationEnd={ {...(!isEditMode && !needsEffectWrapper ? eventHandlers : {})}
effectProperties.appearAnimation ? onAnimationEnd : undefined
}
{...(!isEditMode ? eventHandlers : {})}
> >
{content} {needsEffectWrapper ? (
// Inner wrapper handles hover/focus/active effects independently from animation
<div style={innerEffectStyle} {...eventHandlers}>
<UiElementRenderer
element={element}
resolveUrl={resolveUrl}
isSelected={isSelected}
isEditMode={isEditMode}
onGalleryCardClick={onGalleryCardClick}
onCarouselButtonPositionChange={onCarouselButtonPositionChange}
letterboxStyles={letterboxStyles}
pageTransitionSettings={pageTransitionSettings}
preloadCache={preloadCache}
/>
</div>
) : (
<UiElementRenderer
element={element}
resolveUrl={resolveUrl}
isSelected={isSelected}
isEditMode={isEditMode}
onGalleryCardClick={onGalleryCardClick}
onCarouselButtonPositionChange={onCarouselButtonPositionChange}
letterboxStyles={letterboxStyles}
pageTransitionSettings={pageTransitionSettings}
preloadCache={preloadCache}
/>
)}
</div> </div>
); );
}; };

View File

@ -277,18 +277,24 @@
@-webkit-keyframes element-fade-in { @-webkit-keyframes element-fade-in {
from { from {
opacity: 0; opacity: 0;
-webkit-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
} }
to { to {
opacity: 1; opacity: 1;
-webkit-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
} }
} }
@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);
} }
} }