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

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

הדרך המסורתית לקבל גישה ללוח העריכה של המערכת הייתה דרך 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

מקור

Write()

למעשה, writeText() הוא רק שיטת נוחות לשיטה הגנרית write(), שמאפשרת גם להעתיק תמונות ללוח. כמו writeText(), היא אסינכרונית ומחזירה Promise.

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

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

בשלב הבא מעבירים מערך של אובייקטים ClipboardItem כפרמטר ל-method 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);
}

תמיכה בדפדפן

  • 66
  • 79
  • 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:

תמיכה בדפדפן

  • 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

מקור

Read()

השיטה 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);
  }
}

תמיכה בדפדפן

  • 66
  • 79
  • 13.1

מקור

עבודה עם קבצים שהודבקו

זה שימושי למשתמשים שבהם הם יכולים להשתמש במקשי קיצור כמו Ctrl+c ו-Ctrl+v. Chromium חושף בלוח העריכה קבצים שמוגדרים לקריאה בלבד, כפי שמתואר בהמשך. האפשרות הזו מוצגת כשהמשתמש לוחץ על קיצור הדרך להדבקה שמוגדר כברירת מחדל במערכת ההפעלה, או כשהמשתמש לוחץ על Edit ואז על הדבקה בסרגל התפריטים של הדפדפן. אין צורך בקוד צנרת נוסף.

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

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

הדוגמה הבאה מראה איך לעשות זאת. בדוגמה הזו נעשה שימוש ב-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-readclipboard-write

<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.

אישורים

ה-Asynchronous Clipboard API יושם על ידי DarwinHuang ו-Gary Kačmarčík. דארווין גם סיפק את ההדגמה. תודה ל-Kyarik ושוב לגary Kačmarčík שסוקרת חלקים ממאמר זה.

תמונה ראשית (Hero) של מרקוס וינקלר ב-UnFlood.