تُعدّ أحداث المؤشّر طريقة حديثة للتعامل مع المُدخلات النابعة من مختلف أجهزة التأشير، كالفأرة والقلم والشاشات اللمسيّة وغيرها.
لمحة تاريخية مختصرة
لنأخذ بلمحة قصيرة، لتتمكّن من فهم الصورة العامّة وكذا مكانة أحداث المؤشّر وسط أنواع اﻷحداث اﻷخرى.
-
في القديم، لم تكن هناك سوى أحداث الفأرة. ثمّ انتشرت اﻷجهزة اللمسيّة، مثل الهواتف واﻷجهزة اللوحيّة. ولكي تعمل النصوص البرمجيّة القائمة، كانت هذه اﻷجهزة (ولا زالت) تولّد أحداث الفأرة. فعلى سبيل المثال، يولّد الضغط على شاشة لمسيّة الحدث
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
. هذا ما يحدث عندما يلمس المستخدم شاشة لمسيّة في مكان ما، ثمّ يضع عليها إصبعا آخر في مكان مختلف:
-
عند لمسة الإصبع الأولى:
-
يقع الحدث
pointerdown
معisPrimary=true
وpointerId
ما.
-
يقع الحدث
-
عند وضع الإصبع الثاني والمزيد من اﻷصابع (مع افتراض أن اﻷوّل لا يزال ملامسا):
-
يقع الحدث
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
في مثال عمليّ لنرى كيف يمكن أن يمسّنا. لنفترض أنّنا نودّ إنجاز سحب وإفلات لكرة ما، تماما كما في بداية المقال السحب والإفلات باستخدام أحداث الفأرة. ستأخذ أفعال المستخدم والأحداث المقابلة لها المجرى التالي:
-
يضغط المستخدم على الصورة لابتداء السحب
-
ينطلق الحدث
pointerdown
-
ينطلق الحدث
-
ثم يبدأ بتحريك المؤشّر (وبذلك سحب الصورة)
-
ينطلق الحدث
pointermove
، ربّما عدّة مرّات
-
ينطلق الحدث
-
وبعدها تحصل المفاجأة! للمتصفّح دعم أصيل (native) لسحب وإفلات الصور، فيستجيب ويستولي على عمليّة السحب والإفلات، مولّدا بذلك الحدث
pointercancel
.- يعالج المتصفّح الآن سحب وإفلات الصورة بنفسه. يستطيع المستخدم سحب صورة الكرة حتى إلى خارج المتصفّح، إلى برنامج البريد الخاصّ به أو إلى مدير الملفّات.
-
لا مزيد من الأحداث
pointermove
بالنسبة لنا.
فالمشكلة إذًا هي أنّ المتصفّح "يختطف" التفاعل: ينطلق الحدث pointercancel
في بداية عمليّة "السحب والإفلات"، ولا تتولّد المزيد من أحداث pointermove
.
يمكن مشاهدة السحب والإفلات من هنا، مع تسجيلٍ لأحداث المؤشّر (up/down
وmove
وcancel
فقط) في المساحة النصيّة textarea
.
نريد الآن إنجاز السحب والإفلات بأنفسنا، فلنطلب من المتصفّح إذًا عدم الاستيلاء عليه.
منع فعل المتصفّح الافتراضيّ لتجنّب pointercancel
.
نحتاج إلى فعل أمرين:
-
منع السحب والإفلات اﻷصيل من الوقوع:
-
يمكننا فعل ذلك عن طريق وضع
ball.ondragstart = () => false
، تماما كما هو موضّح في المقال السحب والإفلات باستخدام أحداث الفأرة. - يعمل ذلك جيّدا مع أحداث الفأرة.
-
يمكننا فعل ذلك عن طريق وضع
-
بالنسبة للأجهزة اللمسيّة، هناك أفعال أخرى للمتصفّح متعلّقة باللمس (عدا السحب والإفلات). لتجنّب المشاكل معها أيضا:
-
يمكن منعها بواسطة وضع
#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
) في داخله. ثم يعمل هكذا:
-
يضغط المستخدم على الزلّاجة
thumb
- يقعpointerdown
. -
ثمّ يحرّك المستخدم المؤشّر - يقع
pointermove
، ونحرّكthumb
تبعًا. -
…عند تحرّك المؤشّر، قد يفارق الزلّاجة
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 إذ تقع عند
يمكن مشاهدة المثال كاملا من هنا.
في النهاية، لأسر المؤشّر فائدتان:
- تصير الشيفرة أنظف، إذ لا نحتاج إلى إضافة/إزالة المعالجات إلى كامل المستند. يُفكّ الرّبط تلقائيّا.
-
إذا كانت هناك أيّة معالجات
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
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.