はじめに

私たちを取り巻くWeb技術は、もはや社会的なインフラとしてめまぐるしく進化しています。HTMLやCSSはもちろんのこと、JavaScriptやライブラリ、フレームワークなど、それぞれがニーズにキャッチアップする形で、機能強化を繰り返しています。その中でも、Web技術の中核に位置するにもかかわらず、意外と見過ごされがちなのがHTMLの進化です。本連載は、このHTMLと関連するJavaScript APIにフォーカスして、その新機能を手軽に試していただこうというものです。理解も利用もたやすいHTMLなので、ライトな気持ちで「こんなことができるようになったのか」を感じていただきます。

[NOTE]サンプルについて
本記事の配布サンプルは、以下のURLから入手できます。新機能を試していくので、ブラウザは最新である方がよいでしょう。本記事のサンプルは、執筆時点で最新のChromeで動作することを確認しています。
https://github.com/wateryinhare62/mynavi_html/

連載第9回の目的

この回では、クリップボードを扱えるClipboard APIを試します。ページのコンテンツをクリックでコピーしたり、ページをクリップボードの内容で更新したりする例を紹介します。

Clipboard API

Clipboard APIは、OSのクリップボードを操作するJavaScript APIです。ブラウザでクリップボードを使うには、ページ上のコンテンツを選択してコピー、テキスト入力ボックスなどのUI要素に貼り付け、というのが定番です。しかしながら、いずれもキーボードショートカットを使用したり、メニューを表示させる必要があるなど手順的にはやや面倒であり、モバイルデバイスでは使いにくいのが難点です。
Clipboard APIを使うことで、JavaScriptコードからクリップボードを操作できるようになります。イベントと組み合わせることで、ボタンクリックでコピー、貼り付けするといった操作が可能になり、あらかじめコピー/貼り付け対象の要素を決めておけば、それらを選択する手間も不要です。 例えば、以下のような活用が考えられるでしょう。

  • ユーザーがボタンをクリックしてテキストやURLをコピーできるようにすることで、簡単に情報を共有
  • フォームの入力内容を一括コピーできるようにすることで、申請や登録などの作業効率を向上
  • サイト内で生成されたパスワードやコードをワンクリックでコピーできるようにして、セキュリティや利便性を向上
  • ドラッグ&ドロップ操作と併用し、画像やファイルをクリップボード経由で保存/取得することで、直感的なUIを実現

Clipboard APIのオブジェクト/メソッド

Clipboard APIには、以下のオブジェクトおよびメソッドが用意されています。これらを使って、クリップボードへコピー/貼り付ける例を紹介していきます。

  • navigator.clipboard:クリップボードのオブジェクト。ブラウザがサポートしない場合にはundefinedになる
  • read/write:任意のオブジェクトの読み出しと書き込みのためのメソッド
  • readText/writeText:テキストに特化した読み出しと書き込みのためのメソッド

Clipboard APIのBaselineは、writeTextメソッドのみがWidely availableでほとんどのブラウザで使えます。残るメソッドは、2024年から多くのブラウザでサポートの始まったNewly availableです。このため、一部のブラウザでは動作しない可能性があります(Chromeの最新版では問題なく動作します)。

  • 図1:完成サンプル

    図1:完成サンプル

Clipboard APIが利用できるか判定する

Clipboard APIに限らず、その機能が利用できる環境であるのかの判定は必須です。以降のサンプルでは、ボタン押下などのイベントハンドラ内で判定関数を呼び出し、利用できる環境なのかをチェックしています(そのため、それぞれのサンプルではこの関数と呼び出しコードは省略)。

// Clipboard APIが利用できるか判定する関数
function canUseClipboard() {
  return !!navigator.clipboard && window.isSecureContext;
}
…略…
// イベントハンドラ内で判定結果をページに反映する
copyButton.addEventListener('click', async () => {
  if (!canUseClipboard()) {
    feedbackElement.textContent = 'Clipboard APIは安全なコンテキストでのみ利用可能です。';
    return;
  }
  …略…
}

canUseClipboard関数で使われている2つの式は、それぞれ以下のような意味です。

  • !!navigator.clipboard:ブラウザがClipboard APIをサポートするか(clipboardオブジェクトが有効か)
  • window.isSecureContext:当該ウィンドウが、保護されたコンテキスト(セキュアコンテキスト)を満たしているか

保護されたコンテキストとは、認証と機密性の最低基準を満たしているウィンドウやワーカーをいいます。具体的にはhttpsなどの安全な接続である、必要な場合には認証されている、クリップボードのアクセスにユーザーの許可を得ている、などです。Clipboard APIを含む多くのAPIや機能は、保護されたコンテキストでのみアクセス可能となっています。なお、navigator.clipboardに「!!」を前置するのは、完全なboolean値として評価するためです。

クリップボードへのコピー

Webページのコンテンツをクリップボードへコピーしたいという局面は一般的で、textWriteメソッドが早期からサポートされたのもそれが理由と思われます。よくあるのは、サンプルコードをクリック一つでコピーするといった局面です。ここでは、Webページに用意したテキストや画像をクリップボードにコピーする例を紹介します。

クリップボードへのテキストのコピー

クリップボードへテキストをコピーするには、clipboardオブジェクトのwriteTextメソッドを使います。Webページ上のプログラムコードをコピーする例を通じて、writeTextメソッドの使い方を紹介します。リスト1は、コピーするテキスト領域を含むHTMLです。

リスト1:copy_text.html

<p><pre id="target">let kukuText = '';
for (let i = 1; i <= 9; i++) {
    for (let j = 1; j <= 9; j++) {
        kukuText += `${i} × ${j} = ${i * j}`;
        if (j < 9) {
            kukuText += '\n';
        }
    }
    if (i < 9) {
        kukuText += '\n\n';
    }
}</pre></p>
<div><button id="button">テキストのコピー</button></div>
<p id="feedback">結果はここに表示されます。</p>

以下は、以降のサンプルにも通じるHTMLの基本形の説明です。

  • id属性が"target"である要素からコピー、あるいは要素へ貼り付ける
  • id属性が"button"である要素のイベントでコピー/貼り付けを実行する
  • id属性が"feedback"である要素にコピー/貼り付けの結果を反映する

それぞれのid属性に対応する要素オブジェクトをJavaScriptコード中で取得していますが、定型的なコードであるので掲載は省いています。また、見た目を調整するためにCSSファイルを適用していますが、本項の内容と直接の関係はないので、掲載は割愛しています。いずれも全容は配布サンプルを参照してください。
リスト2は、ボタンクリックで呼び出されるJavaScriptコードです。

リスト2:copy_text.js

(1)イベントハンドラはasync関数とする
buttonElement.addEventListener('click', async () => {
  …略…
  const sourceText = document.getElementById('target').textContent;
  (2)メソッド呼び出しをtry~catchで処理する
  try {
    (3)writeTextメソッドをawait付きで呼び出す
    await navigator.clipboard.writeText(sourceText);
    feedbackElement.textContent = 'コピーしました!';
  } catch (err) {
    (4)失敗したらその旨を反映する
    feedbackElement.textContent = `コピーできませんでした: ${err.message}`;
  }
});

writeTextメソッドをはじめ、Clipboard APIのメソッドは呼び出し結果をPromiseオブジェクトで返します。このため、呼び出しの終了/失敗をハンドリングする必要がありますが、本稿では簡略化のためにasync/await構文を使用しています。これを受けて、本稿のサンプルは以下のような共通の構造となっています。

(1)イベントハンドラの関数はasyncな関数として定義する
(2)メソッド呼び出しをtry~catch構文で処理する
(3)メソッドはawait付きで呼び出す
(4)メソッド呼び出し失敗時の処理はcatchブロックに記述する

このサンプルは、pre要素から取り出したテキストを、そのままwriteTextメソッドに渡しているというシンプルなものです。pre要素であるので、改行もそのまま結果に反映されますが、本来はHTMLタグを含んだテキストを処理するものとして、writeTextメソッドに渡す前にタグを除去するなどの前処理を施すことになるでしょう。
ブラウザ上でボタンをクリックし、メモ帳などのアプリにペーストすると、元のテキストが貼り付けられることが確認できます(図2)。

  • 図2:クリップボードへのテキストのコピー

    図2:クリップボードへのテキストのコピー

クリップボードへの画像のコピー

クリップボードへ画像をコピーするには、clipboardオブジェクトのwriteメソッドを使います。このメソッドは画像に限らず、任意の形式のオブジェクトをクリップボードへ書き込む汎用的なものです(writeTextはテキストに特化)。Webページに表示された画像をコピーする例を通じて、writeメソッドの使い方を紹介します。リスト3は、コピーする画像を含むHTMLです(id属性などのルールは前項と同じ)。

リスト3:copy_image.html

<div><img id="target" src="img/image.jpg" alt="Sample Image"></div>
<div><button id="button">画像のコピー</button></div>
<p id="feedback">結果はここに表示されます。</p>

リスト4は、ボタンクリックで呼び出されるJavaScriptコードです。画像をコピーするという性質上、writeTextメソッドを使う場合に比べ、やや複雑な手順となります。

リスト4:copy_image.js

buttonElement.addEventListener('click', async () => {
…略…
  const sourceImage = document.getElementById('target');
  (1)canvasオブジェクトを作成して画像を書き込む
  const canvas = document.createElement('canvas');
  canvas.width = sourceImage.naturalWidth;
  canvas.height = sourceImage.naturalHeight;
  const ctx = canvas.getContext('2d');
  ctx.drawImage(sourceImage, 0, 0);
  (2)canvasオブジェクトの内容をBLOBに変換してクリップボードに書き込む
  canvas.toBlob(async (blob) => {
    (3)ClipboardItemオブジェクトをMIMEタイプimage/pngとして作成する
    const item = new ClipboardItem({
      'image/png': blob
    });
    try {
      (4)クリップボードに書き込む
      await navigator.clipboard.write([item]);
      feedbackElement.textContent = 'コピーしました!';
    } catch (err) {
      feedbackElement.textContent = `コピーできませんでした: ${err.message}`;
    }
  });
}

前項と共通する部分は説明を省略します。

まず、(4)でwriteメソッドに渡すデータはClipboardItemオブジェクト(クリップボードのデータそのものに対応するオブジェクト)の配列である必要があるので、それを構築する処理が大半となります。
ClipboardItemオブジェクトの構築は、以下の流れとなります。

(1)canvasオブジェクトを作成して画像を書き込む。これは、ClipboardItemオブジェクトに格納するBLOB(Binary Large Object)を生成するために必要。なお、書き込む画像のサイズは、ブラウザによる補正の入らないサイズであるnaturalWidthプロパティとnaturalHeightプロパティを使う
(2)canvasオブジェクトのtoBlobメソッドでBLOBを生成する。toBlobメソッドの引数はBLOBを引数とする関数で、以下の(3)(4)の処理を行う。2番目の引数には画像形式を指定できるが、省略しているので既定値であるimage/pngとなる
(3)ClipboardItemオブジェクトにキーをMIMEタイプimage/pngとしてBLOBを格納
(4)出来上がったClipboardItemオブジェクトの配列を引数としてwriteメソッドを呼び出す

ブラウザ上でボタンをクリックし、ペイントなどのアプリにペーストすると、元の画像が貼り付けられることが確認できます(図3)。

  • 図3:クリップボードへの画像のコピー

    図3:クリップボードへの画像のコピー

クリップボードからの貼り付け

Webページからクリップボードへコピーしたいという局面の方が多いと思われますが、その逆も可能です。クリップボードのテキストをテキスト入力ボックスなどにボタン一つで貼り付けたり、ペイントなどのアプリで編集中の画像をキャンバスに貼り付けるといった局面です。ここでは、前節と同じくクリップボードにあるテキストと画像をWebページに貼り付ける例を紹介します。

クリップボードからのテキストの貼り付け

クリップボードからテキストを取得するには、clipboardオブジェクトのreadTextメソッドを使います。readTextメソッドは、クリップボードにテキストデータがあれば、それを返します。クリップボードのテキストをtextarea要素に貼り付ける例を通じて、readTextメソッドの使い方を紹介します。リスト5は、textarea要素を配置したHTMLです(id属性などのルールはこれまでと同じ)。

リスト5:paste_text.html

<textarea id="target" cols="40" rows="10">テキストはここに貼り付けられます。</textarea>
<button id="button">テキストの貼り付け</button>
<p id="feedback">結果はここに表示されます。</p>

リスト6は、ボタンクリックで呼び出されるJavaScriptコードです。

リスト6:paste_text.js

buttonElement.addEventListener('click', async () => {
  …略…
  try {
    (1)クリップボードからテキストを取得して対象要素に設定する
    const text = await navigator.clipboard.readText();
    targetElement.value = text;
    feedbackElement.textContent = '貼り付けました!';
  } catch (err) {
    feedbackElement.textContent = `貼り付けできませんでした: ${err.message}`;
  }
});

テキストの取得は、非常にシンプルです。readTextメソッドの呼び出し結果を、そのままtextarea要素に設定しているだけです。
メモ帳アプリなどで適当なテキストをコピーし、それをブラウザ上の[テキストの貼り付け]ボタンで貼り付けました。図4のようにテキストが貼り付けられたことが確認できます。

  • 図4:クリップボードからのテキストの貼り付け

    図4:クリップボードからのテキストの貼り付け

クリップボードからの画像の貼り付け

クリップボードから画像をはじめとする任意のオブジェクトを取得するには、clipboardオブジェクトのreadメソッドを使います。readメソッドは、クリップボードにデータがあれば、ClipboardItemsオブジェクトを返します。ClipboardItemsオブジェクトは、ClipboardItemオブジェクトの配列で、具体的にはクリップボードにあるさまざまな形式のオブジェクトです。一般的な手順として、配列から順番にClipboardItemオブジェクトを取り出し、目的のデータ形式であれば処理する、といった流れになります。クリップボードの画像をcanvas要素に貼り付ける例を通じて、readメソッドの使い方を紹介します。リスト7は、canvas要素を配置したHTMLです(id属性などのルールはこれまでと同じ)。

リスト7:paste_image.html

<button id="button">画像の貼り付け</button>
<p id="feedback">結果はここに表示されます。</p>
<canvas id="target" width="500" height="500"></canvas>

リスト8は、ボタンクリックで呼び出されるJavaScriptコードです。

リスト8:paste_image.js

buttonElement.addEventListener('click', async () => {
  …略…
  try {
    (1)readメソッドでClipboardItemsオブジェクトを取得しPNG画像があるか調べる
    const clipboardItems = await navigator.clipboard.read();
    if (!clipboardItems.some(item => item.types.includes('image/png'))) {
      feedbackElement.textContent = 'クリップボードに画像が見つかりませんでした。';
    } else {
      (2)すべてのClipboardItemオブジェクトについて処理する
      for (const clipboardItem of clipboardItems) {
        (3)データが画像(image/png)であるときのみ処理する
        if (clipboardItem.types.includes('image/png')) {
          (4)画像(image/png)を取得しcanvasに書き込む
          const blob = await clipboardItem.getType('image/png');
          const img = new Image();
          img.onload = () => {
            const targetCanvas = document.getElementById('target');
            const targetContext = targetCanvas.getContext('2d');
            targetContext.drawImage(img, 0, 0);
            feedbackElement.textContent = '貼り付けました!';
          };
          img.src = URL.createObjectURL(blob);
          break;
        }
      }
    }
  } catch (err) {
    feedbackElement.textContent = `貼り付けできませんでした: ${err.message}`;
  }
});

(1)のようにreadメソッドを呼び出すと、ClipboardItemsオブジェクトが得られます。まずは、それを使ってPNG画像(MIMEタイプがimage/pngである画像)が存在するか、typesプロパティのincludeメソッドを使って調べます。
存在すれば、(2)のようにfor文でそれぞれのClipboardItemオブジェクトについて処理します。クリップボードには、さまざまな形式のデータが含まれる可能性があるので、(3)のようにtypesプロパティを参照して、最初に見つかるPNG画像のみを処理します。(4)でgetTypeメソッドで画像データをBLOBとして取得し、それをもとにImageオブジェクトを構築して、最終的にcanvasに書き込みます。
ペイントアプリで適当な画像をコピーし、それをブラウザ上の[画像の貼り付け]ボタンで貼り付けました。図5のように画像が貼り付けられたことが確認できます。

  • 図5:クリップボードからの画像の貼り付け

    図5:クリップボードからの画像の貼り付け

まとめ

Clipboard APIはいかがでしたでしょうか。ページ上のテキストをワンタッチでコピー!というのはよく見る場面ですが、逆に貼り付けたり、画像も取り扱えるなど、使い勝手が大幅に向上しているのをお伝えできたのではないかと思います。次回は、ローカルのファイルシステムにアクセスするFile System Access APIを試します。ファイルを開いて読み込んだり、コンテンツを保存したり、フォルダ構造にアクセスする例を紹介します。

WINGSプロジェクト 山内直(著) 山田 祥寛(監修)
有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティ(代表山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手がける。現在も執筆メンバーを募集中。興味のある方は、どしどし応募頂きたい。著書、記事多数。
RSS
X:@WingsPro_info(公式)@WingsPro_info/wings(メンバーリスト)
Facebook

<著者について>
WINGSプロジェクト所属のテクニカルライター。出版社を経てフリーランスとして独立。ライター、エディター、デベロッパー、講師業に従事。屋号は「たまデジ。」。