디렉터리를 드래그 앤 드롭하는 방법

HTML 드래그 앤 드롭 인터페이스를 사용하면 웹 애플리케이션이 웹페이지에서 드래그 앤 드롭된 파일을 수락할 수 있습니다. 드래그 앤 드롭 작업 중에 드래그된 파일 및 디렉터리 항목은 각각 파일 항목 및 디렉터리 항목과 연결됩니다. 파일을 브라우저로 드래그 앤 드롭하는 방법에는 최신 방식과 기존 방식의 두 가지가 있습니다.

최신 방식

File System Access API의 DataTransferItem.getAsFileSystemHandle() 메서드 사용

DataTransferItem.getAsFileSystemHandle() 메서드는 드래그된 항목이 파일인 경우 FileSystemFileHandle 객체가 포함된 프로미스를 반환하고 드래그된 항목이 디렉터리인 경우 FileSystemDirectoryHandle 객체가 포함된 프로미스를 반환합니다. 이러한 핸들을 사용하면 파일 또는 디렉터리를 읽고 선택적으로 다시 쓸 수 있습니다. 드래그 앤 드롭 인터페이스의 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.webkitGetAsEntry() 메서드 사용

DataTransferItem.webkitGetAsEntry() 메서드는 항목이 파일인 경우 드래그 데이터 항목의 FileSystemFileEntry를 반환하고 항목이 디렉터리인 경우 FileSystemDirectoryEntry를 반환합니다. 파일 또는 디렉터리를 읽을 수는 있지만 다시 쓰는 방법은 없습니다. 이 메서드는 표준 트랙에 없다는 단점이 있지만 디렉터리를 지원한다는 장점이 있습니다.

Browser Support

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

Source

점진적 개선

아래 스니펫은 지원되는 경우 최신 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`;
    }
  }
});