-
Notifications
You must be signed in to change notification settings - Fork 204
feat: ClipboardReadAction + typed JsBuilder.callback primitive #24397
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| /* | ||
| * Copyright 2000-2026 Vaadin Ltd. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); you may not | ||
| * use this file except in compliance with the License. You may obtain a copy of | ||
| * the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||
| * License for the specific language governing permissions and limitations under | ||
| * the License. | ||
| */ | ||
|
|
||
| /** | ||
| * Textual clipboard payload sent to the server, matching the Java | ||
| * ClipboardPayload record. | ||
| */ | ||
| interface VaadinClipboardPayload { | ||
| text: string | null; | ||
| html: string | null; | ||
| } | ||
|
|
||
| /** | ||
| * Reads the first item from the system clipboard and returns its text/plain | ||
| * and text/html representations. Either field is {@code null} if the | ||
| * corresponding MIME type is not present. | ||
| * | ||
| * The caller is expected to be inside a transient user gesture and to have | ||
| * been granted the {@code clipboard-read} permission; otherwise | ||
| * {@code navigator.clipboard.read} rejects and this function propagates the | ||
| * rejection. | ||
| */ | ||
| async function readClipboardPayload(): Promise<VaadinClipboardPayload | null> { | ||
| const items = await navigator.clipboard.read(); | ||
| if (!items.length) { | ||
| return null; | ||
| } | ||
| const item = items[0]; | ||
| const get = async (type: string): Promise<string | null> => | ||
| item.types.includes(type) ? (await item.getType(type)).text() : null; | ||
| return { | ||
| text: await get('text/plain'), | ||
| html: await get('text/html') | ||
| }; | ||
| } | ||
|
|
||
| const $wnd = window as any; | ||
| $wnd.Vaadin ??= {}; | ||
| $wnd.Vaadin.Flow ??= {}; | ||
| $wnd.Vaadin.Flow.clipboard = { | ||
| readPayload: readClipboardPayload | ||
| }; | ||
|
|
||
| // Empty export to ensure TypeScript emits this as an ES module, | ||
| // which is required for Vite to load it via import. | ||
| export {}; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| /* | ||
| * Copyright 2000-2026 Vaadin Ltd. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); you may not | ||
| * use this file except in compliance with the License. You may obtain a copy of | ||
| * the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||
| * License for the specific language governing permissions and limitations under | ||
| * the License. | ||
| */ | ||
| package com.vaadin.flow.component.trigger.internal; | ||
|
|
||
| import java.io.Serializable; | ||
|
|
||
| import org.jspecify.annotations.Nullable; | ||
|
|
||
| /** | ||
| * Textual clipboard contents delivered to {@link ClipboardReadAction}'s | ||
| * handler. Either field may be {@code null} if the corresponding MIME type was | ||
| * not present on the clipboard item. | ||
| * <p> | ||
| * For internal use only. May be renamed or removed in a future release. | ||
| * | ||
| * @param text | ||
| * {@code text/plain} contents, or {@code null} if not present | ||
| * @param html | ||
| * {@code text/html} contents, or {@code null} if not present | ||
| */ | ||
| public record ClipboardPayload(@Nullable String text, | ||
| @Nullable String html) implements Serializable { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| /* | ||
| * Copyright 2000-2026 Vaadin Ltd. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); you may not | ||
| * use this file except in compliance with the License. You may obtain a copy of | ||
| * the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||
| * License for the specific language governing permissions and limitations under | ||
| * the License. | ||
| */ | ||
| package com.vaadin.flow.component.trigger.internal; | ||
|
|
||
| import java.util.Objects; | ||
|
|
||
| import org.jspecify.annotations.Nullable; | ||
|
|
||
| import com.vaadin.flow.function.SerializableConsumer; | ||
|
|
||
| /** | ||
| * Reads the user's clipboard via {@code navigator.clipboard.read()} when the | ||
| * bound trigger fires and delivers the textual contents to the handler on the | ||
| * UI thread. | ||
| * <p> | ||
| * The Clipboard API requires the call to happen inside a short-lived user | ||
| * gesture (click, key press, …) AND the user to grant the | ||
| * {@code clipboard-read} permission — without both, the browser rejects the | ||
| * read. Bind this action to a {@link Trigger} that fires during such a gesture, | ||
| * typically a {@link ClickTrigger}. | ||
| * <p> | ||
| * The handler receives a {@link ClipboardPayload} with the {@code text/plain} | ||
| * and {@code text/html} representations of the first clipboard item, or | ||
| * {@code null} if the read fails for any reason (permission denied, no item, | ||
| * unsupported browser, …). Error detail is intentionally not exposed — see | ||
| * {@link PromiseAction}'s subclasses for the success/error-split shape if that | ||
| * distinction matters. | ||
| * | ||
| * <pre>{@code | ||
| * new ClickTrigger(pasteButton).triggers(new ClipboardReadAction(payload -> { | ||
| * if (payload == null) { | ||
| * notification.show("Clipboard read denied"); | ||
| * } else { | ||
| * editor.setValue( | ||
| * payload.html() != null ? payload.html() : payload.text()); | ||
| * } | ||
| * })); | ||
| * }</pre> | ||
| * | ||
| * For internal use only. May be renamed or removed in a future release. | ||
| */ | ||
| public class ClipboardReadAction extends Action { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One possible potential improvement could be if What would get better:
Then: And there one "but": this is an internal API so far, so no high priority and maybe no need for now - just something to think about in the code design. |
||
|
|
||
| private final SerializableConsumer<@Nullable ClipboardPayload> handler; | ||
|
|
||
| /** | ||
| * Creates an action that reads the user's clipboard and delivers the | ||
| * contents to {@code handler}. | ||
| * | ||
| * @param handler | ||
| * invoked on the UI thread with the clipboard contents, or | ||
| * {@code null} if the read failed; not {@code null} | ||
| */ | ||
| public ClipboardReadAction( | ||
| SerializableConsumer<@Nullable ClipboardPayload> handler) { | ||
| this.handler = Objects.requireNonNull(handler, | ||
| "handler must not be null"); | ||
| } | ||
|
|
||
| @Override | ||
| protected void appendStatement(JsBuilder builder, StringBuilder out) { | ||
| String cb = builder.callback(ClipboardPayload.class, handler); | ||
| // The actual clipboard read + {text, html} extraction lives in | ||
| // Clipboard.ts (window.Vaadin.Flow.clipboard.readPayload); this | ||
| // Action just routes its resolved value (or null on any failure) | ||
| // to the typed server callback. | ||
| out.append("window.Vaadin.Flow.clipboard.readPayload()") | ||
| .append(".then(p=>").append(cb).append("(p))") | ||
| .append(".catch(()=>").append(cb).append("(null))"); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the future we may want to add more content types (already have plans for images), this can bring incompatible changes. But not blocking this intermediate low level PR I think.