ביטול חסימה של גישה ללוח

גישה בטוחה יותר לטקסט ולתמונות בלוח העריכה, ללא חסימות

הדרך המסורתית לקבל גישה ללוח העריכה של המערכת הייתה באמצעות 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() היא רק שיטה נוחה ל-method הגנרי write(), שמאפשרת גם להעתיק תמונות ללוח. כמו writeText(), היא אסינכרונית ומחזירה Promise.

כדי לכתוב תמונה בלוח העריכה צריך אותה בתור blob. אחת הדרכים לעשות זאת היא לבקש את התמונה משרת באמצעות fetch(), ואז להפעיל את blob() בתגובה.

יכול להיות שלא תרצו או לא תוכלו לבקש תמונה מהשרת מסיבות שונות. למרבה המזל, אפשר גם לצייר את התמונה על קנבס ולהפעיל את השיטה toBlob().

בשלב הבא, מעבירים מערך של אובייקטים מסוג ClipboardItem כפרמטר לשיטה write(). בשלב הזה אפשר להעביר רק תמונה אחת בכל פעם, אבל אנחנו מקווים להוסיף תמיכה בכמה תמונות בעתיד. הפונקציה 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() וממתינים שההבטחה שתוחזר תתבצע:

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()‎

גם ה-method של navigator.clipboard.read() היא אסינכרונית ומחזירה הבטחה. כדי לקרוא תמונה מלוח העריכה, משיגים רשימה של האובייקטים ClipboardItem ולאחר מכן חוזרים עליהם.

כל ClipboardItem יכול להכיל את התוכן שלו בסוגים שונים, כך שתצטרכו לחזור על רשימת הסוגים, שוב באמצעות לולאת for...of. לכל סוג, צריך לקרוא ל-method 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.
  • 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

ברוב הטמעות ה-Clipboard, הנתונים מועברים ללוח בכמה פורמטים בפעולה אחת של חיתוך או העתקה. יש לכך שתי סיבות: כמפתחי אפליקציות, אין לכם אפשרות לדעת מהן היכולות של האפליקציה שאליה המשתמש רוצה להעתיק טקסט או תמונות, ויש אפליקציות רבות שתומכות בהדבקת נתונים מובְנים כטקסט פשוט. בדרך כלל האפשרות הזו מוצגת למשתמשים בתפריט עריכה עם שם כמו הדבקה והתאמה לסגנון או הדבקה ללא עיצוב.

הדוגמה הבאה ממחישה איך עושים את זה. בדוגמה הזו נעשה שימוש ב-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 ב-iframes, צריך להפעיל אותו באמצעות מדיניות ההרשאות, שמגדירה מנגנון שמאפשר להפעיל ולכבות באופן סלקטיבי תכונות שונות של הדפדפן וממשקי API שונים. באופן ספציפי, צריך להעביר את הערך של clipboard-read או clipboard-write, או את שניהם, בהתאם לצרכים של האפליקציה.

<iframe
    src="index.html"
    allow="clipboard-read; clipboard-write"
>
</iframe>

זיהוי תכונות

כדי להשתמש ב-Async Clipboard API תוך תמיכה בכל הדפדפנים, צריך לבדוק אם הערך של navigator.clipboard הוא true, ואם לא, לעבור לשיטות קודמות. לדוגמה, כך תוכלו להטמיע הדבקה כדי לכלול דפדפנים אחרים.

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. דרוין גם סיפק את הדמו. תודה ל-Kyarik ול-Gary Kačmarčík על בדיקת חלקים מהמאמר הזה.

התמונה הראשית (Hero) היא של Markus Winkler מ-Unsplash.