กำลังเลิกบล็อกการเข้าถึงคลิปบอร์ด

เข้าถึงคลิปบอร์ดที่ปลอดภัยยิ่งขึ้นสำหรับข้อความและรูปภาพ

วิธีการเข้าถึงคลิปบอร์ดของระบบแบบดั้งเดิมคือ 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);
  }
}

การรองรับเบราว์เซอร์

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

แหล่งที่มา

เขียน()

อันที่จริงแล้ว writeText() เป็นเพียงเมธอดที่สะดวกสำหรับเมธอด write() แบบทั่วไป ซึ่งให้คุณคัดลอกรูปภาพไปยังคลิปบอร์ดได้ด้วย ชอบ writeText() เป็นแบบไม่พร้อมกันและแสดงผล Promise

ในการเขียนภาพไปยังคลิปบอร์ด คุณต้องมีภาพนั้นเป็น blob วิธีหนึ่งในการทำเช่นนี้คือการขอรูปภาพจากเซิร์ฟเวอร์โดยใช้ 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);
}

การรองรับเบราว์เซอร์

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

แหล่งที่มา

กิจกรรมการคัดลอก

ในกรณีที่ผู้ใช้เริ่มคัดลอกคลิปบอร์ดแต่ไม่ได้เรียกใช้ preventDefault() เหตุการณ์ copy จะมีพร็อพเพอร์ตี้ clipboardData ที่มีรายการอยู่ในรูปแบบที่ถูกต้องอยู่แล้ว หากต้องการใช้ตรรกะของคุณเอง คุณต้องโทรหา preventDefault() เพื่อ ป้องกันลักษณะการทำงานเริ่มต้นให้เป็นประโยชน์กับคุณ ในกรณีนี้ clipboardData จะว่างเปล่า ลองพิจารณาหน้าเว็บที่มีข้อความและรูปภาพ และเมื่อผู้ใช้เลือกทั้งหมดและ เริ่มคัดลอกคลิปบอร์ด โซลูชันที่กำหนดเองของคุณควรทิ้งข้อความ คัดลอกรูปภาพ ซึ่งทำได้ดังที่แสดงในตัวอย่างโค้ดด้านล่าง สิ่งที่ไม่ครอบคลุมในตัวอย่างนี้คือการกลับไปใช้เวอร์ชันเก่า API เมื่อระบบไม่รองรับ 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 กิจกรรม

การรองรับเบราว์เซอร์

  • Chrome: 1.
  • ขอบ: 12.
  • Firefox: 22.
  • Safari: 3.

แหล่งที่มา

สำหรับ ClipboardItem

การรองรับเบราว์เซอร์

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

แหล่งที่มา

วาง: อ่านข้อมูลจากคลิปบอร์ด

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

การรองรับเบราว์เซอร์

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

แหล่งที่มา

อ่าน()

เมธอด navigator.clipboard.read() เป็นแบบไม่พร้อมกันและแสดงผล สัญญา หากต้องการอ่านรูปภาพจากคลิปบอร์ด ให้รับรายการ 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);
  }
}

การรองรับเบราว์เซอร์

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

แหล่งที่มา

การทำงานกับไฟล์ที่วาง

ฟีเจอร์นี้มีประโยชน์สำหรับผู้ใช้ในการใช้แป้นพิมพ์ลัดต่างๆ ในคลิปบอร์ด เช่น 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());
});

การรองรับเบราว์เซอร์

  • Chrome: 3.
  • ขอบ: 12.
  • Firefox: 3.6
  • Safari: 4.

แหล่งที่มา

กิจกรรมการวาง

ตามที่ได้แจ้งไว้ก่อนหน้านี้ เรามีแผนที่จะเปิดตัวเหตุการณ์ต่างๆ เพื่อใช้งานร่วมกับ Clipboard API แต่ตอนนี้คุณสามารถใช้เหตุการณ์ paste ที่มีอยู่แล้วได้ ซึ่งทำงานร่วมกับเมธอดแบบไม่ประสานเวลาใหม่สําหรับการอ่านข้อความในคลิปบอร์ดได้เป็นอย่างดี อย่าลืมโทรหา preventDefault() เช่นเดียวกับกิจกรรม copy

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

การรองรับเบราว์เซอร์

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

แหล่งที่มา

การจัดการ MIME หลายประเภท

การใช้งานส่วนใหญ่จะวางรูปแบบข้อมูลหลายรูปแบบไว้ในคลิปบอร์ด เพื่อประสบการณ์การใช้งานเพียงครั้งเดียว หรือคัดลอกก็ได้ สาเหตุมี 2 ข้อ ในฐานะนักพัฒนาแอป คุณไม่มีทางทราบความสามารถของแอปที่ผู้ใช้ต้องการคัดลอกข้อความหรือรูปภาพไปวาง และแอปพลิเคชันจำนวนมากรองรับการวาง Structured Data เป็นข้อความธรรมดา โดยปกติแล้ว ตัวเลือกนี้จะแสดงต่อผู้ใช้เป็นรายการเมนูแก้ไขที่มีชื่อ เช่น วางและจับคู่สไตล์ หรือวางโดยไม่จัดรูปแบบ

ตัวอย่างต่อไปนี้แสดงวิธีดำเนินการ ตัวอย่างนี้ใช้ 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 / หรือ รูปภาพระเบิดเพื่อบีบอัด ลงในคลิปบอร์ด

ข้อความแจ้งของเบราว์เซอร์ที่ขอสิทธิ์เข้าถึงคลิปบอร์ดจากผู้ใช้
ข้อความแจ้งสิทธิ์สำหรับ Clipboard API

การให้สิทธิ์เข้าถึงคลิปบอร์ดแบบอ่านอย่างเดียวแก่หน้าเว็บนั้นยิ่งทำให้เกิดปัญหามากขึ้น ผู้ใช้มักจะคัดลอกข้อมูลที่ละเอียดอ่อน เช่น รหัสผ่านและรายละเอียดส่วนบุคคล ไปยังคลิปบอร์ด ซึ่งหน้าเว็บใดๆ ก็สามารถอ่านข้อมูลดังกล่าวได้โดยที่ผู้ใช้ไม่รู้

เช่นเดียวกับ 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 จะเป็นไปตามสัญญา นี่เป็นความโปร่งใสทั้งหมด และสาเหตุที่ผู้ใช้ปฏิเสธสิทธิ์เข้าถึงคลิปบอร์ด คำสัญญาว่าจะปฏิเสธ เพื่อให้หน้าเว็บตอบสนองได้อย่างเหมาะสม

เนื่องจากเบราว์เซอร์อนุญาตให้เข้าถึงคลิปบอร์ดเฉพาะเมื่อหน้าเว็บเป็นแท็บที่ใช้งานอยู่ คุณจึงอาจพบว่าตัวอย่างบางส่วนที่นี่ไม่ทํางานหากวางลงในคอนโซลของเบราว์เซอร์โดยตรง เนื่องจากเครื่องมือสําหรับนักพัฒนาซอฟต์แวร์เป็นแท็บที่ใช้งานอยู่ แต่มีวิธีแก้ไข: เลื่อนเวลาการเข้าถึงคลิปบอร์ดโดยใช้ 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 คุณ รีมิกซ์ การสาธิตข้อความ ได้ หรือการสาธิตรูปภาพเพื่อ มาลองใช้กัน

ตัวอย่างที่ 1 แสดงการย้ายข้อความไปและออกจากคลิปบอร์ด

หากต้องการลองใช้ API กับรูปภาพ ให้ใช้การสาธิตนี้ โปรดทราบว่าระบบรองรับไฟล์ PNG เท่านั้น และเฉพาะใน หลายเบราว์เซอร์

ขอขอบคุณ

Async Clipboard API ติดตั้งใช้งานโดย Darwin Huang และ Gary Kačmarčík Darwin นำเสนอการสาธิตด้วย ขอขอบคุณ Kyarik และ Gary Kačmarčík อีกครั้งที่อ่านบทความนี้

รูปภาพหลักโดย Markus Winkler ใน หน้าจอแนะนํา