UNPKG

@churchapps/apphelper-markdown

Version:

ChurchApps markdown/lexical editor components

188 lines (187 loc) 9.77 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; import { mergeRegister } from "@lexical/utils"; import { $getSelection, $isRangeSelection, FORMAT_TEXT_COMMAND, SELECTION_CHANGE_COMMAND, COMMAND_PRIORITY_LOW } from "lexical"; import { useCallback, useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { Box, IconButton } from "@mui/material"; import { styled } from "@mui/material/styles"; import { FormatBold, FormatItalic, FormatUnderlined, StrikethroughS, Code, Superscript, Subscript } from "@mui/icons-material"; const FloatingDivContainer = styled(Box)({ display: "flex", background: "#fff", padding: 4, verticalAlign: "middle", position: "absolute", top: 0, left: 0, zIndex: 1400, opacity: 0, backgroundColor: "#fff", boxShadow: "0px 5px 10px rgba(0, 0, 0, 0.3)", borderRadius: 8, transition: "opacity 0.5s", height: 35, willChange: "transform" }); function TextFormatFloatingToolbar({ editor, anchorElem, isBold, isItalic, isUnderline, isCode, isStrikethrough, isSubscript, isSuperscript }) { const popupCharStylesEditorRef = useRef(null); const updateTextFormatFloatingToolbar = useCallback(() => { const selection = $getSelection(); const popupCharStylesEditorElem = popupCharStylesEditorRef.current; const nativeSelection = window.getSelection(); if (popupCharStylesEditorElem === null) { return; } const rootElement = editor.getRootElement(); if (selection !== null && nativeSelection !== null && !nativeSelection.isCollapsed && rootElement !== null && rootElement.contains(nativeSelection.anchorNode)) { const rangeRect = getDOMRangeRect(nativeSelection, rootElement); setFloatingElemPosition(rangeRect, popupCharStylesEditorElem, anchorElem); } }, [editor, anchorElem]); useEffect(() => { const scrollerElem = anchorElem.parentElement; const update = () => { editor.getEditorState().read(() => { updateTextFormatFloatingToolbar(); }); }; window.addEventListener("resize", update); if (scrollerElem) { scrollerElem.addEventListener("scroll", update); } return () => { window.removeEventListener("resize", update); if (scrollerElem) { scrollerElem.removeEventListener("scroll", update); } }; }, [editor, updateTextFormatFloatingToolbar, anchorElem]); useEffect(() => { editor.getEditorState().read(() => { updateTextFormatFloatingToolbar(); }); return mergeRegister(editor.registerUpdateListener(({ editorState }) => { editorState.read(() => { updateTextFormatFloatingToolbar(); }); }), editor.registerCommand(SELECTION_CHANGE_COMMAND, () => { updateTextFormatFloatingToolbar(); return false; }, COMMAND_PRIORITY_LOW)); }, [editor, updateTextFormatFloatingToolbar]); return (_jsxs(FloatingDivContainer, { ref: popupCharStylesEditorRef, children: [_jsx(IconButton, { onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold"), className: isBold ? "active" : "", size: "small", "aria-label": "Format text as bold", children: _jsx(FormatBold, { fontSize: "small" }) }), _jsx(IconButton, { onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "italic"), className: isItalic ? "active" : "", size: "small", "aria-label": "Format text as italic", children: _jsx(FormatItalic, { fontSize: "small" }) }), _jsx(IconButton, { onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "underline"), className: isUnderline ? "active" : "", size: "small", "aria-label": "Format text as underline", children: _jsx(FormatUnderlined, { fontSize: "small" }) }), _jsx(IconButton, { onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "strikethrough"), className: isStrikethrough ? "active" : "", size: "small", "aria-label": "Format text with strikethrough", children: _jsx(StrikethroughS, { fontSize: "small" }) }), _jsx(IconButton, { onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "superscript"), className: isSuperscript ? "active" : "", size: "small", "aria-label": "Format text as superscript", children: _jsx(Superscript, { fontSize: "small" }) }), _jsx(IconButton, { onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "subscript"), className: isSubscript ? "active" : "", size: "small", "aria-label": "Format text as subscript", children: _jsx(Subscript, { fontSize: "small" }) }), _jsx(IconButton, { onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "code"), className: isCode ? "active" : "", size: "small", "aria-label": "Insert code block", children: _jsx(Code, { fontSize: "small" }) })] })); } function useFloatingTextFormatToolbar(editor, anchorElem) { const [isText, setIsText] = useState(false); const [isBold, setIsBold] = useState(false); const [isItalic, setIsItalic] = useState(false); const [isUnderline, setIsUnderline] = useState(false); const [isStrikethrough, setIsStrikethrough] = useState(false); const [isSubscript, setIsSubscript] = useState(false); const [isSuperscript, setIsSuperscript] = useState(false); const [isCode, setIsCode] = useState(false); const updatePopup = useCallback(() => { editor.getEditorState().read(() => { // Don't update during IME composition (for CJK languages) if (editor.isComposing()) { return; } const selection = $getSelection(); const nativeSelection = window.getSelection(); const rootElement = editor.getRootElement(); if (nativeSelection !== null && (!$isRangeSelection(selection) || rootElement === null || !rootElement.contains(nativeSelection.anchorNode))) { setIsText(false); return; } if (!$isRangeSelection(selection)) { return; } setIsBold(selection.hasFormat("bold")); setIsItalic(selection.hasFormat("italic")); setIsUnderline(selection.hasFormat("underline")); setIsStrikethrough(selection.hasFormat("strikethrough")); setIsSubscript(selection.hasFormat("subscript")); setIsSuperscript(selection.hasFormat("superscript")); setIsCode(selection.hasFormat("code")); // Check for actual text content (ignore whitespace-only selections) const textContent = selection.getTextContent().replace(/\n/g, ""); if (!$isRangeSelection(selection) || textContent === "") { setIsText(false); } else { setIsText(true); } }); }, [editor]); useEffect(() => { document.addEventListener("selectionchange", updatePopup); return () => { document.removeEventListener("selectionchange", updatePopup); }; }, [updatePopup]); useEffect(() => { return mergeRegister(editor.registerUpdateListener(() => { updatePopup(); }), editor.registerRootListener(() => { if (editor.getRootElement() === null) { setIsText(false); } })); }, [editor, updatePopup]); if (!isText) { return null; } return createPortal(_jsx(TextFormatFloatingToolbar, { editor: editor, anchorElem: anchorElem, isBold: isBold, isItalic: isItalic, isCode: isCode, isStrikethrough: isStrikethrough, isSubscript: isSubscript, isSuperscript: isSuperscript, isUnderline: isUnderline }), anchorElem); } export default function FloatingTextFormatToolbarPlugin({ anchorElem }) { const [editor] = useLexicalComposerContext(); return useFloatingTextFormatToolbar(editor, anchorElem); } function getDOMRangeRect(nativeSelection, rootElement) { const domRange = nativeSelection.getRangeAt(0); let rect; if (nativeSelection.anchorNode === rootElement) { let inner = rootElement; while (inner.firstElementChild != null) { inner = inner.firstElementChild; } rect = inner.getBoundingClientRect(); } else { rect = domRange.getBoundingClientRect(); } return rect; } const VERTICAL_GAP = 10; const HORIZONTAL_OFFSET = 5; function setFloatingElemPosition(targetRect, floatingElem, anchorElem, verticalGap = VERTICAL_GAP, horizontalOffset = HORIZONTAL_OFFSET) { const scrollerElem = anchorElem.parentElement; if (targetRect === null || !scrollerElem) { floatingElem.style.opacity = "0"; floatingElem.style.transform = "translate(-10000px, -10000px)"; return; } const floatingElemRect = floatingElem.getBoundingClientRect(); const anchorElementRect = anchorElem.getBoundingClientRect(); const editorScrollerRect = scrollerElem.getBoundingClientRect(); let top = targetRect.top - floatingElemRect.height - verticalGap; let left = targetRect.left - horizontalOffset; if (top < editorScrollerRect.top) { top = targetRect.bottom + verticalGap; } if (left + floatingElemRect.width > editorScrollerRect.right) { left = editorScrollerRect.right - floatingElemRect.width - horizontalOffset; } top -= anchorElementRect.top; left -= anchorElementRect.left; floatingElem.style.opacity = "1"; floatingElem.style.transform = `translate(${left}px, ${top}px)`; }