ממשק ה-API לגרירה ושחרור של HTML5

בפוסט הזה נסביר את העקרונות הבסיסיים של גרירה ושחרור.

יצירת תוכן שניתן לגרירה

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

כדי לאפשר גרירה של אובייקט, מגדירים את הערך draggable=true לאלמנט הזה. אפשר לגרור כמעט כל דבר, כולל תמונות, קבצים, קישורים, רכיבי קוד או כל דבר אחר בדף.

בדוגמה הבאה נוצר ממשק כדי לסדר מחדש עמודות שתוצגו באמצעות CSS Grid. הרכיב הבסיסי של הרכיבים של העמודות נראה כך, כאשר המאפיין draggable של כל עמודה מוגדר כ-true:

<div class="container">
  <div draggable="true" class="box">A</div>
  <div draggable="true" class="box">B</div>
  <div draggable="true" class="box">C</div>
</div>

זהו הקוד ב-CSS של הרכיבים container ו-box. קוד ה-CSS היחיד שקשור לתכונה של גרירה הוא הנכס cursor: move. שאר הקוד קובע את הפריסה ואת הסגנון של האלמנטים של הקונטיינר והתיבה.

.container {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 10px;
}

.box {
  border: 3px solid #666;
  background-color: #ddd;
  border-radius: .5em;
  padding: 10px;
  cursor: move;
}

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

האזנה לאירועי גרירה

כדי לעקוב אחרי תהליך הגרירה, אפשר להאזין לאחד מהאירועים הבאים:

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

התחלה וסיום של רצף גרירה

אחרי שמגדירים מאפייני draggable="true" בתוכן, צריך לצרף טיפול באירוע dragstart כדי להתחיל את רצף הגרירה של כל עמודה.

הקוד הזה מגדיר את השקיפות של העמודה ל-40% כשהמשתמש מתחיל לגרור אותה, ואז מחזיר אותה ל-100% כשאירוע הגרירה מסתיים.

function handleDragStart(e) {
  this.style.opacity = '0.4';
}

function handleDragEnd(e) {
  this.style.opacity = '1';
}

let items = document.querySelectorAll('.container .box');
items.forEach(function (item) {
  item.addEventListener('dragstart', handleDragStart);
  item.addEventListener('dragend', handleDragEnd);
});

התוצאה מוצגת בדמו של Glitch שבהמשך. גוררים פריט והשקיפות שלו משתנה. מכיוון שרכיב המקור מכיל את האירוע dragstart, הגדרת הערך של this.style.opacity ל-40% מספקת למשתמש משוב חזותי על כך שרכיב זה הוא הבחירה הנוכחית שנעשית. כשמשחררים את הפריט, רכיב המקור חוזר לשקיפות של 100%, גם אם עדיין לא הגדרתם את התנהגות השחרור.

הוספת עוד רמזים חזותיים

כדי לעזור למשתמש להבין איך לבצע אינטראקציה עם הממשק, אפשר להשתמש בבוררי האירועים dragenter, ‏ dragover ו-dragleave. בדוגמה הזו, העמודות משמשות גם כנקודות השמטה וגם כעמודות שניתן לגרור. כדי לעזור למשתמש להבין את זה, אפשר להציג קו מקווקו כשהמשתמש מחליק פריט מעל עמודה. לדוגמה, ב-CSS אפשר ליצור את הכיתה over לרכיבים שהם יעדי השמטה:

.box.over {
  border: 3px dotted #666;
}

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

document.addEventListener('DOMContentLoaded', (event) => {

  function handleDragStart(e) {
    this.style.opacity = '0.4';
  }

  function handleDragEnd(e) {
    this.style.opacity = '1';

    items.forEach(function (item) {
      item.classList.remove('over');
    });
  }

  function handleDragOver(e) {
    e.preventDefault();
    return false;
  }

  function handleDragEnter(e) {
    this.classList.add('over');
  }

  function handleDragLeave(e) {
    this.classList.remove('over');
  }

  let items = document.querySelectorAll('.container .box');
  items.forEach(function(item) {
    item.addEventListener('dragstart', handleDragStart);
    item.addEventListener('dragover', handleDragOver);
    item.addEventListener('dragenter', handleDragEnter);
    item.addEventListener('dragleave', handleDragLeave);
    item.addEventListener('dragend', handleDragEnd);
    item.addEventListener('drop', handleDrop);
  });
});

יש כמה נקודות שחשוב להתייחס אליהן בקוד הזה:

  • פעולת ברירת המחדל של האירוע dragover היא להגדיר את המאפיין dataTransfer.dropEffect לערך "none". המאפיין dropEffect מוסבר בהמשך הדף. בשלב הזה, חשוב רק לדעת שהיא מונעת את הפעלת האירוע drop. כדי לשנות את ההתנהגות הזו, צריך לבצע קריאה ל-e.preventDefault(). שיטה מומלצת נוספת היא להחזיר את הערך false באותו בורר.

  • פונקציית הטיפול באירוע dragenter משמשת להפעלה או להשבתה של הכיתה over במקום dragover. אם משתמשים ב-dragover, האירוע יופעל שוב ושוב בזמן שהמשתמש מחזיק את הפריט שגרר מעל עמודה, וכתוצאה מכך מחלקת ה-CSS תעבור מצב שוב ושוב. כך הדפדפן צריך לבצע הרבה עיבוד גרפי מיותר, שעלול להשפיע על חוויית המשתמש. מומלץ מאוד לצמצם את מספר הרענונים, ואם אתם צריכים להשתמש ב-dragover, כדאי להגביל את קצב שליחת הבקשות או לבטל את הזמן שלאחר הקליק (debouncing) של מאזין האירועים.

השלמת ההעברה

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

function handleDrop(e) {
  e.stopPropagation(); // stops the browser from redirecting.
  return false;
}

חשוב לרשום את ה-handler החדש לצד ה-handlers האחרים:

  let items = document.querySelectorAll('.container .box');
  items.forEach(function(item) {
    item.addEventListener('dragstart', handleDragStart);
    item.addEventListener('dragover', handleDragOver);
    item.addEventListener('dragenter', handleDragEnter);
    item.addEventListener('dragleave', handleDragLeave);
    item.addEventListener('dragend', handleDragEnd);
    item.addEventListener('drop', handleDrop);
  });

אם מריצים את הקוד בשלב הזה, הפריט לא יורד למיקום החדש. כדי לעשות זאת, משתמשים באובייקט DataTransfer.

הנכס dataTransfer מכיל את הנתונים שנשלחים בפעולת גרירה. dataTransfer מוגדר באירוע dragstart ומוקרא או מטופל באירוע ההשלכה. קריאה ל-e.dataTransfer.setData(mimeType, dataPayload) מאפשרת להגדיר את סוג ה-MIME ואת עומס הנתונים של האובייקט.

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

function handleDragStart(e) {
  this.style.opacity = '0.4';

  dragSrcEl = this;

  e.dataTransfer.effectAllowed = 'move';
  e.dataTransfer.setData('text/html', this.innerHTML);
}

באירוע drop, מעבדים את הוספת העמודה על ידי הגדרת ה-HTML של עמודת המקור ל-HTML של עמודת היעד שאליה הוספת את הנתונים. זה כולל בדיקה שהמשתמש לא מושך בחזרה לאותה עמודה שממנה הוא גרר.

function handleDrop(e) {
  e.stopPropagation();

  if (dragSrcEl !== this) {
    dragSrcEl.innerHTML = this.innerHTML;
    this.innerHTML = e.dataTransfer.getData('text/html');
  }

  return false;
}

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

מאפיינים נוספים של גרירה

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

  • dataTransfer.effectAllowed מגביל את 'סוג הגרירה' שהמשתמש יכול לבצע על הרכיב. הוא משמש במודל העיבוד של גרירה ושחרור כדי לאתחל את dropEffect במהלך האירועים dragenter ו-dragover. הנכס יכול לקבל את הערכים הבאים: none,‏ copy,‏ copyLink,‏ copyMove,‏ link,‏ linkMove,‏ move,‏ all ו-uninitialized.
  • dataTransfer.dropEffect קובעת את המשוב שהמשתמש מקבל במהלך האירועים dragenter ו-dragover. כשהמשתמש מעביר את הסמן מעל אלמנט יעד, הסמן בדפדפן מציין את סוג הפעולה שתתבצע, למשל העתקה או העברה. האפקט יכול לקבל אחד מהערכים הבאים: none, ‏ copy,‏ link, ‏ move.
  • e.dataTransfer.setDragImage(imgElement, x, y) פירושו שבמקום להשתמש במשוב ברירת המחדל של הדפדפן 'תמונה רפאים', אפשר להגדיר סמל גרירה.

העלאת קבצים

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

לעתים קרובות משתמשים ב'גרירה ושחרור' כדי לאפשר למשתמשים לגרור פריטים מהמחשב לאפליקציה. ההבדל העיקרי הוא בטיפול באירוע drop. במקום להשתמש ב-dataTransfer.getData() כדי לגשת לקבצים, הנתונים שלהם נכללים בנכס dataTransfer.files:

function handleDrop(e) {
  e.stopPropagation(); // Stops some browsers from redirecting.
  e.preventDefault();

  var files = e.dataTransfer.files;
  for (var i = 0, f; (f = files[i]); i++) {
    // Read the File objects in this FileList.
  }
}

מידע נוסף זמין במאמר גרירה ושחרור בהתאמה אישית.

מקורות מידע נוספים