テキストと画像のクリップボードへのアクセスを安全にブロック解除
従来、システム クリップボードにアクセスするには、クリップボード操作用の document.execCommand()
を使用していました。このカットと貼り付けの方法は広くサポートされていますが、クリップボードへのアクセスが同期的であり、DOM への読み取りと書き込みのみが可能であるというコストがあります。
短いテキストの場合は問題ありませんが、クリップボードへの転送のためにページをブロックすると、エクスペリエンスが低下するケースは少なくありません。時間のかかるサニタイズや
コンテンツを安全に貼り付けるには、画像のデコードが必要になる場合があります。ブラウザは、貼り付けたドキュメントからリンクされたリソースを読み込むか、インライン化する必要があります。この場合、
ディスクまたはネットワークで待機している間、ページをブロックします。権限を追加して、クリップボードへのアクセスをリクエストする際にブラウザがページをブロックするようにするとします。一方、クリップボード操作用の document.execCommand()
に設定されている権限は、厳密に定義されておらず、ブラウザによって異なります。
Async Clipboard API は、これらの問題に対処し、ページをブロックしない明確な権限モデルを提供します。Async Clipboard API は、ほとんどのブラウザでテキストと画像の処理に限定されていますが、サポートは異なります。ブラウザをよく調査し、 互換性の概要について説明します。
コピー: クリップボードにデータを書き込む
writeText()
テキストをクリップボードにコピーするには、writeText()
を呼び出します。この API は非同期であるため、writeText()
関数は、渡されたテキストが正常にコピーされたかどうかに応じて解決または拒否される Promise を返します。
async function copyPageUrl() {
try {
await navigator.clipboard.writeText(location.href);
console.log('Page URL copied to clipboard');
} catch (err) {
console.error('Failed to copy: ', err);
}
}
write()
実際には、writeText()
は汎用的な write()
メソッドの便利なメソッドにすぎません。このメソッドでは、画像をクリップボードにコピーすることもできます。writeText()
と同様に、非同期で Promise を返します。
クリップボードに画像を書き込むには、画像を blob
として指定する必要があります。これを実現する 1 つの方法は、
これは、fetch()
を使用してサーバーに画像をリクエストしてから、
次の日付の blob()
:
レスポンスが返されます。
サーバーからの画像のリクエストは、
理由もさまざまです。幸いなことに、キャンバスに画像を描画して、
キャンバスを呼び出して
toBlob()
メソッドを呼び出します。
次に、ClipboardItem
オブジェクトの配列をパラメータとして write()
メソッドに渡します。現在、一度に渡せる画像は 1 つだけですが、今後は複数の画像のサポートを追加する予定です。ClipboardItem
は、
MIME タイプをキーとし、blob を値として指定します。blob の場合
fetch()
または canvas.toBlob()
(blob.type
プロパティ)から取得したオブジェクト
には、画像の正しい MIME タイプが自動的に含まれます。
try {
const imgURL = '/images/generic/file.png';
const data = await fetch(imgURL);
const blob = await data.blob();
await navigator.clipboard.write([
new ClipboardItem({
// The key is determined dynamically based on the blob's type.
[blob.type]: blob
})
]);
console.log('Image copied.');
} catch (err) {
console.error(err.name, err.message);
}
または、ClipboardItem
オブジェクトにプロミスを書き込むこともできます。このパターンでは、データの MIME タイプを事前に把握しておく必要があります。
try {
const imgURL = '/images/generic/file.png';
await navigator.clipboard.write([
new ClipboardItem({
// Set the key beforehand and write a promise as the value.
'image/png': fetch(imgURL).then(response => response.blob()),
})
]);
console.log('Image copied.');
} catch (err) {
console.error(err.name, err.message);
}
copy イベント
ユーザーがクリップボードのコピーを開始し、preventDefault()
を呼び出さない場合、copy
イベントには、アイテムがすでに正しい形式になっている clipboardData
プロパティが含まれます。独自のロジックを実装する場合は、preventDefault()
を呼び出して、
独自の実装を優先してデフォルトの動作を防止できます。
この場合、clipboardData
は空になります。あるページにテキストと画像が含まれていて、ユーザーがすべてと画像を選択すると、
クリップボードのコピーを開始した場合、カスタム ソリューションではテキストを破棄し、
画像をコピーします。以下のコードサンプルに示すように、これを実現できます。
上記の例では、前のステップにフォールバックする方法については、
(Clipboard API がサポートされていない場合)。
<!-- The image we want on the clipboard. -->
<img src="kitten.webp" alt="Cute kitten.">
<!-- Some text we're not interested in. -->
<p>Lorem ipsum</p>
document.addEventListener("copy", async (e) => {
// Prevent the default behavior.
e.preventDefault();
try {
// Prepare an array for the clipboard items.
let clipboardItems = [];
// Assume `blob` is the blob representation of `kitten.webp`.
clipboardItems.push(
new ClipboardItem({
[blob.type]: blob,
})
);
await navigator.clipboard.write(clipboardItems);
console.log("Image copied, text ignored.");
} catch (err) {
console.error(err.name, err.message);
}
});
copy
イベントの場合:
ClipboardItem
の場合:
対応ブラウザ
- <ph type="x-smartling-placeholder">
- <ph type="x-smartling-placeholder">
- <ph type="x-smartling-placeholder">
貼り付け: クリップボードからデータを読み取っている
readText()
クリップボードからテキストを読み取るには、navigator.clipboard.readText()
を呼び出して、返された promise が解決するのを待ちます。
async function getClipboardContents() {
try {
const text = await navigator.clipboard.readText();
console.log('Pasted content: ', text);
} catch (err) {
console.error('Failed to read clipboard contents: ', err);
}
}
read()
navigator.clipboard.read()
メソッドも非同期で、Promise を返します。クリップボードから画像を読み取るには、
ClipboardItem
それらに対して反復処理を行います。
各 ClipboardItem
は異なるタイプのコンテンツを保持できるため、for...of
ループを使用して、タイプリストを反復処理する必要があります。タイプごとに、現在のタイプを引数として getType()
メソッドを呼び出して、対応する blob を取得します。これまでと同様に、このコードは画像に関連付けられておらず、今後の他のファイル形式でも機能します。
async function getClipboardContents() {
try {
const clipboardItems = await navigator.clipboard.read();
for (const clipboardItem of clipboardItems) {
for (const type of clipboardItem.types) {
const blob = await clipboardItem.getType(type);
console.log(URL.createObjectURL(blob));
}
}
} catch (err) {
console.error(err.name, err.message);
}
}
貼り付けたファイルの操作
ユーザーが ctrl+c や ctrl+v などのクリップボードのキーボード ショートカットを使用できるようにすることは便利です。Chromium は、以下に示すように、クリップボードに読み取り専用のファイルを公開します。ユーザーがオペレーティング システムのデフォルトの貼り付けショートカットを押すとトリガーされます。 またはユーザーがブラウザのメニューバーの [編集]、[貼り付け] の順にクリックしたとき。 追加のプラミングを行う必要はありません。
document.addEventListener("paste", async e => {
e.preventDefault();
if (!e.clipboardData.files.length) {
return;
}
const file = e.clipboardData.files[0];
// Read the file's contents, assuming it's a text file.
// There is no way to write back to it.
console.log(await file.text());
});
対応ブラウザ
- <ph type="x-smartling-placeholder">
- <ph type="x-smartling-placeholder">
- <ph type="x-smartling-placeholder">
貼り付けイベント
前述したように、今後、Clipboard API と連携するイベントを導入する予定です。
現時点では、既存の paste
イベントを使用できます。これは、クリップボードのテキストを読み取る新しい非同期メソッドとうまく連携します。copy
イベントと同様に、
preventDefault()
の呼び出しを忘れる。
document.addEventListener('paste', async (e) => {
e.preventDefault();
const text = await navigator.clipboard.readText();
console.log('Pasted text: ', text);
});
複数の MIME タイプの処理
ほとんどの実装では、1 回のカットで複数のデータ形式をクリップボードに配置できます。 使用できます。この理由は 2 つあります。アプリ デベロッパーは、ユーザーがテキストや画像をコピーするアプリの機能を把握できません。また、多くのアプリケーションは、構造化データをプレーンテキストとして貼り付けることをサポートしています。これは通常 [編集] メニュー項目とともに [貼り付けと [スタイルに合わせる] または [書式なしで貼り付け] を選択します。
次の例は、その方法を示しています。この例では、fetch()
を使用して以下を取得します。
画像データだけでなく、さまざまな画像から
<canvas>
または File System Access API を使用できます。
async function copy() {
const image = await fetch('kitten.png').then(response => response.blob());
const text = new Blob(['Cute sleeping kitten'], {type: 'text/plain'});
const item = new ClipboardItem({
'text/plain': text,
'image/png': image
});
await navigator.clipboard.write([item]);
}
セキュリティと権限
クリップボードへのアクセスは、常にブラウザのセキュリティ上の懸念事項でした。適切な権限がないと、ページがあらゆる種類の悪意のあるコンテンツをユーザーのクリップボードにサイレントでコピーし、貼り付けたときに致命的な結果をもたらす可能性があります。rm -rf /
や圧縮爆弾画像をクリップボードにサイレントでコピーするウェブページがあるとします。
ウェブページにクリップボードへの無制限の読み取りアクセス権を付与するのはさらに厄介です。ユーザーは、パスワードや個人情報などの機密情報をクリップボードにコピーするのが一般的ですが、この情報はユーザーの知らないうちに任意のページによって読み取られる可能性があります。
多くの新しい API と同様に、Clipboard API は HTTPS 経由で提供されるページでのみサポートされます。不正使用を防ぐため、クリップボードへのアクセスは、ページがアクティブなタブの場合にのみ許可されます。アクティブなタブのページは、権限をリクエストせずにクリップボードに書き込むことができますが、クリップボードからの読み取りには常に権限が必要です。
コピーと貼り付けの権限が Permissions API に追加されました。clipboard-write
権限は、ページがクロールされたときに自動的に付与されます。
クリックします。clipboard-read
権限をリクエストする必要があります。これを行うには、
クリップボードからデータを読み取ろうとします。次のコードは、後者を示しています。
const queryOpts = { name: 'clipboard-read', allowWithoutGesture: false };
const permissionStatus = await navigator.permissions.query(queryOpts);
// Will be 'granted', 'denied' or 'prompt':
console.log(permissionStatus.state);
// Listen for changes to the permission state
permissionStatus.onchange = () => {
console.log(permissionStatus.state);
};
allowWithoutGesture
オプションを使用して、カットや貼り付けを呼び出すためにユーザー ジェスチャーが必要かどうかを制御することもできます。この値のデフォルトはブラウザによって異なるため、常に含める必要があります。
ここで、Clipboard API の非同期性がとても役に立ちます。 クリップボード データを読み書きしようとすると、ユーザーの入力を まだ付与されていない場合は付与してください。API は Promise ベースのものなので これは完全に透過的であり、ユーザーがクリップボードの権限を拒否すると、 ページが適切に応答できるように、拒否するプロミスを約束する。
ブラウザでは、ページがアクティブなタブの場合にのみクリップボードへのアクセスが許可されるため、デベロッパー ツール自体がアクティブなタブであるため、ここで紹介する例の一部は、ブラウザのコンソールに直接貼り付けると実行されません。コツはやはり先延ばしです。
setTimeout()
を使用してクリップボードにアクセスし、ページ内をすばやくクリックして
関数が呼び出される前にフォーカスを合わせます。
setTimeout(async () => {
const text = await navigator.clipboard.readText();
console.log(text);
}, 2000);
権限ポリシーの統合
この API を iframe で使用するには、
権限ポリシー
選択的に有効化および有効化できるメカニズムを
ブラウザの各種機能や API を無効にできます。具体的には、環境変数、
アプリのニーズに応じて、clipboard-read
と clipboard-write
のいずれかまたは両方を選択できます。
<iframe
src="index.html"
allow="clipboard-read; clipboard-write"
>
</iframe>
特徴検出
すべてのブラウザをサポートしながら Async Clipboard API を使用するには、以下をテストします。
navigator.clipboard
を実行し、以前のメソッドにフォールバックします。たとえば、他のブラウザを含めるように貼り付けを実装する方法は次のとおりです。
document.addEventListener('paste', async (e) => {
e.preventDefault();
let text;
if (navigator.clipboard) {
text = await navigator.clipboard.readText();
}
else {
text = e.clipboardData.getData('text/plain');
}
console.log('Got pasted text: ', text);
});
それだけではありません。Async Clipboard API の登場以前は、ウェブブラウザ間でコピーと貼り付けの実装が異なっていました。ほとんどのブラウザでは
ブラウザ独自のコピー&ペーストは、
document.execCommand('copy')
と document.execCommand('paste')
。テキストが
DOM に存在しない文字列の場合は、
DOM と選択済み:
button.addEventListener('click', (e) => {
const input = document.createElement('input');
input.style.display = 'none';
document.body.appendChild(input);
input.value = text;
input.focus();
input.select();
const result = document.execCommand('copy');
if (result === 'unsuccessful') {
console.error('Failed to copy text.');
}
input.remove();
});
デモ
以下のデモで Async Clipboard API を試すことができます。Glitch では、テキストのデモまたは画像のデモをリミックスして試すことができます。
最初の例では、テキストをクリップボードに移動したり、クリップボードから移動したりする方法を示します。
画像で API を試すには、このデモを使用します。前に説明したように、サポートされているのは PNG のみです。 かつ、 いくつかのブラウザで動作します。
関連リンク
謝辞
非同期クリップボード API は、Darwin Huang と Gary Kačmarčík によって実装されました。Darwin もデモを提供しました。この記事の一部を確認してくれた Kyarik と Gary Kačmarčík に感謝します。
Unsplash の Markus Winkler によるヒーロー画像。