|
|
@@ -39,8 +39,8 @@ export function extractMentionQuery(setMentionStartOffset: (offset: number | nul
|
|
|
|
|
|
export function updatePopoverPosition(
|
|
|
editor: { getRootElement: () => HTMLElement | null },
|
|
|
- leftRef: React.MutableRefObject<number | null>,
|
|
|
- setPosition: (pos: { top: number; left: number }) => void,
|
|
|
+ leftRef: React.MutableRefObject<{ left: number; width: number } | null>,
|
|
|
+ setPosition: (pos: { top: number; left: number; placement: "top" | "bottom" }) => void,
|
|
|
) {
|
|
|
const root = editor.getRootElement()
|
|
|
if (!root) return
|
|
|
@@ -54,32 +54,54 @@ export function updatePopoverPosition(
|
|
|
if (!root.contains(range.startContainer)) return
|
|
|
|
|
|
const rect = range.getBoundingClientRect()
|
|
|
- const rootRect = root.getBoundingClientRect()
|
|
|
const gap = 8
|
|
|
|
|
|
+ const viewportHeight = window.innerHeight
|
|
|
+ const popover = root.ownerDocument.querySelector<HTMLElement>("[data-mention-popover]")
|
|
|
+ const popoverRect = popover ? popover.getBoundingClientRect() : null
|
|
|
+ const rawHeight = popoverRect && popoverRect.height > 0 ? Math.ceil(popoverRect.height) : 280
|
|
|
+ const desiredHeight = Math.min(rawHeight, viewportHeight - gap * 2)
|
|
|
+ const below = viewportHeight - rect.bottom - gap
|
|
|
+ const above = rect.top - gap
|
|
|
+ const placement =
|
|
|
+ above >= desiredHeight ? "top" : below >= desiredHeight ? "bottom" : above >= below ? "top" : "bottom"
|
|
|
+
|
|
|
const viewportWidth = window.innerWidth
|
|
|
- const estimatedWidth = 500
|
|
|
+ const rawWidth = popoverRect && popoverRect.width > 0 ? Math.ceil(popoverRect.width) : 500
|
|
|
+ const desiredWidth = Math.min(rawWidth, viewportWidth - gap * 2)
|
|
|
const minLeft = window.scrollX + gap
|
|
|
- const maxLeft = window.scrollX + viewportWidth - estimatedWidth - gap
|
|
|
+ const maxLeft = window.scrollX + viewportWidth - desiredWidth - gap
|
|
|
|
|
|
- let left = leftRef.current
|
|
|
- if (left === null) {
|
|
|
- left = rect.left + window.scrollX
|
|
|
- }
|
|
|
- if (maxLeft <= minLeft) {
|
|
|
- left = minLeft
|
|
|
- }
|
|
|
- if (left < minLeft) {
|
|
|
- left = minLeft
|
|
|
- }
|
|
|
- if (left > maxLeft) {
|
|
|
- left = maxLeft
|
|
|
+ const clamp = (value: number) => {
|
|
|
+ if (maxLeft <= minLeft) return minLeft
|
|
|
+ if (value < minLeft) return minLeft
|
|
|
+ if (value > maxLeft) return maxLeft
|
|
|
+ return value
|
|
|
}
|
|
|
|
|
|
- leftRef.current = left
|
|
|
+ const anchor = rect.left + window.scrollX
|
|
|
+ const stored = leftRef.current
|
|
|
+ const shouldReanchor = stored === null || Math.abs(stored.width - desiredWidth) > 8
|
|
|
+ const openedLeft = (() => {
|
|
|
+ const viewportRight = window.scrollX + viewportWidth - gap
|
|
|
+ const start = anchor
|
|
|
+ const end = anchor - desiredWidth
|
|
|
+
|
|
|
+ if (start + desiredWidth <= viewportRight) return start
|
|
|
+ if (end >= minLeft) return end
|
|
|
+ return clamp(start)
|
|
|
+ })()
|
|
|
+
|
|
|
+ const preferred = shouldReanchor ? openedLeft : stored.left
|
|
|
+ const left = clamp(preferred)
|
|
|
+
|
|
|
+ leftRef.current = { left, width: desiredWidth }
|
|
|
+
|
|
|
+ const top = placement === "top" ? rect.top + window.scrollY - gap : rect.bottom + window.scrollY + gap
|
|
|
|
|
|
setPosition({
|
|
|
- top: rootRect.top + window.scrollY - gap,
|
|
|
+ top,
|
|
|
left,
|
|
|
+ placement,
|
|
|
})
|
|
|
}
|