Berhenti memblokir akses papan klip

Akses papan klip yang lebih aman dan tidak diblokir 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 memotong dan menempel ini menimbulkan 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 memblokir halaman untuk transfer papan klip memberikan pengalaman yang buruk. Pembersihan atau decoding gambar yang memakan waktu mungkin diperlukan sebelum konten dapat ditempel dengan aman. Browser mungkin perlu memuat atau menyisipkan resource tertaut dari dokumen yang ditempel. Hal itu akan memblokir halaman saat menunggu disk 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 tidak didefinisikan dengan jelas dan bervariasi di antara browser.

Async Clipboard API mengatasi masalah ini, dengan menyediakan model izin yang terdefinisi dengan baik yang tidak memblokir halaman. Async Clipboard API terbatas untuk menangani teks dan gambar di sebagian besar browser, tetapi dukungan bervariasi. Pastikan untuk mempelajari ringkasan kompatibilitas browser dengan cermat untuk setiap bagian berikut.

Menyalin: menulis data ke papan klip

writeText()

Untuk menyalin teks ke papan klip, panggil writeText(). Karena API ini bersifat asinkron, fungsi writeText() menampilkan Promise yang di-resolve atau 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);
  }
}

Browser Support

  • Chrome: 66.
  • Edge: 79.
  • Firefox: 63.
  • Safari: 13.1.

Source

write()

Sebenarnya, writeText() hanyalah metode praktis untuk metode write() generik, yang juga memungkinkan Anda menyalin gambar ke papan klip. Seperti writeText(), fungsi ini bersifat asinkron dan menampilkan Promise.

Untuk menulis gambar ke papan klip, Anda memerlukan gambar sebagai blob. Salah satu cara untuk melakukannya adalah dengan meminta gambar dari server menggunakan fetch(), lalu memanggil blob() pada respons.

Meminta gambar dari server mungkin tidak diinginkan atau tidak memungkinkan karena berbagai alasan. Untungnya, Anda juga dapat menggambar gambar ke kanvas dan memanggil metode toBlob() kanvas.

Selanjutnya, teruskan array objek ClipboardItem sebagai parameter ke metode 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 nilai. Untuk objek blob yang diperoleh dari fetch() atau canvas.toBlob(), properti blob.type 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 janji ke objek ClipboardItem. Untuk pola ini, Anda perlu mengetahui jenis MIME data sebelumnya.

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);
}

Browser Support

  • Chrome: 76.
  • Edge: 79.
  • Firefox: 127.
  • Safari: 13.1.

Source

Acara salin

Jika pengguna memulai penyalinan ke papan klip dan tidak memanggil preventDefault(), peristiwa copy mencakup properti clipboardData dengan item yang sudah dalam format yang benar. Jika ingin menerapkan logika Anda sendiri, Anda harus memanggil preventDefault() untuk mencegah perilaku default demi implementasi Anda sendiri. Dalam hal ini, clipboardData akan kosong. Pertimbangkan halaman dengan teks dan gambar, dan saat pengguna memilih semua dan memulai penyalinan ke 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 dibahas 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 acara copy:

Browser Support

  • Chrome: 1.
  • Edge: 12.
  • Firefox: 22.
  • Safari: 3.

Source

Untuk ClipboardItem:

Browser Support

  • Chrome: 76.
  • Edge: 79.
  • Firefox: 127.
  • Safari: 13.1.

Source

Tempel: membaca data dari papan klip

readText()

Untuk membaca teks dari papan klip, panggil navigator.clipboard.readText() dan tunggu hingga promise yang ditampilkan selesai:

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);
  }
}

Browser Support

  • Chrome: 66.
  • Edge: 79.
  • Firefox: 125.
  • Safari: 13.1.

Source

read()

Metode navigator.clipboard.read() juga bersifat asinkron dan menampilkan promise. Untuk membaca gambar dari papan klip, dapatkan daftar objek ClipboardItem, lalu lakukan iterasi pada objek tersebut.

Setiap ClipboardItem dapat menyimpan kontennya dalam berbagai jenis, jadi Anda harus melakukan iterasi pada daftar jenis, lagi-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 pada 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);
  }
}

Browser Support

  • Chrome: 76.
  • Edge: 79.
  • Firefox: 127.
  • Safari: 13.1.

Source

Bekerja dengan file yang ditempel

Pengguna dapat menggunakan pintasan keyboard papan klip seperti ctrl+c dan ctrl+v. Chromium mengekspos file hanya baca di papan klip seperti yang diuraikan di bawah. Peristiwa ini dipicu saat pengguna menekan pintasan tempel default sistem operasi atau saat pengguna mengklik Edit, lalu Tempel di menu browser. Tidak diperlukan kode plumbing 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());
});

Browser Support

  • Chrome: 3.
  • Edge: 12.
  • Firefox: 3.6.
  • Safari: 4.

Source

Peristiwa tempel

Seperti yang disebutkan sebelumnya, ada rencana untuk memperkenalkan peristiwa yang akan digunakan dengan Clipboard API, tetapi untuk saat ini Anda dapat menggunakan peristiwa paste yang ada. API ini berfungsi dengan baik dengan metode asinkron baru untuk membaca teks papan klip. Seperti peristiwa copy, jangan lupa memanggil preventDefault().

document.addEventListener('paste', async (e) => {
  e.preventDefault();
  const text = await navigator.clipboard.readText();
  console.log('Pasted text: ', text);
});

Browser Support

  • Chrome: 1.
  • Edge: 12.
  • Firefox: 22.
  • Safari: 3.

Source

Menangani beberapa jenis MIME

Sebagian besar implementasi menempatkan beberapa format data di papan klip untuk satu operasi pemotongan atau penyalinan. Ada dua alasan untuk hal ini: sebagai developer aplikasi, Anda tidak dapat mengetahui kemampuan aplikasi yang ingin digunakan pengguna untuk menyalin teks atau gambar, dan banyak aplikasi mendukung penempelan data terstruktur sebagai teks biasa. Biasanya, opsi ini ditampilkan kepada pengguna dengan item menu Edit yang memiliki nama seperti Tempel dan cocokkan gaya atau Tempel tanpa format.

Contoh berikut menunjukkan cara melakukannya. Contoh ini menggunakan fetch() untuk mendapatkan data gambar, tetapi data tersebut 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 secara diam-diam ke papan klip pengguna yang akan menghasilkan hasil yang sangat buruk saat ditempelkan. Bayangkan halaman web yang secara diam-diam menyalin rm -rf / atau a gambar bom dekompresi ke papan klip Anda.

Perintah browser yang meminta izin papan klip kepada pengguna.
Dialog izin untuk Clipboard API.

Memberikan akses baca tanpa batas ke papan klip untuk halaman web bahkan lebih bermasalah. Pengguna secara rutin menyalin informasi sensitif seperti sandi dan detail pribadi ke papan klip, yang kemudian dapat dibaca oleh halaman mana pun tanpa sepengetahuan pengguna.

Seperti banyak API baru, Clipboard API hanya didukung untuk halaman yang disalurkan melalui HTTPS. Untuk membantu mencegah penyalahgunaan, akses papan klip hanya diizinkan saat halaman menjadi tab aktif. Halaman di tab aktif dapat menulis ke papan klip tanpa meminta izin, tetapi membaca dari papan klip selalu memerlukan izin.

Izin untuk menyalin dan menempel telah ditambahkan ke Permissions API. Izin clipboard-write diberikan secara otomatis ke halaman saat halaman tersebut menjadi tab aktif. Izin clipboard-read harus diminta, yang dapat Anda lakukan dengan mencoba membaca data dari papan klip. Kode di bawah menunjukkan 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 gestur pengguna diperlukan untuk memanggil pemotongan atau penempelan menggunakan opsi allowWithoutGesture. Nilai default untuk nilai ini berbeda-beda menurut browser, jadi Anda harus selalu menyertakannya.

Di sinilah sifat asinkron Clipboard API benar-benar berguna: mencoba membaca atau menulis data papan klip secara otomatis akan meminta izin pengguna jika belum diberikan. Karena API berbasis promise, hal ini sepenuhnya transparan, dan pengguna yang menolak izin papan klip akan menyebabkan promise ditolak sehingga halaman dapat merespons dengan tepat.

Karena browser hanya mengizinkan akses papan klip saat halaman menjadi tab aktif, Anda akan mendapati bahwa beberapa contoh di sini tidak berjalan jika ditempelkan langsung ke konsol browser, karena alat developer itu sendiri adalah tab 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 di iframe, Anda harus mengaktifkannya dengan Permissions Policy, yang menentukan mekanisme yang memungkinkan pengaktifan dan penonaktifan berbagai fitur dan API browser secara selektif. Secara konkret, Anda perlu meneruskan salah satu atau kedua clipboard-read atau clipboard-write, bergantung pada kebutuhan aplikasi Anda.

<iframe
    src="index.html"
    allow="clipboard-read; clipboard-write"
>
</iframe>

Deteksi fitur

Untuk menggunakan Async Clipboard API sambil mendukung semua browser, uji navigator.clipboard dan kembali ke metode sebelumnya. Misalnya, berikut cara Anda dapat menerapkan operasi tempel 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);
});

Namun, itu bukan keseluruhan ceritanya. Sebelum Async Clipboard API, ada berbagai implementasi salin dan tempel di seluruh browser web. Di sebagian besar browser, salin dan tempel browser dapat dipicu menggunakan document.execCommand('copy') dan document.execCommand('paste'). Jika teks yang akan disalin adalah string yang tidak ada di DOM, teks tersebut harus disisipkan 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. Contoh pertama menunjukkan cara memindahkan teks ke dan dari papan klip.

Untuk mencoba API dengan gambar, gunakan demo ini. Ingatlah bahwa hanya PNG yang didukung dan hanya di beberapa browser.

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 sebagian artikel ini.

Gambar banner besar oleh Markus Winkler di Unsplash.