API การลากและวางของ HTML5

โพสต์นี้อธิบายพื้นฐานของการลากและวาง

ในเบราว์เซอร์ส่วนใหญ่ คุณจะลากการเลือกข้อความ รูปภาพ และลิงก์ได้ เช่น หากลากลิงก์ในหน้าเว็บ คุณจะเห็นช่องเล็กๆ ที่มีชื่อและ URL ซึ่งคุณสามารถวางในแถบที่อยู่หรือเดสก์ท็อปเพื่อสร้างทางลัดหรือไปยังลิงก์ได้ หากต้องการให้เนื้อหาประเภทอื่นๆ ลากได้ คุณต้องใช้ HTML5 Drag and Drop API

หากต้องการให้ลากวัตถุได้ ให้ตั้งค่า 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 สำหรับองค์ประกอบคอนเทนเนอร์และกล่องมีดังนี้ 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);
  });
});

โค้ดนี้มีข้อควรทราบ 2-3 ข้อดังนี้

  • การดำเนินการเริ่มต้นสำหรับเหตุการณ์ dragover คือการตั้งค่าพร็อพเพอร์ตี้ dataTransfer.dropEffect เป็น "none" พร็อพเพอร์ตี้ dropEffect จะกล่าวถึงในหน้านี้ต่อ ในระหว่างนี้ ให้ทราบว่าการดำเนินการนี้ป้องกันไม่ให้เหตุการณ์ drop ทำงาน หากต้องการลบล้างลักษณะการทำงานนี้ ให้โทรหา e.preventDefault() แนวทางปฏิบัติแนะนำอีกอย่างหนึ่งคือให้แสดงผล false ในตัวแฮนเดิลเดียวกันนั้น

  • ตัวแฮนเดิลเหตุการณ์ dragenter ใช้เพื่อเปิด/ปิดคลาส over แทน dragover หากคุณใช้ dragover เหตุการณ์จะทริกเกอร์ซ้ำๆ ขณะที่ผู้ใช้ถือรายการที่ลากไว้เหนือคอลัมน์อยู่ ซึ่งทําให้คลาส CSS เปิด/ปิดซ้ำๆ ซึ่งทำให้เบราว์เซอร์ต้องทำงานแสดงผลที่ไม่จำเป็นเป็นจำนวนมาก ซึ่งอาจส่งผลต่อประสบการณ์ของผู้ใช้ เราขอแนะนําอย่างยิ่งให้ลดการวาดภาพใหม่ และหากจําเป็นต้องใช้ dragover ให้พิจารณาควบคุมหรือยกเลิกการหน่วงเวลา Listener เหตุการณ์

วางสินค้าให้เสร็จสิ้น

หากต้องการประมวลผลรายการที่วาง ให้เพิ่ม Listener เหตุการณ์สําหรับเหตุการณ์ drop ในตัวแฮนเดิล drop คุณจะต้องป้องกันลักษณะการทํางานเริ่มต้นของเบราว์เซอร์สําหรับการหยุดทำงาน ซึ่งโดยปกติแล้วคือการเปลี่ยนเส้นทางที่น่ารําคาญ โดยโทรไปที่ e.stopPropagation()

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

อย่าลืมลงทะเบียนตัวแฮนเดิลใหม่ควบคู่ไปกับตัวแฮนเดิลอื่นๆ ดังนี้

  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) หมายความว่าคุณสามารถตั้งค่าไอคอนการลากแทนการใช้การตอบกลับ "ภาพผี" เริ่มต้นของเบราว์เซอร์

อัปโหลดไฟล์

ตัวอย่างง่ายๆ นี้ใช้คอลัมน์เป็นทั้งแหล่งที่มาของการลากและเป้าหมายการลาก กรณีนี้อาจเกิดขึ้นใน UI ที่ขอให้ผู้ใช้จัดเรียงรายการใหม่ ในบางสถานการณ์ เป้าหมายการลากและแหล่งที่มาอาจเป็นองค์ประกอบประเภทต่างๆ เช่น ในอินเทอร์เฟซที่ผู้ใช้ต้องเลือกรูปภาพ 1 รูปเป็นรูปภาพหลักของผลิตภัณฑ์โดยการลากรูปภาพที่เลือกไปยังเป้าหมาย

การลากและวางมักใช้เพื่อให้ผู้ใช้ลากรายการจากเดสก์ท็อปไปยังแอปพลิเคชันได้ ความแตกต่างหลักอยู่ที่ตัวแฮนเดิล drop ข้อมูลของไฟล์จะอยู่ในพร็อพเพอร์ตี้ dataTransfer.files แทนที่จะใช้ dataTransfer.getData() เพื่อเข้าถึงไฟล์

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

ดูข้อมูลเพิ่มเติมเกี่ยวกับเรื่องนี้ได้ในการลากและวางที่กำหนดเอง

แหล่งข้อมูลเพิ่มเติม