UNPKG

@mdxeditor/editor

Version:

React component for rich text markdown editing

276 lines (275 loc) 9.13 kB
import React__default from "react"; import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; import { useLexicalNodeSelection } from "@lexical/react/useLexicalNodeSelection"; import { mergeRegister } from "@lexical/utils"; import { useCellValues } from "@mdxeditor/gurx"; import classNames from "classnames"; import { $isNodeSelection, $getSelection, $getNodeByKey, $setSelection, SELECTION_CHANGE_COMMAND, COMMAND_PRIORITY_LOW, CLICK_COMMAND, DRAGSTART_COMMAND, KEY_DELETE_COMMAND, KEY_BACKSPACE_COMMAND, KEY_ENTER_COMMAND, KEY_ESCAPE_COMMAND } from "lexical"; import { imagePlaceholder$, disableImageResize$, allowSetImageDimensions$, imagePreviewHandler$, editImageToolbarComponent$ } from "./index.js"; import styles from "../../styles/ui.module.css.js"; import { readOnly$ } from "../core/index.js"; import { $isImageNode } from "./ImageNode.js"; import ImageResizer from "./ImageResizer.js"; const BROKEN_IMG_URI = "data:image/svg+xml;charset=utf-8," + encodeURIComponent( /* xml */ ` <svg id="imgLoadError" xmlns="http://www.w3.org/2000/svg" width="100" height="100"> <rect x="0" y="0" width="100" height="100" fill="none" stroke="red" stroke-width="4" stroke-dasharray="4" /> <text x="50" y="55" text-anchor="middle" font-size="20" fill="red">⚠️</text> </svg> ` ); const imgCache = { __cache: {}, read(src) { if (!this.__cache[src]) { this.__cache[src] = new Promise((resolve) => { const img = new Image(); img.onerror = () => { this.__cache[src] = BROKEN_IMG_URI; resolve(); }; img.onload = () => { this.__cache[src] = src; resolve(); }; img.src = src; }); } if (this.__cache[src] instanceof Promise) { throw this.__cache[src]; } return this.__cache[src]; } }; function LazyImage({ title, alt, className, imageRef, src, width, height }) { return /* @__PURE__ */ React__default.createElement( "img", { className: className ?? void 0, alt, src: imgCache.read(src), title, ref: imageRef, draggable: "false", width, height } ); } function ImageEditor({ src, title, alt, nodeKey, width, height, rest }) { const [ImagePlaceholderComponent, disableImageResize, allowSetImageDimensions, imagePreviewHandler, readOnly, EditImageToolbar] = useCellValues( imagePlaceholder$, disableImageResize$, allowSetImageDimensions$, imagePreviewHandler$, readOnly$, editImageToolbarComponent$ ); const imageRef = React__default.useRef(null); const buttonRef = React__default.useRef(null); const [isSelected, setSelected, clearSelection] = useLexicalNodeSelection(nodeKey); const [editor] = useLexicalComposerContext(); const [selection, setSelection] = React__default.useState(null); const activeEditorRef = React__default.useRef(null); const [isResizing, setIsResizing] = React__default.useState(false); const [imageSource, setImageSource] = React__default.useState(null); const [initialImagePath, setInitialImagePath] = React__default.useState(null); const onDelete = React__default.useCallback( (payload) => { if (isSelected && $isNodeSelection($getSelection())) { const event = payload; event.preventDefault(); const node = $getNodeByKey(nodeKey); if ($isImageNode(node)) { node.remove(); } } return false; }, [isSelected, nodeKey] ); const onEnter = React__default.useCallback( (event) => { const latestSelection = $getSelection(); const buttonElem = buttonRef.current; if (isSelected && $isNodeSelection(latestSelection) && latestSelection.getNodes().length === 1) { if (buttonElem !== null && buttonElem !== document.activeElement) { event.preventDefault(); buttonElem.focus(); return true; } } return false; }, [isSelected] ); const onEscape = React__default.useCallback( (event) => { if (buttonRef.current === event.target) { $setSelection(null); editor.update(() => { setSelected(true); const parentRootElement = editor.getRootElement(); if (parentRootElement !== null) { parentRootElement.focus(); } }); return true; } return false; }, [editor, setSelected] ); React__default.useEffect(() => { if (imagePreviewHandler) { const callPreviewHandler = async () => { if (!initialImagePath) setInitialImagePath(src); const updatedSrc = await imagePreviewHandler(src); setImageSource(updatedSrc); }; callPreviewHandler().catch((e) => { console.error(e); }); } else { setImageSource(src); } }, [src, imagePreviewHandler, initialImagePath]); React__default.useEffect(() => { if (allowSetImageDimensions && imageRef.current) { const { current: image } = imageRef; syncDimensionWithImageResizer(image, "width", width); syncDimensionWithImageResizer(image, "height", height); } }, [allowSetImageDimensions, width, height]); React__default.useEffect(() => { let isMounted = true; const unregister = mergeRegister( editor.registerUpdateListener(({ editorState }) => { if (isMounted) { setSelection(editorState.read(() => $getSelection())); } }), editor.registerCommand( SELECTION_CHANGE_COMMAND, (_, activeEditor) => { activeEditorRef.current = activeEditor; return false; }, COMMAND_PRIORITY_LOW ), editor.registerCommand( CLICK_COMMAND, (payload) => { const event = payload; if (isResizing) { return true; } if (event.target === imageRef.current) { if (event.shiftKey) { setSelected(!isSelected); } else { clearSelection(); setSelected(true); } return true; } return false; }, COMMAND_PRIORITY_LOW ), editor.registerCommand( DRAGSTART_COMMAND, (event) => { if (event.target === imageRef.current) { event.preventDefault(); return true; } return false; }, COMMAND_PRIORITY_LOW ), editor.registerCommand(KEY_DELETE_COMMAND, onDelete, COMMAND_PRIORITY_LOW), editor.registerCommand(KEY_BACKSPACE_COMMAND, onDelete, COMMAND_PRIORITY_LOW), editor.registerCommand(KEY_ENTER_COMMAND, onEnter, COMMAND_PRIORITY_LOW), editor.registerCommand(KEY_ESCAPE_COMMAND, onEscape, COMMAND_PRIORITY_LOW) ); return () => { isMounted = false; unregister(); }; }, [clearSelection, editor, isResizing, isSelected, nodeKey, onDelete, onEnter, onEscape, setSelected]); const onResizeEnd = (nextWidth, nextHeight) => { setTimeout(() => { setIsResizing(false); }, 200); editor.update(() => { const node = $getNodeByKey(nodeKey); if ($isImageNode(node)) { node.setWidthAndHeight(nextWidth, nextHeight); } }); }; const onResizeStart = () => { setIsResizing(true); }; const draggable = $isNodeSelection(selection); const isFocused = isSelected; const passedClassName = React__default.useMemo(() => { if (rest.length === 0) { return null; } const className = rest.find((attr) => attr.type === "mdxJsxAttribute" && (attr.name === "class" || attr.name === "className")); if (className) { return className.value; } return null; }, [rest]); return imageSource !== null ? /* @__PURE__ */ React__default.createElement(React__default.Suspense, { fallback: ImagePlaceholderComponent ? /* @__PURE__ */ React__default.createElement(ImagePlaceholderComponent, null) : null }, /* @__PURE__ */ React__default.createElement("div", { className: styles.imageWrapper, "data-editor-block-type": "image" }, /* @__PURE__ */ React__default.createElement("div", { draggable }, /* @__PURE__ */ React__default.createElement( LazyImage, { width, height, className: classNames( { [styles.focusedImage]: isFocused }, passedClassName ), src: imageSource, title: title ?? "", alt: alt ?? "", imageRef } )), draggable && isFocused && !disableImageResize && /* @__PURE__ */ React__default.createElement(ImageResizer, { editor, imageRef, onResizeStart, onResizeEnd }), readOnly || /* @__PURE__ */ React__default.createElement( EditImageToolbar, { nodeKey, imageSource, initialImagePath, title: title ?? "", alt: alt ?? "", width, height } ))) : null; } const syncDimensionWithImageResizer = (image, key, value) => { if (typeof value === "number") { image.style[key] = `${value}px`; } else { image.style.removeProperty(key); } }; export { ImageEditor };