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

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


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

تُعدّ أحداث المؤشّر طريقة حديثة للتعامل مع المُدخلات النابعة من مختلف أجهزة التأشير، كالفأرة والقلم والشاشات اللمسيّة وغيرها.

لمحة تاريخية مختصرة

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

  • في القديم، لم تكن هناك سوى أحداث الفأرة. ثمّ انتشرت اﻷجهزة اللمسيّة، مثل الهواتف واﻷجهزة اللوحيّة. ولكي تعمل النصوص البرمجيّة القائمة، كانت هذه اﻷجهزة (ولا زالت) تولّد أحداث الفأرة. فعلى سبيل المثال، يولّد الضغط على شاشة لمسيّة الحدث mousedown. فكانت الأجهزة اللمسيّة بذلك تعمل جيّدا مع صفحات الويب.

    لكنّ اﻷجهزة اللمسيّة لها قدرات أكثر من الفأرة. فمن الممكن مثلا لمس عدّة نقاط في نفس الوقت ("تعدّد اللمسات"). مع ذلك، لا تملك أحداث الفأرة الخاصّيّات اللازمة للتعامل مع مثل هذه اللمسات المتعدّدة.

  • ولذلك السبب أُدرجت أحداث اللمس، مثل touchstart وtouchend وtouchmove، التي لها خاصيّات تتعلّق باللمس (لن نتطرّق إليها بالتفصيل هنا، ﻷنّ أحداث المؤشّر أفضل منها).

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

لحلّ هذه المشاكل، أُدرجت مواصفة أحداث المؤشّر الجديدة. وهي تقدّم مجموعة واحدة من اﻷحداث لجميع أنواع أجهزة التأشير.

حاليًّا، تدعم كافّةُ المتصفّحات المشهورة مواصفة أحداث المؤشّر المستوى 2 ، بينما لا زالت المواصفة اﻷحدث أحداث المؤشّر المستوى 3 قيد الإنشاء، وهي في الغالب متوافقة مع أحداث المؤشّر المستوى 2.

ما لم تكن تطوّر من أجل المتصفّحات القديمة، مثل Internet Explorer 10 أو Safari 12، فلا داعي لاستخدام أحداث الفأرة أو اللمس بعد الآن -- يمكنك التغيير نحو أحداث المؤشّر، وستعمل شيفرتك حينها مع كلٍّ من الفأرة واﻷجهزة اللمسيّة.

والحال كذلك، هناك بعض اﻷمور الدقيقة المهمّة التي يجب معرفتها لاستخدام أحداث المؤشّر بشكل صحيح وتجنّب المفاجآت. سننبّه إليها في هذا المقال.

أنواع أحداث المؤشّر

تكون تسمية أحداث المؤشّر بطريقة مشابهة لأحداث الفأرة:

حدث المؤشّر حدث الفأرة المشابه
pointerdown mousedown
pointerup mouseup
pointermove mousemove
pointerover mouseover
pointerout mouseout
pointerenter mouseenter
pointerleave mouseleave
pointercancel -
gotpointercapture -
lostpointercapture -

كما يمكن أن نرى، لكلّ حدث فأرة mouse<event>‎، هناك حدث مؤشّر pointer<event>‎ يؤدّي غرضا مشابهًا. هناك أيضا 3 أحداث مؤشّر إضافيّة ليس لها أحداث mouse...‎ تقابلها، سنشرحها قريبا.


ملاحظة: استبدال mouse<event>‎ بـ pointer<event>‎ في الشيفرة

يمكننا استبدال mouse<event>‎ بـ pointer<event>‎ في شيفرتنا مع التأكّد من بقاء اﻷمور تعمل كما ينبغي باستخدام الفأرة.


إضافة إلى ذلك، سيتحسّن دعم اﻷجهزة اللمسيّة تلقائيّا. بالرغم من أنّنا قد نحتاج إلى إضافة touch-action: none في بعض المواضع في CSS. سنتطرّق إلى ذلك في اﻷسفل في القسم الذي حول pointercancel.

خاصيات حدث المؤشر

لأحداث المؤشّر نفس خاصّيّات أحداث الفأرة، مثل clientX/Y وtarget وغيرها، بالإضافة إلى بعض الخاصّيّات اﻷخرى:

  • pointerId - المعرّف الفريد للمؤشّر الذي تسبّب في الحدث، وهو مولَّد من طرف المتصفّح. يمكّننا من التعامل مع عدّة مؤشّرات، مثل شاشة لمسيّة مع قلم ولمسات متعدّدة (ستأتي الأمثلة).
  • pointerType - نوع جهاز التأشير. لابدّ أن يكون سلسلة نصّيّة من هذه الثلاث: "mouse" أو "pen" أو "touch". يمكننا استخدام هذه الخاصّيّة للتجاوب بشكل مختلف مع شتّى أنواع المؤشّرات.
  • isPrimary - تكون true بالنسبة للمؤشّر اﻷوّليّ (أوّل اصبع عند تعدّد اللمسات).

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

  • width - عرض المساحة التي يلمس فيها المؤشّر (الإصبع مثلا) الجهاز. إذا لم يكن الجهاز يدعم اللمس، كالفأرة مثلا، فإنّ قيمتها تكون دائما 1.
  • height - طول المساحة التي يلمس فيها المؤشّر الجهاز. إذا لم يكن الجهاز يدعم اللمس، فإنّ قيمتها تكون دائما 1.
  • pressure - قيمة الضغط الناتج عن المؤشّر، وتكون في المجال الذي بين 0 و 1. في الأجهزة التي لا تدعم ذلك، تكون قيمتها إمّا 0.5 (مضغوط) أو 0.
  • tangentialPressure - الضغط المماسيّ المطبّع (normalized tangential pressure).
  • tiltX وtiltY وtwist - خاصّيات متعلّقة بالقلم، وتصف طريقة تموضعه بالنسبة للسطح.

ليست هذه الخاصّيات مدعومة في معظم الأجهزة، لهذا يندر استخدامها. يمكنك أن تجد التفاصيل المتعلّقة بها في المواصفة عند الحاجة.

تعدد اللمسات

من اﻷمور التي لا تدعمها أحداث الفأرة إطلاقا هو تعدّد اللمسات: قد يلمس المستخدم هاتفه أو جهازه اللوحيّ في عدّة أماكن في نفس الوقت، أو قد يؤدّي بعض الحركات الخاصّة. تمكّن أحداث المؤشّر من التعامل مع تعدّد اللمسات بفضل الخاصّيّات pointerId وisPrimary. هذا ما يحدث عندما يلمس المستخدم شاشة لمسيّة في مكان ما، ثمّ يضع عليها إصبعا آخر في مكان مختلف:

  1. عند لمسة الإصبع الأولى:
    • يقع الحدث pointerdown مع isPrimary=true وpointerId ما.
  2. عند وضع الإصبع الثاني والمزيد من اﻷصابع (مع افتراض أن اﻷوّل لا يزال ملامسا):
    • يقع الحدث pointerdown مع isPrimary=false وpointerId مختلف لكلّ إصبع.

يُرجى التنبّه: لا يُسند pointerId إلى الجهاز ككلّ، ولكن لكلّ إصبع ملامس. إذا استخدمنا 5 أصابع للمس الشاشة في نفس الوقت، سنحصل على 5 أحداث pointerdown، كلٌّ له إحداثيّاته الخاصّة وpointerId مختلف.

للأحداث المرتبطة بالإصبع اﻷول دائما isPrimary=true.

يمكننا تتبّع عدّة أصابع ملامسة باستخدام pointerId الخاصّ بها. عندما يحرّك المستخدم إصبعا ثم يزيله، فسنحصل على اﻷحداث pointermove وpointerup مع نفس الـ pointerId الذي حصلنا عليه في pointerdown.

يمكنك مشاهدة هذا المثال من هنا الذي يعمل على تسجيل اﻷحداث pointerdown وpointerup.

يُرجى التنبّه: يجب أن تستخدم جهازا لمسيّا، كهاتف أو جهاز لوحيّ، لرؤية الفرق في pointerId/isPrimary. بالنسبة للأجهزة الأحاديّة اللمس، كالفأرة مثلا، يكون هناك دائما نفس الـ pointerId وisPrimary=true ، عند جميع أحداث المؤشّر.

الحدث pointercancel

ينطلق الحدث pointercancel عندما يبدأ هناك تفاعل مع المؤشّر، ثمّ يحصل شيء ما يؤدّي إلى إلغاء ذلك، فلا تتولّد المزيد من أحداث المؤشّر. قد يتسبّب في ذلك عذّة أشياء:

  • تعطيل عتاد جهاز التأشير تعطيلا حسّيًّا.
  • تغيير اتجاه الجهاز (كتدوير الجهاز اللوحيّ).
  • قرار المتصفّح معالجة التفاعل بنفسه، ظنًّا منه أنّها حركة للفأرة أو حركة تكبير وتحريك أو غير ذلك.

سنستعرض الحدث pointercancel في مثال عمليّ لنرى كيف يمكن أن يمسّنا. لنفترض أنّنا نودّ إنجاز سحب وإفلات لكرة ما، تماما كما في بداية المقال السحب والإفلات باستخدام أحداث الفأرة. ستأخذ أفعال المستخدم والأحداث المقابلة لها المجرى التالي:

  1. يضغط المستخدم على الصورة لابتداء السحب
    • ينطلق الحدث pointerdown
  2. ثم يبدأ بتحريك المؤشّر (وبذلك سحب الصورة)
    • ينطلق الحدث pointermove، ربّما عدّة مرّات
  3. وبعدها تحصل المفاجأة! للمتصفّح دعم أصيل (native) لسحب وإفلات الصور، فيستجيب ويستولي على عمليّة السحب والإفلات، مولّدا بذلك الحدث pointercancel.
    • يعالج المتصفّح الآن سحب وإفلات الصورة بنفسه. يستطيع المستخدم سحب صورة الكرة حتى إلى خارج المتصفّح، إلى برنامج البريد الخاصّ به أو إلى مدير الملفّات.
    • لا مزيد من الأحداث pointermove بالنسبة لنا.

فالمشكلة إذًا هي أنّ المتصفّح "يختطف" التفاعل: ينطلق الحدث pointercancel في بداية عمليّة "السحب والإفلات"، ولا تتولّد المزيد من أحداث pointermove.

يمكن مشاهدة السحب والإفلات من هنا، مع تسجيلٍ لأحداث المؤشّر (up/down وmove وcancel فقط) في المساحة النصيّة textarea.

نريد الآن إنجاز السحب والإفلات بأنفسنا، فلنطلب من المتصفّح إذًا عدم الاستيلاء عليه.

منع فعل المتصفّح الافتراضيّ لتجنّب pointercancel.

نحتاج إلى فعل أمرين:

  1. منع السحب والإفلات اﻷصيل من الوقوع:
  2. بالنسبة للأجهزة اللمسيّة، هناك أفعال أخرى للمتصفّح متعلّقة باللمس (عدا السحب والإفلات). لتجنّب المشاكل معها أيضا:
    • يمكن منعها بواسطة وضع ‎#ball { touch-action: none }‎ في CSS.
    • عندئذ ستصير شيفرتنا تعمل على اﻷجهزة اللمسيّة.

بعد القيام بذلك، ستعمل اﻷحداث كما ينبغي، ولن يختطف المتصفّح العمليّة ولن يرسل الحدث pointercancel.

يضيف المثال السابق (تجربه حية) هذه اﻷسطر. وكما ترى، لا مزيد من اﻷحداث pointercancel بعد الآن.

يمكننا الآن إضافة ميزة تحريك الكرة تحريكًا واقعيًا وستعمل عملية السحب والإفلات مع الأجهزة التي تدعم الفأرة والأجهزة اللمسية.

أسر المؤشر

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

التابع العامّ هو:

  • elem.setPointerCapture(pointerId)‎ - يربط اﻷحداثَ التي لها pointerId المُعطى بالعنصر elem. بعد الاستدعاء، ستكون لجميع أحداث المؤشّر التي لها نفس pointerId الهدفَ elem (كما لو أنّها وقعت على elem)، أيّا كان مكان وقوعها حقيقةً في المستند.

بعبارة أخرى، يعيد التابع elem.setPointerCapture(pointerId)‎ توجيه جميع ما يلي من اﻷحداث التي لها pointerId المعطى نحو elem.

يُزال هذا الربط:

  • تلقائيّا عندما تقع اﻷحداث pointerup أو pointercancel،
  • تلقائيّا عندما يُزال العنصر elem من المستند،
  • عند استدعاء elem.releasePointerCapture(pointerId)‎.

قد يُستخدم أسر المؤشّر لتبسيط التفاعلات التي من نوع السحب والإفلات.

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

  1. يضغط المستخدم على الزلّاجة thumb - يقع pointerdown.
  2. ثمّ يحرّك المستخدم المؤشّر - يقع pointermove، ونحرّك thumb تبعًا.
  3. …عند تحرّك المؤشّر، قد يفارق الزلّاجة thumb، فيذهب فوقها أو تحتها. يجب ألّا يتحرّك thumb إلا أفقيًّا، مع بقائه موازيًا للمؤشّر.

فمن أجل تتبّع جميع حركات المؤشّر، بما في ذلك ذهابه فوق/تحت thumb، كان علينا عندها إسناد معالج الحدث pointermove إلى المستند document.

يبدو ذلك الحلّ "قبيحا" بعض الشيء. إحدى المشاكل معه هي أنّ تحرّك المؤشّر حول المستند قد يتسبّب في آثار جانبيّة، كتشغيل معالجات أخرى لنفس الحدث لا علاقة لها بالمنزلق.

يقدّم أسر المتصفّح وسيلة لربط الحدث pointermove بالعنصر thumb وتجنّب أيّ من هذه المشاكل:

  • يمكننا استدعاء thumb.setPointerCapture(event.pointerId)‎ في معالج pointerdown،
  • فيُعاد توجيه أحداث المؤشّر نحو thumb إلى حين وقوع pointerup/cancel.
  • عند وقوع pointerup (انتهاء السحب)، يزول الربط تلقائيّا، وليس علينا أن نكترث له.

فبذلك حتى لو حرّك المستخدم المؤشّر حول المستند برُمَّته، ستُستدعى معالجات اﻷحداث على thumb. ما عدا ذلك، تبقى خاصّيات الإحداثيّات في كائنات اﻷحداث، مثل clientX/clientY صحيحة - يمسّ اﻷسر target/currentTarget فقط.

إليك الشيفرة اﻷساسيّة:

thumb.onpointerdown = function(event) {
  // thumb إلى (pointerup إلى حين‎) يعاد توجيه جميع أحداث المؤشّر
  thumb.setPointerCapture(event.pointerId);
};

thumb.onpointermove = function(event) {
  // إذ جميع أحداث المؤشّر موجّهة نحوه ،thumb تحريك المؤشّر: يكون الإنصات في
  let newLeft = event.clientX - slider.getBoundingClientRect().left;
  thumb.style.left = newLeft + 'px';
};

// ،thumb.releasePointerCapture ملاحظة: لا حاجة لاستدعاء
// تلقائيّا pointerup إذ تقع عند 

يمكن مشاهدة المثال كاملا من هنا.

في النهاية، لأسر المؤشّر فائدتان:

  1. تصير الشيفرة أنظف، إذ لا نحتاج إلى إضافة/إزالة المعالجات إلى كامل المستند. يُفكّ الرّبط تلقائيّا.
  2. إذا كانت هناك أيّة معالجات pointermove على المستند، فلن يؤدّي المؤشّر إلى اشتغالها دون قصد عندما يسحب المستخدم المنزلق.

أحداث أسر المؤشر

يوجد اثنان من أحداث المؤشّر التي تتعلّق باﻷسر:

  • ينطلق gotpointercapture عندما يستخدم عنصرٌ ما setPointerCapture لتفعيل اﻷسر.
  • ينطلق lostpointercapture عندما يُفكّ اﻷسر: سواءً كان ذلك صراحة بواسطة استدعاء releasePointerCapture، أو تلقائيّا عند pointerup/pointercancel.

الملخص

تمكّن أحداث المؤشّر من التعامل مع أحداث الفأرة واللمس والقلم في نفس الوقت، باستخدام شيفرة واحدة.

أحداث المؤشّر هي توسعة لأحداث الفأرة. يمكن استبدال mouse بـ pointer في أسماء اﻷحداث وسيظلّ كلّ شيء يعمل مع الفأرة، بالإضافة إلى دعم أفضل ﻷنواع اﻷجهزة اﻷخرى.

بالنسبة للسحب والإفلات وغيرها من تفاعلات اللمس المعقّدة التي قد يقرّر المتصفّح اختطافها ومعالجتها بنفسه، تذكّر إلغاء الفعل الافتراضيّ للأحداث ووضع touch-events: none في CSS للعناصر التي نتعامل معها.

لأحداث المؤشّر القدرات الإضافيّة التالية:

  • دعم اللمس المتعدّد باستخدام pointerId وisPrimary.
  • الخاصيّات المتعلّقة ببعض اﻷجهزة، مثل pressure وwidth/height وغيرها.
  • أسر المؤشّر: يمكننا إعادة توجيه جميع أحداث المؤشر نحو عنصر محدّد إلى حين وقوع pointerup/pointercancel.

حاليّا، تدعم جميع المتصفّحات المشهورة أحداث المؤشّر، وبذلك يمكننا الانتقال إليها بأمان، خاصّة إذا لم يُحتج إلى -IE10 و-Safari 12. بل حتّى مع تلك المتصفّحات، هناك ترقيعات متعدّدة (polyfills) تمكّن من دعم أحداث المؤشّر.

ترجمة -وبتصرف- للمقال Pointer events من سلسلة 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.


×
×
  • أضف...