import * as React from "react" import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext" import { $getSelection, $insertNodes, $isRangeSelection, $setSelection, COMMAND_PRIORITY_EDITOR, createCommand, } from "lexical" import type { RangeSelection } from "lexical" import { Button } from "@workspace/ui/components/button" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@workspace/ui/components/dialog" import { Field, FieldDescription, FieldGroup, FieldLabel, } from "@workspace/ui/components/field" import { Input } from "@workspace/ui/components/input" import { readFileAsDataUrl } from "../file-utils" import { useLexicalMessage } from "../i18n" import { lexicalMessages } from "../messages" import { $createLexicalMediaNode, type LexicalMediaKind, type LexicalMediaPayload, } from "../nodes/media-node" interface PendingMedia { kind: LexicalMediaKind selection: RangeSelection } export const OPEN_MEDIA_DIALOG_COMMAND = createCommand( "OPEN_MEDIA_DIALOG_COMMAND" ) export function LexicalMediaDialogPlugin() { const [editor] = useLexicalComposerContext() const [pendingMedia, setPendingMedia] = React.useState( null ) const [src, setSrc] = React.useState("") const [alt, setAlt] = React.useState("") const [caption, setCaption] = React.useState("") const [imageFileName, setImageFileName] = React.useState("") const [imageFileError, setImageFileError] = React.useState(false) const [isReadingImageFile, setIsReadingImageFile] = React.useState(false) const [poster, setPoster] = React.useState("") const imageFileInputRef = React.useRef(null) const imageFileReadId = React.useRef(0) const imageDescription = useLexicalMessage(lexicalMessages.imageDescription) const videoDescription = useLexicalMessage(lexicalMessages.videoDescription) const imageUrlLabel = useLexicalMessage(lexicalMessages.imageUrl) const imageFileLabel = useLexicalMessage(lexicalMessages.imageFile) const imageFileDescription = useLexicalMessage( lexicalMessages.imageFileDescription ) const chooseImageFileLabel = useLexicalMessage( lexicalMessages.chooseImageFile ) const readingImageFileLabel = useLexicalMessage( lexicalMessages.readingImageFile ) const imageFileReadErrorLabel = useLexicalMessage( lexicalMessages.imageFileReadError ) const videoUrlLabel = useLexicalMessage(lexicalMessages.videoUrl) const urlDescription = useLexicalMessage(lexicalMessages.urlDescription) const imageAltLabel = useLexicalMessage(lexicalMessages.imageAlt) const imageAltDescription = useLexicalMessage( lexicalMessages.imageAltDescription ) const videoPosterLabel = useLexicalMessage(lexicalMessages.videoPoster) const captionLabel = useLexicalMessage(lexicalMessages.caption) const cancelLabel = useLexicalMessage(lexicalMessages.cancel) const optionalLabel = useLexicalMessage(lexicalMessages.optional) React.useEffect( () => editor.registerCommand( OPEN_MEDIA_DIALOG_COMMAND, (kind) => { const selection = editor.getEditorState().read(() => { const currentSelection = $getSelection() return $isRangeSelection(currentSelection) ? currentSelection.clone() : null }) if (!selection) return false setSrc("") setAlt("") setCaption("") setImageFileName("") setImageFileError(false) setIsReadingImageFile(false) setPoster("") imageFileReadId.current += 1 if (imageFileInputRef.current) imageFileInputRef.current.value = "" setPendingMedia({ kind, selection }) return true }, COMMAND_PRIORITY_EDITOR ), [editor] ) const closeDialog = () => { imageFileReadId.current += 1 setPendingMedia(null) editor.focus() } const selectImageFile = () => imageFileInputRef.current?.click() const readImageFile = async (event: React.ChangeEvent) => { const file = event.target.files?.[0] if (!file) return const readId = imageFileReadId.current + 1 imageFileReadId.current = readId setImageFileName(file.name) setImageFileError(false) setIsReadingImageFile(true) setSrc("") try { const dataUrl = await readFileAsDataUrl(file) if (imageFileReadId.current === readId) setSrc(dataUrl) } catch { if (imageFileReadId.current === readId) { setImageFileError(true) setImageFileName("") } } finally { if (imageFileReadId.current === readId) setIsReadingImageFile(false) } } const insertMedia = (event: React.FormEvent) => { event.preventDefault() if (!pendingMedia) return const normalizedSrc = src.trim() if (!normalizedSrc) return const payload: LexicalMediaPayload = { alt: pendingMedia.kind === "image" ? alt.trim() || undefined : undefined, caption: caption.trim() || undefined, kind: pendingMedia.kind, poster: pendingMedia.kind === "video" ? poster.trim() || undefined : undefined, src: normalizedSrc, } editor.update(() => { $setSelection(pendingMedia.selection.clone()) $insertNodes([$createLexicalMediaNode(payload)]) }) closeDialog() } const isImage = pendingMedia?.kind === "image" const title = useLexicalMessage( isImage ? lexicalMessages.insertImage : lexicalMessages.insertVideo ) return ( { if (!open && pendingMedia) closeDialog() }} > editor.getRootElement()} showCloseButton={false} > {title} {isImage ? imageDescription : videoDescription}
{isImage ? imageUrlLabel : videoUrlLabel} { imageFileReadId.current += 1 setImageFileError(false) setImageFileName("") setIsReadingImageFile(false) setSrc(event.target.value) }} /> {urlDescription} {isImage ? ( <> {imageFileLabel}
{isReadingImageFile ? readingImageFileLabel : imageFileName || imageFileDescription}
{imageFileError && ( {imageFileReadErrorLabel} )}
{imageAltLabel} setAlt(event.target.value)} /> {imageAltDescription} ) : ( {videoPosterLabel} setPoster(event.target.value)} /> )} {captionLabel} setCaption(event.target.value)} />
) }