איך גוררים ומשחררים קבצים

ממשקי HTML לגרירה ולשחרור מאפשרים לאפליקציות אינטרנט לקבל קבצים שנגררו וששוחררו בדף אינטרנט. במהלך פעולת גרירה ושחרור, פריטים של קבצים וספריות שנגררים משויכים לרשומות של קבצים ולרשומות של ספריות, בהתאמה. כשמדובר בגרירה ושחרור של קבצים בדפדפן, יש שתי דרכים לעשות את זה: הדרך המודרנית והדרך הקלאסית.

הדרך המודרנית

שימוש בשיטה DataTransferItem.getAsFileSystemHandle() של File System Access API

השיטה DataTransferItem.getAsFileSystemHandle() מחזירה הבטחה עם אובייקט FileSystemFileHandle אם הפריט שנגרר הוא קובץ, והבטחה עם אובייקט FileSystemDirectoryHandle אם הפריט שנגרר הוא ספרייה. ה-handle האלה מאפשרים לכם לקרוא, ואם תרצו גם לכתוב בחזרה לקובץ או לספרייה. שימו לב: בממשק הגרירה והשחרור, הסמל DataTransferItem.kind יהיה "file" גם לקבצים וגם לתיקיות, ואילו ב-File System Access API, הסמל FileSystemHandle.kind יהיה "file" לקבצים ו-"directory" לתיקיות.

Browser Support

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

Source

הדרך הקלאסית

שימוש בשיטה הקלאסית DataTransferItem.getAsFile()

השיטה DataTransferItem.getAsFile() מחזירה את האובייקט File של פריט הנתונים שנגרר. אם הפריט הוא לא קובץ, הפונקציה הזו מחזירה null. אפשר לקרוא את הקובץ, אבל אי אפשר לכתוב בו. החיסרון בשיטה הזו הוא שאי אפשר להשתמש בה בספריות.

Browser Support

  • Chrome: 11.
  • Edge: 12.
  • Firefox: 50.
  • Safari: 5.1.

Source

שיפור הדרגתי

בקטע הקוד שבהמשך נעשה שימוש בשיטה DataTransferItem.getAsFileSystemHandle() של File System Access API המודרני, אם היא נתמכת, ואז חוזרים לשיטה DataTransferItem.webkitGetAsEntry() הלא סטנדרטית, ולבסוף חוזרים לשיטה DataTransferItem.getAsFile() הקלאסית. חשוב לבדוק את הסוג של כל handle, כי הוא יכול להיות אחד מהסוגים הבאים:

  • FileSystemFileHandle כשבוחרים בנתיב הקוד המודרני.
  • File כשבוחרים בנתיב הקוד הקלאסי.

לכל הסוגים יש מאפיין name, ולכן אפשר לרשום אותו ביומן והוא תמיד יפעל.

// Run feature detection.
const supportsFileSystemAccessAPI =
  'getAsFileSystemHandle' 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();
  // 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 `File`.
        : item.getAsFile(),
    );
  // Loop over the array of promises.
  for await (const handle of fileHandlesPromises) {
    // This is where we can actually exclusively act on the files.
    if (handle.kind === 'file' || handle.isFile) {
      console.log(`File: ${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 files</title>
  </head>
  <body>
    <main>
      <h1>How to drag and drop files</h1>
      <p>Drag and drop one or multiple files 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`;
    }
  }
});