@churchapps/apphelper-markdown
Version:
ChurchApps markdown/lexical editor components
188 lines (187 loc) • 9.77 kB
JavaScript
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)`;
}