API Kéo và thả HTML5

Bài đăng này giải thích các khái niệm cơ bản của thao tác kéo và thả.

Tạo nội dung có thể kéo

Trong hầu hết các trình duyệt, theo mặc định, người dùng có thể kéo lựa chọn văn bản, hình ảnh và đường liên kết. Ví dụ: nếu kéo một đường liên kết trên một trang web, bạn sẽ thấy một hộp nhỏ có tiêu đề và URL. Bạn có thể thả tiêu đề và URL này vào thanh địa chỉ hoặc màn hình để tạo lối tắt hoặc chuyển đến đường liên kết đó. Để kéo được các loại nội dung khác, bạn cần sử dụng API Kéo và thả HTML5.

Để làm cho một đối tượng có thể kéo được, hãy đặt draggable=true trên phần tử đó. Bạn có thể bật tính năng kéo bất kỳ thứ gì, bao gồm cả hình ảnh, tệp, đường liên kết, tệp hoặc bất kỳ mã đánh dấu nào trên trang.

Ví dụ sau đây sẽ tạo một giao diện để sắp xếp lại các cột đã được bố trí bằng Lưới CSS. Mã đánh dấu cơ bản cho các cột có dạng như sau, với thuộc tính draggable cho từng cột được đặt thành 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>

Đây là CSS cho các phần tử vùng chứa và hộp. CSS duy nhất liên quan đến tính năng kéo là thuộc tính cursor: move. Phần còn lại của mã kiểm soát bố cục và kiểu của các phần tử vùng chứa và hộp.

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

Tại thời điểm này, bạn có thể kéo các mục nhưng không có gì khác xảy ra. Để thêm hành vi, bạn cần sử dụng API JavaScript.

Theo dõi các sự kiện kéo

Để theo dõi quá trình kéo, bạn có thể theo dõi bất kỳ sự kiện nào sau đây:

Để xử lý luồng kéo, bạn cần một loại phần tử nguồn (khi bắt đầu kéo), tải trọng dữ liệu (đối tượng được kéo) và mục tiêu (một khu vực để nắm bắt sự thả). Phần tử nguồn có thể là bất kỳ loại phần tử nào. Mục tiêu là vùng giảm hoặc tập hợp các vùng giảm chấp nhận dữ liệu mà người dùng đang cố gắng thả. Không phải phần tử nào cũng có thể là mục tiêu. Ví dụ: mục tiêu của bạn không được là một hình ảnh.

Bắt đầu và kết thúc trình tự kéo

Sau khi bạn xác định các thuộc tính draggable="true" trên nội dung của mình, hãy đính kèm trình xử lý sự kiện dragstart để bắt đầu trình tự kéo cho từng cột.

Mã này đặt độ mờ của cột thành 40% khi người dùng bắt đầu kéo cột, sau đó trả về 100% khi sự kiện kéo kết thúc.

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);
});

Bạn có thể xem kết quả trong bản minh hoạ sự cố sau đây. Kéo một mục và độ mờ của mục đó sẽ thay đổi. Vì phần tử nguồn có sự kiện dragstart, nên việc đặt this.style.opacity thành 40% sẽ cho người dùng phản hồi bằng hình ảnh rằng phần tử đó là lựa chọn hiện tại đang được di chuyển. Khi bạn thả mục, phần tử nguồn sẽ trở về độ mờ 100%, mặc dù bạn chưa xác định hành vi thả.

Thêm chỉ dẫn bằng hình ảnh khác

Để giúp người dùng nắm được cách tương tác với giao diện của bạn, hãy sử dụng các trình xử lý sự kiện dragenter, dragoverdragleave. Trong ví dụ này, các cột còn là mục tiêu thả ngoài mục tiêu có thể kéo. Hãy giúp người dùng hiểu điều này bằng cách làm mờ đường viền khi họ giữ một mục đã kéo trên một cột. Ví dụ: trong CSS, bạn có thể tạo một lớp over cho các phần tử là mục tiêu thả:

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

Sau đó, trong JavaScript, hãy thiết lập các trình xử lý sự kiện, thêm lớp over khi cột được kéo qua và xoá lớp đó khi phần tử được kéo rời khỏi. Trong trình xử lý dragend, chúng tôi cũng đảm bảo xoá các lớp ở cuối thao tác kéo.

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);
  });
});

Có một vài điểm đáng chú ý trong mã này:

  • Hành động mặc định cho sự kiện dragover là đặt thuộc tính dataTransfer.dropEffect thành "none". Phần sau của trang này sẽ đề cập đến cơ sở lưu trú dropEffect. Bây giờ, chỉ cần biết rằng mã này ngăn sự kiện drop kích hoạt. Để ghi đè hành vi này, hãy gọi e.preventDefault(). Một phương pháp hay khác là trả về false trong cùng một trình xử lý đó.

  • Trình xử lý sự kiện dragenter dùng để chuyển đổi lớp over thay vì dragover. Nếu bạn sử dụng dragover, sự kiện này sẽ kích hoạt nhiều lần khi người dùng giữ mục được kéo trên một cột, khiến lớp CSS chuyển đổi nhiều lần. Điều này khiến trình duyệt thực hiện nhiều công việc kết xuất không cần thiết, có thể ảnh hưởng đến trải nghiệm người dùng. Bạn nên giảm thiểu hoạt động vẽ lại. Nếu cần sử dụng dragover, hãy cân nhắc điều tiết hoặc loại bỏ trình nghe sự kiện.

Hoàn tất thao tác thả

Để xử lý sự kiện thả xuống, hãy thêm trình nghe sự kiện cho sự kiện drop. Trong trình xử lý drop, bạn cần ngăn chặn hành vi mặc định của trình duyệt đối với các lượt thả, thường là một loại lệnh chuyển hướng khó chịu. Để thực hiện việc này, hãy gọi e.stopPropagation().

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

Hãy nhớ đăng ký trình xử lý mới cùng với các trình xử lý khác:

  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);
  });

Nếu bạn chạy mã tại thời điểm này, mục sẽ không giảm xuống vị trí mới. Để làm việc đó, hãy sử dụng đối tượng DataTransfer.

Thuộc tính dataTransfer lưu giữ dữ liệu được gửi trong một thao tác kéo. dataTransfer được đặt trong sự kiện dragstart và đọc hoặc xử lý trong sự kiện thả. Khi gọi e.dataTransfer.setData(mimeType, dataPayload), bạn có thể đặt loại MIME và tải trọng dữ liệu của đối tượng.

Trong ví dụ này, chúng tôi sẽ cho phép người dùng sắp xếp lại thứ tự của các cột. Để làm được việc đó, trước tiên, bạn cần lưu trữ HTML của phần tử nguồn khi quá trình kéo bắt đầu:

function handleDragStart(e) {
  this.style.opacity = '0.4';

  dragSrcEl = this;

  e.dataTransfer.effectAllowed = 'move';
  e.dataTransfer.setData('text/html', this.innerHTML);
}

Trong sự kiện drop, bạn xử lý việc thả cột bằng cách đặt HTML của cột nguồn thành HTML của cột mục tiêu mà bạn đã thả dữ liệu vào đó. Điều này bao gồm việc kiểm tra để đảm bảo rằng người dùng không quay lại cùng cột mà họ đã kéo từ đó.

function handleDrop(e) {
  e.stopPropagation();

  if (dragSrcEl !== this) {
    dragSrcEl.innerHTML = this.innerHTML;
    this.innerHTML = e.dataTransfer.getData('text/html');
  }

  return false;
}

Bạn có thể xem kết quả trong bản minh hoạ sau đây. Để làm được điều này, bạn cần có trình duyệt dành cho máy tính. API Kéo và thả không được hỗ trợ trên thiết bị di động. Kéo và thả cột A ở đầu cột B rồi chú ý cách các cột đó thay đổi vị trí:

Các thuộc tính kéo khác

Đối tượng dataTransfer hiển thị các thuộc tính để cung cấp phản hồi trực quan cho người dùng trong quá trình kéo và kiểm soát cách mỗi mục tiêu thả phản hồi một loại dữ liệu cụ thể.

  • dataTransfer.effectAllowed hạn chế "kiểu kéo" mà người dùng có thể thực hiện trên phần tử. Tham số này được dùng trong mô hình xử lý kéo và thả để khởi chạy dropEffect trong các sự kiện dragenterdragover. Thuộc tính có thể có các giá trị sau: none, copy, copyLink, copyMove, link, linkMove, move, alluninitialized.
  • dataTransfer.dropEffect kiểm soát ý kiến phản hồi mà người dùng nhận được trong các sự kiện dragenterdragover. Khi người dùng giữ con trỏ trên một phần tử mục tiêu, con trỏ của trình duyệt sẽ cho biết loại thao tác sẽ diễn ra, chẳng hạn như sao chép hoặc di chuyển. Hiệu ứng này có thể nhận một trong các giá trị sau: none, copy, link, move.
  • e.dataTransfer.setDragImage(imgElement, x, y) có nghĩa là thay vì sử dụng phản hồi "hình ảnh ma" mặc định của trình duyệt, bạn có thể đặt biểu tượng kéo.

Tải tệp lên

Ví dụ đơn giản này sử dụng một cột làm cả nguồn kéo và đích kéo. Điều này có thể xảy ra trong một giao diện người dùng yêu cầu người dùng sắp xếp lại các mục. Trong một số trường hợp, đích kéo và nguồn có thể là các loại phần tử khác nhau, như trong một giao diện mà người dùng cần chọn một hình ảnh làm hình ảnh chính cho sản phẩm bằng cách kéo hình ảnh đã chọn vào mục tiêu.

Tính năng Kéo và thả thường được dùng để cho phép người dùng kéo các mục từ máy tính vào một ứng dụng. Sự khác biệt chính nằm ở trình xử lý drop. Thay vì sử dụng dataTransfer.getData() để truy cập vào các tệp, dữ liệu của các tệp đó sẽ nằm trong thuộc tính 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.
  }
}

Bạn có thể tìm thêm thông tin về vấn đề này trong phần Kéo và thả tuỳ chỉnh.

Tài nguyên khác