/** * 智能 Popup 定位工具 * 自動檢測可用空間,確保 popup 始終完全可見 */ export interface PopupPosition { x: number y: number placement: 'top' | 'bottom' | 'center' } export interface ClickPosition { x: number y: number width: number height: number } const POPUP_MARGIN = 16 // 與視窗邊緣的最小距離 const POPUP_ARROW_OFFSET = 10 // 箭頭偏移量 /** * 計算智能 popup 位置 * @param clickRect 點擊元素的位置信息 * @param popupWidth popup 寬度 (預設 384px = w-96) * @param popupHeight popup 高度 (預計值) * @returns 計算後的最佳位置 */ export function calculateSmartPopupPosition( clickRect: ClickPosition, popupWidth: number = 384, popupHeight: number = 400 ): PopupPosition { const viewportWidth = window.innerWidth const viewportHeight = window.innerHeight const scrollY = window.scrollY // 計算點擊元素的中心點 const clickCenterX = clickRect.x + clickRect.width / 2 const clickCenterY = clickRect.y + clickRect.height / 2 + scrollY // 檢測各方向可用空間 const spaceAbove = clickRect.y - POPUP_MARGIN const spaceBelow = viewportHeight - (clickRect.y + clickRect.height) - POPUP_MARGIN const spaceLeft = clickRect.x - POPUP_MARGIN const spaceRight = viewportWidth - (clickRect.x + clickRect.width) - POPUP_MARGIN // 判斷最佳垂直位置 let placement: 'top' | 'bottom' | 'center' = 'bottom' let y: number if (spaceBelow >= popupHeight) { // 底部有足夠空間 placement = 'bottom' y = clickRect.y + clickRect.height + POPUP_ARROW_OFFSET + scrollY } else if (spaceAbove >= popupHeight) { // 頂部有足夠空間 placement = 'top' y = clickRect.y - popupHeight - POPUP_ARROW_OFFSET + scrollY } else { // 兩邊都沒有足夠空間,使用居中模式 placement = 'center' y = Math.max( POPUP_MARGIN + scrollY, Math.min( viewportHeight - popupHeight - POPUP_MARGIN + scrollY, clickCenterY - popupHeight / 2 ) ) } // 計算水平位置 (始終嘗試居中對齊點擊元素) let x = clickCenterX - popupWidth / 2 // 確保不超出視窗邊界 x = Math.max(POPUP_MARGIN, Math.min(x, viewportWidth - popupWidth - POPUP_MARGIN)) return { x, y, placement } } /** * 檢測是否為移動設備 * 移動設備使用底部 modal,桌面使用智能定位 */ export function isMobileDevice(): boolean { if (typeof window === 'undefined') return false return window.innerWidth <= 768 } /** * 獲取元素的位置信息 * @param element DOM 元素 * @returns 位置信息 */ export function getElementPosition(element: HTMLElement): ClickPosition { const rect = element.getBoundingClientRect() return { x: rect.left, y: rect.top, width: rect.width, height: rect.height } } /** * 創建平滑滾動效果,確保 popup 可見 * @param targetY popup 的 Y 位置 * @param popupHeight popup 高度 */ export function ensurePopupVisible(targetY: number, popupHeight: number): void { const viewportHeight = window.innerHeight const scrollY = window.scrollY // 計算 popup 的頂部和底部位置(相對於視窗) const popupTop = targetY - scrollY const popupBottom = popupTop + popupHeight let needsScroll = false let scrollTarget = scrollY // 如果 popup 頂部被遮蔽 if (popupTop < POPUP_MARGIN) { scrollTarget = targetY - POPUP_MARGIN needsScroll = true } // 如果 popup 底部被遮蔽 else if (popupBottom > viewportHeight - POPUP_MARGIN) { scrollTarget = targetY + popupHeight - viewportHeight + POPUP_MARGIN needsScroll = true } // 執行平滑滾動 if (needsScroll) { window.scrollTo({ top: scrollTarget, behavior: 'smooth' }) } }