Skip to content

feat: add Clipboard.onFilePaste for server-side file paste handling#24485

Draft
Artur- wants to merge 10 commits into
mainfrom
feature/paste-file
Draft

feat: add Clipboard.onFilePaste for server-side file paste handling#24485
Artur- wants to merge 10 commits into
mainfrom
feature/paste-file

Conversation

@Artur-
Copy link
Copy Markdown
Member

@Artur- Artur- commented May 30, 2026

Pastes whose clipboard carries files (screenshots, files copied from a
file manager) flow through the supplied UploadHandler — one fetch POST
per file with X-Filename + raw body, matching vaadin-upload's wire
format. The client-side helper lives in flow-client/Clipboard.ts
(window.Vaadin.Flow.clipboard.uploadPastedFiles) so the Java side only
emits a one-line filter expression. Each fetch carries X-Paste-Id
(monotonic per browser tab) and X-Paste-File-Count headers so
server-side handlers can correlate the parallel uploads of one paste.

PasteFileHandler offers two flavours that consume those headers:
inMemory(consumer) delivers each file as a PasteFile (with newPaste()
flagging the first file of each paste), and session() builds a
three-step onStart / onFile / onComplete listener that knows when the
whole paste has finished delivering. Sessions are per paste id and run
independently — a newer paste does not cancel the still-in-flight
uploads of an older paste, so two pastes in sequence (or interleaved)
both complete on their own timelines.

onFilePaste is independent of onPaste; both fire on the same gesture
when both are registered.

Artur- added 9 commits May 29, 2026 22:21
Adds writeImage(Component) — fire-and-forget and observed flavours — on
ClipboardBinding, plus image(Component) on ClipboardContent for the
multi-format path. The source can be any rasterisable image
({@code image/png}, {@code image/jpeg}, {@code image/svg+xml}, ...) with
intrinsic dimensions; cross-origin sources need
{@code crossorigin="anonymous"} on the {@code <img>} plus matching CORS
headers, otherwise the canvas is tainted and the write fails.

Internals:
  - ImageBlobInput extends Action.Input<Object>; its toJs yields the
    source <img> element verbatim.
  - WriteToClipboardAction gains a third (image) slot; the rendered
    helper call is writePayload($0(event), $1(event), $2(event)).
  - The TS helper writeClipboardPayload takes an HTMLImageElement third
    argument and re-encodes it via imageToPngBlob (canvas + toBlob).
    The resulting Promise<Blob> is fed directly to ClipboardItem so the
    navigator.clipboard.write call stays synchronous inside the user
    gesture — Safari otherwise loses activation on the first await.

Tests cover the image/png slot and the multi-format case with all three
slots together.
…ardAction

Restores the original 2-arg (text, html) and 4-arg observed constructors
that main had, and adds matching 1-arg (image) and 3-arg observed image
constructors. The 3-arg / 5-arg multi-format constructors stay as the
underlying implementation that the dedicated overloads delegate into
via this(...). Callers in the typical text-only, html-only, or image-only
shapes no longer need to pass null placeholders for the unused slots.

ClipboardBinding's write* methods, SignalInput/LiteralInput Javadoc
snippets, SignalInputTest and TriggerWriteToClipboardView are all
adjusted to use the dedicated overloads.
ClipboardContent is now a passive data holder with public slot getters;
WriteToClipboardAction gains constructors that accept a ClipboardContent
and read its slots. The text+html and image dedicated constructors
delegate into private 3-arg / 5-arg primitives, so the previously
public multi-format Action.Input constructors disappear from the API
surface.

ClipboardBinding.write(content[, callbacks]) now constructs the action
as new WriteToClipboardAction(content[, …]). The two existing test
cases that called the input-based 3-arg constructor directly now go
through ClipboardContent.
Adds writeImage(DownloadHandler) and its observed counterpart on
ClipboardBinding so server-defined image bytes can be copied to the
clipboard without the caller having to add a hidden Image to the page
themselves. The overload appends a display:none <img> child to the
trigger host, bound to the handler via the same setAttribute path
Image.setSrc(DownloadHandler) uses, then routes it through
ImageBlobInput.

ImageBlobInput gains an Element-accepting constructor so the binding
can hand it the freshly built <img> element without wrapping it in a
Component.

The browser begins fetching the image as soon as the binding is set
up, so the bytes are typically decoded before the user clicks. If the
click races the load, ImageBlobInput's canvas converter falls back to
the <img>'s load event before drawing.
Adds three IT scenarios to TriggerWriteToClipboardView/IT:
  - writeImage(Image) with an in-DOM data-URL <img>
  - write(ClipboardContent.text + image) packing both into one
    ClipboardItem
  - writeImage(DownloadHandler) with a server-served PNG generated
    at view-class load via ImageIO

The recording shim in the IT awaits Promise<Blob> entries from the
ClipboardItem and normalises them to {type, size}, so the assertions
can inspect the resulting image/png blob without dealing with binary
content.
Restructures TriggerWriteToClipboardView into headed sections, each
with a one-line description of what should land in the clipboard so a
manual tester can paste into an external app and verify. Adds:
  - visible 32x32 image sources (was 4x4; too small to see)
  - a distinct blue image for writeImage(DownloadHandler), so a
    pasted result makes it obvious which button was used
  - a "Copy text + html + image" button exercising all three slots in
    one ClipboardItem, with a matching IT case
Adds Clipboard.onPaste(Component, listener) — and a PasteOptions
overload — that forwards the browser's native paste event to a
server-side listener as a PasteEvent carrying text/plain, text/html,
the source Component, and the closest Flow-tracked target Element.
Pass any Component for scope: the listener fires for pastes targeting
that component or its descendants. For UI-wide scope, pass the UI.

Internals:
  - The listener is a plain Element.addEventListener("paste", ...)
    wrapper. Two addEventData JS expressions pull text/plain and
    text/html out of event.clipboardData; `?.` guards synthetic events
    without a DataTransfer and `|| null` collapses the browser's "" to
    JSON null at the wire boundary so callers don't need !isEmpty()
    guards.
  - PasteOptions.defaults() installs a setFilter that walks
    event.composedPath() and rejects pastes whose target (or any
    ancestor in the composed path, including through open shadow DOMs
    like <vaadin-text-field>'s inner <input>) is an input, textarea,
    or contenteditable element. Filtering happens client-side, so
    skipped pastes never round-trip. PasteOptions.includingInputFields()
    is the no-filter form and is the default when no options are
    passed.
  - getTargetElement() is populated via mapEventTargetElement(): the
    browser walks event.target's DOM ancestors to find the closest
    state-node element. Using composedPath via that mechanism means
    the result reflects DOM hierarchy, not the server-side state tree
    (which can diverge for virtual children, slotted content, etc.).
    Callers reach the enclosing Component via Element.getComponent().
  - No UI.getCurrent dependency: the DOM listener binds directly to
    the component's element and is applied on attach, so callers from
    a background thread can register without needing the UI lock to
    set up.

Out of scope for this commit: file/image paste, copy/cut listeners,
and a preventDefault hook.

Tests cover the wiring (component + UI hosts, with and without a
current UI), and a TriggerPasteIT dispatches synthetic ClipboardEvents
with a populated DataTransfer to verify text-only, html-only, both,
neither, empty-string collapse, and the editable-target filter (a
paste on a sibling <input> is skipped by the default options and
fires only with includingInputFields()).
flow-server doesn't depend on flow-html-components, so
{@link com.vaadin.flow.component.html.Div Div} fails to resolve when
the Javadoc tool runs in CI. Drop the link and use {@code Div}
instead. The {@link UI} references were unqualified and PasteEvent /
Clipboard sit in com.vaadin.flow.component.clipboard, a different
package from UI, so the short form doesn't resolve either; spell out
{@link com.vaadin.flow.component.UI UI} explicitly.
Pastes whose clipboard carries files (screenshots, files copied from a
file manager) flow through the supplied UploadHandler — one fetch POST
per file with X-Filename + raw body, matching vaadin-upload's wire
format. The client-side helper lives in flow-client/Clipboard.ts
(window.Vaadin.Flow.clipboard.uploadPastedFiles) so the Java side only
emits a one-line filter expression. Each fetch carries X-Paste-Id
(monotonic per browser tab) and X-Paste-File-Count headers so
server-side handlers can correlate the parallel uploads of one paste.

PasteFileHandler offers two flavours that consume those headers:
inMemory(consumer) delivers each file as a PasteFile (with newPaste()
flagging the first file of each paste), and session() builds a
three-step onStart / onFile / onComplete listener that knows when the
whole paste has finished delivering. Sessions are per paste id and run
independently — a newer paste does not cancel the still-in-flight
uploads of an older paste, so two pastes in sequence (or interleaved)
both complete on their own timelines.

onFilePaste is independent of onPaste; both fire on the same gesture
when both are registered.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 30, 2026

Test Results

 1 433 files  + 3   1 433 suites  +3   1h 22m 7s ⏱️ - 2m 37s
10 084 tests +32  10 016 ✅ +32  68 💤 ±0  0 ❌ ±0 
10 556 runs  +32  10 487 ✅ +32  69 💤 ±0  0 ❌ ±0 

Results for commit bec5ac9. ± Comparison against base commit b238ec1.

♻️ This comment has been updated with latest results.

FlowClassesSerializableTest and HtmlComponentSerializableTest walk
every public class in the module and run it through an
ObjectOutputStream. PasteFileHandler is a static-factory utility class
(private constructor, no fields) but the test still expects every
public class in the package to implement Serializable, mirroring
Clipboard. Add the interface — no instance state to worry about.
@sonarqubecloud
Copy link
Copy Markdown

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant