UNPKG

@mdxeditor/editor

Version:

React component for rich text markdown editing

256 lines (255 loc) 8.5 kB
import React__default from "react"; import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext.js"; import { useLexicalNodeSelection } from "@lexical/react/useLexicalNodeSelection.js"; import { mergeRegister } from "@lexical/utils"; 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 { disableImageResize$, disableImageSettingsButton$, imagePreviewHandler$, openEditImageDialog$ } from "./index.js"; import styles from "../../styles/ui.module.css.js"; import { iconComponentFor$, readOnly$, useTranslation } from "../core/index.js"; import { $isImageNode } from "./ImageNode.js"; import ImageResizer from "./ImageResizer.js"; import { useCellValues, usePublisher } from "@mdxeditor/gurx"; const imageCache = /* @__PURE__ */ new Set(); function useSuspenseImage(src) { if (!imageCache.has(src)) { throw new Promise((resolve) => { const img = new Image(); img.src = src; img.onerror = img.onload = () => { imageCache.add(src); resolve(null); }; }); } } function LazyImage({ title, alt, className, imageRef, src, width, height }) { useSuspenseImage(src); return /* @__PURE__ */ React__default.createElement( "img", { className: className ?? void 0, alt, src, title, ref: imageRef, draggable: "false", width, height } ); } function ImageEditor({ src, title, alt, nodeKey, width, height }) { const [disableImageResize, disableImageSettingsButton, imagePreviewHandler, iconComponentFor, readOnly] = useCellValues( disableImageResize$, disableImageSettingsButton$, imagePreviewHandler$, iconComponentFor$, readOnly$ ); const openEditImageDialog = usePublisher(openEditImageDialog$); 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 t = useTranslation(); 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(() => { 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; return imageSource !== null ? /* @__PURE__ */ React__default.createElement(React__default.Suspense, { fallback: 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 }), src: imageSource, title: title ?? "", alt: alt ?? "", imageRef } )), draggable && isFocused && !disableImageResize && /* @__PURE__ */ React__default.createElement(ImageResizer, { editor, imageRef, onResizeStart, onResizeEnd }), readOnly || /* @__PURE__ */ React__default.createElement("div", { className: styles.editImageToolbar }, /* @__PURE__ */ React__default.createElement( "button", { className: styles.iconButton, type: "button", title: t("image.delete", "Delete image"), disabled: readOnly, onClick: (e) => { e.preventDefault(); editor.update(() => { var _a; (_a = $getNodeByKey(nodeKey)) == null ? void 0 : _a.remove(); }); } }, iconComponentFor("delete_small") ), !disableImageSettingsButton && /* @__PURE__ */ React__default.createElement( "button", { type: "button", className: classNames(styles.iconButton, styles.editImageButton), title: t("imageEditor.editImage", "Edit image"), disabled: readOnly, onClick: () => { openEditImageDialog({ nodeKey, initialValues: { src: !initialImagePath ? imageSource : initialImagePath, title: title ?? "", altText: alt ?? "" } }); } }, iconComponentFor("settings") )))) : null; } export { ImageEditor };