UNPKG

text-editor-studio-ts

Version:

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

1 lines 19.2 kB
{"version":3,"file":"inline-image-component-DufDpKRG.cjs","sources":["../src/components/editor/editor-ui/inline-image-component.tsx"],"sourcesContent":["import { Suspense, useCallback, useEffect, useRef, useState, JSX } from \"react\";\n\nimport { AutoFocusPlugin } from \"@lexical/react/LexicalAutoFocusPlugin\";\nimport { useLexicalComposerContext } from \"@lexical/react/LexicalComposerContext\";\nimport { LexicalErrorBoundary } from \"@lexical/react/LexicalErrorBoundary\";\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 { BaseSelection, LexicalEditor, NodeKey } from \"lexical\";\nimport {\n $getNodeByKey,\n $getSelection,\n $isNodeSelection,\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 SELECTION_CHANGE_COMMAND,\n} from \"lexical\";\n\nimport { Button } from \"../../../../components/Button\"\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport { DialogFooter } from \"../../../../components/Dialog\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"../../../../components/Select\"\n\nimport { useEditorModal } from \"@/components/editor/editor-hooks/use-modal\";\nimport type { Position } from \"@/components/editor/nodes/inline-image-node\";\nimport {\n $isInlineImageNode,\n InlineImageNode,\n} from \"@/components/editor/nodes/inline-image-node\";\nimport { LinkPlugin } from \"@/components/editor/plugins/link-plugin\";\nimport { ContentEditable } from \"@/components/editor/editor-ui/content-editable\";\n\nconst imageCache = new Set();\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 });\n }\n}\n\nfunction LazyImage({\n altText,\n className,\n imageRef,\n src,\n width,\n height,\n position,\n}: {\n altText: string;\n className: string | null;\n height: \"inherit\" | number;\n imageRef: { current: null | HTMLImageElement };\n src: string;\n width: \"inherit\" | number;\n position: Position;\n}): JSX.Element {\n useSuspenseImage(src);\n return (\n <img\n className={className || undefined}\n src={src}\n alt={altText}\n ref={imageRef}\n data-position={position}\n style={{\n display: \"block\",\n height,\n width,\n }}\n draggable=\"false\"\n />\n );\n}\n\nexport function UpdateInlineImageDialog({\n activeEditor,\n nodeKey,\n onClose,\n}: {\n activeEditor: LexicalEditor;\n nodeKey: NodeKey;\n onClose: () => void;\n}): JSX.Element {\n const editorState = activeEditor.getEditorState();\n const node = editorState.read(\n () => $getNodeByKey(nodeKey) as InlineImageNode\n );\n const [altText, setAltText] = useState(node.getAltText());\n const [showCaption, setShowCaption] = useState(node.getShowCaption());\n const [position, setPosition] = useState<Position>(node.getPosition());\n // @ts-ignore\n const handleShowCaptionChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n setShowCaption(e.target.checked);\n };\n // @ts-ignore\n const handlePositionChange = (e: React.ChangeEvent<HTMLSelectElement>) => {\n setPosition(e.target.value as Position);\n };\n\n const handleOnConfirm = () => {\n const payload = { altText, position, showCaption };\n if (node) {\n activeEditor.update(() => {\n node.update(payload);\n });\n }\n onClose();\n };\n\n return (\n <div className=\"space-y-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"alt-text\">Alt Text</Label>\n <Input\n id=\"alt-text\"\n placeholder=\"Descriptive alternative text\"\n onChange={(e) => setAltText(e.target.value)}\n value={altText}\n data-test-id=\"image-modal-alt-text-input\"\n />\n </div>\n\n <div className=\"space-y-2\">\n <Label htmlFor=\"position-select\">Position</Label>\n <Select\n value={position}\n onValueChange={(value) => setPosition(value as Position)}\n >\n <SelectTrigger id=\"position-select\">\n <SelectValue placeholder=\"Select position\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"left\">Left</SelectItem>\n <SelectItem value=\"right\">Right</SelectItem>\n <SelectItem value=\"full\">Full Width</SelectItem>\n </SelectContent>\n </Select>\n </div>\n\n <div className=\"flex items-center space-x-2\">\n <Checkbox\n id=\"caption\"\n checked={showCaption}\n onCheckedChange={(checked) => setShowCaption(checked as boolean)}\n />\n <Label htmlFor=\"caption\">Show Caption</Label>\n </div>\n\n <DialogFooter>\n <Button\n data-test-id=\"image-modal-file-upload-btn\"\n onClick={handleOnConfirm}\n >\n Confirm\n </Button>\n </DialogFooter>\n </div>\n );\n}\n\nexport default function InlineImageComponent({\n src,\n altText,\n nodeKey,\n width,\n height,\n showCaption,\n caption,\n position,\n}: {\n altText: string;\n caption: LexicalEditor;\n height: \"inherit\" | number;\n nodeKey: NodeKey;\n showCaption: boolean;\n src: string;\n width: \"inherit\" | number;\n position: Position;\n}): JSX.Element {\n const [modal, showModal] = useEditorModal();\n const imageRef = useRef<null | HTMLImageElement>(null);\n const buttonRef = useRef<HTMLButtonElement | null>(null);\n const [isSelected, setSelected, clearSelection] =\n useLexicalNodeSelection(nodeKey);\n const [editor] = useLexicalComposerContext();\n const [selection, setSelection] = useState<BaseSelection | null>(null);\n const activeEditorRef = useRef<LexicalEditor | null>(null);\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 if (isSelected && $isNodeSelection(deleteSelection)) {\n editor.update(() => {\n deleteSelection.getNodes().forEach((node) => {\n if ($isInlineImageNode(node)) {\n node.remove();\n }\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 useEffect(() => {\n let isMounted = true;\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 (payload) => {\n const event = payload;\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 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 return () => {\n isMounted = false;\n unregister();\n };\n }, [\n clearSelection,\n editor,\n isSelected,\n nodeKey,\n $onDelete,\n $onEnter,\n $onEscape,\n setSelected,\n ]);\n\n const draggable = isSelected && $isNodeSelection(selection);\n const isFocused = isSelected && isEditable;\n return (\n <Suspense fallback={null}>\n <>\n <span draggable={draggable}>\n {isEditable && (\n <Button\n className=\"image-edit-button absolute right-1 top-1\"\n variant={\"BorderStyle\"}\n ref={buttonRef}\n onClick={() => {\n showModal(\"Update Inline Image\", (onClose) => (\n <UpdateInlineImageDialog\n activeEditor={editor}\n nodeKey={nodeKey}\n onClose={onClose}\n />\n ));\n }}\n >\n Edit\n </Button>\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 position={position}\n />\n </span>\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 initialEditor={caption}>\n <AutoFocusPlugin />\n <LinkPlugin />\n <RichTextPlugin\n contentEditable={\n <ContentEditable\n placeholder=\"Enter a caption...\"\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 />\n }\n ErrorBoundary={LexicalErrorBoundary}\n />\n </LexicalNestedComposer>\n </div>\n )}\n </>\n {modal}\n </Suspense>\n );\n}\n"],"names":["imageCache","Set","LazyImage","altText","className","imageRef","src","width","height","position","has","Promise","resolve","img","Image","onload","add","useSuspenseImage","jsxRuntime","jsx","alt","ref","style","display","draggable","UpdateInlineImageDialog","activeEditor","nodeKey","onClose","node","getEditorState","read","$getNodeByKey","setAltText","useState","getAltText","showCaption","setShowCaption","getShowCaption","setPosition","getPosition","jsxs","children","Label","htmlFor","Input","id","placeholder","onChange","e","target","value","Select","onValueChange","SelectTrigger","SelectValue","SelectContent","SelectItem","Checkbox","checked","onCheckedChange","DialogFooter","Button","onClick","payload","update","caption","modal","showModal","useEditorModal","useRef","buttonRef","isSelected","setSelected","clearSelection","useLexicalNodeSelection","editor","useLexicalComposerContext","selection","setSelection","activeEditorRef","isEditable","useLexicalEditable","$onDelete","useCallback","deleteSelection","$getSelection","$isNodeSelection","preventDefault","getNodes","forEach","$isInlineImageNode","remove","$onEnter","event","latestSelection","buttonElem","current","length","$setSelection","focus","document","activeElement","$onEscape","parentRootElement","getRootElement","useEffect","isMounted","unregister","mergeRegister","registerUpdateListener","editorState","registerCommand","SELECTION_CHANGE_COMMAND","_","COMMAND_PRIORITY_LOW","CLICK_COMMAND","shiftKey","DRAGSTART_COMMAND","KEY_DELETE_COMMAND","KEY_BACKSPACE_COMMAND","KEY_ENTER_COMMAND","KEY_ESCAPE_COMMAND","isFocused","Suspense","fallback","Fragment","variant","LexicalNestedComposer","initialEditor","AutoFocusPlugin","o$1","LinkPlugin","RichTextPlugin","h","contentEditable","ContentEditable","placeholderClassName","ErrorBoundary","LexicalErrorBoundary"],"mappings":"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"}