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

الأعضاء
  • المساهمات

    27
  • تاريخ الانضمام

  • تاريخ آخر زيارة

السُّمعة بالموقع

8 Neutral

4 متابعين

  1. يقع الحدث submit عندما تُسلَّم الاستمارة، ويُستخدم عادة للتحقّق من الاستمارة قبل إرسالها إلى الخادم، أو لإلغاء التسليم والقيام بمعالجته في جافاسكربت. يمكّن التابع form.submit()‎ من إرسال الاستمارة بواسطة جافاسكربت. ويمكننا استخدامه لإنشاء وإرسال استماراتنا إلى الخادم ديناميكيًّا. لنرى المزيد من التفاصيل حولهما. الحدث: submit هناك طريقتان رئيسيّتان لتسليم الاستمارة: الأولى بالنقر على <input type="submit"‎> أو <input type="image"‎>. الثانية بالضغط على المفتاح Enter داخل حقل المُدخل. يؤدّي كلا هذين الفعلين إلى وقوع الحدث submit على الاستمارة. يمكن للمعالج التحقّق من المعطيات، وإذا وجد أنّ هناك أخطاء، فإنّه يبيّنها ويستدعى event.preventDefault()‎، لكيلا تُرسل الاستمارة إلى الخادم. في الاستمارة أدناه، يمكن النقر على <input type="submit"‎> أو الذهاب إلى داخل الحقل النصّيّ والضغط على Enter. سيؤدّي كلا الفعلين إلى إظهار alert ولا تُرسَل الاستمارة إلى أيّ مكان بسبب وجود return false. <form onsubmit="alert('submit!');return false"> First: Enter in the input field <input type="text" value="text"><br> Second: Click "submit": <input type="submit" value="Submit"> </form> See the Pen JS-forms-submit-ex1 by Hsoub (@Hsoub) on CodePen. ملاحظة: العلاقة بين submit وclick عندما تُرسَل الاستمارة باستخدام المفتاح Enter في حقل المُدخل، فإنّ الحدث click يقع على العنصر <input type="submit"‎>. هذا طريف، إذ لم يكن هناك نقرٌ على الإطلاق. يمكن تجربة ذلك بواسطة هذه الشيفرة: <form onsubmit="return false"> <input type="text" size="30" value="Focus here and press enter"> <input type="submit" value="Submit" onclick="alert('click')"> </form> See the Pen JS-forms-submit-ex2 by Hsoub (@Hsoub) on CodePen. التابع: submit لتسليم استمارة ما إلى الخادم يدويّا، يمكننا استدعاء form.submit()‎. لا يتولّد الحدث submit بذلك، لأنّ المبرمج إذا استدعىform.submit()‎، يُفترض أنّ السكربت قد أدّى كلّ المعالجة المطلوبة. يُستخدم ذلك أحيانا لإنشاء وإرسال الاستمارة يدويّا، هكذا: let form = document.createElement('form'); form.action = 'https://google.com/search'; form.method = 'GET'; form.innerHTML = '<input name="q" value="test">'; // يجب أن تكون الاستمارة في المستند لتسليمها document.body.append(form); form.submit(); التمارين استمارة في نافذة منبثقة شرطية (modal) اﻷهميّة: 5 أنشئ الدالّة showPrompt(html, callback)‎ التي تُظهر استمارة فيها الرسالة html وحقل مُدخلٍ والأزار OK/CANCEL. يجب أن يكتب المستخدم شيئا ما في الحقل النصّيّ ثمّ يضغط على المفتاح Enter أو الزرّ OK، ليُستدعى callback(value)‎ بالقيمة التي أُدخلت. وإلاّ فإذا ضغط المستخدم على المفتاح Esc أو الزرّ CANCEL، يُستدعى callback(null)‎ في كلتا الحالتين تنتهي عمليّة الإدخال وتُزال الاستمارة. المتطلّبات: يجب أن تكون الاستمارة في وسط النافذة. الاستمارة هي نافذة منبثقة شرطيّة. بعبارة أخرى، ليس من الممكن التفاعل مع بقيّة الصفحة إلى أن يغلقها المستخدم. عندما تظهر الاستمارة، يجب أن يكون التركيز داخل العنصر <input> للمستخدم. يجب أن تعمل المفاتيح Tab/Shift+Tab على نقل التركيز بين حقول الاستمارة، لكن لا تسمح بمغادرته إلى عناصر الصفحة الأخرى. مثال عن كيفيّة الاستخدام: showPrompt("Enter something<br>...smart :)", function(value) { alert(value); }); يمكن مشاهدة النتيجة من هنا. ملاحظة: في المستند المصدريّ، يوجد HTML/CSS لاستمارة بتموضع ثابت، وترجع لك مهمّة جعلها نافذة منبثقة شرطيّة. أنجز التمرين في البيئة التجريبيّة. الحل يُمكن إنجاز نافذة منبثقة شرطيّة بواسط عنصر <div id="cover-div"‎> نصف شفّاف، يغطّي كامل النافذة، هكذا: #cover-div { position: fixed; top: 0; left: 0; z-index: 9000; width: 100%; height: 100%; background-color: gray; opacity: 0.3; } ولأنّ هذا الـ<div> يغطّي كلّ شيء، فإنّه هو الذي يتلقّى جميع النقرات، وليست الصفحة التي تحته. يمكننا أيضا منع تمرير الصفحة بوضع body.style.overflowY='hidden'‎. يجب أن لا تكون الاستمارة داخل <div>، ولكن بجانبه، لأنّنا لا نريد أن تكون له الشفافيّة opacity. ترجمة -وبتصرف- للمقال Forms: event and method submit من سلسلة Browser: Document, Events, Interfaces لصاحبها Ilya Kantor
  2. لنتناول مختلف اﻷحداث التي ترافق تحديث المُعطيات. الحدث: change يقع الحدث change عند تمام تغيّر العنصر. بالنسبة للمُدخلات النصّيّة، ذلك يعني أنّ الحدث يقع عندما تفقد التركيز. على سبيل المثال، عند الكتابة في الحقل النصيّ أدناه، لا يكون هناك أيّ حدث. لكن عندما ننقل التركيز إلى مكان آخر، كالنقر على زرّ مثلا، سيكون لدينا الحدث change: <input type="text" onchange="alert(this.value)"> <input type="button" value="Button"> See the Pen JS-events-change-input-ex1 by Hsoub (@Hsoub) on CodePen. بالنسبة للعناصر الأخرى select وinput type=checkbox/radio ، فإنّ الحدث يقع بعد تغيّر التحديد مباشرة: <select onchange="alert(this.value)"> <option value="">Select something</option> <option value="1">Option 1</option> <option value="2">Option 2</option> <option value="3">Option 3</option> </select> See the Pen JS-events-change-input-ex2 by Hsoub (@Hsoub) on CodePen. الحدث: input يقع الحدث input كلّما غيّر المستخدم القيمة التي في المُدخل. وبخلاف أحداث لوحة المفاتيح، يقع هذا الحدث عند كلّ تغيّر في القيمة، حتى لو لم يكن ناجما عن أفعال لوحة المفاتيح، كاللصق بواسطة الفأرة أو استعمال التعرّف على الصوت لإملاء النصّ. على سبيل المثال: <input type="text" id="input"> oninput: <span id="result"></span> <script> input.oninput = function() { result.innerHTML = input.value; }; </script> See the Pen JS-events-change-input-ex3 by Hsoub (@Hsoub) on CodePen. إذا أردنا معالجة جميع التغيّرات في <input> فإنّ هذا الحدث هو الخيار اﻷمثل. في المقابل، لا يقع input عند أفعال لوحة المفاتيح أو غيرها من اﻷفعال التي لا تؤدّي إلى تغيّر القيمة، مثل الضغط على مفاتيح اﻷسهم ⇦ و⇨ في المُدخل. ملاحظة: لا يمكن منع أيّ شيء في oninput يقع الحدث input بعد تغيّر القيمة، ولذا لا يمكننا استعمال event.preventDefault()‎ هناك -- قد فات الآوان، لن يكون لذلك أيّ أثر. الأحداث: cut وcopy وpaste تقع هذه اﻷحداث عند قصّ/نسخ/لصق قيمة ما، وهي تنتمي إلى الصنف ClipboardEvent، وتُمكّن من الوصول إلى المُعطيات التي قُصّت/أُلصقت. يمكننا أيضا استخدام event.preventDefault()‎ معها لإلغاء الفعل، فلا يتمّ بذلك نسخ/لصق أيّ شيء. على سبيل المثال، تمنع الشيفرة أدناه وقوع أيّ من هذه اﻷحداث، وتُظهر ماذا نحاول قصّه/نسخه/لصقه: <input type="text" id="input"> <script> input.oncut = input.oncopy = input.onpaste = function(event) { alert(event.type + ' - ' + event.clipboardData.getData('text/plain')); return false; }; </script> See the Pen JS-events-change-input-ex4 by Hsoub (@Hsoub) on CodePen. يُرجى التنبّه إلى أنّه لا يمكن نسخ/لصق النصّ فقط، بل أيّ شيء، مثل نسخ ملفّ في مدير الملفّات في نظام التشغيل، ولصقه. هذا لأنّ clipboardData تتضمّن الواجهة DataTransfer، التي تُستخدم في السحب والإفلات والنسخ/اللصق. تُعدّ هذه الأمور خارجة قليلا عن موضوعنا الآن، لكن يمكنك أن تجد التوابع الخاصّة بها في المواصفة. . تنبيه: ClipboardAPI: قيود تتعلّق بسلامة المستخدم الحافظة هي شيء "عموميّ" على مستوى نظام التشغيل. لذا فإنّ معظم المتصفّحات تمكّن من الوصول إلى الحافظة قراءة وكتابة لكن في نطاق بعض أفعال المستخدم فقط بداعي السلامة، كما في معالجات اﻷحداث onclick مثلا. ويُمنع كذلك توليد أحداث "مخصّصّة" للحافظة باستخدام dispatchEvent في جميع المتصفّحات باستثناء Firefox. الملخص أحداث تغيير المعطيات: الحدث الوصف الخصائص change عند تغيّر قيمة ما بالنسبة للمُدخلات النصّيّة، يقع عند فقدانها التركيز. input عند كلّ تغيّر في المُدخلات النصّية يقع مباشرة، على خلاف change. cut/copy/paste عند أفعال القصّ/النسخ/اللصق. يمكن منع الفعل، وتتيح الخاصّيّةُ event.clipboardData إمكانيّة القراءة من/الكتابة في الحافظة. table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } التمارين حاسبة الإيداع اﻷهميّة: 5 أنشئ واجهة تُمكّن من إدخال القيمة المودعة في البنك إضافة إلى النسبة، ثمّ تحسب كم ستصبح القيمة بعد مدّة مُعيّنة من الزمن. كما في المثال من هنا. يجب أن يُعالج أيّ تغيّر في المُدخل مباشرة. المعادلة هي كالتالي: // القيمة الابتدائيّة للنقود :initial // تعني 0.05 مثلا، %5 سنويّا :interest // كم عاما من الانتظار :years let result = Math.round(initial * (1 + interest * years)); أنجز التمرين في البيئة التجريبيّة الحل افتح الحلّ في البيئة التجريبيّة ترجمة -وبتصرف- للمقال Events: change, input, cut, copy, paste من سلسلة Browser: Document, Events, Interfaces لصاحبها Ilya Kantor
  3. يمكن للعنصر أن يحصل على التركيز من المستخدم إمّا بالنقر عليه مباشرة أو باستعمال المفتاح Tab من لوحة المفاتيح، ويمكنه أيضا أن يحصل عليه افتراضيًّا عند تحميل الصفحة بواسطة السمة autofocus في HTML، إلى غير ذلك من وسائل الحصول على التركيز. يُقصد بالتركيز على العنصر عموما "التهيّء لاستقبال المُعطيات"، وبذلك فإنّه الوقت المناسب لإجراء الشيفرة التي تعمل على تهيئة الوظائف المطلوبة. قد تكون لحظة فقدان التركيز (أو ما يُعرف بـ "blur" أي "الضبابيّة") أكثر أهميّة. وتحصل عندما ينقر المستخدم في مكان آخر أو يضغط على المفتاح Tab لينتقل إلى الحقل الموالي في الاستمارة، أو غير ذلك من الوسائل الممكنة الأخرى. يُقصد بفقدان التركيز عموما أنّ "المُعطيات قد أُدخلت"، و يمكننا إذًا إجراء الشيفرة التي تعمل على تفحّصها أو حفظها في الخادم وماشابه ذلك. الأحداث focus/blur يُستدعى الحدث focus عند التركيز على العنصر، ويُستدعى blur عندما يفقد العنصر التركيز. لنستخدم هذه الأحداث لغرض التحقّق من القيم المُدخَلة في حقول المدخلات. إليك المثال التالي: يتحقّق معالج blur من أنّ الحقل قد أُدخل فيه بريد الكترونيّ، فإذا لم يكن قد أُدخل فيه بريد الكترونيّ، يظهر رسالة خطأ. يُخفي معالج focus رسالة الخطأ (ليجري التحقّق عند blur مرّة أخرى). <style> .invalid { border-color: red; } #error { color: red } </style> Your email please: <input type="email" id="input"> <div id="error"></div> <script> input.onblur = function() { if (!input.value.includes('@')) { // ليس بريدا الكترونيّا input.classList.add('invalid'); error.innerHTML = 'Please enter a correct email.' } }; input.onfocus = function() { if (this.classList.contains('invalid')) { // أزل إشارة “الخطأ”، لأنّ المستخدم يريد إعادة إدخال شيء ما this.classList.remove('invalid'); error.innerHTML = ""; } }; </script> See the Pen JS-focus-blur-ex1 by Hsoub (@Hsoub) on CodePen. تُمكّننا لغة HTML الحديثة من إجراء عدّة تحقّقات بواسطة سمات العناصر input مثل required وpattern وغيرها. وهي أحيانا كلّ ما نحتاجه. يُمكن أن تُستخدم جافاسكربت إذا أردنا المزيد من المرونة. ويمكننا أيضا إرسال القيمة المُعدّلة إلى الخادم تلقائيّا إذا كانت صحيحة. التوابع focus/blur تجعل التوابعُ elem.focus()‎ وelem.blur()‎ العنصر يكتسب/يفقد التركيز. على سبيل المثال، يمكن أن نمنع الزائر من مغادرة حقل المُدخَل إذا كانت القيمة غير صحيحة: <style> .error { background: red; } </style> Your email please: <input type="email" id="input"> <input type="text" style="width:220px" placeholder="make email invalid and try to focus here"> <script> input.onblur = function() { if (!this.value.includes('@')) { // ليس بريدا الكترونيّا // أظهر الخطأ this.classList.add("error"); // وأرجع التركيز كما كان... input.focus(); } else { this.classList.remove("error"); } }; </script> See the Pen JS-focus-blur-ex2 by Hsoub (@Hsoub) on CodePen. يعمل ذلك على جميع المتصفّحات باستثناء فايرفكس (خطأ برمجيّ). إذا أدخلنا شيئا ما في الحقل ثمّ حاولنا استعمال المفتاح Tab أو النقر خارج الـ<input>، فإنّ onblur يعمل على استعادة التركيز. يُرجى التنبّه أنّه لا يمكننا "منع فقدان التركيز" بواسطة استدعاء event.preventDefault()‎ عند onblur، لأنّ onblur يعمل بعد أن يفقد العنصر التركيز. تنبيه: فقدان التركيز الناشئ من جافاسكربت قد يحدث فقدان التركيز لعدّة أسباب. إحداها أن ينقر المستخدم على مكان آخر. لكن يمكن لجافاسكربت ذاتها أن تتسبّب في ذلك أيضا، على سبيل المثال: يجلب alert التركيز إليه، وبذلك فهو يتسبّب في فقدان التركيز عن العنصر (الحدث blur)، وعندما يُزال alert فإن التركيز يرجع حيث كان (حدث focus). إذا حُذف عنصر ما من DOM، فإنّ ذلك يتسبّب في فقدان التركيز أيضا. لكن إذا أُدرج ثانية، لا يرجع معه التركيز. تتسبّب هذه الميزات أحيانا في سوء سلوك معالجات focus/blur -- تشتغل دون الحاجة إليها. أفضل توصيفة هي الحذر عند استعمال هذه الأحداث. إذا أردنا تتبّع فقدان التركيز الناشئ من قبل المستخدمين، فعلينا أن نتجنّب التسبّب فيه بأنفسنا. تمكين التركيز على أيّ عنصر: tabindex هناك العديد من العناصر التي لا تدعم التركيز افتراضيًّا. تختلف القائمة قليلا بين المتصفّحات، لكنّ هناك شيئا ثابتا: يكون دعم focus/blur مضمونا بالنسبة للعناصر التي يمكن للمستخدم أن يتفاعل معها، مثل <button> و<input> و<select> و<a> وما إلى ذلك. في المقابل، لا تقبل العناصر التي أوجدت بغرض التنسيق، مثل <div> و<span> و<table>، التركيز افتراضيًّا. لا يعمل التابع elem.focus()‎ معها، ولا تقع عليها الأحداث focus/blur أبدا. يمكن تغيير ذلك بواسطة السمة tabindex في HTML. يصير أيّ عنصر قابلا للتركيز إذا كانت له السّمة tabindex. تمثّل قيمة السمة رقم ترتيب العنصر بين العناصر عند استخدام المفتاح Tab (أو شيء من هذا القبيل) للتنقّل بينها. هذا يعني لو أنّ لدينا عنصرين، أحدهما له tabindex="1"‎، والثاني له tabindex="2"‎، فإنّ ضغط المفتاح Tab في العنصر الأوّل ينقل التركيز إلى العنصر الثاني. يكون ترتيب التنقّل على النحو التالي: العناصر التي لها tabindex من 1 فصاعدا تأتي أوّلا (حسب ترتيب tabindex)، ثمّ العناصر التي ليس لها tabindex (أي عناصر <input> العاديّة). يُنتقل إلى العناصر التي لها نفس قيمة tabindex حسب ترتيبها في الشيفرة المصدريّة للمستند (الترتيب الافتراضيّ). وهناك قيمتان خاصّتان: tabindex="0"‎ تجعل العنصر ضمن العناصر التي ليس لها tabindex. بمعنى، عند التنقّل بين العناصر، تأتي العناصر التي لها tabindex=0 بعد العناصر التي لها tabindex ≥ 1. تُستخدم عادة لجعل عنصر ما قابلا للتركيز، مع المحافظة على ترتيب التنقّل الافتراضيّ. لجعل العنصر جزءًا من الاستمارة ومضاهيا لـ <input>. tabindex="-1"‎ تمكّن من التركيز برمجيّا فقط على العنصر. يتجاهل المفتاح Tab مثل هذه العناصر، لكنّ التابع elem.focus()‎ يعمل معها. على سبيل المثال، إليك القائمة التالية: Click the first item and press Tab. Keep track of the order. Please note that many subsequent Tabs can move the focus out of the iframe in the example. <ul> <li tabindex="1">One</li> <li tabindex="0">Zero</li> <li tabindex="2">Two</li> <li tabindex="-1">Minus one</li> </ul> <style> li { cursor: pointer; } :focus { outline: 1px dashed green; } </style> See the Pen JS-focus-blur-ex3 by Hsoub (@Hsoub) on CodePen. لو نقرنا على العنصر الأوّل ثم ضغطنا على Tab، سيكون الترتيب هكذا: 1 - 2 - 0. عادة، لا تدعم <li> التركيز، لكنّ tabindex يجعل ذلك ممكنا كلّيّا، بما في ذلك الأحداث وإمكانيّة التنسيق بواسطة focus:. ملاحظة: تعمل الخاصيّة elem.tabIndex أيضا يمكننا إضافة tabindex من خلال جافاسكربت باستخدام الخاصيّة elem.tabIndex. يكون لذلك نفس الأثر. التفويض: focusin/focusout لا تنتشر الأحداث focus وblur نحو الأعلى. على سبيل المثال، لا يمكننا إسناد onfocus إلى <form> لإبرازه، هكذا: <!-- عند التركيز على الاستمارة، أضف الصنف --> <form onfocus="this.className='focused'"> <input type="text" name="name" value="Name"> <input type="text" name="surname" value="Surname"> </form> <style> .focused { outline: 1px solid red; } </style> See the Pen JS-focus-blur-ex4 by Hsoub (@Hsoub) on CodePen. لا يعمل المثال أعلاه، لأنّه عندما يركّز المستخدم على <input>، فإنّ الحدث focus يقع على ذلك المُدخَل فقط. لا ينتشر نحو الأعلى. وبذلك لا يشتغل form.onfocus أبدا. يوجد حلّان. أوّلا، هناك ميزة تاريخيّة طريفة: لا تنتشر الأحداث focus/blur نحو الأعلى، لكنّها تنتشر نحو الأسفل. سيعمل هذا: <form id="form"> <input type="text" name="name" value="Name"> <input type="text" name="surname" value="Surname"> </form> <style> .focused { outline: 1px solid red; } </style> <script> // (true ضع المعالج في مرحلة الانتشار نحو اﻷسفل (قيمة الوسيط اﻷخير form.addEventListener("focus", () => form.classList.add('focused'), true); form.addEventListener("blur", () => form.classList.remove('focused'), true); </script> See the Pen JS-focus-blur-ex5 by Hsoub (@Hsoub) on CodePen. ثانيا، توجد هناك الأحداث focusin وfocusout التي هي مثل focus/blur تماما غير أنّها تنتشر نحو الأعلى. تنبّه أنّه يجب إسنادها باستخدام elem.addEventListener، وليس on<event>‎. وبذلك هذا هو الحلّ البديل: <form id="form"> <input type="text" name="name" value="Name"> <input type="text" name="surname" value="Surname"> </form> <style> .focused { outline: 1px solid red; } </style> <script> form.addEventListener("focusin", () => form.classList.add('focused')); form.addEventListener("focusout", () => form.classList.remove('focused')); </script> See the Pen JS-focus-blur-ex6 by Hsoub (@Hsoub) on CodePen. الملخص تقع الأحداث focus وblur على العنصر عندما يكتسب/يفقد التركيز. خصائصها هي: لا تنتشر نحو الأعلى. يمكن استخدام حالة الانتشار نحو الأسفل بدل ذلك أو focusin/focusout. لا تدعم معظم العناصر التركيز افتراضيًّا. استخدم tabindex لجعل أيّ شيء قابلا للتركيز. يوجد العنصر المُركّز عليه حاليا في document.activeElement. التمارين عنصر div قابل للتحرير الأهميّة:5 أنشئ عنصر<div> بحيث يتحوّل إلى مساحة نصّيّة <textarea> عند النقر عليه. تُمكّن المساحة النصّيّة من التعديل على شيفرة HTML التي بداخل العنصر <div>. عندما يضغط المستخدم على المفتاح Enter أو عندما تفقد المساحة النصّيّة التركيز، فإنّها تعود <div>، ويحلّ محتواها محلّ شيفرة HTML التي كانت بداخل <div>. كما هو مبيّن من هنا. أنجز التمرين في البيئة التجريبية. الحل افتح الحلّ في البيئة التجريبية. التحرير في خانة الجدول عند النقر عليها الأهميّة: 5 اجعل خانات الجدول قابلة للتحرير عند النقر عليها. عند النقر، يجب أن تصير الخانة "قابلة للتحرير" (تظهر مساحة نصّيّة داخلها)، ويمكننا عندها التعديل على HTML. يجب أن لا يكون هناك أي تغيّر في الحجم، تبقى جميع الأبعاد على حالها. تظهر الأزرار OK وCANCEL تحت الخانة لإتمام/إلغاء التحرير. يمكن التحرير في خانة واحدة فقط في نفس الوقت. عندما تكون خانة ما في "وضع التحرير"، تُتجاهل النقرات على الخانات الأخرى. قد يكون في الجدول العديد من الخانات. استخدم تفويض الأحداث. يمكن مشاهدة المثال من هنا. أنجز التمرين في البيئة التجريبية الحل عند النقر، استبدل innerHTML الخاص بالخانة بـ <textarea> بنفس الحجم وبدون إطار. يمكن استخدام جافاسكربت أو CSS لضبط الحجم الصحيح. اجعل textarea.value تساوي td.innerHTML. ركّز على المساحة النصّيّة. أظهر الأزرار OK/CANCEL تحت الخانة، وعالج النقر عليها. افتح الحل في البيئة التجريبية. التحكّم في الفأرة باستخدام لوحة المفاتيح الأهميّة: 4 ركّز على الفأرة، ثمّ استخدم مفاتيح الأسهم لتحريكها. كما في المثال من هنا. ملاحظة: لا تضع معالجات الأحداث في غير العنصر ‎#mouse. ملاحظة أخرى: لا تغيّر HTML/CSS. يجب أن تكون الطريقة عامّة وتعمل مع أيّ عنصر كا. أنجز التمرين في البيئة التجريبية الحل يمكننا استخدام mouse.onclick لمعالجة النقر، وجعل الفأرة "قابلة للتحريك" بواسطة position:fixed، ثمّ استخدام mouse.onkeydown لمعالجة مفاتيح الأسهم. المزلق الوحيد هو أنّ keydown يقع فقط على العناصر التي فيها التركيز. لذا نحتاج إلى إضافة tabindex للعنصر. وبما أنّه يُمنع تغيير HTML، يمكننا استخدام الخاصيّة mouse.tabIndex لذلك. ملاحظة: يمكننا أيضا استبدال mouse.onclick بـmouse.onfocus. ترجمة -وبتصرف- للمقال Focusing: focus/blur من سلسلة Browser: Document, Events, Interfaces لصاحبها Ilya Kantor
  4. تمتلك الاستمارات وعناصر التحكّم فيها، مثل <input>، الكثير من الخاصيّات والأحداث الخاصّة بها. يساعد تعلّم هذه الخاصّيات والأحداث على التعامل مع الاستمارات براحة أكثر. التنقل: الاستمارة والعناصر تنتمي استمارات المستند إلى المجموعة الخاصّة document.forms، وهي ما يُطلق عليها "مجموعةً مُسمّاة (named collection)"، لأنّها مُرتّبة و مُسمّاة في آن واحد. يمكننا الحصول على استمارة ما في المستند إمّا بواسطة الرقم أو الاسم. document.forms.my; // the form with name="my" document.forms[0]; // the first form in the document إذا كان لدينا استمارة ما، فإنّ جميع العناصر التي فيها تكون موجودة في المجموعة المُسمّاة form.elements. على سبيل المثال: <form name="my"> <input name="one" value="1"> <input name="two" value="2"> </form> <script> // الحصول على الاستمارة let form = document.forms.my; // <form name="my"> العنصر // الحصول على العنصر let elem = form.elements.one; // <input name="one"> العنصر alert(elem.value); // 1 </script> See the Pen JS-form-elements-ex1 by Hsoub (@Hsoub) on CodePen. إذا كانت لدينا عدّة عناصر لها نفس الاسم، كما هو الحال عادة مع أزرار الانتقاء (radio buttons)، فإنّ form.elements[name]‎ تكون عبارة عن مجموعة (set). على سبيل المثال: <form> <input type="radio" name="age" value="10"> <input type="radio" name="age" value="20"> </form> <script> let form = document.forms[0]; let ageElems = form.elements.age; alert(ageElems[0]); // [object HTMLInputElement] </script> See the Pen JS-form-elements-ex2 by Hsoub (@Hsoub) on CodePen. لا تتعلّق خاصيّات التنقّل هذه بهيكل الوسوم. جميع عناصر التحكّم، مهما كان عمقها في الاستمارة، موجودة في form.elements. ملاحظة: العناصر Fieldset كاستمارات فرعيّة قد تحتوي الاستمارة على عناصر <fieldset>، التي تمتلك بدورها الخاصيّة elements التي تسرد عناصر التحكّم التي داخلها. على سبيل المثال: <body> <form id="form"> <fieldset name="userFields"> <legend>info</legend> <input name="login" type="text"> </fieldset> </form> <script> alert(form.elements.login); // <input name="login"> let fieldset = form.elements.userFields; alert(fieldset); // HTMLFieldSetElement // fieldset يمكننا الحصول على المُدخل بواسطة الاسم سواء من الاستمارة أو من alert(fieldset.elements.login == form.elements.login); // true </script> </body> See the Pen JS-form-elements-ex3 by Hsoub (@Hsoub) on CodePen. تنبيه: الصيغة المختصرة form.name هناك صيغة مختصرة تمكّننا من الوصول إلى العنصر بواسطة form[index/name]‎. بعبارة أخرى، بدل form.elements.login، يمكننا كتابة form.login. تصلح هذه الطريقة أيضا، لكنّ هناك مشكلة صغيرة: إذا أتينا عنصرا ما، وغيّرنا الاسم name الخاصّ به، فسيظلّ من الممكن الوصول إليه بواسطة الاسم القديم (بالإضافة إلى الاسم الجديد). من السهل رؤية ذلك في هذا مثال: <form id="form"> <input name="login"> </form> <script> alert(form.elements.login == form.login); // <input> نفس الـ ،true form.login.name = "username"; // تغيير اسم المُدخل // :الاسم form.elements غيّرت alert(form.elements.login); // undefined alert(form.elements.username); // input // تتيح الاستمارة كلا الاسمين، القديم والجديد alert(form.username == form.login); // true </script> See the Pen JS-form-elements-ex4 by Hsoub (@Hsoub) on CodePen. لا يشكّل ذلك مشكلة في الغالب، لأنّنا قلّ ما نغّير أسماء العناصر في الاستمارات. الإشارة العكسيّة (Backreference)‏: element.form يمكن الوصول إلى الاستمارة من أيّ عنصر داخلها بواسطة element.form. وبذلك، تشير الاستمارة إلى جميع العناصر، وتشير العناصر إلى الاستمارة، كما هو موضّح في هذه الصورة: على سبيل المثال: <form id="form"> <input type="text" name="login"> </form> <script> // element <- form let login = form.login; // form <- element alert(login.form); // HTMLFormElement </script> See the Pen JS-form-elements-ex5 by Hsoub (@Hsoub) on CodePen. عناصر الاستمارة لنتحدّث عن عناصر التحكّم في الاستمارة. input و textarea يمكننا الوصول إلى القيم التي في هذه العناصر بواسطة input.value (سلسلة نصيّة) أو input.checked (منطقيّة) بالنسبة لصناديق التأشير (checkboxes)، هكذا: input.value = "New value"; textarea.value = "New text"; input.checked = true; // بالنسبة لصناديق التأشير أو أزرار الانتقاء تنبيه: استخدم textarea.value، وليس textarea.innerHTML يُرجى التنبّه إلى أنّه حتى لو كانت القيمة التي في داخل <textarea>...</textarea> هي عبارة عن عناصر HTML متداخلة، فلا ينبغي أبدا أن نستخدم الخاصيّة textarea.innerHTML للوصول إليها. ذلك لأنّ هذه الخاصيّة تحتوي على شيفرة HTML التي كانت في الصفحة بدايةً، وليس القيمة الحاليّة. elect وoptions للعنصر <select> ثلاث خاصيّات مهمّة: select.options -- مجموعة عناصر <option> الفرعيّة. select.value -- قيمة <option> المحدّدة حاليّا. select.selectedIndex -- عدد عناصر <option> المحدّدة حاليّا. وتوفّر هذه الخاصّيات ثلاث طرق مختلفة لإعطاء قيمة إلى <select>: العثور على العنصر المُراد <option>، ثمّ إعطاء option.selected القيمة true. وضع القيمة في select.value. جعل قيمة select.selectedIndex هي رقم الخيار option المُراد. قد تكون الطريقة الأولى هي الأوضح، لكنّ الطريقتين (2) و(3) أكثر ملائمة في الغالب. إليك هذا المثال: <select id="select"> <option value="apple">Apple</option> <option value="pear">Pear</option> <option value="banana">Banana</option> </select> <script> // تؤدي جميع اﻷسطر الثلاث نفس الوظيفة select.options[2].selected = true; select.selectedIndex = 2; select.value = 'banana'; </script> See the Pen JS-form-elements-ex6 by Hsoub (@Hsoub) on CodePen. بخلاف معظم عناصر التحكّم الأخرى، يتيح <select> تحديد عدّة خيارات في نفس الوقت إذا كانت له السمة multiple. قلّ ما تُستخدم هذه الوظيفة، لكن إن كان ولابد، اعتمد الطريقة الأولى: أضف/أزل الخاصيّة selected في عناصر <option> الفرعيّة. يمكن الحصول على مجموعة العناصر الفرعيّة بواسطة select.options، على سبيل المثال: <select id="select" multiple> <option value="blues" selected>Blues</option> <option value="rock" selected>Rock</option> <option value="classic">Classic</option> </select> <script> // الحصول على جميع القيم المُحدّدة من التحديد المتعدّد let selected = Array.from(select.options) .filter(option => option.selected) .map(option => option.value); alert(selected); // blues,rock </script> See the Pen JS-form-elements-ex7 by Hsoub (@Hsoub) on CodePen. يمكن الاطلاع على المواصفة الكاملة للعنصر <select> من هنا. new Option من النادر استعمال هذه الصيغة على انفراد، لكنّ هناك شيئا مثيرا للاهتمام. تحتوي المواصفة على صيغة مختصرة وجميلة لإنشاء عناصر <option>: option = new Option(text, value, defaultSelected, selected); حيث الوسائط: text -- النصّ داخل الخيار، value -- قيمة الخيار، defaultSelected -- إذا كانت true، تُنشأ السمة selected في HTML، selected -- إذا كانت true، فإنّ الخيار يكون محدَّدا. قد يحصل التباس طفيف بين defaultSelected وselected. الأمر بسيط: تهيّئ defaultSelected سمة HTML التي يمكننا الحصول عليها بواسطة option.getAttribute('selected')‎. بينما تبيّن selected ما إذا كان الخيار مُحدّدا أو لا، ولها أهمّية أكبر. قد تكون للخاصيّتين القيمة true أو لا تكون لها قيم بتاتا (وهو كما لو أعطيت القيمة false). على سبيل المثال: let option = new Option("Text", "value"); // <option value="value">Text</option> تنشئ تحديد نفس العنصر: let option = new Option("Text", "value", true, true); لعناصر option الخاصّيات التالية: option.selected : هل الخيار مُحددّ؟ option.index : رقم الخيار بين خيارات <select> الأخرى. option.text : المحتوى النصّي للخيار (الذي يراه الزائر). الملخص التنقّل في الاستمارة: document.forms: يمكن الوصول إلى الاستمارة بواسطة document.forms[name/index]‎. form.elements: يمكن الوصول إلى عناصر الاستمارة بواسطة form.elements[name/index]‎، أو فقط باستخدام form[name/index]‎. تعمل الخاصيّة elements مع <fieldset> كذلك. element.form: تشير إلى الاستمارة الخاصّة بها بواسطة الخاصيّة form. يمكن الوصول إلى القيمة بواسطة input.value وtextarea.value وselect.value وغيرها، بالإضافة إلى input.checked بالنسبة لصناديق التأشير وأزرار الانتقاء. بالنسبة لـ <select>، يمكننا أيضا الحصول على القيمة بواسطة الرقم الاستدلاليّ select.selectedIndex أو من خلال مجموعة الخيارات select.options. هذه هي الأساسيّات التي تمكّن من بدء العمل بالاستمارات. سنقف لاحقا في هذا الدليل على المزيد من الأمثلة. في المقال التالي، سنتناول الأحداث focus وblur التي يمكن أن تقع على أيّ عنصر، لكنّها تُعالج غالبا في الاستمارات. التمارين أضف خيارا إلى select الأهميّة: 5 هناك عنصر <select>: <select id="genres"> <option value="rock">Rock</option> <option value="blues" selected>Blues</option> </select> باستخدام جافاسكربت: أظهر القيمة والنصّ للخيار المُحدّد. أضف الخيار <option value="classic">Classic</option>. اجعله قابلا للتحديد. لاحظ أنّك إذا أنجزت كلّ شيء بشكل صحيح، يجب أن تُظهِر alert الكلمة blues. الحل إليك الحلّ خطوة بخطوة: <select id="genres"> <option value="rock">Rock</option> <option value="blues" selected>Blues</option> </select> <script> // 1) let selectedOption = genres.options[genres.selectedIndex]; alert( selectedOption.value ); // 2) let newOption = new Option("Classic", "classic"); genres.append(newOption); // 3) newOption.selected = true; </script> المراجع المواصفة forms ترجمة -وبتصرف- للمقال Form properties and methods من سلسلة Browser: Document, Events, Interfaces لصاحبها Ilya Kantor
  5. يمكّن الحدث scroll من الاستجابة لتمرير الصفحة أو عنصرٍ ما. يمكننا فعل العديد من الأمور الجيّدة في هذا الباب. على سبيل المثال: إظهار/إخفاء أزرار التحكّم أو معلوماتٍ إضافيّة حسب مكان المستخدم في المستند. تحميل المزيد من البيانات عندما يمرّر المستخدم الصفحة حتى النهاية. إليك دالّة صغيرة تعرض الموضع الحاليّ للتمرير: window.addEventListener('scroll', function() { document.getElementById('showScroll').innerHTML = window.pageYOffset + 'px'; }); عند اشتغالها: See the Pen JS-onscroll-ex01 by Hsoub (@Hsoub) on CodePen. يعمل الحدث scroll سواءً مع النافذة window أو العناصر القابلة للتمرير. منع التمرير كيف يمكن أن نجعل شيئا ما غير قابل للتمرير؟ لا يمكن منع التمرير بمجرّد استخدام event.preventDefault()‎ في منصت الحدث onscroll، لأنّ هذا الحدث يقع بعد حصول التمرير. لكن يمكننا منع التمرير باستخدام event.preventDefault()‎ مع الحدث الذي يتسبّب في التمرير، مثل حدث keydown عند الضغط على المفاتيح pageUp وpageDown. فإذا وضعنا معالجات لهذه اﻷحداث مع event.preventDefault()‎ داخلها، لن يحصل التمرير. هناك عدّة طرق لحصول التمرير، لذا يجدر استخدام الخاصّيّة overflow في CSS. هذه بعض التمارين التي يمكنك حلّها أو النظر فيها للاطلاع على بعض تطبيقات onscroll. التمارين صفحة لا متناهية أنشئ صفحة لا متناهية. عندما يقوم الزائر بالتمرير حتى النهاية، يُلحَق التاريخ والوقت الحاليّان بالنصّ تلقائيّا (ليستطيع الزائر تمرير المزيد)، كما يمكنك مشاهدة ذلك من هنا. يُرجى التنبّه إلى ميزتين مهمّتين للتمرير: التمرير "متمدّد". يمكننا مواصلة التمرير قليلا إلى ما وراء بداية أو نهاية المستند في بعض المتصفّحات/الأجهزة (تظهر مساحة فارغة في اﻷسفل، قبل أن "يرتدّ" المستند كما كان). التمرير غير دقيق. عندما نمرّر الصفحة حتى النهاية، قد نكون في الواقع على بعد حوالي 0-50px من نهاية المستند الحقيقيّة. وبذلك، فإنّ "التمرير حتى النهاية" يعني أنّه لا بدّ للزائر ألّا يبعد أكثر من 100px من نهاية المستند. ملاحظة: قد يكون الغرض في الحالات الواقعيّة عرض "المزيد من الرسائل" أو "المزيد من السلع". أنجز التمرين في البيئة التجريبيّة. الحلّ يكمن الحلّ في إنشاء دالّة تضيف المزيد من التواريخ إلى الصفحة (أو تحمّل المزيد من الأشياء في الحالات الواقعيّة) عندما نكون في آخر الصفحة. يمكننا استدعاؤها مباشرة وإضافتها كمعالج window.onscroll. لكنّ أهمّ سؤال هو: "كيف نعرف أنّ الصفحة قد وصلت إلى اﻷسفل؟". لنستخدم في ذلك الإحداثيّات بالنسبة إلى النافذة. يمكننا الحصول على الإحداثيّات بالنسبة إلى النافذة في كامل المستند بواسطة document.documentElement.getBoundingClientRect()‎، وتكون الخاصيّة bottom هي إحداثيّات أسفل المستند بالنسبة إلى النافذة. على سبيل المثال، إذا افترضنا أنّ ارتفاع كامل مستند HTML هو 2000px، يكون لدينا: // عندما نكون في أعلى الصفحة // القمّة بالنسبة للنافذة = 0 document.documentElement.getBoundingClientRect().top = 0 // الأسفل بالنسبة للنافذة = 2000 // المستند طويل، لذا فإنّه يتجاوز على اﻷرجح أسفل النافذة بكثير document.documentElement.getBoundingClientRect().bottom = 2000 إذا مرّرنا 500px، فإنّ: // 500px أعلى المستند فوق النافذة بـ document.documentElement.getBoundingClientRect().top = -500 // 500px أسفل المستند أقرب بـ document.documentElement.getBoundingClientRect().bottom = 1500 إذا مرّرنا إلى النهاية، بافتراض أنّ ارتفاع النافذة هو 600px: // 1400px أعلى المستند فوق النافذة بـ document.documentElement.getBoundingClientRect().top = -1400 // 600px أسفل المستند تحت النافذة بـ document.documentElement.getBoundingClientRect().bottom = 600 يُرجى التنبّه أنّ اﻷسفل bottom لا يمكن أن يكون 0 لأنّه لا يرتقي إلى أعلى النافذة أبدا. أقلّ حدّ للإحداثيّة bottom هو ارتفاع النافذة (افترضنا أنّها 600)، لا نستطيع تمريرها أعلى من ذلك. يمكننا الحصول على ارتفاع النافذة بواسطة document.documentElement.clientHeight. في هذا التمرين، نحتاج أن نعرف متى نبعد عن أسفل المستند بأقلّ من 100px (بمعنى يكون أسفل المستند 600-700px بالنسبة للنافذة، إذا كان ارتفاعها 600). هذه هي الدالّة إذًا: function populate() { while(true) { // أسفل المستند let windowRelativeBottom = document.documentElement.getBoundingClientRect().bottom; // على النهاية)‏‎ 100px إذا لم يمرّر المستخدم بعيدا بما يكفي (أكثر من if (windowRelativeBottom > document.documentElement.clientHeight + 100) break; // لنُضف المزيد من البيانات document.body.insertAdjacentHTML("beforeend", `<p>Date: ${new Date()}</p>`); } } شاهد الحلّ في البيئة التجريبيّة. زرّ اﻷعلى/اﻷسفل اﻷهميّة:5 أنشئ زرّ "الانتقال نحو اﻷعلى" للمساعدة على تمرير الصفحة. يجب أن يعمل كالتالي: إذا لم تُمرَّر النافذة نحو اﻷسفل بمقدار ارتفاع النافذة على اﻷقلّ، فإنّه لا يظهر. إذا مُرّرت الصفحة نحو اﻷسفل بمقدار يزيد على ارتفاع النافذة، يظهر سهم يشير نحو اﻷعلى في الزاوية العليا من اليسار. إذا أعيد تمرير الصفحة نحو اﻷعلى، فإنّه يختفي. عند الضغط على السهم، فإنّ الصّفحة تُمرّر حتى القمّة. كما هو مبيّن هنا (في الزاوية العليا من اليسار، مرّر الصفحة لتراه). أنجز التمرين في البيئة التجريبيّة. الحل شاهد الحلّ في البيئة التجريبيّة. حمّل الصور المرئيّة اﻷهميّة: 4 لنفترض أنّ لدينا عميلا ذا سرعة قليلة ونريد أن نحفظ له بيانات الجوّال. فقرّرنا لهذا الغرض ألّا نظهر الصور فورا، بل نضع مكانها نائبًا (placeholder)، بهذا الشكل: <img src="placeholder.svg" width="128" height="128" data-src="real.jpg"> في البداية، تكون كلّ الصّور placeholder.svg. وعندما تُمرّر الصفحة إلى موضع يمكن للمستخدم فيه أن يرى الصورة، نغيّر قيمة src إلى القيمة الموجودة في data-src، فتُحمَّل الصّورة. يمكن مشاهدة المثال حيّا من هنا. قم بالتمرير لترى الصور تُحمّل "حسب الطلب". المتطلّبات: عندما تُحمّل الصفحة، يجب أن تُحمّل الصور التي على الشاشة فورا، قبل حصول التمرير. قد تكون بعض الصور عاديّة، دون data-src. يجب أن تتركها الشيفرة على حالها. بعد انتهاء تحميل الصورة، يجب ألّا يُعاد تحميلها مرّة أخرى عند تمريرها إلى اﻷسفل/اﻷعلى. ملاحظة: إذا أمكن، أنجز حلّا أكثر تقدّما يتيح "التحميل المسبق" للصور التي تكون قبل/بعد الموضع الحاليّ بمقدار صفحة. ملاحظة أخرى: ينبغي فقط معالجة التمرير العموديّ، لا التمرير الأفقيّ. أنجز التمرين في البيئة التجريبيّة. الحل يجب أن يعمل المعالجُ onscroll على تحديد الصّور مرئيّة وعرضها. نريده أيضا أن يشتغل عند تحميل الصفحة، لاكتشاف الصور المرئيّة فورا وعرضها. يجب أن تشتغل الشيفرة بعد الانتهاء من تحميل المستند، لتتمكّن من الوصول إلى محتواه. أو توضع أسفل العنصر <body>. // ...محتوى الصفحة الفوق... function isVisible(elem) { let coords = elem.getBoundingClientRect(); let windowHeight = document.documentElement.clientHeight; // مرئيّ؟ elem هل الحدّ العلويّ لـ let topVisible = coords.top > 0 && coords.top < windowHeight; // مرئيّ؟ elem هل الحدّ السفليّ لـ let bottomVisible = coords.bottom < windowHeight && coords.bottom > 0; return topVisible || bottomVisible; } تستخدم الدالّةُ showVisible()‎ اختبارَ الرؤية، المُنفَّذ بواسطة isVisible()‎، لتحميل الصور المرئيّة: function showVisible() { for (let img of document.querySelectorAll('img')) { let realSrc = img.dataset.src; if (!realSrc) continue; if (isVisible(img)) { img.src = realSrc; img.dataset.src = ''; } } } showVisible(); window.onscroll = showVisible; ملاحظة: هناك أيضا حلّ بديل حيث تعمل فيه isVisible " على "التحميل المسبق" للصور الموجودة ضمن صفحة واحدة أعلى/أسفل موضع التمرير الحاليّ للمستند. ترجمة -وبتصرف- للمقال Scrolling من سلسلة Browser: Document, Events, Interfaces لصاحبها Ilya Kantor
  6. قبل أن نأتي إلى لوحة المفاتيح، يُرجى التنبّه إلى أنّ للأجهزة الحديثة سُبلًا أخرى إلى "إدخال شيء ما". فقد يستخدم الناس مثلا تقنيّة التعرّف على الكلام (خاصّة على اﻷجهزة المحمولة) أو النسخ/اللصق بواسطة الفأرة. ولذلك، إذا أردنا تتبّع جميع المُدخلات في الحقل <input>، فإنّ أحداث لوحة المفاتيح لا تفي بالغرض. يوجد هناك حدث آخر اسمه input لتتبّع التغيّرات التي تحصل في الحقل <input>، مهما كانت الوسيلة. وقد يكون الخيار اﻷفضل لهذه المهمّة. سنتناول هذا الحدث لاحقا في مقال يشرح اﻷحداث change وinput وcut وcopy وpaste. ينبغي استخدام أحداث لوحة المفاتيح عند التعامل مع اﻷفعال التي تحصل بواسطة لوحة المفاتيح (يدخل في ذلك لوحة المفاتيح الافتراضيّة)، كالاستجابة للضغط على المفاتيح السهميّة Up وDown أو مفاتيح الاختصارات (بما في ذلك الجمع بين المفاتيح). منصة الاختبار لفهم أحداث لوحة المفاتيح بشكل أفضل، يمكنك استخدام منصّة الاختبار الموجودة هنا أو المثال الحي الآتي. جرّب تجميعات مختلفة من المفاتيح في المساحة النصيّة. See the Pen JS-keyboard-events-ex01 by Hsoub (@Hsoub) on CodePen. Keydown وkeyup يقع حدث keydown عندما يُضغط مفتاح ما، ثمّ يقع keyup عندما يُحرّر. event.code وevent.key تسمح الخاصّيّة key لكائن الحدث بالحصول على المحرف، بينما تُمكّنه الخاصّيّة code من الحصول على "كود المفتاح الملموس". على سبيل المثال، يمكن أن يُضغط نفس المفتاح Z مع أو بدون المفتاح Shift. سيعطينا ذلك محرفان مختلفان: z الصغير وZ الكبير. تعبّر الخاصّيّة event.key عن المحرف، وقد تختلف. لكنّ event.code تبقى نفسها: Key ‏ event.key ‏ event.code Z ‏z (الصغير) KeyZ Shift+Z ‏Z (الكبير) KeyZ table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } إذا كان المستخدم يستعمل لغات مختلفة، فإنّ تغييره للغة أخرى سيعطي محرفا مختلفا تماما بدل "Z". وسيصبح ذلك هو قيمة event.key، بينما تبقى event.code هي نفسها دائما: "KeyZ". ملاحظة: "KeyZ" وغيرها من أكواد المفاتيح لكلّ مفتاحٍ كود يتعلّق بمكانه على لوحة المفاتيح. أكواد المفاتيح موضّحة في مواصفة أكواد أحداث واجهة المستخدم. على سبيل المثال: مفاتيح الأحرف لها أكواد من الشكل "Key<letter>‎"‏: "KeyA" و"KeyB" وهكذا. مفاتيح اﻷرقام لها أكواد من الشكل "Digit<number>‎": ‏"Digit0" و"Digit1" وهكذا. المفاتيح الخاصّة مكوّدة بأسمائها "Enter" و"Backspace" و"Tab" وهكذا. هناك عدّة تخطيطات (layouts) شائعة للوحات المفاتيح، وتعطي المواصفة أكواد مفاتيح لكلّ منها؛ طالع قسم الحروف اﻷبجديّة واﻷرقام في المواصفة لمزيد من اﻷكواد، أو اضغط فقط على المفتاح في منصّة الاختبار الموجودة هنا. تنبيه: حجم الأحرف مهمّ: "KeyZ" وليس "keyZ" يبدو ذلك واضحا، لكن لا يزال النّاس يخطئون في ذلك. يُرجى تجنّب الأخطاء عند الكتابة: إنّه KeyZ وليس keyZ. لن ينجح التحقّق event.code=="keyZ" مثلا، إذ يجب أن يكون أوّل حرف من "Key" كبيرا. لكن ماذا لو لم يكن المفتاح يعطي أيّ محرف؟ مثل Shift أو F1 أو غير ذلك. بالنسبة لتلك المفاتيح، event.key هي في الغالب نفس event.code: Key ‏ event.key ‏event.code F1 ‏F1 ‏ ‏‏F1 Backspace ‏Backspace ‏Backspace Shift ‏Shift ‏ShiftRight أو ShiftLeft يُرجى التنبّه أنّ event.code يبيّن المفتاح قد ضُعط بالضبط. فعلى سبيل المثال، تملك أغلب لوحات المفاتيح مفتاحي Shift، على اليمين وعلى اليسار. يخبرنا event.code أيّ واحد قد ضُغط بالضبط، أمّا event.key فهو مسؤول عن "معنى" المفتاح: ما هو (إنّه "تبديل"). لنفترض أنّنا نريد معالجة أحد مفاتيح الاختصارات: Ctrl+Z (أو Cmd+Z في ماك). تربط معظم محرّرات النّصوص فعل "التراجع" بها. يمكننا وضع منصت إلى keydown وتفحّص المفتاح الذي ضُغط. لكنّ هناك معضلة هنا: في مثل هذه المنصتات، هل يجب علينا تفحّص قيمة event.key أو event.code؟ من جهة، قيمة event.key هي المحرف، وتتغيّر حسب اللغة. فإذا كان للزائر عدّة لغات في نظام التشغيل وينتقل بينها، فإنّ نفس المفتاح يعطي محرفات مختلفة. لذا فمن المنطقيّ تفحّص event.code، لأنّ لها نفس القيمة دائما. هكذا: document.addEventListener('keydown', function(event) { if (event.code == 'KeyZ' && (event.ctrlKey || event.metaKey)) { alert('Undo!') } }); في الجهة المقابلة، هناك مشكلة مع event.code. مع اختلاف تخطيطات لوحة المفاتيح، قد يكون لنفس المفتاح محرفات مختلفة. على سبيل المثال، إليك التخطيط اﻷمريكيّ ("QWERTY") والتخطيط الألمانيّ ("QWERTZ") تحته (من ويكيبيديا): عند نفس المفتاح، نجد في التخطيط اﻷمريكيّ "Z"، بينما في التخطيط اﻷلمانيّ "Y" (اﻷحرف منعكسة). حرفيًّا، تكون قيمة event.code هي KeyZ بالنسبة للذين عندهم التخطيط اﻷلماني عند الضغط على المفتاح Y. لو أجرينا التحقّق event.code == 'KeyZ'‎ في شيفرتنا، فإنّ الناس الذين عندهم التخطيط اﻷلمانيّ سينجح معهم هذا التحقّق إذا ضغطوا على المفتاح "Y". يبدو هذا غريبا حقّا، لكنّه كذلك. تذكر المواصفة هذا السلوك صراحة. من الممكن إذًا أن يوافق event.code المحرفَ الخطأ في التخطيط غير المتوقّع. فقد تقابل نفس اﻷحرف مفاتيح ملموسة مختلفة باختلاف التخطيطات، مما يؤدّي إلى أكواد مختلفة. لحسن الحظّ، يحدث هذا مع بعض اﻷكواد فقط مثل keyA وkeyQ وkeyZ (كما رأينا)، ولا يحدث مع المفاتيح الخاصّة مثل Shift. يمكن أن تجد القائمة في المواصفة. لتتبّع المحرفات التي تتعلّق بالتخطيط بشكل موثوق، قد يكون event.key هو الطريقة اﻷفضل. في المقابل، يمتاز event.code بأنّه يكون هو نفسه على الدوام، ومرتبطا بمكان المفتاح الملموس، حتى لو غيّر الزائر اللغة. وبذلك، ستعمل مفاتيح الاختصارات التي تعتمد عليه حتى في حال تغيير اللغة. إذا كنّا نودّ معالجة المفاتيح التي تتعلّق بالتخطيط، فإنّ event.key هو الحلّ. وإذا كنّا نريد لمفاتيح الاختصارات أن تعمل ولو عند تغيير اللغة، فقد يكون event.code هو اﻷفضل. التكرار التلقائي إذا استغرق الضغط على مفتاح ما مدّة طويلة بما يكفي، فإنّه يبتدئ "التكرار التلقائيّ": يقع الحدث keydown مرار وتكرار، إلى أن يُحرّر المفتاح فنحصل عندها على keyup. لذلك قد يكون من الطبيعيّ أن نحصل على العديد من keydown بالإضافة إلى keyup وحيد. إذا وقعت الأحداث بواسطة التكرار التلقائيّ، فإنّ قيمة الخاصّية event.repeat لكائن الحدث تكون true. اﻷفعال الافتراضيّة تتنوّع الأفعال الافتراضيّة، إذ هناك العديد من اﻷمور التي قد تحصل بواسطة لوحة المفاتيح. على سبيل المثال: يظهر محرف على الشاشة (النتيجة اﻷظهر). يُحذف محرف (مفتاح Delete). تُمرَّر الصفحة (مفتاح PageDown). يفتح المتصفّح حوار "حفظ الصفحة" (Ctrl+S). …وهكذا. قد يؤدّي منع الفعل الافتراضيّ عند keydown إلى إلغاء معظمها، باستثناء المفاتيح الخاصّة المضمّنة في نظام التشغيل. على سبيل المثال، يعمل Alt+F4 في ويندوز على إغلاق نافذة المتصفّح الحاليّة. ولا سبيل إلى إيقاف ذلك بواسطة منع الفعل الافتراضيّ في جافاسكربت. مثلا، يتوقّع المُدخل <input> أدناه رقم هاتف، فلا يقبل المفاتيح التي بخلاف اﻷرقام أو + أو () أو -: <script> function checkPhoneKey(key) { return (key >= '0' && key <= '9') || key == '+' || key == '(' || key == ')' || key == '-'; } </script> <input onkeydown="return checkPhoneKey(event.key)" placeholder="Phone, please" type="tel"> See the Pen JS-keyboard-events-ex02 by Hsoub (@Hsoub) on CodePen. يُرجى التنبّه أنّ المفاتيح الخاصّة، مثل Backspace وLeft وRight وCtrl+V، لا تعمل في المُدخل. هذا أثر جانبيّ للمرشّح checkPhoneKey الصارم. لنقم بإرخائه بعض الشيء: <script> function checkPhoneKey(key) { return (key >= '0' && key <= '9') || key == '+' || key == '(' || key == ')' || key == '-' || key == 'ArrowLeft' || key == 'ArrowRight' || key == 'Delete' || key == 'Backspace'; } </script> <input onkeydown="return checkPhoneKey(event.key)" placeholder="Phone, please" type="tel"> See the Pen JS-keyboard-events-ex03 by Hsoub (@Hsoub) on CodePen. تعمل كلّ من مفاتيح اﻷسهم والحذف جيّدا الآن. لكن لايزال بإمكاننا إدخال أيّ شيء باستخدام الفأرة عن طريق النقر باليمين ثمّ اللصق. فالمرشّح إذًا ليس موثوقا مئة بالمئة. يمكننا تركه كذلك، لأنّه يعمل في غالب الأحيان. أو بدلا من ذلك، يمكننا تتبّع حدث input الذي يقع عند كلّ تغيير، ونتفحّصه عندئذ و نبرزه/نعدّله إذا لم يكن صالحًا. الأحداث والخاصيات القديمة في القديم، كان هناك الحدث keypress، بالإضافة إلى خاصّيّات كائن الحدث keyCode وcharCode و which. كان هناك الكثير من حالات عدم توافق المتصفّح عند العمل بها، مما أجبر العاملين على المواصفة على التخلّي عنها جميعا وإنشاء أحداث جديدة (كما بُيّنت في أعلى هذا المقال). لا تزال الشيفرات القديمة تعمل، إذ بقيت المتصفّحات تدعمها، لكن ليس هناك سبب إطلاقا لاستخدامها الآن. الملخص يؤدّي الضغط على مفتاح ما إلى توليد حدث من أحداث لوحة المفاتيح، سواءً كان مفتاح رمز أو مفتاحا خاصّا مثل Shift أو Ctrl أو غيرها. الاستثناء الوحيد هو المفتاح Fn الذي يكون أحيانا في الحواسيب المحمولة. ليس هناك حدث لوحة مفاتيح له، لأنّه يكون في الغالب في مستوى أقلّ انخفاضا من مستوى نظام التشغيل. أحداث لوحة المفاتيح: keydown -- عند الضغط على المفتاح (يتكرّر تلقائيّا إذا ضُغط المفتاح طويلا)، keyup -- عند تحرير المفتاح. خاصّيات أحداث لوحة المفاتيح اﻷساسيّة: code -- "كود المفتاح" ("KeyA" و"ArrowLeft" وهكذا)، متعلّق بالمكان الملموس للمفتاح على لوحة المفاتيح. key -- المحرف ("A" و"a" وهكذا). لمفاتيح غير المحارف، مثل Esc، تكون له نفس قيمة code عادة. في السابق، كانت أحداث لوحة المفاتيح تُستعمل أحيانا لتتبّع مُدخلات المستخدم في حقول النماذج. هذا غير موثوق، لأنّ المُدخلات قد تأتي من مصادر متنوّعة. لذلك، لدينا الأحداث input وchange التي تمكّننا من معالجة أيّ مدخل كان (سنتطرّق لها لاحقا في مقال اﻷحداث change وinput وcut وcopy وpaste). تقع تلك اﻷحداث عند إدخال أيّ نوع من المدخلات، بما في ذلك النسخ واللصق والتعرّف على الكلام. التمارين مفاتيح الاختصارات الموسعة الأهميّة: 5 أنشئ الدالّة runOnKeys(func, code1, code2, ... code_n)‎ التي تعمل على بتنفيذ func عند الضغط في نفس الوقت على المفاتيح التي لها اﻷكواد code1, code2, …, code_n. على سبيل المثال، تظهر الشيفرة أدناه alert عندما تُضغط المفاتيح "Q" و"W" معا (في أيّ لغة، مع أو بدون تفعيل الأحرف الكبيرة). runOnKeys( () => alert("Hello!"), "KeyQ", "KeyW" ); يمكن مشاهدة المثال يعمل من هنا. الحل لابدّ أن نستخدم اثنين من المعالجات: document.onkeydown وdocument.onkeyup. لننشئ المجموعة pressed = new Set()‎ لحفظ المفاتيح المضغوطة حاليّا. يعمل المعالج اﻷوّل على الإضافة إليها، بينما يعمل المعالج الثاني على النزع منها. وكلّما وقع الحدث keydown نتحقّق إن كان لدينا ما يكفي من المفاتيح المضغوطة، وننفّذ الدالّة إذا كان الحال كذلك. افتح الحلّ في البيئة التجريبيّة ترجمة -وبتصرف- للمقال Keyboard: keydown and keyup من سلسلة Browser: Document, Events, Interfaces لصاحبها Ilya Kantor
  7. تُعدّ أحداث المؤشّر طريقة حديثة للتعامل مع المُدخلات النابعة من مختلف أجهزة التأشير، كالفأرة والقلم والشاشات اللمسيّة وغيرها. لمحة تاريخية مختصرة لنأخذ بلمحة قصيرة، لتتمكّن من فهم الصورة العامّة وكذا مكانة أحداث المؤشّر وسط أنواع اﻷحداث اﻷخرى. في القديم، لم تكن هناك سوى أحداث الفأرة. ثمّ انتشرت اﻷجهزة اللمسيّة، مثل الهواتف واﻷجهزة اللوحيّة. ولكي تعمل النصوص البرمجيّة القائمة، كانت هذه اﻷجهزة (ولا زالت) تولّد أحداث الفأرة. فعلى سبيل المثال، يولّد الضغط على شاشة لمسيّة الحدث 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 - table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } كما يمكن أن نرى، لكلّ حدث فأرة 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. See the Pen JS-p2-pointer-events-ex01 by Hsoub (@Hsoub) on CodePen. يمكنك مشاهدة هذا المثال من هنا الذي يعمل على تسجيل اﻷحداث pointerdown وpointerup. يُرجى التنبّه: يجب أن تستخدم جهازا لمسيّا، كهاتف أو جهاز لوحيّ، لرؤية الفرق في pointerId/isPrimary. بالنسبة للأجهزة الأحاديّة اللمس، كالفأرة مثلا، يكون هناك دائما نفس الـ pointerId وisPrimary=true ، عند جميع أحداث المؤشّر. الحدث pointercancel ينطلق الحدث pointercancel عندما يبدأ هناك تفاعل مع المؤشّر، ثمّ يحصل شيء ما يؤدّي إلى إلغاء ذلك، فلا تتولّد المزيد من أحداث المؤشّر. قد يتسبّب في ذلك عذّة أشياء: تعطيل عتاد جهاز التأشير تعطيلا حسّيًّا. تغيير اتجاه الجهاز (كتدوير الجهاز اللوحيّ). قرار المتصفّح معالجة التفاعل بنفسه، ظنًّا منه أنّها حركة للفأرة أو حركة تكبير وتحريك أو غير ذلك. سنستعرض الحدث pointercancel في مثال عمليّ لنرى كيف يمكن أن يمسّنا. لنفترض أنّنا نودّ إنجاز سحب وإفلات لكرة ما، تماما كما في بداية المقال السحب والإفلات باستخدام أحداث الفأرة. ستأخذ أفعال المستخدم والأحداث المقابلة لها المجرى التالي: يضغط المستخدم على الصورة لابتداء السحب ينطلق الحدث pointerdown ثم يبدأ بتحريك المؤشّر (وبذلك سحب الصورة) ينطلق الحدث pointermove، ربّما عدّة مرّات وبعدها تحصل المفاجأة! للمتصفّح دعم أصيل (native) لسحب وإفلات الصور، فيستجيب ويستولي على عمليّة السحب والإفلات، مولّدا بذلك الحدث pointercancel. يعالج المتصفّح الآن سحب وإفلات الصورة بنفسه. يستطيع المستخدم سحب صورة الكرة حتى إلى خارج المتصفّح، إلى برنامج البريد الخاصّ به أو إلى مدير الملفّات. لا مزيد من الأحداث pointermove بالنسبة لنا. فالمشكلة إذًا هي أنّ المتصفّح "يختطف" التفاعل: ينطلق الحدث pointercancel في بداية عمليّة "السحب والإفلات"، ولا تتولّد المزيد من أحداث pointermove. See the Pen JS-p2-pointer-events-ex02 by Hsoub (@Hsoub) on CodePen. يمكن مشاهدة السحب والإفلات من هنا، مع تسجيلٍ لأحداث المؤشّر (up/down وmove وcancel فقط) في المساحة النصيّة textarea. نريد الآن إنجاز السحب والإفلات بأنفسنا، فلنطلب من المتصفّح إذًا عدم الاستيلاء عليه. منع فعل المتصفّح الافتراضيّ لتجنّب pointercancel. نحتاج إلى فعل أمرين: منع السحب والإفلات اﻷصيل من الوقوع: يمكننا فعل ذلك عن طريق وضع ball.ondragstart = () => false، تماما كما هو موضّح في المقال السحب والإفلات باستخدام أحداث الفأرة. يعمل ذلك جيّدا مع أحداث الفأرة. بالنسبة للأجهزة اللمسيّة، هناك أفعال أخرى للمتصفّح متعلّقة باللمس (عدا السحب والإفلات). لتجنّب المشاكل معها أيضا: يمكن منعها بواسطة وضع ‎#ball { touch-action: none }‎ في CSS. عندئذ ستصير شيفرتنا تعمل على اﻷجهزة اللمسيّة. بعد القيام بذلك، ستعمل اﻷحداث كما ينبغي، ولن يختطف المتصفّح العمليّة ولن يرسل الحدث pointercancel. See the Pen JS-p2-pointer-events-ex03 by Hsoub (@Hsoub) on CodePen. يضيف المثال السابق (تجربه حية) هذه اﻷسطر. وكما ترى، لا مزيد من اﻷحداث 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 إذ تقع عند See the Pen JS-p2-pointer-events-ex04 by Hsoub (@Hsoub) on CodePen. يمكن مشاهدة المثال كاملا من هنا. في النهاية، لأسر المؤشّر فائدتان: تصير الشيفرة أنظف، إذ لا نحتاج إلى إضافة/إزالة المعالجات إلى كامل المستند. يُفكّ الرّبط تلقائيّا. إذا كانت هناك أيّة معالجات 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
  8. يُعدّ السحب والإفلات وسيلة جيّدة في واجهة المستخدم. إذ أنّ أخذ الشيء وسحبه ثمّ إفلاته طريقة واضحة وبسيطة لفعل الكثير من اﻷمور، من نسخ المستندات ونقلها (كما في مدير الملفّات) إلى إجراء الطلبات (كوضع السلع في سلّة المشتريات). في مواصفة HTML الحديثة، هناك قسم حول السحب والإفلات مع أحداث خاصّة بها مثل dragstart وdragend وغيرها. تمكّننا هذه اﻷحداث من دعم أنواع مخصّصة من السحب والإفلات، كالتعامل مع سحب ملفّ من مدير الملفّات في نظام التشغيل وإفلاته في نافذة المتصفّح، لتتمكّن جافاسكربت عندئذ من الوصول إلى محتوى هذه الملفّات. لكن في المقابل، لأحداث السحب اﻷصيلة تلك حدود أيضًا. فعلى سبيل المثال، لا يمكننا منع السحب في مساحات معيّنة. كما لا يمكننا حصر اتجاه السحب إمّا "أفقيّا" أو "عموديّا". وهناك أيضا العديد من مهامّ السحب والإفلات التي لا يمكن فعلها بواسطة هذه اﻷحداث. لذا سنرى هنا كيفيّة إنجاز السحب والإفلات باستخدام أحداث الفأرة. خوارزمية السحب والإفلات يمكن وصف الخوارزميّة اﻷساسيّة للسحب والإفلات كالتالي: عند mousedown، جهّز العنصر لتحريكه، عند الحاجة لذلك (ربما بإنشاء نسخة عنه، أو إضافة صنف له أو أيًّا كان ذلك). ثمّ عند mousemove، حرّكه عن طريق تغيير left/top بواسطة position:absolute. عند mouseup، قم بجميع اﻷفعال المرتبطة بإنهاء السحب والإفلات. هذه هي الأساسيّات. سنرى لاحقا كيفيّة إضافة ميزات أخرى، مثل إبراز العناصر التحتيّة الحاليّة عند السحب فوقها. هذا تطبيق لعمليّة السحب على كرة: ball.onmousedown = function(event) { // (1) z-index تجهّز للتحريك: اجعل وضعيّة الكرة مطلقة وفي الأعلى بواسطة ball.style.position = 'absolute'; ball.style.zIndex = 1000; // مباشرة body افصلها عن أيّ آباءٍ لها وألحقها بـ // body اجعلها متموضعة بالنسبة لـ document.body.append(ball); // (pageX, pageY) مركِز الكرةَ في الإحداثيّات function moveAt(pageX, pageY) { ball.style.left = pageX - ball.offsetWidth / 2 + 'px'; ball.style.top = pageY - ball.offsetHeight / 2 + 'px'; } // حرّك الكرة المطلقة التموضع أسفل المؤشّر moveAt(event.pageX, event.pageY); function onMouseMove(event) { moveAt(event.pageX, event.pageY); } // (2) mousemove حرّك الكرة عند document.addEventListener('mousemove', onMouseMove); // (3) أفلت الكرة وأزل المعالجات غير المرغوبة ball.onmouseup = function() { document.removeEventListener('mousemove', onMouseMove); ball.onmouseup = null; }; }; إذا أجرينا الشيفرة، يمكننا ملاحظة شيء غريب. في بداية السحب والإفلات، "تُستنسخ" الكرة، ونقوم بسحب "نسختها". (يمكن مشاهدة المثال يعمل من هنا). See the Pen PobeaRq by Hsoub (@Hsoub) on CodePen. جرّب اسحب الكرة وأفلتها باستخدام الفأرة وسترى هذا السّلوك. يرجع هذا إلى أنّ للمتصفّح دعمه الخاصّ لسحب وإفلات الصور وبعض العناصر اﻷخرى. وهو يجري بشكل تلقائيّ ويتعارض مع ما نقوم به. لتعطيله: ball.ondragstart = function() { return false; }; سيكون الآن كلّ شيء على ما يرام، كما يمكن رؤية ذلك من هنا. See the Pen JS-p2-mouse-drag-and-drop-ex02 by Hsoub (@Hsoub) on CodePen. هناك جانب مهمّ آخر. نتتبّع الحدث mousemove في document، وليس في ball. من الوهلة اﻷولى، قد يبدو اﻷمر وكأنّ الفأرة تكون دائما فوق الكرة، وبذلك يمكن أن ننصت إلى mousemove من هناك. لكن كما تذكر، يقع الحدث mousemove بكثرة، ولكن ليس من أجل كلّ بكسل. بحركة سريعة يمكن أن يقفز المؤشّر من الكرة إلى مكان ما وسط المستند (أو حتى خارج النافذة). ولهذا يجب أن ننصت إلى الحدث في document لالتقاطه. التموضع الصحيح في الأمثلة أعلاه تتحرّك الكرة ويبقى منتصفها تحت المؤشّر على الدوام: ball.style.left = pageX - ball.offsetWidth / 2 + 'px'; ball.style.top = pageY - ball.offsetHeight / 2 + 'px'; هذا أمر جيّد، لكنّ له أثرًا جانبيًّا. عند السحب والإفلات، قد نُحدث mousedown في أيّ مكان على الكرة. فإذا "أخذناها" من حافّتها، فسوف "تقفز" الكرة فجأة لتصبح متمركزة تحت مؤشّر الفأرة. قد يكون من اﻷفضل الإبقاء على الإزاحة اﻷوليّة للعنصر بالنسبة للمؤشّر. على سبيل المثال، إذا بدأنا السحب من حافّة الكرة، فإنّ المؤشّر يبقى على الحافّة خلال السحب. لنحدّث الخوارزميّة: أولًا، عندما يضغط الزائر على الزرّ (mousedown)، احفظ المسافة من المؤشّر إلى الزاوية العليا من اليسار للكرة في المتغيّرات shiftX/shiftY. سنحافظ على تلك المسافة خلال السحب. يمكننا الحصول على تلك الإزاحات عن طريق طرح الإحداثيّات: // onmousedown في let shiftX = event.clientX - ball.getBoundingClientRect().left; let shiftY = event.clientY - ball.getBoundingClientRect().top; ثانيًا، بعد ذلك نُبقي الكرة على نفس الإزاحة بالنسبة للمؤشّر خلال السحب هكذا: // onmousemove في // position:absolute الكرة لها ball.style.left = event.pageX - *!*shiftX*/!* + 'px'; ball.style.top = event.pageY - *!*shiftY*/!* + 'px'; هذه الشيفرة النهائيّة لتموضع أفضل: ball.onmousedown = function(event) { let shiftX = event.clientX - ball.getBoundingClientRect().left; let shiftY = event.clientY - ball.getBoundingClientRect().top; ball.style.position = 'absolute'; ball.style.zIndex = 1000; document.body.append(ball); moveAt(event.pageX, event.pageY); // (pageX, pageY) تحريك الكرة إلى الإحداثيّات // مع أخذ الإزاحة اﻷوّليّة في الحسبان function moveAt(pageX, pageY) { ball.style.left = pageX - *!*shiftX*/!* + 'px'; ball.style.top = pageY - *!*shiftY*/!* + 'px'; } function onMouseMove(event) { moveAt(event.pageX, event.pageY); } // mousemove حرّك الكرة عند document.addEventListener('mousemove', onMouseMove); // أفلت الكرة وأزل المعالجات غير المرغوبة ball.onmouseup = function() { document.removeEventListener('mousemove', onMouseMove); ball.onmouseup = null; }; }; ball.ondragstart = function() { return false; }; يمكن مشاهدة ذلك يعمل من هنا. See the Pen JS-p2-mouse-drag-and-drop-ex03 by Hsoub (@Hsoub) on CodePen. يظهر الفرق جليًّا إذا جررنا الكرة من زاويتها السفلى من اليمين. في المثال السابق "تقفز" الكرة تحت المؤشر. أمّا الآن فهي تتبع المؤشّر بسلاسة من وضعيّتها اﻷوليّة. أهداف الإفلات المحتملة (العناصر القابلة للإفلات فوقها) في اﻷمثلة السابقة، يمكن أن تُفلت الكرةُ "في أيّ مكان" ستبقى فيه. ما نقوم به في الواقع عادة، هو أخذ عنصر ما وإفلاته داخل عنصر آخر، كإفلات "ملفّ" داخل "مجلّد" أو غير ذلك. بعبارة مجملة، نأخذ عنصرا "قابلا للسحب" ونُفلته داخل عنصر "قابل للإفلات فوقه". نحتاج لذلك إلى معرفة: مكان إفلات العنصر في نهاية السحب والإفلات -- للقيام بالفعل المناسب، ومن المستحسن، العناصر القابلة للإفلات فوقها عند السحب فوقها، لإبرازها. قد يكون الحلّ مثيرا للاهتمام نوعا ما، وشائكًا بعض الشيء لذا سنعمل على شرحه هنا. ما الفكرة التي يمكن أن تتبادر إلى الذهن؟ ربّما إسناد معالجات لـ mouseover/mouseup إلى العناصر المحتملة القابلة للإفلات فوقها؟ لن يعمل ذلك. المشكلة هي أنّه خلال قيامنا بالسحب، يكون العنصر القابل للسحب دائما في أعلى العناصر اﻷخرى. وتقع أحداث الفأرة على العنصر العلويّ فقط، لا على التي تحته. على سبيل المثال، يوجد في اﻷسفل عنصرا <div>، حيث أنّ العنصر اﻷحمر فوق العنصر اﻷزرق (يغطّيه تماما). لا سبيل إلى التقاط حدثٍ في العنصر اﻷزرق لأنّ اﻷحمر موجود أعلاه: <style> div { width: 50px; height: 50px; position: absolute; top: 0; } </style> <div style="background:blue" onmouseover="alert('never works')"></div> <div style="background:red" onmouseover="alert('over red!')"></div> See the Pen JS-p2-mouse-drag-and-drop-ex04 by Hsoub (@Hsoub) on CodePen. ينطبق اﻷمر على العنصر القابل للسحب. توجد الكرة دائما فوق العناصر اﻷخرى، وبذلك فإنّ اﻷحداث تقع عليها. مهما كانت المعالجات التي نسندها إلى العناصر السفليّة، فإنّها لن تشتغل. لهذا السبب لا تعمل فكرة إسناد معالجات إلى ما يُحتمل من العناصر القابلة للإفلات فوقها في الواقع. لن تشتغل هذه المعالجات. فما العمل إذًا؟ يوجد هناك تابع يُطلق عليه document.elementFromPoint(clientX, clientY)‎. يُعيد هذا التابع العنصر السفليّ الموجود عند الإحداثيّات المُعطاة بالنسبة إلى النافذة (أو null إذا كانت الإحداثيّات المُعطاة خارج النافذة). يمكننا استخدامه في أيٍّ من معالجات أحداث الفأرة التي لدينا لاكتشاف العناصر المحتملة القابلة للإفلات فوقها تحت المؤشّر، هكذا: // داخل معالج حدث فأرة ball.hidden = true; // (*) أخفِ العنصر الذي نسحبه let elemBelow = document.elementFromPoint(event.clientX, event.clientY); // هو العنصر الذي تحت الكرة، قد يكون قابلا للإفلات فوقه elemBelow ball.hidden = false; يُرجى التنبّه: نحتاج إلى إخفاء الكرة قبل الاستدعاء (*)، وإلّا فستكون الكرة في تلك الإحداثيّات في الغالب، لأنّها هي العنصر العلويّ تحت المؤشّر: elemBelow=ball. لذا نخفيها ثمّ نظهرها من جديد مباشرة. يمكننا استخدام هذه الشيفرة لمعرفة العنصر الذي "نحلّق فوقه" في أيّ وقت كان. ثم معالجة الإفلات عند حدوثه. هذه شيفرة مفصّلة لـ onMouseMove لإيجاد العناصر "القابلة للإفلات فوقها": // العنصر المحتمل القابل للإفلات فوقه الذي نحلّق فوقه الآن let currentDroppable = null; function onMouseMove(event) { moveAt(event.pageX, event.pageY); ball.hidden = true; let elemBelow = document.elementFromPoint(event.clientX, event.clientY); ball.hidden = false; // خارج النافذة (عندما نسحب الكرة إلى خارج الشاشة) ‏ mousemove قد تقع اﻷحداث // null يعيد elementFromPoint خارج النافذة، فإنّ clientX/clientY إذا كانت if (!elemBelow) return; // potential droppables are labeled with the class "droppable" (can be other logic) // (قد يكون منطقًا آخر) “droppable” تُعلَّم العناصر المحتملة القابلة للسحب بالصنف let droppableBelow = elemBelow.closest('.droppable'); if (currentDroppable != droppableBelow) { // ... نحلّق دخولا وخروجا // null انتبه: قد تكون كلتا القيمتان // (إذا لم نكن فوق عنصر قابل للإفلات فوقه قبل وقوع هذا الحدث (كمساحة فارغة currentDroppable=null // إذا لم نكن فوق عنصر قابل للإفلات فوقه الآن، خلال هذا الحدث droppableBelow=null if (currentDroppable) { // (المنطق المُعالَج عند “التحليق خارج” العنصر القابل للإفلات فوقه (إزالة الإبراز leaveDroppable(currentDroppable); } currentDroppable = droppableBelow; if (currentDroppable) { // المنطق المُعالَج عند “التحليق داخل” العنصر القابل للإفلات فوقه enterDroppable(currentDroppable); } } } في المثال الذي يمكن مشاهدته من هنا، عندما تُسحب الكرة إلى المرمى، يُبرز الهدف. See the Pen JS-p2-mouse-drag-and-drop-ex05 by Hsoub (@Hsoub) on CodePen. نملك الآن "هدف الإفلات" الحاليّ، الذي نحلّق فوقه، في المتغيّر currentDroppable خلال كامل العمليّة ويمكننا استخدامه للإبراز أو لأيّ غرض آخر. الملخّص لقد درسنا خوارزميّة قاعديّة للسحب والإفلات، مكوّناتها الأساسيّة هي: مجرى اﻷحداث: ball.mousedown ‏-> document.mousemove ‏-> ball.mouseup (لا تنس إلغاء ondragstart الأصيل). عند بداية السحب، تذكّر الإزاحة اﻷوّليّة للمؤشّر بالنسبة للعنصر، أي shiftX/shiftY، وحافظ عليها خلال السحب. اكتشف العناصر القابلة للإفلات فوقها تحت المؤشّر باستخدام document.elementFromPoint. يمكننا بناء الكثير فوق هذا اﻷساس. عند mouseup يمكننا إنهاء السحب بشكل أفضل: كالقيام بتغيير المعطيات وترتيب العناصر. يمكننا إبراز العناصر التي نحلّق فوقها. يمكننا حصر السحب في مساحات أو اتجاهات معيّنة. يمكننا استخدام تفويض اﻷحداث مع mousedown/up. يستطيع معالج حدث على نطاق واسع أن يدير السحب والإفلات لمئات العناصر، بواسطة تفحّص event.target. وهكذا. هناك أطر عمل تبني هندستها على هذا اﻷساس: DragZoneوDroppableوDraggable` وغيرها من الأصناف. يقوم معظمها بأمور تُشبه ما تمّ وصفه أعلاه، لذا فسيكون من السهل فهمها الآن. أو أنجز واحدا خاصّا بك، فكما رأيت إنّه من السهل فعل ذلك، بل أحيانا أسهل من محاولة تكييف حلّ من طرف ثالث. التمارين المنزلق اﻷهميّة: 5 أنشئ منزلقًا كالذي هو مبيّن هنا. اسحب المقبض اﻷزرق بواسطة الفأرة وحرّكه. تفاصيل مهمّة: عند الضغط على زرّ الفأرة، يمكن للمؤشّر الذهاب فوق وتحت المنزلق خلال السحب، ويبقى المنزلق يعمل بشكل عاديّ (هذا ملائم للمستخدم). عندما تتحرّك الفأرة بسرعة إلى اليمين واليسار، يجب أن يتوقّف المقبض عند الحافّة بالضبط. أنجز التمرين في البيئة التجريبيّة الحل كما يمكننا أن نرى من خلال HTML/CSS، أنّ المنزلق هو <div> بخلفيّة ملوّنة، يحتوي على زلّاجة -- التي هي <div> آخر له position:relative. نغيّر موضع الزلّاجة باستخدام position:relative بإعطاء الإحداثيّات نسبةً إلى العنصر اﻷب، وهي هنا أكثر ملائمة من استخدام position:absolute. ثمّ ننجز السحب والإفلات المحصور أفقيّا، والمحدود بالعُرض. شاهد الحلّ في البيئة التجريبيّة اسحب اﻷبطال الخارقين حول الميدان اﻷهميّة: 5 قد يساعدك هذا التمرين في اختبار فهمك لعدّة جوانب للسحب والإفلات وDOM. اجعل جميع العناصر التي لها الصنف draggable قابلة للسحب. مثل الكرة في المقال. المتطلّبات: استخدم تفويض اﻷحداث لترصّد بداية السحب: معالج واحد للحدث mousedown على document. إذا سُحبت العناصر إلى الحوافّ العلويّة/السفليّة للنافذة، تُمرَّر الصّفحة لتمكين المزيد من السحب. ليس هناك تمرير أفقيّ للصفحة (يجعل هذا التمرين أبسط قليلا، بالرغم أنّه من السهل إضافتها). يجب ألّا تفارق العناصر القابلة للسحب أو أيٌّ من أجزائها النافذة، ولو تحرّكت الفأرة بسرعة. يمكن مشاهدة النتيجة من هنا. أنجز التمرين في البيئة التجريبيّة. الحلّ لسحب العنصر يمكننا استخدام position:fixed، فهي تجعل الإحداثيات أسهل في الإدارة. في النهاية يجب علينا إرجاعها إلى position:absolute لوضع العنصر في المستند. عندما تكون الإحداثيّات في أعلى/أسفل النافذة، نستخدم window.scrollTo لتمريرها. المزيد من التفاصيل في التعليقات التي في الشيفرة. شاهد الحلّ في البيئة التجريبيّة ترجمة -وبتصرف- للمقال Drag'n'Drop with mouse events من سلسلة Browser: Document, Events, Interfaces لصاحبها Ilya Kantor
  9. لنتعمّق في المزيد من التفاصيل حول اﻷحداث التي تقع عندما تتحرّك الفأرة بين العناصر. اﻷحداث 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. See the Pen JS-p2-mousemove-mouseover-mouseout-mouseenter-mouseleave-ex01 by Hsoub (@Hsoub) on CodePen. تنبيه: يمكن أن يأخذ relatedTarget القيمة null يمكن للخاصيّة relatedTarget أن تكون null. هذا أمر عاديّ ويعني فقط أنّ الفأرة لم تأتِ من عنصر آخر، ولكن من خارج النافذة. أو أنّها قد غادرت النافذة. تخطي العناصر يقع الحدث mousemove عندما تتحرّك الفأرة. ولكنّ ذلك لا يعني أنّ كلَّ بكسل يؤدّي إلى حدث. يرصد المتصفّح الفأرة من وقت لآخر. وإذا لاحظ تغيّرات، فإنّه يعمل على وقوع اﻷحداث. هذا يعني لو حرّك المستخدم الفأرة بسرعة كبيرة فإنّ بعض عناصر DOM قد تُتخطّى: إذا تحرّكت الفأرة بسرعة كبيرة من العنصر ‎#FROM إلي العنصر ‎#TO كما هو مبيّن أعلاه، فإنّ عناصر <div> التي بين هذين العنصرين (أو بعضها) قد تُتخطّى. قد يقع الحدث mouseout على ‎#FROM ثم مباشرة على ‎#TO. يساعد هذا على تحسين اﻷداء، لأنّه قد تكون هناك الكثير من العناصر البينيّة، ولا نريد حقًّا معالجة الدخول والخروج في كلٍّ منها. في المقابل، يجب أن ندرك دائما أن مؤشّر الفأرة لا "يزور" جميع العناصر في طريقه، بل قد "يقفز". فمثلا، من الممكن أن يقفز المؤشّر إلى وسط الصفحة مباشرة قادما من خارج الصفحة. في هذه الحالة، تكون قيمة relatedTarget هي null، لأنّه قد أتى من "لا مكان": يمكنك رؤية ذلك "حيًّا" في المثال التالي أو من منصّة الاختبار التي من هنا: See the Pen JS-p2-mousemove-mouseover-mouseout-mouseenter-mouseleave-ex02 by Hsoub (@Hsoub) on CodePen. يوجد هناك في 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]‎ (مجيء المؤشّر إلى العنصر الابن، انتشر هذا الحدث نحو اﻷعلى). See the Pen JS-p2-mousemove-mouseover-mouseout-mouseenter-mouseleave-ex03 by Hsoub (@Hsoub) on CodePen. كما هو ظاهر، عندما يتحرّك المؤشّر من العنصر ‎#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 = ''; }; يمكنك مشاهدتها تعمل من هنا. كلّما تنقّلت الفأرة عبر العناصر في هذا الجدول، سيكون العنصر الحالي بارزا على الدوام: See the Pen JS-p2-mousemove-mouseover-mouseout-mouseenter-mouseleave-ex04 by Hsoub (@Hsoub) on CodePen. في حالتنا هذه نودّ أن نعالج الانتقالات بين خانات الجدول <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> ككلّ. يمكنك مشاهدة المثال كاملا بجميع التفاصيل من هنا: See the Pen JS-p2-mousemove-mouseover-mouseout-mouseenter-mouseleave-ex05 by Hsoub (@Hsoub) on CodePen. جرّب تحريك المؤشّر إلى داخل وخارج خانات الجدول وكذلك بداخلها. بسرعة أو ببطء، لا يهمّ ذلك. لا تُبرَز إلّا <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
  10. سنتناول في هذا المقال أحداث الفأرة وخاصّياتها بمزيد من التفصيل. يُرجى التنبه: لا تأتي هذه الأحداث فقط من "الأجهزة ذوات الفأرة"، ولكن من أجهزة أخرى أيضًا مثل الهواتف والأجهزة اللوحيّة، حيث تُحاكى فيها هذه الأحداث لتحقيق التوافق. أنواع أحداث الفأرة سبق وأن رأينا بعض هذه الأحداث: mousedown/mouseup: يحدث عندما يُنقر / يُحرَّر زرّ الفأرة فوق عنصر ما. mouseover/mouseout: يحدث عندما يبلغ / يغادر مؤشّر الفأرة عنصرا ما. mousemove: يحدث كلّما تحرّك مؤشّر الفأرة فوق عنصر ما. click: يحدث بعد حدوث mousedown ثم mouseup فوق نفس العنصر باستخدام الزرّ الأيسر للفأرة. dblclick؛ يحدث بعد النقر مرّتين على نفس العنصر خلال إطار زمنيّ قصير. يندر استخدامه هذه الأيام. contextmenu: يحدث عندما يُضغط الزرّ الأيمن للفأرة. وبما أنّ هناك طرقا أخرى لفتح القائمة المنبثقة، كاستخدام زرّ خاصّ في لوحة المفاتيح مثلا، فإنّه يحدث عند ذلك أيضا، ولا يُعدّ بذلك من الأحداث المختصّة بالفأرة على وجه الدقّة. هناك العديد من الأحداث الأخرى أيضا، سنتطرّق إليها لاحقا. ترتيب الأحداث ⁧⁧⁧⁧⁧⁧⁧⁧⁧⁧⁧⁧⁧⁧⁧⁧⁧⁧ مثلما يمكن أن تلاحظ في القائمة أعلاه، قد يؤدّي فعل واحد للمستخدم إلى وقوع عدّة أحداث. على سبيل المثال، يؤدّي النقر بالزرّ اﻷيسر أوّلا إلى حدوث mousedown عند الضغط على الزرّ، ثم إلى حدوث mouseup وclick عند تحريره. في الحالات التي يبتدئُ فيها فعلٌ واحد عدّةَ أحداث، فإنّ ترتيبها يكون ثابتا. بمعنى أن المعالجات نُستدعى هنا حسب الترتيب mousedown ⁧-> mouseup ⁧-> click. إليك التجربة الحيّة التالية لمثال تُسجَّل فيه جميع أحداث الفأرة، وإذا كان هناك فارق بأكثر من ثانية بينها ستُفصل بخطّ أفقيّ. يمكنك أيضا رؤية الخاصّيّة button التي تمكّن من اكتشاف زرّ الفأرة، وسيُشرح ذلك في أسفله: See the Pen JS-p2-mouse-events-basics-ex01 by Hsoub (@Hsoub) on CodePen. زر الفأرة تكون للأحداث المتعلّقة بالنقر دائما الخاصّيّة button، التي تمكّن من معرفة الزرّ الذي ضُغط بالضبط. قد لا نحتاج إلى استعمالها في العادة مع اﻷحداث click وcontextmenu، لأّن اﻷوّل يحدث فقط عند النقر باليسار، بينما يحدث اﻷخير فقط عند النقر باليمين. لكن في المقابل، قد تحتاج معالجات الأحداث mousedown وmouseup إلى الخاصّيّة event.button، لأنّ تلك اﻷحداث قد تقع عند النقر على أيٍّ من الأزرار، فتمكّن button من التمييز بين "mousedown اﻷيمن" و"mousedown اﻷيسر". يمكن أن تأخذ الخاصّيّة button القيم التالية: حالة الزرّ event.button الزرّ اﻷيسر (اﻷوليّ) 0 الزرّ اﻷوسط (الملحق) 1 الزرّ اﻷيمن (الثانويّ) 2 الزرّ X1 (الخلف) 3 الزرّ X2 (اﻷمام) 4 table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } ليس لدى معظم أجهزة الفأرة سوى الزرّين اﻷيمن واﻷيسر، فتكون القيم الممكنة 0 أو 2. تولّد اﻷجهزة اللمسيّة أيضا مثل هذه اﻷحداث عندما ينقر أحدهم عليها. ثم إنّ هناك الخاصّيّة event.buttons التي تعبّر عن جميع الأزرار المضغوطة حاليّا بعدد صحيح، يكون ناتج مجموع قيم تلك اﻷزرار. يندر جدّا استخدام هذه الخاصّيّة في الواقع، لكن يمكنك أن تجد المزيد من التفاصيل حولها من MDN إن حصل واحتجت لها. تنبيه: خاصّيّة event.which المندثرة قد تستعمل الشيفراتُ القديمة الخاصّيّة event.which وهي طريقة قديمة وغير قياسية للحصول على الزرّ، ويمكن أن تأخذ القيم التالية: event.which == 1 -- الزرّ اﻷيسر، event.which == 2 -- الزرّ اﻷوسط، event.which == 3 -- الزرّ اﻷيمن. حاليّا، تُعدّ event.which مهملة (deprecated)، ولا ينبغي استعمالها. مفاتيح التبديل: shift و alt و ctrl و meta تحتوي جميع أحداث الفأرة على المعلومات الخاصّة بمفاتيح التبديل المضغوطة. فللكائن event الخاصّيّات التالية: shiftKey: المفتاح Shift altKey: المفتاح Alt (أو المفتاح Opt في ماك) ctrlKey: المفتاح Ctrl metaKey: المفتاح Cmd في ماك تكون قيم هذه الخاصّيّات true إذا كان المفتاح المقابل لها مضغوطا عند وقوع الحدث. فعلى سبيل المثال، لا يعمل الزرّ أسفله إلا عند النقر مع الضغط على المفاتيح Alt وShift معا: <button id="button">Alt+Shift+Click on me!</button> <script> button.onclick = function(event) { if (event.altKey && event.shiftKey) { alert('Hooray!'); } }; </script> See the Pen JS-p2-mouse-events-basics-ex02 by Hsoub (@Hsoub) on CodePen. تنبيه: عادة ما يُستعمل المفتاح Cmd بدل Ctrl في ماك توجد في ويندوز ولينكس مفاتيح التبديل Alt وShift وCtrl. كما يوجد في ماك المفتاح الإضافيّ Cmd الذي تقابله الخاصّيّة metaKey. في معظم التطبيقات، عندما يُستعمل في ويندوز ولينكس المفتاح Ctrl فإنّ في ماك يُستعمل المفتاح Cmd. بمعنى أنّه عندما يضغط مستخدم ويندوز على المفتاحين Ctrl وEnter أو Ctrl وA، فإنّ من المفترض أن يضغط مستخدم ماك على المفتاحين Cmd وEnter أو Cmd وA، وهكذا. فإذا أردت أن تعتمد الجمع بين Ctrl والنقر مثلا، فمن المعقول استخدام الجمع بين Cmd والنقر في ماك. فذلك أريح لمستخدمي ماك. حتى وإن فضّلنا اضطرار مستخدمي ماك إلى الجمع بين Ctrl والنقر -- ذلك صعب بعض الشيء، فإنّ المشكلة أن النقر باليسار مع الضغط على Ctrl في نظام التشغيل ماك يُفهم على أنّه نقر باليمين، ويؤدّي إلى توليد الحدث contextmenu، وليس click كما في ويندوز ولينكس. فإذا أردنا أن نحرص على راحة مستخدمي جميع أنظمة التشغيل، فينبغي أن نفحص كلتى الخاصّيتين ctrlKey وmetaKey. ما يعني ذلك في شيفرة جافاسكربت هو أن نتحقّق if (event.ctrlKey || event.metaKey)‎. تنبيه: هناك أيضا أجهزة محمولة يُعدّ الجمع بين المفاتيح إضافة جيّدة في سير العمل. فإذا كان الزائر يستخدم لوحة المفاتيح، فستعمل بشكل جيّد. لكن إذ لم يكن جهازه متضمّنا لها، فلابدّ إذا من وجود سبيل للعمل دون مفاتيح التبديل. الإحداثيات: clientX/Y وpageX/Y تقدّم جميع أحداث الفأرة نوعين من الإحداثيّات: بالنسبة إلى النافذة: clientX وclientY. بالنسبة إلى المستند: pageX وpageY. باختصار، تُحتسب الإحداثيّات بالنسبة إلى النافذة pageX/Y ابتداءً من الزاوية العليا من اليسار للمستند، ولا تتغيّر عند تمرير (scroll) الصفحة، بينما تُحتسب clientX/Y ابتداءً من الزاوية العليا من اليسار للنافذة الحاليّة وتتغيّر عند تمرير الصفحة. على سبيل المثال، إذا كانت لدينا نافذة قياسها 500x500، والفأرة موجودة في الزاوية العليا من اليسار، فإنّ قيمتا clientX وclientY هي 0، مهما مُرّرت الصفحة. بينما إذا كانت الفأرة في المنتصف، فإن قيمتا clientX وclientY هي 250، أيًّا كان مكانها في المستند. وهما بهذا الاعتبار متشابهتان مع position:fixed. حرّك الفأرة فوق حقل المُدخل لرؤية clientX/clientY (المثال موجود داخل iframe، لذا فإنّ الإحداثيّات منسوبة إلى iframe). <input onmousemove="this.value=event.clientX+':'+event.clientY" value="Mouse over me"> See the Pen JS-p2-mouse-events-basics-ex03 by Hsoub (@Hsoub) on CodePen. تجنب التحديد مع mousedown للنقر المزدوج بالفأرة أثر جانبيّ قد يكون مزعجا في بعض الواجهات، ألا وهو تحديد النصّ. على سبيل المثال، يؤدّي النقر المزدوج على النصّ أدناه إلى تحديده بالإضافة تشغيل المعالج المسند إليه: <span ondblclick="alert('dblclick')">Double-click me</span> See the Pen JS-p2-mouse-events-basics-ex04 by Hsoub (@Hsoub) on CodePen. وإذا ضغط أحدهم زرّ الفأرة اﻷيمن، وحرك الفأرة دون تحريره، فسيعمل ذلك أيضا على تحديد النص، ولا يُرغب في ذلك غالبا. هناك عدّة طرق لتجنُّب التحديد، يمكنك الاطّلاع عليها في مقال التحديد والمدى. لكن في هذه الحالة خاصّة، أنسب طريقة لذلك هي منع فعل المتصفّح عند mousedown، ليحول ذلك دون حصول هذه التحديدات: Before... <b ondblclick="alert('Click!')" onmousedown="return false"> Double-click me </b> ...After See the Pen JS-p2-mouse-events-basics-ex05 by Hsoub (@Hsoub) on CodePen. لا يُحدَّد العنصر ذو الخطّ العريض الآن عند النقر المزدوج عليه، ولا يؤدّي النقر عليه باليسار إلى ابتداء تحديده أيضا. لكن يُرجى التنبّه: يبقى النصّ داخله قابلا للتحديد، إذا ابتُدئ التحديد قبل النصّ أو بعده، لا عليه مباشرة، ولا بأس في ذلك بالنسبة للمستخدمين عادة. ملاحظة: منع النسخ إذا أردنا أن نعطّل تحديد النصّ لحماية محتوى صفحتنا من النسخ واللصق، فيمكننا عندها استعمال حدث آخر: oncopy. <div oncopy="alert('Copying forbidden!');return false"> Dear user, The copying is forbidden for you. If you know JS or HTML, then you can get everything from the page source though. </div> See the Pen JS-p2-mouse-events-basics-ex06 by Hsoub (@Hsoub) on CodePen. فلو حاولت نسخ جزء من النصّ الذي بداخل <div>، فلن يعمل ذلك، ﻷن الفعل الافتراضيّ oncopy قد مُنع. يمكن للمستخدم بالتأكيد الوصول إلى مصدر HTML، وأخذ المحتوى من هناك، لكن لا يُحسن الجميع فعل ذلك. الملخص لأحداث الفأرة الخاصّيّات التالية: الزرّ: button. مفاتيح التبديل: (true عند الضغط عليها): altKey وctrlKey وshiftKey وmetaKey (ماك). إذا كنت تودّ اعتماد Ctrl، لا تنس مستخدمي ماك، إذ يستعملون عادة Cmd، فيُفضّل إذًا أن نتحقّق if (e.metaKey || e.ctrlKey)‎. الإحداثيّات بالنسبة للنافذة: clientX/clientY. الإحداثيّات بالنسبة للمستند: pageX/pageY. فعل المتصفح الافتراضيّ عند mousedown هو تحديد النصّ، فإذا لم يكن مناسبا في الواجهة، ينبغي منعه. في المقال التالي سنتناول المزيد من التفاصيل حول اﻷحداث التي تلي حركة المؤشّر وكيفية تتبّع تغيير العناصر التي تحته. التمارين قائمة قابلة للتحديد اﻷهمية: 5 أنشئ قائمة تكون عناصرها قابلة للتحديد، كما في مديري الملفّات. بالنقر على عنصر من القائمة يُحدَّد ذلك العنصر فقط (بإضافة الصنف selected‎.) ويزال التحديد عن بقيّة العناصر. إذا تمّ النقر مع المفتاح Ctrl (‏Cmd بالنسبة لماك)، فإنّ التحديد ينقلب (toggled) في ذلك العنصر، ولا تتغيّر بقيّة العناصر. يمكن مشاهدة المثال من هنا. ملاحظة: في هذا التمرين، يمكننا افتراض أنّ عناصر القائمة مكوّنة من نصّ فقط. ليست هناك وسوم متداخلة. ملاحظة أخرى: امنع تحديد المتصفّح اﻷصيل للنصوص عند النقر عليها. أنجز التمرين في البيئة التجريبية الحل افتح الحل في البيئة التجريبية ترجمة -وبتصرف- للمقال Mouse events من سلسلة Browser: Document, Events, Interfaces لصاحبها Ilya Kantor
  11. لا يمكننا فقط إسناد معالجات للأحداث من خلال جافاسكربت، ولكن يمكننا أيضا توليد أحداث مخصّصة. يُمكن استعمال الأحداث المخصّصة لإنشاء "مكوّنات رسوميّة". على سبيل المثال، يمكن للعنصر الجذر في قائمةٍ تعمل بواسطة جافاسكربت افتعال أحداثٍ تُنبئ بما يحصل مع القائمة: 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> See the Pen JS-p2-dispatch-events -ex1 by Hsoub (@Hsoub) on CodePen. ملاحظة: 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> See the Pen JS-p2-dispatch-events -ex2 by Hsoub (@Hsoub) on CodePen. ملاحظات: يجب أن نستخدم 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> See the Pen JS-p2-dispatch-events -ex3 by Hsoub (@Hsoub) on CodePen. يمكن أن تحوي هذه الخاصيّة أي معطيات. في الحقيقة، من الممكن أن نعمل بدونها، لأننا نستطيع أن نسند أيّ خاصيّة إلى كائن 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> See the Pen JS-p2-dispatch-events -ex4 by Hsoub (@Hsoub) on CodePen. يرجى التنبه هنا: يجب أن يكون للحدث الراية 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> See the Pen JS-p2-dispatch-events -ex5 by Hsoub (@Hsoub) on CodePen. يكون ترتيب المخرجات كما يلي: 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> See the Pen JS-p2-dispatch-events -ex6 by Hsoub (@Hsoub) on CodePen. تعمل الآن 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
  12. تتسبّب الكثير من الأحداث في قيام المتصفّح تلقائيّا بأفعالٍ معيّنة. على سبيل المثال: يتسبّب النقر على رابط في الانتقال إلى عنوانه. يتسبّب النقر على زرّ الإرسال في نموذج في إرساله إلى الخادم. يتسبّب الضغط على زرّ الفأرة فوق نصّ وتحريكها في تحديد ذلك النصّ. في حال معالجة الحدث في جافاسكربت، قد لا نريد حصول فعل المتصفّح المقابل له، ونريد تطبيق سلوكٍ آخر في مكانه. منع أفعال المتصفح هناك طريقتان لإخبار المتصفّح بأنّنا لا نريده أن يعمل: الطريقة الرئيسيّة هي استخدام الكائن event، الذي لديه التابع event.preventDefault()‎. إذا أٌسند المعالج باستخدام on<event>‎ (لا باستخدام addEventListener) ، فإنّ إعادة القيمة false تؤدي نفس الغرض أيضا. في شيفرة HTML أسفله، لا يتسبّب النقر على الرابط في الانتقال إلى عنوانه، لا يفعل المتصفّح أيّ شيء. <a href="/" onclick="return false">Click here</a> or <a href="/" onclick="event.preventDefault()">here</a> See the Pen JS-p2-browser-default-actions -ex1 by Hsoub (@Hsoub) on CodePen. في المثال التالي، سنستخدم هذه الطريقة لإنشاء قائمة تعمل بواسطة جافاسكربت. تنبيه: إعادة false من المعالج هو استثناء تُهمل عادةً القيمة المُعادة من طرف المعالج. الاستثناء الوحيد هو return false من المعالج المسنَد باستخدام on<event>‎. في جميع الحالات الأخرى، تُهمل قيمة return. على وجه الخصوص، لا معنى لإعادة true. مثال: القائمة لنفترض قائمة موقع كهذه: <ul id="menu" class="menu"> <li><a href="/html">HTML</a></li> <li><a href="/javascript">JavaScript</a></li> <li><a href="/css">CSS</a></li> </ul> بإضافة بعض تنسيقات CSS ستبدو كما هنا. تُنشأ عناصر القائمة على شكل روابط HTML أي <a>، لا على شكل أزرار <button>. هناك عدّة أسباب لفعل ذلك، فمثلًا: يحبّذ كثيرٌ من الناس استخدام "النقر باليمين" ثم "افتح في نافذة جديدة". لو استخدمنا <button> أو <span> فلن يعمل ذلك. تتتبّع محرّكات البحث الروابط <a href="..."‎> عند الفهرسة. بالتالي سنستخدم <a> عند إضافة الروابط في HTML. لكنّّنا نودّ عادةً معالجة النقرات على الروابط بواسطة جافاسكربت. فيجب إذًا أن نمنع فعل المتصفّح الافتراضي.كما هو مبيّن هنا: menu.onclick = function(event) { if (event.target.nodeName != 'A') return; let href = event.target.getAttribute('href'); alert( href ); // قد يُحمّل من الخادم أو تُولّد واجهة المستخدم إلى غير ذلك ... return false; // (لا تذهب إلى العنوان) ‎ امنع فعل المتصفّح }; إذا حذفنا return false، فسيقوم المتصفّح بعد تنفيذ الشيفرة "بفعله الافتراضي" -- الانتقال إلى العنوان الذي في href. ولا نحتاج ذلك هنا، إذ نعالج النقرات بأنفسنا. بالمناسبة، يساعد استخدام تفويض الحدث هنا في جعل القائمة مرنة. يمكننا إضافة قوائم متداخلة وتنسيقها باستخدام CSS حتى "تنزلق إلى الأسفل". ملاحظة: الأحداث المتتابعة تجري بعض الأحداث الواحد تلو الآخر. فإذا منعنا الحدث الأوّل، لن يكون الثاني. على سبيل المثال، يؤدّي وقوع الحدث mousedown على حقل <input> إلى التركيز عليه، ووقوع حدث focus. فإذا منعنا الحدث mousedown، لن يكون هناك تركيز. جرّب النقر على <input> الأوّل من هنا -- يقع الحدث focus. لكن لو نقرت على الثاني، لن يكون هناك تركيز. <input value="Focus works" onfocus="this.value=''"> <input onmousedown="return false" onfocus="this.value=''" value="Click me"> See the Pen JS-p2-browser-default-actions -ex2 by Hsoub (@Hsoub) on CodePen. هذا لأنّ فعل المتصفّح قد أُلغي عند mousedown. يبقى وقوع التركيز ممكنا إذا استخدمنا طريقة أخرى للوصول إلى المُدخَل. على سبيل المثال، بواسطة المفتاح Tab للانتقال من المُدخَل الأوّل إلى الثاني. لكن ليس باستخدام النقر بالفأرة. خيار المعالج "السلبي (passive)" يشير الخيار الاختياري passive: true لـ addEventListener إلى المتصفّح بأنّ المعالج لن يستدعي preventDefault()‎. لكن لماذا قد يُحتاج لذلك؟ تتسبّب بعض الأحداث مثل touchmove على الأجهزة اللمسية (عندما يحرّك المستخدم اصبعه على الشاشة) في تمرير (scroll) الصفحة افتراضيًّا، لكن قد يُمنع ذلك التمرير باستخدام preventDefault()‎ في المعالج. فإذا تحسّس المتصفّح مثل هذه الأحداث، عليه أوّلًا دراسة جميع المعالجات للتحقّق من عدم استدعاء preventDefault في أيٍّ منها، ليتمكّن عندها من الشروع في التمرير. قد يُسبّب هذا تأخيرات و "ارتجاجات" غير ضروريّة في واجهة المستخدم. يُعلِم الخيارُ passive: true المتصفّحَ بأنّ المعالج لن يلغي التمرير. فيقوم المتصفّح بالتمرير مباشرة مقدّمًا بذلك تجربة سلسلة للغاية. تكون قيمة passive في بعض المتصفّحات (Firefox, Chrome) هي true افتراضيًّا للأحداث touchstart و touchmove. vent.defaultPrevented تكون قيمة الخاصيّة event.defaultPrevented هي true إذا مُنع الفعل الافتراضي، وإلّا فهي false. هناك حالة استخدامٍ لذلك مثيرة للاهتمام. تذكر أنّنا تحدّثنا في مقال انتشار الأحداث عن event.stopPropagation()‎، وكيف أنّ إيقاف انتشار الأحداث نحو الأعلى أمرٌ سيّء. يمكن أحيانًا استخدام event.defaultPrevented بدل ذلك، لإعلام المعالجات الأخرى بأنّ الحدث قد تمّت معالجته. لنرى مثالًا عمليّا لذلك. يُظهر المتصفّح افتراضيًّا عند الحدث contextmenu (النقر بزرّ الفأرة الأيمن) قائمة منبثقة تضمّ خيارات اعتياديّة. يمكننا منع ذلك، وإظهار قائمتنا نحن بدلها، هكذا: <button>Right-click shows browser context menu</button> <button oncontextmenu="alert('Draw our menu'); return false"> Right-click shows our context menu </button> See the Pen JS-p2-browser-default-actions -ex3 by Hsoub (@Hsoub) on CodePen. بالإضافة إلى تلك القائمة المنبثقة، نودّ الآن إنشاء قائمة منبثقة تعمل على مستوى المستند. عند النقر باليمين، يجب أن تظهر أقرب قائمة. <p>Right-click here for the document context menu</p> <button id="elem">Right-click here for the button context menu</button> <script> elem.oncontextmenu = function(event) { event.preventDefault(); alert("Button context menu"); }; document.oncontextmenu = function(event) { event.preventDefault(); alert("Document context menu"); }; </script> See the Pen JS-p2-browser-default-actions -ex4 by Hsoub (@Hsoub) on CodePen. المشكلة أنّه عندما ننقر على elem، فإننا نحصل على قائمتين: التي على مستوى الزرّ و (بانتشار الحدث نحو الأعلى) التي على مستوى المستند. كيف يمكن إصلاح ذلك؟ يعتمد أحد الحلول على هذه الفكرة: "عند معالجة حدث النقر باليمين من طرف المعالج المسند إلى الزرّ، لنقم بإيقاف انتشاره نحو الأعلى باستخدام event.stopPropagation()‎: <p>Right-click for the document menu</p> <button id="elem">Right-click for the button menu (fixed with event.stopPropagation)</button> <script> elem.oncontextmenu = function(event) { event.preventDefault(); event.stopPropagation(); alert("Button context menu"); }; document.oncontextmenu = function(event) { event.preventDefault(); alert("Document context menu"); }; </script> See the Pen JS-p2-browser-default-actions -ex5 by Hsoub (@Hsoub) on CodePen. تعمل الآن القائمة التي على مستوى الزرّ كما ينبغي. لكن الثمن باهظ. سنحرم جميع الشيفرات الخارجية من الوصول إلى المعلومات الخاصة بالنقرات اليمنى إلى الأبد، بما في ذلك العدّادات التي تحصي وما إلى ذلك. ليس هذا من الحكمة على الإطلاق. الحلّ البديل هو التحقّق في معالج document ممّا إذا كان الفعل الافتراضي قد مُنع. وإذا كان كذلك، فإنّ الحدث قد عولج، ولا حاجة لكي نستجيب له. <p>Right-click for the document menu (added a check for event.defaultPrevented)</p> <button id="elem">Right-click for the button menu</button> <script> elem.oncontextmenu = function(event) { event.preventDefault(); alert("Button context menu"); }; document.oncontextmenu = function(event) { if (event.defaultPrevented) return; event.preventDefault(); alert("Document context menu"); }; </script> See the Pen JS-p2-browser-default-actions -ex6 by Hsoub (@Hsoub) on CodePen. كل شيء يعمل الآن بشكل صحيح. إذا كانت لدينا عناصر متداخلة، ولكلٍّ منها قائمته المنبثقة، فإنّ ذلك سيعمل أيضا. احرص فقط على التحقّق من وجود event.defaultPrevented في كلّ معالجات contextmenu. ملاحظة: ()event.stopPropagation و ()event.preventDefault كما يمكننا رؤية ذلك بوضوح، event.stopPropagation()‎ و event.preventDefault()‎ (أو ما يُعرف أيضا بـ return false) هما شيئان مختلفان. لا علاقة لهما ببعضهما البعض. ملاحظة: هندسة القوائم المنبثقة المتداخلة هناك أيضا طرق بديلة لتطبيق قوائم منبثقة متداخلة. إحداها هو أخذ كائن عامّ وحيد له المعالج document.oncontextmenu، وله كذلك توابع تمكّننا من تخزين معالجات أخرى داخله. سيعمل الكائن على التقاط كلّ النقرات اليمنى، ثم ينظر في المعالجات المخزَّنة ويُشغّل المعالج المناسب. لكن عندها، بجب على كلّ قطعة شيفرة تريد قائمة منبثقة أن تكون على علم بذلك المعالج، وتستعين به بدل معالج contextmenu خاصّ بها. الملخص هناك عدة أفعال افتراضيًّة للمتصفّح: mousedown -- يبدأ عمليّة التحديد (تُحرٌك الفأرة للتحديد). click على <input type="checkbox"‎> -- إضافة أو إزالة التأشير على الخانة input. submit -- يتسبّب النقر على <input type="submit"‎> أو الضغط على مفتاح Enter داخل حقل في نموذج بوقوع هذا الحدث، ويرسل المتصفّحُ النموذجَ بعد ذلك. keydown -- قد يتسبّب الضغط على مفتاحٍ في إضافة حرف داخل حقل، أو إلى فعل آخر. contextmenu -- يقع الحدث عند النقر باليمين، ويكون الفعل هو إظهار قائمة المتصفّح المنبثقة. … و يوجد هناك المزيد … يُمكن منع جميع الأفعال الافتراضية إذا أردنا معالجة الحدث حصريّا من خلال جافاسكربت. لمنع الفعل الافتراضيّ، استخدم إمّا event.preventDefault()‎ أو return false. لا تعمل الطريقة الثانية إلّا مع المعالجات المسندة بواسطة on<event>‎. يُعلِم الخيارُ passive: true المتصفّحَ بأنّ الفعل الافتراضيّ لن يُمنع. هذا مفيد لبعض أحداث الأجهزة المحمولة، مثل touchstart و touchmove، لإعلام المتصفّح بأنّ ليس عليه انتظار الانتهاء من تشغيل جميع المعالجات قبل التمرير. إذا تمّ منع الفعل الافتراضيّ، فإنّ قيمة event.defaultPrevented تصير true، وإلّا فهي false. تنبيه: حافظ على الدلاليّة، لا تفرط فنيّا، بمنع الأفعال الافتراضية واستخدام جافاسكربت يمكننا تخصيص سلوك أيّ عنصر. على سبيل المثال، يمكننا جعل رابط <a> يعمل مثل زرّ، وجعل زرّ <button> يتصرّف مثل رابط (ينقل إلى عنوان آخر ونحوه). لكن يجب عمومًا المحافظة على على المعاني الدلاليّة لعناصر HTML. على سبيل المثال، يجب أن يعمل على الانتقال الرابط، وليس الزرّ. إلى جانب كون ذلك "مجرّد أمر جيّد"، يجعل هذا شيفرة HTML أفضل من ناحية إمكانية الوصول (accessibility). بالإضافة إلى ذلك، يُرجى التنبّه إلى أن المتصفّح يتيح لنا إمكانيّة فتح الروابط <a> في نافذة مستقلّة (عن طريق النقر عليها باليمين وغير ذلك). ويودّ الناس ذلك. لكن لو جعلنا مكانها زرّا يعمل كرابط بواسطة جافاسكربت بل وجعلناه أيضا يبدو كرابط بواسطة CSS، فإنّ المزايا المختصّة بـ <a> التي يتيحها المتصفّح لن تعمل. التمارين لماذا لا تعمل "return false"؟ الأهمية: 3 لماذا لا تعمل return false إطلاقًا في هذه الشيفرة: <script> function handler() { alert( "..." ); return false; } </script> <a href="https://w3.org" onclick="handler()">the browser will go to w3.org</a> يتّبع المتصفّح العنوان عند النقر، لكنّنا لا نريد ذلك. كيف يمكن إصلاحه؟ الحل عندما يقرأ المتصفّح الخاصيّة on* مثل onclick، فإنّه ينشئ معالجًا من محتواها. فمن أجل onclick="handler()"‎ تصير الدالّة: function(event) { handler() // onclick محتوى } نستطيع الآن أن نرى أن القيمة المعادة من طرف handler()‎ غير مستعملة ولا تؤثر في النتيجة. طريقة إصلاح ذلك بسيطة: <script> function handler() { alert("..."); return false; } </script> <a href="https://w3.org" onclick="return handler()">w3.org</a> يمكننا أيضا استخدام event.preventDefault()‎، هكذا: <script> function handler(event) { alert("..."); event.preventDefault(); } </script> <a href="https://w3.org" onclick="handler(event)">w3.org</a> التقط الروابط داخل العنصر الأهمية: 5 اجعل جميع الروابط داخل العنصر الذي له id="contents"‎ تسأل المستخدم أوّلا إن كان يودّ المغادرة حقّا. فإذا كان لا يرغب، لا تذهب إلى العنوان. كما هو مبيّن هنا. التفاصيل: يمكن أن تُحمّل شيفرة HTML داخل العنصر يدويّا، أو يمكن أن تُولّد ديناميكيّا في أيّ وقت، فلا سبيل لإيجاد جميع الروابط وإسناد معالجات إليها. استخدم تفويض الأحداث. قد يكون داخل المحتوى وسوم متداخلة. وداخل الروابط أيضا، مثل <a href=".."><i>...</i></a>. افتح البيئة التجريبية لإنجاز التمرين الحل هذا استعمال جيّد لنمط تفويض الأحداث. في الحالات الواقعية، بدل سؤال المستخدم، يمكننا إرسال طلب "التسجيل" (logging" request") إلى الخادم الذي يحفظ مكان مغادرة المستخدم. أو يمكننا تحميل المحتوى وعرضه في نفس الصفحة (إذا كان ذلك مسموحًا). كلّ ما نحتاجه هو التقاط الأحداث contents.onclick واستخدام confirm لسؤال المستخدم. قد يمثّل استخدام link.getAttribute('href')‎ بدل link.href فكرة جيدة. انظر في الحل للتفاصيل. افتح الحل في البيئة التجريبية. معرض الصور الأهمية: 5 أنشئ معرضًا للصور بحيث تتغير الصورة الرئيسيّة بالنقر على الصور المصغّرة. كما هو مبيّن هنا. ملاحظة: استخدم تفويض الأحداث. أنجز التمرين في البيئة التجريبية. الحل يكون الحل بإسناد معالج إلى الحاوي وتتبّع النقرات. فإذا كان النقر على الرابط <a>، فنغير src الخاصّة بـ ‎#largeImg إلى href الخاصة بتلك الصورة المصغرة. افتح الحل في البيئة التجريبية. ترجمة -وبتصرف- للمقال Browser default actions من سلسلة Browser: Document, Events, Interfaces لصاحبها Ilya Kantor
  13. يمكّننا انتشار اﻷحداث نحو اﻷسفل واﻷعلى من تطبيق أحد أقوى أنماط معالجة الأحداث، وهو ما يُسمى تفويض الأحداث (event delegation). تتلخّص الفكرة في أنه إذا كان لدينا الكثير من العناصر التي تُعالج بطريقة متماثلة، فبدل أن نسند معالجا لكلّ منها، فإنّنا نسند معالجا واحدا إلى السلف الذي يلتقون فيه. في داخل المعالج، يمكن أن نتعرّف على مكان وقوع الحدث من خلال event.target، ثم نعالجه. لنرى مثالا على ذلك -- مخطط باكوا الذي يعكس الفلسفة الصينية القديمة، كما هو مبيّن من هنا. ويمكن تمثيله بواسطة HTML كالتالي: <table> <tr> <th colspan="3"><em>Bagua</em> Chart: Direction, Element, Color, Meaning</th> </tr> <tr> <td class="nw"><strong>Northwest</strong><br>Metal<br>Silver<br>Elders</td> <td class="n">...</td> <td class="ne">...</td> </tr> <tr>...2 more lines of this kind...</tr> <tr>...2 more lines of this kind...</tr> </table> See the Pen JS-p2-event-delegation -ex1 by Hsoub (@Hsoub) on CodePen. يحتوي الجدول على 9 خانات، لكنها قد تكون 99 أو 9999، لا يهمّ ذلك. مهمّتنا هي إبراز الخانة عند النقر عليها. بدل إسناد معالج onclick إلى كلّ <td> (قد يكون هناك الكثير منها)، سنسند المعالج "catch-all" إلى العنصر <table>. يستخدم المعالجُ الخاصيّة event.target للحصول على العنصر الذي نُقر عليه ثم يبرزه. إليك الشيفرة: let selectedTd; table.onclick = function(event) { let target = event.target; // أين كان النقر؟ if (target.tagName != 'TD') return; // ؟ إذًا لا يهمّنا ذلك TD ليس في highlight(target); // أبرزه }; function highlight(td) { if (selectedTd) { // أزل الإبراز الحالي إن وُجد selectedTd.classList.remove('highlight'); } selectedTd = td; selectedTd.classList.add('highlight'); // الجديدة td أبرز الـ } لا تكترث هذه الشيفرة بعدد الخانات التي في الجدول. يمكننا إضافة وإزالة الخانات <td> ديناميكيا في أي وقت، وستظل وظيفة الإبراز تعمل. لكن تبقى هناك نقيصة. قد لا يقع النقر على العنصر <td> بعينه ولكن على عنصر آخر بداخله. ففي حالتنا هذه، لو ألقينا نظرة داخل HTML، سنلاحظ أوسمة مدرجة داخل <td>، مثل <strong>: <td> <strong>Northwest</strong> ... </td> فمن الطبيعي أنه لو وقع النقر على <strong>، فسيصير هو القيمة التي يحملها event.target. داخل المعالج table.onclick، ينبغي علينا أن نأخذ event.target ونتحقق إن كان النقر قد وقع داخل <td> أو لا. هذه هي الشيفرة المحسّنة: table.onclick = function(event) { let td = event.target.closest('td'); // (1) if (!td) return; // (2) if (!table.contains(td)) return; // (3) highlight(td); // (4) }; إليك بعض الإيضاحات: يعيد التابع elem.closest(selector)‎ أقرب سلف يطابق المُحدِّد selector. في حالتنا، نبحث عن أقرب <td> نصادفه صعودًا من العنصر المصدري. إذا لم يكن event.target بداخل أيّ <td>، فسيُعاد الاستدعاء مباشرة، إذ ليس هناك شيء لفعله. في حال تداخل الجداول، قد تكون event.target هي <td>، لكنّها موجودة خارج الجدول الحاليّ. فنتحقق إذًا من أنّ <td> خاصّة بجدولنا الحاليّ. وإذا كانت كذلك، نبرزها. وبذلك، تكون لدينا شيفرة سريعة وفعّالة للإبراز، لا تكترث بعدد الخانات التي في الجدول. مثال عن التفويض: الأفعال داخل الترميز (markup) هناك استعمالات أخرى لتفويض الأحداث. لنقُل أننا نود إنشاء قائمة من الأزرار: "حفظ" و "تحميل" و "بحث" وغير ذلك. ويوجد هناك كائن له التوابع save و load و search … فكيف يتم الربط بين هذه التوابع واﻷزرار؟ أوّل ما قد يتبادر إلى الذهن هو إسناد معالج إلى كلّ زر. ولكنّ هناك حلًّا أكثر أناقة. يمكننا إسناد معالج إلى القائمة بأكملها و إضافة سمات data-action للأزرار تحمل التابع الذي سيُستدعى: <button data-action="save">Click to Save</button> يقرأ المعالج السمة، وينفّذ التابع. ألقِ نظرة على المثال أدناه: <div id="menu"> <button data-action="save">Save</button> <button data-action="load">Load</button> <button data-action="search">Search</button> </div> <script> class Menu { constructor(elem) { this._elem = elem; elem.onclick = this.onClick.bind(this); // (*) } save() { alert('saving'); } load() { alert('loading'); } search() { alert('searching'); } onClick(event) { let action = event.target.dataset.action; if (action) { this[action](); } }; } new Menu(menu); </script> See the Pen JS-p2-event-delegation -ex2 by Hsoub (@Hsoub) on CodePen. يُرجى ملاحظة أن this.onClick مرتبط بـ this في (*). هذا مهمّ، لأنّه لو لم يكن كذلك فإن this الذي بداخله سيشير إلى عنصر (elem) ، وليس الكائن Menu، ولا يكون بذلك this[action]‎ هو ما نحتاجه. فما هي إذًا المزايا التي يقدّمها تفويض الأحداث هنا؟ لا نحتاج كتابة شيفرة لإسناد معالج لكل زر. بل ننشئ فقط تابعًا ونضعه في الترميز. تصبح بنية HTML مرنة، فيمكننا إضافة وإزالة أزرار بسهولة. يمكننا أيضا استخدام أصناف مثل ‎.action-save و ‎.action-load، لكن سمة مثل data-action أفضل من الناحية الدلالية، بالإضافة إلى إمكانية استخدامها في قواعد CSS. نمط "السلوك" يمكننا أيضا استخدام تفويض الأحداث لإضافة "سلوكيّات" للعناصر تصريحيًّا، بواسطة سمات وأصناف خاصة. يتألّف هذا النمط من جزأين: نضيف سمة مخصّصة إلى العنصر تعبّر عن سلوكه. يتتبع الأحداثَ معالجٌ على نطاق المستند، فإذا وقع حدث على عنصر له سمة، فإنه يقوم بالفعل المناسب. مثال عن السلوك: العداد على سبيل المثال، تضيف السمة data-counter هنا سلوك "زيادة القيمة عند النقر" للأزرار: Counter: <input type="button" value="1" data-counter> One more counter: <input type="button" value="2" data-counter> <script> document.addEventListener('click', function(event) { if (event.target.dataset.counter != undefined) { // إذا كانت السمة موجودة... event.target.value++; } }); </script> See the Pen JS-p2-event-delegation -ex3 by Hsoub (@Hsoub) on CodePen. إذا نقرنا على أحد الأزرار، فإن القيمة التي عليه ستزداد. بغضّ النظر عن هذه الأزرار، فإن المنهجية العامة المتبعة هنا مهمّة. يمكن أن يكون هناك من السمات مع data-counter بقدر ما نرغب. يمكننا إضافة سمات جديدة إلى HTML في أي وقت. باستخدام تفويض الأحداث، نكون قد "وسّعنا" HTML من خلال إضافة سمة تعبّر عن سلوك جديد. تنبيه: استخدم دائما addEventListener في المعالجات التي على مستوى المستند عند إسناد معالج حدثٍ إلى الكائن document، يجب أن نستخدم دائما addEventListener، وليس document.on<event>‎، لأن هذا الأخير سيؤدي إلى تعارضات: تستبدل المعالجاتُ الجديدة المعالجات القديمة. في المشاريع الواقعية، من الطبيعي أن تكون هناك عدة معالجات قد أُسندت بواسطة أجزاء مختلفة من الشيفرة. مثال عن السلوك: القالِب (toggler) لنرى مثالا آخر عن السلوك. يؤدي النقر على عنصرٍ له السمة data-toggle-id إلى إخفاء وإظهار العنصر الذي له ذاك الـ id. <button data-toggle-id="subscribe-mail"> Show the subscription form </button> <form id="subscribe-mail" hidden> Your mail: <input type="email"> </form> <script> document.addEventListener('click', function(event) { let id = event.target.dataset.toggleId; if (!id) return; let elem = document.getElementById(id); elem.hidden = !elem.hidden; }); </script> See the Pen JS-p2-event-delegation -ex4 by Hsoub (@Hsoub) on CodePen. لنلاحظ مرة أخرى ما قمنا به. لإضافة وظيفة القلب إلى عنصرٍ ما من الآن فصاعدًا، لا حاجة لمعرفة جافاسكربت، يكفي استخدام السمة data-toggle-id. قد يصير هذا ملائما بالفعل -- لا حاجة لكتابة جافاسكربت لكل واحد من هذه العناصر. يكفي استخدام السلوك فقط. يجعل المعالج الذي على مستوى المستند ذلك يعمل مع أي عنصر على الصفحة. يمكننا أن نجمع بين عدّة معالجات في نفس العنصر أيضا. قد يشكل نمط "السلوك" بديلا عن الأجزاء المصغرة (mini-fragments) في جافاسكربت. الملخص تفويض الأحداث رائع حقًّا! إذ يُعدّ واحدا من أنفع الأنماط المتعلّقة بأحداث DOM. كثيرا ما يُستخدم تفويض اﻷحداث لإضافة نفس المعالج لعدّة عناصر متماثلة، لكن لا يقتصر اﻷمر على ذلك. الخوارزمية: أسند معالجًا وحيدًا إلى العنصر الحاوي. في المعالج -- افحص العنصر المصدري event.target. إذا وقع الحدث داخل عنصر يهمّنا، عالج الحدث. المزايا: يبسّط التهيئة ويوفّر الذاكرة: لا حاجة لإضافة عدة معالجات. أقلّ شيفرة: عند إضافة أو إزالة عناصر، لا داعي لإضافة أو إزالة المعالجات. التعديلات على DOM: يمكننا إضافة أو إزالة العناصر جماعيا بواسطة innerHTML وما إلى ذلك. للتفويض حدود أيضا بالطبع: أولًا، يجب أن يكون الحدث منتشرًا نحو اﻷعلى. بعض الأحداث لا تنتشر نحو اﻷعلى. يجب كذلك أن لا تَستخدم المعالجاتُ التي في الأسفل event.stopPropagation()‎. ثانيًا، قد يضيف التفويض عبئًا على وحدة المعالجة المركزية (CPU)، لأنّ المعالج الذي على مستوى الحاوي يستجيب للأحداث في أي مكان في الحاوي، بغضّ النظر عن كونها مهمّة لنا أو لا. لكن العبئ عادةً طفيف، فلا نأخذه بالحسبان. التمارين أخفي الرسائل باستخدام التفويض الأهمية: 5 هناك قائمة من الرسائل لها أزرار لإزالتها [x]. اجعل الأزرار تعمل. كما هو مبيّن هنا. ملاحظة: يجب أن يكون هناك منصت واحد للأحداث على الحاوي. استخدم تفويض الأحداث. أنجز التمرين في البيئة التجريبية الحل افتح الحل في البيئة التجريبية قائمة شجرية الأهمية: 5 أنشئ شجرة يمكن فيها إظهار وإخفاء العقد الأبناء بواسطة النقر: كما هو مبيّن هنا. المتطلبات: معالج واحد للأحداث فقط (استخدم التفويض). يجب ألا يفعل النقر خارج عقدة العنوان (في مساحة فارغة) أي شيء. أنجز التمرين في البيئة التجريبية الحل ينقسم الحل إلى جزئين: ضع كلّ عقدة عنوان في الشجرة داخل <span>. بهذا يمكننا إضافة تنسيقات CSS إلى :‎hover و معالجة النقرات على النص بالضبط، لأن عُرض <span> هو نفس عُرض النص بالضبط (بخلاف ما لو كان بدونه). عيّن معالجًا على العقدة الجذر tree، وعالج النقرات على العنوانين <span> تلك. افتح الحل في البيئة التجريبية جدول قابل للترتيب الأهمية: 4 اجعل الجدول قابلًا للترتيب: يجب أن يؤدي النقر على العناصر <th> إلى ترتيبه حسب العمود الموافق. لكلّ <th> نوع معين موجود بداخل السمة، هكذا: <table id="grid"> <thead> <tr> <th data-type="number">Age</th> <th data-type="string">Name</th> </tr> </thead> <tbody> <tr> <td>5</td> <td>John</td> </tr> <tr> <td>10</td> <td>Ann</td> </tr> ... </tbody> </table> في المثال أعلاه، يحتوي العمود الأول على الأرقام، و العمود الثاني على الحروف. يجب أن تقوم دالة الترتيب بمعالجة الترتيب حسب النوع. يستلزم فقط أن يُدعم النوعان "string" و "number". يمكن مشاهدة المثال يعمل من هنا. ملاحظة: يمكن أن يكون الجدول كبيرا، بأي عدد من الأسطر والأعمدة. أنجز التمرين في البيئة التجريبية الحل افتح الحل في البيئة التجريبية سلوك التلميحات الأهمية: 5 أنشئ شفرة جافاسكربت لأجل سلوك التلميحات (tooltips). عندما يحوم مؤشر الفأرة فوق عنصر له السمة data-tooltip، فيجب أن تظهر التلميحة فوقه، وعندما يفارقه فإنها تختفي. هذا مثال لشفرة HTML مع الشرح: <button data-tooltip="the tooltip is longer than the element">Short button</button> <button data-tooltip="HTML<br>tooltip">One more button</button> يجب أن تعمل كما هنا. سنفترض في هذا التمرين أن جميع العناصر التي لها data-tooltip تحتوي على نص فقط. لا وسوم متداخلة (بعد). التفاصيل: يجب أن تكون المسافة بين العنصر والتلميحة 5px. يجب أن تكون التلميحة في منتصف العنصر، إن أمكن ذلك. يجب ألا تقطع التلميحة حوافّ النافذة. من المفترض أن تكون التلميحة فوق العنصر، فإذا كان العنصر في أعلى الصفحة ولا مكان هناك للتلميحة، فإنها تكون تحته. يُعطى محتوى التلميحة في السمة data-tooltip. يمكنها أن تحوي أي شفرة HTML. ستحتاج إلى حدثين هنا: mouseover يحصل عندما يحوم المؤشر فوق العنصر. mouseout يحصل عندما يفارق المؤشر العنصر. يُرجى استخدام تفويض الأحداث: أسند اثنين من المعالجات إلى document لتتبّع كلّ "الحومان" و "المفارقة" للعناصر التي لها data-tooltip وقم بإدارة التلميحات من هناك. بعد تطبيق السلوك، يمكن ولو لأناس غير متعودين على جافاسكربت إضافة عناصر بتلميحات. ملاحظة: يجب أن تظهر تلميحة واحدة فقط في نفس الوقت. أنجز التمرين في البيئة التجريبية الحل افتح الحل في البيئة التجريبية ترجمة -وبتصرف- للمقال Event delegation من سلسلة Browser: Document, Events, Interfaces لصاحبها Ilya Kantor
  14. لنبتدئ بمثال. المعالج التالي مُسندٌ إلى العنصر <div>، لكنّه أيضًا يشتغل عند النقر على الوسوم الداخلة تحته مثل <em> أو <code>. <div onclick="alert('The handler!')"> <em>If you click on <code>EM</code>, the handler on <code>DIV</code> runs.</em> </div> See the Pen JS-p2-bubbling-and-capturing -ex1 by Hsoub (@Hsoub) on CodePen. أليس هذا غريبًا بعض الشيء؟ لماذا يشتغل المعالج المُسنَد إلى <div> إذا كان النقر في الواقع على <em>؟ انتشار اﻷحداث نحو اﻷعلى مبدأ الانتشار نحو اﻷعلى (bubbling) بسيط. عندما يقع حدثٌ على عنصرٍ ما، فإنّه يُشغّل المعالجات المسندة إليه، ثم المسندة إلى أبيه، وهكذا صعودًا إلى المعالجات المسندة إلى أسلافه الآخرين. لنفترض أنّ لدينا ثلاثة عناصر متداخلة P < DIV < FORM ، مع معالجٍ لكلّ منها: <style> body * { margin: 10px; border: 1px solid blue; } </style> <form onclick="alert('form')">FORM <div onclick="alert('div')">DIV <p onclick="alert('p')">P</p> </div> </form> See the Pen JS-p2-bubbling-and-capturing -ex2 by Hsoub (@Hsoub) on CodePen. يؤدّي النّقر على العنصر <p> الذي بالداخل أوّلًا إلى تشغيل onclick : المُسند إلى <p> ذاك. ثم المُسند إلى <div> الذي خارجه. ثم المُسند إلى <form> الذي خارجه. وهكذا صعودًا إلى الكائن document. فإذا نقرنا على <p> ، سنرى ثلاثة تنبيهات متتالية: form <- div <- p. تُعرف على هذه العمليّة بالانتشار نحو اﻷعلى (bubbling)، لأنّ الأحداث تنتشر من العنصر الداخلي صعودًا عبر آبائه كالفقّاعة في الماء. تنبيه: تنتشر مُعظم اﻷحداث نحو اﻷعلى ينبغي التنبه في هذه الجملة إلى كلمة "مُعظم". على سبيل المثال، لا ينتشر الحدث focus نحو اﻷعلى. وسنرى أيضا أمثلة أخرى. لكنّها تبقى استثناءً عوض القاعدة، فمعظم اﻷحداث تنتشر نحو اﻷعلى. event.target يمكن للمعالج المسند إلى عنصرٍ أبٍ أن يتحصّل دائمًا على تفاصيل مكان وقوع الحدث. يُسمّى العنصر اﻷدنى الذي نشأ عنه الحدث بالعنصر "الهدف"، ويمكن الوصول إليه بواسطة event.target. لاحظ الاختلاف الذي بينه و this (الذي هو نفس event.currentTarget): event.target -- هو العنصر "الهدف" الذي أنشأ الحدث، ولا يتغيّر خلال عمليّة الانتشار نحو اﻷعلى. this -- هو العنصر "الحاليّ"، أي الذي أُسنِد إليه المعالجُ الذي يشتغل حاليّا. على سبيل المثال، إذا كان لدينا معالجٌ وحيدٌ form.onclick مُسندٌ إلى النموذج <form>، فإنّه يمكنه "التقاط" جميع النقرات داخل النموذج. أيّا كان مكان وقوعها، فإنّها تنتشر نحو اﻷعلى إلى <form> وتشغّل المعالج. في المعالج form.onclick: this (الذي هو نفس event.currentTarget) هو العنصر <form>، لأنّ المعالج المُشتغل مسندٌ إليه. event.target هو العنصر الذي نُقر عليه داخل النموذج. يمكنك رؤية ذلك من هنا ، من خلال النقر على مختلف العناصر لإظهار event.target و this في كلّ حالة. قد يكون event.target هو نفسه this، كما لو نقرنا هنا على العنصر <form> مباشرة. إيقاف الانتشار نحو اﻷعلى ينطلق الحدث عند انتشاره نحو اﻷعلى من العنصر الهدف مباشرة. ويواصل الانتشار عادةً إلى أن يصل إلى <html>، ومن ثَمّ إلى الكائن document، بل إنّ بعض اﻷحداث قد تصل إلى window، ويشغّل جميع المعالجات في طريقه إلى هناك. لكن قد يقرّر أحد المعالجات أن الحدث قد تمّت معالجته ويوقف بذلك عمليّة الانتشار. يتوقّف الانتشار نحو اﻷعلى بواسطة التابع event.stopPropagation()‎. على سبيل المثال، لا يشتغل المعالج body.onclick هنا عند النقر على <button>: <body onclick="alert(`the bubbling doesn't reach here`)"> <button onclick="event.stopPropagation()">Click me</button> </body> See the Pen JS-p2-bubbling-and-capturing -ex3 by Hsoub (@Hsoub) on CodePen. ملاحظة: ()event.stopImmediatePropagation إذا أُسنِد إلى عنصرٍ ما عدّةُ معالجات لنفس الحدث، فحتىّ لو أوقف أحدها الانتشار نحو اﻷعلى، ستشتغل المعالجات الأخرى. بعبارة أخرى، يوقِف التابع event.stopPropagation()‎‎ الانتشار نحو اﻷعلى، لكن ستشتغل بقيّة المعالجات المسندة إلى العنصر الحاليّ. لإيقاف الانتشار نحو اﻷعلى، ومنع اشتغال بقيّة المعالجات المُسندة إلى العنصر الحاليّ أيضا، يوجد هناك تابع لذلك event.stopImmediatePropagation()‎‎ لا يشتغل بعده معالج. تنبيه: لا توقف الانتشار نحو اﻷعلى دون الحاجة لذلك! الانتشار نحو اﻷعلى أمرٌ ملائم. لا توقفه دون سبب وجيه، يكون واضحًا ومُمحصًّا هندسيًّا. قد يُحدث التابع event.stopPropagation()‎‎ أحيانًا مزالق تتسبّب لاحقًا في مشاكل. على سبيل المثال: ننشئ قائمة متداخلة. تعالج كلُّ قائمة داخليّة النقرات التي على عناصرها، وتستدعيstopPropagation لتفادي تفعيل القائمة الخارجيّة. نقرّر بعدها أن نلتقط جميع النقرات على النافذة، لتتبع سلوك المستخدمين (أين ينقر الناس). تقوم بعض أنظمة التحليل بذلك، وعادةً ما تستخدم الشيفرةُ التابعَ document.addEventListener('click'…)‎‎ لالتقاط جميع النقرات. لن يعمل نظام التحليل على المساحة التي أوقف فيها انتشار النقرات نحو اﻷعلى بواسطة stopPropagation. فيكون بذلك لدينا "منطقة ميّتة" للأسف. لا توجد في العادة حاجة حقيقيّة لإيقاف الانتشار نحو اﻷعلى. فالمهام التي تبدو أنّها تتطلب ذلك يمكن حلّها بوسائل أخرى. من بين هذه الوسائل، استخدام اﻷحداث المخصّصة (custom events) التي سنتناولها لاحقا. يمكننا أيضا كتابة بياناتٍ على الكائن event في معالج وقراءتها في معالج آخر، ليتسنى بذلك تمرير معلومات إلى المعالجات المسندة إلى الآباء حول المعالجة التي تمت في اﻷسفل. الانتشار نحو الأسفل توجد هناك مرحلة أخرى لمعالجة اﻷحداث يُطلق عليها "الانتشار نحو اﻷسفل" (capturing). من النادر استخدامها في شيفرات واقعيّة، لكنّها قد تكون مفيدة أحيانا. يصِف معيار أحداث DOM ثلاث مراحل لانتشار الأحداث: مرحلة الانتشار نحو اﻷسفل (Capturing phase) - ينزل الحدث إلى العنصر. مرحلة الهدف (Target phase) - يصل الحدث إلى العنصر الهدف. مرحلة الانتشار نحو اﻷعلى (Bubbling phase) - ينتشر الحدث صعودًا من العنصر. هذه صورة لما يحصل عند النقر على <td> داخل جدول، مأخوذة من المواصفة: ما يعني ذلك: بالنقر على <td> ، ينتشر الحدث أوّلا عبر سلسلة الأسلاف نزولًا إلى العنصر (مرحلة الانتشار نحو اﻷسفل)، فيبلغ الهدفَ ويتفعّل هناك (مرحلة الهدف)، ثم ينتشر صعودًا (مرحلة الانتشار نحو اﻷعلى) مستدعيًا المعالجات في طريقه. اقتصرنا في السابق على مرحلة الانتشار نحو اﻷعلى، لأنّه من النادر استخدام مرحلة الانتشار نحو اﻷسفل. لا تظهر لنا عادةً. لا يعلم المعالجون الذين عُيّنوا على شكل خاصيّة on<event>‎‎ ، أو على شكل سمة HTML، أو باستخدام addEventListener(event, handler)‎‎ بوسيطين فقط، شيئًا عن الانتشار نحو اﻷسفل، فهم يشتغلون فقط في المرحلتين الثانية والثالثة. لالتقاط حدثٍ في مرحلة الانتشار نحو اﻷسفل، يجب تغيير قيمة الخيار capture إلى true. elem.addEventListener(..., {capture: true}) // {capture: true} فهو اختصار لـ ،"true" أو فقط elem.addEventListener(..., true) يمكن أن يأخذ الخيار capture قيمتين: إذا كانت false (افتراضيًّا)، فإنّ المعالج يوضع في مرحلة الانتشار نحو اﻷعلى. إذا كانت true، فإنّ المعالج يوضع في مرحلة الانتشار نحو اﻷسفل. لاحظ رغم أنّه يوجد رسميًّا ثلاث مراحل، إلّا أن المرحلة الثانية (مرحلة الهدف: عندما يبلغ الحدثُ الهدف) لا تُعالَج بشكل مستقل، بل تشتغل كلٌّ من المعالجات الموضوعة في مرحلتي الانتشار نحو اﻷسفل واﻷعلى في هذه المرحلة أيضا. لنرى كلًّا من الانتشار نحو اﻷسفل والأعلى حال عملهما: <style> body * { margin: 10px; border: 1px solid blue; } </style> <form>FORM <div>DIV <p>P</p> </div> </form> <script> for(let elem of document.querySelectorAll('*')) { elem.addEventListener("click", e => alert(`Capturing: ${elem.tagName}`), true); elem.addEventListener("click", e => alert(`Bubbling: ${elem.tagName}`)); } </script> See the Pen JS-p2-bubbling-and-capturing -ex4 by Hsoub (@Hsoub) on CodePen. تُسند الشيفرة معالجاتٍ لحدث النّقر إلى جميع العناصر التي في المستند لرؤية أيّها تعمل. عند النقر على <p>، فإنّ التسلسل يكون كالتالي: DIV ‹- FORM ‹- BODY ‹- HTML (مرحلة الانتشار نحو اﻷسفل، أوّل المنصتين). P (مرحلة الهدف، تُفعّل مرّتين لأنّنا وضعنا مُنصتَين اثنين: الانتشار نحو اﻷسفل واﻷعلى). HTML ‹- BODY ‹- FORM ‹- DIV (مرحلة الانتشار نحو اﻷسفل، ثاني المنصتين). توجد هناك الخاصيّة event.eventPhase التي تخبرنا برقم المرحلة التي تمّ فيها التقاط الحدث. لكن يندر استخدامها لأنّنا نعلم ذلك من خلال المعالج عادةً. ملاحظة: لحذف المعالج، يستلزم التابع removeEventListener إعطاء نفس المرحلة عند إضافة معالجٍ بهذا الشكل addEventListener(..., true)‎‎، فيجب ذكر نفس المرحلة أيضًا في removeEventListener(..., true)‎‎ لحذف المعالج بشكل صحيح. ملاحظة: تشتغل المعالجات التي أُسندت إلى نفس العنصر وفي نفس المرحلة حسب الترتيب الذي أُنشئت به إذا كانت لدينا عدّة معالجات للأحداث مُسنَدة إلى نفس العنصر، وفي نفس المرحلة فإنها تشتغل بحسب الترتيب الذي أُنشئت به. elem.addEventListener("click", e => alert(1)); // من المؤكّد أن يشتغل أوّلا elem.addEventListener("click", e => alert(2)); الملخص عندما يقع الحدث، فإن أدنى العناصر الذي وقع فيه الحدث يُعلَّم بالعنصر"الهدف" (event.target). ثم ينتشر الحدث نزولًا من جذر المستند إلى event.target مستدعيًا في طريقه المعالجين المعيّنين بواسطة addEventListener(..., true)‎‎ (القيمة ‎‎ true هي اختصار لـ {capture: true}) ثم نُستدعى المعالجات المُسندة إلى العنصر الهدف نفسه. ثم ينتشر الهدف صعودًا من event.target إلى الجذر، مناديًا المعالجات المعيّنة بواسطة on<event>‎‎ و addEventListener دون الوسيط الثالث false/{capture:false}‎‎. يمكن لأيّ معالجٍ أن يستخدم خاصيّات الكائن event التالية: event.target -- أدنى العناصر الذي نشأ عنه الحدث. event.currentTarget (هو نفس this) -- العنصر الذي يعالج الحدث حاليًّا (الذي أُسند إليه المعالج). event.eventPhase -- المرحلة الحاليّة (1=الانتشار نحو اﻷسفل، 2=الهدف، 3=الانتشار نحو اﻷعلى). يمكن لأيّ معالجٍ أن يوقف انتشار الحدث من خلال استدعاء event.stopPropagation()‎‎، لكن لا يُنصح بذلك، لأنّه لا يمكن التأكّد حقًّا من عدم الحاجة إليه في الأعلى، ربّما في أمورٍ مختلفة تماما. يندر جدًّا استخدام مرحلة الانتشار نحو اﻷسفل، إذ تُعالَج الأحداث عادةً عند انتشارها نحو اﻷعلى. هناك سبب منطقي وراء ذلك. في الواقع، عندما يقع حادث ما، تُبلَّغ السلطات المحليّة أوّلًا. فهم أفضل من يعرف المنطقة التي وقع فيها الحادث. ثمّ تُبلّغ السلطات الأعلى عند الحاجة لذلك. ينطبق الأمر على معالجات الأحداث. تكون الشيفرة المسؤولة عن إسناد المعالج إلى عنصرٍ ما أعلم بالتفاصيل المتعلّقة بذلك العنصر ومالذي يفعله. فيكون المعالج المسند إلى العنصر<td> خصيصا أنسب بتولي أمر ذلك العنصر بالذات، إذ يعلم كلّ شيء بخصوصه، وينبغي أن تُمنح له الفرصة أوّلا. ثم يأتي أبوه المباشر، الذي يكون له اطّلاع على السياق لكنّه أقلّ معرفةً به. وهكذا إلى أعلى العناصر، الذي يعالج الأمور العامّة ويكون آخرهم اشتغالا. يرسي مفهوم الانتشار نحو اﻷسفل واﻷعلى اﻷساس لموضوع "تفويض الأحداث"، الذي يُعدّ نمط معالجةٍ للأحداث قويًّا للغاية. سندرسه في المقال التالي. ترجمة -وبتصرف- للمقال Bubbling and capturing من سلسلة Browser: Document, Events, Interfaces لصاحبها Ilya Kantor
  15. تمثّل الأحداث (events) إشاراتٍ إلى أنّ شيئًا ما قد حصل. يمكن أن تنشأ هذه الإشارات من أيّ عقدة في DOM (لكنّها لا تقتصر فقط على DOM). على سبيل المثال، هذه قائمة لأكثر الأحداث فائدةً: أحداث الفأرة: click -- عند النقر بالفأرة على عنصرٍ ما (أو عند الضغط عليه باستخدام الأجهزة اللمسية) contextmenu -- عند النقر بالزرّ الأيمن للفأرة على عنصرٍ ما. mouseout \ mouseover -- عندما يبلغ / يغادر مؤشّر الفأرة عنصرًا ما. mouseup \ mousedown -- عند ضغط / تحرير زرّ الفأرة. mousemove -- عند تحريك مؤشّر الفأرة. أحداث لوحة المفاتيح: keyup \ keydown -- عند ضغط / إرسال أحد أزرار لوحة المفاتيح. أحداث النماذج: submit -- عندما يرسل المستخدم النموذج <form>. focus -- عندما يحدّد المستخدم عنصرًا ما في النموذج، كتحديده عنصر <input> مثلا. أحداث المستند: DOMContentLoaded -- عند الفراغ من تحميل ملف HTML ومعالجتِه، وبناء كامل شجرة DOM. الأحداث المتعلقة بـ CSS: transitionend - عند انتهاء تحريكCSS (animation) ‎. وهناك العديد من الأحداث الأخرى، سنتناول بعضها بمزيدٍ من التفصيل في مقالاتٍ لاحقة. معالجات الأحداث من أجل الاستجابة للأحداث، يمكننا تعيين معالجٍ (handler) على شكل دالّة تُنفَّذ عند وقوع الحدث. وتُعدّ بذلك المعالجات وسيلةً لتنفيذ شيفرات جافاسكربت وفقًا لما يقوم به المستخدم. توجد عدة طرق لتعيين معالجٍ للحدث. سنتناولها بدءًا بأبسطها. على شكل سمة HTML يمكن تعيين المعالج في HTML على شكل سمةٍ (attribute) يكون اسمها على النحو on<event>‎ أي on متبوعة باسم الحدث. على سبيل المثال، لتعيين معالجٍ لحدث click على عنصر input، يمكننا استخدام السمة onclick كالتالي: <input value="Click me!" onclick="alert('Click!')" type="button"> See the Pen JS-p2-introduction-browser-events-ex1 by Hsoub (@Hsoub) on CodePen. عند النقر بالفأرة، تُنفّذ الشيفرة التي بداخل onclick. يرجى التنبّه هنا إلى أنّنا استخدمنا علامات الاقتباس غير المزدوجة داخل onclick، لأنّ السّمة نفسها محاطة بعلامات اقتباس مزدوجة. فلو غفلنا عن أنّ الشيفرة موجودة داخل السّمة واستخدمنا داخلها علامات الاقتباس المزدوجة هكذا onclick="alert("Click!")"‎ فلن تعمل الشيفرة بشكل صحيح. لا تعدّ سمة HTML مكانا مناسبا لكتابة الكثير من الشيفرة، فيحسُن إذًا أن ننشئ دالّة جافاسكربت ونستدعيها هناك. في المثال أدناه، يؤدي النقر إلى تنفيذ الدالّة countRabbits()‎ <script> function countRabbits() { for(let i=1; i<=3; i++) { alert("Rabbit number " + i); } } </script> <input type="button" onclick="countRabbits()" value="Count rabbits!"> See the Pen JS-p2-introduction-browser-events-ex2 by Hsoub (@Hsoub) on CodePen. كما هو معلوم، لا فرق بين الأحرف الكبيرة والصغيرة في تسمية سمات HTML، فتمثل كلّ من ONCLICK و onClick و onCLICK نفس السّمة، لكن في الغالب تُكتب السّمات بأحرف صغيرة هكذا onclick. على شكل خاصيّة DOM يمكننا تعيين معالجٍ على شكل خاصيّة DOM يكون اسمها على النحو on<event>‎. على سبيل المثال، elem.onclick: <input id="elem" type="button" value="Click me"> <script> elem.onclick = function() { alert('Thank you'); }; </script> See the Pen JS-p2-introduction-browser-events-ex3 by Hsoub (@Hsoub) on CodePen. عند تعيين المعالج على شكل سمة HTML، فإنّ المتصفّح يقرأها ثمّ يُنشئ من محتواها دالّة، ويكتبها على شكل خاصيّة في DOM. فتكون بذلك هذه الطريقة لتعيين المعالج مساوية للتي قبلها. تؤدي هاتان الشيفرتان الوظيفة نفسها: مجرّد HTML: <input type="button" onclick="alert('Click!')" value="Button"> HTML + JS: <input type="button" id="button" value="Button"> <script> button.onclick = function() { alert('Click!'); }; </script> استخدمنا في المثال الأوّل سمة HTML لتهيئة button.onclick، بينما في المثال الثاني استخدمنا سكربتًا لذلك. هذا الفرق الذي هناك. بما أنّ هناك خاصيّة واحدة فقط تحمل الاسم onclick، فلا يمكننا تعيين أكثر من معالج واحد لنفس الحدث. في المثال أدناه، تؤدي إضافة معالجٍ بواسطة جافاسكربت إلى استبدال المعالج الموجود مسبقًا. <input type="button" id="elem" onclick="alert('Before')" value="Click me"> <script> elem.onclick = function() { // يستبدل المعالج الموجود alert('After'); // هذا ما سيظهر فقط }; </script> See the Pen JS-p2-introduction-browser-events-ex4 by Hsoub (@Hsoub) on CodePen. لحذف المعالج، يكفي إعطاء الخاصيّة القيمة null هكذا: elem.onclick = null. الوصول إلى العنصر بواسطة this تشير الكلمة this داخل المعالج إلى نفس العنصر الذي أُسند إليه المعالج. ففي الشيفرة أدناه، يُظهر العنصر button محتواه بواسطة this.innerHTML <button onclick="alert(this.innerHTML)">Click me</button> See the Pen JS-p2-introduction-browser-events-ex5 by Hsoub (@Hsoub) on CodePen. أخطاء محتملة في بداية التعامل مع معالجات الأحداث، يُرجى التنبه لعدد من الأمور الدقيقة. يمكن تعيين معالج من دالة موجودة مسبقا: function sayThanks() { alert('Thanks!'); } elem.onclick = sayThanks; لكن انتبه، يجب إسناد الدالة كـ sayThanks وليس sayThanks()‎. // صحيح button.onclick = sayThanks; // خطأ button.onclick = sayThanks(); بإضافة الأقواس، تصير sayThanks()‎ استدعاءًا للدالة. وبالتالي، يأخذ السطر الأخير ناتج تنفيذ الدالّة (الذي هو undefined بحكم أنّ الدالة لا تعيد أيّ شيء) ويضعه في onclick. هذا لا يصلح. … في المقابل، نحتاج في HTML إلى تضمين الأقواس: <input type="button" id="button" onclick="sayThanks()"> يمكن توضيح سبب ذلك كالتالي: عندما يقرأ المتصفّح السّمة، فإنّه ينشئ معالجًا على شكل دالّة لها نفس محتوى هذه السّمة. فيقوم HTML الذي لدينا بإنشاء الخاصيّة التالية: button.onclick = function() { sayThanks(); // <-- يصير محتوى السّمة هنا }; لا تستخدم setAttribute لتعيين المعالجات. هذه الشيفرة لا تصلح: // إلى توليد أخطاء <body> يؤدي النفر على // بحكم أنّ السّمات هي سلاسل نصيّة، فتصير الدالة سلسلة نصيّة ايضا document.body.setAttribute('onclick', function() { alert(1) }); تفرّق خاصيّات DOM بين الأحرف الكبيرة والصغيرة. يجب تعيين المعالج في elem.onclick بدل elem.ONCLICK ، لأنّ خاصيّات DOM تفرُق معها الأحرف الكبيرة والصغيرة. addEventListener تكمن المشكلة الأساسية في طرق تعيين المعالج السالفة الذكر، في عدم إمكانيّة تعيين عدّة معالجات لحدث واحد. لنفترض أن جزءًا من الشيفرة التي لدينا يهدف إلى إبراز أحد الأزرار عند النقر عليه، بينما يهدف جزء آخر من الشيفرة إلى إظهار رسالة ما عند نفس النقرة. قد نودّ تعيين عدّة معالجات للحدث لتحقيق ذلك، لكن بإضافة خاصيّة جديدة إلى DOM، تُستبدل الخاصيّة الموجودة مسبقًا. input.onclick = function() { alert(1); } // ... input.onclick = function() { alert(2); } // يستبدل المعالج السابق أدرك العاملون على معايير الويب هذه المشكلة منذ القدم، واقترحوا طريقة بديلة لإدارة معالجات الأحداث، وذلك بواسطة التوابع الخاصة addEventListener و removeEventListener. تكون صيغة إضافة معالجٍ فيها كالتالي: element.addEventListener(event, handler, [options]); حيث أن: event: هو اسم الحدث، كـ "click" مثلًا. handler: هي دالّة المعالج. options: هو كائن إضافي اختياري، وله الخاصيّات التالية: once: إذا كانت قيمتها true، فإن منصت الحدث (event listener) يزول تلقائيا بعد حصول الحدث. capture المرحلة التي يُعالَج فيها الحدث، وسنتطرّق إليها لاحقا في مقال انتشار اﻷحداث. لأسباب تاريخية، يمكن أن تحمل options القيمة true \ false ويكون لذلك نفس معنى {capture: false/true}. passive: إذا كانت قيمتها true، فلن يستدعي المعالجُ التابعَ preventDefault()‎، وسنشرح ذلك في مقال أفعال المتصفّح الافتراضية. يمكن حذف المعالج بواسطة: element.removeEventListener(event, handler, [options]); تنبيه: يتطلّب الحذفُ الدالةَ نفسَها لحذف المعالج يجب تمرير نفس الدالة التي عُيّنت من قبل. فلو جرّبنا مثلًا: elem.addEventListener( "click" , () => alert('Thanks!')); // .... elem.removeEventListener( "click", () => alert('Thanks!')); لن يحُذف المعالج، لأن removeEventListener قد تلقّى دالة أخرى -- لها نفس الشيفرة، لكن لا يهم ذلك لأنها كائن دالة آخر. هذه هي الطريقة الصحيحة لحذف المعالج: function handler() { alert( 'Thanks!' ); } input.addEventListener("click", handler); // .... input.removeEventListener("click", handler); يرُجى التنبّه هنا إلى أنّه إذا لم نحفظ الدالة في متغيّر، فلا يمكننا حذفها. إذ لا سبيل إلى "إعادة قراءة" المعالجات المُعيّنة بواسطة addEventListener. يمكّن الاستدعاء المتكرّر لـ addEventListener من تعيين عدّة معالجات كالتالي: <input id="elem" type="button" value="Click me"/> <script> function handler1() { alert('Thanks!'); }; function handler2() { alert('Thanks again!'); } elem.onclick = () => alert("Hello"); elem.addEventListener("click", handler1); // Thanks! elem.addEventListener("click", handler2); // Thanks again! </script> See the Pen JS-p2-introduction-browser-events-ex6 by Hsoub (@Hsoub) on CodePen. كما هو مبيّن في المثال أعلاه، يمكن تعيين معالجات باستخدام كلٍّ من خاصيّة DOM وaddEventListener معًا، لكن في الغالب تُستخدم إحداهما فقط. تنبيه: لا يمكن تعيين معالجات لبعض الأحداث إلا بواسطة addEventListener توجد أحداثٌ لا يمكن تعيين معالجات لها عن طريق خاصيّة DOM ، بل تشترط استخدام addEventListener. على سبيل المثال، الحدث DOMContentLoaded، الذي يحصل حين الانتهاء من تحميل المستند وبناء DOM. // لن يتم تنفيذ هذا أبدا document.onDOMContentLoaded = function() { alert("DOM built"); }; // هذه الطريقة أصحّ document.addEventListener("DOMContentLoaded", function() { alert("DOM built"); }); بذلك يكونaddEventListener أشمل، رغم أنّ هذه الأحداث تُعدّ استثناءًا وليست القاعدة. كائن الحدث لمعالجة الحدث كما ينبغي، قد يلزمنا معرفة المزيد عمّا حصل بالضبط. فليس مجرّد "النقر" أو "الضغط"، بل أيضا ما هي إحداثيات المؤشر؟ أو ما هو الزر التي ضُغط؟ إلى غير ذلك. عند وقوع حدثٍ ما، يُنشئ المتصفّحُ كائن حدث ويضع فيه التفاصيل، ثم يمرّره على شكل وسيط للمعالج. هذا مثال لكيفية الحصول على إحداثيات المؤشر من كائن الحدث: <input type="button" value="Click me" id="elem"> <script> elem.onclick = function(event) { // يظهر نوع الحدث والعنصر وإحداثيات النقر alert(event.type + " at " + event.currentTarget); alert("Coordinates: " + event.clientX + ":" + event.clientY); }; </script> See the Pen JS-p2-introduction-browser-events-ex7 by Hsoub (@Hsoub) on CodePen. هذه بعض خاصيّات كائن الحدث event: event.type: نوع الحدث، و هو في هذا المثال "click" event.currentTarget: العنصر الذي عالج الحدث. وهو نفس this، إلا إذا كان المعالج دالة سهمية أو أن this الخاصّ به مرتبط (bound) بشيء آخر، فعندها يمكن الحصول على العنصر بواسطة event.currentTarget. event.clientX / event.clientY: هي إحداثيات المؤشّر بالنسبة للنافذة، عند الأحداث المتعلقة بالمؤشّر. هناك المزيد من الخاصيّات، والكثير منها متعلق بنوع الحدث. فأحداث لوحة المفاتيح لها مجموعة من الخاصيّات، وأحداث المؤشر لها مجموعة أخرى. سندرس ذلك لاحقا عندما نتطرّق لمختلف الأحداث بالتفصيل. ملاحظة: كائن الحدث متوفر أيضا للمعالجات في HTML إذا عينّا معالجًا في HTML، فإنّه من الممكن أيضا استخدام الكائن event، كالتالي: <input type="button" onclick="alert(event.type)" value="Event type"> See the Pen JS-p2-introduction-browser-events-ex8 by Hsoub (@Hsoub) on CodePen. يمكننا ذلك لأن المتصفّح حينما يقرأ السمة، فإنّه ينشئ معالجًا بهذا الشكل: function(event) { alert(event.type) }‎، فيكون اسم وسيطه الأول "event"، ومحتواه مأخوذا من السمة. الكائنات المعالجة: handleEvent يمكن بواسطة addEventListener تعيين معالجٍ على شكل كائن أيضا، وعند وقوع الحدث، يُستدعى التابع handleEvent. على سبيل المثال: <button id="elem">Click me</button> <script> let obj = { handleEvent(event) { alert(event.type + " at " + event.currentTarget); } }; elem.addEventListener('click', obj); </script> See the Pen JS-p2-introduction-browser-events-ex8 by Hsoub (@Hsoub) on CodePen. كما نرى، عندما يستقبل addEventListener كائنًا، فإنه يستدعي obj.handleEvent(event)‎ في حال وقوع الحدث. يمكننا أيضا استخدام صنفٍ لذلك: <button id="elem">Click me</button> <script> class Menu { handleEvent(event) { switch(event.type) { case 'mousedown': elem.innerHTML = "Mouse button pressed"; break; case 'mouseup': elem.innerHTML += "...and released."; break; } } } let menu = new Menu(); elem.addEventListener('mousedown', menu); elem.addEventListener('mouseup', menu); </script> See the Pen JS-p2-introduction-browser-events-ex9 by Hsoub (@Hsoub) on CodePen. يقوم نفس الكائن هنا بمعالجة كلا الحدثين. لكن ينبغي التنبه إلى أنه يجب تحديد الأحداث المراد الإنصات إليها باستخدام addEventListener صراحةً. يتلقى الكائن menu هنا الحدثين mousedown و mouseup دون غيرهما من أنواع الأحداث الأخرى. لا يلزم التابع handleEvent أن يقوم بكامل العمل، بل يمكنه استدعاء توابع أخرى مختصة بأحداث معيّنة، كما يلي: <button id="elem">Click me</button> <script> class Menu { handleEvent(event) { // mousedown -> onMousedown let method = 'on' + event.type[0].toUpperCase() + event.type.slice(1); this[method](event); } onMousedown() { elem.innerHTML = "Mouse button pressed"; } onMouseup() { elem.innerHTML += "...and released."; } } let menu = new Menu(); elem.addEventListener('mousedown', menu); elem.addEventListener('mouseup', menu); </script> See the Pen JS-p2-introduction-browser-events-ex10 by Hsoub (@Hsoub) on CodePen. هكذا يتم فصل المعالجات على حدة، مما قد يجعل دعمها أسهل. الملخص توجد ثلاث طرق لتعيين معالجات للأحداث: سمة HTML: ‏ onclick="..."‎. خاصيّة DOM:‏ elem.onclick = function. توابع: elem.addEventListener(event, handler[, phase])‎ للإضافة، و removeEventListener للحذف. يندر استخدام سمات HTML، لأن جافاسكربت تبدو غريبة وسط وسم HTML. بالإضافة إلى أنه لا يمكن كتابة الكثير من الشيفرة هناك. لا بأس باستخدام خاصيّات DOM، غير أنه لا يمكن تعيين أكثر من معالج لنفس الحدث. في الكثير من الأحيان قد لا يمثّل هذا القيد مشكلة تُذكر. تُعدّ الطريقة الأخيرة أكثرها مرونة، لكنها أيضا أطولها كتابة. هناك بعض الأحداث التي لا تعمل إلا بواسطتها، على سبيل المثال transitionend و DOMContentLoaded (ستُدرس لاحقًا). بالإضافة إلى ذلك، تدعم addEventListener تعيين معالجات للأحداث على شكل كائنات، ويُستدعى حينها التابع handleEvent عند وقوع الحدث. أيّا كانت طريقة تعيين المعالج، فإنّه يستقبل كائن حدث كوسيط أول. يحتوي هذا الكائن تفاصيل ما قد حصل. سنتعلم المزيد عن الأحداث عموما، وعن مختلف أنواع الأحداث في المقالات القادمة. التمارين الإخفاء عند النقر الأهمية: 5 أضف جافاسكربت إلى الزر button لجعل <div id="text"‎> يختفي عند النقر عليه، كما هنا افتح البيئة التجريبيّة لإنجاز التمرين الحل افتح البيئة التجريبيّة لمشاهدة الحلّ الاختفاء عند النقر الأهمية: 5 أنشئ زرّا يخفي نفسه عند النقر عليه. الحل يمكن استخدام this للإشارة إلى "العنصر نفسه" هنا: <input type="button" onclick="this.hidden=true" value="Click to hide"> أيّ المعالجات ستُنفّذ؟ اﻷهمية: 5 يحتوي المتغير button على زرّ، وليس عليه معالجات. أيّ من المعالجات ستُنفّذ بعد تشغيل الشيفرة أدناه؟ ما هي التنبيهات التي تظهر؟ button.addEventListener("click", () => alert("1")); button.removeEventListener("click", () => alert("1")); button.onclick = () => alert(2); الحل الجواب هو 1 و 2. يستجيب المعالج الأول لأنه لم يُحذف بواسطة removeEventListener. لحذف المعالج، يجب تمرير نفس الدالة التي عُيّنت بالضبط. وفي الكود أعلاه، مُررت دالة جديدة، تبدو مثلها تماما لكنها تبقى دالة أخرى. لحذف كائن دالة، نحتاج أن نحفظ مرجعا لها، كما يلي: function handler() { alert(1); } button.addEventListener("click", handler); button.removeEventListener("click", handler); أما المعالج button.onclick فهو يعمل بشكل مستقل وبالإضافة إلى addEventListener. حرّك الكرة في الملعب حرّك الكرة في الملعب بواسطة النقر، كما هنا المطلوب: يجب أن ينتقل مركز الكرة إلى موضع المؤشر عند النقر (حبذا دون الخروج عن حافة الملعب). تحريكات CSS مُرحّب بها. يجب ألا تخرج الكرة عن حدود الملعب. يجب ألا يؤدي تمرير الصفحة إلى اختلاط الأمور . ملاحظات: يجب أن تشتغل الشيفرة مع مختلف أحجام الكرة والملعب، وألا تكون مرتبطة بقيم معينة. استخدم الخاصيّات event.clientX/event.clientY للحصول على إحداثيات النقر. افتح البيئة التجريبيّة لإنجاز التمرين الحل أولا، علينا أن نختار طريقة لتغيير موضع الكرة. لا يمكننا استخدام position:fixed لذلك، لأن تمرير الصفحة قد يخرج الكرة عن الملعب. لذا يلزمنا استخدام position:absolute، ولكي يكون التموضع جيد الإحكام، علينا أن نعطي للملعب نفسه وضعية، لتكون بذلك الكرة مُموضَعة بالنسبة إلى الملعب: #field { width: 200px; height: 150px; position: relative; } #ball { position: absolute; left: 0; /* (بالنسبة إلى أقرب سلف مموضَع (الملعب */ top: 0; transition: 1s all; /* الكرة تطير leftو top المتعلقة بـ CSS تجعل تحريكات */ } بعدها، علينا أن نعطي القيم المناسبة لـ ball.style.left/top ، إذ هي الآن تمثل إحداثيات الكرة بالنسبة إلى الملعب. هذه هي الصورة: تمثل event.clientX/clientY إحداثيات موضع النقر بالنسبة إلى النافذة. للحصول على الإحداثية left لموضع النقر بالنسبة إلى الملعب، يمكننا أن نطرح كلّا من الحافة اليسرى الملعب وسمك الحد: let left = event.clientX - fieldCoords.left - field.clientLeft; عادةً، يشير ball.style.left إلى "الحافة اليسرى للعنصر" (الكرة)، فإذا أعطيناه قيمة المتغير left، فإن حافة الكرة هي التي ستكون تحت المؤشر وليس مركزها. علينا إذًا أن نزيح الكرة بمقدار نصف عرضها إلى اليسار، وبمقدار نصف طولها إلى الأعلى كي نجعلها في المنتصف. فتكون القيمة النهائية لـ left هي: let left = event.clientX - fieldCoords.left - field.clientLeft - ball.offsetWidth/2; تُحسب الإحداثية العمودية بنفس الطريقة. يُرجى التنبه إلى أنه يجب أن يكون عرض الكرة وطولها معلومين عند قراءة ball.offsetWidth. يجب أن يُحدد ذلك في HTML أو CSS. افتح البيئة التجريبيّة لمشاهدة الحلّ أنشئ قائمة منحدرة اﻷهمية: 5 أنشئ قائمة تُفتح وتُغلق عند النقر كما هنا ملاحظة: ينبغي التعديل على الملف المصدري لـ HTML/CSS. افتح البيئة التجريبيّة لإنجاز التمرين الحل HTML/CSS لننشئ أولا HTML/CSS. تُعدّ القائمة مُكوّنا رسوميا مستقلا بذاته في الصفحة، لذا فيفضّل وضعها في عنصر DOM واحد. يمكن تنسيق عناصر القائمة على شكل ul/li. هذا مثال عن البنية: <div class="menu"> <span class="title">Sweeties (click me)!</span> <ul> <li>Cake</li> <li>Donut</li> <li>Honey</li> </ul> </div> استخدمنا <span> للعنوان، لأن <div> له ضمنيّا الخاصيّة display:block ، مما سيجعله يحتل 100% من المساحة الأفقية، هكذا: <div style="border: solid red 1px" onclick="alert(1)">Sweeties (click me)!</div> فإذا وضعنا عليه onclick ، فإنه سيستجيب للنقرات التي على يمين النص أيضا. وبما أن <span> له ضمنيّا الخاصيّة display: inline، فإنه سيحتل من المساحة فقط ما يكفي لاحتواء النص: <span style="border: solid red 1px" onclick="alert(1)">Sweeties (click me)!</span> إقلاب القائمة يؤدي إقلاب (toggling) القائمة إلى تغيير السهم وإظهار أو إخفاء عناصرها. يمكن القيام بكل هذه التغيرات من خلال CSS. في جافاسكربت علينا فقط تمييز الوضع الحالي للقائمة من خلال إضافة أو إزالة الصنف open.. بدون open. تكون القائمة منقبضة: .menu ul { margin: 0; list-style: none; padding-left: 20px; display: none; } .menu .title::before { content: '▶ '; font-size: 80%; color: green; } … ومع وجوده يتغير السهم وتظهر عناصر القائمة: .menu.open .title::before { content: '▼ '; } .menu.open ul { display: block; } افتح البيئة التجريبيّة لمشاهدة الحلّ أضف زرّا للإغلاق اﻷهمية: 5 هناك مجموعة من الرسائل. باستخدام جافاسكربت، أضف زرا في أقصى اليمين العلوي للرسائل لإغلاقها. يجب أن تبدو النتيجة كما هنا. افتح البيئة التجريبيّة لإنجاز التمرين الحل لإضافة الزر، يمكن استخدام كلّ من position:absolute (وجعل وضعية اللوحة نسبية position:relative) أو float:right. تتميز طريقة float:right بأنها تضمن عدم تقاطع الزر مع النص، لكن position:absolute تمنح المزيد من الحرية. فالخيار لك. بذلك تكون الشيفرة لكل من الألواح كالتالي: pane.insertAdjacentHTML("afterbegin", '<button class="remove-button">[x]</button>'); و يصير <button> بذلك pane.firstChild، مما يمكّننا من تعيين معالج له هكذا: pane.firstChild.onclick = () => pane.remove(); افتح البيئة التجريبيّة لمشاهدة الحلّ الدوّار اﻷهمية: 4 أنجز دوّارا -- شريط من الصور يمكن تمريره بواسطة أسهم، كما هنا يمكننا لاحقا إضافة المزيد من المزايا كالتمرير اللامتناهي، والتحميل الديناميكي وغير ذلك. ملاحظة: في هذا التمرين، تُمثل بنية HTML/CSS في الحقيقة 90% من الحل. افتح البيئة التجريبيّة لإنجاز التمرين الحل يمكن تمثيل شريط الصور بقائمة ul/li من الصور <img>. من المفترض أن هذا الشريط واسع، لكننا نحدّه بـ <div> ثابت الحجم لقطعه، فيبدو بذلك جزء من الشريط فقط: لعرض القائمة أفقيا، يجب تطبيق الخاصيّات المناسبة لـ <li> في CSS، مثل display: inline-block. بالنسبة لـ <img> يجب أيضا تعديل display، لأنها تكون افتراضيا inline. توجد هناك مساحة تحت العناصر inline مخصصة لأذيال الحروف، فيمكن استخدام display:block لإزالتها. للقيام بالتمرير يمكن إزاحة <ul>. هناك عدة طرق لفعل ذلك، مثل تغيير margin-left أو (لأداء أفضل) استخدام translateX()‎: بما أن العرض الخارجي لـ <div> محدود، فإن الصور "الزائدة" ستُخفى. يُعد الدوّار بأكمله مكونا "رسوميا" مستقلا بذاته، فيُفضل وضعه في <div class="carousel"> واحد وتنسيق جميع الأمور بداخله. افتح البيئة التجريبيّة لمشاهدة الحلّ ترجمة -وبتصرف- للمقال Introduction to browser events من سلسلة Browser: Document, Events, Interfaces لصاحبها Ilya Kantor