hololake-system-architecture/product-source/hololake-platform/src/components/contextMenuPosition.ts

91 lines
2.9 KiB
TypeScript

import type { CSSProperties } from 'react'
const CONTEXT_MENU_VIEWPORT_PADDING = 8
export interface ContextMenuPoint {
x: number
y: number
}
export interface ContextMenuPositionOptions {
maxWidth?: CSSProperties['maxWidth']
minWidth?: CSSProperties['minWidth']
}
function parseZoomValue(source: string | undefined): number | null {
if (!source || source === 'normal') return null
const value = source.endsWith('%')
? Number.parseFloat(source) / 100
: Number.parseFloat(source)
return Number.isFinite(value) && value > 0 ? value : null
}
function getRootZoom(): number {
const overlayZoom = parseZoomValue(
getComputedStyle(document.documentElement).getPropertyValue('--tolaria-overlay-zoom-factor').trim(),
)
if (overlayZoom !== null) return overlayZoom
const computedZoom = parseZoomValue(getComputedStyle(document.documentElement).zoom)
if (computedZoom !== null) return computedZoom
const inlineZoom = parseZoomValue(document.documentElement.style.getPropertyValue('zoom'))
return inlineZoom ?? 1
}
function getViewportSize() {
const zoom = getRootZoom()
return {
height: (window.visualViewport?.height ?? window.innerHeight) / zoom,
width: (window.visualViewport?.width ?? window.innerWidth) / zoom,
zoom,
}
}
function clampToViewport(value: number, viewportSize: number): number {
const max = Math.max(CONTEXT_MENU_VIEWPORT_PADDING, viewportSize - CONTEXT_MENU_VIEWPORT_PADDING)
return Math.min(Math.max(value, CONTEXT_MENU_VIEWPORT_PADDING), max)
}
function trailingOffset(viewportSize: number, coordinate: number): number {
return Math.max(CONTEXT_MENU_VIEWPORT_PADDING, viewportSize - coordinate)
}
function spaceBefore(coordinate: number): number {
return Math.max(CONTEXT_MENU_VIEWPORT_PADDING, coordinate - CONTEXT_MENU_VIEWPORT_PADDING)
}
function spaceAfter(viewportSize: number, coordinate: number): number {
return Math.max(CONTEXT_MENU_VIEWPORT_PADDING, viewportSize - coordinate - CONTEXT_MENU_VIEWPORT_PADDING)
}
export function getContextMenuPositionStyle(point: ContextMenuPoint, options: ContextMenuPositionOptions = {}): CSSProperties {
const viewport = getViewportSize()
const x = clampToViewport(point.x / viewport.zoom, viewport.width)
const y = clampToViewport(point.y / viewport.zoom, viewport.height)
const availableAbove = spaceBefore(y)
const availableBelow = spaceAfter(viewport.height, y)
const style: CSSProperties = {
maxWidth: options.maxWidth ?? `calc(100vw - ${CONTEXT_MENU_VIEWPORT_PADDING * 2}px)`,
overflowY: 'auto',
}
if (options.minWidth !== undefined) style.minWidth = options.minWidth
if (x > viewport.width / 2) {
style.right = trailingOffset(viewport.width, x)
} else {
style.left = x
}
if (availableAbove > availableBelow) {
style.bottom = trailingOffset(viewport.height, y)
style.maxHeight = availableAbove
} else {
style.top = y
style.maxHeight = availableBelow
}
return style
}