Make File Uploader/Preview Handle Multiple Files

Viewed 723

I've built a file uploader (that runs on php in the backend) that previews an image file prior to upload.

The problem I'm having is I can't get it work with multiple files.

It's based on a tutorial I watched and the crux of the issue is in the updateThumbnail function. When this function is called for multiple file uploads I think I need to change the second parameter from fileUploader.files[0] to fileUploader.files, but I'm struggling with the actual function itself.

I clearly need to run a foreach loop (or similar) in the updateThumbnail function but I can't get it to play ball.

Note: It seems CodePen doesn't allow drag & drop functionality, but there is an input file element that is also assigned to the drop-zone that is hidden in the HTML with display:none. This uses a click event listener and fileUploader.click() so when you click the drop-zone you can bring up the file picker window.

Codepen: https://codepen.io/pauljohnknight/pen/JjNNyzO

// hidden on the form, but has drag & drop files assigned to it
var fileUploader = document.getElementById("standard-upload-files");

var dropZone = document.getElementById("drop-zone");
var showSelectedImages = document.getElementById("show-selected-images");

dropZone.addEventListener("click", (e) => {
//assigns the dropzone to the hidden input element so when you click 'select files' it brings up a file picker window
  fileUploader.click();
});

fileUploader.addEventListener("change", (e) => {
  if (fileUploader.files.length) {
    // this function is further down but declared here and shows a thumbnail of the image
    updateThumbnail(dropZone, fileUploader.files[0]);
  }
});

dropZone.addEventListener('dragover', e => {
    e.preventDefault()
})

dropZone.addEventListener('dragend', e => {
    e.preventDefault()
})

// When the files are dropped in the 'drop-zone'

dropZone.addEventListener("drop", (e) => {
  e.preventDefault();

  // assign dropped files to the hidden input element
  if (e.dataTransfer.files.length) {
    fileUploader.files = e.dataTransfer.files;
  }
  // function is declared here but written further down
  updateThumbnail(dropZone, e.dataTransfer.files[0]);
});

// updateThumbnail function that needs to be able to handle multiple files
function updateThumbnail(dropZone, file) {
  var thumbnailElement = document.querySelector(".drop-zone__thumb");

  if (!thumbnailElement) {
    thumbnailElement = document.createElement("img");
    thumbnailElement.classList.add("drop-zone__thumb");

    // append to showSelectedImages div
    showSelectedImages.appendChild(thumbnailElement);
  }

  if (file.type.startsWith("image/")) {
    var reader = new FileReader();

    reader.readAsDataURL(file);

    reader.onload = () => {
      thumbnailElement.src = reader.result;
    };
  } else {
    thumbnailElement.src = null;
  }
  
} // end of 'updateThumbnail' function
body {
  margin: 0;
  display: flex;
  justify-content: center;
  width: 100%;
}

form {
  width: 30%;
}

#drop-zone {
  border: 1px dashed;
  width: 100%;
  padding: 1rem;
  margin-bottom: 1rem;
}

.select-files {
  text-decoration: underline;
  cursor: pointer;
}

/* image that is preview prior to form submit*/
.drop-zone__thumb {
  width: 200px;
  height: auto;
  display: block;
}

#submit-images {
  margin-top: 1rem;
}
<form id="upload-images-form" enctype="multipart/form-data" method="post">
  <h1>Upload Your Images</h1>
  <div id="drop-zone" class="drop-zone flex">
    <p class="td text-center">DRAG AND DROP IMAGES HERE</p>
    <p>Or</p>
    <p class="select-files">Select Files</p>
  </div>
  <div id="show-selected-images"></div>
  <div class="inner-input-wrapper">
    <div class="upload-label-wrapper">
      <input id="standard-upload-files" style="display:none" type="file" name="standard-upload-files[]" multiple>
    </div>
    <input type="submit" name="submit-images" id="submit-images" value="SUBMIT IMAGES">
  </div>
</form>

2 Answers

I did quick Codesandbox example from your Codepen

Yes, you just need to iterate over your files and for each file add a preview. You can use for loop or just use Array.from and then .forEach (because FileList is not really an array, you need to convert it to array first to be able to use array inbuilt methods)

    Array.from(fileUploader.files).forEach((file) => {
      updateThumbnail(dropZone, file);
    });

As for previews and updateThumbnail function - it all depends on how you want to use it. If you want users to be able to add more files after first selection then you can just append new previews. If you want to clear the old ones if the user select new ones then you would need to delete old previews. Or maybe you could add "Delete" button for each preview so the user could delete one of them after adding.

Here is the variant when you just want to append new previews:

function updateThumbnail(dropZone, file) {
  if (file.type.startsWith('image/')) {
    var reader = new FileReader();

    reader.readAsDataURL(file);

    reader.onload = () => {
      var thumbnailElement = document.createElement('img');
      thumbnailElement.classList.add('drop-zone__thumb');
      thumbnailElement.src = reader.result;
      showSelectedImages.appendChild(thumbnailElement);
    };
  }
}

For drop you basically do the same:

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

  // .. do whatever you want or need here

  Array.from(e.dataTransfer.files).forEach((file) => {
    updateThumbnail(dropZone, file);
  });
});

As you can see functions for handling drop and select are quite similar, you can even make separate function which accepts fileList and then do something with it, so would not need to duplicate your code for both cases.

Made some minor changes to @Danila's version

Most notable differences is the use of es6 and a way to load image faster using URL.createObjectURL. The File reader is pretty much a legecy thing now when there is object urls + new promise based read methods on the prototype itself. Using bae64 is a waste of time decoding/encoding to/from base64

https://codesandbox.io/s/httpsstackoverflowcomquestions68416563-forked-o5spy?file=/src/index.js

import "./styles.css";

// Query all needed elements in one go
const [dropZone, showSelectedImages, fileUploader] = document.querySelectorAll(
  "#standard-upload-files, #drop-zone, #show-selected-images"
);

dropZone.addEventListener("click", (evt) => {
  // assigns the dropzone to the hidden input element so when you click 'select files' it brings up a file picker window
  fileUploader.click();
});

// Prevent browser default when draging over
dropZone.addEventListener("dragover", (evt) => {
  evt.preventDefault();
});

fileUploader.addEventListener("change", (evt) => {
  // Clear the already selected images
  showSelectedImages.innerHTML = "";
  // this function is further down but declared here and shows a thumbnail of the image
  [...fileUploader.files].forEach(updateThumbnail);
});

dropZone.addEventListener("drop", (evt) => {
  evt.preventDefault();
  // Clear the already selected images
  showSelectedImages.innerHTML = "";

  // assign dropped files to the hidden input element
  if (evt.dataTransfer.files.length) {
    fileUploader.files = evt.dataTransfer.files;
  }

  // function is declared here but written further down
  [...evt.dataTransfer.files].forEach(updateThumbnail);
});

// updateThumbnail function that needs to be able to handle multiple files
function updateThumbnail(file) {
  if (file.type.startsWith("image/")) {
    const thumbnailElement = new Image();
    thumbnailElement.classList.add("drop-zone__thumb");
    thumbnailElement.src = URL.createObjectURL(file);
    showSelectedImages.append(thumbnailElement);
  }
} // end of 'updateThumbnail' function
Related