UNPKG

text-editor-studio-ts

Version:

A powerful mobile-responsive rich text editor built with Lexical and React

337 lines (336 loc) 9.84 kB
import { jsx, jsxs, Fragment } from "react/jsx-runtime"; import { useRef, useState, useCallback, useEffect, Suspense } from "react"; import { u, o, a, c as $isImageNode, d as o$1, e as a$1, h, f as a$2, C as ContentEditable } from "./index-BwW17RmP.js"; import { l } from "./LexicalCollaborationContext.prod-_XIRbBSl.js"; import { f } from "./LexicalNestedComposer.prod-DcHyIDfI.js"; import { mergeRegister } from "@lexical/utils"; import { createCommand, $getSelection, $isNodeSelection, $setSelection, $isRangeSelection, SELECTION_CHANGE_COMMAND, COMMAND_PRIORITY_LOW, CLICK_COMMAND, DRAGSTART_COMMAND, KEY_DELETE_COMMAND, KEY_BACKSPACE_COMMAND, KEY_ENTER_COMMAND, KEY_ESCAPE_COMMAND, RootNode, TextNode, ParagraphNode, $getNodeByKey } from "lexical"; import { I as ImageResizer } from "./image-resizer-CFedlLqf.js"; const imageCache = /* @__PURE__ */ new Set(); const RIGHT_CLICK_IMAGE_COMMAND = createCommand("RIGHT_CLICK_IMAGE_COMMAND"); function useSuspenseImage(src) { if (!imageCache.has(src)) { throw new Promise((resolve) => { const img = new Image(); img.src = src; img.onload = () => { imageCache.add(src); resolve(null); }; img.onerror = () => { imageCache.add(src); }; }); } } function LazyImage({ altText, className, imageRef, src, width, height, maxWidth, onError }) { useSuspenseImage(src); return /* @__PURE__ */ jsx( "img", { className: className || void 0, src, alt: altText, ref: imageRef, style: { height, maxWidth, width }, onError, draggable: "false" } ); } function BrokenImage() { return /* @__PURE__ */ jsx( "img", { src: "", style: { height: 200, opacity: 0.2, width: 200 }, draggable: "false" } ); } function ImageComponent({ src, altText, nodeKey, width, height, maxWidth, resizable, showCaption, caption, captionsEnabled }) { const imageRef = useRef(null); const buttonRef = useRef(null); const [isSelected, setSelected, clearSelection] = u(nodeKey); const [isResizing, setIsResizing] = useState(false); const { isCollabActive } = l(); const [editor] = o(); const [selection, setSelection] = useState(null); const activeEditorRef = useRef(null); const [isLoadError, setIsLoadError] = useState(false); const isEditable = a(); const $onDelete = useCallback( (payload) => { const deleteSelection = $getSelection(); if (isSelected && $isNodeSelection(deleteSelection)) { const event = payload; event.preventDefault(); editor.update(() => { deleteSelection.getNodes().forEach((node) => { if ($isImageNode(node)) { node.remove(); } }); }); } return false; }, [editor, isSelected] ); const $onEnter = useCallback( (event) => { const latestSelection = $getSelection(); const buttonElem = buttonRef.current; if (isSelected && $isNodeSelection(latestSelection) && latestSelection.getNodes().length === 1) { if (showCaption) { $setSelection(null); event.preventDefault(); caption.focus(); return true; } else if (buttonElem !== null && buttonElem !== document.activeElement) { event.preventDefault(); buttonElem.focus(); return true; } } return false; }, [caption, isSelected, showCaption] ); const $onEscape = useCallback( (event) => { if (activeEditorRef.current === caption || buttonRef.current === event.target) { $setSelection(null); editor.update(() => { setSelected(true); const parentRootElement = editor.getRootElement(); if (parentRootElement !== null) { parentRootElement.focus(); } }); return true; } return false; }, [caption, editor, setSelected] ); const onClick = useCallback( (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; }, [isResizing, isSelected, setSelected, clearSelection] ); const onRightClick = useCallback( (event) => { editor.getEditorState().read(() => { const latestSelection = $getSelection(); const domElement = event.target; if (domElement.tagName === "IMG" && $isRangeSelection(latestSelection) && latestSelection.getNodes().length === 1) { editor.dispatchCommand( RIGHT_CLICK_IMAGE_COMMAND, event ); } }); }, [editor] ); useEffect(() => { let isMounted = true; const rootElement = editor.getRootElement(); 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, onClick, COMMAND_PRIORITY_LOW ), editor.registerCommand( RIGHT_CLICK_IMAGE_COMMAND, onClick, 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 ) ); rootElement?.addEventListener("contextmenu", onRightClick); return () => { isMounted = false; unregister(); rootElement?.removeEventListener("contextmenu", onRightClick); }; }, [ clearSelection, editor, isResizing, isSelected, nodeKey, $onDelete, $onEnter, $onEscape, onClick, onRightClick, setSelected ]); const setShowCaption = () => { editor.update(() => { const node = $getNodeByKey(nodeKey); if ($isImageNode(node)) { node.setShowCaption(true); } }); }; 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 = isSelected && $isNodeSelection(selection) && !isResizing; const isFocused = (isSelected || isResizing) && isEditable; return /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx("div", { draggable, children: isLoadError ? /* @__PURE__ */ jsx(BrokenImage, {}) : /* @__PURE__ */ jsx( LazyImage, { className: `max-w-full cursor-default ${isFocused ? `${$isNodeSelection(selection) ? "draggable cursor-grab active:cursor-grabbing" : ""} focused ring-2 ring-primary ring-offset-2` : null}`, src, altText, imageRef, width, height, maxWidth, onError: () => setIsLoadError(true) } ) }), showCaption && /* @__PURE__ */ jsx("div", { className: "image-caption-container absolute bottom-1 left-0 right-0 m-0 block min-w-[100px] overflow-hidden border-t bg-background-system-body-primary/90 p-0", children: /* @__PURE__ */ jsxs( f, { initialEditor: caption, initialNodes: [RootNode, TextNode, ParagraphNode], children: [ /* @__PURE__ */ jsx(o$1, {}), /* @__PURE__ */ jsx(a$1, {}), /* @__PURE__ */ jsx( h, { contentEditable: /* @__PURE__ */ jsx( ContentEditable, { className: "ImageNode__contentEditable user-select-text word-break-break-word relative block min-h-5 w-[calc(100%-20px)] cursor-text resize-none whitespace-pre-wrap border-0 p-2.5 text-sm caret-primary outline-none", placeholderClassName: "ImageNode__placeholder text-sm text-content-system-global-secondary overflow-hidden absolute top-2.5 left-2.5 pointer-events-none text-ellipsis user-select-none whitespace-nowrap inline-block", placeholder: "Enter a caption..." } ), ErrorBoundary: a$2 } ) ] } ) }), resizable && $isNodeSelection(selection) && isFocused && /* @__PURE__ */ jsx( ImageResizer, { showCaption, setShowCaption, editor, buttonRef, imageRef, maxWidth, onResizeStart, onResizeEnd, captionsEnabled: !isLoadError && captionsEnabled } ) ] }) }); } export { RIGHT_CLICK_IMAGE_COMMAND, ImageComponent as default }; //# sourceMappingURL=image-component-DFvqSqFs.js.map