I am using Vuejs along with DataTransfer to upload files asynchronously, and I want to allow multiple files to be dragged and dropped for upload at once.
I can get the first upload to happen, but by the time that upload is done, Javascript has either garbage collected or changed the DataTransfer items object.
How can I rework this (or clone the event/DataTransfer object) so that the data is still available to me throughout the ajax calls?
I've followed the MDN docs on how to use DataTransfer but I'm having a hard time applying it to my specific case. I also have tried copying the event objects, as you can see in my code, but it obviously does not do a deep copy, just passes the reference, which doesn't help.
methods: {
dropHandler: function (event) {
if (event.dataTransfer.items) {
let i = 0;
let self = this;
let ev = event;
function uploadHandler() {
let items = ev.dataTransfer.items;
let len = items.length;
// len NOW EQUALS 4
console.log("LEN: ", len);
if (items[i].kind === 'file') {
var file = items[i].getAsFile();
$('#id_file_name').val(file.name);
var file_form = $('#fileform2').get(0);
var form_data = new FormData(file_form);
if (form_data) {
form_data.append('file', file);
form_data.append('type', self.type);
}
$('#file_progress_' + self.type).show();
var post_url = '/blah/blah/add/' + self.object_id + '/';
$.ajax({
url: post_url,
type: 'POST',
data: form_data,
contentType: false,
processData: false,
xhr: function () {
var xhr = $.ajaxSettings.xhr();
if (xhr.upload) {
xhr.upload.addEventListener('progress', function (event) {
var percent = 0;
var position = event.loaded || event.position;
var total = event.total;
if (event.lengthComputable) {
percent = Math.ceil(position / total * 100);
$('#file_progress_' + self.type).val(percent);
}
}, true);
}
return xhr;
}
}).done((response) => {
i++;
if (i < len) {
// BY NOW, LEN = 0. ????
uploadHandler();
} else {
self.populate_file_lists();
}
}
);
}
}
uploadHandler();
}
},
Once you call await
you're no longer in the original call stack of the function. This is something that would matter particularly in the event listener.
We can reproduce the same effect with setTimeout
:
dropZone.addEventListener('drop', async (e) => {
e.preventDefault();
console.log(e.dataTransfer.items);
setTimeout(()=> {
console.log(e.dataTransfer.items);
})
});
For example, dragging four files will output:
DataTransferItemList {0: DataTransferItem, 1: DataTransferItem, 2: DataTransferItem, 3: DataTransferItem, length: 4}
DataTransferItemList {length: 0}
After the event had happened the state has changed and items have been lost.
There are two ways to handle this issue:
Promise.all
The second solution is more intuitive than using await
in the loop. Also, consider parallel connections are limited. With an array you can create chunks to limit simultaneous uploads.
function pointlessDelay() {
return new Promise((resolve, reject) => {
setTimeout(resolve, 1000);
});
}
const dropZone = document.querySelector('.dropZone');
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
});
dropZone.addEventListener('drop', async (e) => {
e.preventDefault();
console.log(e.dataTransfer.items);
const queue = [];
for (const item of e.dataTransfer.items) {
console.log('next loop');
const entry = item.webkitGetAsEntry();
console.log({item, entry});
queue.push(pointlessDelay().then(x=> console.log(`${entry.name} uploaded`)));
}
await Promise.all(queue);
});
body {
font-family: sans-serif;
}
.dropZone {
display: inline-flex;
background: #3498db;
color: #ecf0f1;
border: 0.3em dashed #ecf0f1;
border-radius: 0.3em;
padding: 5em;
font-size: 1.2em;
}
<div class="dropZone">
Drop Zone
</div>