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

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

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

การสนับสนุนเบราว์เซอร์

  • 66
  • 79
  • 63
  • 13.1

แหล่งที่มา

เขียน()

จริงๆ แล้ว 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);
}

หรือคุณจะเขียนคำสัญญาลงในออบเจ็กต์ 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);
}

การสนับสนุนเบราว์เซอร์

  • 66
  • 79
  • 13.1

แหล่งที่มา

เหตุการณ์การคัดลอก

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

การสนับสนุนเบราว์เซอร์

  • 1
  • 12
  • 22
  • 3

แหล่งที่มา

สำหรับ ClipboardItem:

การสนับสนุนเบราว์เซอร์

  • 76
  • 79
  • 13.1

แหล่งที่มา

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

readText()

หากต้องการอ่านข้อความจากคลิปบอร์ด ให้โทรหา navigator.clipboard.readText() และรอให้คำสัญญาที่ส่งคืนกลับมาแก้ไขปัญหาดังนี้

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

การสนับสนุนเบราว์เซอร์

  • 66
  • 79
  • 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);
  }
}

การสนับสนุนเบราว์เซอร์

  • 66
  • 79
  • 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());
});

การสนับสนุนเบราว์เซอร์

  • 3
  • 12
  • 3.6
  • 4

แหล่งที่มา

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

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

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

การสนับสนุนเบราว์เซอร์

  • 1
  • 12
  • 22
  • 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

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

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 ค่าเริ่มต้นของค่านี้จะแตกต่างกันไปตามเบราว์เซอร์ คุณจึงควรใส่ค่าเริ่มต้นเสมอ

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

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

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

ข้อความแสดงการยอมรับ

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

รูปภาพหลักของ markus Winkler ใน Unsplash