Tại Cafedev, chúng tôi khám phá thế giới JavaScript từ cơ bản đến nâng cao qua các kỹ thuật Drag’n’Drop(Kéo và thả) với sự kiện chuột. Kéo và thả không chỉ mang lại giao diện trực quan mà còn là công cụ mạnh mẽ trong việc xử lý và tổ chức dữ liệu. Trong bài viết này, bạn sẽ tìm hiểu cách sử dụng các sự kiện chuột để tạo ra hiệu ứng kéo và thả linh hoạt, từ việc di chuyển các phần tử đến xác định các khu vực thả mục tiêu. Hãy cùng Cafedev làm quen với các kỹ thuật này để nâng cao kỹ năng lập trình của bạn!

Drag’n’Drop(Kéo và thả) là một giải pháp giao diện tuyệt vời. Việc kéo và thả một đối tượng là một cách rõ ràng và đơn giản để thực hiện nhiều việc, từ việc sao chép và di chuyển tài liệu (như trong các trình quản lý tệp) đến việc sắp xếp (thả các mục vào giỏ hàng).
Trong chuẩn HTML hiện đại có một phần về Kéo và Thả với các sự kiện đặc biệt như dragstart, dragend, và các sự kiện khác.

Những sự kiện này cho phép chúng ta hỗ trợ các loại kéo và thả đặc biệt, chẳng hạn như xử lý việc kéo một tệp từ trình quản lý tệp của hệ điều hành và thả nó vào cửa sổ trình duyệt. Sau đó, JavaScript có thể truy cập nội dung của các tệp đó.

Tuy nhiên, các sự kiện kéo và thả gốc cũng có những hạn chế. Ví dụ, chúng ta không thể ngăn chặn việc kéo từ một khu vực nhất định. Cũng không thể chỉ kéo theo “hướng ngang” hoặc “hướng dọc”. Và có nhiều nhiệm vụ kéo và thả khác không thể thực hiện được bằng cách sử dụng chúng. Hỗ trợ cho các thiết bị di động với các sự kiện này cũng rất yếu.

Vì vậy, ở đây chúng ta sẽ xem cách triển khai Drag’n’Drop bằng cách sử dụng các sự kiện chuột.

1. Thuật toán Drag’n’Drop

Thuật toán Drag’n’Drop cơ bản trông như sau:
1. Trên sự kiện mousedown – chuẩn bị đối tượng để di chuyển, nếu cần (có thể tạo một bản sao của nó, thêm lớp vào nó hoặc làm gì đó tương tự).
2. Sau đó, trên sự kiện mousemove di chuyển nó bằng cách thay đổi left/top với position:absolute.

3.Trên sự kiện mouseup – thực hiện tất cả các hành động liên quan đến việc kết thúc kéo và thả.

    Đây là những điểm cơ bản. Sau đó, chúng ta sẽ xem cách thêm các tính năng khác, chẳng hạn như làm nổi bật các phần tử hiện tại khi chúng ta kéo qua chúng.

    Đây là cách triển khai kéo một quả bóng:

    ball.onmousedown = function(event) {
      // (1) prepare to moving: make absolute and on top by z-index
      ball.style.position = 'absolute';
      ball.style.zIndex = 1000;
    
      // move it out of any current parents directly into body
      // to make it positioned relative to the body
      document.body.append(ball);
    
      // centers the ball at (pageX, pageY) coordinates
      function moveAt(pageX, pageY) {
        ball.style.left = pageX - ball.offsetWidth / 2 + 'px';
        ball.style.top = pageY - ball.offsetHeight / 2 + 'px';
      }
    
      // move our absolutely positioned ball under the pointer
      moveAt(event.pageX, event.pageY);
    
      function onMouseMove(event) {
        moveAt(event.pageX, event.pageY);
      }
    
      // (2) move the ball on mousemove
      document.addEventListener('mousemove', onMouseMove);
    
      // (3) drop the ball, remove unneeded handlers
      ball.onmouseup = function() {
        document.removeEventListener('mousemove', onMouseMove);
        ball.onmouseup = null;
      };
    
    };

    Nếu chúng ta chạy mã, chúng ta có thể nhận thấy điều gì đó kỳ lạ. Vào đầu của thao tác kéo và thả, quả bóng nhân bản: chúng ta bắt đầu kéo bản sao của nó.

    Thử kéo và thả bằng chuột và bạn sẽ thấy hành vi như vậy.
    Điều này xảy ra vì trình duyệt có hỗ trợ kéo và thả riêng cho hình ảnh và một số yếu tố khác. Nó hoạt động tự động và gây xung đột với hệ thống của chúng ta.

    Để vô hiệu hóa nó:

    ball.ondragstart = function() {
      return false;
    };

    Bây giờ mọi thứ sẽ hoạt động đúng như yêu cầu.

    Một khía cạnh quan trọng khác là chúng ta theo dõi mousemove trên document, chứ không phải trên ball. Ban đầu có thể có vẻ như chuột luôn nằm trên quả bóng, và chúng ta có thể đặt mousemove trên nó.
    Nhưng như chúng ta đã biết, mousemove được kích hoạt thường xuyên, nhưng không phải cho mỗi pixel. Vì vậy, sau khi di chuyển nhanh, con trỏ có thể nhảy từ quả bóng vào giữa tài liệu (hoặc thậm chí ra ngoài cửa sổ).

    Vì vậy, chúng ta nên lắng nghe trên document để bắt được nó.

    2. Định vị chính xác

    Trong các ví dụ trên, quả bóng luôn được di chuyển sao cho trung tâm của nó nằm dưới con trỏ:

    ball.style.left = pageX - ball.offsetWidth / 2 + 'px';
    ball.style.top = pageY - ball.offsetHeight / 2 + 'px';

    Không tệ, nhưng có một tác dụng phụ. Để bắt đầu kéo và thả, chúng ta có thể mousedown ở bất kỳ đâu trên quả bóng. Nhưng nếu “nhấc” nó từ cạnh, thì quả bóng đột nhiên “nhảy” để trở thành trung tâm dưới con trỏ chuột.
    Sẽ tốt hơn nếu chúng ta giữ khoảng cách ban đầu của đối tượng so với con trỏ.

    Ví dụ, nếu chúng ta bắt đầu kéo từ cạnh của quả bóng, thì con trỏ nên giữ nguyên vị trí trên cạnh trong khi kéo.

    Hãy cập nhật thuật toán của chúng ta:
    1. Khi người dùng nhấn nút (mousedown) – nhớ khoảng cách từ con trỏ đến góc trên bên trái của quả bóng trong các biến shiftX/shiftY. Chúng ta sẽ giữ khoảng cách đó trong khi kéo.
    Để có được những sự thay đổi này, chúng ta có thể trừ các tọa độ:

    // onmousedown
        let shiftX = event.clientX - ball.getBoundingClientRect().left;
        let shiftY = event.clientY - ball.getBoundingClientRect().top;

    2. Sau đó, trong khi kéo, chúng ta định vị quả bóng với khoảng cách giống nhau so với con trỏ, như sau:

    // onmousemove
        // ball has position:absolute
        ball.style.left = event.pageX - *!*shiftX*/!* + 'px';
        ball.style.top = event.pageY - *!*shiftY*/!* + 'px';

    Mã cuối cùng với định vị tốt hơn:

    ball.onmousedown = function(event) {
    
    
      let shiftX = event.clientX - ball.getBoundingClientRect().left;
      let shiftY = event.clientY - ball.getBoundingClientRect().top;
    
      ball.style.position = 'absolute';
      ball.style.zIndex = 1000;
      document.body.append(ball);
    
      moveAt(event.pageX, event.pageY);
    
      // moves the ball at (pageX, pageY) coordinates
      // taking initial shifts into account
      function moveAt(pageX, pageY) {
        ball.style.left = pageX - *!*shiftX*/!* + 'px';
        ball.style.top = pageY - *!*shiftY*/!* + 'px';
      }
    
      function onMouseMove(event) {
        moveAt(event.pageX, event.pageY);
      }
    
      // move the ball on mousemove
      document.addEventListener('mousemove', onMouseMove);
    
      // drop the ball, remove unneeded handlers
      ball.onmouseup = function() {
        document.removeEventListener('mousemove', onMouseMove);
        ball.onmouseup = null;
      };
    
    };
    
    ball.ondragstart = function() {
      return false;
    };

    Trong hành động (bên trong <iframe>):
    Sự khác biệt đặc biệt rõ ràng nếu chúng ta kéo quả bóng từ góc dưới bên phải của nó. Trong ví dụ trước, quả bóng “nhảy” dưới con trỏ. Bây giờ nó di chuyển mượt mà theo con trỏ từ vị trí hiện tại.

    3. Các mục tiêu thả tiềm năng (droppables)

    Trong các ví dụ trước, quả bóng có thể được thả ở bất kỳ đâu để giữ lại. Trong thực tế, chúng ta thường lấy một phần tử và thả nó lên phần tử khác. Ví dụ, “tệp” vào “thư mục” hoặc cái gì đó khác.
    Nói chung, chúng ta lấy một phần tử “có thể kéo” và thả nó lên phần tử “có thể thả”.

    Chúng ta cần biết:

    • nơi phần tử được thả vào cuối quá trình Kéo và Thả — để thực hiện hành động tương ứng,
    • và, nếu có thể, biết phần tử có thể thả mà chúng ta đang kéo qua, để làm nổi bật nó.

    Giải pháp có vẻ thú vị và hơi phức tạp một chút, vì vậy hãy tìm hiểu nó ở đây.
    Ý tưởng đầu tiên có thể là gì? Có thể đặt các trình xử lý mouseover/mouseup trên các phần tử có thể thả?

    Nhưng điều đó không hoạt động.
    Vấn đề là, trong khi chúng ta kéo, phần tử có thể kéo luôn nằm trên các phần tử khác. Và các sự kiện chuột chỉ xảy ra trên phần tử ở trên cùng, không phải trên những phần tử bên dưới nó.

    Ví dụ, dưới đây là hai phần tử <div>, phần tử màu đỏ nằm trên phần tử màu xanh (che phủ hoàn toàn). Không có cách nào để bắt sự kiện trên phần tử màu xanh vì phần tử màu đỏ nằm phía trên.

    
    <style>
      div {
        width: 50px;
        height: 50px;
        position: absolute;
        top: 0;
      }
    </style>
    <div style="background:blue" onmouseover="alert('never works')"></div>
    <div style="background:red" onmouseover="alert('over red!')"></div>

    Điều tương tự với phần tử có thể kéo. Quả bóng luôn nằm trên cùng các phần tử khác, vì vậy các sự kiện xảy ra trên nó. Dù chúng ta có đặt trình xử lý trên các phần tử phía dưới, chúng cũng không hoạt động.
    Đó là lý do tại sao ý tưởng ban đầu để đặt trình xử lý trên các phần tử có thể thả không hoạt động trong thực tế. Chúng sẽ không được kích hoạt.

    Vậy thì, chúng ta nên làm gì?

    Có một phương thức gọi là document.elementFromPoint(clientX, clientY). Nó trả về phần tử lồng ghép nhất tại các tọa độ tương đối của cửa sổ (hoặc null nếu các tọa độ đó nằm ngoài cửa sổ). Nếu có nhiều phần tử chồng lên nhau tại cùng một tọa độ, phần tử trên cùng sẽ được trả về.
    Chúng ta có thể sử dụng nó trong bất kỳ trình xử lý sự kiện chuột nào để phát hiện phần tử có thể thả dưới con trỏ, như sau:

    // in a mouse event handler
    ball.hidden = true; // (*) hide the element that we drag
    
    let elemBelow = document.elementFromPoint(event.clientX, event.clientY);
    // elemBelow is the element below the ball, may be droppable
    
    ball.hidden = false;

    Xin lưu ý: chúng ta cần ẩn quả bóng trước khi gọi (*). Nếu không, thường chúng ta sẽ có quả bóng ở những tọa độ này, vì nó là phần tử trên cùng dưới con trỏ: elemBelow=ball. Vì vậy, chúng ta ẩn nó và ngay lập tức hiển thị lại.
    Chúng ta có thể sử dụng mã này để kiểm tra phần tử nào mà chúng ta đang “bay qua” vào bất kỳ lúc nào. Và xử lý việc thả khi nó xảy ra.

    Một mã mở rộng của onMouseMove để tìm các phần tử “có thể thả”:

    // potential droppable that we're flying over right now
    let currentDroppable = null;
    
    function onMouseMove(event) {
      moveAt(event.pageX, event.pageY);
    
      ball.hidden = true;
      let elemBelow = document.elementFromPoint(event.clientX, event.clientY);
      ball.hidden = false;
    
      // mousemove events may trigger out of the window (when the ball is dragged off-screen)
      // if clientX/clientY are out of the window, then elementFromPoint returns null
      if (!elemBelow) return;
    
      // potential droppables are labeled with the class "droppable" (can be other logic)
      let droppableBelow = elemBelow.closest('.droppable');
    
      if (currentDroppable != droppableBelow) {
        // we're flying in or out...
        // note: both values can be null
        //   currentDroppable=null if we were not over a droppable before this event (e.g over an empty space)
        //   droppableBelow=null if we're not over a droppable now, during this event
    
        if (currentDroppable) {
          // the logic to process "flying out" of the droppable (remove highlight)
          leaveDroppable(currentDroppable);
        }
        currentDroppable = droppableBelow;
        if (currentDroppable) {
          // the logic to process "flying in" of the droppable
          enterDroppable(currentDroppable);
        }
      }
    }

    Trong ví dụ dưới đây, khi quả bóng được kéo qua khung thành bóng đá, khung thành được làm nổi bật.

    Ví dụ đầy đủ

    4. Tổng kết

    Chúng ta đã xem xét một thuật toán Drag’n’Drop cơ bản.
    Các thành phần chính:

    1. Luồng sự kiện: ball.mousedown -> document.mousemove -> ball.mouseup (đừng quên hủy sự kiện ondragstart mặc định).
    2. Tại thời điểm bắt đầu kéo — ghi nhớ sự thay đổi ban đầu của con trỏ so với phần tử: shiftX/shiftY và giữ nó trong suốt quá trình kéo.
    3. Phát hiện các phần tử có thể thả dưới con trỏ bằng cách sử dụng document.elementFromPoint.

    Chúng ta có thể xây dựng rất nhiều trên nền tảng này.

    • Vào thời điểm mouseup, chúng ta có thể hoàn thiện việc thả một cách thông minh: thay đổi dữ liệu, di chuyển các phần tử xung quanh.
    • Chúng ta có thể làm nổi bật các phần tử mà chúng ta đang bay qua.
    • Chúng ta có thể hạn chế việc kéo theo một khu vực hoặc hướng nhất định.
    • Chúng ta có thể sử dụng phân phối sự kiện cho mousedown/up. Một trình xử lý sự kiện với diện tích lớn kiểm tra event.target có thể quản lý Drag’n’Drop cho hàng trăm phần tử.
    • Và còn nhiều điều khác.

    Có những framework xây dựng kiến trúc dựa trên điều này: các lớp như DragZone, Droppable, Draggable và các lớp khác. Hầu hết chúng thực hiện các công việc tương tự như những gì đã được mô tả ở trên, vì vậy bạn sẽ dễ hiểu chúng hơn bây giờ. Hoặc bạn có thể tự xây dựng, như bạn thấy, việc này đủ dễ dàng, đôi khi còn dễ hơn là thích ứng với giải pháp của bên thứ ba.

    Tại Cafedev, chúng tôi đã cùng bạn khám phá cách sử dụng các sự kiện chuột để thực hiện tính năng kéo và thả trong JavaScript. Bằng cách áp dụng những kỹ thuật này, bạn có thể tạo ra những giao diện tương tác mạnh mẽ và thân thiện với người dùng. Hy vọng rằng các ví dụ và hướng dẫn từ Cafedev sẽ giúp bạn thực hành và phát triển kỹ năng lập trình của mình. Tiếp tục theo dõi Cafedev để không bỏ lỡ những bài học và mẹo hay trong hành trình học JavaScript của bạn!Tham khảo thêm: MIỄN PHÍ 100% | Series tự học Javascrypt chi tiết, dễ hiểu từ cơ bản tới nâng cao + Full Bài Tập thực hành nâng cao trình devCác nguồn kiến thức MIỄN PHÍ VÔ GIÁ từ cafedev tại đâyNếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của CafeDev để nhận được nhiều hơn nữa: Group Facebook

  1. Fanpage
  2. Youtube
  3. Instagram
  4. Twitter
  5. Linkedin
  6. Pinterest
  7. Reddit
  8. Tumblr
  9. Trang chủ
  10. Chào thân ái và quyết thắng!

    Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!