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

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

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

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

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

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

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

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

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