Skip to content

[lexical-playground] Feature: Add file attachment support with pluggable storage abstraction#8084

Open
Yjason-K wants to merge 8 commits intofacebook:mainfrom
Yjason-K:main
Open

[lexical-playground] Feature: Add file attachment support with pluggable storage abstraction#8084
Yjason-K wants to merge 8 commits intofacebook:mainfrom
Yjason-K:main

Conversation

@Yjason-K
Copy link

Description

This PR is a revised implementation of #7895, addressing the feedback about storage architecture.

Changes from previous PR (#7895)

Feedback addressed:

  • Before: Base64 encoding directly in document (not suitable for production)
  • After: Pluggable AttachmentStore interface allowing custom backend implementations (S3, Cloudflare R2, etc.)

New architecture:

  • AttachmentStore interface with upload(), delete(), getUrl(), toBase64() methods
  • DemoAttachmentStore: Default blob URL implementation for playground demo
  • AttachmentStoreContext: React context for dependency injection
  • attachmentId field for store reference instead of embedding file data
  • Configurable serializeAsBase64 option for self-contained export when needed
  • Console warning for demo mode to guide production usage

Core Implementation (from #7895)

  • AttachmentNode: Decorator node for file metadata (name, size, type, URL)
  • AttachmentComponent: React component with file type icons and controls
  • AttachmentPlugin: Drag & drop, file insertion commands, attachment management
  • File validation: Multiple file types with 3MB size limit
  • UI integration: Toolbar button with file upload dialog

Test plan

Storage Abstraction

  • DemoAttachmentStore creates blob URLs for session-scoped files
  • Console warning displayed for demo mode
  • attachmentId properly persisted in serialization
  • Custom store can be injected via AttachmentStoreProvider

File Upload & Display

  • Toolbar dialog with file preview and validation
  • Drag & drop file upload
  • File type icons (PDF, Word, Excel, etc.)
  • Keyboard navigation and deletion

Serialization

  • JSON export/import with base64 (configurable)
  • DOM export/import maintains attachment data
  • Object URL cleanup prevents memory leaks

E2E Test Coverage

  • File upload via dialog
  • Selection and floating toolbar
  • Download functionality
  • Keyboard navigation
  • Copy/Paste support
  • Multiple attachment management

Related

Closes #7895 (supersedes with improved architecture)

- Created AttachmentNode and AttachmentPlugin to handle file attachments.
- Added AttachmentComponent for rendering file previews with metadata (name, size, type).
- Integrated attachment insertion into the toolbar and drag-and-drop/paste workflow.
- Supported various file types including PDFs, documents, spreadsheets, videos, and audio.
- Implemented base64 serialization for non-URL-based attachments to ensure persistence in JSON.
- Added a 3MB file size limit for playground attachments.
- Added styling and a new paperclip icon for the attachment UI.
- Add AttachmentStore and DemoAttachmentStore for managed file handling.
- Introduce AttachmentStoreContext for global store access.
- Update AttachmentNode to include attachmentId for reference-based storage.
- Update plugins to utilize the store for file uploads and serialization.
- Inserting attachments via the toolbar.
- Selection and floating toolbar visibility.
- File download functionality from the floating toolbar.
- Keyboard navigation (arrow keys) around attachment nodes.
- Deletion via Backspace, Delete keys, and the toolbar button.
- Copy/Paste support for attachment nodes.
- Multiple attachment management.
- Proper rendering and HTML structure verification.
@meta-cla meta-cla bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Jan 18, 2026
@vercel
Copy link

vercel bot commented Jan 18, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lexical Ready Ready Preview, Comment Feb 16, 2026 11:53am
lexical-playground Ready Ready Preview, Comment Feb 16, 2026 11:53am

Request Review

@vadimkantorov
Copy link

vadimkantorov commented Jan 19, 2026

A bit related, in my image component / image upload experiments (for GitHub repo as a storage), I implemented the following design:

  1. First image is added to the document via blob URL
  2. There is a way to enumerate all inserted images (which are not uploaded yet and have temporary blob URLs) - so that all new images can be committed at once in a commit / PR (by the user who invokes the storage backend themselves)
  3. After the user commits changes, there is a way for replacing in the document temporary blob URLs to the committed real servable URLs from the storage backend

I think this kind of abstraction (temporary and final URLs) can be useful for other image/media storage backends too (and can support the notion of "commit" where we don't want to upload anything if the user hasn't "committed"/"approved" or when it can take time for the uploaded document to become available for serving via final https url - true for SSG sites)

Also, for practical dealing with images, some way for installing preprocessing hooks can be needed: e.g. resizing in multiple sizes before uploading to the static storage

@levensta
Copy link
Contributor

Hi, @vadimkantorov, I also implemented a similar image loading mechanism and for this I added a callback that communicates between the editor and my main application which implements the interface for user commits changes. Here's what the interface for such a callback might look like:

type FileLoadCallback = (
  // File object from URL.createObjectURL or FileReader
  file: File,
  // Signal about deleting a file from the lexical document
  signal: AbortSignal,
  // Callback for successful file upload to the server
  onSuccess: (
    // The final source to the uploaded file on the server (S3, Cloudflare R2, etc.)
    src: string
  ) => Promise<void>,
  // Callback for failed file upload
  onError: (
    // Error message
    error: string
  ) => Promise<void>
) => void;

It can be called every time an image or other resource is added to the document, and AbortController can be called every time that resource is removed, so that the main application can filter the list of resources and load only the relevant files when a user saves changes.

I think AttachmentStore abstraction provides a great interface that can be implemented in your own way, including for correctly applying user changes

I'm interested in this PR, so maybe my idea will help you come up with concrete ways to improve @Yjason-K implementation

@etrepum etrepum added the extended-tests Run extended e2e tests on a PR label Jan 20, 2026
@vadimkantorov
Copy link

vadimkantorov commented Jan 21, 2026

@levensta If you feel like checking my design, it's at https://github.com/vadimkantorov/moncms

One nugget is I have a "url resolver" hook, so that the url in object model (e.g. relative image urls in markdown) can be dynamically resolved into a real servable image url (in the simplest case it means just prepending a prefix)

In https://github.com/vadimkantorov/moncms/blob/master/src/main.tsx it's implemented in the ImageCache abstraction

Copy link
Collaborator

@etrepum etrepum left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't done a close look at this PR because it has not had a successful e2e test run

@etrepum
Copy link
Collaborator

etrepum commented Feb 9, 2026

It still doesn't pass tests, something in here is probably not backwards compatible.

- Add waitForSelector in insertAttachment test helper to prevent race condition
  when modal has not fully mounted before file input interaction
- Fix showFloatingToolbar toggle bug: clicking attachment toggled toolbar off
  immediately (true→false) instead of showing it; now always sets true on select
- Add KEY_BACKSPACE_COMMAND handler alongside KEY_DELETE_COMMAND for consistent
  deletion behavior
- Use showFloatingToolbar state in render condition so dismiss actually works
- Lazy-initialize drag image element to avoid module-scope document access
@etrepum
Copy link
Collaborator

etrepum commented Feb 14, 2026

Doesn't look like those changes fixed the failures

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. extended-tests Run extended e2e tests on a PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

Comments