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

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

הדרך המסורתית לקבל גישה ללוח העריכה של המערכת הייתה באמצעות 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.
  • קצה: 79.
  • Firefox: 63.
  • Safari: 13.1.

מקור

Write()

למעשה, writeText() היא רק שיטה נוחה לשימוש ב-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: 66.
  • קצה: 79.
  • Firefox: 127.
  • Safari: 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:

תמיכה בדפדפן

  • 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.
  • קצה: 79.
  • Firefox: 125.
  • Safari: 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);
  }
}

תמיכה בדפדפן

  • Chrome: 66.
  • קצה: 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.
  • קצה: 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.
  • קצה: 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. ברירת המחדל של הערך הזה משתנה בהתאם לדפדפן, כך שתמיד צריך לכלול אותו.

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

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

setTimeout(async () => {
  const text = await navigator.clipboard.readText();
  console.log(text);
}, 2000);

שילוב של מדיניות ההרשאות

כדי להשתמש ב-API במסגרות iframe, צריך להפעיל אותו עם במדיניות ההרשאות, המגדיר מנגנון שמאפשר להפעיל באופן סלקטיבי השבתת תכונות דפדפן שונות וממשקי API שונים. באופן קונקרטי, צריך להעביר או שניהם, clipboard-read או clipboard-write, בהתאם לצרכים של האפליקציה שלך.

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

זיהוי תכונות

כדי להשתמש ב-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);
});

זה לא כל הסיפור. לפני השימוש בממשק ה-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();
});

הדגמות

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

הדוגמה הראשונה ממחישה העברה של טקסט בלוח העריכה ומחוצה לו.

אפשר להשתמש בהדגמה הזו כדי לנסות את ה-API עם תמונות. תזכורת: המערכת תומכת רק בקובצי PNG ורק ב- בכמה דפדפנים.

אישורים

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

התמונה הראשית (Hero) של Markus Winkler באתר ביטול הפתיחה.