4

Currently, I'm developing a drag and drop feature. The problem is that I can't figure out how to get multiple files from user separately. Let's say that we have a drop zone container and when user drops there images, it assigns them to <input type="file">. Let's say, a user drops here an image and then decides to drop another image and we have to somehow add this second image to the input. I tried finding solution in the Internet(of course :)) but found nothing that solves this problem.

Here is my HTML:

<form action="" method="POST" enctype="multipart/form-data">

    <div class="drop_zone">
        <span class="drop_zone__prompt">Drop your files here</span>
        <input required name="images" type="file" multiple class="drop_zone__input">
    </div>
    <input type="submit" value="Отправить">
</form>

JavaScript:

document.querySelectorAll('.drop_zone__input').forEach(InputElement => {

    const dropZoneElement = InputElement.closest('.drop_zone');

    dropZoneElement.addEventListener('drop', e => {
        e.preventDefault();

        if (e.dataTransfer.files.length){
            InputElement.files = e.dataTransfer.files;
        }
    });

I tried this:

dropZoneElement.addEventListener('drop', e => {
    e.preventDefault();

    if (e.dataTransfer.files.length){
        myfiles = e.dataTransfer.add(InputElement.files);
        InputElement.files = myfiles;
    }
});

But it returns error saying that 'e.dataTransfer.add is not a function'

Why I tried this:

I found add() method here

And this article says:

The DataTransferItemList object is a list of DataTransferItem objects representing items being dragged. During a drag operation, each DragEvent has a dataTransfer property and that property is a DataTransferItemList.

Daniil
  • 133
  • 2
  • 11
  • Are you submitting the files using HTTP (form submission) or ajax? – T.J. Crowder Mar 10 '21 at 15:54
  • 1
    @OleHaugset - No there's no `type="file[]"` in HTML. Multiple vs. single is handled via the `multiple` boolean attribute. Details: https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement – T.J. Crowder Mar 10 '21 at 15:55
  • @T.J Crowder, Django handles all the processing – Daniil Mar 10 '21 at 15:57
  • For some people reading this, you may need to change your input file name to an array, i.e. "images[]" where it is "images" in the above example. Mine worked using PHP server side after this change. – AdheneManx Feb 28 '23 at 07:06

3 Answers3

3

Actually, there is a way to do that. It's far from straightforward, but it does work.

The only way to create a FileList object is to create a custom DataTransfer object. You can add files to it using dataTransfer.items.add(), and obtain the corresponding FileList through dataTransfer.files.

So, create a new DataTransfer object every time you want to add files, add the existing and the new files to it, and assign its FileList to the files property of the input element.

Note: You can't use the drop event's DataTransfer object for this, because it's read-only.

document.querySelectorAll('.drop_zone__input').forEach(InputElement => {
  const dropZoneElement = InputElement.closest('.drop_zone');
  dropZoneElement.addEventListener('dragover', e => {
    e.preventDefault()
  });
  dropZoneElement.addEventListener('drop', e => {
     e.preventDefault();

     //Create a new DataTransfer object
     const dataTransfer = new DataTransfer

     //Add new files from the event's DataTransfer
     for(let i = 0; i < e.dataTransfer.files.length; i++)
       dataTransfer.items.add(e.dataTransfer.files[i])

     //Add existing files from the input element
     for(let i = 0; i < InputElement.files.length; i++)
       dataTransfer.items.add(InputElement.files[i])

     //Assign the files to the input element
     InputElement.files = dataTransfer.files
  });
})
.drop_zone{
  height: 200px;
  width: 200px;
  border: solid black 1px;
}
<form action="" method="POST" enctype="multipart/form-data">

  <div class="drop_zone">
    <span class="drop_zone__prompt">Drop your files here</span>
    <input required name="images" type="file" multiple class="drop_zone__input">
  </div>
  <input type="submit" value="Отправить">
</form>

You can also reuse the same DataTransfer object, so you don't have to re-add the existing files.

However, in this case, you also have to handle input events on the input element.

document.querySelectorAll('.drop_zone__input').forEach(InputElement => {
  const dropZoneElement = InputElement.closest('.drop_zone');
  
  const dataTransfer = new DataTransfer
  dropZoneElement.addEventListener('dragover', e => {
    e.preventDefault()
  });
  dropZoneElement.addEventListener('drop', e => {
     e.preventDefault();

     //Add new files from the event's DataTransfer
     for(let i = 0; i < e.dataTransfer.files.length; i++)
       dataTransfer.items.add(e.dataTransfer.files[i])
       

     //Assign the files to the input element
     InputElement.files = dataTransfer.files
  });
  
   InputElement.addEventListener('input', e => {
    dataTransfer.items.clear()
    for(let i = 0; i < InputElement.files.length; i++)
      dataTransfer.items.add(InputElement.files[i])
  })
})
.drop_zone{
  height: 200px;
  width: 200px;
  border: solid black 1px;
}
<form action="" method="POST" enctype="multipart/form-data">

  <div class="drop_zone">
    <span class="drop_zone__prompt">Drop your files here</span>
    <input required name="images" type="file" multiple class="drop_zone__input">
  </div>
  <input type="submit" value="Отправить">
</form>

Or, if you want to add the files when interacting with the file input instead of replacing them, you can do this:

document.querySelectorAll('.drop_zone__input').forEach(InputElement => {
  const dropZoneElement = InputElement.closest('.drop_zone');
  
  const dataTransfer = new DataTransfer
  dropZoneElement.addEventListener('dragover', e => {
    e.preventDefault()
  });
  dropZoneElement.addEventListener('drop', e => {
     e.preventDefault();

     //Add new files from the event's DataTransfer
     for(let i = 0; i < e.dataTransfer.files.length; i++)
       dataTransfer.items.add(e.dataTransfer.files[i])
       

     //Assign the files to the input element
     InputElement.files = dataTransfer.files
  });
  
   InputElement.addEventListener('input', e => {
    e.preventDefault()
    
    for(let i = 0; i < InputElement.files.length; i++)
      dataTransfer.items.add(InputElement.files[i])
    InputElement.files = dataTransfer.files
  })
})
.drop_zone{
  height: 200px;
  width: 200px;
  border: solid black 1px;
}
<form action="" method="POST" enctype="multipart/form-data">

  <div class="drop_zone">
    <span class="drop_zone__prompt">Drop your files here</span>
    <input required name="images" type="file" multiple class="drop_zone__input">
  </div>
  <input type="submit" value="Отправить">
</form>

You can remove files from the DataTransfer object using dataTransfer.items.remove():

document.querySelectorAll('.drop_zone__input').forEach(InputElement => {
  const dropZoneElement = InputElement.closest('.drop_zone');
  const removeFirstElement = dropZoneElement.querySelector('.drop_zone__remove_first')
  
  const dataTransfer = new DataTransfer
  dropZoneElement.addEventListener('dragover', e => {
    e.preventDefault()
  });
  dropZoneElement.addEventListener('drop', e => {
     e.preventDefault();

     //Add new files from the event's DataTransfer
     for(let i = 0; i < e.dataTransfer.files.length; i++)
       dataTransfer.items.add(e.dataTransfer.files[i])
       

     //Assign the files to the input element
     InputElement.files = dataTransfer.files
  });
  
   InputElement.addEventListener('input', e => {
    e.preventDefault()
    
    for(let i = 0; i < InputElement.files.length; i++)
      dataTransfer.items.add(InputElement.files[i])
    InputElement.files = dataTransfer.files
  })
  
  removeFirstElement.addEventListener('click', () => {
    dataTransfer.items.remove(0)
    InputElement.files = dataTransfer.files
  })
})
.drop_zone{
  height: 200px;
  width: 200px;
  border: solid black 1px;
}
<form action="" method="POST" enctype="multipart/form-data">

  <div class="drop_zone">
    <span class="drop_zone__prompt">Drop your files here</span>
    <input required name="images" type="file" multiple class="drop_zone__input">
    <input type="button" class="drop_zone__remove_first" value="Remove first file">
  </div>
  <input type="submit" value="Отправить">
</form>
FZs
  • 16,581
  • 13
  • 41
  • 50
1

You can't add to the files to the input's list of files, although apparently you can replace that list as described by FZs, which largely comes to the same thing.

Another way to deal with this is to make the original input hidden via CSS and add a new input where it used to be, so it can receive new files. You can make the UI clean by listing all of the files you're going to upload separately from the input.

How you deal with on submission depends on how you're handling submission.

If you're doing a standard HTTP form submission, ensure that your server side script handles all of the files regardless of which input they came from (it will receive more than one).

If you're submitting via ajax, you can use a FormData object and append each file to it, then submit that.

Here's a quick and dirty example maintaining the files in a FormData object:

let nextFileNum = 1;
const formData = new FormData();
const fileList = document.getElementById("file-list");
document.getElementById("inputs").addEventListener("input", function() {
    let input = event.target.closest("input[type=file]");
    if (!input) {
        return;
    }
    for (const file of input.files) {
        const fileId = `file${nextFileNum++}`;
        formData.append(fileId, file, file.name);
        debugShowCurrentFiles("added");
        let div = document.createElement("div");
        // This is a Q&D sketch, needs a11y
        div.innerHTML = "<span tabindex class=delete>[X]</span><span class=filename></span>";
        div.querySelector(".filename").textContent = file.name;
        div.querySelector(".delete").addEventListener("click", function() {
            formData.delete(fileId);
            debugShowCurrentFiles("deleted");
            div.remove();
            div = null;
        });
        fileList.appendChild(div);
    }
    this.removeChild(input);
    this.insertAdjacentHTML("beforeend", input.outerHTML);
    input = null;
});
function debugShowCurrentFiles(action) {
    const contents = [...formData.entries()];
    console.log(`--- ${action}, updated formData contents (${contents.length}):`);
    for (const [key, value] of contents) {
        console.log(`${key}: ${value.name}`);
    }
}
#file-list .delete {
    margin-right: 2em;
    cursor: pointer;
}
<div id="inputs">
    <input type="file" multiple>
</div>
<div id="file-list"></div>
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • Thank you a lot! I would like to ask a question. If it is possible to entirely override input like this: `InputElement.files = e.dataTransfer.files;`. So, when user drops a file, is it possible to get `InputElement.files` and append it to the `e.dataTransfer.files`, and then assign all this to the input? – Daniil Mar 10 '21 at 16:10
  • 1
    @Daniil - I'm fairly sure not, but I've never tried. Anything like that just screams security hole (despite the source value being from a data transfer object). BTW, I've updated the answer with a quick and dirty example. – T.J. Crowder Mar 10 '21 at 16:19
  • Wow, impressive. But I don't understand why it's dirty and has security risks :( – Daniil Mar 10 '21 at 16:27
  • @Daniil - I didn't say it had security risks. I said it probably wasn't a11y friendly (accessibility). I don't know for sure it isn't, but any time you swap out elements, there may be the need for a call to `focus` or similar. When I say "quick and dirty" I just mean I haven't done rigorous testing, etc. You should do that if you decide to use code based on it. :-) – T.J. Crowder Mar 10 '21 at 16:45
  • 1
    Thank you very much! I find it very helpful! – Daniil Mar 10 '21 at 16:52
-1

You can do this with FileList, heres an article about it. Also added code snippet which outputs file name and type after you submit the form just for visualization.

let form = document.getElementById('file_drop_form');
function getFiles(event) {
  event.preventDefault();
  let files = document.getElementById('file').files;
  for(let i = 0; i < files.length; i++) {
    document.write(`Title: ${files[i].name}; Type: ${files[i].type} <br>`);
  }
}
form.addEventListener('submit', getFiles);
<form id="file_drop_form" method="post" enctype="multipart/form-data">
 <div>
   <label for="file">Drop your files here</label>
   <input type="file" id="file" name="file" required multiple>
 </div>
 <input type="submit" value="Submit">
</form>