From: Adam Wagner Date: Thu, 11 May 2017 10:13:10 +0000 (+0300) Subject: Make it possible to upload files by dropping them onto a drop target (#9277) X-Git-Tag: 8.1.0.alpha8~41 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=e2e3058a497f43f34f2fcfadf6b63de9211be659;p=vaadin-framework.git Make it possible to upload files by dropping them onto a drop target (#9277) Fixes #8891 --- diff --git a/client/src/main/java/com/vaadin/client/extensions/FileDropTargetConnector.java b/client/src/main/java/com/vaadin/client/extensions/FileDropTargetConnector.java new file mode 100644 index 0000000000..f496e90d7f --- /dev/null +++ b/client/src/main/java/com/vaadin/client/extensions/FileDropTargetConnector.java @@ -0,0 +1,202 @@ +/* + * Copyright 2000-2016 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.client.extensions; + +import java.util.HashMap; +import java.util.Map; + +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.dom.client.DataTransfer; +import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.xhr.client.XMLHttpRequest; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.dnd.FileDropTargetClientRpc; +import com.vaadin.shared.ui.dnd.FileDropTargetRpc; +import com.vaadin.shared.ui.dnd.FileDropTargetState; +import com.vaadin.shared.ui.dnd.FileParameters; +import com.vaadin.ui.FileDropTarget; + +import elemental.events.Event; +import elemental.html.File; +import elemental.html.FileList; + +/** + * Extension to add file drop target functionality to a widget. It allows + * dropping files onto the widget and uploading the dropped files to the server. + * + * @author Vaadin Ltd + * @since 8.1 + */ +@Connect(FileDropTarget.class) +public class FileDropTargetConnector extends DropTargetExtensionConnector { + + /** + * Contains files and their IDs that are waiting to be uploaded. + */ + private Map filesToUpload = new HashMap<>(); + + /** + * Contains file IDs and upload URLs. + */ + private Map uploadUrls = new HashMap<>(); + + /** + * Counting identifier for the files to be uploaded. + */ + private int fileId = 0; + + /** + * Indicates whether a file is being uploaded. + */ + private boolean uploading = false; + + /** + * Constructs file drop target connector. + */ + public FileDropTargetConnector() { + registerRpc(FileDropTargetClientRpc.class, + (FileDropTargetClientRpc) urls -> { + uploadUrls.putAll(urls); + uploadNextFile(); + }); + } + + /** + * Uploads a file from the waiting list in case there are no files being + * uploaded. + */ + private void uploadNextFile() { + Scheduler.get().scheduleDeferred(() -> { + if (!uploading && uploadUrls.size() > 0) { + uploading = true; + String nextId = uploadUrls.keySet().stream().findAny().get(); + + String url = uploadUrls.remove(nextId); + File file = filesToUpload.remove(nextId); + + FileUploadXHR xhr = (FileUploadXHR) FileUploadXHR.create(); + xhr.setOnReadyStateChange(xmlHttpRequest -> { + if (xmlHttpRequest.getReadyState() == XMLHttpRequest.DONE) { + uploading = false; + uploadNextFile(); + xmlHttpRequest.clearOnReadyStateChange(); + } + }); + xhr.open("POST", getConnection().translateVaadinUri(url)); + xhr.postFile(file); + } + }); + } + + @Override + protected void onDrop(Event event) { + DataTransfer dataTransfer = ((NativeEvent) event).getDataTransfer(); + FileList files = getFiles(dataTransfer); + + if (files != null) { + Map fileParams = new HashMap<>(); + for (int i = 0; i < files.getLength(); i++) { + File file = files.item(i); + + // Make sure the item is indeed a file and not a folder + if (isFile(file, i, dataTransfer)) { + String id = String.valueOf(++this.fileId); + + filesToUpload.put(id, file); + fileParams.put(id, new FileParameters(file.getName(), + (long) file.getSize(), file.getType())); + } + } + + // Request a list of upload URLs for the dropped files + if (fileParams.size() > 0) { + getRpcProxy(FileDropTargetRpc.class).drop(fileParams); + } + } + + event.preventDefault(); + event.stopPropagation(); + } + + @Override + public FileDropTargetState getState() { + return (FileDropTargetState) super.getState(); + } + + /** + * Returns the files parameter of the dataTransfer object. + * + * @param dataTransfer + * DataTransfer object to retrieve files from. + * @return {@code DataTransfer.files} parameter of the given dataTransfer + * object. + */ + private native FileList getFiles(DataTransfer dataTransfer) + /*-{ + return dataTransfer.files; + }-*/; + + /** + * Checks whether the file on the given index is indeed a file or a folder. + * + * @param file + * File object to prove it is not a folder. + * @param fileIndex + * Index of the file object. + * @param dataTransfer + * DataTransfer object that contains the list of files. + * @return {@code true} if the given file at the given index is not a + * folder, {@code false} otherwise. + */ + private native boolean isFile(File file, int fileIndex, + DataTransfer dataTransfer) + /*-{ + // Chrome >= v21 and Opera >= v? + if (dataTransfer.items) { + var item = dataTransfer.items[fileIndex]; + if (typeof item.webkitGetAsEntry == "function") { + var entry = item.webkitGetAsEntry(); + if (typeof entry !== "undefined" && entry !== null) { + return entry.isFile; + } + } + } + + // Zero sized files without a type are also likely to be folders + if (file.size == 0 && !file.type) { + return false; + } + + // TODO Make it detect folders on all browsers + + return true; + }-*/; + + /** + * XHR that is used for uploading a file to the server. + */ + private static class FileUploadXHR extends XMLHttpRequest { + + protected FileUploadXHR() { + } + + public final native void postFile(File file) /*-{ + this.setRequestHeader('Content-Type', 'multipart/form-data'); + this.send(file); + }-*/; + + } +} diff --git a/documentation/advanced/advanced-dragndrop.asciidoc b/documentation/advanced/advanced-dragndrop.asciidoc index 57764b55dd..6390eff201 100644 --- a/documentation/advanced/advanced-dragndrop.asciidoc +++ b/documentation/advanced/advanced-dragndrop.asciidoc @@ -287,4 +287,68 @@ When dragging data over a drop target Grid's row, depending on the drop mode and (((range="endofrange", startref="term.advanced.dragndrop"))) +== Drag and Drop Files +Files can be uploaded to the server by dropping them onto a file drop target. To make a component a file drop target, apply the [classname]#FileDropTarget# extension to it by creating a new instance and passing the component as first constructor parameter to it. + +You can handle the dropped files with the `FileDropHandler` that you add as the second constructor parameter. The [classname]#FileDropEvent#, received by the handler, contains information about the dropped files such as file name, file size and mime type. +In the handler you can decide if you would like to upload each of the dropped files. + +To start uploading a file, set a `StreamVariable` to it. The stream variable provides an output stream where the file will be written and has callback methods for all the stages of the upload process. + +[source,java] +---- +Label dropArea = new Label("Drop files here"); +FileDropTarget