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

Trong hầu hết các trình duyệt, văn bản đã chọn, hình ảnh và đường liên kết đều có thể kéo được 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.

Để tạo một đối tượng có thể kéo, hãy đặt draggable=true trên phần tử đó. Bạn có thể kéo mọi thứ, 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.

Ví dụ sau đây tạo một giao diện để sắp xếp lại các cột đã được bố trí bằng CSS Grid. 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 sẽ không có gì 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 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" trên nội dung, hãy đính kèm 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ạ Glitch sau. 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ẽ cung cấp cho người dùng phản hồi trực quan rằng phần tử đó là lựa chọn hiện đ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 các tín hiệu 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 sử 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 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 đề 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". Thuộc tính dropEffect sẽ được trình bày ở phần sau của trang này. Hiện tại, bạn chỉ cần biết rằng lớp 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 đượ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 liên tục trong khi người dùng giữ mục đã kéo trên một cột, khiến lớp CSS bật/tắt liên tục. Điều này khiến trình duyệt phải 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 loại bỏ độ trễ của trình nghe sự kiện.

Hoàn tất việc thả

Để 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 hành vi mặc định của trình duyệt đối với các mục thả, thường là một số loại chuyển hướng gây phiền toái. Để 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 chuyển 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 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 ta 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. Để 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 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 với một loại dữ liệu cụ thể.

  • dataTransfer.effectAllowed hạn chế "loại thao tác 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 này 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 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 một sản phẩm bằng cách kéo hình ảnh đã chọn vào một mục tiêu.

Thao tác 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 để bàn 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