import React, { FC, useContext, useId, useMemo, useRef, useState } from 'react'; import JSXStyle from 'styled-jsx/style'; import { AnchorSide, ArticleItemType, getLayoutStyles, ItemAny, AreaAnchor, PositionType, DimensionsType } from '@cntrl-site/sdk'; import { useCntrlContext } from '../../provider/useCntrlContext'; import { useItemScale } from './useItemScale'; import { ScaleAnchorMap } from '../../utils/ScaleAnchorMap'; import { useSectionHeightData } from '../Section/useSectionHeightMap'; import { getItemTopStyle, getPercentageBasedTopStyle } from '../../utils/getItemTopStyle'; import { useStickyItemTop } from './useStickyItemTop'; import { getAnchoredItemTop } from '../../utils/getAnchoredItemTop'; import { useLayoutContext } from '../useLayoutContext'; import { ArticleRectContext } from '../../provider/ArticleRectContext'; import { useExemplary } from '../../common/useExemplary'; import { KeyframesContext } from '../../provider/KeyframesContext'; import { useItemInteractionCtrl } from '../../interactions/useItemInteractionCtrl'; import { isItemType } from '../../utils/isItemType'; import { RichTextWrapper } from './RichTextWrapper'; import { itemsMap } from './itemsMap'; import { useItemTriggers } from './useItemTriggers'; import { parseSizing, useSizing } from './useSizing'; import { useItemPointerEvents } from './useItemPointerEvents'; import { useItemArea } from './useItemArea'; import { useDraggable } from './useDraggable'; import { ItemGeometryContext } from '../../ItemGeometry/ItemGeometryContext'; export interface ItemProps { item: I; sectionId: string; articleHeight?: number; onResize?: (height: number) => void; interactionCtrl?: ReturnType; onVisibilityChange: (isVisible: boolean) => void; isInCompound?: boolean; } export interface ItemWrapperProps { item: ItemAny; sectionId: string; articleHeight?: number; sectionHeight?: number; isInGroup?: boolean; isParentVisible?: boolean; } const stickyFix = ` -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); `; const noop = () => null; export const Item: FC = ({ item, sectionId, articleHeight, isParentVisible = true, isInGroup = false, sectionHeight = 0 }) => { const itemWrapperRef = useRef(null); const itemInnerRef = useRef(null); const rectObserver = useContext(ArticleRectContext); const keyframesRepo = useContext(KeyframesContext); const reactId = useId(); const id = `${reactId}-item-${item.id}`; const keyframes = useMemo(() => keyframesRepo.getItemKeyframes(item.id), [keyframesRepo, item.id]); const { layouts } = useCntrlContext(); const layout = useLayoutContext(); const exemplary = useExemplary(); const { handleVisibilityChange, allowPointerEvents } = useItemPointerEvents( item.commonParams.pointerEvents ?? 'when_visible', isParentVisible ); const [wrapperHeight, setWrapperHeight] = useState(undefined); const [itemHeight, setItemHeight] = useState(undefined); const itemScale = useItemScale(item, sectionId); const interactionCtrl = useItemInteractionCtrl(item.id); const triggers = useItemTriggers(interactionCtrl); const [position, setPosition] = useState({ x: 0, y: 0 }); const [isDraggingActive, setIsDraggingActive] = useState(false); const wrapperStateProps = interactionCtrl?.getState(['top', 'left']); const innerStateProps = interactionCtrl?.getState(['width', 'height', 'scale']); const geometryService = useContext(ItemGeometryContext); const hasItem = geometryService.hasItem(item.id); const geometryController = useMemo(() => hasItem ? geometryService.getControllerById(item.id) : undefined, [hasItem]); if (geometryController) { geometryController.setScale(itemScale!); } const { width, height, top, left } = useItemArea(item, sectionId, { top: wrapperStateProps?.styles?.top as number, left: wrapperStateProps?.styles?.left as number, width: innerStateProps?.styles?.width as number, height: innerStateProps?.styles?.height as number }); const sectionHeightLayoutMap = useSectionHeightData(sectionId); const stickyTop = useStickyItemTop(item, sectionId, wrapperStateProps?.styles?.top as number); const layoutValues: Record[] = [item.area, item.hidden]; layoutValues.push(item.sticky); layoutValues.push(sectionHeightLayoutMap); if (item.layoutParams) { layoutValues.push(item.layoutParams); } const sticky = layout ? item.sticky[layout] : undefined; const sizingAxis = useSizing(item); const ItemComponent = itemsMap[item.type] || noop; const sectionTop = rectObserver ? rectObserver.getSectionTop(sectionId) : 0; const layoutParams = layout ? item.layoutParams[layout] : undefined; const isDraggable = layoutParams && 'isDraggable' in layoutParams ? layoutParams.isDraggable : undefined; const blendMode = layoutParams && 'blendMode' in layoutParams ? layoutParams.blendMode : undefined; useDraggable({ draggableRef: itemInnerRef.current, isEnabled: isDraggable ?? false }, ({ startX, startY, currentX, currentY, lastX, lastY, drag }) => { const item = itemInnerRef.current; if (!item) return; if (drag) { setIsDraggingActive(true); setPosition({ x: (currentX - startX) + lastX, y: (currentY - startY) + lastY }); } else { setIsDraggingActive(false); } }); const handleItemResize = (height: number) => { if (!layout) return; const sticky = item.sticky[layout]; if (!sticky || stickyTop === undefined || !articleHeight) { setWrapperHeight(undefined); return; } const itemArticleOffset = sectionTop / window.innerWidth + stickyTop; const maxStickyTo = articleHeight - itemArticleOffset - height; const end = sticky.to !== undefined ? Math.min(maxStickyTo, sticky.to) : articleHeight - itemArticleOffset - height; const wrapperHeight = end - sticky.from + height; setItemHeight(height); setWrapperHeight(wrapperHeight); }; const isRichText = isItemType(item, ArticleItemType.RichText); const anchorSide = layout ? item.area[layout].anchorSide : AnchorSide.Top; const dimensionsType = layout ? item.area[layout].dimensionsType : DimensionsType.PixelsBased; const isContainItem = dimensionsType === DimensionsType.PercentageBased; const itemInnerHeight = isContainItem ? `${height}vh` : `${height! * 100}vw`; const positionType = layout ? item.area[layout].positionType : PositionType.ScreenBased; const isScreenBasedBottom = positionType === PositionType.ScreenBased && anchorSide === AnchorSide.Bottom; const scale = innerStateProps?.styles?.scale ?? itemScale; const hasClickTriggers = interactionCtrl?.getHasTrigger(item.id, 'click') ?? false; return (
{ e.stopPropagation(); interactionCtrl?.handleTransitionEnd?.(e.propertyName); }} style={{ ...(top !== undefined ? { top: isScreenBasedBottom ? 'unset' : isContainItem ? getPercentageBasedTopStyle(top, height, anchorSide) : getItemTopStyle(top, anchorSide) } : {}), ...(left !== undefined ? { left: `${left * 100}vw` } : {}), ...(top !== undefined ? { bottom: isScreenBasedBottom ? `${-top * 100}vw` : 'unset' } : {}), ...(wrapperHeight !== undefined ? { height: `${wrapperHeight * 100}vw` } : {}), transition: wrapperStateProps?.transition ?? 'none', ...(blendMode && { mixBlendMode: blendMode as React.CSSProperties['mixBlendMode'] }) }} >
{` ${getLayoutStyles(layouts, layoutValues, ([area, hidden, sticky, sectionHeight, layoutParams]) => { const sizingAxis = parseSizing(layoutParams.sizing); const isScreenBasedBottom = area.positionType === PositionType.ScreenBased && area.anchorSide === AnchorSide.Bottom; const scaleAnchor = area.scaleAnchor as AreaAnchor; const innerHeight = area?.dimensionsType && area?.dimensionsType === DimensionsType.PercentageBased ? `${area.height}vh` : `${area.height * 100}vw`; return (` .item-${item.id} { position: ${sticky ? 'sticky' : 'absolute'}; top: ${sticky ? `${getAnchoredItemTop(area.top - sticky.from, sectionHeight, area.anchorSide)}` : 0}; transition: opacity 0.2s linear 0.1s; pointer-events: none; display: ${hidden ? 'none' : 'block'}; height: fit-content; } .item-${item.id}-inner { width: ${sizingAxis.x === 'manual' ? `${area.width * 100}vw` : 'max-content'}; height: ${sizingAxis.y === 'manual' ? innerHeight : 'unset'}; transform-origin: ${ScaleAnchorMap[scaleAnchor]}; transform: scale(${area.scale}); position: relative; } .item-wrapper-${item.id} { position: ${area.positionType === PositionType.ScreenBased ? 'fixed' : 'absolute'}; z-index: ${area.zIndex}; ${!isInGroup && sticky ? stickyFix : ''} pointer-events: none; bottom: ${isScreenBasedBottom ? `${-area.top * 100}vw` : 'unset'}; top: ${isScreenBasedBottom ? 'unset' : isContainItem ? getPercentageBasedTopStyle(area.top, area.height, area.anchorSide) : getItemTopStyle(area.top, area.anchorSide)}; left: ${area.left * 100}vw; } `); })} `}
); };