@churchapps/apphelper-markdown
Version:
ChurchApps markdown/lexical editor components
190 lines (189 loc) • 9.87 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { useCallback, useRef, useEffect } from "react";
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
import { mergeRegister } from "@lexical/utils";
import { $isCustomLinkNode } from "./CustomLinkNode";
import { $getSelection, SELECTION_CHANGE_COMMAND, $isRangeSelection, $getNodeByKey } from "lexical";
import { TOGGLE_CUSTOM_LINK_NODE_COMMAND } from "./CustomLinkNode";
import { getSelectedNode } from "../ToolbarPlugin";
import { FormControl, InputLabel, Select, MenuItem, TextField, Button } from "@mui/material";
const positionEditorElement = (editor, rect) => {
if (rect === null) {
editor.style.opacity = "0";
editor.style.top = "-1000px";
editor.style.left = "-1000px";
}
else {
editor.style.opacity = "1";
// Add viewport height check
const editorHeight = editor.offsetHeight;
const viewportHeight = window.innerHeight;
let topPosition = rect.top + rect.height + 10;
// If editor would go off bottom of screen, position it above the selection instead
if (topPosition + editorHeight > viewportHeight) {
topPosition = rect.top - editorHeight - 10;
}
editor.style.top = `${topPosition}px`;
// Ensure editor stays within horizontal bounds
const leftPosition = Math.max(0, Math.min(rect.left + window.pageXOffset - editor.offsetWidth / 2 + rect.width / 2, window.innerWidth - editor.offsetWidth));
editor.style.left = `${leftPosition}px`;
}
};
const LowPriority = 1;
const FloatingLinkEditor = ({ linkUrl, setLinkUrl, classNamesList, setClassNamesList, targetAttribute, setTargetAttribute, selectedElementKey }) => {
const [editor] = useLexicalComposerContext();
const editorRef = useRef(null);
const mouseDownRef = useRef(false);
/*
const [lastSelection, setLastSelection] = useState<
GridSelection | NodeSelection | RangeSelection | null
>(null);
*/
const updateLinkEditor = useCallback(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
const node = getSelectedNode(selection);
const parent = node.getParent();
if ($isCustomLinkNode(parent)) {
const _url = editor.getElementByKey(parent.__key)?.getAttribute("href");
if (_url) {
setLinkUrl(_url);
}
}
else if ($isCustomLinkNode(node)) {
const _url = editor.getElementByKey(node.__key)?.getAttribute("href");
if (_url) {
setLinkUrl(_url);
}
}
}
const editorElem = editorRef.current;
const nativeSelection = window.getSelection();
if (!nativeSelection)
return;
const activeElement = document.activeElement;
if (editorElem === null) {
return;
}
const rootElement = editor.getRootElement();
if (selection !== null
&& !nativeSelection?.isCollapsed
&& rootElement !== null
&& rootElement.contains(nativeSelection.anchorNode)) {
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();
}
if (!mouseDownRef.current) {
positionEditorElement(editorElem, rect);
}
//setLastSelection(selection);
}
else if (!activeElement || activeElement.className !== "link-input") {
positionEditorElement(editorElem, null);
//setLastSelection(null);
}
return true;
}, [editor]);
useEffect(() => (mergeRegister(editor.registerUpdateListener(({ editorState }) => {
editorState.read(() => {
updateLinkEditor();
});
}), editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
editor.getEditorState().read(() => {
updateLinkEditor();
});
return false;
}, LowPriority))), [editor, updateLinkEditor]);
useEffect(() => {
editor.getEditorState().read(() => {
updateLinkEditor();
});
}, [editor, updateLinkEditor]);
useEffect(() => {
editor.getEditorState().read(() => {
updateLinkEditor();
});
}, []);
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 handleSave = () => {
editor.dispatchCommand(TOGGLE_CUSTOM_LINK_NODE_COMMAND, {
url: linkUrl,
classNames: classNamesList,
target: targetAttribute
});
editor.update(() => {
if (!selectedElementKey)
return;
const selectedNode = $getNodeByKey(selectedElementKey);
selectedNode?.selectEnd();
});
};
const getVariantKeyName = (variant) => {
const keyNameParts = variant.split(" ");
keyNameParts[0] = keyNameParts[0].toLowerCase();
return keyNameParts.join("");
};
const handleVariantChange = (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);
};
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 (_jsxs("div", { ref: editorRef, className: "link-editor", children: [_jsx(TextField, { label: "Url", value: linkUrl, onChange: e => { setLinkUrl(e.target.value); }, fullWidth: true, size: "small" }), _jsxs(FormControl, { fullWidth: true, 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"]);
}, 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("div", { children: [_jsxs(FormControl, { fullWidth: true, children: [_jsx(InputLabel, { children: "Variant" }), _jsx(Select, { name: "classNames", fullWidth: true, label: "Variant", size: "small", value: classNamesList[1], onChange: handleVariantChange, children: getVariantItems() })] }), _jsxs(FormControl, { fullWidth: true, 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);
}, children: sizes.map((optionValue) => (_jsx(MenuItem, { value: "btn-" + optionValue.toLowerCase(), children: optionValue }, appearance + " btn-" + optionValue.toLowerCase()))) })] })] }), _jsxs("div", { className: "target-check", children: [_jsx("input", { type: "checkbox", checked: targetAttribute === "_blank", onChange: (e) => {
setTargetAttribute((currentValue) => currentValue === "_blank" ? "_self" : "_blank");
} }), "- Open in new window"] }), _jsx("br", {}), _jsx(Button, { fullWidth: true, variant: "contained", onClick: handleSave, children: "Save" })] }));
};
export default FloatingLinkEditor;