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

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 type file

<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 in 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.

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:

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:

// 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;
}

Note: The webkitGetAsEntry function also works in Microsoft Edge.

Paste file

Currently, you can only paste images. This is very convenient to upload screenshots. I use it frequently when creating an issue on JIRA or VSTS 😃 We've already done the hard work. Indeed, you can simply handle the paste event and use the previous getFilesAsync function:

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

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