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