UNPKG

@churchapps/apphelper-markdown

Version:

ChurchApps markdown/lexical editor components

280 lines (279 loc) 16.2 kB
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { SELECTION_CHANGE_COMMAND, FORMAT_TEXT_COMMAND, $getSelection, $isRangeSelection, $createParagraphNode, $getNodeByKey } from "lexical"; import { $wrapNodes, $isAtNodeEnd } from "@lexical/selection"; import { $getNearestNodeOfType, mergeRegister } from "@lexical/utils"; import { INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, REMOVE_LIST_COMMAND, $isListNode, ListNode } from "@lexical/list"; import { createPortal } from "react-dom"; import { $createHeadingNode, $createQuoteNode, $isHeadingNode } from "@lexical/rich-text"; import { $isCodeNode, getDefaultCodeLanguage, getCodeLanguages, $createCodeNode } from "@lexical/code"; import { Icon } from "@mui/material"; import FloatingLinkEditor from "./customLink/FloatingLinkEditor"; import { TOGGLE_CUSTOM_LINK_NODE_COMMAND, $isCustomLinkNode } from "./customLink/CustomLinkNode"; const LowPriority = 1; const supportedBlockTypes = new Set([ "paragraph", "quote", "code", "h1", "h2", "h3", "h4", "ul", "ol" ]); const blockTypeToBlockName = { code: "Code Block", h1: "Large Heading", h2: "Small Heading", h3: "Heading", h4: "Heading", h5: "Heading", ol: "Numbered List", paragraph: "Normal", quote: "Quote", ul: "Bulleted List" }; function Divider() { return _jsx("div", { className: "divider" }); } /* function positionEditorElement(editor: any, rect: any) { if (rect === null) { editor.style.opacity = "0"; editor.style.top = "-1000px"; editor.style.left = "-1000px"; } else { editor.style.opacity = "1"; editor.style.top = `${rect.top + rect.height + window.pageYOffset + 10}px`; editor.style.left = `${rect.left + window.pageXOffset - editor.offsetWidth / 2 + rect.width / 2}px`; } } */ function Select({ onChange, className, options, value }) { return (_jsxs("select", { className: className, onChange: onChange, value: value, children: [_jsx("option", { hidden: true, value: "" }), options.map((option) => (_jsx("option", { value: option, children: option }, option)))] })); } export function getSelectedNode(selection) { const anchor = selection.anchor; const focus = selection.focus; const anchorNode = selection.anchor.getNode(); const focusNode = selection.focus.getNode(); if (anchorNode === focusNode) { return anchorNode; } const isBackward = selection.isBackward(); if (isBackward) { return $isAtNodeEnd(focus) ? anchorNode : focusNode; } else { return $isAtNodeEnd(anchor) ? focusNode : anchorNode; } } function BlockOptionsDropdownList({ editor, blockType, toolbarRef, setShowBlockOptionsDropDown }) { const dropDownRef = useRef(null); useEffect(() => { const toolbar = toolbarRef.current; const dropDown = dropDownRef.current; if (toolbar !== null && dropDown !== null) { const { top, left } = toolbar.getBoundingClientRect(); dropDown.style.top = `${top + 40}px`; dropDown.style.left = `${left}px`; } }, [dropDownRef, toolbarRef]); useEffect(() => { const dropDown = dropDownRef.current; const toolbar = toolbarRef.current; if (dropDown !== null && toolbar !== null) { const handle = (event) => { const target = event.target; if (!dropDown.contains(target) && !toolbar.contains(target)) { setShowBlockOptionsDropDown(false); } }; document.addEventListener("click", handle); return () => { document.removeEventListener("click", handle); }; } }, [dropDownRef, setShowBlockOptionsDropDown, toolbarRef]); const formatParagraph = () => { if (blockType !== "paragraph") { editor.update(() => { const selection = $getSelection(); if ($isRangeSelection(selection)) { $wrapNodes(selection, () => $createParagraphNode()); } }); } setShowBlockOptionsDropDown(false); }; const formatLargeHeading = () => { if (blockType !== "h1") { editor.update(() => { const selection = $getSelection(); if ($isRangeSelection(selection)) { $wrapNodes(selection, () => $createHeadingNode("h1")); } }); } setShowBlockOptionsDropDown(false); }; const formatSmallHeading = () => { if (blockType !== "h2") { editor.update(() => { const selection = $getSelection(); if ($isRangeSelection(selection)) { $wrapNodes(selection, () => $createHeadingNode("h2")); } }); } setShowBlockOptionsDropDown(false); }; const formatHeading3 = () => { if (blockType !== "h3") { editor.update(() => { const selection = $getSelection(); if ($isRangeSelection(selection)) { $wrapNodes(selection, () => $createHeadingNode("h3")); } }); } setShowBlockOptionsDropDown(false); }; const formatHeading4 = () => { if (blockType !== "h4") { editor.update(() => { const selection = $getSelection(); if ($isRangeSelection(selection)) { $wrapNodes(selection, () => $createHeadingNode("h4")); } }); } setShowBlockOptionsDropDown(false); }; const formatBulletList = () => { if (blockType !== "ul") { editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND); } else { editor.dispatchCommand(REMOVE_LIST_COMMAND); } setShowBlockOptionsDropDown(false); }; const formatNumberedList = () => { if (blockType !== "ol") { editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND); } else { editor.dispatchCommand(REMOVE_LIST_COMMAND); } setShowBlockOptionsDropDown(false); }; const formatQuote = () => { editor.update(() => { const selection = $getSelection(); if ($isRangeSelection(selection)) { if (blockType === "quote") { $wrapNodes(selection, () => $createParagraphNode()); } else { $wrapNodes(selection, () => $createQuoteNode()); } } }); setShowBlockOptionsDropDown(false); }; const formatCodeBlock = () => { if (blockType !== "code") { editor.update(() => { const selection = $getSelection(); if ($isRangeSelection(selection)) { $wrapNodes(selection, () => $createCodeNode()); } }); } setShowBlockOptionsDropDown(false); }; return (_jsxs("div", { className: "dropdown", ref: dropDownRef, children: [_jsxs("button", { className: "item", onClick: formatParagraph, children: [_jsx("span", { className: "icon paragraph" }), _jsx("span", { className: "text", children: "Normal" }), blockType === "paragraph" && _jsx("span", { className: "active" })] }), _jsxs("button", { className: "item", onClick: formatLargeHeading, children: [_jsx("span", { className: "icon large-heading" }), _jsx("span", { className: "text", children: "Large Heading" }), blockType === "h1" && _jsx("span", { className: "active" })] }), _jsxs("button", { className: "item", onClick: formatSmallHeading, children: [_jsx("span", { className: "icon small-heading" }), _jsx("span", { className: "text", children: "Small Heading" }), blockType === "h2" && _jsx("span", { className: "active" })] }), _jsxs("button", { className: "item", onClick: formatHeading3, children: [_jsx("span", { className: "icon h3" }), _jsx("span", { className: "text", children: "Heading 3" }), blockType === "h3" && _jsx("span", { className: "active" })] }), _jsxs("button", { className: "item", onClick: formatHeading4, children: [_jsx("span", { className: "icon h4" }), _jsx("span", { className: "text", children: "Heading 4" }), blockType === "h4" && _jsx("span", { className: "active" })] }), _jsxs("button", { className: "item", onClick: formatBulletList, children: [_jsx("span", { className: "icon bullet-list" }), _jsx("span", { className: "text", children: "Bullet List" }), blockType === "ul" && _jsx("span", { className: "active" })] }), _jsxs("button", { className: "item", onClick: formatNumberedList, children: [_jsx("span", { className: "icon numbered-list" }), _jsx("span", { className: "text", children: "Numbered List" }), blockType === "ol" && _jsx("span", { className: "active" })] }), _jsxs("button", { className: "item", onClick: formatQuote, children: [_jsx("span", { className: "icon quote" }), _jsx("span", { className: "text", children: "Quote" }), blockType === "quote" && _jsx("span", { className: "active" })] }), _jsxs("button", { className: "item", onClick: formatCodeBlock, children: [_jsx("span", { className: "icon code" }), _jsx("span", { className: "text", children: "Code" }), blockType === "code" && _jsx("span", { className: "active" })] })] })); } export function ToolbarPlugin(props) { const [editor] = useLexicalComposerContext(); const toolbarRef = useRef(null); const [blockType, setBlockType] = useState("paragraph"); const [selectedElementKey, setSelectedElementKey] = useState(null); const [showBlockOptionsDropDown, setShowBlockOptionsDropDown] = useState(false); const [codeLanguage, setCodeLanguage] = useState(""); const [isLink, setIsLink] = useState(false); const [isBold, setIsBold] = useState(false); const [isItalic, setIsItalic] = useState(false); const [isUnderline, setIsUnderline] = useState(false); const [isCode, setIsCode] = useState(false); //const [isStrikethrough, setIsStrikethrough] = useState(false); const [linkUrl, setLinkUrl] = useState("https://"); const [targetAttribute, setTargetAttribute] = useState("_self"); const [classNamesList, setClassNamesList] = useState(["primary"]); const updateToolbar = useCallback(() => { const selection = $getSelection(); if ($isRangeSelection(selection)) { const anchorNode = selection.anchor.getNode(); const element = anchorNode.getKey() === "root" ? anchorNode : anchorNode.getTopLevelElementOrThrow(); const elementKey = element.getKey(); const elementDOM = editor.getElementByKey(elementKey); if (elementDOM !== null) { setSelectedElementKey(elementKey); if ($isListNode(element)) { const parentList = $getNearestNodeOfType(anchorNode, ListNode); const type = parentList ? parentList.getTag() : element.getTag(); setBlockType(type); } else { const type = $isHeadingNode(element) ? element.getTag() : element.getType(); setBlockType(type); if ($isCodeNode(element)) { setCodeLanguage(element.getLanguage() || getDefaultCodeLanguage()); } } } // Update text format setIsBold(selection.hasFormat("bold")); setIsItalic(selection.hasFormat("italic")); setIsUnderline(selection.hasFormat("underline")); setIsCode(selection.hasFormat("code")); //setIsStrikethrough(selection.hasFormat("strikethrough")); // Update links const node = getSelectedNode(selection); const parent = node.getParent(); if ($isCustomLinkNode(parent) || $isCustomLinkNode(node)) { setIsLink(true); } else { setIsLink(false); } } }, [editor]); useEffect(() => mergeRegister(editor.registerUpdateListener(({ editorState }) => { editorState.read(() => { updateToolbar(); }); }), editor.registerCommand(SELECTION_CHANGE_COMMAND, (_payload, newEditor) => { updateToolbar(); return false; }, LowPriority)), [editor, updateToolbar]); const codeLanguges = useMemo(() => getCodeLanguages(), []); const onCodeLanguageSelect = useCallback((e) => { editor.update(() => { if (selectedElementKey !== null) { const node = $getNodeByKey(selectedElementKey); if ($isCodeNode(node)) { node.setLanguage(e.target.value); } } }); }, [editor, selectedElementKey]); const insertLink = useCallback(() => { editor.dispatchCommand(TOGGLE_CUSTOM_LINK_NODE_COMMAND, { url: linkUrl, classNames: classNamesList, target: targetAttribute }); }, [editor, isLink]); const editorEl = (typeof window !== "undefined") && window.document.getElementById("elementEditDialog"); const portalKey = editorEl ? editorEl : document.body; return (_jsxs("div", { className: "toolbar", ref: toolbarRef, children: [supportedBlockTypes.has(blockType) && (_jsxs(_Fragment, { children: [_jsxs("button", { className: "toolbar-item block-controls", onClick: () => setShowBlockOptionsDropDown(!showBlockOptionsDropDown), "aria-label": "Formatting Options", children: [_jsx("span", { className: "icon block-type " + blockType }), _jsx("span", { className: "text", children: blockTypeToBlockName[blockType] }), _jsx("i", { className: "chevron-down" })] }), showBlockOptionsDropDown && createPortal(_jsx(BlockOptionsDropdownList, { editor: editor, blockType: blockType, toolbarRef: toolbarRef, setShowBlockOptionsDropDown: setShowBlockOptionsDropDown }), portalKey), _jsx(Divider, {})] })), blockType === "code" ? (_jsxs(_Fragment, { children: [_jsx(Select, { className: "toolbar-item code-language", onChange: onCodeLanguageSelect, options: codeLanguges, value: codeLanguage }), _jsx("i", { className: "chevron-down inside" })] })) : (_jsxs(_Fragment, { children: [_jsx("button", { onClick: () => { editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold"); }, className: "toolbar-item spaced " + (isBold ? "active" : ""), "aria-label": "Format Bold", children: _jsx("i", { className: "format bold" }) }), _jsx("button", { onClick: () => { editor.dispatchCommand(FORMAT_TEXT_COMMAND, "italic"); }, className: "toolbar-item spaced " + (isItalic ? "active" : ""), "aria-label": "Format Italics", children: _jsx("i", { className: "format italic" }) }), _jsx("button", { onClick: () => { editor.dispatchCommand(FORMAT_TEXT_COMMAND, "underline"); }, className: "toolbar-item spaced " + (isUnderline ? "active" : ""), "aria-label": "Format Underline", children: _jsx("i", { className: "format underline" }) }), _jsx("button", { onClick: insertLink, className: "toolbar-item spaced " + (isLink ? "active" : ""), "aria-label": "Insert Link", children: _jsx("i", { className: "format link" }) }), _jsx("button", { onClick: () => { editor.dispatchCommand(FORMAT_TEXT_COMMAND, "code"); }, className: "toolbar-item spaced " + (isCode ? "active" : ""), "aria-label": "Format Code", children: _jsx("i", { className: "format code" }) }), isLink && createPortal(_jsx(FloatingLinkEditor, { selectedElementKey: selectedElementKey, linkUrl: linkUrl, setLinkUrl: setLinkUrl, classNamesList: classNamesList, setClassNamesList: setClassNamesList, targetAttribute: targetAttribute, setTargetAttribute: setTargetAttribute }), portalKey)] })), _jsx(Divider, {}), _jsx("button", { onClick: () => { props.goFullScreen(); }, className: "toolbar-item spaced", "aria-label": "Full Screen", children: _jsx(Icon, { children: "fullscreen" }) })] })); }