dramaling-vocab-learning/frontend/lib/utils/popupPositioning.ts

139 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 智能 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'
})
}
}