Chào mừng bạn đến với Cafedev! Trong hành trình học JavaScript từ cơ bản đến nâng cao, việc nắm vững các sự kiện con trỏ (Pointer events) là rất quan trọng. Cafedev sẽ giúp bạn khám phá cách các sự kiện này hoạt động, từ việc theo dõi các tương tác của người dùng cho đến việc cải thiện khả năng tương tác của ứng dụng web. Hãy cùng tìm hiểu cách sử dụng Pointer events để tạo ra những trải nghiệm người dùng mượt mà và hiệu quả hơn.

Pointer events là cách hiện đại để xử lý đầu vào từ nhiều thiết bị chỉ định khác nhau, chẳng hạn như chuột, bút/chìa khóa, màn hình cảm ứng, và nhiều hơn nữa.

1. Lịch sử ngắn gọn

Hãy điểm qua một chút, để bạn hiểu bức tranh tổng thể và vị trí của Pointer Events giữa các loại sự kiện khác.
– Ngày xưa, chỉ có sự kiện chuột.

Sau đó, các thiết bị cảm ứng trở nên phổ biến, đặc biệt là điện thoại và máy tính bảng. Để các kịch bản hiện có hoạt động, chúng tạo ra (và vẫn tạo ra) các sự kiện chuột. Ví dụ, chạm vào màn hình cảm ứng tạo ra mousedown. Do đó, các thiết bị cảm ứng hoạt động tốt với các trang web.
Nhưng thiết bị cảm ứng có nhiều khả năng hơn chuột. Ví dụ, có thể chạm vào nhiều điểm cùng một lúc (“multi-touch”). Mặc dù, các sự kiện chuột không có thuộc tính cần thiết để xử lý nhiều điểm chạm như vậy.

  • Do đó, các sự kiện cảm ứng đã được giới thiệu, như touchstart, touchend, touchmove, có các thuộc tính đặc biệt cho cảm ứng (chúng tôi không đề cập chi tiết ở đây, vì pointer events còn tốt hơn).

Vẫn chưa đủ, vì còn nhiều thiết bị khác, như bút, có các tính năng riêng của chúng. Ngoài ra, việc viết mã lắng nghe cả sự kiện cảm ứng và chuột là rất rườm rà.
– Để giải quyết các vấn đề này, tiêu chuẩn mới Pointer Events đã được giới thiệu. Nó cung cấp một tập hợp các sự kiện duy nhất cho tất cả các loại thiết bị chỉ định.

Hiện tại, Pointer Events Level 2 được hỗ trợ trên tất cả các trình duyệt chính, trong khi phiên bản mới hơn Pointer Events Level 3 đang trong quá trình phát triển và chủ yếu tương thích với Pointer Events Level 2.
Trừ khi bạn phát triển cho các trình duyệt cũ, chẳng hạn như Internet Explorer 10, hoặc Safari 12 trở xuống, thì không còn lý do gì để sử dụng các sự kiện chuột hoặc cảm ứng nữa — chúng ta có thể chuyển sang sử dụng pointer events.

Vậy thì mã của bạn sẽ hoạt động tốt với cả thiết bị cảm ứng và chuột.

Tuy nhiên, có một số đặc điểm quan trọng mà bạn cần biết để sử dụng Pointer Events một cách chính xác và tránh những bất ngờ. Chúng tôi sẽ lưu ý về chúng trong bài viết này.

2. Các loại sự kiện Pointer

Các sự kiện Pointer được đặt tên tương tự như các sự kiện chuột:

Pointer eventSimilar mouse event
pointerdownmousedown
pointerupmouseup
pointermovemousemove
pointerovermouseover
pointeroutmouseout
pointerentermouseenter
pointerleavemouseleave
pointercancel
gotpointercapture
lostpointercapture


Như chúng ta thấy, với mỗi sự kiện mouse<event>, có một sự kiện pointer<event> đảm nhiệm vai trò tương tự. Cũng có 3 sự kiện pointer bổ sung không có sự kiện chuột tương ứng, chúng tôi sẽ giải thích chúng ngay sau đây.
Chúng ta có thể thay thế các sự kiện mouse<event> bằng pointer<event> trong mã của mình và mong đợi mọi thứ tiếp tục hoạt động tốt với chuột.

Hỗ trợ cho các thiết bị cảm ứng cũng sẽ “tự động” cải thiện. Mặc dù, chúng ta có thể cần thêm touch-action: none ở một số nơi trong CSS. Chúng tôi sẽ đề cập đến điều đó dưới đây trong phần về pointercancel.

3. Các thuộc tính của sự kiện Pointer

Các sự kiện Pointer có cùng thuộc tính với các sự kiện chuột, chẳng hạn như clientX/Y, target, v.v., cộng với một số thuộc tính khác:
pointerId – định danh duy nhất của con trỏ gây ra sự kiện.
Được trình duyệt tạo ra. Cho phép chúng ta xử lý nhiều con trỏ, chẳng hạn như màn hình cảm ứng với bút và multi-touch (các ví dụ sẽ theo sau).
pointerType – loại thiết bị chỉ định. Phải là một chuỗi, một trong các giá trị: “mouse”, “pen” hoặc “touch”.

Chúng ta có thể sử dụng thuộc tính này để phản ứng khác nhau với các loại con trỏ khác nhau.
isPrimary – là true đối với con trỏ chính (ngón tay đầu tiên trong multi-touch).

Một số thiết bị con trỏ đo diện tích tiếp xúc và áp suất, ví dụ như đối với một ngón tay trên màn hình cảm ứng, có thêm các thuộc tính sau:
width – chiều rộng của vùng mà con trỏ (ví dụ: ngón tay) chạm vào thiết bị. Đối với các thiết bị không hỗ trợ, ví dụ như chuột, luôn là 1.
height – chiều cao của vùng mà con trỏ chạm vào thiết bị. Đối với các thiết bị không hỗ trợ, luôn là 1.
pressure – áp suất của đầu con trỏ, trong khoảng từ 0 đến 1. Đối với các thiết bị không hỗ trợ áp suất phải là 0.5 (đã nhấn) hoặc 0.
tangentialPressure – áp suất tiếp tuyến chuẩn hóa.
tiltX, tiltY, twist – các thuộc tính đặc thù của bút mô tả cách mà bút được định vị so với bề mặt.
Các thuộc tính này không được hầu hết các thiết bị hỗ trợ, vì vậy chúng hiếm khi được sử dụng. Bạn có thể tìm chi tiết về chúng trong specification nếu cần.

4. Đa điểm chạm

Một trong những điều mà sự kiện chuột hoàn toàn không hỗ trợ là đa điểm chạm: người dùng có thể chạm vào nhiều vị trí cùng lúc trên điện thoại hoặc máy tính bảng của họ, hoặc thực hiện các cử chỉ đặc biệt.
Sự kiện con trỏ cho phép xử lý đa điểm chạm với sự trợ giúp của các thuộc tính pointerIdisPrimary.

Đây là những gì xảy ra khi người dùng chạm vào màn hình cảm ứng ở một vị trí, sau đó đặt ngón tay khác ở một vị trí khác trên màn hình:

  1. Khi ngón tay đầu tiên chạm vào: – pointerdown với isPrimary=true và một số pointerId.
  2. Đối với ngón tay thứ hai và các ngón tay thêm nữa (giả sử ngón tay đầu tiên vẫn đang chạm): – pointerdown với isPrimary=false và một pointerId khác cho mỗi ngón tay.

Xin lưu ý: pointerId không được gán cho toàn bộ thiết bị, mà cho mỗi ngón tay đang chạm. Nếu chúng ta sử dụng 5 ngón tay để chạm vào màn hình cùng lúc, chúng ta sẽ có 5 sự kiện pointerdown, mỗi sự kiện với tọa độ tương ứng và một pointerId khác nhau.

Các sự kiện liên quan đến ngón tay đầu tiên luôn có isPrimary=true.

Chúng ta có thể theo dõi nhiều ngón tay đang chạm bằng cách sử dụng pointerId của chúng. Khi người dùng di chuyển và sau đó dỡ bỏ một ngón tay, chúng ta nhận được các sự kiện pointermovepointerup với cùng một pointerId như trong pointerdown.

Đây là bản demo ghi lại các sự kiện pointerdownpointerup: Demo

Xin lưu ý: bạn phải sử dụng một thiết bị màn hình cảm ứng, như điện thoại hoặc máy tính bảng, để thực sự thấy sự khác biệt trong pointerId/isPrimary. Đối với các thiết bị chạm đơn, như chuột, sẽ luôn có cùng một pointerId với isPrimary=true cho tất cả các sự kiện con trỏ.

5. Sự kiện: pointercancel

Sự kiện pointercancel xảy ra khi có một tương tác con trỏ đang diễn ra và sau đó có điều gì đó xảy ra khiến nó bị hủy bỏ, vì vậy không còn sự kiện con trỏ nào được tạo ra nữa.
Các nguyên nhân có thể là:

  • Phần cứng thiết bị con trỏ bị vô hiệu hóa về mặt vật lý.
  • Hướng thiết bị thay đổi (máy tính bảng bị xoay).
  • Trình duyệt quyết định xử lý tương tác theo cách riêng của nó, coi đó là cử chỉ chuột hoặc hành động zoom và di chuyển hoặc cái gì đó khác.

Chúng tôi sẽ minh họa pointercancel qua một ví dụ thực tế để xem nó ảnh hưởng như thế nào.
Giả sử chúng ta đang triển khai chức năng kéo và thả cho một quả bóng, giống như ở phần đầu của bài viết Drag’n’Drop with mouse events.

Dưới đây là quy trình các hành động của người dùng và các sự kiện tương ứng:

1) Người dùng nhấn vào một hình ảnh để bắt đầu kéo

– sự kiện pointerdown được kích hoạt.

2) Sau đó, họ bắt đầu di chuyển con trỏ (do đó kéo hình ảnh)

– sự kiện pointermove xảy ra, có thể nhiều lần.

3) Và rồi điều bất ngờ xảy ra! Trình duyệt có hỗ trợ kéo và thả hình ảnh sẵn có, và bắt đầu xử lý và chiếm quyền kiểm soát quá trình kéo và thả, do đó tạo ra sự kiện pointercancel.

– Trình duyệt giờ đây tự xử lý việc kéo và thả hình ảnh. Người dùng thậm chí có thể kéo hình ảnh quả bóng ra khỏi trình duyệt, vào chương trình Mail hoặc Trình quản lý Tệp.

– Không còn các sự kiện pointermove cho chúng ta nữa.
Vấn đề là trình duyệt “chiếm đoạt” tương tác: pointercancel xảy ra ngay đầu quá trình “kéo và thả”, và không còn sự kiện pointermove nào được tạo ra nữa.

Đây là bản demo kéo và thả với việc ghi lại các sự kiện con trỏ (chỉ up/down, movecancel) trong textarea:

DEMO

Chúng tôi muốn tự triển khai chức năng kéo và thả, vì vậy hãy yêu cầu trình duyệt không chiếm quyền kiểm soát.
Ngăn chặn hành động mặc định của trình duyệt để tránh pointercancel.

Chúng ta cần thực hiện hai việc:
1. Ngăn chặn việc kéo và thả mặc định xảy ra: – Chúng ta có thể làm điều này bằng cách đặt ball.ondragstart = () => false, như đã mô tả trong bài viết . – Hoạt động tốt với các sự kiện chuột.
2. Đối với các thiết bị cảm ứng, có các hành động liên quan đến cảm ứng khác của trình duyệt (ngoài kéo và thả). Để tránh gặp vấn đề với chúng:

  • Ngăn chặn chúng bằng cách đặt #ball { touch-action: none } trong CSS.
  • Sau đó, mã của chúng ta sẽ hoạt động trên các thiết bị cảm ứng.
    Sau khi thực hiện điều đó, các sự kiện sẽ hoạt động như mong đợi, trình duyệt sẽ không chiếm đoạt quá trình và không phát sinh sự kiện pointercancel.
    Bản demo này thêm các dòng mã sau: DEMO

Như bạn có thể thấy, không còn pointercancel nữa.
Giờ chúng ta có thể thêm mã để thực sự di chuyển quả bóng, và chức năng kéo và thả của chúng ta sẽ hoạt động cho cả thiết bị chuột và thiết bị cảm ứng.

6. Bắt con trỏ

Bắt con trỏ là một tính năng đặc biệt của sự kiện con trỏ.
Ý tưởng rất đơn giản, nhưng có thể trông khá lạ lùng lúc đầu, vì không có kiểu sự kiện nào khác tương tự như vậy.

Phương thức chính là:

  • elem.setPointerCapture(pointerId) — gán các sự kiện với pointerId cho elem. Sau cuộc gọi này, tất cả các sự kiện con trỏ với cùng một pointerId sẽ có elem là mục tiêu (như thể xảy ra trên elem), không quan trọng chúng thực sự xảy ra ở đâu trong tài liệu.

    Nói cách khác, elem.setPointerCapture(pointerId) sẽ thay đổi mục tiêu của tất cả các sự kiện tiếp theo với pointerId đã cho thành elem.
    Việc gán bị loại bỏ:

  • tự động khi các sự kiện pointerup hoặc pointercancel xảy ra,
  • tự động khi elem bị loại bỏ khỏi tài liệu,
  • khi gọi elem.releasePointerCapture(pointerId).

Vậy thì điều này có ích gì? Đã đến lúc xem một ví dụ thực tế.
Bắt con trỏ có thể được sử dụng để đơn giản hóa các tương tác kiểu kéo và thả.

Hãy nhớ lại cách chúng ta có thể triển khai một thanh trượt tùy chỉnh, như đã mô tả trong bài Drag’n’Drop with mouse events..
Chúng ta có thể tạo một phần tử slider để đại diện cho thanh và “cán” (thumb) bên trong nó:

<div class="slider">
  <div class="thumb"></div>
</div>

Với các kiểu dáng, nó trông như thế này:
Và đây là logic hoạt động, như đã được mô tả, sau khi thay thế các sự kiện chuột bằng các sự kiện con trỏ tương tự:
1. Người dùng nhấn vào `thumb` của thanh trượt — sự kiện `pointerdown` được kích hoạt.
2. Sau đó, người dùng di chuyển con trỏ — sự kiện `pointermove` được kích hoạt và mã của chúng ta di chuyển phần tử `thumb` theo.

Trong giải pháp dựa trên sự kiện chuột, để theo dõi tất cả các chuyển động của con trỏ, bao gồm khi nó di chuyển lên/xuống `thumb`, chúng ta phải gán trình xử lý sự kiện `mousemove` trên toàn bộ `document`.

Tuy nhiên, đó không phải là giải pháp sạch nhất. Một trong những vấn đề là khi người dùng di chuyển con trỏ xung quanh tài liệu, nó có thể kích hoạt các trình xử lý sự kiện (như `mouseover`) trên các phần tử khác, gọi các chức năng giao diện người dùng không liên quan, và chúng ta không muốn điều đó.

Đây là lúc `setPointerCapture` phát huy tác dụng.

– Chúng ta có thể gọi `thumb.setPointerCapture(event.pointerId)` trong trình xử lý sự kiện `pointerdown`,

– Sau đó, các sự kiện con trỏ trong tương lai cho đến `pointerup/cancel` sẽ được định hướng lại cho `thumb`.
– Khi `pointerup` xảy ra (kéo hoàn tất), việc gán sẽ được loại bỏ tự động, chúng ta không cần phải lo lắng về điều đó.
Vì vậy, ngay cả khi người dùng di chuyển con trỏ xung quanh toàn bộ tài liệu, các trình xử lý sự kiện sẽ được gọi trên `thumb`. Tuy nhiên, các thuộc tính tọa độ của đối tượng sự kiện, như `clientX/clientY`, vẫn sẽ chính xác – việc bắt chỉ ảnh hưởng đến `target/currentTarget`.
Đây là mã cơ bản:

thumb.onpointerdown = function(event) {
  // retarget all pointer events (until pointerup) to thumb
  thumb.setPointerCapture(event.pointerId);

  // start tracking pointer moves
  thumb.onpointermove = function(event) {
    // moving the slider: listen on the thumb, as all pointer events are retargeted to it
    let newLeft = event.clientX - slider.getBoundingClientRect().left;
    thumb.style.left = newLeft + 'px';
  };

  // on pointer up finish tracking pointer moves
  thumb.onpointerup = function(event) {
    thumb.onpointermove = null;
    thumb.onpointerup = null;
    // ...also process the "drag end" if needed
  };
};

// note: no need to call thumb.releasePointerCapture,
// it happens on pointerup automatically

Bản demo đầy đủ:
Trong bản demo, còn có một phần tử bổ sung với trình xử lý `onmouseover` hiển thị ngày hiện tại.
Xin lưu ý: trong khi bạn kéo `thumb`, bạn có thể di chuột qua phần tử này, và trình xử lý của nó *không* được kích hoạt.

Vì vậy, việc kéo giờ đây không có tác dụng phụ, nhờ vào `setPointerCapture`.

Cuối cùng, việc bắt con trỏ mang lại cho chúng ta hai lợi ích:

1. Mã trở nên sạch hơn vì chúng ta không cần phải thêm/xóa các trình xử lý trên toàn bộ `document` nữa. Việc gán được giải phóng tự động.
2. Nếu có các trình xử lý sự kiện con trỏ khác trong tài liệu, chúng sẽ không bị kích hoạt ngẫu nhiên bởi con trỏ trong khi người dùng kéo thanh trượt.

Các sự kiện bắt con trỏ

Còn một điều nữa cần đề cập, để hoàn chỉnh.
Có hai sự kiện liên quan đến việc bắt con trỏ:

– `gotpointercapture` xảy ra khi một phần tử sử dụng `setPointerCapture` để kích hoạt việc bắt con trỏ.
– `lostpointercapture` xảy ra khi việc bắt con trỏ bị giải phóng: hoặc là rõ ràng với cuộc gọi `releasePointerCapture`, hoặc tự động khi xảy ra `pointerup`/`pointercancel`.

7. Tóm tắt

Các sự kiện con trỏ cho phép xử lý các sự kiện chuột, cảm ứng và bút đồng thời, với một đoạn mã duy nhất.
Các sự kiện con trỏ mở rộng các sự kiện chuột. Chúng ta có thể thay thế `mouse` bằng `pointer` trong tên sự kiện và mong đợi mã của chúng ta tiếp tục hoạt động với chuột, với sự hỗ trợ tốt hơn cho các loại thiết bị khác.

Đối với kéo và thả và các tương tác cảm ứng phức tạp mà trình duyệt có thể quyết định chiếm quyền và xử lý theo cách riêng của nó – hãy nhớ hủy bỏ hành động mặc định trên các sự kiện và đặt `touch-action: none` trong CSS cho các phần tử mà chúng ta sử dụng.

Các khả năng bổ sung của sự kiện con trỏ là:

– Hỗ trợ đa điểm chạm sử dụng `pointerId` và `isPrimary`.
– Các thuộc tính đặc thù của thiết bị, chẳng hạn như `pressure`, `width/height`, và các thuộc tính khác.
– Bắt con trỏ: chúng ta có thể thay đổi mục tiêu của tất cả các sự kiện con trỏ đến một phần tử cụ thể cho đến khi xảy ra `pointerup`/`pointercancel`.
Tính đến hiện tại, các sự kiện con trỏ được hỗ trợ trên tất cả các trình duyệt chính, vì vậy chúng ta có thể chuyển sang sử dụng chúng một cách an toàn, đặc biệt nếu không cần các trình duyệt IE10- và Safari 12-. Và ngay cả với các trình duyệt đó, vẫn có các polyfill cho phép hỗ trợ các sự kiện con trỏ.

Như vậy, qua bài viết này, bạn đã có cái nhìn rõ hơn về các sự kiện con trỏ trong JavaScript và cách áp dụng chúng để nâng cao khả năng tương tác của ứng dụng web. Cafedev hy vọng rằng kiến thức này sẽ giúp bạn phát triển các ứng dụng mượt mà và hiệu quả hơn. Đừng quên tiếp tục theo dõi Cafedev để cập nhật thêm nhiều kiến thức JavaScript hữu ích khác trong hành trình học tập 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 dev

Các nguồn kiến thức MIỄN PHÍ VÔ GIÁ từ cafedev tại đây

Nế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:

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!