Files
simple-react-app-kit/packages/lexical/src/plugins/media-dialog-plugin.tsx
T

216 lines
6.6 KiB
TypeScript
Raw Normal View History

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 { 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<LexicalMediaKind>(
"OPEN_MEDIA_DIALOG_COMMAND"
)
export function LexicalMediaDialogPlugin() {
const [editor] = useLexicalComposerContext()
const [pendingMedia, setPendingMedia] = React.useState<PendingMedia | null>(
null
)
const [src, setSrc] = React.useState("")
const [alt, setAlt] = React.useState("")
const [caption, setCaption] = React.useState("")
const [poster, setPoster] = React.useState("")
const imageDescription = useLexicalMessage(lexicalMessages.imageDescription)
const videoDescription = useLexicalMessage(lexicalMessages.videoDescription)
const imageUrlLabel = useLexicalMessage(lexicalMessages.imageUrl)
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("")
setPoster("")
setPendingMedia({ kind, selection })
return true
},
COMMAND_PRIORITY_EDITOR
),
[editor]
)
const closeDialog = () => {
setPendingMedia(null)
editor.focus()
}
const insertMedia = (event: React.FormEvent<HTMLFormElement>) => {
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 (
<Dialog
open={pendingMedia !== null}
onOpenChange={(open) => {
if (!open && pendingMedia) closeDialog()
}}
>
<DialogContent
finalFocus={() => editor.getRootElement()}
showCloseButton={false}
>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>
{isImage ? imageDescription : videoDescription}
</DialogDescription>
</DialogHeader>
<form className="contents" onSubmit={insertMedia}>
<FieldGroup className="gap-4">
<Field>
<FieldLabel htmlFor="lexical-media-src">
{isImage ? imageUrlLabel : videoUrlLabel}
</FieldLabel>
<Input
autoFocus
id="lexical-media-src"
inputMode="url"
placeholder={
isImage
? "https://example.com/image.jpg"
: "https://example.com/video.mp4"
}
required
value={src}
onChange={(event) => setSrc(event.target.value)}
/>
<FieldDescription>{urlDescription}</FieldDescription>
</Field>
{isImage ? (
<Field>
<FieldLabel htmlFor="lexical-media-alt">
{imageAltLabel}
</FieldLabel>
<Input
id="lexical-media-alt"
placeholder={imageAltLabel}
value={alt}
onChange={(event) => setAlt(event.target.value)}
/>
<FieldDescription>{imageAltDescription}</FieldDescription>
</Field>
) : (
<Field>
<FieldLabel htmlFor="lexical-media-poster">
{videoPosterLabel}
</FieldLabel>
<Input
id="lexical-media-poster"
inputMode="url"
placeholder="https://example.com/poster.jpg"
value={poster}
onChange={(event) => setPoster(event.target.value)}
/>
</Field>
)}
<Field>
<FieldLabel htmlFor="lexical-media-caption">
{captionLabel}
</FieldLabel>
<Input
id="lexical-media-caption"
placeholder={optionalLabel}
value={caption}
onChange={(event) => setCaption(event.target.value)}
/>
</Field>
</FieldGroup>
<DialogFooter>
<Button type="button" variant="outline" onClick={closeDialog}>
{cancelLabel}
</Button>
<Button type="submit">{title}</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}