diff --git a/articles/flow/browser-apis/clipboard-api.adoc b/articles/flow/browser-apis/clipboard-api.adoc index 94e53aa4d1..bd0d0cbff6 100644 --- a/articles/flow/browser-apis/clipboard-api.adoc +++ b/articles/flow/browser-apis/clipboard-api.adoc @@ -1,8 +1,8 @@ --- title: Clipboard page-title: How to use the Clipboard API in Vaadin -description: Using the Clipboard API to copy text and HTML to the user's clipboard from the server. -meta-description: Learn how to copy text and HTML to the user's clipboard in Vaadin applications using the server-side Clipboard API. +description: Using the Clipboard API to copy text, HTML, and images to the user's clipboard, read clipboard content, and handle paste events from the server. +meta-description: Learn how to copy text, HTML, and images to the clipboard, read clipboard content, and handle paste events in Vaadin with the Clipboard API. order: 10 --- @@ -10,7 +10,7 @@ order: 10 = [since:com.vaadin:vaadin@V25.2]#Clipboard# :toc: -The [classname]`Clipboard` API lets server-side Java code copy text and HTML to the user's clipboard through the https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API[browser Clipboard API]. Actions are bound to a clickable component and run during the DOM event that fires the underlying click, so the browser sees a fresh user gesture and allows the write. +The [classname]`Clipboard` API lets server-side Java code copy text, HTML, and images to the user's clipboard through the https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API[browser Clipboard API]. Actions are bound to a clickable component and run during the DOM event that fires the underlying click, so the browser sees a fresh user gesture and allows the write. The same click binding can also <<#reading-the-clipboard,read the clipboard>>, and separate listeners react to browser paste events -- including <<#handling-pasted-files,pasted files>> -- without any click binding. [NOTE] The Clipboard API requires a secure context (HTTPS), except on `localhost` during development. Browsers also require the [code]`write` call to happen inside a short-lived user gesture (click, key press, …) -- writes triggered outside of a gesture, for example from a background thread or a timer, will fail with a [code]`NotAllowedError`. @@ -72,16 +72,39 @@ Clipboard.onClick(copy).writeHtml("Hello, world!"); Most targets fall back to a plain-text representation when only [code]`text/html` is present, but the fallback is browser-dependent. To control both representations explicitly, use the multi-format form below. +== Copying Images + +Use [methodname]`writeImage()` to copy a PNG version of an [code]`` element to the clipboard: + +[source,java] +---- +Image preview = new Image("images/chart.png", "Chart preview"); +Clipboard.onClick(copy).writeImage(preview); +---- + +The source can be a same-origin image, a [code]`data:` URL, or any cross-origin image served with matching CORS headers and [code]`crossorigin="anonymous"` on the [code]``. If the image is served from the server, pass a [classname]`DownloadHandler` instead: + +[source,java] +---- +Clipboard.onClick(copy).writeImage( + DownloadHandler.forClassResource(MainView.class, "chart.png")); +---- + +To include an image together with text or HTML, use [methodname]`ClipboardContent.image()` in the multi-format form. + + == Multi-Format Content -[classname]`ClipboardContent` packs several MIME types into a single clipboard item, so the paste target can pick the representation it understands. Set the [code]`text/plain` and [code]`text/html` slots independently; at least one must be set. +[classname]`ClipboardContent` packs several MIME types into a single clipboard item, so the paste target can pick the representation it understands. Set the [code]`text/plain`, [code]`text/html`, and [code]`image/png` slots independently; at least one must be set. [source,java] ---- +Image preview = new Image("images/chart.png", "Chart preview"); Button copy = new Button("Copy"); Clipboard.onClick(copy).write(ClipboardContent.create() .text("Hello, world!") - .html("Hello, world!")); + .html("Hello, world!") + .image(preview)); ---- The [methodname]`text()` setter also accepts a component, with the same client-side read semantics as [methodname]`writeText(Component)`: @@ -97,6 +120,45 @@ Clipboard.onClick(copy).write(ClipboardContent.create() [NOTE] The HTML slot only accepts a literal string -- there is no component overload. If the HTML depends on a field value, build it on the server before binding, or use the plain-text component overload alongside a static HTML literal. +[NOTE] +The image slot only accepts a component whose root element is an [code]``. If the image source is not already on the page, use [methodname]`writeImage(DownloadHandler)` to serve it through a hidden image element. + + +[#reading-the-clipboard] +== Reading the Clipboard + +The same [classname]`ClipboardBinding` that writes can also read. The [methodname]`read*` methods read the clipboard via [code]`navigator.clipboard.read()` when the bound click fires and deliver the result to a server-side callback. [methodname]`readText()` delivers the [code]`text/plain` content: + +[source,java] +---- +TextField address = new TextField("Address"); +Button paste = new Button("Paste"); +Clipboard.onClick(paste).readText( + text -> address.setValue(text != null ? text : ""), + error -> Notification.show("Couldn't read: " + error.message())); +---- + +Both consumers are required. The payload consumer receives [code]`null` when the clipboard is empty or has no representation of the requested type. [methodname]`readHtml()` works the same way for the [code]`text/html` representation. + +To receive both textual representations at once, use [methodname]`read()`. It delivers a [classname]`ClipboardPayload` record whose [methodname]`text()` and [methodname]`html()` accessors are each [code]`null` when the corresponding MIME type is not present on the clipboard item: + +[source,java] +---- +Clipboard.onClick(paste).read( + payload -> { + if (payload == null) { + Notification.show("Clipboard is empty"); + } else if (payload.html() != null) { + renderHtml(payload.html()); + } else { + renderText(payload.text()); + } + }, + error -> Notification.show("Couldn't read: " + error.message())); +---- + +[NOTE] +Reading is more restricted than writing: in addition to the user gesture, the browser requires the user to grant the [code]`clipboard-read` permission, typically through a prompt on the first read. If the permission is denied, the read is rejected with a [code]`NotAllowedError`, which is delivered to the [code]`onError` consumer -- see <<#handling-errors,Handling Errors>>. [#observing-the-outcome] @@ -123,6 +185,7 @@ Clipboard.onClick(copy).writeText(token, ---- +[#handling-errors] == Handling Errors The [methodname]`onError` consumer receives a [classname]`PromiseAction.Error` record with the browser's rejection details: @@ -157,3 +220,109 @@ The Clipboard API requires every write to happen inside a fresh user gesture. A - Writes cannot be initiated from a background thread, a scheduled task, or a server push. The gesture must originate in the browser. - Each click is one gesture. Chaining multiple [methodname]`write*` calls on the same binding attaches multiple actions to the same click, but each one needs to succeed against the browser's gesture budget. - If the application performs a long server round-trip before the write call, the browser may consider the gesture stale and reject the write. + +Reads bound to a click are subject to the same requirement -- the click satisfies the gesture, but the [code]`clipboard-read` permission is still checked separately. + + +[#handling-paste-events] +== Handling Paste Events + +[methodname]`Clipboard.onPaste(component, listener)` registers a server-side listener for the browser's native [code]`paste` event. Unlike the write and read APIs, it needs no click binding -- the listener is attached directly to the component's element and is invoked on the UI thread once per paste gesture targeting the component, or any of its descendants, since [code]`paste` bubbles. The listener receives a [classname]`PasteEvent` carrying the [code]`text/plain` and [code]`text/html` representations of the pasted content: + +[source,java] +---- +Div pasteTarget = new Div(); +pasteTarget.getElement().setAttribute("tabindex", "0"); +add(pasteTarget); + +Clipboard.onPaste(pasteTarget, event -> { + if (event.hasHtml()) { + renderHtml(event.getHtml()); + } else if (event.hasText()) { + renderText(event.getText()); + } +}); +---- + +The call returns a [classname]`Registration`; call [methodname]`remove()` on it to detach the listener. The component does not need to be attached at registration time -- the DOM listener is applied when the element is attached to a UI. + +[classname]`PasteEvent` exposes the paste through these methods: + +`getText()`, `getHtml()`:: The [code]`text/plain` and [code]`text/html` content, or [code]`null` when the paste did not include that representation. The browser returns an empty string both for an absent MIME type and for a pasted empty string; [classname]`PasteEvent` collapses both into [code]`null`, so [methodname]`hasText()` and [methodname]`hasHtml()` are sufficient checks. +`getTargetElement()`:: The closest Flow-tracked [classname]`Element` ancestor of the paste's DOM target, resolved against the live DOM in the browser. For a paste inside a Vaadin web component this is typically the component's host element. Use [methodname]`Element.getComponent()` to look up the enclosing component. Returns [code]`null` in the rare case that no Flow-tracked element encloses the target. +`getSource()`:: The component the listener was registered on. + +A few browser caveats apply: + +- The browser only fires [code]`paste` when the target element is focused at the moment the user invokes paste. Non-editable elements such as a plain [classname]`Div` must be made focusable, typically via [code]`tabindex="0"`. +- On editable targets ([code]``, [code]`