feat: add Clipboard.onFilePaste for server-side file paste handling#24485
Draft
Artur- wants to merge 10 commits into
Draft
feat: add Clipboard.onFilePaste for server-side file paste handling#24485Artur- wants to merge 10 commits into
Artur- wants to merge 10 commits into
Conversation
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.
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.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



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.