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

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

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

หากต้องการให้วัตถุลากได้ ให้ตั้งค่า 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

ฟังเหตุการณ์การลาก

หากต้องการตรวจสอบกระบวนการลาก คุณสามารถฟังเหตุการณ์ต่อไปนี้

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

เริ่มและสิ้นสุดลำดับการลาก

หลังจากที่กำหนดแอตทริบิวต์ 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.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.
  }
}

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

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