Skip to content

File Uploads

Galley does not upload files itself. It detects paste and drop operations, gives your app the files and editor selection, and inserts the Markdown returned by your handler.

<GalleyEditor
onFiles={async (input) => {
input.report({ phase: 'progress', progress: 0.2, message: 'Uploading' });
const markdown = await Promise.all(
input.files.map(async (file) => {
const url = await uploadFile(file);
return `![${file.name}](${url})`;
}),
);
input.report({ phase: 'progress', progress: 0.9, message: 'Finishing' });
return markdown;
}}
/>

Return values:

ReturnBehavior
stringInsert the string at the original paste/drop selection.
string[]Join with newlines and insert.
null or falseDo not insert anything. Use this when your app handled the files separately.
interface GalleyFileInput {
id: string;
files: File[];
source: 'paste' | 'drop';
event: ClipboardEvent | DragEvent;
view: EditorView;
selection: { from: number; to: number; anchor: number; head: number };
report(update: GalleyFileStatusUpdate): void;
}

selection is captured when the paste or drop happens, so async uploads can finish after the user moves the cursor.

Galley emits start before calling onFiles, complete after successful insertion, and error when the handler rejects.

<GalleyEditor
onFileStatus={(status) => {
console.log(status.id, status.phase, status.progress, status.message);
}}
onFileError={(error, input) => {
reportUploadError(error, input.files);
}}
/>

Use input.report() inside your handler for progress updates. Galley forwards those updates to onFileStatus and active upload renderers.

For app-level progress UI, keep a small upload log or status summary outside the editor:

const [uploads, setUploads] = useState<GalleyFileStatus[]>([]);
<GalleyEditor
onFiles={uploadFiles}
onFileStatus={(status) => {
setUploads((current) => [
...current.filter((upload) => upload.id !== status.id),
status,
]);
}}
footer={{
after: () => {
const active = uploads.filter((upload) => upload.phase !== 'complete');
return active.length ? <span>{active.length} upload active</span> : null;
},
}}
/>;
<GalleyEditor uploadInteraction="inline" />
<GalleyEditor uploadInteraction="overlay" />
<GalleyEditor uploadInteraction="locked" />
ModeBehavior
inlineShows editor-resident placeholders and drop indicators.
overlayAdds an aggregate overlay while uploads are active.
lockedShows the overlay and blocks document edits until uploads finish.

Renderers return an HTMLElement or null.

<GalleyEditor
uploadPlaceholderRenderer={(upload) => {
const node = document.createElement('span');
node.textContent = upload.message ?? 'Uploading...';
return node;
}}
uploadOverlayRenderer={(uploads) => {
const node = document.createElement('div');
node.textContent = `${uploads.length} upload in progress`;
return node;
}}
/>

Use a custom placeholder for inline progress when uploads are slow:

<GalleyEditor
onFiles={async (input) => {
for (let step = 1; step <= 4; step += 1) {
await wait(250);
input.report({
phase: 'progress',
progress: step / 4,
message: `Uploading ${input.files[0]?.name ?? 'file'}`,
});
}
return input.files.map((file) => `![${file.name}](${createAssetUrl(file)})`);
}}
uploadPlaceholderRenderer={(upload) => {
const node = document.createElement('span');
const percent = Math.round((upload.progress ?? 0) * 100);
node.textContent = `${upload.message ?? 'Uploading'} - ${percent}%`;
return node;
}}
/>

Use uploadInteraction="locked" when partial edits during upload would confuse the workflow, such as inserting a batch of dropped photos into a fixed section:

<GalleyEditor
onFiles={uploadFiles}
uploadInteraction="locked"
uploadOverlayRenderer={(uploads) => {
const node = document.createElement('div');
const done = uploads.filter((upload) => upload.phase === 'complete').length;
node.textContent = `${done}/${uploads.length} files uploaded`;
return node;
}}
/>

Keep custom upload UI concise and non-blocking unless you intentionally use locked mode. If uploads can fail, show a recovery path in your surrounding application UI through onFileError or app-owned status controls.