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

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

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

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

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

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

คุณสามารถดูผลลัพธ์ได้ในการสาธิตต่อไปนี้ หากต้องการให้ทำเช่นนี้ คุณต้องมีเบราว์เซอร์จากเดสก์ท็อป อุปกรณ์เคลื่อนที่ไม่รองรับการลากและวาง ลากและวางคอลัมน์ 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.
  }
}

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

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