139 lines
3.8 KiB
TypeScript
139 lines
3.8 KiB
TypeScript
/**
|
||
* 智能 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'
|
||
})
|
||
}
|
||
} |