[JavaScript] ファイル選択ダイアログを表示してファイルを読み書きする

<font color="#ff8080">※2024-09-10: 記事を全体的に編集しました。</font>

本記事では、ユーザーが `<a>` 要素や `<input>` 要素をクリックしたタイミングでなく、JavaScript のコード中の好きなタイミングでファイル選択ダイアログを出す方法について説明します。

click イベントが発生してから何か処理をした後にダイアログを出したい場合も有効です。

※新しいタブでファイルを表示する場合またはファイルを開く場合、ユーザーの操作によってコードが実行されなければ多くの WEB ブラウザでブロックされます。

本記事では `document.createElement()` で HTML 要素を作成しますが、静的な HTML でも同様です。

# 1. ファイルを保存する

データから URL を生成する方法がいくつか有ります:

- オブジェクト URL を使う
- データ URL を使う

生成した URL を `<a>` 要素で指定し click イベントを発火することで、ファイルを保存することが出来ます。

※ WEB ブラウザの設定によってはファイル選択ダイアログを表示せずにファイルを保存する場合があります。

参考「[HTMLElement: click() メソッド - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/click)」

Firefox バージョン 74 以前は click イベントを発生させるために `<a>` 要素を DOM 上に追加する必要がありましたが、最新版では不要です。

参考「[DOM - Firefox 75 for developers - Mozilla | MDN](https://developer.mozilla.org/ja/docs/Mozilla/Firefox/Releases/75#dom)」

## 1.1. オブジェクト URL を使う場合

`URL.createObjectURL()` を用いて以下のオブジェクトからオブジェクト URL を生成出来ます:

- `File`
- `Blob`
- `MediaSource`

※オブジェクト URL が不要になったら `URL.revokeObjectURL()` で解放してください。

```js
const blob = new Blob(['foo\n'], { type: 'text/plain' });

const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'foo.txt';

a.click();
```

参考「[URL: createObjectURL() 静的メソッド - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/URL/createObjectURL_static)」

## 1.2. データ URL を使う場合

データ URL を用いてもファイルの保存が可能ですが、データ URL を使用することによって様々な問題が発生する可能性があるため注意が必要です。

プレーンテキストや JSON などのサイズがあまり大きくないファイルを保存する場合に使用します。

※本記事ではプレーンテキストの例を記載しますが、Base64 エンコードを用いてバイナリデータも扱えます。

```js
const text = 'foo\n';

const a = document.createElement('a');
a.href = 'data:text/plain,' + encodeURIComponent(text);
a.download = 'foo.txt';

a.click();
```

参考「[データ URL - URI | MDN](https://developer.mozilla.org/ja/docs/Web/URI/Schemes/data)」
参考「[よくある問題 - データ URL - URI | MDN](https://developer.mozilla.org/ja/docs/Web/URI/Schemes/data#%E3%82%88%E3%81%8F%E3%81%82%E3%82%8B%E5%95%8F%E9%A1%8C)」

## 1.3. その他の方法

`location.href = url;` や `open(url, '_blank');` を用いることで、オブジェクト URL をファイルとして扱うことが出来ます。

※データ URL の場合は多くの WEB ブラウザでセキュリティ上の都合でブロックされます。

```js
const blob = new Blob(['foo\n'], { type: 'text/plain' });

location.href = URL.createObjectURL(blob);
```

```js
const blob = new Blob(['foo\n'], { type: 'text/plain' });

open(URL.createObjectURL(blob), '_blank');
```

参考「[location: href プロパティ - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/Location/href)」
参考「[Window: open() メソッド - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/Window/open)」

# 2. ファイルを開く

`<input type="file">` で click イベントを発火することで、ファイルを開くことが出来ます。

```js
const showOpenFileDialog = () => new Promise(resolve => {
	const input = document.createElement('input');
	input.type = 'file';
	input.accept = 'text/plain';
	input.onchange = () => { resolve(input.files); };
	input.click();
});

(async () => {

	const files = await showOpenFileDialog();
 
	const content = await files[0].text();
	console.log(content);
 
})();
```

参考「[\<input type="file"> - HTML: ハイパーテキストマークアップ言語 | MDN](https://developer.mozilla.org/ja/docs/Web/HTML/Element/input/file)」
参考「[HTML 属性: accept - HTML: ハイパーテキストマークアップ言語 | MDN](https://developer.mozilla.org/ja/docs/Web/HTML/Attributes/accept)」
参考「[HTMLInputElement: files プロパティ - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/HTMLInputElement/files)」
参考「[HTMLElement: click() メソッド - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/click)」

上記ではテキストを読み込むため `file.text()` を用いていますが、ファイルは様々な扱い方が可能です:

- `URL.createObjectURL(file)` の URL を用いる
    - 画像の表示等
- ファイルを他の型で扱う
    - `file.arrayBuffer()`: `ArrayBuffer`
    - `file.bytes()`: `Uint8Array`
        - **※多くの環境で未実装**
    - `file.stream()`: `ReadableStream`
    - `file.text()`

参考「[インスタンスメソッド - File - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/File#%E3%82%A4%E3%83%B3%E3%82%B9%E3%82%BF%E3%83%B3%E3%82%B9%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89)」
参考「[Instance methods - Blob - Web APIs | MDN](https://developer.mozilla.org/en-US/docs/Web/API/Blob#instance_methods)」

# 3. その他

## 3.1. 任意の DOM 要素を `<input type="file">` のように使う

上記の方法を用いて、任意の DOM 要素で発生したイベントハンドラからファイル選択ダイアログを表示可能なため、好きな DOM 要素を `<input type="file">` の代わりにすることが出来ます。

本記事の内容と直接の関係はありませんが、`<input type="file">` のスタイルを `display: none;` にして `<label>` 要素で囲むことで、`<label>` のクリックでイベントを発火することも可能です。

## 3.2. jQuery を使う

jQuery オブジェクトの `click()` (用途に関わらず**非推奨**) および `trigger('click')` は jQuery 上で設定されたイベントハンドラを呼ぶだけのため、要素をクリックした扱いになりません。

実際にクリックしたようにするためには `HTMLElement` の `click()` メソッドを呼ぶ必要があります。

`jQueryObject[0]` とすると jQuery オブジェクトから `HTMLElement` を得ることが出来ます。

参考「[How do I pull a native DOM element from a jQuery object? | jQuery Learning Center](https://learn.jquery.com/using-jquery-core/faq/how-do-i-pull-a-native-dom-element-from-a-jquery-object/)」

### 3.2.1. ファイルを保存する

```js:オブジェクト URL を使う場合
const blob = new Blob(['foo\n'], { type: 'text/plain' });

$('<a>', {
	href: URL.createObjectURL(blob),
	download: 'foo.txt',
})[0].click();
```

```js:データ URL を使う場合
const text = 'foo\n';

$('<a>', {
	href: 'data:text/plain,' + encodeURIComponent(text),
	download: 'foo.txt',
})[0].click();
```

### 3.2.2. ファイルを開く

```js
$('<input type="file" accept="text/plain">').on('change', async event => {

	const files = event.target.files;

	const content = await files[0].text();
	console.log(content);

})[0].click();
```