لنتعمّق في المزيد من التفاصيل حول اﻷحداث التي تقع عندما تتحرّك الفأرة بين العناصر.
اﻷحداث mouseover/mouseout و relatedTarget
يقع الحدث mouseover
عندما يأتي مؤشّر الفأرة إلى عنصر ما، ويقع mouseout
عندما يغادره.
تتميّز هذه اﻷحداث بأنّ لديها الخاصيّة relatedTarget
. تُكمِّل هذه الخاصيّةُ target
. عندما تغادر الفأرة عنصرا ما نحو عنصر آخر، يصير إحدى هذه العناصر target
، ويصير الآخر relatedTarget
.
بالنسبة للحدث mouseover
:
-
event.target
هو العنصر الذي أتت إليه الفأرة. -
event.relatedTarget
هو العنصر الذي أتت منه الفأرة (relatedTarget
-> target
).
والعكس بالنسبة للحدث mouseout
:
-
event.target
هو العنصر الذي غادرته الفأرة. -
event.relatedTarget
هو العنصر الذي غادرت نحوه الفأرة، وصار تحت المؤشّر (target
->relatedTarget
)
في المثال التالي (يمكن مشاهدته من هنا)، يشكّل كلُّ وجهٍ وتقاسيمُه عناصر منفصلة. عند تحريك الفأرة، يمكنك في المساحة النصيّة أسفله مشاهدة الأحداث التي تقع. يرافق كلَّ حدث المعلوماتُ المتعلّقة بكلٍّ من target
وrelatedTarget
.
تنبيه: يمكن أن يأخذ relatedTarget
القيمة null
يمكن للخاصيّة relatedTarget
أن تكون null
. هذا أمر عاديّ ويعني فقط أنّ الفأرة لم تأتِ من عنصر آخر، ولكن من خارج النافذة. أو أنّها قد غادرت النافذة.
تخطي العناصر
يقع الحدث mousemove
عندما تتحرّك الفأرة. ولكنّ ذلك لا يعني أنّ كلَّ بكسل يؤدّي إلى حدث. يرصد المتصفّح الفأرة من وقت لآخر. وإذا لاحظ تغيّرات، فإنّه يعمل على وقوع اﻷحداث.
هذا يعني لو حرّك المستخدم الفأرة بسرعة كبيرة فإنّ بعض عناصر DOM قد تُتخطّى:
إذا تحرّكت الفأرة بسرعة كبيرة من العنصر #FROM
إلي العنصر #TO
كما هو مبيّن أعلاه، فإنّ عناصر <div>
التي بين هذين العنصرين (أو بعضها) قد تُتخطّى. قد يقع الحدث mouseout
على #FROM
ثم مباشرة على #TO
.
يساعد هذا على تحسين اﻷداء، لأنّه قد تكون هناك الكثير من العناصر البينيّة، ولا نريد حقًّا معالجة الدخول والخروج في كلٍّ منها.
في المقابل، يجب أن ندرك دائما أن مؤشّر الفأرة لا "يزور" جميع العناصر في طريقه، بل قد "يقفز". فمثلا، من الممكن أن يقفز المؤشّر إلى وسط الصفحة مباشرة قادما من خارج الصفحة. في هذه الحالة، تكون قيمة relatedTarget
هي null
، لأنّه قد أتى من "لا مكان":
يمكنك رؤية ذلك "حيًّا" في المثال التالي أو من منصّة الاختبار التي من هنا:
يوجد هناك في HTML عنصران متداخلان: العنصر <div id="child">
موجود داخل العنصر <div id="parent">
. إذا حرّكتَ الفأرة بسرعة فوقهما، فربّما يقع الحدث على العنصر div الابن فقط، أو ربّما على اﻷب، أو ربّما لن تكون هناك أحداث مطلقا.
قم أيضا بتحريك المؤشّر إلى داخل العنصر div
الابن، ثم حرّكه بسرعة نحو اﻷسفل عبر العنصر اﻷب. إذا كانت الحركة سريعة كفاية، فسيُتجاهل العنصر اﻷب. ستعبر الفأرة العنصر اﻷب دون الانتباه له.
ملاحظة: إذا وقع mouseover
، فلابدّ أن يكون هناك mouseout
في حالة تحرّك الفأرة بسرعة، فقد تُتجاهل العناصر البينيّة، لكن هناك شيء مؤكّد: إذا دخل المؤشّر "رسميًّا" إلى عنصر ما (تولًّد الحدثُ mouseover
)، فعند مغادرته إيّاه سنحصل دائما على mouseout
.
Mouseout عند المغادرة نحو عنصر ابني
من الميزات المهمّة للحدث mouseout
هي أنّه يقع عندما يتحّرك المؤشّر من عنصرٍ ما إلى عناصره السليلة، كأن يتحرّك من #parent
إلى #child
في شيفرة HTML هذه:
<div id="parent"> <div id="child">...</div> </div>
فلو كنّا في #parent
ثم حرّكنا المؤشّر إلى داخل #child
، فسنتحصّل على mouseout
في #parent
!
قد يبدو هذا غريبا، لكن يمكن تفسيره بسهولة.
حسب منطق المتصفّح، يمكن للمؤشّر أن يكون فوق عنصر واحد فقط في نفس الوقت، وهو العنصر الذي يأتي في اﻷسفل وفق ترتيب القيمة z-index.
فإذا ذهب المؤشّر نحو عنصر آخر (ولو كان سليلًا)، فإنّه يغادر العنصر السابق.
يُرجى التنبّه إلى جرئيّة مهمّة أخرى في معالجة اﻷحداث. ينتشر الحدث mouseover
الذي يقع في عنصر سليل نحو اﻷعلى. فإذا كان للعنصر #parent
معالج للحدث mouseover
فإنّه سيشتغل:
يمكنك رؤية ذلك جيّدا في المثال الذي من هنا. العنصر <div id="child">
هو داخل العنصر <div id="parent">
. هناك معالجات لـ mouseover/out
مسندة إلى العنصر #parent
تعمل على إخراج تفاصيل الحدث.
فإذا حرّكتَ الفأرةَ من #parent
نحو #child
، ستلاحظ وقوع حدثين في #parent
:
-
mouseout [target: parent]
(مغادرة المؤشّر للعنصر اﻷب)، ثم -
mouseover [target: child]
(مجيء المؤشّر إلى العنصر الابن، انتشر هذا الحدث نحو اﻷعلى).
كما هو ظاهر، عندما يتحرّك المؤشّر من العنصر #parent
نحو العنصر #child
، يشتغل معالجان في العنصر اﻷب: mouseout
وmouseover
:
parent.onmouseout = function(event) { /* العنصر اﻷب :event.target */ }; parent.onmouseover = function(event) { /* العنصر الابن (انتشر نحو اﻷعلى) :event.target */ };
إذا لم نفحص event.target
داخل المعالجات، فقد يبدو اﻷمر وكأنّ مؤشّر الفأرة قد غادر العنصر `#parent ثم عاد إليه مباشرة.
لكن ليس اﻷمر كذلك! لا يزال المؤشر فوق العنصر اﻷب، وقد تحرّك فقط إلى داخل العنصر الابن.
إذا كانت هناك أفعال تحصل بمغادرة العنصر اﻷبويّ مثل إجراء حركات في parent.onmouseout
، فلا نودّ حصولها عادة بمجرّد انتقال المؤشّر في داخل #parent
. لتجنّب ذلك، يمكننا تفحّص relatedTarget
في المعالج، وإذا كان المؤشّر لا يزال داخل العنصر، فإنّنا نتجاهل الحدث. أو بدلًا عن ذلك، يمكننا استخدام اﻷحداث mouseenter
وmouseleave
التي سنتناولها الآن، إذ ليس لها مثل هذه المشاكل.
اﻷحداث mouseenter وmouseleave
الأحداث mouseenter/mouseleave
مثل الأحداث mouseover/mouseout
، فهي تقع عندما يدخل/يغادر مؤشّر الفأرة العنصر. لكن هناك فرقان مهمّان:
- لا تُحتسب الانتقالات التي تحدث داخل العنصر، من وإلى عناصره السليلة.
-
لا تنتشر الأحداث
mouseenter/mouseleave
نحو الأعلى.
هذه الأحداث بسيطة للغاية. عندما يدخل المؤشّر العنصر، يقع mouseenter
، ولا يهمّ مكانه داخل العنصر أو داخل عناصره السليلة بالضبط. ثمّ عندما يغادر المؤشّر العنصر، يقع mouseleave
.
يشبه المثال الذي يمكن مشاهدته من هنا المثال أعلاه، لكن العنصر العلويّ الآن لديه mouseenter/mouseleave
بدل mouseover/mouseout
.
مثلما يمكن أن ترى، الأحداث التي تولّدت هي الأحداث الناتجة عن تحريك المؤشّر إلى داخل وخارج العنصر العلويّ فقط. لا شيء يحدث عندما يذهب المؤشر إلى العنصر الابن ويرجع. تُتجاهل الانتقالات بين العناصر السليلة.
تفويض الأحداث
الأحداث mouseenter/leave
بسيطة للغاية وسهلة الاستخدام. لكنّها لا تنتشر نحو الأعلى. بالتالي لا يمكننا استخدام تفويض الأحداث معها.
تصوّر أنّنا نريد معالجة دخول/مغادرة الفأرة لخانات جدول ما. وهناك المئات من الخانات.
بكون الحلّ الطبيعيّ عادةً بإسناد المعالج إلى <table>
ومعالجة الأحداث هناك. لكنّ mouseenter/leave
لا تنتشر نحو الأعلى. فإذا وقعت الأحداث في <td>
، فلا يمكن التقاطها إلّا بواسطة معالج مُسنَد إلى ذلك العنصر <td>
.
تشتغل المعالجات المسندة إلى <table>
فقط عندما يدخل/يغادر المؤشّر الجدول ككلّ. يستحيل الحصول على أيّة معلومات حول الانتقالات داخله.
لذا، فلنستخدم mouseover/mouseout
. لنبدأ بمعالجات بسيطة تعمل على إبراز العنصر الذي تحت المؤشّر.
// لنبرز العنصر الذي تحت المؤشّر table.onmouseover = function(event) { let target = event.target; target.style.background = 'pink'; }; table.onmouseout = function(event) { let target = event.target; target.style.background = ''; };
يمكنك مشاهدتها تعمل من هنا. كلّما تنقّلت الفأرة عبر العناصر في هذا الجدول، سيكون العنصر الحالي بارزا على الدوام:
في حالتنا هذه نودّ أن نعالج الانتقالات بين خانات الجدول <td>
، أي عند دخول خانة أو مغادرتها. لا تهمّنا الانتقالات الأخرى كالتي تحصل داخل الخانة أو خارج كلّ الخانات. لنُنحّيها جانبا.
هذا ما يمكننا فعله:
-
حفظ العنصر
<td>
المُبرَز حاليّا في متغيّر، ولنسمّيهcurrentElem
. -
تجاهل الحدث
mouseover
عند وقوعه في داخل العنصر<td>
الحالي.
هذا مثال لشيفرة تأخذ جميع الحالات الممكنة في الحسبان:
// التي تحت الفأرة الآن (إن وُجدت) <td> let currentElem = null; table.onmouseover = function(event) { // قبل دخول عنصر جديد، تغادر الفأرة دائما العنصر السابق // السابق <td> نفسه، فإنّنا لم نغادر currentElem إذا بقي العنصر // بداخله، تجاهل الحدث mouseover هذا مجرّد if (currentElem) return; let target = event.target.closest('td'); // تجاهل - <td> تحرّكنا إلى غير if (!target) return; // لكن خارج جدولنا (يمكن ذلك في حالة الجداول المتداخلة) <td> تحرّكنا داخل // تجاهل if (!table.contains(target)) return; // جديد <td> مرحا! دخلنا currentElem = target; onEnter(currentElem); }; table.onmouseout = function(event) { // الآن، تجاهل الحدث <td> إذا كنّا خارج أيّ // <td> يُحتمل أنّ هذا تحرّك داخل الجدول، ولكن خارج // آخر <tr> إلى <tr> مثلا من if (!currentElem) return; // نحن بصدد مغادرة العنصر – إلى أين؟ ربمّا إلى عنصر سليل؟ let relatedTarget = event.relatedTarget; while (relatedTarget) { // currentElem اصعد مع سلسلة الآباء وتحقّق إذا كنّا لا نزال داخل // فهو إذًا انتقال داخليّ – تجاهله if (relatedTarget == currentElem) return; relatedTarget = relatedTarget.parentNode; } // .حقًّا .<td> لقد غادرنا العنصر onLeave(currentElem); currentElem = null; }; // أيّة دالّة لمعالجة دخول/مغادرة العنصر function onEnter(elem) { elem.style.background = 'pink'; // أظهر ذلك في المساحة النصيّة text.value += `over -> ${currentElem.tagName}.${currentElem.className}\n`; text.scrollTop = 1e6; } function onLeave(elem) { elem.style.background = ''; // أظهر ذلك في المساحة النصيّة text.value += `out <- ${elem.tagName}.${elem.className}\n`; text.scrollTop = 1e6; }
مرّة أخرى، أهمّ ما يميّز هذه الطريقة هو التالي:
-
تستخدم تفويض الأحداث لمعالجة دخول/مغادرة أيّ
<td>
داخل الجدول. فهي تعتمد علىmouseover/out
بدلmouseenter/leave
التي لا تنتشر نحو الأعلى وبذلك لا تسمح بتفويض الأحداث. -
تُنحّى الأحداث الأخرى، مثل التنقّل بين العناصر السليلة لـ
<td>
، جانبًا لكيلا تشتغل المعالجاتonEnter/Leave
إلّا عندما يدخل أو يغادر المؤشّر<td>
ككلّ.
يمكنك مشاهدة المثال كاملا بجميع التفاصيل من هنا:
جرّب تحريك المؤشّر إلى داخل وخارج خانات الجدول وكذلك بداخلها. بسرعة أو ببطء، لا يهمّ ذلك. لا تُبرَز إلّا <td>
ككلّ، بخلاف المثال السابق.
الملخص
تناولنا الأحداث mouseover
وmouseout
وmousemove
وmouseenter
وmouseleave
.
من الجيّد التنبّه لهذه الأمور:
- قد تتخطّى الحركةُ السريعة للفأرة العناصر البينيّة.
-
تملك الأحداث
mouseover/out
وmouseenter/leave
الخاصيّة الإضافيّةrelatedTarget
، وهي تمثّل العنصر الذي أتينا منه/إليه، وتكمّل الخاصيّةtarget
.
تقع الأحداث mouseover/out
حتى لو ذهبنا من عنصر أبويّ إلى عنصر ابنيّ. يَفترِض المتصفّحُ أنّه يمكن للفأرة أن تكون فوق عنصر واحد فقط في نفس الوقت، وهو العنصر الذي في الأسفل.
تختلف الأحداث mouseenter/leave
في هذا الجانب، فهي تقع فقط عندما تأتي الفأرة من/إلى العنصر ككلّ. إضافة إلى ذلك، هي لا تنتشر نحو الأعلى.
التمارين
سلوك التلميحة المحسَّن
الأهميّة: 5
اكتب شيفرة جافاسكربت لإظهار تلميحة فوق العنصر الذي له السمة data-tooltip
. يجب أن تصير قيمة هذه السمة نصّ التلميحة.
يشبه هذا التمرين سلوك التلميحة، لكن يمكن هنا أن تكون العناصر الموسّمة متداخلة، ويحب حينها أن تُعرض التلميحة التي تكون في الأسفل. ويمكن أن تظهر تلميحة واحدة فقط في نفس الوقت. على سبيل المثال:
<div data-tooltip="Here – is the house interior" id="house"> <div data-tooltip="Here – is the roof" id="roof"></div> ... <a href="https://en.wikipedia.org/wiki/The_Three_Little_Pigs" data-tooltip="Read on…">Hover over me</a> </div>
يمكن مشاهدة النتيجة من هنا.
أنجز التمرين في البيئة التجريبية
الحل
تلميحة "ذكيّة"
الأهمية: 5
اكتب دالّة لإظهار تلميحة فوق عنصر ما فقط عندما يحرّك المستخدم الفأرة إلى داخل العنصر وليس عبره.
بعبارة أخرى، إذا حرّك المستخدم الفأرة إلى عنصر ما وتوقّفت هناك، أظهر التلميحة. وإذا حرّكها مرورا به فقط، فلا داعي لذلك، إذ لا أحد يريد ومضات زائدة.
فنيّا، يمكننا قياس سرعة الفأرة فوق العنصر، فإذا كانت بطيئة يمككنا افتراض أنّها أتت "فوق العنصر" وإظهار التلميحة. وإذا كانت سريعة تجاهلناها.
أنشئ كائنا عموميّا new HoverIntent(options)
لهذا الغرض، له الخيارات options
التالية:
-
elem
هو العنصر الذي نتتبّعه. -
over
هي الدالّة التي نستدعيها إذا أتت الفأرة إلى العنصر: أي إذا تحرّكت ببطء أو توقّفت فوقه. -
out
هي الدالّة التي نستدعيها إذا غادرت الفأرة العنصر (في حال استُدعيتover
).
هذا مثال لاستخدام هذا الكائن للتلميحة:
// تلميحة تجريبيّة let tooltip = document.createElement('div'); tooltip.className = "tooltip"; tooltip.innerHTML = "Tooltip"; // over/out سيتتبّع الكائن الفأرة ويستدعي new HoverIntent({ elem, over() { tooltip.style.left = elem.getBoundingClientRect().left + 'px'; tooltip.style.top = elem.getBoundingClientRect().bottom + 5 + 'px'; document.body.append(tooltip); }, out() { tooltip.remove(); } });
يمكن مشاهدة النتيجة من هنا. إذا حرّكت الفأرة فوق "الساعة" بسرعة فلن يحصل شيء، وإذا حرّكتها ببطء أو توقّفت عندها، فستكون هناك تلميحة.
يُرجى التنبّه: لا "تومض" التلميحة عندما يتحرّك المؤشّر بين العناصر الفرعيّة للساعة.
أنجز التمرين في البيئة التجريبية.
الحل
تبدو الخوارزمية بسيطة:
-
أسند معالجات
onmouseover/out
إلى العنصر. يمكن أيضا استخدامonmouseenter/leave
هنا، لكنّها أقلّ عموما، ولن تعمل إذا أدخلنا التفويض. -
إذا دخل المؤشّر العنصر، ابدأ بقياس السرعة في
mousemove
. -
إذا كانت السرعة صغيرة، شغّل
over
. -
عند الذهاب إلى خارج العنصر وقد نُفّذت
over
، شغّلout
.
لكن كيف تُقاس السرعة؟
قد تكون أوّل فكرة: شغّل دالّة كلّ 100ms
وقس المسافة بين الإحداثيات السابقة والجديدة. إذا كانت المسافة صغيرة، فإنّ السرعة صغيرة.
للأسف، لا سبيل للحصول على"الإحداثيّات الحاليّة للفأرة" في جافاسكربت. لا وجود لدالّة مثل getCurrentMouseCoordinates()
.
السبيل الوحيد للحصول على الإحداثيّات هو الإنصات لأحداث الفأرة، مثل mousemove
وأخذ الإحداثيّات من كائن الحدث.
لنضع إذًا معالجًا للحدث mousemove
، لتتبّع الإحداثيّات وتذكّرها. ثمّ لمقارنتها مرّة كلّ 100ms
.
ملاحظة: تَستخدم اختباراتُ الحلّ dispatchEvent
لرؤية ما إذا كانت التلميحة تعمل جيّدا.
شاهد الحل في البيئة التجريبية.
ترجمة -وبتصرف- للمقال Moving the mouse: mouseover/out, mouseenter/leave من سلسلة Browser: Document, Events, Interfaces لصاحبها Ilya Kantor
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.