text-editor-studio-ts
Version:
A powerful mobile-responsive rich text editor built with Lexical and React
1 lines • 18.7 kB
Source Map (JSON)
{"version":3,"file":"image-component-BHEd7PZU.cjs","sources":["../src/components/editor/editor-ui/image-component.tsx"],"sourcesContent":["import { Suspense, useCallback, useEffect, useRef, useState, JSX } from \"react\";\n\nimport { AutoFocusPlugin } from \"@lexical/react/LexicalAutoFocusPlugin\";\nimport { useCollaborationContext } from \"@lexical/react/LexicalCollaborationContext\";\nimport { useLexicalComposerContext } from \"@lexical/react/LexicalComposerContext\";\nimport { LexicalErrorBoundary } from \"@lexical/react/LexicalErrorBoundary\";\nimport { HistoryPlugin } from \"@lexical/react/LexicalHistoryPlugin\";\nimport { LexicalNestedComposer } from \"@lexical/react/LexicalNestedComposer\";\nimport { RichTextPlugin } from \"@lexical/react/LexicalRichTextPlugin\";\nimport { useLexicalEditable } from \"@lexical/react/useLexicalEditable\";\nimport { useLexicalNodeSelection } from \"@lexical/react/useLexicalNodeSelection\";\nimport { mergeRegister } from \"@lexical/utils\";\nimport type {\n BaseSelection,\n LexicalCommand,\n LexicalEditor,\n NodeKey,\n} from \"lexical\";\nimport {\n $getNodeByKey,\n $getSelection,\n $isNodeSelection,\n $isRangeSelection,\n $setSelection,\n CLICK_COMMAND,\n COMMAND_PRIORITY_LOW,\n DRAGSTART_COMMAND,\n KEY_BACKSPACE_COMMAND,\n KEY_DELETE_COMMAND,\n KEY_ENTER_COMMAND,\n KEY_ESCAPE_COMMAND,\n ParagraphNode,\n RootNode,\n SELECTION_CHANGE_COMMAND,\n TextNode,\n createCommand,\n} from \"lexical\";\n\nimport { $isImageNode } from \"@/components/editor/nodes/image-node\";\n// import brokenImage from '@/registry/default/editor/images/image-broken.svg';\nimport { ContentEditable } from \"@/components/editor/editor-ui/content-editable\";\nimport { ImageResizer } from \"@/components/editor/editor-ui/image-resizer\";\n\nconst imageCache = new Set();\n\nexport const RIGHT_CLICK_IMAGE_COMMAND: LexicalCommand<MouseEvent> =\n createCommand(\"RIGHT_CLICK_IMAGE_COMMAND\");\n\nfunction useSuspenseImage(src: string) {\n if (!imageCache.has(src)) {\n throw new Promise((resolve) => {\n const img = new Image();\n img.src = src;\n img.onload = () => {\n imageCache.add(src);\n resolve(null);\n };\n img.onerror = () => {\n imageCache.add(src);\n };\n });\n }\n}\n\nfunction LazyImage({\n altText,\n className,\n imageRef,\n src,\n width,\n height,\n maxWidth,\n onError,\n}: {\n altText: string;\n className: string | null;\n height: \"inherit\" | number;\n imageRef: { current: null | HTMLImageElement };\n maxWidth: number;\n src: string;\n width: \"inherit\" | number;\n onError: () => void;\n}): JSX.Element {\n useSuspenseImage(src);\n return (\n <img\n className={className || undefined}\n src={src}\n alt={altText}\n ref={imageRef}\n style={{\n height,\n maxWidth,\n width,\n }}\n onError={onError}\n draggable=\"false\"\n />\n );\n}\n\nfunction BrokenImage(): JSX.Element {\n return (\n <img\n src={\"\"}\n style={{\n height: 200,\n opacity: 0.2,\n width: 200,\n }}\n draggable=\"false\"\n />\n );\n}\n\nexport default function ImageComponent({\n src,\n altText,\n nodeKey,\n width,\n height,\n maxWidth,\n resizable,\n showCaption,\n caption,\n captionsEnabled,\n}: {\n altText: string;\n caption: LexicalEditor;\n height: \"inherit\" | number;\n maxWidth: number;\n nodeKey: NodeKey;\n resizable: boolean;\n showCaption: boolean;\n src: string;\n width: \"inherit\" | number;\n captionsEnabled: boolean;\n}): JSX.Element {\n const imageRef = useRef<null | HTMLImageElement>(null);\n const buttonRef = useRef<HTMLButtonElement | null>(null);\n const [isSelected, setSelected, clearSelection] =\n useLexicalNodeSelection(nodeKey);\n const [isResizing, setIsResizing] = useState<boolean>(false);\n // @ts-ignore\n const { isCollabActive } = useCollaborationContext();\n const [editor] = useLexicalComposerContext();\n const [selection, setSelection] = useState<BaseSelection | null>(null);\n const activeEditorRef = useRef<LexicalEditor | null>(null);\n const [isLoadError, setIsLoadError] = useState<boolean>(false);\n const isEditable = useLexicalEditable();\n\n const $onDelete = useCallback(\n (payload: KeyboardEvent) => {\n const deleteSelection = $getSelection();\n if (isSelected && $isNodeSelection(deleteSelection)) {\n const event: KeyboardEvent = payload;\n event.preventDefault();\n editor.update(() => {\n deleteSelection.getNodes().forEach((node) => {\n if ($isImageNode(node)) {\n node.remove();\n }\n });\n });\n }\n return false;\n },\n [editor, isSelected]\n );\n\n const $onEnter = useCallback(\n (event: KeyboardEvent) => {\n const latestSelection = $getSelection();\n const buttonElem = buttonRef.current;\n if (\n isSelected &&\n $isNodeSelection(latestSelection) &&\n latestSelection.getNodes().length === 1\n ) {\n if (showCaption) {\n // Move focus into nested editor\n $setSelection(null);\n event.preventDefault();\n caption.focus();\n return true;\n } else if (\n buttonElem !== null &&\n buttonElem !== document.activeElement\n ) {\n event.preventDefault();\n buttonElem.focus();\n return true;\n }\n }\n return false;\n },\n [caption, isSelected, showCaption]\n );\n\n const $onEscape = useCallback(\n (event: KeyboardEvent) => {\n if (\n activeEditorRef.current === caption ||\n buttonRef.current === event.target\n ) {\n $setSelection(null);\n editor.update(() => {\n setSelected(true);\n const parentRootElement = editor.getRootElement();\n if (parentRootElement !== null) {\n parentRootElement.focus();\n }\n });\n return true;\n }\n return false;\n },\n [caption, editor, setSelected]\n );\n\n const onClick = useCallback(\n (payload: MouseEvent) => {\n const event = payload;\n\n if (isResizing) {\n return true;\n }\n if (event.target === imageRef.current) {\n if (event.shiftKey) {\n setSelected(!isSelected);\n } else {\n clearSelection();\n setSelected(true);\n }\n return true;\n }\n\n return false;\n },\n [isResizing, isSelected, setSelected, clearSelection]\n );\n\n const onRightClick = useCallback(\n (event: MouseEvent): void => {\n editor.getEditorState().read(() => {\n const latestSelection = $getSelection();\n const domElement = event.target as HTMLElement;\n if (\n domElement.tagName === \"IMG\" &&\n $isRangeSelection(latestSelection) &&\n latestSelection.getNodes().length === 1\n ) {\n editor.dispatchCommand(\n RIGHT_CLICK_IMAGE_COMMAND,\n event as MouseEvent\n );\n }\n });\n },\n [editor]\n );\n\n useEffect(() => {\n let isMounted = true;\n const rootElement = editor.getRootElement();\n const unregister = mergeRegister(\n editor.registerUpdateListener(({ editorState }) => {\n if (isMounted) {\n setSelection(editorState.read(() => $getSelection()));\n }\n }),\n editor.registerCommand(\n SELECTION_CHANGE_COMMAND,\n (_, activeEditor) => {\n activeEditorRef.current = activeEditor;\n return false;\n },\n COMMAND_PRIORITY_LOW\n ),\n editor.registerCommand<MouseEvent>(\n CLICK_COMMAND,\n onClick,\n COMMAND_PRIORITY_LOW\n ),\n editor.registerCommand<MouseEvent>(\n RIGHT_CLICK_IMAGE_COMMAND,\n onClick,\n COMMAND_PRIORITY_LOW\n ),\n editor.registerCommand(\n DRAGSTART_COMMAND,\n (event) => {\n if (event.target === imageRef.current) {\n // TODO This is just a temporary workaround for FF to behave like other browsers.\n // Ideally, this handles drag & drop too (and all browsers).\n event.preventDefault();\n return true;\n }\n return false;\n },\n COMMAND_PRIORITY_LOW\n ),\n editor.registerCommand(\n KEY_DELETE_COMMAND,\n $onDelete,\n COMMAND_PRIORITY_LOW\n ),\n editor.registerCommand(\n KEY_BACKSPACE_COMMAND,\n $onDelete,\n COMMAND_PRIORITY_LOW\n ),\n editor.registerCommand(KEY_ENTER_COMMAND, $onEnter, COMMAND_PRIORITY_LOW),\n editor.registerCommand(\n KEY_ESCAPE_COMMAND,\n $onEscape,\n COMMAND_PRIORITY_LOW\n )\n );\n\n rootElement?.addEventListener(\"contextmenu\", onRightClick);\n\n return () => {\n isMounted = false;\n unregister();\n rootElement?.removeEventListener(\"contextmenu\", onRightClick);\n };\n }, [\n clearSelection,\n editor,\n isResizing,\n isSelected,\n nodeKey,\n $onDelete,\n $onEnter,\n $onEscape,\n onClick,\n onRightClick,\n setSelected,\n ]);\n\n const setShowCaption = () => {\n editor.update(() => {\n const node = $getNodeByKey(nodeKey);\n if ($isImageNode(node)) {\n node.setShowCaption(true);\n }\n });\n };\n\n const onResizeEnd = (\n nextWidth: \"inherit\" | number,\n nextHeight: \"inherit\" | number\n ) => {\n // Delay hiding the resize bars for click case\n setTimeout(() => {\n setIsResizing(false);\n }, 200);\n\n editor.update(() => {\n const node = $getNodeByKey(nodeKey);\n if ($isImageNode(node)) {\n node.setWidthAndHeight(nextWidth, nextHeight);\n }\n });\n };\n\n const onResizeStart = () => {\n setIsResizing(true);\n };\n\n const draggable = isSelected && $isNodeSelection(selection) && !isResizing;\n const isFocused = (isSelected || isResizing) && isEditable;\n return (\n <Suspense fallback={null}>\n <>\n <div draggable={draggable}>\n {isLoadError ? (\n <BrokenImage />\n ) : (\n <LazyImage\n className={`max-w-full cursor-default ${\n isFocused\n ? `${\n $isNodeSelection(selection)\n ? \"draggable cursor-grab active:cursor-grabbing\"\n : \"\"\n } focused ring-2 ring-primary ring-offset-2`\n : null\n }`}\n src={src}\n altText={altText}\n imageRef={imageRef}\n width={width}\n height={height}\n maxWidth={maxWidth}\n onError={() => setIsLoadError(true)}\n />\n )}\n </div>\n\n {showCaption && (\n <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\">\n <LexicalNestedComposer\n initialEditor={caption}\n initialNodes={[RootNode, TextNode, ParagraphNode]}\n >\n <AutoFocusPlugin />\n <HistoryPlugin />\n <RichTextPlugin\n contentEditable={\n <ContentEditable\n 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\"\n 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\"\n placeholder=\"Enter a caption...\"\n />\n }\n ErrorBoundary={LexicalErrorBoundary}\n />\n </LexicalNestedComposer>\n </div>\n )}\n {resizable && $isNodeSelection(selection) && isFocused && (\n <ImageResizer\n showCaption={showCaption}\n setShowCaption={setShowCaption}\n editor={editor}\n buttonRef={buttonRef}\n imageRef={imageRef}\n maxWidth={maxWidth}\n onResizeStart={onResizeStart}\n onResizeEnd={onResizeEnd}\n captionsEnabled={!isLoadError && captionsEnabled}\n />\n )}\n </>\n </Suspense>\n );\n}\n"],"names":["imageCache","Set","RIGHT_CLICK_IMAGE_COMMAND","createCommand","LazyImage","altText","className","imageRef","src","width","height","maxWidth","onError","has","Promise","resolve","img","Image","onload","add","onerror","useSuspenseImage","jsxRuntime","jsx","alt","ref","style","draggable","BrokenImage","opacity","nodeKey","resizable","showCaption","caption","captionsEnabled","useRef","buttonRef","isSelected","setSelected","clearSelection","useLexicalNodeSelection","isResizing","setIsResizing","useState","isCollabActive","useCollaborationContext","editor","useLexicalComposerContext","selection","setSelection","activeEditorRef","isLoadError","setIsLoadError","isEditable","useLexicalEditable","$onDelete","useCallback","payload","deleteSelection","$getSelection","$isNodeSelection","preventDefault","update","getNodes","forEach","node","$isImageNode","remove","$onEnter","event","latestSelection","buttonElem","current","length","$setSelection","focus","document","activeElement","$onEscape","target","parentRootElement","getRootElement","onClick","shiftKey","onRightClick","getEditorState","read","tagName","$isRangeSelection","dispatchCommand","useEffect","isMounted","rootElement","unregister","mergeRegister","registerUpdateListener","editorState","registerCommand","SELECTION_CHANGE_COMMAND","_","activeEditor","COMMAND_PRIORITY_LOW","CLICK_COMMAND","DRAGSTART_COMMAND","KEY_DELETE_COMMAND","KEY_BACKSPACE_COMMAND","KEY_ENTER_COMMAND","KEY_ESCAPE_COMMAND","addEventListener","removeEventListener","isFocused","Suspense","fallback","children","jsxs","Fragment","LexicalNestedComposer","f","initialEditor","initialNodes","RootNode","TextNode","ParagraphNode","AutoFocusPlugin","o$1","HistoryPlugin","a$1","RichTextPlugin","h","contentEditable","ContentEditable","placeholderClassName","placeholder","ErrorBoundary","LexicalErrorBoundary","ImageResizer","setShowCaption","$getNodeByKey","onResizeStart","onResizeEnd","nextWidth","nextHeight","setTimeout","setWidthAndHeight"],"mappings":"yXA2CMA,MAAiBC,IAEVC,EACXC,gBAAc,6BAkBhB,SAASC,GAAUC,QACjBA,EAAAC,UACAA,EAAAC,SACAA,EAAAC,IACAA,EAAAC,MACAA,EAAAC,OACAA,EAAAC,SACAA,EAAAC,QACAA,IAaE,OArCJ,SAA0BJ,GACxB,IAAKR,EAAWa,IAAIL,GACZ,MAAA,IAAIM,QAASC,IACX,MAAAC,EAAM,IAAIC,MAChBD,EAAIR,IAAMA,EACVQ,EAAIE,OAAS,KACXlB,EAAWmB,IAAIX,GACfO,EAAQ,OAEVC,EAAII,QAAU,KACZpB,EAAWmB,IAAIX,KAIvB,CAqBEa,CAAiBb,GAEfc,EAAAC,IAAC,MAAA,CACCjB,UAAWA,QAAa,EACxBE,MACAgB,IAAKnB,EACLoB,IAAKlB,EACLmB,MAAO,CACLhB,SACAC,WACAF,SAEFG,UACAe,UAAU,SAGhB,CAEA,SAASC,IAEL,OAAAN,EAAAC,IAAC,MAAA,CACCf,IAAK,GACLkB,MAAO,CACLhB,OAAQ,IACRmB,QAAS,GACTpB,MAAO,KAETkB,UAAU,SAGhB,qDAEA,UAAuCnB,IACrCA,EAAAH,QACAA,EAAAyB,QACAA,EAAArB,MACAA,EAAAC,OACAA,EAAAC,SACAA,EAAAoB,UACAA,EAAAC,YACAA,EAAAC,QACAA,EAAAC,gBACAA,IAaM,MAAA3B,EAAW4B,SAAgC,MAC3CC,EAAYD,SAAiC,OAC5CE,EAAYC,EAAaC,GAC9BC,EAAAA,EAAwBV,IACnBW,EAAYC,GAAiBC,EAAAA,UAAkB,IAEhDC,eAAEA,GAAmBC,OACpBC,GAAUC,OACVC,EAAWC,GAAgBN,EAAAA,SAA+B,MAC3DO,EAAkBf,SAA6B,OAC9CgB,EAAaC,GAAkBT,EAAAA,UAAkB,GAClDU,EAAaC,EAAAA,IAEbC,EAAYC,EAAAA,YACfC,IACO,MAAAC,EAAkBC,EAAAA,gBACpB,GAAAtB,GAAcuB,mBAAiBF,GAAkB,CACtBD,EACvBI,iBACNf,EAAOgB,OAAO,KACZJ,EAAgBK,WAAWC,QAASC,IAC9BC,EAAAA,aAAaD,IACfA,EAAKE,YAGV,CAEI,OAAA,GAET,CAACrB,EAAQT,IAGL+B,EAAWZ,EAAAA,YACda,IACO,MAAAC,EAAkBX,EAAAA,gBAClBY,EAAanC,EAAUoC,QAE3B,GAAAnC,GACAuB,mBAAiBU,IACqB,IAAtCA,EAAgBP,WAAWU,OAC3B,CACA,GAAIzC,EAKK,OAHP0C,EAAAA,cAAc,MACdL,EAAMR,iBACN5B,EAAQ0C,SACD,EAEP,GAAe,OAAfJ,GACAA,IAAeK,SAASC,cAIjB,OAFPR,EAAMR,iBACNU,EAAWI,SACJ,CACT,CAEK,OAAA,GAET,CAAC1C,EAASI,EAAYL,IAGlB8C,EAAYtB,EAAAA,YACfa,IAEGnB,EAAgBsB,UAAYvC,GAC5BG,EAAUoC,UAAYH,EAAMU,UAE5BL,EAAAA,cAAc,MACd5B,EAAOgB,OAAO,KACZxB,GAAY,GACN,MAAA0C,EAAoBlC,EAAOmC,iBACP,OAAtBD,GACFA,EAAkBL,WAGf,GAIX,CAAC1C,EAASa,EAAQR,IAGd4C,EAAU1B,EAAAA,YACbC,IACC,MAAMY,EAAQZ,EAEd,QAAIhB,GAGA4B,EAAMU,SAAWxE,EAASiE,UACxBH,EAAMc,SACR7C,GAAaD,IAEEE,IACfD,GAAY,KAEP,IAKX,CAACG,EAAYJ,EAAYC,EAAaC,IAGlC6C,EAAe5B,EAAAA,YAClBa,IACQvB,EAAAuC,iBAAiBC,KAAK,KACrB,MAAAhB,EAAkBX,EAAAA,gBAGC,QAFNU,EAAMU,OAEZQ,SACXC,oBAAkBlB,IACoB,IAAtCA,EAAgBP,WAAWU,QAEpB3B,EAAA2C,gBACLvF,EACAmE,MAKR,CAACvB,IAGH4C,EAAAA,UAAU,KACR,IAAIC,GAAY,EACV,MAAAC,EAAc9C,EAAOmC,iBACrBY,EAAaC,EAAAA,cACjBhD,EAAOiD,uBAAuB,EAAGC,kBAC3BL,GACF1C,EAAa+C,EAAYV,KAAK,IAAM3B,EAAAA,oBAGxCb,EAAOmD,gBACLC,EAAAA,yBACA,CAACC,EAAGC,KACFlD,EAAgBsB,QAAU4B,GACnB,GAETC,EAAAA,sBAEFvD,EAAOmD,gBACLK,EAAAA,cACApB,EACAmB,EAAAA,sBAEFvD,EAAOmD,gBACL/F,EACAgF,EACAmB,EAAAA,sBAEFvD,EAAOmD,gBACLM,EAAAA,kBACClC,GACKA,EAAMU,SAAWxE,EAASiE,UAG5BH,EAAMR,kBACC,GAIXwC,EAAAA,sBAEFvD,EAAOmD,gBACLO,EAAAA,mBACAjD,EACA8C,EAAAA,sBAEFvD,EAAOmD,gBACLQ,EAAAA,sBACAlD,EACA8C,EAAAA,sBAEFvD,EAAOmD,gBAAgBS,oBAAmBtC,EAAUiC,EAAAA,sBACpDvD,EAAOmD,gBACLU,EAAAA,mBACA7B,EACAuB,EAAAA,uBAMJ,OAFaT,GAAAgB,iBAAiB,cAAexB,GAEtC,KACOO,GAAA,EACDE,IACED,GAAAiB,oBAAoB,cAAezB,KAEjD,CACD7C,EACAO,EACAL,EACAJ,EACAP,EACAyB,EACAa,EACAU,EACAI,EACAE,EACA9C,IAGF,MA8BMX,EAAYU,GAAcuB,EAAiBA,iBAAAZ,KAAeP,EAC1DqE,GAAazE,GAAcI,IAAeY,EAChD,OACG/B,EAAAC,IAAAwF,WAAA,CAASC,SAAU,KAClBC,SACE3F,EAAA4F,KAAAC,WAAA,CAAAF,SAAA,OAAC,MAAI,CAAAtF,YACFsF,SACC9D,EAAA5B,MAACK,GAAY,GAEbN,EAAAC,IAACnB,EAAA,CACCE,UAAW,6BACTwG,GAEMlD,EAAAA,iBAAiBZ,GACb,+CACA,IAHN,6CAKA,OAENxC,MACAH,UACAE,WACAE,QACAC,SACAC,WACAC,QAAS,IAAMwC,GAAe,OAKnCpB,KACCT,IAAC,MAAI,CAAAjB,UAAU,qJACb2G,SAAA3F,EAAA4F,KAACE,EAAAC,EAAA,CACCC,cAAerF,EACfsF,aAAc,CAACC,EAAAA,SAAUC,EAAAA,SAAUC,iBAEnCT,SAAA,CAAA3F,EAAAC,IAACoG,EAAgBC,IAAA,UAChBC,EAAcC,IAAA,IACfxG,EAAAC,IAACwG,EAAAC,EAAA,CACCC,gBACE3G,EAAAC,IAAC2G,EAAAA,gBAAA,CACC5H,UAAU,6MACV6H,qBAAqB,kMACrBC,YAAY,uBAGhBC,cAAeC,EAAAA,WAKtBvG,GAAa6B,EAAAA,iBAAiBZ,IAAc8D,GAC3CxF,EAAAC,IAACgH,EAAAA,aAAA,CACCvG,cACAwG,eApFa,KACrB1F,EAAOgB,OAAO,KACN,MAAAG,EAAOwE,gBAAc3G,GACvBoC,EAAAA,aAAaD,IACfA,EAAKuE,gBAAe,MAiFhB1F,SACAV,YACA7B,WACAI,WACA+H,cA/DY,KACpBhG,GAAc,IA+DNiG,YAjFU,CAClBC,EACAC,KAGAC,WAAW,KACTpG,GAAc,IACb,KAEHI,EAAOgB,OAAO,KACN,MAAAG,EAAOwE,gBAAc3G,GACvBoC,EAAAA,aAAaD,IACVA,EAAA8E,kBAAkBH,EAAWC,MAsE9B3G,iBAAkBiB,GAAejB,QAM7C"}