UNPKG

@churchapps/apphelper-markdown

Version:

ChurchApps markdown/lexical editor components

280 lines (279 loc) 16.9 kB
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import { useCallback, useEffect, useRef, useState } from "react"; import { $getSelection, $isRangeSelection, SELECTION_CHANGE_COMMAND, COMMAND_PRIORITY_LOW } from "lexical"; import { $isLinkNode } from "@lexical/link"; import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; import { mergeRegister } from "@lexical/utils"; import { createPortal } from "react-dom"; import { Box, TextField, FormControl, InputLabel, Select, MenuItem, Button, Checkbox, FormControlLabel, Typography, Divider } from "@mui/material"; import { Check, Link as LinkIcon } from "@mui/icons-material"; import { TOGGLE_CUSTOM_LINK_NODE_COMMAND } from "./customLink/CustomLinkNode"; export default function FloatingLinkEditorPlugin({ anchorElem, isLinkEditMode, setIsLinkEditMode }) { const [editor] = useLexicalComposerContext(); const [linkUrl, setLinkUrl] = useState("https://"); const [classNamesList, setClassNamesList] = useState(["", "btn-primary", "btn-medium"]); const [targetAttribute, setTargetAttribute] = useState("_self"); const [isEditingLink, setIsEditingLink] = useState(false); const linkEditorRef = useRef(null); const currentLinkNodeKey = useRef(null); const updateLinkEditor = useCallback(() => { // Don't update if we're already in link edit mode if (isLinkEditMode) { return; } const selection = $getSelection(); if ($isRangeSelection(selection)) { const node = selection.anchor.getNode(); const parent = node.getParent(); if ($isLinkNode(parent)) { setLinkUrl(parent.getURL()); setIsEditingLink(true); } else if ($isLinkNode(node)) { setLinkUrl(node.getURL()); setIsEditingLink(true); } else { setIsEditingLink(false); setLinkUrl("https://"); } } }, [isLinkEditMode]); useEffect(() => { const handleDoubleClick = (e) => { const target = e.target; if (target.tagName === "A" || target.closest("a")) { e.preventDefault(); const linkElement = (target.tagName === "A" ? target : target.closest("a")); editor.update(() => { // Find the link node from the DOM element const linkNodes = editor._editorState._nodeMap; let foundLinkNode = null; linkNodes.forEach((node) => { if ($isLinkNode(node)) { const domElement = editor.getElementByKey(node.__key); if (domElement === linkElement) { foundLinkNode = node; } } }); if (foundLinkNode) { // Store the link node key for later use currentLinkNodeKey.current = foundLinkNode.__key; // Select the entire link node foundLinkNode.select(); // Get link attributes const url = foundLinkNode.getURL(); const target = foundLinkNode.getTarget(); // Extract class names from the DOM element const classes = Array.from(linkElement.classList); // Determine the proper class list structure let newClassList = ["", "btn-primary", "btn-medium"]; if (classes.length > 0) { // Check if it's a button by looking for btn class or btn-* classes const hasBtn = classes.includes("btn"); const hasBtnBlock = classes.includes("btn-block"); // Look for appearance class (first position) let appearanceClass = ""; if (hasBtn && hasBtnBlock) { appearanceClass = "btn btn-block"; } else if (hasBtn) { appearanceClass = "btn"; } // Look for variant class (second position) const variantClass = classes.find(c => c.startsWith("btn-") && c !== "btn-block" && !c.match(/btn-(small|medium|large|xl|2x|3x|4x)$/i)) || "btn-primary"; // Look for size class (third position) const sizeClass = classes.find(c => c.match(/btn-(small|medium|large|xl|2x|3x|4x)$/i)) || "btn-medium"; newClassList = [ appearanceClass, variantClass, sizeClass ]; } setLinkUrl(url || "https://"); setTargetAttribute(target || "_self"); setClassNamesList(newClassList); setIsLinkEditMode(true); } }); } }; const rootElement = editor.getRootElement(); if (rootElement) { rootElement.addEventListener("dblclick", handleDoubleClick); } return mergeRegister(editor.registerUpdateListener(({ editorState }) => { editorState.read(() => { updateLinkEditor(); }); }), editor.registerCommand(SELECTION_CHANGE_COMMAND, () => { updateLinkEditor(); return false; }, COMMAND_PRIORITY_LOW), () => { if (rootElement) { rootElement.removeEventListener("dblclick", handleDoubleClick); } }); }, [editor, updateLinkEditor, setIsLinkEditMode, setLinkUrl, setTargetAttribute, setClassNamesList]); const handleLinkSubmit = () => { const appearance = classNamesList[0]; const classes = []; if (appearance && appearance.length > 0) { classes.push(appearance); classes.push(classNamesList[1]); classes.push(classNamesList[2]); } // Update the specific link node directly using its stored key if (currentLinkNodeKey.current) { editor.update(() => { const lexicalNode = editor._editorState._nodeMap.get(currentLinkNodeKey.current); if (lexicalNode && $isLinkNode(lexicalNode)) { // Get a writable version of the node const writableNode = lexicalNode.getWritable(); // Update the node's properties using the writable node writableNode.__url = linkUrl; writableNode.__target = targetAttribute; writableNode.__classNames = classes; // Manually update the DOM element since updateDOM returns false const domElement = editor.getElementByKey(currentLinkNodeKey.current); if (domElement && domElement instanceof HTMLAnchorElement) { domElement.href = linkUrl; domElement.target = targetAttribute; domElement.className = classes.join(" "); } } }); } else { // Creating a new link - dispatch the command to wrap selected text editor.dispatchCommand(TOGGLE_CUSTOM_LINK_NODE_COMMAND, { url: linkUrl, classNames: classes, target: targetAttribute }); } setIsLinkEditMode(false); setIsEditingLink(false); currentLinkNodeKey.current = null; }; const handleCancel = () => { setIsLinkEditMode(false); setIsEditingLink(false); setLinkUrl(""); currentLinkNodeKey.current = null; }; if (!isLinkEditMode) return null; const variants = [ "Light", "Light Accent", "Accent", "Dark Accent", "Dark", "Transparent Light", "Transparent Light Accent", "Transparent Accent", "Transparent Dark Accent", "Transparent Dark", "Primary", "Secondary", "Success", "Danger", "Warning", "Info" ]; const sizes = ["Small", "Medium", "Large", "XL", "2X", "3X", "4X"]; let appearance = "link"; if (classNamesList[0]?.indexOf("btn") > -1) appearance = "btn"; if (classNamesList[0]?.indexOf("btn-block") > -1) appearance = "btn btn-block"; const getVariantKeyName = (variant) => { const keyNameParts = variant.split(" "); keyNameParts[0] = keyNameParts[0].toLowerCase(); return keyNameParts.join(""); }; const getVariantItems = () => { const result = []; variants.forEach((variant, idx) => { result.push(_jsx(MenuItem, { value: "btn-" + getVariantKeyName(variant), children: variant }, appearance + " btn-" + getVariantKeyName(variant))); if (idx === 4 || idx === 9) result.push(_jsx(MenuItem, { disabled: true, children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" })); }); return result; }; return createPortal(_jsxs(_Fragment, { children: [_jsx(Box, { onClick: (e) => { if (e.target === e.currentTarget) { handleCancel(); } }, sx: { position: "fixed", top: 0, left: 0, right: 0, bottom: 0, backgroundColor: "rgba(0, 0, 0, 0.5)", zIndex: 1499 } }), _jsxs(Box, { ref: linkEditorRef, sx: { position: "fixed", top: "50%", left: "50%", transform: "translate(-50%, -50%)", background: "#fff", borderRadius: 2, boxShadow: "0px 11px 15px -7px rgba(0,0,0,0.2), 0px 24px 38px 3px rgba(0,0,0,0.14), 0px 9px 46px 8px rgba(0,0,0,0.12)", minWidth: 420, maxWidth: 500, zIndex: 1500 }, children: [_jsxs(Box, { sx: { p: 3, pb: 2 }, children: [_jsxs(Box, { sx: { display: "flex", alignItems: "center", gap: 1.5, mb: 2 }, children: [_jsx(LinkIcon, { color: "primary" }), _jsx(Typography, { variant: "h6", component: "h2", sx: { fontWeight: 600 }, children: "Edit Link" })] }), _jsxs(Box, { sx: { display: "flex", flexDirection: "column", gap: 2 }, children: [_jsx(TextField, { label: "URL", value: linkUrl, onChange: (e) => setLinkUrl(e.target.value), size: "small", fullWidth: true, placeholder: "https://example.com" }), _jsxs(FormControl, { fullWidth: true, size: "small", children: [_jsx(InputLabel, { children: "Appearance" }), _jsxs(Select, { name: "classNames", fullWidth: true, label: "Appearance", size: "small", value: appearance, onChange: (e) => { let className = ""; if (e.target.value.toString() !== "link") className = e.target.value.toString(); setClassNamesList([className, "btn-primary", "btn-medium"]); }, MenuProps: { slotProps: { paper: { sx: { zIndex: 9999 } }, root: { sx: { zIndex: 9999 } } }, style: { zIndex: 9999 } }, children: [_jsx(MenuItem, { value: "link", children: "Standard Link" }), _jsx(MenuItem, { value: "btn", children: "Button" }), _jsx(MenuItem, { value: "btn btn-block", children: "Full Width Button" })] })] }), appearance !== "link" && (_jsxs(_Fragment, { children: [_jsxs(FormControl, { fullWidth: true, size: "small", children: [_jsx(InputLabel, { children: "Variant" }), _jsx(Select, { name: "classNames", fullWidth: true, label: "Variant", size: "small", value: classNamesList[1], onChange: (e) => { const newArray = [...classNamesList]; let index = 0; newArray.forEach((item, i) => { variants.forEach((element) => { if (item.includes(getVariantKeyName(element))) { index = i; } }); }); newArray.splice(index, 1, e.target.value.toString()); setClassNamesList(newArray); }, MenuProps: { slotProps: { paper: { sx: { zIndex: 9999 } }, root: { sx: { zIndex: 9999 } } }, style: { zIndex: 9999 } }, children: getVariantItems() })] }), _jsxs(FormControl, { fullWidth: true, size: "small", children: [_jsx(InputLabel, { children: "Size" }), _jsx(Select, { name: "classNames", fullWidth: true, label: "Size", size: "small", value: classNamesList[2], onChange: (e) => { const newArray = [...classNamesList]; let index = 0; newArray.forEach((item, i) => { sizes.forEach((element) => { if (item.includes(element.toLowerCase())) { index = i; } }); }); newArray.splice(index, 1, e.target.value.toString()); setClassNamesList(newArray); }, MenuProps: { slotProps: { paper: { sx: { zIndex: 9999 } }, root: { sx: { zIndex: 9999 } } }, style: { zIndex: 9999 } }, children: sizes.map((optionValue) => (_jsx(MenuItem, { value: "btn-" + optionValue.toLowerCase(), children: optionValue }, appearance + " btn-" + optionValue.toLowerCase()))) })] })] })), _jsx(FormControlLabel, { control: _jsx(Checkbox, { checked: targetAttribute === "_blank", onChange: () => setTargetAttribute((v) => (v === "_blank" ? "_self" : "_blank")), size: "small" }), label: "Open in new window", sx: { mt: -0.5 } })] })] }), _jsx(Divider, {}), _jsxs(Box, { sx: { display: "flex", justifyContent: "flex-end", gap: 1.5, p: 2 }, children: [_jsx(Button, { onClick: handleCancel, variant: "outlined", size: "medium", children: "Cancel" }), _jsx(Button, { onClick: handleLinkSubmit, variant: "contained", color: "primary", size: "medium", startIcon: _jsx(Check, {}), children: "Save" })] })] })] }), document.body); }