import { useRef, useState } from "react"; import { NodeViewWrapper, type NodeViewProps } from "@tiptap/react"; import { FIGURE_WIDTH, isResizablePlacement, type FigurePlacement } from "../model/book"; const PLACEMENTS = [ { key: "inline", label: "Inline" }, { key: "full-width", label: "Full width" }, { key: "full-page", label: "Full page" }, { key: "float-top", label: "Float" }, ]; const clampWidth = (value: number) => Math.min(100, Math.max(20, Math.round(value))); export function FigureView({ node, updateAttributes, selected }: NodeViewProps) { const { src, alt, caption, placement } = node.attrs; const frameRef = useRef(null); const [dragWidth, setDragWidth] = useState(null); const resizable = isResizablePlacement(placement as FigurePlacement); const baseWidth = node.attrs.width ?? FIGURE_WIDTH[placement as FigurePlacement] ?? 100; const width = resizable ? (dragWidth ?? baseWidth) : null; const startResize = (e: React.PointerEvent) => { e.preventDefault(); const wrapper = frameRef.current?.closest(".figure") as HTMLElement | null; const container = wrapper?.parentElement; if (!wrapper || !container) return; const startX = e.clientX; const startPx = wrapper.getBoundingClientRect().width; const containerPx = container.clientWidth || 1; const percentAt = (clientX: number) => clampWidth(((startPx + clientX - startX) / containerPx) * 100); const onMove = (ev: PointerEvent) => setDragWidth(percentAt(ev.clientX)); const onUp = (ev: PointerEvent) => { window.removeEventListener("pointermove", onMove); window.removeEventListener("pointerup", onUp); updateAttributes({ width: percentAt(ev.clientX) }); setDragWidth(null); }; window.addEventListener("pointermove", onMove); window.addEventListener("pointerup", onUp); }; return (
{src ? {alt} :
No image
} {selected && src && resizable && (
)}
updateAttributes({ caption: e.target.value })} /> {selected && (
{PLACEMENTS.map((p) => ( ))} {resizable && src && {Math.round(width ?? baseWidth)}%}
)} ); }