import { useCallback, useEffect, useRef, useState } from "react" import type { ComponentProps } from "react" import { createPortal } from "react-dom" import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext" import { cn } from "@workspace/ui/lib/utils" import { LexicalActionTree } from "./actions-view" import { useLexicalActions } from "./actions-context" import { LexicalActionRuntimeProvider } from "./toolbar" type BubblePosition = { left: number top: number placement: "above" | "below" } export type LexicalBubbleToolbarProps = ComponentProps<"div"> export function LexicalBubbleToolbar({ className, children, ...props }: LexicalBubbleToolbarProps) { const actions = useLexicalActions("bubble") if (actions.length === 0 && children == null) return null return ( {children} ) } function LexicalBubbleToolbarContent({ actions, className, children, ...props }: LexicalBubbleToolbarProps & { actions: ReturnType }) { const [editor] = useLexicalComposerContext() const [position, setPosition] = useState(null) const frameRef = useRef(null) const updatePosition = useCallback(() => { if (frameRef.current !== null) { cancelAnimationFrame(frameRef.current) } frameRef.current = requestAnimationFrame(() => { frameRef.current = null const rootElement = editor.getRootElement() const selection = window.getSelection() if ( !rootElement || !selection || selection.rangeCount === 0 || selection.isCollapsed || !selection.anchorNode || !rootElement.contains(selection.anchorNode) ) { setPosition(null) return } const range = selection.getRangeAt(0) if (typeof range.getBoundingClientRect !== "function") { setPosition(null) return } const rect = range.getBoundingClientRect() if (rect.width === 0 && rect.height === 0) { setPosition(null) return } const placement = rect.top >= 72 ? "above" : "below" setPosition({ left: Math.min( Math.max(rect.left + rect.width / 2, 24), window.innerWidth - 24 ), top: placement === "above" ? rect.top - 8 : rect.bottom + 8, placement, }) }) }, [editor]) useEffect(() => { const unregisterUpdate = editor.registerUpdateListener(updatePosition) document.addEventListener("selectionchange", updatePosition) window.addEventListener("resize", updatePosition) window.addEventListener("scroll", updatePosition, true) return () => { unregisterUpdate() document.removeEventListener("selectionchange", updatePosition) window.removeEventListener("resize", updatePosition) window.removeEventListener("scroll", updatePosition, true) if (frameRef.current !== null) { cancelAnimationFrame(frameRef.current) } } }, [editor, updatePosition]) if (!position || typeof document === "undefined") return null return createPortal(
{children}
, document.body ) }