casualroom/apps/fenghuo/web/hooks/use-cursor-visibility.ts

117 lines
3.1 KiB
TypeScript
Executable File

"use client"
import * as React from "react"
import type { Editor } from "@tiptap/react"
import { useWindowSize } from "@/hooks/use-window-size"
/**
* Interface defining required parameters for the cursor visibility hook
*/
export interface CursorVisibilityOptions {
/**
* The TipTap editor instance
*/
editor: Editor | null
/**
* Reference to the toolbar element that may obscure the cursor
*/
overlayHeight?: number
/**
* Reference to the element to track for cursor visibility
*/
elementRef?: React.RefObject<HTMLElement> | null
}
/**
* Simplified DOMRect type containing only the essential positioning properties
*/
export type RectState = Pick<DOMRect, "x" | "y" | "width" | "height">
/**
* Custom hook that ensures the cursor remains visible when typing in a TipTap editor.
* Automatically scrolls the window when the cursor would be hidden by the toolbar.
*
* This is particularly useful for long-form content editing where the cursor
* might move out of the visible area as the user types.
*
* @param options Configuration options for cursor visibility behavior
* @returns void
*/
export function useCursorVisibility({
editor,
overlayHeight = 0,
elementRef = null,
}: CursorVisibilityOptions) {
const { height: windowHeight } = useWindowSize()
const [rect, setRect] = React.useState<RectState>({
x: 0,
y: 0,
width: 0,
height: 0,
})
const updateRect = React.useCallback(() => {
const element = elementRef?.current ?? document.body
const { x, y, width, height } = element.getBoundingClientRect()
setRect({ x, y, width, height })
}, [elementRef])
React.useEffect(() => {
const element = elementRef?.current ?? document.body
updateRect()
const resizeObserver = new ResizeObserver(() => {
window.requestAnimationFrame(updateRect)
})
resizeObserver.observe(element)
window.addEventListener("scroll", updateRect, { passive: true })
return () => {
resizeObserver.disconnect()
window.removeEventListener("scroll", updateRect)
}
}, [elementRef, updateRect])
React.useEffect(() => {
const ensureCursorVisibility = () => {
if (!editor) return
const { state, view } = editor
if (!view.hasFocus()) return
// Get current cursor position coordinates
const { from } = state.selection
const cursorCoords = view.coordsAtPos(from)
if (windowHeight < rect.height) {
if (cursorCoords) {
// Check if there's enough space between cursor and bottom of window
const availableSpace =
windowHeight - cursorCoords.top - overlayHeight > 0
// If not enough space, scroll to position cursor in the middle of viewport
if (!availableSpace) {
const targetScrollY =
// TODO: Needed?
// window.scrollY + (cursorCoords.top - windowHeight / 2)
cursorCoords.top - windowHeight / 2
window.scrollTo({
top: targetScrollY,
behavior: "smooth",
})
}
}
}
}
ensureCursorVisibility()
}, [editor, overlayHeight, windowHeight, rect.height])
return rect
}