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

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

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

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

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

בדוגמה הבאה נוצר ממשק לסידור מחדש של עמודות שפותחים באמצעות רשת CSS. תגי העיצוב הבסיסיים של העמודות נראים כך, כאשר המאפיין 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 לרכיבי הקונטיינר והתיבה. קוד ה-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;
}

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

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

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

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

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

אחרי שמגדירים מאפייני 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 באותו handler.

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

השלמת ההשקה

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

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

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

  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 של האובייקט ואת המטען הייעודי (payload) של הנתונים.

בדוגמה הזו נאפשר למשתמשים לשנות את הסדר של העמודות. כדי לעשות זאת, תחילה צריך לאחסן את ה-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;
}

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

העלאת קבצים

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

לעתים קרובות משתמשים ב'גרירה ושחרור' כדי לאפשר למשתמשים לגרור פריטים מהמחשב לאפליקציה. ההבדל העיקרי הוא ב-handler של 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.
  }
}

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

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