Upload files and directories using an input, drag and drop, or copy and paste with HTML5

 
 
  • Gérald Barré
 

Many websites have a form to upload a file. For some sites, such as OneDrive or Google Drive, uploading files is a major feature. As time went on, webpages are more and more interactive. So, users expect more interactions such as dragging and dropping files or directories or pasting screenshots.

Let's see what browsers provide to upload files!

#Basic file input

The basic way is by using a form and an input with the file type:

HTML
<form method="post" enctype="multipart/form-data">
  <input type="file" name="photo">
  <input type="submit" value="Submit">
</form>

You can improve this with two optional attributes:

  • multiple: allows multiple files to be selected
  • accept: choose expected mime types, for instance: image/*, application/pdf

#Drag and drop files

Most users expect to be able to drag and drop files on the page. This functionality is well-supported by browsers and very easy to implement. Once you get the files, you need to do something such as uploading them to an API or read their content using the FileReader api.

TypeScript
const dropZone = document.body;
if (dropZone) {
    const hoverClassName = "hover";

    // Handle drag* events to handle style
    // Add the css you want when the class "hover" is present
    dropZone.addEventListener("dragenter", function (e) {
        e.preventDefault();
        dropZone.classList.add(hoverClassName);
    });

    dropZone.addEventListener("dragover", function (e) {
        e.preventDefault();
        dropZone.classList.add(hoverClassName);
    });

    dropZone.addEventListener("dragleave", function (e) {
        e.preventDefault();
        dropZone.classList.remove(hoverClassName);
    });

    // This is the most important event, the event that gives access to files
    dropZone.addEventListener("drop", function (e) {
        e.preventDefault();
        dropZone.classList.remove(hoverClassName);

        const files = Array.from(e.dataTransfer.files);
        console.log(files);
        // TODO do somethings with files...
    });
}

You can easily upload files to an API using the fetch API:

TypeScript
if (files.length > 0) {
    const data = new FormData();
    for (const file of files) {
        data.append('file', file);
    }

    fetch('/upload', {
        method: 'POST',
        body: data
    })
    .then(() => console.log("file uploaded"))
    .catch(reason => console.error(reason));
}

#Drag and drop directories

Google Chrome and Microsoft Edge now support dropping directories. This is very useful when you want to upload your files to OneDrive or Google Drive. The API is not very convenient as it uses callback functions with a recursive tree, but it's not that hard. Let's see some code:

TypeScript
// drag* events are omitted for brevity (get them from the previous section).

dropZone.addEventListener("drop", async function (e) {
    e.preventDefault();
    dropZone.classList.remove(hoverClassName);

    console.log(await getFilesAsync(e));
});

async function getFilesAsync(dataTransfer: DataTransfer) {
    const files: File[] = [];
    for (let i = 0; i < dataTransfer.items.length; i++) {
        const item = dataTransfer.items[i];
        if (item.kind === "file") {
            if (typeof item.webkitGetAsEntry === "function") {
                const entry = item.webkitGetAsEntry();
                const entryContent = await readEntryContentAsync(entry);
                files.push(...entryContent);
                continue;
            }

            const file = item.getAsFile();
            if (file) {
                files.push(file);
            }
        }
    }

    return files;
}

// Returns a promise with all the files of the directory hierarchy
function readEntryContentAsync(entry: FileSystemEntry) {
    return new Promise<File[]>((resolve, reject) => {
        let reading = 0;
        const contents: File[] = [];

        readEntry(entry);

        function readEntry(entry: FileSystemEntry) {
            if (isFile(entry)) {
                reading++;
                entry.file(file => {
                    reading--;
                    contents.push(file);

                    if (reading === 0) {
                        resolve(contents);
                    }
                });
            } else if (isDirectory(entry)) {
                readReaderContent(entry.createReader());
            }
        }

        function readReaderContent(reader: FileSystemDirectoryReader) {
            reading++;

            reader.readEntries(function (entries) {
                reading--;
                for (const entry of entries) {
                    readEntry(entry);
                }

                if (reading === 0) {
                    resolve(contents);
                }
            });
        }
    });
}

// for TypeScript typing (type guard function)
// https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards
function isDirectory(entry: FileSystemEntry): entry is FileSystemDirectoryEntry {
    return entry.isDirectory;
}

function isFile(entry: FileSystemEntry): entry is FileSystemFileEntry {
    return entry.isFile;
}

#Paste file

Currently, you can only paste images, not image files nor any other types of file. This may change in the future, but at the moment this is enough to upload screenshots. I often paste images when creating an issue on JIRA or VSTS 😃

Supporting image pasting is a quick win because you can reuse the previous code. Indeed, you can simply handle the paste event and use the previous getFilesAsync function:

TypeScript
dropZone.addEventListener("paste", async function (e) {
    e.preventDefault();

    const files = await getFilesAsync(e.clipboardData);
    console.log(files);
});

You are now masters in uploading files and directories using all the possible ways your browser support (at the time of writing)!

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?Buy Me A Coffee💖 Sponsor on GitHub