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

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

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

זה בסדר בקטעי טקסט קטנים, אבל יש מקרים רבים שבהם חוסמים את להעברת הדף אל לוח העריכה, החוויה גרועה. יכול להיות שתצטרכו לבצע תהליך ניטרול זמן רב או פענוח תמונות כדי להדביק את התוכן בצורה בטוחה. יכול להיות שהדפדפן יצטרך לטעון משאבים מקושרים ממסמך מודבק או להוסיף אותם לקוד. הפעולה הזו חסמה את הדף בזמן ההמתנה לדיסק או לרשת. הוספת הרשאות בתוך השילוב, כך שהדפדפן יחסום את הדף בזמן שהבקשה גישה ללוח העריכה. במקביל, ההרשאות ניתנות document.execCommand() לאינטראקציה עם הלוח מוגדר באופן חופשי ומשתנה בין דפדפנים.

ממשק API אסינכרוני של לוח לטיפול בבעיות האלה, ומספק מודל הרשאות מוגדר היטב לחסום את הדף. ה-API של הלוח האסינכרוני מוגבל לטיפול בטקסט ובתמונות ברוב הדפדפנים, אך התמיכה משתנה. חשוב לקרוא בעיון את הסקירה הכללית על תאימות הדפדפנים בכל אחד מהקטעים הבאים.

העתקה: כתיבת נתונים ללוח

writeText()

כדי להעתיק טקסט ללוח, קוראים לפונקציה writeText(). מכיוון שה-API הזה אסינכרונית, הפונקציה writeText() מחזירה הבטחה שפותרת את הבעיה נדחתה אם הטקסט שהועבר הועתק בהצלחה:

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.
  • קצה: 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()‎

השיטה navigator.clipboard.read() היא גם אסינכרונית ומחזירה הבטחה (promise). כדי לקרוא תמונה מהלוח, מקבלים רשימה של אובייקטים מסוג 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

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

הדוגמה הבאה ממחישה איך עושים את זה. בדוגמה הזו נשתמש ב-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 ורק ב- בכמה דפדפנים.

אישורים

את ממשק ה-API האסינכרוני של הלוח הוטמע על ידי Darwin הואנג וגרי Kačmarčík. דרוין גם סיפק את הדמו. תודה ל-Kyarik ול-Gary Kačmarčík על בדיקת חלקים מהמאמר הזה.

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