export type ViewportBox = { left: number; top: number; width: number; height: number }; export type DOMRectLike = { left: number; right: number; top: number; bottom: number; width: number; height: number; }; export type SurfacePlacement = { left: number; top: number; maxHeight: number }; export function intersectViewport(viewport: ViewportBox, host: DOMRectLike): ViewportBox { let right = Math.min(viewport.left + viewport.width, host.right); let bottom = Math.min(viewport.top + viewport.height, host.bottom); let left = Math.max(viewport.left, host.left); let top = Math.max(viewport.top, host.top); return { left, top, width: Math.max(0, right - left), height: Math.max(0, bottom - top), }; } /** Place fixed editor chrome within the pixels the browser currently exposes. */ export function placeSurface( anchor: DOMRectLike, surface: { width: number; height: number }, viewport: ViewportBox, gap = 8, prefer: "auto" | "above" = "auto", ): SurfacePlacement { let leftEdge = viewport.left + gap; let rightEdge = viewport.left + viewport.width - gap; let topEdge = viewport.top + gap; let bottomEdge = viewport.top + viewport.height - gap; let left = clamp(anchor.left, leftEdge, rightEdge - surface.width); let above = Math.max(0, anchor.top - gap - topEdge); let below = Math.max(0, bottomEdge - anchor.bottom - gap); // `above` keeps a surface off the line under a selection and flips only // when the whole surface no longer fits over it and below has more room. let useAbove = prefer === "above" ? above >= surface.height || above >= below : above > below; if (useAbove) { let top = Math.max(topEdge, anchor.top - gap - surface.height); return { left, top, maxHeight: Math.max(0, Math.min(surface.height, anchor.top - gap - top, bottomEdge - top)), }; } let top = Math.max(topEdge, Math.min(anchor.bottom + gap, bottomEdge)); return { left, top, maxHeight: Math.max(0, Math.min(surface.height, bottomEdge - top)), }; } function clamp(value: number, lower: number, upper: number): number { return Math.min(Math.max(value, lower), Math.max(lower, upper)); } /** * The part of a selection the viewport shows, or nothing once most of it has * scrolled away. A selection taller than the viewport keeps its visible slice. */ export function visibleAnchor(anchor: DOMRectLike, viewport: ViewportBox): DOMRectLike | undefined { let top = Math.max(anchor.top, viewport.top); let bottom = Math.min(anchor.bottom, viewport.top + viewport.height); let shown = bottom - top; if (shown <= 0 || shown < Math.min(anchor.height / 2, 20)) return undefined; return { ...anchor, top, bottom, height: shown }; }