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

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

วิธีที่ดั้งเดิมในการเข้าถึงคลิปบอร์ดของระบบคือผ่าน 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() แบบทั่วไป ซึ่งให้คุณคัดลอกรูปภาพไปยังคลิปบอร์ดได้ด้วย เช่นเดียวกับ writeText() ฟังก์ชันนี้ทำงานแบบไม่เรียลไทม์และแสดงผล Promise

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