UNPKG

@assistant-ui/react

Version:

Open-source TypeScript/React library for building production-grade AI chat experiences

203 lines (202 loc) • 6.27 kB
"use client"; import { Primitive as Primitive$1 } from "../../utils/Primitive.js"; import { useThreadRootElementRef } from "../thread/ThreadRootElementContext.js"; import { getSelectionMessageId } from "../../utils/getSelectionMessageId.js"; import { c } from "@assistant-ui/tap/react-shim/compiler-runtime"; import { createContext, forwardRef, useContext, useEffect, useRef, useState } from "@assistant-ui/tap/react-shim"; import { jsx } from "react/jsx-runtime"; import { composeEventHandlers } from "radix-ui/internal"; import { createPortal } from "react-dom"; // src/primitives/selectionToolbar/SelectionToolbarRoot.tsx const SelectionToolbarContext = createContext(null); const useSelectionToolbarInfo = () => { return useContext(SelectionToolbarContext); }; /** * A floating toolbar that appears when text is selected within a message. * * Listens for browser selection changes, validates that the selection is * within a single message, and renders a positioned portal near the * selection. Prevents mousedown from clearing the selection. * * @example * ```tsx * <SelectionToolbarPrimitive.Root> * <SelectionToolbarPrimitive.Quote>Quote</SelectionToolbarPrimitive.Quote> * </SelectionToolbarPrimitive.Root> * ``` */ const SelectionToolbarPrimitiveRoot = forwardRef((t0, forwardedRef) => { const $ = c(21); let onMouseDown; let props; let style; if ($[0] !== t0) { ({onMouseDown, style, ...props} = t0); $[0] = t0; $[1] = onMouseDown; $[2] = props; $[3] = style; } else { onMouseDown = $[1]; props = $[2]; style = $[3]; } const [info, setInfo] = useState(null); const threadRootRef = useThreadRootElementRef(); const warnedAboutMissingThreadRootRef = useRef(false); let t1; let t2; if ($[4] !== threadRootRef) { t1 = () => { let pendingFrame = null; let isMouseDragging = false; const checkSelection = () => { if (pendingFrame !== null) cancelAnimationFrame(pendingFrame); pendingFrame = requestAnimationFrame(() => { pendingFrame = null; const sel = window.getSelection(); if (!sel || sel.isCollapsed) { setInfo(null); return; } const text = sel.toString().trim(); if (!text) { setInfo(null); return; } if (threadRootRef && !threadRootRef.current) { if (process.env.NODE_ENV !== "production" && !warnedAboutMissingThreadRootRef.current) { warnedAboutMissingThreadRootRef.current = true; console.warn("[SelectionToolbarPrimitive.Root] ThreadPrimitive.Root did not provide a DOM element, so the selection cannot be scoped to its thread. Ensure a custom root child forwards its ref."); } } const messageId = getSelectionMessageId(sel, threadRootRef?.current); if (!messageId) { setInfo(null); return; } const rect = sel.getRangeAt(0).getBoundingClientRect(); setInfo({ text, messageId, rect }); }); }; const handleSelectionChange = () => { const sel_0 = window.getSelection(); if (!sel_0 || sel_0.isCollapsed) { if (pendingFrame !== null) { cancelAnimationFrame(pendingFrame); pendingFrame = null; } setInfo(null); return; } if (!isMouseDragging) checkSelection(); }; const handleScroll = () => { if (pendingFrame !== null) { cancelAnimationFrame(pendingFrame); pendingFrame = null; } setInfo(null); }; const handleMouseDown = () => { isMouseDragging = true; }; const handleMouseUp = () => { isMouseDragging = false; checkSelection(); }; const handleMouseCancel = () => { isMouseDragging = false; }; const handleWindowBlur = () => { if (isMouseDragging) handleMouseUp(); }; document.addEventListener("mousedown", handleMouseDown, true); document.addEventListener("mouseup", handleMouseUp, true); document.addEventListener("dragend", handleMouseUp, true); document.addEventListener("contextmenu", handleMouseCancel, true); window.addEventListener("blur", handleWindowBlur); document.addEventListener("selectionchange", handleSelectionChange); document.addEventListener("scroll", handleScroll, true); return () => { if (pendingFrame !== null) cancelAnimationFrame(pendingFrame); document.removeEventListener("mousedown", handleMouseDown, true); document.removeEventListener("mouseup", handleMouseUp, true); document.removeEventListener("dragend", handleMouseUp, true); document.removeEventListener("contextmenu", handleMouseCancel, true); window.removeEventListener("blur", handleWindowBlur); document.removeEventListener("selectionchange", handleSelectionChange); document.removeEventListener("scroll", handleScroll, true); }; }; t2 = [threadRootRef]; $[4] = threadRootRef; $[5] = t1; $[6] = t2; } else { t1 = $[5]; t2 = $[6]; } useEffect(t1, t2); if (!info) return null; const t3 = `${info.rect.top - 8}px`; const t4 = `${info.rect.left + info.rect.width / 2}px`; let t5; if ($[7] !== style || $[8] !== t3 || $[9] !== t4) { t5 = { position: "fixed", top: t3, left: t4, transform: "translate(-50%, -100%)", zIndex: 50, ...style }; $[7] = style; $[8] = t3; $[9] = t4; $[10] = t5; } else t5 = $[10]; const positionStyle = t5; let t6; if ($[11] !== onMouseDown) { t6 = composeEventHandlers(onMouseDown, _temp); $[11] = onMouseDown; $[12] = t6; } else t6 = $[12]; let t7; if ($[13] !== forwardedRef || $[14] !== positionStyle || $[15] !== props || $[16] !== t6) { t7 = /* @__PURE__ */ jsx(Primitive$1.div, { ...props, ref: forwardedRef, style: positionStyle, onMouseDown: t6 }); $[13] = forwardedRef; $[14] = positionStyle; $[15] = props; $[16] = t6; $[17] = t7; } else t7 = $[17]; let t8; if ($[18] !== info || $[19] !== t7) { t8 = createPortal(/* @__PURE__ */ jsx(SelectionToolbarContext.Provider, { value: info, children: t7 }), document.body); $[18] = info; $[19] = t7; $[20] = t8; } else t8 = $[20]; return t8; }); SelectionToolbarPrimitiveRoot.displayName = "SelectionToolbarPrimitive.Root"; function _temp(e) { e.preventDefault(); } export { SelectionToolbarPrimitiveRoot, useSelectionToolbarInfo }; //# sourceMappingURL=SelectionToolbarRoot.js.map