API Kéo và thả HTML5

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

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

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

Để cho phép một đối tượng có thể kéo, 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 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 của bạn.

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

Dưới đây là CSS cho 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.

Nghe 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 số loại phần tử nguồn (nơi bắt đầu kéo), tải trọng dữ liệu (đối tượng đang được kéo) và một mục tiêu (khu vực để nhận dữ liệu thả). Phần tử nguồn có thể là hầu hết các loại phần tử. Mục tiêu là vùng thả hoặc tập hợp các vùng thả 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à hình ảnh.

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

Sau khi bạn xác định các thuộc tính draggable="true" cho nội dung của mình, hãy đính kèm một trình xử lý sự kiện dragstart để bắt đầu trình tự kéo cho mỗi 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ạ về 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ử đó chính 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 hiểu cách tương tác với giao diện của bạn, hãy dùng trình xử lý sự kiện dragenter, dragoverdragleave. Trong ví dụ này, các cột là mục tiêu thả ngoài việc có thể kéo. Giúp người dùng hiểu điều này bằng cách tạo đường viền nét đứt khi họ giữ một mục được kéo qua 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 trình xử lý sự kiện, thêm lớp over khi kéo cột và xoá lớp đó khi phần tử được kéo rời khỏi. Trong trình xử lý dragend, chúng ta cũng nhớ xoá các lớp ở cuối quá trình 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 đề cập trong mã này:

  • Thao tác mặc định cho sự kiện dragover là đặt thuộc tính dataTransfer.dropEffect thành "none". Tài sản dropEffect sẽ được đề cập ở phần sau của trang này. Hiện tại, chỉ cần biết rằng thao tác này sẽ 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 chính trình xử lý đó.

  • Trình xử lý sự kiện dragenter được dùng để bật/tắt lớp over thay vì dragover. Nếu bạn sử dụng dragover, sự kiện sẽ kích hoạt nhiều lần trong khi người dùng giữ mụ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 việc vẽ lại và nếu cần sử dụng dragover, hãy cân nhắc điều tiết hoặc huỷ bỏ trình nghe sự kiện.

Hoàn tất việc phát hành

Để xử lý thao tác thả, 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 trường hợp thả (thường là một kiểu chuyển hướng gây 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ã vào thời điểm này, mặt hàng sẽ không bị thả xuống vị trí mới. Để làm điều đó, 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 đọc hoặc xử lý trong sự kiện thả. Việc gọi e.dataTransfer.setData(mimeType, dataPayload) cho phép bạn đặ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 việc đó, trước tiên, bạn cần lưu trữ HTML của phần tử nguồn khi thao tác 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. Điều này bao gồm việc kiểm tra để đảm bảo người dùng không thả lại vào cùng một cột mà họ đã kéo.

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 việc 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 lên đầu cột B rồi chú ý cách các cột này 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ử. Phương thức 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 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ỏ của họ 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, mục tiêu kéo và nguồn có thể là các loại phần tử khác nhau, chẳng hạn như trong một giao diện trong đó 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àn hình vào một ứng dụng. Điểm 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 này được chứa 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