📋

Clipboard APIで非同期処理を含む操作をする際の注意点とその対策

2024/08/18に公開

本記事では、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にも記載があります。

https://developer.mozilla.org/ja/docs/Web/API/Clipboard_API#セキュリティの考慮

Chromium系ブラウザ

クリップボードの読み取りにはclipboard-read権限、書き込みにはclipboard-read権限またはUser Activation(以降は簡易的にユーザー操作と呼ぶ)が必要です。

例えば、ユーザー操作なしにClipboard.writeTextが実行されるページでは、実行された時点でclipboard-read権限を要求するPermission Promptが出ます。

Permission Promptが出ている画像

clipboard-read権限が付与されると、以降はユーザー操作なしでもクリップボードにアクセス可能です。

ユーザー操作がトリガーの場合は、clipboard-read権限が付与されていなくても書き込みが可能です。例えば次のコードはコピーが成功します。

const button = document.getElementById("copy-button");
// buttonクリック時にコピーしたいテキストをクリップボードにコピーする
button.addEventListener("click", () => {
  navigator.clipboard.writeText("コピーしたいテキスト")
});

SafariとFirefox

SafariとFirefoxでは、clipboard-readclipboard-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

ClipboardItemClipboard.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を使いづらくなるのでやめました。

脚注
  1. Clipboard API and events: https://www.w3.org/TR/clipboard-apis/ ↩︎

  2. Can I Use: https://caniuse.com/?search=Clipboard ↩︎

GitHubで編集を提案
サイボウズ フロントエンド

Discussion