ディレクトリをドラッグ&ドロップする方法

HTML ドラッグ&ドロップ インターフェースを使用すると、ウェブ アプリケーションでウェブページ上のドラッグ&ドロップ ファイルを受け入れることができます。ドラッグ&ドロップ オペレーションの際、ドラッグされたファイルとディレクトリ アイテムは、それぞれファイル エントリとディレクトリ エントリに関連付けられます。ブラウザにファイルをドラッグ&ドロップする方法は 2 つあります。最新の方法と従来の方法です。

最新の手法

File System Access API の DataTransferItem.getAsFileSystemHandle() メソッドを使用する

DataTransferItem.getAsFileSystemHandle() メソッドは、ドラッグされたアイテムがファイルの場合、FileSystemFileHandle オブジェクトを含む Promise を返します。ドラッグされたアイテムがディレクトリの場合、FileSystemDirectoryHandle オブジェクトを含む Promise を返します。これらのハンドルを使用すると、ファイルまたはディレクトリに対する読み取りを行うことができます。また、必要に応じて書き戻すこともできます。ドラッグ&ドロップ インターフェースの DataTransferItem.kind は、ファイルとディレクトリの両方で "file" になりますが、File System Access API の FileSystemHandle.kind は、ファイルの場合は "file"、ディレクトリの場合は "directory" になります。

対応ブラウザ

  • 86
  • 86
  • x
  • x

ソース

従来のやり方

非標準の DataTransferItem.webkitGetAsEntry() メソッドの使用

DataTransferItem.webkitGetAsEntry() メソッドは、ドラッグデータ アイテムの FileSystemFileEntry を返します(アイテムがファイルの場合)。また、アイテムがディレクトリの場合は FileSystemDirectoryEntry を返します。ファイルまたはディレクトリを読み取ることはできますが、書き戻すことはできません。この方法には、標準トラックにない欠点がありますが、ディレクトリをサポートするという利点があります。

対応ブラウザ

  • 13
  • 14
  • 50
  • 11.1

ソース

段階的な補正

以下のスニペットでは、サポートされている場合は最新の File System Access API の DataTransferItem.getAsFileSystemHandle() メソッドを使用し、標準以外の DataTransferItem.webkitGetAsEntry() メソッドにフォールバックし、最後に従来の DataTransferItem.getAsFile() メソッドにフォールバックします。次のいずれかの型である可能性があるため、各 handle の型を必ず確認してください。

  • FileSystemDirectoryHandle: 最新のコードパスが選択された場合。
  • FileSystemDirectoryEntry: 非標準のコードパスが選択された場合。

すべての型には name プロパティがあるため、ロギングは問題なく、常に機能します。

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

参考資料

デモ

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`;
    }
  }
});