import React, { useEffect, useRef } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; export interface RefPopupPos { x: number; y: number; id: string; type: 'abbrev' | 'footnote'; vol: number; page: number; } interface Props { pos: RefPopupPos | null; onClose: () => void; onJump?: (id: string) => void; } const MIN_W = 240; const MAX_W = 600; const INIT_W = 320; const ReferencePopup: React.FC = ({ pos, onClose, onJump }) => { const popupRef = useRef(null); const [content, setContent] = React.useState(null); const [isLoading, setIsLoading] = React.useState(false); const [popupFontSize, setPopupFontSize] = React.useState(15); const [popupWidth, setPopupWidth] = React.useState(INIT_W); const [popupHeight, setPopupHeight] = React.useState(null); const [visible, setVisible] = React.useState(false); // ── Position state (top-left of popup) ── const [posX, setPosX] = React.useState(0); const [posY, setPosY] = React.useState(0); const [showArrow, setShowArrow] = React.useState(false); const [arrowShift, setArrowShift] = React.useState(0); const [flipUp, setFlipUp] = React.useState(false); // ── Drag state ── const dragging = useRef(false); const dragStart = useRef({ x: 0, y: 0, px: 0, py: 0 }); // ── Resize state ── const resizing = useRef(false); const resizeStart = useRef({ x: 0, y: 0, w: INIT_W, h: 0 }); // ── Initial positioning when pos changes ── useEffect(() => { if (!pos) { setVisible(false); setContent(null); return; } setPopupWidth(INIT_W); setPopupHeight(null); // Fetch content const fetchRef = async () => { setIsLoading(true); try { const response = await fetch( `/api/reference/lookup?vol=${pos.vol}&page=${pos.page}&type=${pos.type}&id=${encodeURIComponent(pos.id)}` ); const data = await response.json(); setContent(data.content); } catch (err) { console.error("Failed to fetch reference:", err); setContent("เกิดข้อผิดพลาดในการโหลดข้อมูล"); } finally { setIsLoading(false); } }; fetchRef(); // Calculate initial position (top-left of popup, not centered) requestAnimationFrame(() => { const viewW = window.innerWidth; const viewH = window.innerHeight; const halfW = INIT_W / 2; const EDGE = 12; // Start: sup center → convert to top-left let left = pos.x - halfW; let top = pos.y + 6; // 6px below sup let flip = false; // Horizontal clamp if (left < EDGE) left = EDGE; else if (left + INIT_W > viewW - EDGE) left = viewW - INIT_W - EDGE; // Arrow horizontal shift const centerX = left + INIT_W / 2; let aShift = pos.x - centerX; // Vertical flip if near bottom const estH = 260; if (pos.y + estH + 20 > viewH) { flip = true; top = pos.y - 6 - estH; if (top < EDGE) top = EDGE; } setPosX(left); setPosY(top); setArrowShift(aShift); setFlipUp(flip); setShowArrow(true); setVisible(true); // Fine-tune after render setTimeout(() => { if (popupRef.current) { const actualH = popupRef.current.offsetHeight; if (flip && pos.y - actualH - 6 < EDGE) { setPosY(EDGE); setFlipUp(false); } else if (!flip && pos.y + actualH + 20 > viewH) { setPosY(Math.max(EDGE, viewH - actualH - 10)); setFlipUp(true); } } }, 60); }); }, [pos]); // ── Drag handler ── const onDragStart = React.useCallback((e: React.MouseEvent) => { e.preventDefault(); dragging.current = true; dragStart.current = { x: e.clientX, y: e.clientY, px: posX, py: posY }; setShowArrow(false); const onMove = (ev: MouseEvent) => { if (!dragging.current) return; setPosX(dragStart.current.px + (ev.clientX - dragStart.current.x)); setPosY(dragStart.current.py + (ev.clientY - dragStart.current.y)); }; const onUp = () => { dragging.current = false; }; document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp, { once: true }); }, [posX, posY]); // ── Resize handler ── const onResizeStart = React.useCallback((e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); resizing.current = true; resizeStart.current = { x: e.clientX, y: e.clientY, w: popupWidth, h: popupHeight || 300 }; const onMove = (ev: MouseEvent) => { if (!resizing.current) return; const newW = Math.max(MIN_W, Math.min(MAX_W, resizeStart.current.w + (ev.clientX - resizeStart.current.x))); const newH = Math.max(150, resizeStart.current.h + (ev.clientY - resizeStart.current.y)); setPopupWidth(newW); setPopupHeight(newH); }; const onUp = () => { resizing.current = false; }; document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp, { once: true }); }, [popupWidth, popupHeight]); // ── Click outside ── useEffect(() => { const onMouseDown = (e: MouseEvent | TouchEvent) => { const target = e.target as HTMLElement; if (target.tagName?.toLowerCase() === 'sup' && (target.classList.contains('abbrev-ref') || target.classList.contains('footnote-ref'))) return; if (popupRef.current && !popupRef.current.contains(e.target as Node)) onClose(); }; document.addEventListener('mousedown', onMouseDown); document.addEventListener('touchstart', onMouseDown); return () => { document.removeEventListener('mousedown', onMouseDown); document.removeEventListener('touchstart', onMouseDown); }; }, [onClose]); return ( {pos && ( {/* Gradient top bar */}
{/* Header — drag handle */}
{/* Drag indicator */} {pos.id} {pos.type === 'abbrev' ? 'คำย่อ' : 'เชิงอรรถ'}
{/* Content */}
{isLoading ? (
) : (
{content || ( ไม่พบข้อมูลอ้างอิง )}
)}
{pos.type === 'abbrev' && !isLoading && (
AI ขยายความ
)} {pos.type === 'footnote' && !isLoading && onJump && (
)}
{/* Resize handle */}
{/* Tail arrow — hidden after manual drag */} {showArrow && ( flipUp ? (
) : (
) )} )} ); }; export default ReferencePopup;