Clipboard APIで非同期処理を含む操作をする際の注意点とその対策
本記事では、Clipboard APIのブラウザ間での仕様の違いと、非同期処理を含む操作をする際の注意点とその対策をまとめました。
Clipboard APIとは
Clipboard APIは、Webアプリケーションがシステムクリップボードにアクセスするためのインターフェースを提供します。
例えば、Clipboard.writeText
を使用すると、クリップボードを特定の文字列で上書きすることができます。
// テキストをクリップボードにコピー
navigator.clipboard.writeText("コピーしたいテキスト").then(() => {
console.log("コピー成功");
}).catch(err => {
console.error("コピー失敗:", err);
});
Clipboard APIは、現在W3CのWorking Draftとして公開されていますが、主要ブラウザで使用可能です[1][2]。
ブラウザ間での仕様の違い
Clipboard APIは、セキュリティ上の理由から利用に制限があり、その仕様がブラウザ間で異なります。詳細はMDNにも記載があります。
Chromium系ブラウザ
クリップボードの読み取りにはclipboard-read
権限、書き込みにはclipboard-read
権限またはUser Activation(以降は簡易的にユーザー操作と呼ぶ)が必要です。
例えば、ユーザー操作なしにClipboard.writeText
が実行されるページでは、実行された時点でclipboard-read
権限を要求するPermission Promptが出ます。
clipboard-read
権限が付与されると、以降はユーザー操作なしでもクリップボードにアクセス可能です。
ユーザー操作がトリガーの場合は、clipboard-read
権限が付与されていなくても書き込みが可能です。例えば次のコードはコピーが成功します。
const button = document.getElementById("copy-button");
// buttonクリック時にコピーしたいテキストをクリップボードにコピーする
button.addEventListener("click", () => {
navigator.clipboard.writeText("コピーしたいテキスト")
});
SafariとFirefox
SafariとFirefoxでは、clipboard-read
とclipboard-write
権限はサポートされていないため、読み書き両方にユーザー操作が必要です。ユーザー操作なしに実行される場合は単にPromiseがrejectされます。
非同期処理を含むクリップボード操作の注意点
例えば、ボタン押下時にfetch APIでリソースを取得し、レスポンスをクリップボードにコピーしたいとします。次のようなコードです。
const button = document.getElementById("copy-button");
button.addEventListener("click", () => {
fetch("https://example.com/api/v1/")
.then(response => response.text())
// fetchの結果をwriteTextに渡す
.then(text => navigator.clipboard.writeText(text))
});
上記コードは、Chrome、Firefoxでは成功しますが、Safariでは失敗します。
Unhandled Promise Rejection: NotAllowedError: The request is not allowed by the user agent or the platform in the current context, possibly because the user denied permission.
Safariでは、ユーザー操作の処理方法が他ブラウザと異なるようです(詳細はWebKitのバグ#222262を参照)。fetch APIに限らず非同期処理のコンテキストでは、クリックイベントハンドラ内でもユーザ操作とみなされずパーミッションエラーになります。
対応策1
ClipboardItem
とClipboard.write
を使用することで非同期処理を含むクリップボード操作を行うことができます。
const button = document.getElementById("copy-button");
button.addEventListener("click", () => {
const clipboardItem = new ClipboardItem({
"text/plain": fetch("https://example.com/api/v1/")
.then(response => response.text())
// Blobオブジェクトをreturnする
.then((text) => new Blob([text], { type: "text/plain" })),
})
navigator.clipboard.write([clipboardItem])
});
ClipboardItem
オブジェクトを作成し、コピーしたい文字列をBlob
オブジェクトとして渡します。その後、ClipboardItem
オブジェクトをClipboard.write
に配列で渡します。
この方法は、Chrome、Safari、Firefoxでコピーが成功することを確認できました。
対応策2
設計自体を変えて、非同期処理とコピー処理を分けてしまう方法です。
const fetchButton = document.getElementById("fetch-button");
const copyButton = document.getElementById("copy-button");
const textDisplay = document.getElementById("text-display");
// データフェッチ後、コピーしたい文字列とコピーボタンを描画する
fetchButton.addEventListener("click", () => {
fetch("https://example.com/api/v2/")
.then((response) => response.text())
.then((text) => {
// コピーしたい文字列をtextDisplay内に描画する
textDisplay.textContent = text;
copyButton.style.display = "block";
});
});
// textDisplay内に表示される文字列をコピーする
copyButton.addEventListener("click", () => {
navigator.clipboard.writeText(textDisplay.innerText)
.then(alert("クリップボードにコピーしました!"));
});
Clipboard.writeText
を非同期処理のコンテキスト外で実行するため、ユーザ操作としてコピーできます。
おわりに
対応策2の方は、ユーザがクリップボードにコピーされる文字列を認識できる点でベターな方法かなと思います。
CodePenを埋め込む方がわかりやすいので迷いましたが、例にfetch APIを使いづらくなるのでやめました。
-
Clipboard API and events: https://www.w3.org/TR/clipboard-apis/ ↩︎
-
Can I Use: https://caniuse.com/?search=Clipboard ↩︎
Discussion