Michael Eriksen
Michael Eriksen

Reputation: 155

Drag'n'drop of multiple files into browser only works in Firefox - not in Chrome or Edge

        function allowDropFiles(event) {
            event.preventDefault();
        }

        function readFile(file) {
            return new Promise((resolve, reject) => {
                var fr = new FileReader();
                fr.onload = () => {
                    resolve(fr.result)
                };
                fr.onerror = reject;
                fr.readAsDataURL(file);
            });
        }

        async function DropFiles(event) {
            event.preventDefault();
            var vVoucherArray = [];
            var vFileNameArray = [];
            var vFileContentArray = [];
            voucherNo = event.target.id.substring(4, event.target.id.length);
            if (event.dataTransfer.items) {
                for (var i = 0; i < event.dataTransfer.items.length; i++) {
                    if (event.dataTransfer.items[i].kind === "file") {
                        const file = event.dataTransfer.items[i].getAsFile();
                        if (file != null) {
                            vVoucherArray.push(voucherNo);
                            vFileNameArray.push(file.name);
                            vFileContentArray.push(await readFile(file));
                        }
                    }
                }
            }
            else {
                for (var i = 0; i < event.dataTransfer.files.length; i++) {
                    const file = event.dataTransfer.files[i];
                    vVoucherArray.push(voucherNo);
                    vFileNameArray.push(file.name);
                    vFileContentArray.push(await readFile(file));
                }
            }
        }
<div id="div0" ondrop="DropFiles(event)" ondragover="allowDropFiles(event)" style="height:100px; width:100px; background-color:red" title="Drop bilag her">

I have a strange problem with the code below. My aim is to upload multiple files at once to the website via Javascript. And it works very well in Firefox - but not in Chrome and Microsoft Edge.

The error message in Chrome and Microsoft Edge is: "Failed to load resource: the server responded with a status of 404 ()"

I can see that event.dataTransfer.items.length have the right number of files which I want to upload - but Chrome/Edge only uploads the first file, and then stop working :-(

So my question is: Is this a bug in these two browsers or do I need to code it differently to acomplish this task?

Any help is appreciated.

Regards Michael

function readFile(file) {
    return new Promise((resolve, reject) => {
        var fr = new FileReader();
        fr.onload = () => {
            resolve(fr.result)
        };
        fr.onerror = reject;
        fr.readAsDataURL(file);
    });
}

async function DropFiles(event) {
    event.preventDefault();
    var vVoucherArray = [];
    var vFileNameArray = [];
    var vFileContentArray = [];
    voucherNo = event.target.id.substring(4, event.target.id.length);
    if (event.dataTransfer.items) {
        for (var i = 0; i < event.dataTransfer.items.length; i++) {
            if (event.dataTransfer.items[i].kind === "file") {
                const file = event.dataTransfer.items[i].getAsFile();
                if (file != null) {
                    vVoucherArray.push(voucherNo);
                    vFileNameArray.push(file.name);
                    vFileContentArray.push(await readFile(file));
                }
            }
        }
        doUploadFile(vVoucherArray, vFileNameArray, vFileContentArray);
    }
}

Upvotes: 3

Views: 844

Answers (2)

traktor
traktor

Reputation: 19311

Drag'n'drop of multiple files into browser not working in Chrome or Edge is a bug*.

What's happening with the posted code is that event.dataTransfer.items and event.dataTransfer.files are being destroyed by setting their lengths to zero, besides replacing them, as property values of the dataTranfer object, with empty list objects when event handling code returns to the event loop. To complete the destruction, the functionality of calling getAsFile on an item when event handling code is resumed is removed. Overall a successful effort in rendering access by the event handler to the drag and drop data store after resuming execution impossible.

This occurs in WebKit browsers (Edge tested for this answer) but not in Firefox. Returning to the event loop is occuring during execution of the await operator, specifically when await readFile(file) is evaluated.

Under WebKit, when the for loop is resumed by await, the loop condition is false because i is 1, and both dataTransfer.items.length and dataTransfer.files.length are 0 - causing the loop to break at the end of the first iteration.

Why

The main potential reason for doing this that comes to mind is to invalidate window.event after return to the event loop. Although now fully deprecated, using window.event instead of an event object parameter value passed to event handles was how early versions of Microsoft IE worked. In modern browsers such usage it no longer thread safe because JavaScript execution can now be interrupted and resumed using the same execution context by the await operator.

Another issue could be the use of a single, static data store for all drag'n'drop operations - which if the case would make the entire drag and drop design non re-entrant.

Solution

Since the length of the original files and/or items lists in the event object are being set to zero, copying the file objects before attempting to read them provides a solution. Here's a snippet I used to explore and get it working:

"use strict";
function allowDropFiles(event) {
    event.preventDefault();
}
async function DropFiles(event) {
    event.preventDefault();
    var files = [];
    var vVoucherArray = [];
    var vFileNameArray = [];
    var vFileContentArray = [];
    var voucherNo = event.target.id.substring(4, event.target.id.length);
    const items = event.dataTransfer.items;
    if (items) {
        console.log( "items: ", items.length);
        for (var i = 0; i < items.length; i++) {
            if( items[i].kind === 'file') {
                files.push(items[i].getAsFile());
            }
        }
        console.log( files);
    }
    for( var i = 0; i < files.length; ++i) {
        var file = files[i];
        if (file) {
            vVoucherArray.push(voucherNo);
            vFileNameArray.push(file.name);
            vFileContentArray.push(await readFile(file));
        }
    }
    doUploadFile(vVoucherArray, vFileNameArray, vFileContentArray);
}

function readFile(file) {
    return new Promise((resolve, reject) => {
        var fr = new FileReader();
        fr.onload = () => {
            resolve(fr.result)
        };
        fr.onerror = reject;
        fr.readAsDataURL(file);
    });
}
function doUploadFile(vouchers, fileNames, fileContent){
    console.log(vouchers,fileNames);
    var total =0;
    fileContent.forEach(text=>total+= text.length);
    console.log("Total bytes: " + total);
}
<div id="div0" ondrop="DropFiles(event)" ondragover="allowDropFiles(event)" style="height:100px; width:100px; background-color:red" title="Drop bilag her">

The snippet has been kept close to the post and iterates through the items array checking for kind==="file" to filter entries. A simpler option may be to clone the files list using the equivalent of:

const files = Array.from(event.dataTransfer.files);

*Is it really a bug

I'd call it a bug to solve a reentrancy problem by making sure it will fail when in doubt. Vendors probably may not agree - hinting that dataTranfer objects can be disassociated from a drag data store in HTML standards is another way of saying it's documented behaviour and not their problem if developers get bitten by it (IMHO).

Upvotes: 3

Simone Rossaini
Simone Rossaini

Reputation: 8162

Instead of use readFile() etc. use formData like:

function allowDropFiles(event) {
  event.preventDefault();
}
let formData = new FormData();
async function DropFiles(event) {
  event.preventDefault();
  if (event.dataTransfer.items) {
    for (var i = 0; i < event.dataTransfer.items.length; i++) {
      if (event.dataTransfer.items[i].kind === "file") {
        const file = event.dataTransfer.items[i].getAsFile();
        if (file != null) {
          formData.append("file", file);
        }
      }
    }
  }
  for (const value of formData.values()) {
    console.log(value);
  }
}
<div id="div0" ondrop="DropFiles(event)" ondragover="allowDropFiles(event)" style="height:100px; width:100px; background-color:red" title="Drop bilag her">

Reference:

Upvotes: 1

Related Questions