【JavaScript】File System Access API – ドラッグ&ドロップ 編

File System Access API は、ファイル選択ダイアログディレクトリ選択ダイアログだけでなく、ドラッグ&ドロップでもファイルやディレクトリを受け入れることができます。以前からドラッグ&ドロップでファイルを読み取ることができましたが、File System Access API のドラッグ&ドロップであればファイル編集やディレクトリ操作まで扱うことができます。

今回は、File System Access API のドラッグ&ドロップによるファイルやディレクトリの操作に加え、旧来からのドラッグ&ドロップによるファイルの読み取りとの違いについて解説します。

ドロップされたファイルを読み取る旧来の方法

ドロップされたファイルを読み取る方法として、次のようなコードを見ることが多いのではないでしょうか。次のサンプルは、div 要素の領域にテキストファイルがドロップされたら、そのファイル名と内容をコンソールに出力します。

<div id="drop-box"></div>
const box_el = document.getElementById('drop-box');

box_el.addEventListener('dragover', (event) => {
  event.preventDefault();
});

box_el.addEventListener('drop', async (event) => {
  event.preventDefault();

  for (let i = 0; i < event.dataTransfer.files.length; i++) {
    // File オブジェクトを取得
    const file = event.dataTransfer.files[i];

    // テキストファイルかどうかを評価
    if (file.type.startsWith('text/')) {
      const content = await file.text();
      console.log('[' + file.name + ']');
      console.log(content);
    }
  }
});

div 要素に対して dragover イベントと drop イベントのリスナーをセットします。それぞれのリスナーではデフォルトアクションを止めるために Event オブジェクト(変数 event)の preventDefault() メソッドを呼び出しています。これでファイルをドロップする準備が整いました。

drop イベントのリスナーでは、event.dataTransfer.files にドロップされたファイルを表す File オブジェクトのリストが格納されます。

このサンプルでは、テキストファイルかどうかをチェックするために File オブジェクト(変数 file)の type プロパティの値が "text/" で始まっていることを評価しています。もしテキストファイルであれば、テキストファイルの中身を読み取って、ファイル名とともにコンソールに出力しています。

この旧来の方法のメリットは、すべてのメジャーブラウザーで動作する点です。一方で不足している点を挙げるとしたら、次の通りです。

  • ドロップしたファイルを編集することができない
  • ディレクトリをドロップして、その中のファイルやディレクトリを操作することができない

この不足をカバーするのが File System Access API です。以降は、2022 年 6 月現在、Chrome や Edge などの Chromium 系ブラウザーでのみ動作します。

File System Access API によるファイルのドロップ

File System Access API を使って前述のサンプルを書き直すと次のようになります。変わった点は、drop イベントのリスナーの処理です。

const box_el = document.getElementById('drop-box');

box_el.addEventListener('dragover', (event) => {
  event.preventDefault();
});

box_el.addEventListener('drop', async (event) => {
  event.preventDefault();

  // ドロップされたファイルまたはディレクトリを一つずつ処理
  for (let i = 0; i < event.dataTransfer.items.length; i++) {
    // DataTransferItem オブジェクトを取得
    const item = event.dataTransfer.items[i];

    // ドロップされた項目がテキストファイルかをチェック
    if (item.kind !== 'file' || !item.type.startsWith('text/')) {
      continue;
    }

    // FileSystemFileHandle オブジェクトを取得
    const handle = await item.getAsFileSystemHandle();

    // File オブジェクトを取得
    const file = await handle.getFile();

    // ファイル名と中身を出力
    const content = await file.text();
    console.log('[' + file.name + ']');
    console.log(content);
  }
});

drop イベントのリスナーで、event.dataTransfer.files ではなく、event.dataTransfer.items を使っている点に注目してください。

event.dataTransfer.files には、ファイルやディレクトリがドロップされたときだけ File オブジェクトが格納されます。そのため、ファイルやディレクトリでないものがドロップされることを考慮する必要はありませんでした。

しかし、event.dataTransfer.items は、ファイルやディレクトリでないものも DataTransferItem オブジェクトとして格納されてしまいますので、それがファイルやディレクトリかどうかを評価する必要があります。ファイルやディレクトリでないものとは、具体的にはテキストです。ページ上のテキストを選択状態にしてから、それをドラッグできることはご存じの通りです。そして、それをドロップすることもできますので、それを想定しなければいけません。

ファイルがドロップされたことが分かったら、DataTransferItem オブジェクト(変数 item)の getAsFileSystemHandle() メソッドを使って、該当のファイルの FileSystemFileHandle オブジェクトを取得します。

このサンプルは当初のコードより量が増えました。単にドロップされたファイルの中身を読み取りたいだけなら、わざわざ File System Access API を使う必要はないでしょう。しかし、FileSystemFileHandle オブジェクトが取得できさえすれば、読み取りはもちろんのこと、編集も可能になります。

ドロップされたファイルを編集

では、前述のサンプルを修正して、ドロップされたファイルの中身を書き換えてみましょう。次のサンプルは、テキストファイルをドロップすると、無条件に「書き換えました。」と書き換えます。以下は、drop イベントのリスナーのみを掲載しています。それ以外の部分は、前述のサンプルと同じです。

box_el.addEventListener('drop', async (event) => {
  event.preventDefault();

  // ドロップされたファイルまたはディレクトリを一つずつ処理
  for (let i = 0; i < event.dataTransfer.items.length; i++) {
    // DataTransferItem オブジェクトを取得
    const item = event.dataTransfer.items[i];

    // ドロップされた項目がテキストファイルかをチェック
    if (item.kind !== 'file' || !item.type.startsWith('text/')) {
      continue;
    }

    // FileSystemFileHandle オブジェクトを取得
    const handle = await item.getAsFileSystemHandle();

    // FileSystemWritableFileStream オブジェクトを取得
    const stream = await handle.createWritable();

    // テキストを書き込む
    await stream.write('書き換えました。');

    // ファイルを閉じる
    await stream.close();
  }
});

ドロップされたファイルを編集しようとすると、次のようなダイアログが表示され、ユーザーに許可を求めます。

ディレクトリをドロップして一覧表示

File System Access API を使ったドラッグ&ドロップではディレクトリを扱うこともできます。次のサンプルは、ディレクトリをドロップしたら、その直下にあるファイルまたはディレクトリの一覧をコンソールに出力します。

box_el.addEventListener('drop', async (event) => {
  event.preventDefault();

  // ドロップされたファイルまたはディレクトリを一つずつ処理
  for (let i = 0; i < event.dataTransfer.items.length; i++) {
    // DataTransferItem オブジェクトを取得
    const item = event.dataTransfer.items[i];

    // ドロップされた項目がファイルまたはディレクトリかをチェック
    if (item.kind !== 'file') {
      continue;
    }

    // FileSystemHandle (FileSystemDirectoryHandle) オブジェクトを取得
    const dropped_handle = await item.getAsFileSystemHandle();

    // ディレクトリかをチェック
    if (dropped_handle.kind !== 'directory') {
      continue;
    }

    // 対象のディレクトリ内のファイルとディレクトリの名前をコンソールに出力
    for await (const handle of dropped_handle.values()) {
      if (handle.kind === 'file') {
        console.log(handle.name);
      } else if (handle.kind === 'directory') {
        console.log(handle.name + '/');
      }
    }
  }
});

DataTransferItem オブジェクト(変数 item)の kind プロパティを使って、ドロップされた項目がファイルまたはディレクトリかをチェックしているコードに注意してください。

if (item.kind !== 'file') {
  continue;
}

ディレクトリがドロップされた場合、DataTransferItem オブジェクト(変数 item)の kind プロパティはファイルがドロップされたときと同じ "file" になります。"directory" ではありませんので注意してください。

ディレクトリにテキストファイルを新規に生成

File System Access API を使ったドラッグ&ドロップであれば、ファイルを新規に作成することもできます。次のサンプルは、ディレクトリをドロップすると、そのディレクトリ直下に new.txt というテキストファイルを新規に生成し、「これは新しいファイルです。」と書き込みます。

box_el.addEventListener('drop', async (event) => {
  event.preventDefault();

  // ドロップされたファイルまたはディレクトリを一つずつ処理
  for (let i = 0; i < event.dataTransfer.items.length; i++) {
    // DataTransferItem オブジェクトを取得
    const item = event.dataTransfer.items[i];

    // ドロップされた項目がファイルまたはディレクトリかをチェック
    if (item.kind !== 'file') {
      continue;
    }

    // FileSystemHandle (FileSystemDirectoryHandle) オブジェクトを取得
    const dropped_handle = await item.getAsFileSystemHandle();

    // ディレクトリかをチェック
    if (dropped_handle.kind !== 'directory') {
      continue;
    }

    // 対象のディレクトリ直下に new.txt を新規に生成して
    // FileSystemFileHandle オブジェクトを取得
    const fh = await dropped_handle.getFileHandle('new.txt', { create: true });

    // FileSystemWritableFileStream オブジェクトを取得
    const stream = await fh.createWritable();

    // テキストを書き込む
    await stream.write('これは新しいファイルです。');

    // ファイルを閉じる
    await stream.close();
  }
});

まとめ

File System Access API を使うことで、ドロップされたファイルの編集はもちろんのこと、ドロップされたディレクトリ内のファイル操作も自由自在になります。つまり、ディレクトリの中にディレクトリを作ったり、既存のファイルやディレクトリをリネームしたり、削除することすらできます。

ドロップされたファイルやディレクトリを表す FileSystemFileHandle オブジェクトや FileSystemDirectoryHandle オブジェクトの使い方については、記事「File System Access API – 仮想的なファイルシステム Origin Private File System 編」をご覧ください。

今回は以上で終わりです。最後まで読んでくださりありがとうございました。それでは次回の記事までごきげんよう。

Share