Akses papan klip yang lebih aman untuk teks dan gambar
Cara tradisional untuk mendapatkan akses ke papan klip sistem adalah melalui
document.execCommand()
untuk interaksi papan klip. Meskipun didukung secara luas, metode pemotongan dan
penempelan ini memiliki biaya: akses papan klip bersifat sinkron, dan hanya dapat membaca
dan menulis ke DOM.
Hal ini tidak masalah untuk teks kecil, tetapi ada banyak kasus saat pemblokiran
halaman untuk transfer papan klip memberikan pengalaman yang buruk. Sanitasi yang memakan waktu atau
decoding gambar mungkin diperlukan sebelum konten dapat ditempelkan dengan aman. Browser
mungkin perlu memuat atau menyisipkan resource tertaut dari dokumen yang ditempel. Itu akan
memblokir laman selagi
menunggu {i>disk<i} atau jaringan. Bayangkan menambahkan izin
ke dalam campuran, yang mengharuskan browser memblokir halaman saat meminta
akses papan klip. Pada saat yang sama, izin yang diterapkan di sekitar
document.execCommand()
untuk interaksi papan klip ditentukan secara longgar dan bervariasi
di antara browser.
Tujuan API Papan Klip Asinkron mengatasi masalah ini, menyediakan model izin yang didefinisikan dengan baik yang tidak memblokir halaman. API Papan Klip Asinkron dibatasi untuk menangani teks dan gambar pada sebagian besar {i>browser<i}, tetapi dukungannya bervariasi. Pastikan untuk mempelajari ringkasan kompatibilitas browser dengan cermat untuk setiap bagian berikut.
{i>Copy<i}: menulis data ke {i>clipboard<i}
writeText()
Untuk menyalin teks ke papan klip, panggil writeText()
. Karena API ini
asinkron, fungsi writeText()
menampilkan Promise yang menyelesaikan atau
akan ditolak bergantung pada apakah teks yang diteruskan berhasil disalin:
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()
Sebenarnya, writeText()
hanyalah metode praktis untuk write()
generik
, yang juga memungkinkan Anda menyalin gambar ke papan klip. Seperti writeText()
, fungsi ini
adalah asinkron dan menampilkan Promise.
Untuk menulis gambar ke papan klip, Anda memerlukan gambar sebagai
blob
. Salah satu cara untuk melakukan
yaitu dengan meminta gambar dari server menggunakan fetch()
, lalu memanggil
blob()
di
yang dihasilkan.
Meminta gambar dari server mungkin
tidak diinginkan atau tidak mungkin dilakukan
berbagai alasan. Untungnya, Anda juga dapat menggambar gambar ke kanvas dan
memanggil metode
toBlob()
kanvas.
Selanjutnya, teruskan array objek ClipboardItem
sebagai parameter ke write()
. Saat ini, Anda hanya dapat meneruskan satu gambar dalam satu waktu, tetapi kami berharap dapat menambahkan
dukungan untuk beberapa gambar pada masa mendatang. ClipboardItem
mengambil objek dengan
jenis MIME gambar sebagai kunci, dan blob sebagai nilainya. Untuk objek blob yang diperoleh dari fetch()
atau canvas.toBlob()
, properti blob.type
secara otomatis berisi jenis MIME yang benar untuk gambar.
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);
}
Atau, Anda dapat menulis promise ke objek ClipboardItem
.
Untuk pola ini, Anda perlu mengetahui jenis data MIME terlebih dahulu.
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);
}
Peristiwa penyalinan
Jika pengguna memulai penyalinan papan klip
dan tidak memanggil preventDefault()
,
Peristiwa copy
menyertakan properti clipboardData
dengan item sudah dalam format yang benar.
Jika ingin mengimplementasikan logika Anda sendiri, Anda perlu memanggil preventDefault()
untuk
mencegah perilaku default yang mendukung implementasi Anda sendiri.
Dalam hal ini, clipboardData
akan kosong.
Pertimbangkan halaman dengan teks dan gambar, dan saat pengguna memilih semua dan
memulai salinan papan klip, solusi kustom Anda harus menghapus teks dan hanya
menyalin gambar. Anda dapat melakukannya seperti yang ditunjukkan dalam contoh kode di bawah.
Yang tidak tercakup dalam contoh ini adalah cara kembali ke API
sebelumnya jika Clipboard API tidak didukung.
<!-- 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);
}
});
Untuk peristiwa copy
:
Untuk ClipboardItem
:
Tempel: membaca data dari papan klip
readText()
Untuk membaca teks dari papan klip, panggil navigator.clipboard.readText()
dan tunggu
promise yang ditampilkan untuk diselesaikan:
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()
Metode navigator.clipboard.read()
juga asinkron dan menampilkan
yang menjanjikan. Untuk membaca gambar dari {i>clipboard<i}, dapatkan daftar
ClipboardItem
objek tersebut, lalu mengulanginya.
Setiap ClipboardItem
dapat menyimpan kontennya dalam jenis yang berbeda, jadi Anda harus
melakukan iterasi pada daftar jenis, sekali lagi menggunakan loop for...of
. Untuk setiap jenis,
panggil metode getType()
dengan jenis saat ini sebagai argumen untuk mendapatkan
blob yang sesuai. Seperti sebelumnya, kode ini tidak terikat dengan gambar, dan akan
berfungsi dengan jenis file lainnya di masa mendatang.
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);
}
}
Menggunakan file yang ditempelkan
Hal ini berguna bagi pengguna untuk dapat menggunakan pintasan keyboard papan klip seperti ctrl+c dan ctrl+v. Chromium menampilkan file hanya baca di papan klip seperti yang dijelaskan di bawah. Tindakan ini dipicu saat pengguna menekan pintasan tempel default sistem operasi atau saat pengguna mengklik Edit, lalu Tempel di menu bar browser. Tidak diperlukan kode saluran air lebih lanjut.
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());
});
Peristiwa tempel
Seperti yang telah disebutkan sebelumnya, ada rencana untuk memperkenalkan peristiwa agar berfungsi dengan Clipboard API,
tetapi untuk saat ini Anda dapat menggunakan peristiwa paste
yang ada. Metode ini berfungsi dengan baik dengan metode asinkron
baru untuk membaca teks papan klip. Seperti halnya peristiwa copy
, jangan
lupa memanggil preventDefault()
.
document.addEventListener('paste', async (e) => {
e.preventDefault();
const text = await navigator.clipboard.readText();
console.log('Pasted text: ', text);
});
Menangani beberapa jenis MIME
Sebagian besar implementasi menempatkan beberapa format data di papan klip untuk satu operasi potong atau salin. Ada dua alasan untuk hal ini: sebagai developer aplikasi, Anda tidak memiliki cara untuk mengetahui kemampuan aplikasi yang ingin digunakan pengguna untuk menyalin teks atau gambar, dan banyak aplikasi mendukung menempelkan data terstruktur sebagai teks biasa. Opsi ini biasanya ditampilkan kepada pengguna dengan item menu Edit dengan nama seperti Tempel dan cocokkan gaya atau Tempel tanpa pemformatan.
Contoh berikut menunjukkan cara melakukannya. Contoh ini menggunakan fetch()
untuk mendapatkan
data gambar, tetapi juga dapat berasal dari
<canvas>
atau 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]);
}
Keamanan dan izin
Akses papan klip selalu menimbulkan masalah keamanan bagi browser. Tanpa
izin yang tepat, halaman dapat menyalin semua jenis konten berbahaya
ke papan klip pengguna secara diam-diam yang akan menghasilkan hasil yang mengerikan saat ditempel.
Bayangkan sebuah halaman web yang diam-diam menyalin rm -rf /
atau
gambar bom dekompresi
ke papan klip.
Memberikan akses baca yang tak terbatas ke {i>clipboard<i} pada laman web bahkan lebih merepotkan. Pengguna secara rutin menyalin informasi sensitif seperti {i>password<i} dan detail pribadi ke {i>clipboard<i}, yang kemudian dapat dibaca oleh laman mana pun tanpa pengetahuan pengguna.
Seperti banyak API baru, Clipboard API hanya didukung untuk halaman yang ditayangkan melalui HTTPS. Untuk membantu mencegah penyalahgunaan, akses papan klip hanya diizinkan jika halaman adalah tab aktif. Halaman dalam tab aktif dapat menulis ke papan klip tanpa meminta izin, tetapi membaca dari papan klip selalu memerlukan izin akses.
Izin untuk menyalin dan menempel telah ditambahkan ke
Permissions API.
Izin clipboard-write
diberikan secara otomatis ke halaman saat halaman tersebut
merupakan tab aktif. Izin clipboard-read
harus diminta, yang dapat Anda lakukan dengan mencoba membaca data dari papan klip. Kode di bawah menampilkan yang terakhir:
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);
};
Anda juga dapat mengontrol apakah {i>gesture <i}pengguna
diperlukan untuk meminta pemotongan atau
menempel menggunakan opsi allowWithoutGesture
. Nilai default untuk nilai ini
bervariasi menurut browser, jadi Anda harus selalu menyertakannya.
Di sinilah sifat asinkron Clipboard API sangat berguna: mencoba membaca atau menulis data papan klip akan otomatis meminta izin kepada pengguna jika belum diberikan. Karena API ini berbasis promise, ini sepenuhnya transparan, dan pengguna yang menolak izin {i>clipboard<i} menyebabkan janji untuk ditolak sehingga laman dapat merespons dengan tepat.
Karena browser hanya mengizinkan akses {i>clipboard<i} ketika laman adalah tab yang aktif,
Anda akan menemukan bahwa beberapa contoh di sini
tidak berfungsi jika ditempelkan secara langsung
konsol browser, karena alat pengembang itu sendiri adalah tab yang aktif. Ada trik: tunda
akses papan klip menggunakan setTimeout()
, lalu klik dengan cepat di dalam halaman untuk
memfokuskannya sebelum fungsi dipanggil:
setTimeout(async () => {
const text = await navigator.clipboard.readText();
console.log(text);
}, 2000);
Integrasi kebijakan izin
Untuk menggunakan API dalam iframe, Anda harus mengaktifkannya dengan
Kebijakan Izin,
yang menentukan mekanisme yang memungkinkan pengaktifan dan
penonaktifan berbagai fitur dan API browser secara selektif. Konkretnya, Anda
harus melewati
atau keduanya dari clipboard-read
atau clipboard-write
, bergantung pada kebutuhan aplikasi Anda.
<iframe
src="index.html"
allow="clipboard-read; clipboard-write"
>
</iframe>
Deteksi fitur
Untuk menggunakan Asynchronous Clipboard API sekaligus mendukung semua browser, uji
navigator.clipboard
dan kembali ke metode sebelumnya. Misalnya, berikut cara
Anda dapat menerapkan penempelan untuk menyertakan browser lain.
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);
});
Itu belum semuanya. Sebelum Asynchronous Clipboard API, terdapat campuran
implementasi {i>copy and paste<i} yang berbeda-beda
di seluruh {i>browser<i} web. Di sebagian besar browser,
salin dan tempel browser sendiri dapat dipicu menggunakan
document.execCommand('copy')
dan document.execCommand('paste')
. Jika teks
yang akan disalin adalah string yang tidak ada di DOM, maka harus dimasukkan ke dalam
DOM dan dipilih:
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();
});
Demo
Anda dapat mencoba Async Clipboard API dalam demo di bawah. Di Glitch Anda dapat membuat remix demo teks atau demo gambar untuk bereksperimen dengan mereka.
Contoh pertama menunjukkan pemindahan teks ke dan dari papan klip.
Untuk mencoba API dengan gambar, gunakan demo ini. Ingat bahwa hanya PNG yang didukung dan hanya di beberapa browser.
Link terkait
Ucapan terima kasih
Asynchronous Clipboard API diimplementasikan oleh Darwin Huang dan Gary Kačmarčík. Darwin juga memberikan demo. Terima kasih kepada Kyarik dan sekali lagi Gary Kačmarčík karena telah meninjau bagian artikel ini.
Banner besar oleh Markus Winkler di Buka pembuka.