Cách kéo và thả thư mục

Giao diện Kéo và thả HTML cho phép các ứng dụng web chấp nhận tệp được kéo và thả trên một trang web. Trong quá trình kéo và thả, các mục tệp và thư mục được kéo sẽ được liên kết với các mục tệp và mục thư mục tương ứng. Khi kéo và thả tệp vào trình duyệt, có 2 cách để thực hiện việc này: cách hiện đại và cách truyền thống.

Cách làm hiện đại

Sử dụng phương thức DataTransferItem.getAsFileSystemHandle() của File System Access API

Phương thức DataTransferItem.getAsFileSystemHandle() trả về một promise có đối tượng FileSystemFileHandle nếu mục được kéo là một tệp và một promise có đối tượng FileSystemDirectoryHandle nếu mục được kéo là một thư mục. Các đối tượng này cho phép bạn đọc và tuỳ ý ghi lại vào tệp hoặc thư mục. Xin lưu ý rằng DataTransferItem.kind của giao diện Kéo và thả sẽ là "file" cho cả tệp thư mục, trong khi FileSystemHandle.kind của File System Access API sẽ là "file" cho tệp và "directory" cho thư mục.

Browser Support

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

Source

Cách cổ điển

Sử dụng phương thức DataTransferItem.webkitGetAsEntry() không chuẩn

Phương thức DataTransferItem.webkitGetAsEntry() trả về FileSystemFileEntry của mục dữ liệu kéo nếu mục đó là một tệp và FileSystemDirectoryEntry nếu mục đó là một thư mục. Mặc dù bạn có thể đọc tệp hoặc thư mục, nhưng không có cách nào để ghi lại vào các tệp hoặc thư mục đó. Phương thức này có nhược điểm là không nằm trong danh sách tiêu chuẩn, nhưng có ưu điểm là hỗ trợ các thư mục.

Browser Support

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

Source

Cải tiến tăng dần

Đoạn mã dưới đây sử dụng phương thức DataTransferItem.getAsFileSystemHandle() của File System Access API hiện đại khi được hỗ trợ, sau đó quay lại phương thức DataTransferItem.webkitGetAsEntry() không chuẩn và cuối cùng quay lại phương thức DataTransferItem.getAsFile() cổ điển. Hãy nhớ kiểm tra loại của từng handle, vì loại này có thể là một trong hai loại sau:

  • FileSystemDirectoryHandle khi bạn chọn đường dẫn mã hiện đại.
  • FileSystemDirectoryEntry khi bạn chọn đường dẫn mã không chuẩn.

Tất cả các loại đều có thuộc tính name, vì vậy, bạn có thể ghi nhật ký thuộc tính này và thuộc tính này sẽ luôn hoạt động.

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

Tài liệu đọc thêm

Bản minh hoạ

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