Vaadin 23: file upload from clipboard / Ctrl+V

366 views Asked by At

My need: I'd like to add an "upload from clipboard" functionality into a Vaadin 23 application so that the user can paste a screenshot into an Upload field.

Known pieces of the puzzle: I know that there is a paste event (see here https://stackoverflow.com/a/51586232/10318272 or here https://developer.mozilla.org/en-US/docs/Web/API/Element/paste_event ) and there's the Vaadin Upload component ( https://vaadin.com/docs/latest/components/upload ).

Question: How can I transfer the pasted data into the Upload field?

1

There are 1 answers

0
S. Doe On BEST ANSWER

Why initially intended solution does not work: It seems that uploading a screenshot via an Upload field is not feasible because the FileList (= model of a file input field) does not allow to add/append a new File object.

(Working) Workaround: So my workaround is a TextArea with a paste-EventListener that does a remote procedure call of a @ClientCallable method at the server.

Left component is the TextArea, right component is a preview Image.

TextArea plus preview Image

Code:

import com.vaadin.flow.component.ClientCallable;
import com.vaadin.flow.component.html.Image;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.textfield.TextArea;
import com.vaadin.flow.server.StreamResource;

import java.io.ByteArrayInputStream;
import java.util.Base64;

public class PasteScreenshotField extends HorizontalLayout {

private final Image previewImg;

public PasteScreenshotField() {
    // This could be any focusable type of component, I guess.
    TextArea textField = new TextArea();
    textField.setWidth("50px");
    textField.setHeight("50px");
    this.add(textField);

    String pasteFunction = "for(const item of event.clipboardData.items) {"
                           + "  if(item.type.startsWith(\"image/\")) {"
                           + "    var blob = item.getAsFile();"
                           + "    var reader = new FileReader();"
                           + "    reader.onload = function(onloadevent) {$1.$server.upload(onloadevent.target.result);};"
                           + "    reader.readAsDataURL(blob);"
                           + "  }"
                           + "}";
    this.getElement().executeJs("$0.addEventListener(\"paste\", event => {"+pasteFunction+"})", textField.getElement(), this);

    // Optional: Preview of the uploaded screenshot
    previewImg = new Image();
    // TODO: Fixed size of 50px x 50px stretches the image. Could be better.
    previewImg.setWidth("50px");
    previewImg.setHeight("50px");
    this.add(previewImg);
}

@ClientCallable()
private void upload(String dataUrl) {
    System.out.println("DataUrl: "+dataUrl);
    if (dataUrl.startsWith("data:")) {
        byte[] imgBytes = Base64.getDecoder().decode(dataUrl.substring(dataUrl.indexOf(',') + 1));
        // Showing a preview is just one of the possible scenarios.
        // TODO: check filename extension. Maybe it's not a png.
        previewImg.setSrc(new StreamResource("preview.png", () -> new ByteArrayInputStream(imgBytes)));
    }
}

}

Extendability: Instead of previewImg.setSrc you could do whatever you want with the uploaded file. The preview is just the proof that the screenshot goes to the server (and could go back to the client again).

Possible connection to Upload component:

If you've got an Upload component and want to extend it with this paste functionality, you can register the paste listener at the Upload component (or at some other component) and instead of previewImg.setSrc you just call this (whereas the onSucceededRunner is a BiConsumer<String, String> in my case that runs the onSucceeded stuff (updating thumbnails, setting attributes at the bound bean, ...)):

    String filename = "screenshot.png";
    String mimeType = "image/png";
    OutputStream outputStream = uploadField.getReceiver().receiveUpload(filename, mimeType);
    outputStream.write(imgBytes);
    outputStream.flush();
    outputStream.close();
    onSucceededRunner.accept(filename, mimeTypeString);

Final result:

This is what my custom upload field looks like in the end (assembled from the code above plus an Upload field plus a TextField for the file name and a thumbnail preview). The user now has to click somewhere at that field (=focus it, because there could be more than one in a form), press Ctrl+V and then a screenshot gets uploaded (if there is any) from clipboard to Vaadin application at the server.

My custom Upload field