"use client"; import { ComponentData } from "@alpaca-headless/alpaca-headless"; import React, { Fragment, ReactElement, ReactNode, useCallback, useEffect, useMemo, useRef, useState, } from "react"; interface MultiComponentEditorProps { children: ReactNode; component: ComponentData; placeholderName: string; thumbZoom?: number; thumbWidth?: string; thumbHeight?: string; onSelectedComponentChange?: (payload: { selectedId: string | null; selectedIndex: number; }) => void; } export function MultiComponentEditor({ children, component, placeholderName, thumbZoom, thumbWidth, thumbHeight, onSelectedComponentChange, }: MultiComponentEditorProps) { const [selectedId, setSelectedId] = useState(null); const thumbnailContainerRef = useRef(null); const placeholder = component.placeholders.find( (x) => x.name === placeholderName ); const components = useMemo( () => placeholder?.components ?? null, [placeholder] ); const handleMessage = useCallback( (event: MessageEvent) => { const data = event.data; if (!data || !components) return; if (data.type === "componentsSelected") { setSelectedId(data.componentIds?.[0] ?? null); return; } // Parhelia (editing-bridge) selects the surrounding component as a // whole, so only ids that resolve to one of our items may change the // selection — everything else would snap back to the first item. if ( data.family === "parhelia/editing-bridge" && data.name === "setSelection" ) { const ids: string[] = data.payload?.componentIds ?? []; const matches = components.filter((c: ComponentData) => ids.some((id) => matchesComponent(id, c)) ); if (matches.length === 1) { setSelectedId(matches[0].id); } else if (matches.length > 1) { console.warn( "[MultiComponentEditor] ambiguous setSelection: %d components matched ids %o, selecting first", matches.length, ids ); setSelectedId(matches[0].id); } } }, [components] ); useEffect(() => { window.addEventListener("message", handleMessage); return () => { window.removeEventListener("message", handleMessage); }; }, [handleMessage]); // Parhelia's interaction overlay swallows click and pointerup before they // reach the thumbnails (its pointer capture retargets the release event), // but pointerdown still hits the DOM — select thumbnails with a native // capture listener so switching works in both editors. useEffect(() => { const container = thumbnailContainerRef.current; if (!container) return; const handlePointerDown = (event: PointerEvent) => { if (event.button !== 0) return; const thumb = (event.target as Element | null)?.closest( "[data-mce-component-id]" ); const id = thumb?.getAttribute("data-mce-component-id"); if (id) { setSelectedId(id); selectComponents([id]); } }; container.addEventListener("pointerdown", handlePointerDown, true); return () => { container.removeEventListener("pointerdown", handlePointerDown, true); }; }, []); const selectedIndex = components?.findIndex((x: ComponentData) => x.id === selectedId) ?? -1; const resolvedIndex = selectedIndex < 0 ? 0 : selectedIndex; const resolvedComponent = components?.[resolvedIndex] ?? null; const resolvedId = resolvedComponent?.id ?? null; useEffect(() => { if (resolvedId !== null && selectedId !== resolvedId) { setSelectedId(resolvedId); } }, [resolvedId, selectedId]); useEffect(() => { onSelectedComponentChange?.({ selectedId: resolvedId, selectedIndex: resolvedIndex, }); }, [resolvedId, resolvedIndex, onSelectedComponentChange]); if (!placeholder || !components) return; const childrenArray = React.Children.toArray(children) as ReactElement[]; const filteredChildren = childrenArray.filter( (child) => child.type !== "script" ); return ( <> {filteredChildren[resolvedIndex]}