UNPKG

tldraw

Version:

A tiny little drawing editor.

152 lines (151 loc) 5.72 kB
import { jsx } from "react/jsx-runtime"; import { getMarkRange } from "@tiptap/core"; import { Box, debounce, track, useEditor, useValue } from "@tldraw/editor"; import { useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "../../hooks/useTranslation/useTranslation.mjs"; import { rectToBox, TldrawUiContextualToolbar } from "../primitives/TldrawUiContextualToolbar.mjs"; import { DefaultRichTextToolbarContent } from "./DefaultRichTextToolbarContent.mjs"; import { LinkEditor } from "./LinkEditor.mjs"; const DefaultRichTextToolbar = track(function DefaultRichTextToolbar2({ children }) { const editor = useEditor(); const textEditor = useValue("textEditor", () => editor.getRichTextEditor(), [editor]); if (editor.getInstanceState().isCoarsePointer || !textEditor) return null; return /* @__PURE__ */ jsx(ContextualToolbarInner, { textEditor, children }); }); function ContextualToolbarInner({ textEditor, children }) { const { isEditingLink, onEditLinkStart, onEditLinkClose } = useEditingLinkBehavior(textEditor); const [currentSelection, setCurrentSelection] = useState(null); const previousSelectionBounds = useRef(void 0); const isMousingDown = useIsMousingDownOnTextEditor(textEditor); const msg = useTranslation(); const getSelectionBounds = useCallback(() => { if (isEditingLink) { return previousSelectionBounds.current; } const selection = window.getSelection(); if (!currentSelection || !selection || selection.rangeCount === 0 || selection.isCollapsed) return; const rangeBoxes = []; for (let i = 0; i < selection.rangeCount; i++) { const range = selection.getRangeAt(i); rangeBoxes.push(rectToBox(range.getBoundingClientRect())); } const bounds = Box.Common(rangeBoxes); previousSelectionBounds.current = bounds; return bounds; }, [currentSelection, isEditingLink]); useEffect(() => { const handleSelectionUpdate = ({ editor: textEditor2 }) => setCurrentSelection(textEditor2.state.selection); textEditor.on("selectionUpdate", handleSelectionUpdate); handleSelectionUpdate({ editor: textEditor }); return () => { textEditor.off("selectionUpdate", handleSelectionUpdate); }; }, [textEditor]); return /* @__PURE__ */ jsx( TldrawUiContextualToolbar, { className: "tlui-rich-text__toolbar", getSelectionBounds, isMousingDown, changeOnlyWhenYChanges: true, label: msg("tool.rich-text-toolbar-title"), children: children ? children : isEditingLink ? /* @__PURE__ */ jsx( LinkEditor, { textEditor, value: textEditor.isActive("link") ? textEditor.getAttributes("link").href : "", onClose: onEditLinkClose } ) : /* @__PURE__ */ jsx(DefaultRichTextToolbarContent, { textEditor, onEditLinkStart }) } ); } function useEditingLinkBehavior(textEditor) { const [isEditingLink, setIsEditingLink] = useState(false); useEffect(() => { if (!textEditor) { setIsEditingLink(false); return; } const handleClick = () => { const isLinkActive = textEditor.isActive("link"); setIsEditingLink(isLinkActive); }; textEditor.view.dom.addEventListener("click", handleClick); return () => { if (textEditor.isInitialized) { textEditor.view.dom.removeEventListener("click", handleClick); } }; }, [textEditor, isEditingLink]); useEffect(() => { if (!textEditor) { return; } if (textEditor.isActive("link")) { try { const { from, to } = getMarkRange( textEditor.state.doc.resolve(textEditor.state.selection.from), textEditor.schema.marks.link ); if (textEditor.state.selection.empty) { textEditor.commands.setTextSelection({ from, to }); } } catch { } } }, [textEditor, isEditingLink]); const onEditLinkStart = useCallback(() => { setIsEditingLink(true); }, []); const onEditLinkCancel = useCallback(() => { setIsEditingLink(false); }, []); const onEditLinkClose = useCallback(() => { setIsEditingLink(false); if (!textEditor) return; const from = textEditor.state.selection.from; textEditor.commands.setTextSelection({ from, to: from }); }, [textEditor]); return { isEditingLink, onEditLinkStart, onEditLinkClose, onEditLinkCancel }; } function useIsMousingDownOnTextEditor(textEditor) { const [isMousingDown, setIsMousingDown] = useState(false); useEffect(() => { if (!textEditor) return; const handlePointingStateChange = debounce(({ isPointing }) => { setIsMousingDown(isPointing); }, 16); const handlePointingDown = () => handlePointingStateChange({ isPointing: true }); const handlePointingUp = () => handlePointingStateChange({ isPointing: false }); const touchDownEvents = ["touchstart", "pointerdown", "mousedown"]; const touchUpEvents = ["touchend", "pointerup", "mouseup"]; touchDownEvents.forEach((eventName) => { textEditor.view.dom.addEventListener(eventName, handlePointingDown); }); touchUpEvents.forEach((eventName) => { document.body.addEventListener(eventName, handlePointingUp); }); return () => { touchDownEvents.forEach((eventName) => { if (textEditor.isInitialized) { textEditor.view.dom.removeEventListener(eventName, handlePointingDown); } }); touchUpEvents.forEach((eventName) => { document.body.removeEventListener(eventName, handlePointingUp); }); }; }, [textEditor]); return isMousingDown; } export { DefaultRichTextToolbar }; //# sourceMappingURL=DefaultRichTextToolbar.mjs.map