اذهب إلى المحتوى

فهم الأحداث المتعلقة بتحريك الفأرة والتعامل معها في جافاسكربت


محمد أمين بوقرة

لنتعمّق في المزيد من التفاصيل حول اﻷحداث التي تقع عندما تتحرّك الفأرة بين العناصر.

اﻷحداث mouseover/mouseout و relatedTarget

يقع الحدث mouseover عندما يأتي مؤشّر الفأرة إلى عنصر ما، ويقع mouseout عندما يغادره.

mouseover-mouseout.png

تتميّز هذه اﻷحداث بأنّ لديها الخاصيّة 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 قد تُتخطّى:

mouseover-mouseout-over-elems.png

إذا تحرّكت الفأرة بسرعة كبيرة من العنصر ‎#FROM إلي العنصر ‎#TO كما هو مبيّن أعلاه، فإنّ عناصر <div> التي بين هذين العنصرين (أو بعضها) قد تُتخطّى. قد يقع الحدث mouseout على ‎#FROM ثم مباشرة على ‎#TO.

يساعد هذا على تحسين اﻷداء، لأنّه قد تكون هناك الكثير من العناصر البينيّة، ولا نريد حقًّا معالجة الدخول والخروج في كلٍّ منها.

في المقابل، يجب أن ندرك دائما أن مؤشّر الفأرة لا "يزور" جميع العناصر في طريقه، بل قد "يقفز". فمثلا، من الممكن أن يقفز المؤشّر إلى وسط الصفحة مباشرة قادما من خارج الصفحة. في هذه الحالة، تكون قيمة relatedTarget هي null، لأنّه قد أتى من "لا مكان":

mouseover-mouseout-from-outside.png

يمكنك رؤية ذلك "حيًّا" في المثال التالي أو من منصّة الاختبار التي من هنا:

يوجد هناك في 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!

mouseover-to-child.png

قد يبدو هذا غريبا، لكن يمكن تفسيره بسهولة.

حسب منطق المتصفّح، يمكن للمؤشّر أن يكون فوق عنصر واحد فقط في نفس الوقت، وهو العنصر الذي يأتي في اﻷسفل وفق ترتيب القيمة z-index.

فإذا ذهب المؤشّر نحو عنصر آخر (ولو كان سليلًا)، فإنّه يغادر العنصر السابق.

يُرجى التنبّه إلى جرئيّة مهمّة أخرى في معالجة اﻷحداث. ينتشر الحدث mouseover الذي يقع في عنصر سليل نحو اﻷعلى. فإذا كان للعنصر ‎#parent معالج للحدث mouseover فإنّه سيشتغل:

mouseover-bubble-nested.png

يمكنك رؤية ذلك جيّدا في المثال الذي من هنا. العنصر <div id="child"‎> هو داخل العنصر <div id="parent"‎>. هناك معالجات لـ mouseover/out مسندة إلى العنصر ‎#parent تعمل على إخراج تفاصيل الحدث.

فإذا حرّكتَ الفأرةَ من ‎#parent نحو ‎#child، ستلاحظ وقوع حدثين في ‎#parent:

  1. mouseout [target: parent]‎ (مغادرة المؤشّر للعنصر اﻷب)، ثم
  2. 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، فهي تقع عندما يدخل/يغادر مؤشّر الفأرة العنصر. لكن هناك فرقان مهمّان:

  1. لا تُحتسب الانتقالات التي تحدث داخل العنصر، من وإلى عناصره السليلة.
  2. لا تنتشر الأحداث 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;
}

مرّة أخرى، أهمّ ما يميّز هذه الطريقة هو التالي:

  1. تستخدم تفويض الأحداث لمعالجة دخول/مغادرة أيّ <td> داخل الجدول. فهي تعتمد على mouseover/out بدل mouseenter/leave التي لا تنتشر نحو الأعلى وبذلك لا تسمح بتفويض الأحداث.
  2. تُنحّى الأحداث الأخرى، مثل التنقّل بين العناصر السليلة لـ <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();
  }
});

يمكن مشاهدة النتيجة من هنا. إذا حرّكت الفأرة فوق "الساعة" بسرعة فلن يحصل شيء، وإذا حرّكتها ببطء أو توقّفت عندها، فستكون هناك تلميحة.

يُرجى التنبّه: لا "تومض" التلميحة عندما يتحرّك المؤشّر بين العناصر الفرعيّة للساعة.

أنجز التمرين في البيئة التجريبية.

الحل

تبدو الخوارزمية بسيطة:

  1. أسند معالجات onmouseover/out إلى العنصر. يمكن أيضا استخدام onmouseenter/leave هنا، لكنّها أقلّ عموما، ولن تعمل إذا أدخلنا التفويض.
  2. إذا دخل المؤشّر العنصر، ابدأ بقياس السرعة في mousemove.
  3. إذا كانت السرعة صغيرة، شغّل over.
  4. عند الذهاب إلى خارج العنصر وقد نُفّذت over، شغّل out.

لكن كيف تُقاس السرعة؟

قد تكون أوّل فكرة: شغّل دالّة كلّ 100ms وقس المسافة بين الإحداثيات السابقة والجديدة. إذا كانت المسافة صغيرة، فإنّ السرعة صغيرة.

للأسف، لا سبيل للحصول على"الإحداثيّات الحاليّة للفأرة" في جافاسكربت. لا وجود لدالّة مثل getCurrentMouseCoordinates()‎.

السبيل الوحيد للحصول على الإحداثيّات هو الإنصات لأحداث الفأرة، مثل mousemove وأخذ الإحداثيّات من كائن الحدث.

لنضع إذًا معالجًا للحدث mousemove، لتتبّع الإحداثيّات وتذكّرها. ثمّ لمقارنتها مرّة كلّ 100ms.

ملاحظة: تَستخدم اختباراتُ الحلّ dispatchEvent لرؤية ما إذا كانت التلميحة تعمل جيّدا.

شاهد الحل في البيئة التجريبية.

ترجمة -وبتصرف- للمقال Moving the mouse: mouseover/out, mouseenter/leave من سلسلة Browser: Document, Events, Interfaces لصاحبها Ilya Kantor


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...