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

פוסט זה מסביר את היסודות של גרירה ושחרור.

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

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

כדי שאובייקט יהיה ניתן לגרירה, מגדירים 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;
}

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

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

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

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

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

אחרי שמגדירים מאפייני draggable="true" בתוכן, צריך לצרף גורם handler של אירועים 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);
});

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

הוספת סימנים חזותיים נוספים

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

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

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

השלמת התהליך

כדי לעבד את הירידה, יש להוסיף event listener לאירוע drop. ב-handler של 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 של האובייקט ואת המטען הייעודי (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.
  }
}

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

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