Search code examples
javascripthtmlvuejs2drag-and-dropdata-transfer-objects

Javascript DataTransfer items not persisting through async calls


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();
            }
        },

Solution

  • 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:

    • Copy items and iterate over them
    • Push async jobs(Promises) into the array and handle them later with 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>