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

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

วิธีที่ดั้งเดิมในการเข้าถึงคลิปบอร์ดของระบบคือผ่าน 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

แหล่งที่มา

write()

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

หากต้องการเขียนรูปภาพลงในคลิปบอร์ด คุณต้องมีรูปภาพเป็น blob วิธีหนึ่งในการทำเช่นนี้คือการขอรูปภาพจากเซิร์ฟเวอร์โดยใช้ fetch() จากนั้นเรียกใช้ blob() ในการตอบกลับ

การขอรูปภาพจากเซิร์ฟเวอร์อาจไม่เหมาะสมหรือทําไม่ได้ด้วยเหตุผลหลายประการ แต่คุณวาดรูปภาพลงใน Canvas และเรียกใช้เมธอด toBlob() ของ Canvas ได้ด้วย

ถัดไป ให้ส่งอาร์เรย์ของออบเจ็กต์ 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);
}

หรือจะเขียน Promise ไปยังออบเจ็กต์ 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.
  • Edge: 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

แหล่งที่มา

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

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

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

แหล่งที่มา

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

การที่ผู้ใช้สามารถใช้แป้นพิมพ์ลัดของคลิปบอร์ด เช่น ctrl+c และ ctrl+v นั้นมีประโยชน์อย่างมาก Chrome จะแสดงไฟล์อ่านอย่างเดียวในคลิปบอร์ดตามที่ระบุไว้ด้านล่าง ซึ่งจะทริกเกอร์เมื่อผู้ใช้กดแป้นพิมพ์ลัดที่เป็นค่าเริ่มต้นของระบบปฏิบัติการ หรือเมื่อผู้ใช้คลิกแก้ไข จากนั้นคลิกวางในแถบเมนูของเบราว์เซอร์ ไม่ต้องใช้รหัสการเดินท่อเพิ่มเติม

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.
  • Edge: 12.
  • Firefox: 3.6
  • Safari: 4.

แหล่งที่มา

เหตุการณ์วาง

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

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

ตัวอย่างต่อไปนี้แสดงวิธีใช้งาน ตัวอย่างนี้ใช้ 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

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

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

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

<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 ในบางเบราว์เซอร์เท่านั้น

กิตติกรรมประกาศ

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

รูปภาพหลักโดย Markus Winkler ใน Unsplash