لا يمكننا فقط إسناد معالجات للأحداث من خلال جافاسكربت، ولكن يمكننا أيضا توليد أحداث مخصّصة.
يُمكن استعمال الأحداث المخصّصة لإنشاء "مكوّنات رسوميّة". على سبيل المثال، يمكن للعنصر الجذر في قائمةٍ تعمل بواسطة جافاسكربت افتعال أحداثٍ تُنبئ بما يحصل مع القائمة: open
(عند فتح القائمة)، select
(عند تحديد عنصر) وهكذا. يمكن أن تنصت شيفرة أخرى لهذه الأحداث وتراقب ماذا يحصل مع القائمة.
لا يمكننا فقط توليد أحداث جديدة كليّا، نخترعها لأغراضنا الخاصّة، ولكن يمكن أيضا توليد اﻷحداث المضمّنة، مثل click
و mousedown
إلى غير ذلك. قد يساعد ذلك عند إجراء الاختبارات الآليّة.
باني الأحداث
تشكّل أصناف الأحداث المضمّنة سُلّميّة مشابهة لسلّميّة أصناف عناصر DOM. يكون الجذر فيها هو الصنف المُضمّن Event. ويمكننا إنشاء كائنات منها بهذه الطريقة:
let event = new Event(type[, options]);
الوسائط:
-
type -- نوع الحدث، ويكون سلسلة نصيّة مثل
"click"
أو إذا كان خاصّا بنا مثل"my-event"
. - options -- كائن بخاصيتين اختياريتين:
-
bubbles: true/false
-- إذا كانتtrue
، فإنّ الحدث ينتشر نحو الأعلى. -
cancelable: true/false
: إذا كانتtrue
، فمن الممكن منع "الفعل الافتراضي". سنرى لاحقًا ما يعني ذلك للأحداث المخصّصة.
تكون قيمة كلتيهما false
افتراضيّا: {bubbles: false, cancelable: false}
.
dispatchEvent
بعد إنشاء كائن الحدث، نستطيع أن "نجريه" على عنصرٍ ما بواسطة الاستدعاء elem.dispatchEvent(event)
.
تستجيب المعالجات له حينها كما لو كان حدث متصفّح عادي. وإذا كان الحدث قد أنشئ وله الراية bubbles
، فإنّه ينتشر نحو الأعلى.
ابتُدِئ الحدث click
في المثال أدناه من خلال جافاسكربت. يعمل المعالج بنفس الطريقة كما لو أن الزرّ قد نُقر بالفعل:
<button id="elem" onclick="alert('Click!');">Autoclick</button> <script> let event = new Event("click"); elem.dispatchEvent(event); </script>
ملاحظة: event.isTrusted
هناك طريقة لمعرفة إذا كان الحدث "حقيقيّا" من المستخدم أو مولّدًا بواسطة سكربت. تكون قيمة الخاصيّة event.isTrusted
هي true
للأحداث التي تأتي من أفعال حقيقيّة للمستخدم و تكون false
للأحداث المولّدة بواسطة سكربت.
مثال عن الانتشار نحو الأعلى
يمكننا إنشاء حدث منتشر نحو الأعلى باسم "hello"
والتقاطه في document
. كلّ ما نحتاجه هو إعطاء bubbles
القيمة true
:
<h1 id="elem">Hello from the script!</h1> <script> // ... document الالتقاط على مستوى document.addEventListener("hello", function(event) { // (1) alert("Hello from " + event.target.tagName); // Hello from H1 }); // elem الإرسال من ... let event = new Event("hello", {bubbles: true}); // (2) elem.dispatchEvent(event); // و يظهر الرسالة document سيشتغل المعالج المسند إلى </script>
ملاحظات:
-
يجب أن نستخدم
addEventListener
لأحداثنا المخصصة، لأنon<event>
توجد فقط للأحداث المضمّنة، فلا تعملdocument.onhello
مثلا. -
يجب وضع
bubbles:true
، وإلا فلن ينتشر الحدث نحو الأعلى.
آلية الانتشار نحو الأعلى هي نفسها للأحداث المُضمّنة (click
) والمخصصة (hello
). هناك أيضا مرحلتا الانتشار نحو الأعلى والانتشار نحو الأسفل.
MouseEvent و KeyboardEvent وغيرهما
هذه قائمة قصيرة لأصناف أحداث واجهة المستخدم مأخوذة من مواصفة أحداث واجهة المستخدم:
-
UIEvent
-
FocusEvent
-
MouseEvent
-
WheelEvent
-
KeyboardEvent
-
…
ينبغي أن نستخدمها عوضا عن new Event
إذا أردنا إنشاء هذه الأحداث. على سبيل المثال، new MouseEvent("click")
.
يمكّن الباني المناسب من تحديد خاصيّات قياسية تتعلّق بنوع الحدث ذاك. مثل clientX/clientY
لأحداث المؤشر:
let event = new MouseEvent("click", { bubbles: true, cancelable: true, clientX: 100, clientY: 100 }); alert(event.clientX); // 100
يرجى التنبه: لا يتيح الباني العام Event
ذلك.
لنجرّب:
let event = new Event("click", { bubbles: true, // cancelable و bubbles فقط cancelable: true, // Event تعملان في الباني clientX: 100, clientY: 100 }); alert(event.clientX); // تُهمل الخاصّية غير المعروفة ،undefined
في الحقيقة، يمكننا الاحتيال على ذلك بإسناد event.clientX=100
مباشرة بعد إنشائه. فيؤول الأمر إلى المناسبة والالتزام بالقواعد. يكون نوع الأحداث التي يولّدها المتصفّح صحيحًا على الدوام.
توجد القائمة الشاملة لمختلف أحداث واجهة المستخدم في المواصفة، على سبيل المثال، MouseEvent.
الأحداث المخصصة
بالنسبة لأنواع الأحداث الخاصّة بنا والجديدة كليّا مثل "hello"
علينا أن نستخدم new CustomEvent
. فنيّا، CustomEvent هي نفس Event
لكن مع استثناء وحيد.
في الوسيط الثاني (object) يمكننا إضافة خاصيّة أخرى detail
من أجل أيّة معلومات مخصصّة نودّ تمريرها مع الحدث. على سبيل المثال:
<h1 id="elem">Hello for John!</h1> <script> // تأتي المزيد من التفاصيل مع الحدث إلى المعالج elem.addEventListener("hello", function(event) { alert(event.detail.name); }); elem.dispatchEvent(new CustomEvent("hello", { detail: { name: "John" } })); </script>
يمكن أن تحوي هذه الخاصيّة أي معطيات. في الحقيقة، من الممكن أن نعمل بدونها، لأننا نستطيع أن نسند أيّ خاصيّة إلى كائن new Event
عاديّ بعد إنشائه. لكن CustomEvent
تزوّده بحقل detail
الخاص لتفادي التعارض مع خاصيّات الحدث الأخرى.
إلى جانب ذلك، يبيّن صنف الحدث "أيّ نوع حدث" هو، وإذا كان الحدث مخصّصا، فينبغي استخدام CustomEvent
لنكون فقط واضحين بخصوصه.
()event.preventDefault
لدى العديد من أحداث المتصفّح "أفعال افتراضيّة"، كالانتقال إلى رابط، بداية تحديد، وما إلى ذلك.
أمّا الأحداث المخصّصة الجديدة، فليس لديها بالطبع أيّة أفعال افتراضيًّة، لكن قد تكون للشيفرة التي ترسل مثل هذه الأحداث خُططًا خاصّة تقوم بها بعد افتعال الحدث.
باستدعاء event.preventDefault()
، يمكن لمعالج الحدث أن يرسل إشارة بإنّ تلك الأفعال المخطّط لها يجب أن تُلغى.
في تلك الحالة يعيد الاستدعاء elem.dispatchEvent(event)
القيمة false
. وتعلم الشيفرةُ التي أرسلت الحدث بأنّ عليها ألا تكمل.
لنرى مثالًا تطبيقيّا لذلك -- أرنب متخفّي (قد يكون قائمة أيضا منغلقة أو أيّ شيئ آخر).
يمكن أن ترى في الأسفل الأرنب rabbit#
وعليه الدالة hide()
التي ترسل الحدث "hide"
، لتعلم جميع الأطراف المعنيّة بأنّ الأرنب سيختفي.
يمكن لأي معالج أن يستمع لذلك الحدث بواسطة rabbit.addEventListener('hide',...)
، وإن تتطلّب الأمر، يمكنه إلغاء الفعل باستخدام event.preventDefault()
. فعندها لن يختفي الأرنب:
<pre id="rabbit"> |\ /| \|_|/ /. .\ =\_Y_/= {>o<} </pre> <button onclick="hide()">Hide()</button> <script> function hide() { let event = new CustomEvent("hide", { cancelable: true // preventDefault بدون تلك الراية لا يعمل }); if (!rabbit.dispatchEvent(event)) { alert('The action was prevented by a handler'); } else { rabbit.hidden = true; } } rabbit.addEventListener('hide', function(event) { if (confirm("Call preventDefault?")) { event.preventDefault(); } }); </script>
يرجى التنبه هنا: يجب أن يكون للحدث الراية cancelable: true
، وإلّا فسيُهمل event.preventDefault()
.
تعمل الأحداث التي داخل أحداث أخرى بشكل متزامن
تُعالَج الأحداث عادةً على شكل طابور. بعبارة أخرى، إذا كان المتصفّح يقوم بمعالجة onclick
ووقع حدث جديد، كتحريك الفأرة مثلًا، فإن معالجته تُصفّ في طابور الانتظار، وتُستدعى المعالجات المتعلّقة بـ mousemove
بعد الانتهاء من معالجة onclick
.
الاستثناء الجدير بالانتباه هنا هو عندما ينشأ حدث من داخل حدث آخر، بواسطة dispatchEvent
مثلا. تُعالج تلك الأحداث فورًا: تُستدعى معالجات الحدث الجديد ثم تستمر معالجة الحدث الحالية.
على سبيل المثال، في الشيفرة أدناه يُفتعل الحدث menu-open
خلال الحدث onclick
. ويُعالَج فورًا دون انتظار معالج onclick
من الانتهاء:
<button id="menu">Menu (click me)</button> <script> menu.onclick = function() { alert(1); menu.dispatchEvent(new CustomEvent("menu-open", { bubbles: true })); alert(2); }; // يفتعل بين 1 و 2 document.addEventListener('menu-open', () => alert('nested')); </script>
يكون ترتيب المخرجات كما يلي: 1 -› الحدث الداخلي -› 2.
يُرجى التنبّه إلى أنّ الحدث الداخليّ menu-open
قد تم ألتقاطه على مستوى document
. يتمّ انتشار الحدث الداخلي ومعالجته قبل عودة المعالجة إلى الشيفرة الخارجية (onclick
).
لا يخصّ هذا فقط dispatchEvent
، بل هناك حالات أخرى. إذا استدعى معالجٌ توابعَ تفتعل أحداثًا أخرى، فإنّها تُعالج تزامنيًّا أيضًا، بشكلٍ متداخل.
لكن لنفترض أنّنا لا نريد ذلك. نريد أن تتمّ معالجة onclick
أوّلًا، باستقلالٍ عن menu-open
أو غيره من اﻷحداث المتداخلة.
يمكننا عندها إمّا أن نضع dispatchEvent
(أو نداءَ افتعال أحداثٍ آخر) في آخر onclick
، أو ربّما أفضل، أن نلفّه بـ setTimeout
منعدمة التأخير:
<button id="menu">Menu (click me)</button> <script> menu.onclick = function() { alert(1); setTimeout(() => menu.dispatchEvent(new CustomEvent("menu-open", { bubbles: true }))); alert(2); }; document.addEventListener('menu-open', () => alert('nested')); </script>
تعمل الآن dispatchEvent
لا تزامنيًّا بعد الانتهاء من تنفيذ الشيفرة الحاليّة، بما في ذلك menu.onclick
، وتصير بذلك معالجات الأحداث منفصلة تمامًا. ويصير ترتيب المخرجات كالتالي: 1 -> 2 -> الحدث الداخلي.
الملخص
لتوليد حدثٍ من خلال الشيفرة، نحتاج أوّلًا أن ننشئ كائن حدث.
يقبل الباني العام Event(name, options)
اسمًا للحدث والكائن options
مع الخاصّيّتين:
-
bubbles: true
إذا كان يجب أن ينتشر الحدث نحو اﻷعلى. -
cancelable: true
إذا كان يجب أن يعملevent.preventDefault()
.
يقبل بانو الأحداث اﻷصليّة الآخرون مثل MouseEvent
و KeyboardEvent
و ما إلى ذلك، خاصّيّات مختصّة بنوع الحدث ذلك. على سبيل المثال، clientX
لأحداث المؤشر.
ينبغي أن نستخدم الباني CustomEvent
للأحداث المخصّصة. فلديه خيار إضافيّ اسمه detail
، يمكن أن نسند إليه المعطيات المختصّة بالحدث. وبذلك يمكن لجميع المعالجات الوصول إليها بواسطة event.detail
.
بالرغم من الإمكانيّة التقنيّة لتوليد أحداث المتصفّح مثل click
أو keydown
، فينبغي استخدامها بحذرٍ شديد.
لا ينبغي أن نعمد إلى توليد أحداث المتصفّح، إذ يعدّ ذلك طريقة مبتذلة لتشغيل المعالجات. وهي هندسة سيّئة في أغلب اﻷحيان.
يمكن أن تُولّد اﻷحداث اﻷصليّة:
- كطريقة مبتذلة لجعل مكتبات الطرف الثالث تعمل كما يجب، إذا لم تكن توفّر وسائل أخرى للتفاعل معها.
- عند إجراء الاختبارات الآليّة، مثل "النقر على الزرّ" من خلال السكربت لمعرفة ما إذا كانت الواجهة تستجيب بشكل صحيح.
تُولَّد الأحداث المخصّصة بأسمائنا الخاصّة لأغراض هندسيّة في الغالب، للإشارة إلى ما يحصل داخل القوائم و الدوّارات وما إلى ذلك.
ترجمة -وبتصرف- للمقال Dispatching custom events من سلسلة Browser: Document, Events, Interfaces لصاحبها Ilya Kantor
أفضل التعليقات
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.