feat(lexical): support local image data URLs

Allow the image insertion dialog to select a local image and embed it as a Base64 data URL, while retaining URL-based insertion and leaving video URL-only.

Extract shared abortable FileReader handling for clipboard uploads and dialog selection; add English and Simplified Chinese messages plus an interaction test covering file selection through insertion.
This commit is contained in:
Maofeng
2026-07-31 16:03:54 +08:00
parent cbf5ee784f
commit 263b400860
7 changed files with 274 additions and 87 deletions
@@ -19,6 +19,7 @@ import {
deleteImageUploadState,
setImageUploadState,
} from "../image-upload-store"
import { readFileAsDataUrl } from "../file-utils"
import {
$createLexicalMediaNode,
$isLexicalMediaNode,
@@ -65,70 +66,6 @@ function isClipboardEvent(
return "clipboardData" in event && event.clipboardData != null
}
function readFileAsDataUrl(
file: File,
signal: AbortSignal,
reportProgress: (progress: number) => void
) {
return new Promise<string>((resolve, reject) => {
const reader = new FileReader()
const handleSignalAbort = () => reader.abort()
const cleanUp = () => signal.removeEventListener("abort", handleSignalAbort)
if (signal.aborted) {
reject(new DOMException("The image read was aborted.", "AbortError"))
return
}
signal.addEventListener("abort", handleSignalAbort, { once: true })
reader.addEventListener(
"error",
() => {
const error =
reader.error ?? new Error(`Unable to read "${file.name}".`)
cleanUp()
reject(error)
},
{ once: true }
)
reader.addEventListener(
"abort",
() => {
cleanUp()
reject(new DOMException("The image read was aborted.", "AbortError"))
},
{ once: true }
)
reader.addEventListener("progress", (event) => {
if (event.lengthComputable && event.total > 0) {
reportProgress(event.loaded / event.total)
}
})
reader.addEventListener(
"load",
() => {
const result = reader.result
cleanUp()
if (typeof result === "string") {
resolve(result)
} else {
reject(new Error(`Unable to read "${file.name}" as a data URL.`))
}
},
{ once: true }
)
try {
reader.readAsDataURL(file)
} catch (error) {
cleanUp()
reject(error)
}
})
}
async function resolveDefaultImage(
file: File,
signal: AbortSignal,
@@ -143,7 +80,7 @@ async function resolveDefaultImage(
return {
alt: file.name,
src: await readFileAsDataUrl(file, signal, reportProgress),
src: await readFileAsDataUrl(file, { onProgress: reportProgress, signal }),
}
}