import { useCallback, useEffect, useRef, useState } from 'react'; interface SectionInfo { id: string; // aria-label top: number; // iframe内での絶対位置 (px) height: number; // セクションの実際の高さ (px) bottom: number; // top + height (px) } interface CardAlignment { sectionId: string; // 対応するセクションID translateY: number; // transform値 (px) visible: boolean; // 表示制御フラグ } interface UsePreciseSectionAlignmentOptions { enabled?: boolean; // フック有効化フラグ debounceMs?: number; // デバウンス時間 (デフォルト: 150ms) offsetTop?: number; // 追加オフセット (デフォルト: 0px) } interface UsePreciseSectionAlignmentReturn { iframeRef: React.RefObject; sectionInfos: SectionInfo[]; cardAlignments: CardAlignment[]; isReady: boolean; updatePositions: () => void; error: Error | null; } // デバウンス関数 function debounce any>(func: T, wait: number): (...args: Parameters) => void { let timeout: NodeJS.Timeout | null = null; return (...args: Parameters) => { if (timeout) clearTimeout(timeout); timeout = setTimeout(() => func(...args), wait); }; } export function usePreciseSectionAlignment(options: UsePreciseSectionAlignmentOptions = {}): UsePreciseSectionAlignmentReturn { const { enabled = true, debounceMs = 150, offsetTop = 0 } = options; const iframeRef = useRef(null); const [sectionInfos, setSectionInfos] = useState([]); const [cardAlignments, setCardAlignments] = useState([]); const [isReady, setIsReady] = useState(false); const [error, setError] = useState(null); // Observer refs const mutationObserverRef = useRef(null); const resizeObserverRef = useRef(null); const retryTimeoutRef = useRef(null); // iframe内セクション位置を測定 const measureSectionPositions = useCallback((): SectionInfo[] => { if (!iframeRef.current?.contentDocument) { console.warn('[PreciseSectionAlignment] iframe document not available'); return []; } try { const iframeDoc = iframeRef.current.contentDocument; const iframeWindow = iframeRef.current.contentWindow; if (!iframeWindow) { console.warn('[PreciseSectionAlignment] iframe window not available'); return []; } const sections = iframeDoc.querySelectorAll('section[aria-label]'); if (sections.length === 0) { console.warn('[PreciseSectionAlignment] No sections found in iframe'); return []; } const infos: SectionInfo[] = []; // iframe自体のビューポート情報を取得 const iframeRect = iframeRef.current.getBoundingClientRect(); sections.forEach((section) => { const ariaLabel = section.getAttribute('aria-label'); if (!ariaLabel) return; // 方法1: offsetTopを使用(より信頼性が高い) const offsetTop = (section as HTMLElement).offsetTop; const offsetHeight = (section as HTMLElement).offsetHeight; // 方法2: getBoundingClientRectとscrollTopを組み合わせる(フォールバック) const rect = section.getBoundingClientRect(); const scrollTop = iframeWindow.pageYOffset || iframeDoc.documentElement.scrollTop || iframeDoc.body.scrollTop || 0; // offsetTopが有効な場合はそれを使用、そうでなければgetBoundingClientRectから計算 const top = offsetTop > 0 ? offsetTop : rect.top + scrollTop; const height = offsetHeight > 0 ? offsetHeight : rect.height; console.log(`[PreciseSectionAlignment] Section "${ariaLabel}":`, { offsetTop, offsetHeight, rectTop: rect.top, rectHeight: rect.height, scrollTop, calculatedTop: top, calculatedHeight: height, iframeViewportTop: iframeRect.top, }); infos.push({ id: ariaLabel, top: top, height: height, bottom: top + height, }); }); console.log('[PreciseSectionAlignment] Measured sections:', infos); return infos; } catch (err) { console.error('[PreciseSectionAlignment] Error measuring sections:', err); setError(err as Error); return []; } }, []); // カード配置位置を計算 const calculateCardAlignments = useCallback( (sections: SectionInfo[]): CardAlignment[] => { return sections.map((section) => ({ sectionId: section.id, translateY: section.top + offsetTop, // iframe内セクション位置に直接対応 + オフセット visible: true, })); }, [offsetTop], ); // 位置を更新 const updatePositions = useCallback(() => { if (!enabled) return; const sections = measureSectionPositions(); if (sections.length > 0) { setSectionInfos(sections); const alignments = calculateCardAlignments(sections); setCardAlignments(alignments); setIsReady(true); setError(null); console.log('[PreciseSectionAlignment] Updated positions:', { sectionCount: sections.length, alignments: alignments, }); } }, [enabled, measureSectionPositions, calculateCardAlignments]); // デバウンス付き更新 const debouncedUpdatePositions = useCallback(debounce(updatePositions, debounceMs), [updatePositions, debounceMs]); // Observer設定 const setupObservers = useCallback(() => { if (!iframeRef.current?.contentDocument || !enabled) return; const iframeDoc = iframeRef.current.contentDocument; // 既存のObserverをクリーンアップ if (mutationObserverRef.current) { mutationObserverRef.current.disconnect(); } if (resizeObserverRef.current) { resizeObserverRef.current.disconnect(); } // MutationObserver: DOM構造変更を監視 mutationObserverRef.current = new MutationObserver(() => { debouncedUpdatePositions(); }); mutationObserverRef.current.observe(iframeDoc.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class', 'src', 'aria-label'], }); // ResizeObserver: セクションサイズ変更を監視 resizeObserverRef.current = new ResizeObserver(() => { debouncedUpdatePositions(); }); // 全セクションをResizeObserver対象に追加 const sections = iframeDoc.querySelectorAll('section[aria-label]'); sections.forEach((section) => { resizeObserverRef.current?.observe(section); }); console.log('[PreciseSectionAlignment] Observers setup complete'); }, [enabled, debouncedUpdatePositions]); // iframe読み込み監視 useEffect(() => { // SSR回避: クライアントサイドでのみ実行 if (typeof window === 'undefined') { return; } console.log('[PreciseSectionAlignment] useEffect running:', { hasIframeRef: !!iframeRef.current, enabled, iframeElementTagName: iframeRef.current?.tagName, iframeElementSrc: iframeRef.current?.src, timestamp: new Date().toISOString(), }); if (!iframeRef.current || !enabled) { console.log('[PreciseSectionAlignment] Skipping - no iframe or disabled:', { noIframe: !iframeRef.current, disabled: !enabled, }); return; } const iframe = iframeRef.current; let retryCount = 0; const maxRetries = 10; const trySetup = () => { console.log('[PreciseSectionAlignment] trySetup called:', { hasContentDocument: !!iframe.contentDocument, hasBody: !!iframe.contentDocument?.body, retryCount, maxRetries, }); if (!iframe.contentDocument?.body) { if (retryCount < maxRetries) { retryCount++; console.log('[PreciseSectionAlignment] Waiting for iframe body, attempt:', retryCount); retryTimeoutRef.current = setTimeout(trySetup, 200); } else { setError(new Error('iframe setup timeout')); console.error('[PreciseSectionAlignment] Max retries reached - iframe body not available'); } return; } // 初回測定 updatePositions(); // Observer設定 setupObservers(); }; const handleLoad = () => { console.log('[PreciseSectionAlignment] iframe load event'); retryCount = 0; // DOM構築を待つ setTimeout(trySetup, 100); }; iframe.addEventListener('load', handleLoad); // 既に読み込み済みの場合 console.log('[PreciseSectionAlignment] Initial check:', { readyState: iframe.contentDocument?.readyState, hasContentDocument: !!iframe.contentDocument, }); if (iframe.contentDocument?.readyState === 'complete') { console.log('[PreciseSectionAlignment] Document already complete, calling handleLoad'); handleLoad(); } else { // srcdoc iframeの場合 console.log('[PreciseSectionAlignment] Document not complete, scheduling trySetup'); setTimeout(trySetup, 100); } return () => { iframe.removeEventListener('load', handleLoad); if (retryTimeoutRef.current) { clearTimeout(retryTimeoutRef.current); } if (mutationObserverRef.current) { mutationObserverRef.current.disconnect(); } if (resizeObserverRef.current) { resizeObserverRef.current.disconnect(); } }; }, [enabled, updatePositions, setupObservers]); // ウィンドウリサイズ時の再計算 useEffect(() => { if (!enabled) return; const handleResize = () => { console.log('[PreciseSectionAlignment] Window resized'); debouncedUpdatePositions(); }; window.addEventListener('resize', handleResize); return () => { window.removeEventListener('resize', handleResize); }; }, [enabled, debouncedUpdatePositions]); // エラー時のリトライ処理 useEffect(() => { if (!error || !enabled) return; console.log('[PreciseSectionAlignment] Error detected, scheduling retry'); const retryTimeout = setTimeout(() => { setError(null); updatePositions(); }, 1000); return () => clearTimeout(retryTimeout); }, [error, enabled, updatePositions]); return { iframeRef, sectionInfos, cardAlignments, isReady, updatePositions, error, }; }