Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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();
            }
        },
like image 546
trpt4him Avatar asked Apr 12 '19 20:04

trpt4him


1 Answers

I ran into this problem, and was looking to persist the entire DataTransfer object, not just the items or types, because my asynchronous code's API consumes the DataTransfer type itself. What I ended up doing is creating a new DataTransfer(), and effectively copying over the original's properties (except the drag image).

Here's the gist (in TypeScript): https://gist.github.com/mitchellirvin/261d82bbf09d5fdee41715fa2622d4a6

// https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem/kind
enum DataTransferItemKind {
  FILE = "file",
  STRING = "string",
}

/**
 * Returns a properly deep-cloned object of type DataTransfer. This is necessary because dataTransfer items are lost
 * in asynchronous calls. See https://stackoverflow.com/questions/55658851/javascript-datatransfer-items-not-persisting-through-async-calls
 * for more details.
 * 
 * @param original the DataTransfer to deep clone
 */
export function cloneDataTransfer(original: DataTransfer): DataTransfer {
  const cloned = new DataTransfer();
  cloned.dropEffect = original.dropEffect;
  cloned.effectAllowed = original.effectAllowed;

  const originalItems = original.items;
  let i = 0;
  let originalItem = originalItems[i];
  while (originalItem != null) {
    switch (originalItem.kind) {
      case DataTransferItemKind.FILE:
        const file = originalItem.getAsFile();
        if (file != null) {
          cloned.items.add(file);
        }
        break;
      case DataTransferItemKind.STRING:
        cloned.setData(originalItem.type, original.getData(originalItem.type));
        break;
      default:
        console.error("Unrecognized DataTransferItem.kind: ", originalItem.kind);
        break;
    }

    i++;
    originalItem = originalItems[i];
  }
  return cloned;
}

You can consume this like so, and then use clone in the same way you originally planned to use evt.dataTransfer:

const clone = cloneDataTransfer(evt.dataTransfer);

like image 187
M. Irvin Avatar answered Sep 28 '22 02:09

M. Irvin