Cómo arrastrar y soltar directorios

Las interfaces de arrastrar y soltar de HTML permiten que las aplicaciones web acepten archivos arrastrados y soltados en una página web. Durante una operación de arrastrar y soltar, los elementos de archivo y directorio arrastrados se asocian con las entradas de archivo y las entradas de directorio, respectivamente. Cuando se trata de arrastrar y soltar archivos en el navegador, existen dos formas de hacerlo: la moderna y la clásica.

La forma moderna

Usa el método DataTransferItem.getAsFileSystemHandle() de la API de File System Access

El método DataTransferItem.getAsFileSystemHandle() muestra una promesa con un objeto FileSystemFileHandle si el elemento arrastrado es un archivo y una promesa con un objeto FileSystemDirectoryHandle si el elemento arrastrado es un directorio. Estos controladores te permiten leer y, de manera opcional, volver a escribir en el archivo o directorio. Ten en cuenta que el DataTransferItem.kind de la interfaz de arrastrar y soltar será "file" para archivos y directorios, mientras que el FileSystemHandle.kind de la API de File System Access será "file" para archivos y "directory" para directorios.

Browser Support

  • Chrome: 86.
  • Edge: 86.
  • Firefox: not supported.
  • Safari: not supported.

Source

La forma clásica

Usa el método no estándar DataTransferItem.webkitGetAsEntry()

El método DataTransferItem.webkitGetAsEntry() muestra el FileSystemFileEntry del elemento de datos de arrastre si el elemento es un archivo y FileSystemDirectoryEntry si el elemento es un directorio. Si bien puedes leer el archivo o el directorio, no hay forma de volver a escribir en ellos. Este método tiene la desventaja de que no está en la ruta de acceso de los estándares, pero tiene la ventaja de que admite directorios.

Browser Support

  • Chrome: 13.
  • Edge: 14.
  • Firefox: 50.
  • Safari: 11.1.

Source

Mejora progresiva

En el siguiente fragmento, se usa el método DataTransferItem.getAsFileSystemHandle() moderno de la API de File System Access cuando es compatible, luego se recurre al método no estándar DataTransferItem.webkitGetAsEntry() y, por último, se recurre al método clásico DataTransferItem.getAsFile(). Asegúrate de verificar el tipo de cada handle, ya que puede ser cualquiera de los siguientes:

  • FileSystemDirectoryHandle cuando se elige la ruta de acceso de código moderna.
  • FileSystemDirectoryEntry cuando se elige la ruta de acceso de código no estándar.

Todos los tipos tienen una propiedad name, por lo que registrarla es correcto y siempre funcionará.

// Run feature detection.
const supportsFileSystemAccessAPI =
  'getAsFileSystemHandle' in DataTransferItem.prototype;
const supportsWebkitGetAsEntry =
  'webkitGetAsEntry' in DataTransferItem.prototype;

// This is the drag and drop zone.
const elem = document.querySelector('main');

// Prevent navigation.
elem.addEventListener('dragover', (e) => {
  e.preventDefault();
});

// Visually highlight the drop zone.
elem.addEventListener('dragenter', (e) => {
  elem.style.outline = 'solid red 1px';
});

// Visually unhighlight the drop zone.
elem.addEventListener('dragleave', (e) => {
  elem.style.outline = '';
});

// This is where the drop is handled.
elem.addEventListener('drop', async (e) => {
  // Prevent navigation.
  e.preventDefault();
  if (!supportsFileSystemAccessAPI && !supportsWebkitGetAsEntry) {
    // Cannot handle directories.
    return;
  }
  // Unhighlight the drop zone.
  elem.style.outline = '';

  // Prepare an array of promises…
  const fileHandlesPromises = [...e.dataTransfer.items]
    // …by including only files (where file misleadingly means actual file _or_
    // directory)…
    .filter((item) => item.kind === 'file')
    // …and, depending on previous feature detection…
    .map((item) =>
      supportsFileSystemAccessAPI
        // …either get a modern `FileSystemHandle`…
        ? item.getAsFileSystemHandle()
        // …or a classic `FileSystemFileEntry`.
        : item.webkitGetAsEntry(),
    );
  // Loop over the array of promises.
  for await (const handle of fileHandlesPromises) {
    // This is where we can actually exclusively act on the directories.
    if (handle.kind === 'directory' || handle.isDirectory) {
      console.log(`Directory: ${handle.name}`);
    }
  }
});

Lecturas adicionales

Demostración

HTML

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>How to drag and drop directories</title>
  </head>
  <body>
    <main>
      <h1>How to drag and drop directories</h1>
      <p>Drag and drop one or multiple files or directories onto the page.</p>
      <pre></pre>
    </main>
  </body>
</html>

CSS


        :root {
  color-scheme: dark light;
  box-sizing: border-box;
}

*,
*:before,
*:after {
  box-sizing: inherit;
}

body {
  margin: 0;
  padding: 1rem;
  font-family: system-ui, sans-serif;
  line-height: 1.5;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

img,
video {
  height: auto;
  max-width: 100%;
}

main {
  flex-grow: 1;
}

footer {
  margin-top: 1rem;
  border-top: solid CanvasText 1px;
  font-size: 0.8rem;
}
        

JS


        const supportsFileSystemAccessAPI =
  "getAsFileSystemHandle" in DataTransferItem.prototype;
const supportsWebkitGetAsEntry =
  "webkitGetAsEntry" in DataTransferItem.prototype;

const elem = document.querySelector("main");
const debug = document.querySelector("pre");

elem.addEventListener("dragover", (e) => {
  // Prevent navigation.
  e.preventDefault();
});

elem.addEventListener("dragenter", (e) => {
  elem.style.outline = "solid red 1px";
});

elem.addEventListener("dragleave", (e) => {
  elem.style.outline = "";
});

elem.addEventListener("drop", async (e) => {
  e.preventDefault();
  elem.style.outline = "";
  const fileHandlesPromises = [...e.dataTransfer.items]
    .filter((item) => item.kind === "file")
    .map((item) =>
      supportsFileSystemAccessAPI
        ? item.getAsFileSystemHandle()
        : supportsWebkitGetAsEntry
        ? item.webkitGetAsEntry()
        : item.getAsFile()
    );

  for await (const handle of fileHandlesPromises) {
    if (handle.kind === "directory" || handle.isDirectory) {
      console.log(`Directory: ${handle.name}`);
      debug.textContent += `Directory: ${handle.name}\n`;
    } else {
      console.log(`File: ${handle.name}`);
      debug.textContent += `File: ${handle.name}\n`;
    }
  }
});