Làm hiệu ứng drag and swap với Javascript

 


Thao tác kéo thả là một thao tác rất quen thuộc, bởi lẽ nó gần với thao tác chuột của máy tính. Và vì lẽ đó, khi làm web có tương tác (interactive) thì việc thêm các hiệu ứng kéo thả cũng là một điều dễ hiểu. Dễ thấy nhất là kéo thả để lựa chọn hoặc kéo thả để thay đổi thứ tự các phần tử của trang.

Đây là một hành động rất phổ biến, bởi vậy có rất nhiều thư viện hỗ trợ điều này, tuy nhiên nếu phải làm từ đầu tới cuối, không có các thư viện hỗ trợ thì sao? ÁC MỘNG. Chính thế, những hành động thông thường lại chứa nhiều logic tới không tưởng. Vậy thì tại sao chúng ta không phân tích nó và bắt tay vào làm thử nhỉ?

Chuẩn bị

Trước tiên chúng ta cần chuẩn bị một file html để bắt đầu làm việc. Hãy bắt đầu với việc tạo một file html với một list, chúng ta sẽ thực hiện thao tác drag, swap trên các phần tử của list.

<!DOCTYPE html>
<html>

  <head>
    <meta charset="UTF-8">
    <title>Document</title>
  </head>

  <body>
    <ul class="dd-box">
      <li>Component 1</li>
      <li>Component 2</li>
      <li>Component 3</li>
      <li>Component 4</li>
      <li>Component 5</li>
    </ul>
  </body>

</html>

List type tôi mong muốn là các khối block, để có thể kéo thả vị trí của chúng cho nhau. Và để thêm phần hiệu ứng thì không thể thiếu một chút css.

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

.dd-box {
  list-style: none;
  padding: 10px;
}

.dd-box li {
  user-select: none;
  background: #fff;
  box-shadow: 0 0 2px 1px rgba(0, 0, 0, .2);
  cursor: grab;
  max-width: 500px;
  padding: 10px;
  transition: box-shadow .3s;
}

.dd-box li.onGrab {
  cursor: grabbing;
  box-shadow: 0 0 10px 2px rgba(0, 0, 0, .15);
  position: absolute;
  z-index: 1;
}

.dd-box li:not(:last-child) {
  margin-bottom: 10px;
}

.hidden {
  visibility: hidden;
}

Tôi có viết sẵn 2 class là .onGrab và .hidden, về ngữ nghĩa và tính năng của nó thì chắc các bạn đều hiểu. 
  • Class onGrab sẽ được thêm vào component mà tôi cầm và drag, có thêm chút box-shadow để hiển thị của nó khác biệt hơn với các component khác. 
  • Class hidden sẽ được tôi dùng để ẩn component mà tôi không muốn nó hiển thị, nhưng vẫn muốn nó "chiếm" một khoảng bằng đúng độ rộng của component đó.
Giờ chúng ta bắt tay vào phần logic thôi nào.

Code logic drag and swap

Trước tiên, hãy lấy ra component list mà chúng ta sắp sửa bóc tách và thêm sự kiện cho chúng nào.
  const ddBox = document.querySelector('.dd-box');
  const ddBoxList = ddBox.querySelectorAll('li');

Ngoài ra thì chúng ta cần một biến để lưu lại đối tượng mà chúng ta đang tác động.
  let data = {
    target: null,
    diffX: 0,
    diffY: 0
  };                                                                                        

Được rồi, giờ chúng ta cần suy nghĩ một chút về logic mà mình định làm. Drag and Drop là một thao tác rất cơ bản, bao gồm 3 sự kiện 
  • Down: Nhấn / chọn 1 component
  • Move: Di chuyển component đang chọn
  • Up: Thả / bỏ chọn và đặt component ở vị trí mới
Khi ở sự kiện down, chúng ta sẽ cần tạo một component temp, để nó hidden và tượng trưng cho vị trí mà chúng ta muốn drag.

Với logic như vậy, đây sẽ là đoạn code mà chúng ta cần viết. Tôi tạo 2 const util và ev để chứa các function.
  const util = {
    index(el) {
      const parent = el.parentElement;
      const siblings = parent.children;
      const siblingsArr = [].slice.call(siblings);
      const idx = siblingsArr.indexOf(el);

      return idx;
    },
    insertClone(target, insertIdx) {
      const cloneName = `ddItemClone_${Math.trunc(Math.random() * 10000)}`;
      const clone = target.cloneNode(true);
      const parent = target.parentElement;
      const siblings = parent.children;

      clone.classList.add('hidden');
      clone.classList.add(cloneName);
      siblings[insertIdx].insertAdjacentElement('afterend', clone);

      return cloneName;
    }};
  const ev = {
    down(e) {
      const target = e.target;
      const pageX = e.pageX;
      const pageY = e.pageY;
      const targetW = target.offsetWidth;
      const targetRect = target.getBoundingClientRect();
      const targetRectX = targetRect.left;
      const targetRectY = targetRect.top;

      data.target = target;
      data.diffX = pageX - targetRectX;
      data.diffY = pageY - targetRectY;
      data.cloneName = util.insertClone(target, util.index(target));
      target.style.width = `${targetW}px`;
      target.classList.add('onGrab');
      window.addEventListener('mousemove', ev.move);
      window.addEventListener('mouseup', ev.up);
    };
  ddBoxList.forEach((el) => {
    el.addEventListener('mousedown', ev.down);
  });

Vậy là giải quyết xong việc tạo (clone) component drag. Giờ hãy bắt đầu sự kiện move và swap thôi nào. Sự kiện move đơn giản là chúng ta sẽ tính toán tọa độ của component đang được drag, so sánh nó với list component hiện có (component clone sẽ ở vị trí mà chúng ta vừa kéo ra, vậy nên độ lớn của toàn thể không bị thay đổi). Nếu giá trị so sánh mà vượt qua 1/2 độ rộng component (ở đây tôi xếp component theo chiều dọc nên tôi sẽ tính bằng 1/2 Y) thì sẽ thực hiện swap vị trí của chúng.

Bổ sung sự kiện này vào ev.
move(e) {
      const target = data.target;
      const pageX = e.pageX;
      const pageY = e.pageY;
      const targetPosL = pageX - data.diffX;
      const targetPosT = pageY - data.diffY;

      target.style.left = `${targetPosL}px`;
      target.style.top = `${targetPosT}px`;
      util.swap(target);
    }

Và bổ sung function swap này vào util.
swap(target) {
      const selfIdx = util.index(target);
      const cloneIdx = selfIdx + 1;
      const parent = target.parentElement;
      const siblings = parent.querySelectorAll(`:scope > *:not(.onGrab):not(.${data.cloneName})`);

      for (let thatIdx = 0, len = siblings.length; thatIdx < len; thatIdx++) {
        const targetW = target.offsetWidth;
        const targetH = target.offsetHeight;
        const targetRect = target.getBoundingClientRect();
        const targetRectX = targetRect.left;
        const targetRectY = targetRect.top;
        const that = siblings[thatIdx];
        const thatW = that.offsetWidth;
        const thatH = that.offsetHeight;
        const thatRect = that.getBoundingClientRect();
        const thatRectX = thatRect.left;
        const thatRectY = thatRect.top;
        const thatRectYHalf = thatRectY + (thatH / 2);
        const hitX = thatRectX <= (targetRectX + targetW) && thatRectX + thatW >= targetRectX;
        const hitY = targetRectY <= thatRectYHalf && (targetRectY + targetH) >= thatRectYHalf;
        const isHit = hitX && hitY;

        if (isHit) {
          const siblingsAll = parent.children;
          const clone = siblingsAll[cloneIdx];

          parent.insertBefore(clone, selfIdx > thatIdx ? that : that.nextSibling);
          parent.insertBefore(target, clone);

          break;
        }
      }
    }

Gần xong rồi. Sau khi hoàn thành drag. Chúng ta cần remove các sự kiện swap này khỏi target / component được chọn, xóa component clone. Để có thể tiếp tục drag and swap các component khác.

Bổ sung sự kiện up vào ev.
up() {
      const target = data.target;
      const cloneSelector = `.${data.cloneName}`;
      const clone = document.querySelector(cloneSelector);

      data.cloneName = '';
      clone.remove();
      target.removeAttribute('style');
      target.classList.remove('onGrab');
      target.classList.remove('onDrag');
      window.removeEventListener('mousemove', ev.move);
      window.removeEventListener('mouseup', ev.up);
    }

Vậy là đã hoàn thành, và đây là thành quả mà chúng ta đạt được.


Đây không phải là cách duy nhất để thực hiện drag and swap. Còn rất nhiều phương pháp Drag and Drop khác, và với một chút coding Javascript, bạn có thể thực hiện Swap chúng dễ dàng. Tôi sẽ tiếp tục viết thêm nhiều bài viết khác hướng dẫn về phần này.

Tạm biệt và hẹn gặp lại các bạn ở các blog tiếp theo.

Đăng nhận xét

Mới hơn Cũ hơn