Spaces:
Sleeping
Sleeping
| 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<HTMLIFrameElement | null>; | |
| sectionInfos: SectionInfo[]; | |
| cardAlignments: CardAlignment[]; | |
| isReady: boolean; | |
| updatePositions: () => void; | |
| error: Error | null; | |
| } | |
| // デバウンス関数 | |
| function debounce<T extends (...args: any[]) => any>(func: T, wait: number): (...args: Parameters<T>) => void { | |
| let timeout: NodeJS.Timeout | null = null; | |
| return (...args: Parameters<T>) => { | |
| 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<HTMLIFrameElement>(null); | |
| const [sectionInfos, setSectionInfos] = useState<SectionInfo[]>([]); | |
| const [cardAlignments, setCardAlignments] = useState<CardAlignment[]>([]); | |
| const [isReady, setIsReady] = useState(false); | |
| const [error, setError] = useState<Error | null>(null); | |
| // Observer refs | |
| const mutationObserverRef = useRef<MutationObserver | null>(null); | |
| const resizeObserverRef = useRef<ResizeObserver | null>(null); | |
| const retryTimeoutRef = useRef<NodeJS.Timeout | null>(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, | |
| }; | |
| } | |