Trên trang Cafedev, chúng ta sẽ khám phá cách JavaScript xử lý các sự kiện liên quan đến việc di chuyển chuột như `mouseover`, `mouseout`, `mouseenter`, và `mouseleave`. Những sự kiện này cho phép bạn theo dõi và phản hồi hành vi của người dùng khi chuột di chuyển qua hoặc ra khỏi các phần tử trên trang. Bằng cách nắm vững những sự kiện này, bạn có thể tạo ra các hiệu ứng tương tác mượt mà và tinh tế, nâng cao trải nghiệm người dùng trên trang web của bạn.
Hãy đi sâu vào chi tiết về các sự kiện xảy ra khi chuột di chuyển giữa các phần tử.
Nội dung chính
1. Sự kiện mouseover/mouseout, relatedTarget
Sự kiện mouseover
xảy ra khi con trỏ chuột di chuyển qua một phần tử, và mouseout
— khi chuột rời khỏi phần tử đó.
Những sự kiện này đặc biệt vì chúng có thuộc tính relatedTarget
. Thuộc tính này bổ sung cho target
. Khi chuột rời một phần tử để đến phần tử khác, một trong số chúng trở thành target
, và phần tử còn lại – relatedTarget
.
Đối với mouseover
:
event.target
— là phần tử mà chuột đã di chuyển qua.event.relatedTarget
— là phần tử từ đó chuột đã di chuyển đến (relatedTarget
->target
).
Đối với mouseout
, điều ngược lại:
event.target
— là phần tử mà chuột đã rời khỏi.event.relatedTarget
— là phần tử mới dưới con trỏ chuột, nơi chuột đã rời đi (target
->relatedTarget
).
Trong ví dụ dưới đây, mỗi mặt và các đặc điểm của nó là các phần tử riêng biệt. Khi bạn di chuyển chuột, bạn có thể thấy các sự kiện chuột trong khu vực văn bản.
Mỗi sự kiện đều có thông tin về cả target
và relatedTarget
:
Thuộc tính relatedTarget
có thể là null
.
Điều đó là bình thường và chỉ có nghĩa là chuột không đến từ một phần tử khác mà từ ngoài cửa sổ. Hoặc là chuột đã rời khỏi cửa sổ.
Chúng ta nên giữ khả năng này trong tâm trí khi sử dụng event.relatedTarget
trong mã của mình. Nếu chúng ta truy cập event.relatedTarget.tagName
, sẽ có lỗi xảy ra.
2. Bỏ qua các phần tử
Sự kiện mousemove
xảy ra khi chuột di chuyển. Nhưng điều đó không có nghĩa là mỗi pixel di chuyển đều dẫn đến một sự kiện.
Trình duyệt kiểm tra vị trí chuột từ thời gian này sang thời gian khác. Và nếu nó phát hiện thấy sự thay đổi thì sẽ kích hoạt các sự kiện.
Điều đó có nghĩa là nếu người dùng di chuyển chuột rất nhanh thì một số phần tử DOM có thể bị bỏ qua:
Nếu chuột di chuyển rất nhanh từ phần tử #FROM đến phần tử #TO như trong hình minh họa ở trên, thì các phần tử <div>
trung gian (hoặc một số trong chúng) có thể bị bỏ qua. Sự kiện mouseout
có thể kích hoạt trên #FROM và sau đó ngay lập tức mouseover
trên #TO.
Điều này tốt cho hiệu suất, vì có thể có nhiều phần tử trung gian. Chúng ta không thực sự muốn xử lý sự kiện vào và ra của từng phần tử.
Mặt khác, chúng ta nên lưu ý rằng con trỏ chuột không “thăm” tất cả các phần tử trên đường đi. Nó có thể “nhảy” qua.
Cụ thể, có thể con trỏ nhảy ngay vào giữa trang từ ngoài cửa sổ. Trong trường hợp đó, relatedTarget
là null
, vì nó đến từ “không đâu cả”:
Bạn có thể kiểm tra “trực tiếp” trên bảng thử nghiệm dưới đây.
HTML có hai phần tử lồng nhau: <div id="child">
nằm bên trong <div id="parent">
. Nếu di chuyển chuột nhanh qua các phần tử này, thì có thể chỉ phần tử con div
kích hoạt sự kiện, hoặc có thể chỉ phần tử cha, hoặc có thể sẽ không có sự kiện nào được kích hoạt.
Cũng hãy di chuyển con trỏ vào div
con, rồi nhanh chóng di chuyển ra ngoài qua phần tử cha. Nếu di chuyển đủ nhanh, thì phần tử cha sẽ bị bỏ qua. Chuột sẽ đi qua phần tử cha mà không nhận ra.
Trong trường hợp di chuyển chuột nhanh, các phần tử trung gian có thể bị bỏ qua, nhưng có một điều chắc chắn: nếu con trỏ “chính thức” vào một phần tử (mouseover
được kích hoạt), thì khi rời khỏi nó, chúng ta luôn nhận được mouseout
.
3. Mouseout khi rời sang phần tử con
Một đặc điểm quan trọng của mouseout
— nó được kích hoạt khi con trỏ di chuyển từ một phần tử sang phần tử con của nó, ví dụ từ #parent
đến #child
trong HTML này:
<div id="parent">
<div id="child">...</div>
</div>
Nếu chúng ta ở trên #parent
và sau đó di chuyển con trỏ sâu vào #child
, chúng ta sẽ nhận được sự kiện mouseout
trên #parent
!
Điều này có thể có vẻ lạ, nhưng có thể dễ dàng giải thích.
Theo logic của trình duyệt, con trỏ chuột chỉ có thể nằm trên một phần tử duy nhất tại một thời điểm — phần tử lồng ghép nhất và nằm trên cùng theo chỉ số z.
Vì vậy, nếu con trỏ chuột di chuyển đến một phần tử khác (ngay cả phần tử con), thì nó sẽ rời khỏi phần tử trước đó.
Hãy lưu ý một chi tiết quan trọng khác của việc xử lý sự kiện.
Sự kiện mouseover
trên một phần tử con sẽ nổi bọt lên. Vì vậy, nếu #parent
có trình xử lý mouseover
, nó sẽ được kích hoạt:
Nếu bạn di chuyển chuột từ #parent
đến #child
, bạn sẽ thấy hai sự kiện trên #parent
:
1. mouseout [target: parent]
(rời khỏi phần tử cha), sau đó
2. mouseover [target: child]
(đi đến phần tử con, nổi bọt lên).
Như được hiển thị, khi con trỏ di chuyển từ phần tử #parent
đến #child
, hai trình xử lý sẽ được kích hoạt trên phần tử cha: mouseout
và mouseover
:
parent.onmouseout = function(event) {
/* event.target: parent element */
};
parent.onmouseover = function(event) {
/* event.target: child element (bubbled) */
};
Nếu chúng ta không kiểm tra event.target
bên trong các trình xử lý, thì có thể có vẻ như con trỏ chuột đã rời khỏi phần tử #parent
, và sau đó ngay lập tức quay trở lại trên nó.
Nhưng đó không phải là trường hợp! Con trỏ vẫn ở trên phần tử cha, chỉ là nó di chuyển sâu hơn vào phần tử con.
Nếu có một số hành động khi rời khỏi phần tử cha, ví dụ như một hoạt ảnh chạy trong parent.onmouseout
, chúng ta thường không muốn điều đó khi con trỏ chỉ đi sâu hơn vào #parent
.
Để tránh điều đó, chúng ta có thể kiểm tra relatedTarget
trong trình xử lý và, nếu chuột vẫn ở bên trong phần tử, thì bỏ qua sự kiện như vậy.
Hoặc chúng ta có thể sử dụng các sự kiện khác: mouseenter
và mouseleave
, mà chúng ta sẽ đề cập ngay bây giờ, vì chúng không gặp phải các vấn đề như vậy.
4. Các sự kiện mouseenter và mouseleave
Các sự kiện mouseenter/mouseleave
giống như mouseover/mouseout
. Chúng được kích hoạt khi con trỏ chuột vào/xuất ra khỏi phần tử.
Nhưng có hai khác biệt quan trọng:
- Các chuyển tiếp bên trong phần tử, đến/từ các phần tử con, không được tính.
- Các sự kiện
mouseenter/mouseleave
không nổi bọt.
Các sự kiện này cực kỳ đơn giản.
Khi con trỏ vào một phần tử — mouseenter
sẽ được kích hoạt. Vị trí chính xác của con trỏ bên trong phần tử hoặc các phần tử con của nó không quan trọng.
Khi con trỏ rời khỏi một phần tử — mouseleave
sẽ được kích hoạt.
Ví dụ này tương tự như ví dụ trên, nhưng giờ đây phần tử trên cùng có mouseenter/mouseleave
thay vì mouseover/mouseout
.
Như bạn có thể thấy, các sự kiện được sinh ra chỉ liên quan đến việc di chuyển con trỏ vào và ra khỏi phần tử trên cùng. Không có gì xảy ra khi con trỏ di chuyển vào phần tử con và quay lại. Các chuyển tiếp giữa các phần tử con bị bỏ qua.
5. Phân phối sự kiện
Các sự kiện mouseenter/leave
rất đơn giản và dễ sử dụng. Nhưng chúng không nổi bọt. Vì vậy, chúng ta không thể sử dụng phân phối sự kiện với chúng.
Giả sử chúng ta muốn xử lý sự kiện con chuột vào/ra khỏi các ô trong bảng. Và có hàng trăm ô.
Giải pháp tự nhiên là — đặt trình xử lý trên <table>
và xử lý các sự kiện ở đó. Nhưng mouseenter/leave
không nổi bọt. Vì vậy, nếu sự kiện xảy ra trên <td>
, thì chỉ có trình xử lý trên <td>
mới có thể bắt được sự kiện đó.
Các trình xử lý cho mouseenter/leave
trên <table>
chỉ được kích hoạt khi con trỏ vào/ra khỏi toàn bộ bảng. Không thể lấy thông tin về các chuyển tiếp bên trong bảng.
Vì vậy, hãy sử dụng mouseover/mouseout
.
Bắt đầu với các trình xử lý đơn giản làm nổi bật phần tử dưới chuột:
// let's highlight an element under the pointer
table.onmouseover = function(event) {
let target = event.target;
target.style.background = 'pink';
};
table.onmouseout = function(event) {
let target = event.target;
target.style.background = '';
};
Đây là chúng trong hành động. Khi chuột di chuyển qua các phần tử của bảng này, phần tử hiện tại được làm nổi bật:
Trong trường hợp của chúng tôi, chúng tôi muốn xử lý các chuyển tiếp giữa các ô trong bảng <td>
: vào một ô và rời khỏi nó. Các chuyển tiếp khác, chẳng hạn như bên trong ô hoặc ra ngoài bất kỳ ô nào, không quan tâm với chúng tôi. Hãy lọc chúng ra.
Đây là những gì chúng ta có thể làm:
- Ghi nhớ phần tử
<td>
hiện tại đang được làm nổi bật trong một biến, gọi làcurrentElem
. - Trong sự kiện
mouseover
— bỏ qua sự kiện nếu chúng ta vẫn ở bên trong<td>
hiện tại. - Trong sự kiện
mouseout
— bỏ qua nếu chúng ta chưa rời khỏi<td>
hiện tại.
Đây là ví dụ mã code tính đến tất cả các tình huống có thể xảy ra:
// <td> under the mouse right now (if any)
let currentElem = null;
table.onmouseover = function(event) {
// before entering a new element, the mouse always leaves the previous one
// if currentElem is set, we didn't leave the previous <td>,
// that's a mouseover inside it, ignore the event
if (currentElem) return;
let target = event.target.closest('td');
// we moved not into a <td> - ignore
if (!target) return;
// moved into <td>, but outside of our table (possible in case of nested tables)
// ignore
if (!table.contains(target)) return;
// hooray! we entered a new <td>
currentElem = target;
onEnter(currentElem);
};
table.onmouseout = function(event) {
// if we're outside of any <td> now, then ignore the event
// that's probably a move inside the table, but out of <td>,
// e.g. from <tr> to another <tr>
if (!currentElem) return;
// we're leaving the element – where to? Maybe to a descendant?
let relatedTarget = event.relatedTarget;
while (relatedTarget) {
// go up the parent chain and check – if we're still inside currentElem
// then that's an internal transition – ignore it
if (relatedTarget == currentElem) return;
relatedTarget = relatedTarget.parentNode;
}
// we left the <td>. really.
onLeave(currentElem);
currentElem = null;
};
// any functions to handle entering/leaving an element
function onEnter(elem) {
elem.style.background = 'pink';
// show that in textarea
text.value += `over -> ${currentElem.tagName}.${currentElem.className}\n`;
text.scrollTop = 1e6;
}
function onLeave(elem) {
elem.style.background = '';
// show that in textarea
text.value += `out <- ${elem.tagName}.${elem.className}\n`;
text.scrollTop = 1e6;
}
Một lần nữa, các tính năng quan trọng là:
- Nó sử dụng phân phối sự kiện để xử lý việc vào/ra của bất kỳ
<td>
nào bên trong bảng. Vì vậy, nó dựa vàomouseover/out
thay vìmouseenter/leave
không nổi bọt và do đó không cho phép phân phối. - Các sự kiện phụ, chẳng hạn như di chuyển giữa các phần tử con của
<td>
, được lọc ra, đểonEnter/Leave
chỉ chạy khi con trỏ rời khỏi hoặc vào<td>
như một tổng thể.
Đây là ví dụ đầy đủ với tất cả các chi tiết:
Hãy thử di chuyển con trỏ vào và ra khỏi các ô trong bảng và bên trong chúng. Nhanh hay chậm — không quan trọng. Chỉ có <td>
như một tổng thể được làm nổi bật, khác với ví dụ trước.
6. Tóm tắt
Chúng ta đã đề cập đến các sự kiện mouseover
, mouseout
, mousemove
, mouseenter
và mouseleave
.
Các điểm cần lưu ý:
- Một chuyển động chuột nhanh có thể bỏ qua các phần tử trung gian.
- Các sự kiện
mouseover/out
vàmouseenter/leave
có thuộc tính bổ sung:relatedTarget
. Đây là phần tử mà chúng ta đến từ hoặc đến, bổ sung chotarget
.
Các sự kiệnmouseover/out
được kích hoạt ngay cả khi chúng ta đi từ phần tử cha sang phần tử con. Trình duyệt giả định rằng chuột chỉ có thể ở một phần tử tại một thời điểm — phần tử sâu nhất.
Các sự kiệnmouseenter/leave
khác ở điểm này: chúng chỉ được kích hoạt khi chuột vào và ra khỏi phần tử như một tổng thể. Đồng thời, chúng không nổi bọt.
Hy vọng rằng qua bài viết trên, bạn đã có cái nhìn rõ hơn về các sự kiện chuột trong JavaScript như `mouseover`, `mouseout`, `mouseenter`, và `mouseleave`. Những sự kiện này cung cấp những công cụ mạnh mẽ để tạo ra các tương tác mượt mà và hiệu quả. Cafedev luôn đồng hành cùng bạn trong hành trình học lập trình. Tiếp tục khám phá và áp dụng kiến thức vào các dự án của bạn để nâng cao kỹ năng và trải nghiệm người dùng trên trang web 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!