เข้าถึงคลิปบอร์ดสำหรับข้อความและรูปภาพได้อย่างปลอดภัยและไม่ถูกบล็อก
วิธีดั้งเดิมในการเข้าถึงคลิปบอร์ดของระบบคือผ่าน
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);
}
}
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);
}
เหตุการณ์การคัดลอก
ในกรณีที่ผู้ใช้เริ่มการคัดลอกไปยังคลิปบอร์ด
และไม่ได้เรียกใช้ 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
ให้ทำดังนี้
สำหรับ ClipboardItem
วาง: อ่านข้อมูลจากคลิปบอร์ด
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);
}
}
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);
}
}
การทำงานกับไฟล์ที่วาง
ผู้ใช้จะใช้แป้นพิมพ์ลัดของคลิปบอร์ดได้ เช่น 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());
});
เหตุการณ์วาง
ดังที่ได้กล่าวไว้ก่อนหน้านี้ เรามีแผนที่จะเปิดตัวเหตุการณ์เพื่อทำงานร่วมกับ Clipboard API
แต่ในตอนนี้คุณสามารถใช้เหตุการณ์ paste
ที่มีอยู่ได้ ซึ่งทำงานได้ดีกับ
เมธอดแบบอะซิงโครนัสใหม่สำหรับการอ่านข้อความในคลิปบอร์ด เช่นเดียวกับcopy
event อย่าลืมเรียกใช้ preventDefault()
document.addEventListener('paste', async (e) => {
e.preventDefault();
const text = await navigator.clipboard.readText();
console.log('Pasted text: ', text);
});
การจัดการประเภท 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 /
หรือรูปภาพระเบิดการคลายการบีบอัด
ไปยังคลิปบอร์ดของคุณอย่างเงียบๆ

การให้สิทธิ์เข้าถึงแบบอ่านคลิปบอร์ดแก่หน้าเว็บโดยไม่มีข้อจำกัดนั้นยิ่ง สร้างปัญหามากขึ้น ผู้ใช้มักจะคัดลอกข้อมูลที่ละเอียดอ่อน เช่น รหัสผ่านและ รายละเอียดส่วนตัว ไปยังคลิปบอร์ด ซึ่งหน้าเว็บใดก็ได้สามารถอ่านได้โดยที่ ผู้ใช้ไม่ทราบ
เช่นเดียวกับ 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 เป็นแบบ Promise จึงมีความโปร่งใสอย่างสมบูรณ์ และผู้ใช้ที่ปฏิเสธสิทธิ์เข้าถึงคลิปบอร์ดจะทำให้ Promise ถูกปฏิเสธเพื่อให้หน้าเว็บตอบสนองได้อย่างเหมาะสม
เนื่องจากเบราว์เซอร์อนุญาตให้เข้าถึงคลิปบอร์ดได้เฉพาะเมื่อหน้าเว็บเป็นแท็บที่ใช้งานอยู่
คุณจะเห็นว่าตัวอย่างบางส่วนที่นี่ไม่ทํางานหากวางลงในคอนโซลของเบราว์เซอร์โดยตรง
เนื่องจากเครื่องมือสําหรับนักพัฒนาซอฟต์แวร์เองเป็นแท็บที่ใช้งานอยู่ เคล็ดลับคือเลื่อนการเข้าถึงคลิปบอร์ดโดยใช้ 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 ในการสาธิตด้านล่าง ตัวอย่างแรกแสดงการย้ายข้อความเข้าและออกจากคลิปบอร์ด
หากต้องการลองใช้ API กับรูปภาพ ให้ใช้การสาธิตนี้ โปรดทราบว่าระบบรองรับเฉพาะ PNG และเฉพาะในเบราว์เซอร์บางตัวเท่านั้น
ลิงก์ที่เกี่ยวข้อง
คำขอบคุณ
Darwin Huang และ Gary Kačmarčík เป็นผู้ใช้ Asynchronous Clipboard API ดาร์วินยังเป็นผู้สาธิตด้วย ขอขอบคุณ Kyarik และ Gary Kačmarčík อีกครั้งที่ช่วยตรวจสอบ ส่วนต่างๆ ของบทความนี้
รูปภาพหลักโดย Markus Winkler ใน Unsplash