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

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

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

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

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

كل منشورات العضو محمد أمين بوقرة

  1. يعتمد مجرى تنفيذ جافاسكربت في المتصفّح، وكذلك في Node.js، على حلقة اﻷحداث event loop. يُعدّ الفهم الجيّد لكيفيّة عمل حلقة اﻷحداث مهمّا عند تطبيق التحسينات optimizations وأحيانا من أجل هندسة صحيحة لما نبنيه. في هذا المقال، سنتناول أوّلا التفاصيل النظريّة لكيفيّة عمل اﻷمور، ثمّ سنرى تطبيقات عمليّة لتلك المعرفة. حلقة اﻷحداث مفهوم حلقة اﻷحداث بسيط للغاية. توجد هناك حلقة لا نهاية لها، حيث ينتظر محرّك جافاسكربت المهامّ، وينفّذها ثم ينام، منتظرا المزيد من المهامّ. الخوارزميّة العامّة للمحرّك: مادام أنّ هناك مهامّا: ينفّذها، بدءًا بالمهمّة الأقدم. ينام إلى أن تظهر مهمّة ما، فينتقل عند ذلك إلى 1. هذا ما يحصل عندما نتصفّح صفحة ما، إذ لا يقوم محرّك جافاسكربت بأيّ شيء في غالب اﻷحيان، بل يشتغل فقط إذا لتنفيذ سكربت/معالج/حدث ما. أمثلة للمهام: عندما يُحمَّل سكربت خارجيّ <script src="..."‎>، تكون المهمّة هي تنفيذه. عندما يحرّك المستخدم فأرته، تكون الهمّة هي إرسال الحدث mousemove وتنفيذ المعالجات. عندما يحين وقت الدالة setTimeout المُنتظَر، تكون المهمة هي تنفيذ رد النداء الخاص بها. وما إلى ذلك. تُضبط المهام ويعالجها المحرّك ثمّ ينتظر المزيد من المهامّ (بينما ينام ولا يستهلك أيّ شيئ تقريبا من وحدة المعالجة المركزيّة). قد يحصل وتأتي مهمّة ما بينما المحرّك مشغول، فتضاف تلك المهمّة إلى رتل آنذاك، إذ تشكّل المهام رتلًا queue، يُطلق عليه "رتل المهامّ الكبرى" macrotasks (مصطلح v8) أو رتل المهام ببساطة (بحسب مواصفة WHATWG): على سبيل المثال، بينما المحرّك مشغول بتنفيذ السكربت script، قد يحرّك المستخدم فأرته متسبّبا في حدوث الحدث mousemove، كما قد يحين وقت setTimeout إلى غير ذلك، تُشكّل هذه المهامّ رتلا، كما هو موضّح في الصورة أعلاه. تُعالج المهامّ في الرتل حسب ترتيب "من يأتي أوّلا - يُخدَم أوّلا". وعندما يفرغ محرّك المتصفّح من script، يعالج حدث mousemove، ثمّ يعالج setTimeout، وهكذا. إلى حدّ الآن، اﻷمر في غاية البساطة، صحيح؟ اثنان من التفاصيل الإضافيّة: لا يحدث التصيير أو الإخراج rendering أبدًا بينما ينفّذ المحرّك مهمّة ما. لا يهمّ إن كانت المهمّة تستغرق وقتا طويلا. لا تظهر التغييرات في DOM إلّا بعد تمام المهمّة. إذا كانت المهمّة تستغرق الكثير من الوقت، فلا يمكن للمتصفّح القيام بمهامّ أخرى، كمعالجة أفعال المستخدم. وبذلك بعد وقت معيّن، سيرفع تنبيها من نحو "لا تستجيب الصفحة"، مقترحا إنهاء المهمّة مع كامل الصفحة. يحصل هذا عندما يكون هناك الكثير من الحسابات المعقّدة أو خطأ برمجيّ يؤدّي إلى حلقة لا متناهية. كان هذا هو النظريّ. لنرى الآن كيف يمكننا تطبيق هذه المعرفة. حالة الاستعمال 1: تقسيم المهام الشرهة لوحدة المعالجة المركزية لنفترض أنّ لدينا مهمّة شرهة لوحدة المعالجة المركزيّة. على سبيل المثال، إبراز الصيغة (التي استُخدمت في تلوين أمثلة الشيفرات في هذه الصفحة) ثقيلة جدّا على وحدة المعالجة المركزيّة. لإبراز الشيفرة، يكون هناك تحليل، وتُنشئ العديد من العناصر الملوّنة، وتضاف إلى الصفحة لمقدار هائل من النصّ، سيأخذ ذلك الكثير من الوقت. مادام المحرّك مشغولا بإبراز الصيغة، لا يمكنه فعل أشياء أخرى متعلّقة بـ DOM، أو معالجة أحداث المستخدمين أو غير ذلك. بل قد يؤدّي ذلك بالمتصفّح إلى "التلكّئ"، أو حتى "التوقّف" قليلا، وهو ما لا يُعدّ مقبولا. يمكننا تجنّب هذه المشاكل بواسطة تقسيم المهامّ الكبيرة إلى أجزاء. كإبراز المئة سطر اﻷولى، ثمّ برمجة setTimeout (بتأخير منعدم) للمئة سطر التالية، وهكذا. لعرض هذه الطريقة، وطلبا للبساطة، بدل إبراز النص، لنأخذ دالّة تعمل على التعداد من 1 إلى 1000000000. لو نفّذت الشيفرة في الأسفل، سوف "يتوقّف" المحرّك لبعض الوقت. أما في جافاسكربت المشتغلة في جانب الخادم سيكون ذلك ظاهرا بوضوح، وإذا كنت تشغّلها في المتصفّح، جرّب النقر على الأزرار اﻷخرى في الصفحة وسترى أنّه لن تُعالج أي من اﻷحداث اﻷخرى إلى حين انتهاء العد. let i = 0; let start = Date.now(); function count() { // do a heavy job for (let j = 0; j < 1e9; j++) { i++; } alert("Done in " + (Date.now() - start) + 'ms'); } count(); See the Pen JS-p2-event-loop-ex1 by Hsoub (@Hsoub) on CodePen. بل إنّ المتصفّح قد يظهر التحذير "استغرق هذا السكربت طويلا". لنقسّم هذه المهمّة باستخدام استدعاءات متداخلة لـ setTimeout: let i = 0; let start = Date.now(); function count() { // القيام بجزء من المهمّة الثقيلة (*) do { i++; } while (i % 1e6 != 0); if (i == 1e9) { alert("Done in " + (Date.now() - start) + 'ms'); } else { setTimeout(count); // برمجة الاستدعاء الجديد (**) } } count(); See the Pen JS-p2-event-loop-ex2 by Hsoub (@Hsoub) on CodePen. تعمل واجهة المستخدم الآن كليّا خلال عمليّة "العدّ". يؤدّي إجراءٌ واحد للدالة count جزءًا من المهمّة (*)، ثمّ يعيد برمجة نفسه (**) عند الحاجة: يعدُّ الإجراء اﻷوّل: i=1...1000000. يعدُّ الإجراء الثاني: i=1000001..2000000. وهكذا. لو ظهرت الآن مهمّة جانبيّة جديدة (حدث onclick مثلا) بينما المحرّك مشغول بتنفيذ الجزء الأوّل، فإنّها تُصَفّ في الرتل ثمّ تُنفّذ عند الانتهاء من الجزء اﻷوّل، قبل الجزء التالي. يعطي الرجوع الدوريّ لحلقة الأحداث ما يكفي من "النفَس" لمحرّك جافاسكربت للقيام بشيء آخر، للاستجابة لأفعال المستخدم اﻷخرى. الشيء الملاحظ هو أنّ كلا النسختين - مع أو دون تقسيم المهمّة بواسطة setTimeout - متقاربتان في السرعة. ليس هناك فرق كبير في الوقت الكلّيّ للعدّ. لتقريبهما أكثر، لنقم بشيء من التحسين. سننقل برمجة setTimeout إلى بداية العدّ count()‎: let i = 0; let start = Date.now(); function count() { // نقل البرمجة إلى البداية if (i < 1e9 - 1e6) { setTimeout(count); // برمجة الاستدعاء التالي } do { i++; } while (i % 1e6 != 0); if (i == 1e9) { alert("Done in " + (Date.now() - start) + 'ms'); } } count(); See the Pen JS-p2-event-loop-ex3 by Hsoub (@Hsoub) on CodePen. حاليّا، عندما نبدأ العدّ count()‎ ونرى أنّنا سنحتاج المزيد من العدّ، نبرمج ذلك مباشرة، قبل أداء المهمّة. إذا فعلت ذلك، من السهل أن تلاحظ أنّه يأخذ وقتا أقلّ بكثير. تسأل لماذا؟ هذا بسيط: كما تذكر، يوجد في المتصفّح تأخير صغير بمقدار 4ms عند تعدّد الاستدعاءات المتداخلة للدالة setTimeout. حتى لو وضعنا 0، فسيكون 4ms (أو أكثر بقليل). فكلّما برمجناه أبكر، جرى بشكل أسرع. في النهاية، لقد قسّمنا مهمّة شرهة لوحدة المعالجة المركزيّة لأجزاء ولا تحبس الآن واجهة المستخدم كما أنّ الوقت الكليّ لتنفيذها ليس أطول بكثير. حالة الاستخدام 2: الإشارة إلى المعالجة من الفوائد اﻷخرى لتقسيم المهامّ الثقيلة لسكربتات المتصفّح هو أنّه يمكّننا من إظهار إشارة إلى المعالجة. كما ذكرنا آنفا، لا تُرسم التغييرات في DOM حتى تتمّ المهمّة الجارية حاليّا، بغضّ النظر عن طول استغراقها. من جهة، هذا جيّد، لأنّ دالّتنا قد تنشئ عدّة عناصر، وتضيفها الواحد تلو الآخر إلى الصفحة وتغيّر تنسيقاتها ولن يرى المستخدمّ أيّة حالة "بينيّة" غير تامة. هذا أمر مهمّ، صحيح؟ في هذا المثال، لن تظهر التغيّرات في i حتى تنتهي الدالّة، وبذلك لن نرى سوى القيمة النهائيّة: <div id="progress"></div> <script> function count() { for (let i = 0; i < 1e6; i++) { i++; progress.innerHTML = i; } } count(); </script> See the Pen JS-p2-event-loop-ex4 by Hsoub (@Hsoub) on CodePen. لكن قد نودّ أيضا إظهار شيء ما خلال المهمّة، كشريط تقدّم مثلا. إذا قسّمنا المهمّة الثقيلة إلى أجزاء باستخدام setTimeout، فسوف تُرسم التغييرات في ما بينها. تبدو هذه أحسن: <script> let i = 0; function count() { // القيام بجزء من المهمّة الثقيلة (*) do { i++; progress.innerHTML = i; } while (i % 1e3 != 0); if (i < 1e7) { setTimeout(count); } } count(); </script> See the Pen JS-p2-event-loop-ex5 by Hsoub (@Hsoub) on CodePen. يُظهر <div> الآن قيما تصاعديّة لـ i، كنوع من شريط التقدّم. حالة الاستخدام 3: فعل شيء ما بعد الحدث في معالج حدثٍ ما، قد نقرّر تأجيل بعض اﻷفعال إلى حين انتشار الحدث نحو الأعلى ومعالجته في جميع المستويات. يمكننا فعل ذلك من خلال إحاطة الشيفرة بالدالة setTimeout بتأخير منعدم. قد رأينا في مقال إرسال اﻷحداث المخصّصة مثالًا لذلك: اُرسل الحدث المخصّص must-open داخل setTimeout، لكي يقع بعد المعالجة الكليّة لحدث "النقر". menu.onclick = function() { // ... // إنشاء حدث مخصّص ببيانات عنصر القائمة الذي نُقر let customEvent = new CustomEvent("menu-open", { bubbles: true }); // إرسال الحدث المخصّص بشكل لا متزامن setTimeout(() => menu.dispatchEvent(customEvent)); }; See the Pen JS-p2-event-loop-ex6 by Hsoub (@Hsoub) on CodePen. المهام الكبرى والمهام الصغرى إلى جانب المهامّ الكبرى macrotasks، التي بُيّنت في هذا المقال، هناك المهامّ الصغرى microtasks، المذكورة في مقال المهامّ الصغرى. لا تأتي المهامّ الصغرى إلّا من شيفرتنا. تُنشأ عادة بواسطة الوعود promises، حيث يصير تنفيذ معالج ‎then/catch/finally مهمّة صغرى. تُستخدم المهامّ الصغرى تحت غطاء await كذلك، إذ يُعدّ ذلك صورة أخرى لمعالجة الوعود. هناك أيضا الدالّة الخاصّة queueMicrotask(func)‎ التي تصفّ الدالة func الممررة إليها لكي تُنفَّذ في رتل المهامّ الصغرى. بعد كلّ مهمّة كبرى مباشرة، ينفّذ المحرّك جميع المهامّ الموجودة في رتل المهام الصغرى، قبل إجراء أيّ مهامّ كبرى أخرى أو التصيير أو أيّ شيء آخر. على سبيل المثال، ألق نظرةً على: setTimeout(() => alert("timeout")); Promise.resolve() .then(() => alert("promise")); alert("code"); See the Pen JS-p2-event-loop-ex7 by Hsoub (@Hsoub) on CodePen. كيف سيكون الترتيب هنا؟ يظهر code أوّلا، ﻷنّه استدعاء لا متزامن عاديّ. يظهر promise ثانيا، لأنّ ‎.then تمرّ عبر رتل المهامّ الصغرى، وتجري بعد الشيفرة الحاليّة. يظهر timeout أخيرا، لأنّه مهمّة كبرى. هذا ما تبدو عليه الصورة الثريّة أكثر لحلقة الأحداث (الترتيب هو من اﻷعلى إلى اﻷسفل، بمعنى: السكربت أوّلا، ثمّ المهامّ الصغرى، ثمّ التصيير وهكذا): تتمّ جميع المهامّ الصغرى قبل أيّ معالج حدث آخر أو تصيير أو أيّ مهمّة كبرى أخرى. هذا مهمّ، ﻷنّه يضمن أنّ بيئة التطبيق هي نفسها أساسا (لا تغيّر في إحداثيات الفأرة، لا بيانات جديدة من الشبكة، إلى غير ذلك) بين المهام الكبرى. لو أردنا تنفيذ دالّة بشكل لا متزامن (بعد الشيفرة الحاليّة)، لكن قبل تصيير التغييرات أو معالجة اﻷحداث الجديدة، يمكننا برمجة ذلك بواسطة queueMicrotask. هذا مثال مع "شريط تقدّم العدّ"، كما في الذي عرضناه سابقا، لكن باستخدام queueMicrotask بدل setTimeout. يمكنك رؤية أنّ التصيير يحصل في آخر المطاف. تماما كالشيفرة المتزامنة: <div id="progress"></div> <script> let i = 0; function count() { // القيام بجزء من مهمّة ثقيلة (*) do { i++; progress.innerHTML = i; } while (i % 1e3 != 0); if (i < 1e6) { queueMicrotask(count); } } count(); </script> See the Pen JS-p2-event-loop-ex8 by Hsoub (@Hsoub) on CodePen. الملخص خوارزميّة أكثر تفصيلا لحلقة اﻷحداث (لكن تبقى مبسّطة مقارنة بالمواصفة): خذ المهّمة اﻷقدم من رتل المهام الكبرى ونفذّها ("سكربت" مثلا). نفّذ جميع المهامّ الصغرى: مادام أنّ رتل المهامّ الصغرى ليس فارغا: خذ المهمّة اﻷقدم من الرتل ونفّذها. صيّر التغييرات إن وُجدت. إذا كان رتل المهامّ الكبرى فارغا، انتظر قدوم مهمّة كبرى. اذهب إلى الخطوة 1. لبرمجة مهمّة كبرى جديدة: استخدم setTimeout(f)‎ منعدمة التأخير. يمكن أن يُستخدم ذلك لتقسيم مهمّة كبيرة ومثقلة بالحسابات إلى أجزاء، حتى يتسنّى للمتصفّح الاستجابة لأحداث المستخدم وإظهار التقدّم بينها. تُستخدم كذلك، في معالجات اﻷحداث لبرمجة فعل ما بعد معالجة الحدث كليّة (انتهاء الانتشار نحو اﻷعلى). لبرمجة مهمّة صغرى جديدة: استخدم queueMicrotask(f)‎. تمرّ معالجات الوعود أيضا عبر رتل المهامّ الصغرى. ليست هناك معالجة لأحداث واجهة المستخدم أو الشبكة بين المهامّ الصغرى: تجري الواحدة تلو اﻷخرى مباشرة. وبذلك، قد يودّ أحدهم استخدام queueMicrotask لتنفيذ دالّة بشكل لا متزامن، لكن ضمن نفس حالة البيئة. ملاحظة: عاملات الويب web workers للحسابات الطويلة والثقيلة التي لا ينبغي أن تحبس حلقة اﻷحداث، يمكننا استخدام عاملات الويب فهذه طريقة لإجراء الشيفرة في عملية أو خيط thread آخر متوازي. يمكن أن تتبادل عاملات الويب الرسائل مع العمليّة اﻷساسيّة، لكن لديها متغيّراتها الخاصّة، ولديها حلقة أحداث خاصّة. لا يمكن لعاملات الويب الوصول إلى DOM، فهي مفيدة، بشكل أساسيّ، في الحسابات، لاستخدام عدّة أنوية لوحدة المعالجة المركزيّة في نفس الوقت. ترجمة -وبتصرف- للمقال Event loop: microtasks and macrotasks من سلسلة Browser: Document, Events, Interfaces لصاحبها Ilya Kantor. اقرأ أيضًا مراقب التحول MutationObserver عبر جافاسكربت لمراقبة شجرة DOM.
  2. سنتناول في هذا المقال التحديد داخل المستند، وكذلك التحديد داخل حقول الاستمارات، مثل <input>. تستطيع جافاسكربت الوصول إلى تحديد موجود، أو تحديد عقد من DOM وإزالة التحديد عنها سواء كليّا أو جزئيّا، بالإضافة إلى حذف المحتوى المحدّد من المستند، أو وضعه داخل وسم، إلى غير ذلك. ستجد بعض الوصفات لمهامّ شائعة في نهاية المقال، في قسم "الملخّص". قد تلبّي تلك الوصفات احتياجاتك الحاليّة، لكنّك ستستفيد أكثر بكثير إذا قرأت النصّ كلّه إذ ستتعرف على الكائنين الأساسيين Range وSelection، ولن تحتاج بفهمها أي مقال آخر لاستعمالهما فيما تريد. المدى يُعدّ المدى هو المفهوم اﻷساسيّ في موضوع التحديد، وهو عبارة عن زوج من "النقط الحدّية": بداية المدى ونهاية المدى. يُنشأ كائن Range دون وسائط: let range = new Range(); يمكننا بعدها ضبط حدود التحديد باستخدام range.setStart(node, offset)‎ وrange.setEnd(node, offset)‎. يمكن للوسيط الأوّل node أن يكون إمّا عقدة نصيّة أو عقدة عنصريّة، ويكون معنى الوسيط الثاني معتمدا على ذلك: إذا كان node عقدة نصيّة، فلابدّ أن يكون offset هو الموضع داخل النصّ. إذا كان node عقدة عنصريّة، فلابدّ أن يكون offset هو رقم الابن. على سبيل المثال، لننشئ مدى في هذا المقطع <p id="p">Example: <i>italic</i> and <b>bold</b></p> هذه هي بنية DOM الخاصّة به: لننشئ مدى من أجل ‎"Example: <i>italic</i>"‎. كما يمكن أن نلاحظ، تتشكّل هذه الجملة من الابنين الأوّل والثاني لـ <p>: لنقطة البداية العقدة الأب <p>، والانحراف offset هو 0. لنقطة النهاية العقدة الأب <p> أيضا، لكن لها الانحراف 2 (تضبط المدى إلى غاية offset، لكن دونه). لو أجرينا الشيفرة أدناه، يمكن أن نرى بأنّ النصّ يُحدّد. <p id="p">Example: <i>italic</i> and <b>bold</b></p> <script> let range = new Range(); range.setStart(p, 0); range.setEnd(p, 2); // للمدى محتوياته على شكل نص، دون الوسوم‏ toString تعيد console.log(range); // Example: italic // لنطبّق هذا المدى على تحديد المستند (سيُوضَّح هذا لاحقا)‏ document.getSelection().addRange(range); </script> هذه منصّة اختبار أكثر مرونة حيث يمكنك تجربة المزيد من التحديدات المختلفة: <p id="p">Example: <i>italic</i> and <b>bold</b></p> From <input id="start" type="number" value=1> – To <input id="end" type="number" value=4> <button id="button">Click to select</button> <script> button.onclick = () => { let range = new Range(); range.setStart(p, start.value); range.setEnd(p, end.value); // طبّق التحديد، سيُوضَّح لاحقا‏ document.getSelection().removeAllRanges(); document.getSelection().addRange(range); }; </script> See the Pen JS-p2-selection-range -ex1 by Hsoub (@Hsoub) on CodePen. على سبيل المثال، يعطي التحديد من 1 إلى 4 المدى التالي: لا يجب علينا استخدام العقدة نفسها في setStart وsetEnd فقد يمتدّ المدى عبر العديد من العقد غير المترابطة. ما يهمّ فقط هو أن تكون النهاية موجودة بعد البداية. تحديد أجزاء من العقد النصية ذلك ممكن أيضا، نحتاج فقط إلى ضبط البداية والنهاية على شكل انحراف نسبيّ في العقد النصّيّة. نحتاج إلى أن ننشئ مدى بحيث: يبدأ من الموضع 2 في الابن اﻷوّل لـ <p> (آخذًا جميع الحروف ما عدا الحرفين اﻷوّلين لـ "Example: "). ينتهي عند الموضع 3 في الابن اﻷوّل لـ <b> (آخذا الحروف الثلاثة اﻷولى لكلمة "bold"، لا أكثر): <p id="p">Example: <i>italic</i> and <b>bold</b></p> <script> let range = new Range(); range.setStart(p.firstChild, 2); range.setEnd(p.querySelector('b').firstChild, 3); console.log(range); // ample: italic and bol // استخدم هذا المدى للتحديد (سيُوضَّح هذا لاحقا)‏ window.getSelection().addRange(range); </script> See the Pen JS-p2-selection-range -ex2 by Hsoub (@Hsoub) on CodePen. لكائن المدى الخاصّيّات التالية: startContainer وstartOffset -- العقدة والانحراف عن البداية. في المثال أعلاه: العقدة النصّيّة اﻷولى داخل <p> و2. endContainer وendOffset -- العقدة والانحراف عن النهاية. في المثال أعلاه: العقدة النصّيّة اﻷولى داخل <b> و3. collapsed -- قيمة بوليانية، تكون true عندما يبدأ المدى وينتهي في النقطة نفسها (وبذلك لا يوجد محتوى داخل المدى)، في المثال أعلاه: false commonAncestorContainer -- أقرب سلف مشترك بين جميع العقد داخل المدى، في المثال أعلاه: <p> توابع المدى هناك العديد من التوابع الملائمة للتحكّم في اﻷمداء سنعرضها كلها، ونبدأ بتلك التي تضبط بداية المدى: setStart(node, offset)‎ - يضع البداية عند موضع الانحراف offset في العقدة node setStartBefore(node)‎ - يضع البداية قبل node مباشرة setStartAfter(node)‎ - يضع البداية بعد node مباشرة ضبط نهاية المدى (توابع مماثلة): setEnd(node, offset)‎ - يضع النهاية عند موضع الانحراف offset في العقدة node setEndBefore(node)‎ - يضع النهاية قبل node مباشرة setEndAfter(node)‎ - يضع النهاية بعد node مباشرة كما سبق بيانه، يمكن أن تكون node عقدة نصّيّة أو عنصريّة، بالنسبة للعقد العنصريّة تتخطّى offset ذلك العدد من الحروف، بينما للعقد العنصريّة تتخطّى ذلك العدد من العقد اﻷبناء. توابع أخرى: selectNode(node)‎ - يضبط المدى بحيث تُحدَّد كامل العقدة node selectNodeContents(node)‎ - يضبط المدى بحيث تُحدَّد جميع محتويات العقدة node collapse(toStart)‎ إذا كانت toStart=true - يضع end=start، وإلّا يضع start=end، فينطوي بذلك المدى. cloneRange()‎ - ينشئ مدى جديدا بنفس البداية/النهاية للتحكّم في المحتوى الموجود ضمن المدى: deleteContents()‎ - يزيل محتوى المدى من المستند extractContents()‎ - يزيل محتوى المدى من المستند ويعيده على شكل قطعة مستند DocumentFragment cloneContents()‎ - يستنسخ محتوى المستند ويعيده على شكل قطعة مستند ‎DocumentFragment insertNode(node)‎ - يدرج العقدة node في المستند عند بداية المدى surroundContents(node)‎ - يحيط العقدة بمحتوى المدى. لكي يعمل هذا، يجب أن يحتوي المدى على وسوم فتح وإغلاق لجميع العناصر التي في داخله: لا وجود ﻷمداء جزئيّة مثل ‎<i>abc. بهذه التوابع يمكننا فعل أيّ شيء بالعقد المُحدّدة. هذه منصّة الاختبار لرؤيتها كيف تعمل: Click buttons to run methods on the selection, "resetExample" to reset it. <p id="p">Example: <i>italic</i> and <b>bold</b></p> <p id="result"></p> <script> let range = new Range(); // جميع التوابع المعروضة ممثّلة هنا let methods = { deleteContents() { range.deleteContents() }, extractContents() { let content = range.extractContents(); result.innerHTML = ""; result.append("extracted: ", content); }, cloneContents() { let content = range.cloneContents(); result.innerHTML = ""; result.append("cloned: ", content); }, insertNode() { let newNode = document.createElement('u'); newNode.innerHTML = "NEW NODE"; range.insertNode(newNode); }, surroundContents() { let newNode = document.createElement('u'); try { range.surroundContents(newNode); } catch(e) { console.log(e) } }, resetExample() { p.innerHTML = `Example: <i>italic</i> and <b>bold</b>`; result.innerHTML = ""; range.setStart(p.firstChild, 2); range.setEnd(p.querySelector('b').firstChild, 3); window.getSelection().removeAllRanges(); window.getSelection().addRange(range); } }; for(let method in methods) { document.write(`<div><button onclick="methods.${method}()">${method}</button></div>`); } methods.resetExample(); </script> See the Pen JS-p2-selection-range -ex3 by Hsoub (@Hsoub) on CodePen. توجد أيضا توابع للمقارنة بين اﻷمداء. لكن يندر استخدامها. إن احتجت لها، يُرجى الرجوع إلى المواصفة أو دليل MDN. التحديد Range هو كائن عامّ لإدارة أمداء التحديد. يمكننا إنشاء مثل هذه الكائنات، وتمريرها إلى هنا وهناك، لكنّها لا تُحدِّد ظاهريّا أيّ شيء بمفردها. يُمثَّل تحديد المستند بواسطة الكائن Selection، الذي يمكن الحصول عليه من window.getSelection()‎ أو document.getSelection()‎. قد يتضمّن التحديد صفرا فأكثر من اﻷمداء. على الأقلّ، كما تنصّ على ذلك مواصفة الواجهة البرمجيّة للتحديد. لكن عمليّا، فايرفوكس فقط هو الذي يمكّن من تحديد أمداء متعدّدة في المستند باستخدام المفاتيح Ctrl+click‏ (Cmd+click بالنسبة لـ Mac). هذه لقطة شاشة فيها تحديد بثلاثة أمداء، تمّ عملها في فايرفوكس: تدعم المتصفّحات اﻷخرى تحديد مدى واحدا على اﻷكثر. كما سنرى، تلمّح بعض توابع Selection أنّه من الممكن أن يكون هناك عدّة أمداء، لكن ثانية، في جميع المتصفّحات ما عدا فايرفوكس، هناك واحد فقط على اﻷكثر. خاصيات التحديد بشكل مشابه للمدى، يملك التحديد بداية، تُسمّى "المرساة (anchor)"، ونهاية تُسمّى "التركيز (focus)". خاصّيّات التحديد الرئيسيّة هي: anchorNode -- العقدة التي يبدأ فيها التحديد anchorOffset -- الانحراف الذي يبدأ منه التحديد في anchorNode focusNode -- العقدة التي ينتهي فيها التحديد focusOffset -- الانحراف الذي ينتهي عنده التحديد في focusNode isCollapsed --‏ true إذا لم يكن التحديد يحدّد أيّ شيء (مدى فارغ)، أو لم يكن موجودا أصلا rangeCount -- عدد اﻷمداء في التحديد، 1 على اﻷكثر في جميع المتصفّحات باستثناء فايرفوكس ملاحظة: قد تكون نهاية التحديد في المستند قبل البداية هناك عدة طرق لتحديد المحتوى، حسب وكيل المستخدم (user agent): الفأرة والمفاتيح المختصرة والضغط على الهاتف، إلى غير ذلك. يمكّن بعضها، مثل الفأرة، من عمل نفس التحديد في الاتجاهين: "من اليمين إلى اليسار" و"من اليسار إلى اليمين". إذا كانت البداية (المرساة) تأتي في المستند قبل النهاية (التركيز)، فيقال أنّ اتجاه هذا التحديد "إلى اﻷمام". إذا بدأ المستخدم مثلا التحديد بالفأرة وذهب من "Example" إلى "italic": على خلاف ذلك، إذا ذهب من نهاية "italic" إلى "Example"، فإنّ التحديد متّجه "إلى الخلف"، ويكون التركيز فيه قبل المرساة: هذا مختلف عن كائنات Range التي تتجه دائما إلى اﻷمام: لا يمكن أن تكون بداية المدى قبل نهايته. أحداث التحديد هناك أحداث تمكّننا من متابعة التحديد: elem.onselectstart -- عندما يبدأ التحديد في elem، كأن يبدأ المستخدم مثلا بتحريك الفأرة والزر مضغوط. يؤدي منع الفعل الافتراضيّ إلى عدم ابتداء التحديد. document.onselectionchange -- كلّما تغيّر التحديد. يُرجى التنبّه: يمكن أن يُسند هذا المعالج إلى document فقط. مثال لتتبع التحديد إليك مثالا صغيرا يظهر حدود التحديد ديناميكيّا حال تغيّرها: <p id="p">Select me: <i>italic</i> and <b>bold</b></p> From <input id="from" disabled> – To <input id="to" disabled> <script> document.onselectionchange = function() { let {anchorNode, anchorOffset, focusNode, focusOffset} = document.getSelection(); from.value = `${anchorNode && anchorNode.data}:${anchorOffset}`; to.value = `${focusNode && focusNode.data}:${focusOffset}`; }; </script> See the Pen JS-p2-selection-range -ex4 by Hsoub (@Hsoub) on CodePen. مثال للحصول على التحديد للحصول على كامل التحديد: على شكل نص: استدعي فقط document.getSelection().toString()‎. على شكل عقد DOM: احصل على الأمداء المنشأة واستدعي توابع cloneContents()‎ الخاصّة بها (المدى الأوّل فقط إذا لم نكن ندعم التحديد المتعدّد لفايرفوكس). هذا مثال للحصول على التحديد سواء على شكل نصّ أو عقد DOM: <p id="p">Select me: <i>italic</i> and <b>bold</b></p> Cloned: <span id="cloned"></span> <br> As text: <span id="astext"></span> <script> document.onselectionchange = function() { let selection = document.getSelection(); cloned.innerHTML = astext.innerHTML = ""; // من اﻷمداء (ندعم التحديد المتعدّد هنا)‏ DOM استنسخ عقد for (let i = 0; i < selection.rangeCount; i++) { cloned.append(selection.getRangeAt(i).cloneContents()); } // على شكل نصّ astext.innerHTML += selection; }; </script> See the Pen JS-p2-selection-range -ex5 by Hsoub (@Hsoub) on CodePen. توابع التحديد توابع التحديد لإضافة/إزالة اﻷمداء: getRangeAt(i)‎ -- يحصل على المدى رقم i، ابتداءًا من 0. في جميع المتصفّحات ما عدا فايرفوكس، لا يُستعمل سوى 0. addRange(range)‎ -- يضيف المدى range إلى التحديد. تتجاهل جميع المتصفّحات ما عدا فايرفوكس هذا الاستدعاء إذا كان للتحديد مدى مرفقًا به removeRange(range)‎ -- يزيل المدى range من التحديد. removeAllRanges()‎ -- يزيل جميع الأمداء. empty()‎ -- اسم بديل لـ removeAllRanges. زيادة على ذلك، هناك توابع ملائمة للتحكّم في المدى الخاصّ بالتحديد مباشرة، دون Range: collapse(node, offset)‎ - استبدل المدى المُحدَّد بواحد جديد يبدأ وينتهي عند العقدة المعطاة، node، وفي الموضع offset. setPosition(node, offset)‎ -- اسم بديل لـ collapse. collapseToStart()‎ -- يطوي (يستبدل بمدى فارغ) إلى بداية التحديد، collapseToEnd()‎ -- يطوي إلى نهاية التحديد، extend(node, offset)‎ -- ينقل تركيز التحديد إلى العقدة المعطاة node، والموضع offset، setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset)‎ -- يستبدل مدى التحديد بالبداية المعطاة anchorNode/anchorOffset والنهاية focusNode/focusOffset. يُحدّد جميع المحتوى الذي بينها. selectAllChildren(node)‎ -- يحدّد جميع أبناء العقدة node. deleteFromDocument()‎ -- يزيل المحتوى المُحدَّد من المستند. containsNode(node, allowPartialContainment = false)‎ -- يفحص ما إذا كان التحديد يحتوي على العقدة node (جزئيّا إذا كان الوسيط الثاني true). وبذلك، يمكننا في كثير من المهامّ استدعاء توابع Selection، ولا حاجة للوصول إلى كائن Range المنشأ. على سبيل المثال، تحديد كامل محتويات الفقرة <p>: <p id="p">Select me: <i>italic</i> and <b>bold</b></p> <script> // إلى آخر ابن <p> يحدّد من الابن رقم 0 لـ document.getSelection().setBaseAndExtent(p, 0, p, p.childNodes.length); </script> See the Pen JS-p2-selection-range -ex6 by Hsoub (@Hsoub) on CodePen. نفس الشيء باستخدام اﻷمداء: <p id="p">Select me: <i>italic</i> and <b>bold</b></p> <script> let range = new Range(); range.selectNodeContents(p); // or selectNode(p) to select the <p> tag too document.getSelection().removeAllRanges(); // clear existing selection if any document.getSelection().addRange(range); </script> See the Pen JS-p2-selection-range -ex7 by Hsoub (@Hsoub) on CodePen. ملاحظة: للقيام بالتحديد، أزل التحديد الموجود أوّلا إذا كان التحديد موجودًا، أفرغه أوّلا باستخدام removeAllRanges()‎. ثمّ أضف اﻷمداء. وإلّا، ستتجاهل جميع المتصفّحات ما عدا فايرفوكس الأمداء الجديدة. يُستثنى من ذلك بعض توابع التحديد، التي تستبدل التحديد الموجود، مثل setBaseAndExtent. التحديد في عناصر التحكم بالاستمارات توفّر عناصر الاستمارات، مثل input وtextarea‏ واجهات برمجيّة خاصّة بالتحديد، دون الكائنات Selection أو Range. بما أنّ قيمة المُدخل هي نصّ صرف، وليس HTML، فلا حاجة هناك لهذه الكائنات، كلّ شيء أبسط بكثير. الخاصّيّات: input.selectionStart -- موضع بداية التحديد (قابل للكتابة) input.selectionEnd -- موضع نهاية التحديد (قابل للكتابة) input.selectionDirection -- اتجاه التحديد، واحد من: "forward" (اﻷمام) أو "backward" (الخلف) أو "none" (إذا كان التحديد بواسطة النقر المزدوج بالفأرة مثلا) اﻷحداث: input.onselect -- يقع عندما يُحدّد شيء ما التوابع: input.select()‎ -- يحدّد كلّ شيء في عنصر التحكّم بالنصّ (قد يكون textarea بدل input) input.setSelectionRange(start, end, [direction])‎ -- يغيّر التحديد ليمتدّ من الموضع start إلى end، في الاتجاه المُعطى (اختياريّ) input.setRangeText(replacement, [start], [end], [selectionMode])‎ -- يستبدل المدى من النصّ بنصّ جديد تضبط الوسائطُ الاختياريّة start وend، إذا أُعطيت، بداية المدى ونهايته، وإلّا فيُعتمد تحديد المستخدم. يوضّح الوسيط اﻷخير، selectionMode، كيفيّة ضبط التحديد بعد استبدال النصّ. القيم الممكنة هي: "select" -- يُحدَّد النصّ المُدرج الجديد. "start" -- ينطوي مدى التحديد قبل النصّ المُدرج مباشرة (يكون المؤشّر قبله مباشرة). "end" -- ينطوي مدى التحديد بعد النصّ المُدرج مباشرة (يكون المؤشّر بعده مباشرة). "preserve" -- يحاول المحافظة على التحديد. هذه هي القيمة الافتراضيّة. لنرى الآن هذه التوابع تعمل. مثال: تتبع التحديد على سبيل المثال، تستخدم هذه الشيفرة حدث onselect لتتبّع التحديد: <textarea id="area" style="width:80%;height:60px"> Selecting in this text updates values below. </textarea> <br> From <input id="from" disabled> – To <input id="to" disabled> <script> area.onselect = function() { from.value = area.selectionStart; to.value = area.selectionEnd; }; </script> See the Pen JS-p2-selection-range -ex8 by Hsoub (@Hsoub) on CodePen. يُرجى التنبّه: يقع onselect عندما يُحدَّد شيء ما، لكن ليس عندما يُزال التحديد. يجب ألّا يقع document.onselectionchange من أجل التحديدات التي بداخل عناصر التحكّم في الاستمارة، وفقا للمواصفة، إذ ليست له علاقة بـتحديد وأمداء المستند document. تقوم بعض المتصفّحات بتوليده، لكن ينبغي ألّا نعتمد عليه. مثال: تحريك المؤشر يمكننا تغيير selectionStart وselectionEnd، التي تضبط التحديد. من الحالات الشاذّة المهمّة هي عندما تكون selectionStart وselectionEnd متساويتان، ويكون ذلك هو موضع المؤشّر بالضبط. أو، بعبارة أخرى، عندما لا يُحدَّد أيّ شيء، فإن التحديد ينطوي عند موضع المؤشّر. فبإعطاء selectionStart وselectionEnd نفس القيمة، فإنّنا نعمل على تحريك المؤشّر. على سبيل المثال: <textarea id="area" style="width:80%;height:60px"> Focus on me, the cursor will be at position 10. </textarea> <script> area.onfocus = () => { // بتأخير منعدم للتنفيذ بعد انتهاء فعل “التركيز” من المتصفّح setTimeout setTimeout(() => { // يمكننا ضبط أيّ تحديد // إذا كانت البداية=النهاية، يكون المؤشّر في ذلك الموضع بالذات area.selectionStart = area.selectionEnd = 10; }); }; </script> See the Pen JS-p2-selection-range -ex9 by Hsoub (@Hsoub) on CodePen. مثال: تعديل التحديد لتعديل محتوى التحديد، يمكننا استخدام التابع input.setRangeText()‎. بالطبع، يمكننا قراءة selectionStart/End و، مع معرفتنا بالتحديد، تغيير السلسلة النصّيّة الجزئيّة الموافقة لـ value، لكنّ setRangeText أقوى و أكثر ملائمة في الكثير من اﻷحيان. هو تابع معقّد بعض الشيء. يقوم في صورته البسيطة ذات وسيط وحيد باستبدال المدى المُحدَّد من طرف المستخدم ويزيل التحديد. على سبيل المثال، سُيحاط تحديد المستخدم هنا بـ *...*: <input id="input" style="width:200px" value="Select here and click the button"> <button id="button">Wrap selection in stars *...*</button> <script> button.onclick = () => { if (input.selectionStart == input.selectionEnd) { return; // nothing is selected } let selected = input.value.slice(input.selectionStart, input.selectionEnd); input.setRangeText(`*${selected}*`); }; </script> See the Pen JS-p2-selection-range -ex10 by Hsoub (@Hsoub) on CodePen. بالمزيد من الوسائط، يمكننا ضبط بداية المدى start ونهايته end. في هذا المثال نجد أنّ "THIS" في نصّ المُدخل، نستبدله ونبقي البديل مُحدّدا: <input id="input" style="width:200px" value="Replace THIS in text"> <button id="button">Replace THIS</button> <script> button.onclick = () => { let pos = input.value.indexOf("THIS"); if (pos >= 0) { input.setRangeText("*THIS*", pos, pos + 4, "select"); input.focus(); // focus to make selection visible } }; </script> See the Pen JS-p2-selection-range -ex11 by Hsoub (@Hsoub) on CodePen. مثال: الإدراج عند المؤشر إذا لم يكن هناك شيء مُحدّد، أو استخدمنا start وend متساويتان في setRangeText، فسيُدرج النصّ الجديد فقط، ولن يزال أيّ شيء، ويمكننا أيضا إدراج شيء ما "عند المؤشّر" باستخدام setRangeText. هذا زرّ يعمل على إدراج "HELLO" عند موضع المؤشّر ويضع المؤشّر بعده مباشرة. إذا لم يكن التحديد فارغا، فإنّه يُستبدل (يمكننا اكتشاف ذلك بالمقارنة selectionStart!=selectionEnd وعمل شيء آخر بدل ذلك): <input id="input" style="width:200px" value="Text Text Text Text Text"> <button id="button">Insert "HELLO" at cursor</button> <script> button.onclick = () => { input.setRangeText("HELLO", input.selectionStart, input.selectionEnd, "end"); input.focus(); }; </script> See the Pen JS-p2-selection-range -ex12 by Hsoub (@Hsoub) on CodePen. جعل الشيء غير قابل للتحديد لجعل شيء ما غير قابل للتحديد، هناك ثلاث طرق: الطريقة الأولى باستخدام الخاصّيّة user-select: none في CSS <style> #elem { user-select: none; } </style> <div>Selectable <div id="elem">Unselectable</div> Selectable</div> See the Pen JS-p2-selection-range -ex13 by Hsoub (@Hsoub) on CodePen. لا يسمح هذا للتحديد بالبدء عند elem لكن قد يبدأ المستخدم التحديد من مكان آخر ويدخل فيه elem. وبذلك يصير elem جزءًا من document.getSelection()‎، ويكون التحديد قد وقع فعلًا، إلّا أنّ مُحتواه يُتجاهل عادة عند النسخ-اللصق. الطريقة الثانية بمنع الفعل الافتراضيّ عند اﻷحداث onselectstart أو mousedown: <div>Selectable <div id="elem">Unselectable</div> Selectable</div> <script> elem.onselectstart = () => false; </script> See the Pen JS-p2-selection-range -ex14 by Hsoub (@Hsoub) on CodePen. يمنع هذا بدأ التحديد من elem، لكن الزائر قد يبدأ من عنصر آخر، ثمّ يمدّه إلى elem. هذا ملائم عندما يكون هناك معالج حدث آخر لنفس الفعل الذي يُحدث التحديد (mousedown مثلا). فيمكننا إذًا تعطيل التحديد لتجنّب التعارض، مع السماح لمحتويات elem أن تُنسخ. الطريقة الثالثة يمكننا أيضا مسح التحديد بعد حصوله باستخدام document.getSelection().empty()‎. يندر استخدام هذا، إذ يتسبّب في ومضات غير محبّذة عند ظهور-اختفاء التحديد. الملخص تناولنا واجهتين برمجيّتين للتحديدات: للصفحة: الكائنان Selection وRange. للمدخلات النصية input وtextarea: توابع وخاصّيّات إضافيّة. الواجهة البرمجيّة الثانية بسيطة للغاية، إذ تعمل مع النصّ. قد تكون أكثر الوصفات استخداما هي: الحصول على التحديد: let selection = document.getSelection(); let cloned = /* العنصر الذي تُستنسخ فيه العقد المُحدَّدة*/; // selection.getRangeAt(0) على Range ثمّ طبّق توابع // أو، كما هنا، على جميع اﻷمداء لدعم التحديد المتعدّد for (let i = 0; i < selection.rangeCount; i++) { cloned.append(selection.getRangeAt(i).cloneContents()); } ضبط التحديد let selection = document.getSelection(); // :مباشرة selection.setBaseAndExtent(...from...to...); // أو يمكننا إنشاء مدى و:‏ selection.removeAllRanges(); selection.addRange(range); وفي النهاية، بالنسبة للمؤشّر. يكون موضع المؤشّر في العناصر القابلة للتحرير، مثل <textarea>، دائما عند بداية التحديد أو نهايته. يمكننا استخدام ذلك للحصول على موضع المؤشّر أو لتحريك المؤشّر بضبط elem.selectionStart وelem.selectionEnd. المراجع مواصفة DOM: المدى الواجهة البرمجيّة للتحديد مواصفة HTML: الواجهات البرمجيّة لتحديدات عناصر التحكّم في النصّ ترجمة -وبتصرف- للمقال Selection and Range من سلسلة Browser: Document, Events, Interfaces لصاحبها Ilya Kantor
  3. الكائن MutationObserver هو كائن مُضمّن built-in object يعمل على مراقبة عنصر من DOM ويطلق ردّ نداء عندما يلاحظ تغيّرا ما. سنلقي في البداية نظرة على صيغة استعماله، ونستكشف بعدها حالة استخدام واقعيّة، لرؤية متى قد يكون مفيدا. الصيغة يعد MutationObserver سهل الاستخدام. أوّلا، ننشئ مراقبا مع دالّة ردّ نداء: let observer = new MutationObserver(callback); ثمّ نربطه بعقدة في DOM: observer.observe(node, config); config هو كائنٌ بخيارات بوليانية تمثّل "نوع التغيرات التي يُستجاب لها": childList -- التغيرات في الأبناء المباشرين للعقدة node، subtree -- في جميع العناصر السليلة للعقدة node، attributes -- سمات العقدة node، attributeFilter -- مصفوفة بأسماء السمات، لمراقبة المحدّدة منها فقط، characterData -- ما إذا تُراقَب node.data (المحتوى النصّي)، بعض الخيارات الأخرى: attributeOldValue -- إذا كانت true، تُمرّر كلا القيمتان القديمة والجديدة للسمة إلى دالة ردّ النداء (انظر أسفله) وإلّا فالجديدة فقط (تحتاج الخيار attributes)، characterDataOldValue -- إذا كانت true، تُمرّر كلا القيمتان القديمة والجديدة لـ node.data إلى دالّة ردّ النداء (انظر أسفله) وإلّا فالجديدة فقط (تحتاج الخيار characterData). وبذلك بعد أيّ تغيّر، تُنفّذ callback: تُمرَّر التغيّرات كوسيط أوّل على شكل قائمة من كائنات سجلّات التحوّل MutationRecord، والمراقب نفسه كوسيط ثاني. لكائنات MutationRecord الخاصيّات التالية: type -- نوع التحوّل، واحد من: "attributes": تغيّرت السمة "characterData": تغيّرت البيانات، تُستخدم مع العقد النصّيّة "childList": أضيفت/أزيلت عناصر أبناء target -- أين وقع التغيّر: يكون عنصرا بالنسبة لـ "attributes"، أوعقدة نصيّة بالنسبة لـ "characterData"، أو عنصرا بالنسبة لتحوّل "childList" addedNodes/removedNodes -- العقد التي أضيفت/أزيلت previousSibling/nextSibling -- الأخ السابق واللاحق للعقد التي أضيفت/أزيلت attributeName/attributeNamespace -- اسم/مساحة اسم namespace بالنسبة لـ XML للسمة التي تغيّرت oldValue -- القيمة السابقة، فقط للتغيّرات في السمات والعقد النصّيّة، إذا كان الخيار الموافق مضبوطا attributeOldValue / characterDataOldValue على سبيل المثال، هذا الـ <div> له السمة contentEditable. تمكّننا هذه السمة من التركيز عليه و تعديله. <div contentEditable id="elem">Click and <b>edit</b>, please</div> <script> let observer = new MutationObserver(mutationRecords => { console.log(mutationRecords); // console.log(التغييرات) }); // راقب كلّ شيء ما عدا السمات observer.observe(elem, { childList: true, // راقب اﻷبناء المباشرين subtree: true, // وكذلك العناصر السليلة في اﻷسفل characterDataOldValue: true // مرّر البيانات القديمة إلى ردّ النداء }); </script> إذا أجرينا هذا المثال في متصفّح، ثمّ ركّزنا على ذلك الـ <div> وغيّرنا النصّ الذي بداخل <b>edit</b>، ستُظهر console.log تحوّلا واحدًا: mutationRecords = [{ type: "characterData", oldValue: "edit", target: <text node>, // الخاصّيّات اﻷخرى فارغة }]; إذا أجرينا تغييرات أكثر تعقيدا، مثل إزالة <b>edit</b>، فإنّ حدث التحوّل قد يحتوي على عدّة سجلّات تحوّل: mutationRecords = [{ type: "childList", target: <div#elem>, removedNodes: [<b>], nextSibling: <text node>, previousSibling: <text node> // الخاصّيّات اﻷخرى فارغة }, { type: "characterData" target: <text node> // تعتمد تفاصيل التحوّل على كيفيّة تعامل المتصفّح مع مثل هذا الحذف... // في عقدة واحدة ", please" و "edit " قد يجمع بين العقدتين المتجاورتين // أو قد يبقيهما عقدتين نصّيّتين منفصلتين }]; وبذلك، تمكّن MutationObserver من الاستجابة لأيّة تغيّرات ضمن الشجرة الفرعيّة. الاستخدام في الإدماج فيما قد يفيدنا شيء كهذا؟ تصوّر حالة تحتاج فيها إلى إضافة سكربت طرف ثالث يحتوي على وظيفة مفيدة، لكنّه أيضا يقوم بشيء غير مرغوب كإظهار الإعلانات <div class="ads">Unwanted ads</div> مثلا، وبالطبع، لن توفّر سكربتات الطرف الثالث تلك آليّات لإزالتها. باستخدام MutationObserver، يمكننا اكتشاف ظهور العنصر غير المرغوب في DOM وإزالته. هناك حالات أخرى يضيف فيها سكربت طرف ثالث شيئا ما إلى المستند الخاصّ بنا، ونريد اكتشاف ذلك عند حصوله، للتمكّن من تكييف صفحتنا، كتغيير حجم شيء ما وغير ذلك وهنا يمكّن MutationObserver من إنجاز ذلك. الاستخدام في الهندسة هناك أيضا حالات يكون فيها MutationObserver جيّدا من المنظور الهندسيّ. لنفترض أنّنا بصدد إنشاء موقع عن البرمجة. بالطبع، قد تحتوي المقالات و الموادّ الأخرى على قصاصات من الشيفرات المصدريّة. تبدو هذه القصاصات من HTML هكذا: ... <pre class="language-javascript"><code> // هذه هي الشيفرة let hello = "world"; </code></pre> ... لمقروئيّة أفضل، وفي الوقت نفسه لتجميلها، سنستخدم في موقعنا (موقع javascript.info) مكتبة لتلوين أو إبراز صيغة جافاسكربت، مثل Prism.js. لإبراز الصيغة في القصاصات أعلاه بواسطة Prism، يُستدعى Prism.highlightElem(pre)‎، الذي يفحص محتويات عناصر pre تلك ويضيف لها وسوما خاصّة وتنسيقات بغرض الإبراز الملوّن للصيغة، مثلما ترى في اﻷمثلة التي في هذه الصفحة. متى ينبغي أن نجري عمليّة الإبراز تلك؟ حسنا، يمكننا القيام بها عند الحدث DOMContentLoaded، أو بوضع السكربت في أسفل الصفحة. حالما يكون DOM الذي لدينا جاهزا، يمكننا البحث عن العناصر pre[class*="language"]‎ واستدعاء Prism.highlightElem عليها: // أبرز جميع قصاصات الشيفرات على الصفحة document.querySelectorAll('pre[class*="language"]').forEach(Prism.highlightElem); إلى الآن كلّ شيء بسيط، صحيح؟ نعثر على قصاصات الشيفرات في HTML ونبرزها. لنواصل الآن. لنفترض أنّنا سنجلب موادّ من الخادم ديناميكيّا. سندرس توابع لهذا الغرض لاحقا في هذا الدليل. ما يهمّ حاليّا هو أن نجلب مقال HTML من خادم الويب وعرضه حسب الطلب: let article = /* جلب المحتوى الجديد من الخادم */ articleElem.innerHTML = article; قد يحتوي مقال HTML الجديد article على قصاصات شيفرات. نحتاج إلى استدعاء Prism.highlightElem عليها، وإلّا فلن يتمّ إبرازها. أين ومتى يُستدعى Prism.highlightElem للمقالات المحمّلة ديناميكيّا؟ يمكننا إلحاق ذلك الاستدعاء بالشيفرة التي تحمّل المقال، هكذا: let article = /* جلب المحتوى الجديد من الخادم */ articleElem.innerHTML = article; let snippets = articleElem.querySelectorAll('pre[class*="language-"]'); snippets.forEach(Prism.highlightElem); لكن، تصوّر لو كان لدينا عدّة مواضع في الشيفرة نحمّل فيها المحتوى الخاصّ بنا - مقالات، اختبارات، مشاركات منتدى، إلى غير ذلك. هل علينا وضع استدعاء الإبراز في كلّ مكان، لإبراز الشيفرة بعد التحميل؟ هذا غير ملائم للغاية. وماذا لو حُمّل المحتوى بواسطة وحدات طرف ثالث؟ على سبيل المثال، إذا كان لدينا منتدى مكتوب بواسطة شخص آخر، يحمّل المحتوى ديناميكيّا، ونريد إضافة إبراز الصيغة فيه. لا أحد يحبّ ترقيع سكربتات الطرف الثالث. لحسن الحظ، هناك خيار آخر. يمكننا استخدام MutationObserver للقيام تلقائيّا باكشاف متى تُدرج قصاصات الشيفرات في الصفحة وإبرازها. وبالتالي سنعالج وظيفة الإبراز في مكان واحد، لنرتاح بذلك من الحاجة للإدماج. عرض الإبراز الديناميكي إليك المثال التالي. إذا أجريت هذه الشيفرة، ستبدأ بمراقبة العناصر في اﻷسفل وتبرز أيّ قصاصات شيفرات تظهر هناك: let observer = new MutationObserver(mutations => { for(let mutation of mutations) { // تفحّص العقد الجديدة، هل هناك أيّ شيء لإبرازه؟ for(let node of mutation.addedNodes) { // نتتبّع العناصر فقط، تجاوز العقد اﻷخرى (العقد النصّيّة مثلا)‏ if (!(node instanceof HTMLElement)) continue; // تحقّق ما إذا كان العنصر المدرج قصاصة شيفرة if (node.matches('pre[class*="language-"]')) { Prism.highlightElement(node); } // أو ربّما هناك قصاصة شيفرة في مكان ما في الشجرة الفرعيّة for(let elem of node.querySelectorAll('pre[class*="language-"]')) { Prism.highlightElement(elem); } } } }); let demoElem = document.getElementById('highlight-demo'); observer.observe(demoElem, {childList: true, subtree: true}); في اﻷسفل عنصر HTML وجافاسكربت لملئه ديناميكيّا باستخدام innerHTML. بإجراء الشيفرة السابقة (التي في اﻷعلى) يُراقب العنصر، ثمّ عند الشيفرة التي في اﻷسفل. تكتشف MutationObserver القصاصة وتبرزها، هكذا: تملأ الشيفرة التالية innerHTML الخاصّ بالعنصر، ما يؤدّي بـ MutationObserver إلى الاستجابة وإبراز محتوياته: let demoElem = document.getElementById('highlight-demo'); // إدراج المحتوى الذي فيه قصاصات الشيفرات ديناميكيّا demoElem.innerHTML = `A code snippet is below: <pre class="language-javascript"><code> let hello = "world!"; </code></pre> <div>Another one:</div> <div> <pre class="language-css"><code>.class { margin: 5px; } </code></pre> </div> `; لدينا الآن MutationObserver يمكنه تتبّع جميع عمليّات الإبراز في العناصر المراقبة أو في كامل document. يمكننا إضافة/إزالة قصاصات الشيفرات في HTML دون التفكير في ذلك. توابع إضافية هناك تابع لإيقاف مراقبة العقدة: observer.disconnect()‎ -- يوقف المراقبة. عندما نوقف المراقبة، قد يكون من الممكن أنّ بعض التغيّرات لم تُعالج بعد من طرف المراقب. في تلك الحالات، نستخدم observer.takeRecords()‎ -- يحصل على قائمة سجّلات التحوّلات غير المعالجة - تلك التي حصلت، لكنّ ردود النداء لم تعالجها. ويمكن أن تُستخدم هذه التوابع معا، هكذا: // احصل على قائمة التحوّلات التي لم تُعالج // يجب أن تُستدعى قبل قطع الاتصال // إذا كنت تهتمّ للتحوّلات الحديثة التي قد لا تكون عولجت let mutationRecords = observer.takeRecords(); // أوقف تتبّع التغيّرات observer.disconnect(); ... ملاحظة: تُحذف السجلّات التي يعيدها observer.takeRecords()‎ من رتل المعالجة لن يُستدعى ردّ النداء للسجلّات المعادة من طرف observer.takeRecords()‎. ملاحظة: التفاعل مع جمع المهملات (garbage collection) تستخدم المراقبات داخليّا مراجع ضعيفة (weak references) للعقد. بمعنى، إذا حُذفت عقدة ما من DOM، وصارت غير قابلة ل لوصول unreachable، فيمكن عندئذ جمعها مع المهملات. مجرّد كون عقدة ما من DOM تحت المراقبة لا يمنع جمعها مع المهملات. الملخص يستطيع MutationObserver الاستجابة للتغيّرات في DOM - السمات، والمحتوى النصّيّ، وإضافة/إزالة العناصر، ويمكن استخدامه لتتبّع التغيّرات النابعة من أجزاء أخرى في شيفرتنا، وكذاك لإدماج سكربتات الطرف الثالث. يستطيع MutationObserver تتبّع أيّ تغيير. تُستخدم خيارات ضبط "مالذي يُراقب" لغرض التحسينات optimizations، لا لإنفاق الموارد على استدعاءات ردّ نداء لا حاجة لها. ترجمة -وبتصرف- للمقال Mutation observer من سلسلة Browser: Document, Events, Interfaces لصاحبها Ilya Kantor. اقرأ أيضًا تحميل الموارد الخارجية في صفحات الويب وتتبع حالتها عبر جافاسكربت. هياكل البيانات: الكائنات والمصفوفات في جافاسكريبت. التركيز على عناصر الاستمارات الإلكترونية والتنقل بينها في جافاسكربت.
  4. يمكّننا المتصفّح من تتبّع تحميل الموارد الخارجيّة، مثل السكربتات والعناصر iframes والصور وما إلى ذلك، وهناك حدثان لهذا الغرض: onload -- نجاح التحميل onerror -- حصل خطأ ما تحميل سكربت لنفترض أنّنا نودّ تحميل سكربت طرف ثالث واستدعاء دالّة موجودة فيه. يمكننا تحميله ديناميكيّا هكذا: let script = document.createElement('script'); script.src = "my.js"; document.head.append(script); لكن كيف يتمّ تنفيذ دالّة مصرّح بها داخل ذلك السكربت؟ علينا أن ننتظر إلى أن يُحمّل السكربت، وعندها فقط يمكننا استدعاؤها. ملاحظة: بالنسبة لسكربتاتنا الخاصّة يمكننا استخدام وحدات جافاسكربت لذلك، لكنّها ليست شائعة التبنّي من مكتبات الطرف الثالث. script.onload المساعد الرئيسيّ هو الحدث load ويقع عند الانتهاء من تحميل السكربت وتنفيذه. على سبيل المثال: let script = document.createElement('script'); // يمكن أن يحمّل أيّ سكربت، من أيّ نطاق script.src = "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.3.0/lodash.js" document.head.append(script); script.onload = function() { // "_" ينشئ السكربت المتغيّر alert( _.VERSION ); // shows library version }; See the Pen JS-p2-onload-onerror -ex1 by Hsoub (@Hsoub) on CodePen. بداخل onload، يمكننا إذًا استخدام متغيّرات السكربت وتنفيذ الدوالّ وما إلى ذلك. لكن ماذا لو فشل التحميل؟ على سبيل المثال، إذا كان لا وجود لهذا السكربت (الخطأ 404) أو الخادم معطّل (غير متاح). script.onerror يمكن تتبّع الأخطاء التي تحصل خلال تحميل السكربت في حدث error. على سبيل المثال، لنقم بطلب سكربت غير موجود: let script = document.createElement('script'); script.src = "https://example.com/404.js"; // لا وجود لهذا السكربت document.head.append(script); script.onerror = function() { alert("Error loading " + this.src); // https://example.com/404.js خطأ في تحميل }; See the Pen JS-p2-onload-onerror -ex2 by Hsoub (@Hsoub) on CodePen. يُرجى التنبّه إلى أنّه لا يمكننا الحصول على تفاصيل خطأ HTTP هنا. لا نعلم إن كان خطأ 400 أو 500 أو شيئا آخر. نعلم فقط أنّ التحميل قد فشل. تنبيه: لا تتبّع الأحداثُ onload/onerror سوى التحميل نفسه. الأخطاء التي تحصل خلال معالجة السكربت وتنفيذه خارجة عن نطاق هذه الأحداث. بمعنى: إذا حُمّل السكربت بنجاح، فإنّ onload يشتغل، حتى لو كان في السكربت أخطاء برمجيّة. لتتبّع أخطاء السكربتات يمكن استخدام المعالج العمومي window.onerror. الموارد الأخرى تعمل الأحداث load وerror مع الموارد الأخرى كذلك، وتعمل أساسا مع أيّ مورد له src خارجيّ. مثلا: let img = document.createElement('img'); img.src = "https://js.cx/clipart/train.gif"; // (*) img.onload = function() { alert(`Image loaded, size ${img.width}x${img.height}`); }; img.onerror = function() { alert("Error occurred while loading image"); }; See the Pen JS-p2-onload-onerror -ex3 by Hsoub (@Hsoub) on CodePen. لكنّ هناك بعض الملاحظات تعود لأسباب تاريخيّة: تبدأ معظم الموارد في التحميل عندما تُضاف إلى المستند لكنّ <img> تُعدّ استثناءً، حيث تبدأ في التحميل عندما تحصل على الرابط أو المصدر src (*). بالنسبة لـ <iframe>، يقع الحدث iframe.onload عندما ينتهي تحميل iframe، سواء بنجاح التحميل أو في حالة خطأ. سياسة تعدد المصادر Crossorigin policy هناك قاعدة تقول: لا يمكن لسكربتات موقع ما الوصول إلى محتويات موقع آخر. فمثلا، لا يستطيع سكربت موجود في https://facebook.com قراءة صندوق بريد المستخدم في https://gmail.com. أو بشكل أدقّ، لا يمكن لأحد المصادر (الثلاثيّة domain/port/protocol البروتوكول/المنفذ/النطاق) الوصول إلى محتوى مصدر آخر. فحتى لو كان لدينا نطاق فرعيّ أو اختلف المنفذ فقط، فستُعدّ هذه مصادر مختلفة ولا يكون لديها مدخل إلى بعضها البعض. تشمل هذه القاعدة أيضا الموارد التي هي من نطاقات أخرى. إذا كنا نستخدم سكربتا من نطاق آخر، وكان فيه خطأ ما، فلا يمكننا الحصول على تفاصيل ذلك الخطأ. على سبيل المثال، لنأخذ السكربت (الفاسد) error.js المكوّن من مجرّد استدعاء لدالّة: // ? error.js noSuchFunction(); ثمّ لنحمّله من نفس الموقع الموجود فيه: <script> window.onerror = function(message, url, line, col, errorObj) { alert(`${message}\n${url}, ${line}:${col}`); }; </script> <script src="/article/onload-onerror/crossorigin/error.js"></script> See the Pen JS-p2-onload-onerror -ex4 by Hsoub (@Hsoub) on CodePen. يمكننا رؤية تقرير جيّد للخطأ، هكذا: Uncaught ReferenceError: noSuchFunction is not defined https://javascript.info/article/onload-onerror/crossorigin/error.js, 1:1 لنحمّل الآن نفس السكربت من نطاق آخر: <script> window.onerror = function(message, url, line, col, errorObj) { alert(`${message}\n${url}, ${line}:${col}`); }; </script> <script src="https://cors.javascript.info/article/onload-onerror/crossorigin/error.js"></script> See the Pen JS-p2-onload-onerror -ex5 by Hsoub (@Hsoub) on CodePen. التقرير الآن مختلف: Script error. , 0:0 قد تختلف التفاصيل حسب المتصفّح، لكنّ الفكرة نفسها: أيّة معلومات حول الأمور الداخليّة للسكربت، بما في ذلك مكدس آثار الخطأ error stack traces، تكون مخفيّة. يرجع ذلك بالتحديد إلى كونها من نطاق آخر. لماذا قد نحتاج إلى تفاصيل الخطأ؟ توجد هناك العديد من الخدمات (يمكن أن نبني واحدة لنا) التي تعمل على الانصات للأخطاء العموميّة بواسطة window.onerror، وحفظها وتوفير واجهة للوصول إليها وتحليلها. هذا جيّد، إذ يمكننا رؤية الأخطاء الحقيقيّة، التي يتسبّب فيها مستخدمونا. لكن إذا كان السكربت من مصدر آخر، فلا تكون هناك الكثير من المعلومات حول الأخطاء فيها، كما شاهدنا للتوّ. تُفرض نفس سياسة تعدّد المصادر CORS على أنواع الموارد الأخرى كذلك. لإتاحة الوصول متعدّد المصادر، يجب أن يكون للوسم <script> السمة crossorigin، ويجب كذلك أن يقدّم الخادم ترويسات Headers خاصّة. هناك ثلاثة مستويات للوصول متعدّد المصادر: لا وجود للسمة crossorigin -- الوصول ممنوع. crossorigin="anonymous"‎ -- الوصول متاح إذا أجاب الخادم بالترويسة Access-Control-Allow-Origin مع * أو مع مصدرنا. لا يرسل المتصفّح معلومات الترخيص authorization وملفّات الارتباط cookies إلى الخادم البعيد. crossorigin="use-credentials"‎ -- الوصول متاح إذا أجاب الخادم بالترويسة Access-Control-Allow-Origin مع مصدرنا و Access-Control-Allow-Credentials: true. يرسل المتصفّح معلومات الترخيص وملفّات الارتباط إلى الخادم البعيد. في حالتنا هذه، لم يكن لدينا أيّ وسم crossorigin. لذا فإنّ الوصول متعدّد المصادر قد مُنع. لنضفه الآن. يمكننا الاختيار بين "anonymous" (لا إرسال لملفات الارتباط، يُحتاج إلى ترويسة واحدة من جانب الخادم) و"use-credentials" (يرسل ملفّات الارتباط أيضا، يُحتاج إلى ترويستين من جانب الخادم). إذا لم نكن نهتمّ بملفّات الارتباط، فإنّ "anonymous" هي الخيار: <script> window.onerror = function(message, url, line, col, errorObj) { alert(`${message}\n${url}, ${line}:${col}`); }; </script> <script crossorigin="anonymous" src="https://cors.javascript.info/article/onload-onerror/crossorigin/error.js"></script> See the Pen JS-p2-onload-onerror -ex6 by Hsoub (@Hsoub) on CodePen. حاليّا، على افتراض أنّ الخادم يوفّر ترويسة Access-Control-Allow-Origin، كلّ شيء على ما يرام، لدينا تقرير الخطأ الكامل. الملخص توفّر الصور <img> والتنسيقات الخارجيّة والسكربتات والموارد الأخرى الأحداث load وerror لتتبّع تحميلها: يقع load عند نجاح التحميل. يقع error عند فشل التحميل. الاستثناء الوحيد هو <iframe>: لأسباب تاريخيّة يحدث load على الدوام، عند أيّ انتهاء للتحميل، حتى لو لم توجد الصفحة. يعمل الحدث readystatechange أيضا مع الموارد، لكن يندر استخدامه، لأنّ الأحداث load/error أبسط. التمارين حمل الصور بواسطة ردود النداء callbacks الأهميّة: 4 عادة، تُحمّل الصور عند إنشائها. فعندما نضيف <img> إلى الصفحة، لا يرى المستخدم الصورة فورا. يحتاج المتصفّح إلى تحميلها أوّلا. لإظهار الصورة فورا، يمكننا إنشاؤها "مسبقا"، هكذا: let img = document.createElement('img'); img.src = 'my.jpg'; يبدأ المتصفّح تحميل الصورة ويحفظها في الذاكرة المؤقّتة. لاحقا، عندما تحضر نفس الصورة في المستند (مهما كانت الطريقة)، فإنّها تظهر فورا. أنشئ الدالّة preloadImages(sources, callback)‎ التي تحمّل جميع الصور التي في المصفوفة sources، وعندما تجهز، تنفّذ callback. على سبيل المثال، سيظهر هذا التنبيه alert بعدما تُحمّل الصور: function loaded() { alert("Images loaded") } preloadImages(["1.jpg", "2.jpg", "3.jpg"], loaded); حتى في حالة خطأ، يجب أن تفترض الدالّة أنّ الصورة "محمّلة". بعبارة أخرى، تُنفّذ callback سواء عند نجاح تحميل الصور أو فشله. تفيدنا هذه الدالّة، على سبيل المثال، عندما نريد إظهار معرض للعديد من الصور القابلة للتمرير، ونريد أن نتأكّد من أنّ جميع الصور قد حُمّلت. ستجد في المستند المصدريّ الشيفرة وروابط لاختبار الصور، للتحقّق من أنّها قد حُمّلت. يجب أن يكون المُخرج هو 300. أنجز الحلّ في البيئة التجريبية الحل الخوارزميّة: اجعل img لكلّ مصدر. أضف onload/onerror لكلّ صورة. زد في العدّاد كلّما اشتغل onload أوonerror. عندما تصير قيمة العدّاد تساوي عدد المصادر، نكون قد انتهينا: callback()‎. افتح الحلّ في البيئة التجريبية ترجمة -وبتصرف- للمقال Resource loading: onload and onerror من سلسلة Browser: Document, Events, Interfaces لصاحبها Ilya Kantor
  5. عندما يُحمّل المتصفّح ملف HTML ويجد وسم <script>...</script>، فلا يمكنه مواصلة بناء DOM. يجب أن ينفّذ السكربت حالا. نفس الشيء بالنسبة للسكربتات الخارجيّة ‎<script src="...">..</script>‎: يجب أن ينتظر المتصفّح تحميل السكربت، ثم ينفّذ السكربت المحمّل، وعندها فقط يمكنه معالجة بقيّة الصفحة. يرافق ذلك مشكلتان مهمّتان: لا يمكن أن تطّلع السكربتات على عناصر DOM التي تحتها، ولا يمكنها إذًا إضافة معالجات وما إلى ذلك. إذا كان هناك سكربت كبير في أعلى الصفحة، فإنّه "يحبس الصفحة". لا يمكن للمستخدمين رؤية محتوى الصفحة حتى يُحمّل السكربت ويُنفّذ: <p>...content before script...</p> <script src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script> <!-- هذا غير مرئيّ إلى أن يُحمّل السكربت --> <p>...content after script...</p> See the Pen JS-p2-script-async-defer -ex1 by Hsoub (@Hsoub) on CodePen. هناك طرق للالتفاف حول تلك المشاكل. على سبيل المثال، يمكننا وضع السكربت في أسفل الصفحة. وبذلك يمكنه الاطلاع على العناصر التي فوقه، ولا تُحبس محتويات الصفحة عن الظهور: <body> ...all content is above the script... <script src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script> </body> See the Pen JS-p2-script-async-defer -ex2 by Hsoub (@Hsoub) on CodePen. لكنّ هذه الطريقة بعيدة عن كونها مثاليّة. على سبيل المثال، لا يعلم المتصفّحُ بوجود السكربت (ولا يمكنه البدء بتحميله) إلا بعد تحميل كامل مستند HTML. بالنسبة لمستندات HTML الطويلة، قد يكون هناك تأخّر ملحوظ. لا تُلاحظ هذه الأمور لمن يملكون اتصالات سريعة جدا، لكن لا يزال الكثير من الناس حول العالم يملكون اتصالات بطيئة بالانترنت ويستخدمون اتصالات انترنت محمولة أبعد ما تكون عن المثاليّة. لحسن الحظّ، هناك سمتان للعنصر <script> يمكنها أن تحلّ لنا هذه المشكلة: defer وasync. السمة defer تطلب السمة defer من المتصفّح أن لا ينتظر السكربت. بل يواصل المتصفّح معالجة HTML وبناء DOM. يُحمّل السكربت في "الخلفيّة"، ثمّ يشتغل عندما يتمّ بناء DOM كاملا. إليك نفس المثال الذي في الأعلى لكن باستخدام defer: <p>...content before script...</p> <script defer src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script> <!-- يكون مرئيّا على الفور --> <p>...content after script...</p> See the Pen JS-p2-script-async-defer -ex3 by Hsoub (@Hsoub) on CodePen. بعبارة أخرى: لا تحبس السكربتات التي لها defer الصفحة أبدا. دائما ما تُنفّذ السكربتات التي لها defer عندما يكون DOM جاهزا (لكن قبل الحدث DOMContentLoaded). يوضّح هذا المثال النقطة الثانية: <p>...content before scripts...</p> <script> document.addEventListener('DOMContentLoaded', () => alert("DOM ready after defer!")); </script> <script defer src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script> <p>...content after scripts...</p> See the Pen JS-p2-script-async-defer -ex4 by Hsoub (@Hsoub) on CodePen. يظهر محتوى الصفحة مباشرة. ينتظر معالج الحدث DOMContentLoaded السكربت المُرجأ (deferred). لا يشتغل حتى يُحمّل السكربت ويُنفّذ. تحافظ السكربتات المُرجأة على الترتيب بينها، تماما كما هو الحال مع السكربتات العاديّة لنفترض أنّ لدينا سكربتين مُرجأين: الطويل long.jsوثمّ القصير small.js: <script defer src="https://javascript.info/article/script-async-defer/long.js"></script> <script defer src="https://javascript.info/article/script-async-defer/small.js"></script> يبحث المتصفّح في الصفحة على السكربتات ويُحمّلها معًا، لتحسين الأداء. ففي المثال أعلاه يُحمّل السكربتان بشكل متوازي. من المُرجّح أن ينتهي small.js أوّلا. لكنّ السمة defer، بالإضافة إلى طلبها من المتصفّح "عدم الحبس"، تضمن المحافظة على الترتيب بين السكربتات. فحتى لو حُمّل small.js أوّلا، فإنّ عليه أن ينتظر ويُنفّذ بعد تنفيذ long.js. قد يكون ذلك مهمّا في الحالات التي نحتاج فيها إلى تحميل مكتبة جافاسكربت أوّلا ثمّ سكربت آخر يعتمد عليها. ملاحظة: السمة defer هي للسكربتات الخارجيّة فقط تُتجاهل السمة defer إذا لم يكن الوسم <script> له src. async السمة async هي مثل defer نوعا ما. هي أيضا تجعل السكربت غير حابس. لكنّ بينهما فروقات مهمّة في السلوك. تعني السمة async أن السكربت مستقلّ تماما: لا يحبس المتصفّح عند السكربتات التي لها async (مثل defer). لا تنتظر السكربتات الأخرى السكربتات التي لها async، ولا تنتظر السكربتات التي لها async السكربتات الأخرى. لا ينتظرDOMContentLoaded والسكربتات غير المتزامنة بعضها البعض: قد يقع DOMContentLoaded سواء قبل السكربت غير المتزامن (عندما ينتهي تحميل السكربت غير المتزامن قبل تمام الصفحة) …أو بعد السكربت غير المتزامن (إذا كان السكربت غير المتزامن قصيرا أو كان في ذاكرة التخزين المؤقّت HTTP). بعبارة أخرى، تُحمّل السكربتات التي لها async في الخلفيّة وتُنفّذ عندما تجهز. لا ينتظرها DOM ولا السكربتات الأخرى، ولا تنتظر هي بدورها أيّ شيء. سكربت مستقلّ تماما يُنفّذ عندما يجهز. من أبسط ما يكون، صحيح؟ إليك مثالا مشابها للذي رأينا مع defer: سكربتان long.js وsmall.js، لكن الآن لها async بدل defer. لا ينتظر أحدهما الآخر. أيّا كان الذي يُحمّل أوّلا (small.js على الأرجح) فإنّه يُنفّذ أوّلا: <p>...content before scripts...</p> <script> document.addEventListener('DOMContentLoaded', () => alert("DOM ready!")); </script> <script async src="https://javascript.info/article/script-async-defer/long.js"></script> <script async src="https://javascript.info/article/script-async-defer/small.js"></script> <p>...content after scripts...</p> See the Pen JS-p2-script-async-defer -ex5 by Hsoub (@Hsoub) on CodePen. يظهر محتوى الصفحة فورا: لا يحبسه async. قد يقع DOMContentLoaded إمّا قبل async أو بعد، لا ضمانات هنا. يأتي السكربت الأقصر small.js ثانيا، لكنّه على الأرجح يُحمّل قبل long.js، وبالتالي فإنّ small.js يُنفّذ أوّلا. رغم ذلك، من الممكن أن يُحمّل long.js أوّلا، إذا كان في ذاكرة التخزين المؤقّتة، فينُفّذ بذلك أوّلا. بعبارة أخرى، تُنفّذ السكربتات غير المتزامنة حسب ترتيب "أوّليّة التحميل". تفيد السكربتات غير المتزامنة كثيرا عندما ندمج سكربت طرف ثالث مستقلّ في الصفحة، كالعدّادات والإعلانات وما إلى ذلك، إذ لا تعتمد على سكربتاتنا، وليس على سكربتاتنا أن تنتظرها. <!-- هكذا عادة Google Analytics تضاف--> <script async src="https://google-analytics.com/analytics.js"></script> السكربتات الديناميكيّة هناك طريقة مهمّة أخرى لإضافة سكربت إلى الصفحة. يمكننا إنشاء سكربت وإلحاقه بالمستند ديناميكيّا باستخدام جافاسكربت: let script = document.createElement('script'); script.src = "/article/script-async-defer/long.js"; document.body.append(script); // (*) يبدأ تحميل السكربت بعد إلحاقه بالمستند مباشرة (*). تتصرّف السكربتات الديناميكيّة افتراضيًّا وكأنّها "غير متزامنة"، يعني ذلك: لا تنتظر أيّ شيء، ولاشيء ينتظرها. السكربت الذي يُحمّل أوّلا يُنفّذ أوّلا (حسب ترتيب "أوّليّة التحميل"). يمكن تغيير ذلك إذا وضعنا script.async=false صراحة. عندها تُنفّذ السكربتات حسب ترتيبها في المستند، تماما مثل defer. في هذا المثال، تضيف الدالّة loadScript(src)‎ سكربتا وتضبط أيضا async على false. وبذلك يُنفّذ long.js أوّلا على الدوام (إذ قد أُضيف أوّلا): function loadScript(src) { let script = document.createElement('script'); script.src = src; script.async = false; document.body.append(script); } // async=false أوّلا لأنّ long.js يُنفّذ loadScript("/article/script-async-defer/long.js"); loadScript("/article/script-async-defer/small.js"); بدون script.async=false، تُنفّذ السكربتات حسب الترتيب الافتراضيّ "أوّليّة التحميل" (small.js أوّلا على الأرجح). ثانيا، كما هو الأمر مع defer، يصير الترتيب مهمّا عندما نريد تحميل مكتبة ما ومن ثمّ سكربت يعتمد عليها. الملخص لكلّ من async وdefer أمر مشترك: لا يحبس تحميلُ هذه السكربتات تصييرَ (rendering) الصفحة. وبذلك يمكن للمستخدم الاطلاع على الصفحة وقراءة محتواها فورا. لكنّ هناك أيضا فروقات جوهريّة بينها: الترتيب ‏ DOMContentLoaded async‏ الترتيب وفق أوليّة التحميل. لا يهمّ ترتيبها في المستند -- الذي يُحمّل أوّلا يُنفّذ أوّلا لا علاقة له. قد يُحمّل السكربت ويُنفّذ قبل أن يتمّ تحميل كامل المستند. يحصل هذا إذا كانت السكربتات صغيرة أو مخزّنة في الذاكرة المؤقّتة، والمستند طويل كفاية. defer‏ الترتيب وفق المستند (حسب موضعها في المستند). تُنفّذ السكربتات بعد الانتهاء من تحميل المستند وتفسيره (تنتظر عند الحاجة لذلك)، مباشرة قبل DOMContentLoaded. 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; } عمليّا، تُستخدم defer في السكربتات التي تحتاج DOM كاملا و/أو التي يكون ترتيب التنفيذ فيما بينها مهمّا. وتُستخدم async في السكربتات المستقلّة، مثل العدّادات والإعلانات. ويكون ترتيب تنفيذها غير مهمّ. تنبيه: يجب أن تكون الصفحة قابلة للاستخدام دون السكربتات يُرجى التنبه: إذا استخدمت defer أو async، فسيرى المستخدم الصفحة قبل تحميل السكربت. في هذه الحالة، قد لا تكون بعض المكوّنات الرسوميّة تهيّئت بعد. لا تنس وضع إشارة "التحميل" وتعطيل الأزرار التي لا تعمل بعد. دع المستخدم يرى بوضوح ما الذي يمكن فعله على الصفحة، وما الذي لم يتجهّز بعد. ترجمة -وبتصرف- للمقال Scripts: async, defer من سلسلة Browser: Document, Events, Interfaces لصاحبها Ilya Kantor
  6. لدورة حياة صفحة HTML ثلاثة أحداث مهمّة: DOMContentLoaded -- حمّل المتصفّحُ بنية HTML الكاملة للصفحة، وبُنيت شجرة DOM، لكنّ الموارد الخارجيّة كالصور <img> وأوراق التنسيق stylesheets قد لا تكون حُمّلت بعد. load -- حمّل المتصفّح الـ HTML، وكذلك جميع الموارد الخارجيّة من صور وتنسيقات وغيرها. beforeunload/unload -- يغادر المستخدم الصفحة. يمكن لأيّ من هذه الأحداث أن يكون ذا فائدة: الحدث DOMContentLoaded -- صارت شجرة DOM جاهزة، ويمكن للمعالج إذًا البحث فيه عن العقد، وتهيئة الواجهة. الحدث load -- قد حُمّلت الموارد الخارجيّة، وبذلك تكون التنسيقات قد طُبّقت، ومقاسات الصور عُلمت، وما إلى ذلك. الحدث beforeunload -- يهمّ المُستخدم بالمغادرة. يمكننا عندها أن نتحقّق من أنّه قد حفظ التغييرات وأن نسأله إن كان حقّا يودّ المغادرة. الحدث unload -- المُستخدم على وشك المغادرة، لكنّنا لازلنا نستطيع إجراء بعض العمليّات كإرسال الإحصائيّات مثلا. لنطّلع على تفاصيل هذه الأحداث. الحدث DOMContentLoaded يقع الحدث DOMContentLoaded على الكائن document، ولابدّ أن نستعمل addEventListener لالتقاطه: document.addEventListener("DOMContentLoaded", ready); // "document.onDOMContentLoaded = ..." ليس على سبيل المثال: <script> function ready() { alert('DOM is ready'); // 0x0 لم تُحمّل الصورة بعد (إلّا إن كانت قد خُبّئت في ذاكرة التخزين المؤقّت)، لذا فمقاسها هو alert(`Image size: ${img.offsetWidth}x${img.offsetHeight}`); } document.addEventListener("DOMContentLoaded", ready); </script> <img id="img" src="https://en.js.cx/clipart/train.gif?speed=1&cache=0"> See the Pen JS-p2-loading -ex1 by Hsoub (@Hsoub) on CodePen. في هذا المثال، يشتغل معالج DOMContentLoaded عندما يُحمَّل المستند، ويمكنه عندها رؤية جميع العناصر، بما في ذلك <img> الذي في الأسفل. لكنّه لا ينتظر الصورة حتى تُحمّل، ولذلك يُظهر alert المقاسات صفرا. من الوهلة الأولى، يبدو الحدث DOMContentLoaded بسيطا للغاية. تجهز شجرة DOM، فيقع الحدث. مع ذلك، ينبغي التنبيه على بعض الدقائق. DOMContentLoaded والسكربتات عندما يعالج المتصفّح مستند HTML ويلاقي وسم <script>، فيجب عليه تنفيذه قبل أن يتابع بناء DOM. هذا من باب الاحتياط، إذ قد تودّ السكربتات تغيير DOM، أو حتى إجراء document.write عليه، فيجب إذًا على DOMContentLoaded أن ينتظر. لذلك لابدّ أن يكون حدوث DOMContentLoaded بعد هذه السكربتات: <script> document.addEventListener("DOMContentLoaded", () => { alert("DOM ready!"); }); </script> <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.3.0/lodash.js"></script> <script> alert("Library loaded, inline script executed"); </script> See the Pen JS-p2-loading -ex2 by Hsoub (@Hsoub) on CodePen. في المثال أعلاه، نلاحظ "Library loaded…‎" أوّلا، ثمّ "DOM ready!‎" (جميع السكربتات قد نُفّذت). تنبيه: السكربتات التي لا تعيق DOMContentLoaded هناك استثناءان لهذه القاعدة: السكربتات ذوات السمة async، التي سنتناولها عن قريب، لا تعيق DOMContentLoaded. السكربتات االتي تُولَّد ديناميكيّا بواسطة document.createElement('script')‎ ثمّ تُضاف إلى صفحة الويب لا تعيق هذا الحدث أيضا. DOMContentLoaded والتنسيقات لا تؤثّر أوراق التنسيق في DOM، لذا فإنّ DOMContentLoaded لا ينتظرها. لكنّ هناك مزلقا. إذا كان لدينا سكربت موجود بعد التنسيق، فيجب على ذلك السكربت أن ينتظر حتى تُحمّل ورقة التنسيق: <link type="text/css" rel="stylesheet" href="style.css"> <script> // لا يُنفّذ السكربت حتى تُحمّل ورقة التنسيق alert(getComputedStyle(document.body).marginTop); </script> See the Pen JS-p2-loading -ex3 by Hsoub (@Hsoub) on CodePen. سبب ذلك أنّ ذلك السكربت قد يودّ الحصول على إحداثيّات أو غيرها من خاصيّات العناصر المتعلّقة بالتنسيق، كما في المثال أعلاه. فمن الطبيعيّ إذا أن ينتظر التنسيقات حتى تُحمّل. بانتظار الحدث DOMContentLoaded للسكربتات، عليه انتظار التنسيقات التي قبلها كذلك. تعبئة الاستمارت تلقائيا في المتصفح تُعبّئ المتصفّحاتُ Firefox وChrome وOpera الاستمارات تلقائيّا عند حدوث DOMContentLoaded. على سبيل المثال، إذا كان في الصفحة استمارة لاسم المستخدم وكلمة المرور، وتذكّر المتصفّح القيم الخاصّة بها، فعند حدوث DOMContentLoaded، قد يحاول تعبئتها تلقائيّا (إذا أقرّ المستخدم ذلك). فإذا أُجِّل DOMContentLoaded بسبب سكربتات طويلة التحميل، فإنّ التعبئة التلقائيّة تنتظر أيضا. قد تكون لاحظت ذلك في بعض المواقع (إذا كنت تستخدم تعبئة المتصفّح التلقائيّة) -- لا تُعبّأ حقول اسم المستخدم وكلمة المرور فورا، بل إنّ هناك تأخيرا إلى حين تحميل الصفحة بالكامل. ذلك في الواقع هو الوقت الذي يأخذه DOMContentLoaded للحدوث. الحدث window.onload يقع الحدث load على كائن النافذة window عندما تُحمّل الصفحة كاملة، بما في ذلك التنسيقات والصور وغيرها من الموارد. هذا الحدث متاح من خلال الخاصّيّة onload. يظهر المثال أدناه مقاسات الصور الصحيحة، لأنّ window.onload ينتظر جميع الصور: <script> window.onload = function() { // window.addEventListener('load', (event) => { :هذا مماثل لكتابة alert('Page loaded'); // تُحمّل الصورة في هذه اﻷثناء alert(`Image size: ${img.offsetWidth}x${img.offsetHeight}`); }; </script> <img id="img" src="https://en.js.cx/clipart/train.gif?speed=1&cache=0"> See the Pen JS-p2-loading -ex4 by Hsoub (@Hsoub) on CodePen. الحدث window.onunload عندما يغادر الزائر الصفحة، يقع الحدث unload على window. يمكننا عندها القيام بأمور لا تتطلّب تأخيرا، كإغلاق النوافذ المنبثقة popup مثلا. الاستثناء الجدير بالذكر هو إرسال التحليلات analytics. لنفترض أنّنا نودّ تجميع معطيات حول كيفيّة استخدام الصفحة: نقرات الفأرة والتمريرات واﻷجزاء المُشاهدَة من الصفحة وما إلى ذلك. من الطبيعيّ أن يقع الحدث unload عندما يغادرنا المستخدم، ونريد عندها أن نحفظ المعطيات في خادمنا. هناك التابع navigator.sendBeacon(url, data)‎ المخصّص لهذا الغرض، وهو مبيّن في المواصفة. يعمل هذا التابع على إرسال المعطيات في الخلفيّة، فلا يتأخّر بذلك الانتقال إلى صفحة أخرى: يغادر المتصفّح الصفحة، لكنّه يبقى مؤدّيا لـ sendBeacon. هذه كيفيّة استخدامه: let analyticsData = { /* كائن فيه المعطيات المُجمّعة */ }; window.addEventListener("unload", function() { navigator.sendBeacon("/analytics", JSON.stringify(analyticsData)); }); يُرسل الطلب على شكل POST. لا يمكننا إرسال السلاسل النصّيّة فقط، بل الاستمارات وصيغ أخرى كذلك، كما سنوضع ذلك في مقال لاحق، لكن يُرسل غالبا كائن محوّل إلى سلسلة نصّيّة stringified object. يُحدّ حجم المعطيات بـ 64kb. عند انتهاء الطلب sendBeacon، يكون المتصفّح قد غادر المستند، وبالتالي لا سبيل للحصول على إجابة من الخادم (التي تكون في العادة فارغة بالنسبة للتحليلات). هناك أيضا الراية keepalive، للقيام بطلبات مماثلة تتعلّق بما "بعد مغادرة الصفحة"، في التابع Fetch الخاصّ بالطلبات العامّة على الشبكة. يمكنك الاطلاع على المزيد من المعلومات حول ذلك في مقال الواجهة البرمجية fetch في JavaScript. إذا أردنا إلغاء الانتقال إلى صفحة أخرى، لا يمكننا فعل ذلك هنا. لكن يمكننا استعمال حدث آخر -- onbeforeunload. الحدث window.onbeforeunload إذا شرع الزائر في مغادرة الصفحة أو حاول إغلاق النافذة، يطلب معالج beforeunload المزيد من التثبّت. وإذا ألغينا الحدث، قد يسأل المتصفّح المستخدم إن كان بالفعل متأكّدا. يمكنك تجربة ذلك بواسطة إجراء هذه الشيفرة ثم إعادة تحميل الصفحة: window.onbeforeunload = function() { return false; }; لأسباب تاريخيّة، تُعدّ إعادة سلسلة نصيّة غير فارغة أيضا بمثابة إلغاء الحدث. اعتادت المتصفّحات في السابق على إظهارها على شكل رسالة، لكن كما تنصّ [المواصفة الحديثة](https://html.spec.whatwg.org/#unloading-documents )، يجب ألا يفعلوا ذلك. هذا مثال: window.onbeforeunload = function() { return "There are unsaved changes. Leave now?"; }; غُيّر هذا السلوك، لأنّ بعض مديري المواقع أساؤوا استخدام معالج الحدث هذا بإظهار رسائل مضلّلة ومزعجة. فحاليّا، قد لا تزال المتصفّحات القديمة تظهرها كرسالة، لكن ما عدا ذلك، لا سبيل لتخصيص الرسالة التي تظهر للمستخدم. الخاصية readyState ماذا يحصل لو وضعنا معالجا للحدث DOMContentLoaded بعد تحميل المستند؟ من الطبيعيّ أن لا يشتغل أبدا. هناك حالات لا نكون فيها متأكّدين ما إذا كان المستند جاهزا أو لا. نريد لدالّتنا أن تُنفّذ عندما يكون DOM قد حُمّل، كان ذلك الآن أو لاحقا. تخبرنا الخاصّيّة document.readyState بحالة التحميل الحاليّة. وتأخذ ثلاث قيم ممكنة: "loading" -- المستند قيد التحميل. "interactive" -- تمّت قراءة المستند بأكمله. "complete" -- تمّت قراءة المستند بأكمله وحُمّلت جميع الموارد (مثل الصور) أيضا. فيمكننا إذًا تفحّص document.readyState وتهيئة معالج أو تنفيذ الشيفرة مباشرة إن كان المستند جاهزا. هكذا: function work() { /*...*/ } if (document.readyState == 'loading') { // لازال التحميل، انتظر الحدث document.addEventListener('DOMContentLoaded', work); } else { // جاهز DOM work(); } الحدث readystatechange هو آليّة بديلة لتتبّع حالة تحميل المستند، وقد ظهر قديما. أمّا حاليّا، فيندر استخدامه. لنرى مجرى الأحداث كاملا من باب التمام: هذا مستند فيه <iframe> و<img> ومعالجات تقوم بتسجيل اﻷحداث: <script> log('initial readyState:' + document.readyState); document.addEventListener('readystatechange', () => log('readyState:' + document.readyState)); document.addEventListener('DOMContentLoaded', () => log('DOMContentLoaded')); window.onload = () => log('window onload'); </script> <iframe src="iframe.html" onload="log('iframe onload')"></iframe> <img src="http://en.js.cx/clipart/train.gif" id="img"> <script> img.onload = () => log('img onload'); </script> يمكن مشاهدة المثال يعمل في البيئة التجريبيّة النتيجة الاعتياديّة: [1] initial readyState:loading [2] readyState:interactive [2] DOMContentLoaded [3] iframe onload [4] img onload [4] readyState:complete [4] window onload تشير اﻷرقام داخل اﻷقواس المربّعة إلى وقت الحدوث بالتقريب. تقع الأحداث المُعلّمة بنفس الأرقام في نفس الوقت تقريبا (+- بضع ms). document.readyState يصير interactive مباشرة قبل DOMContentLoaded. يعني هذان نفس الشيء في الحقيقة. document.readyState يصير complete عندما تُحمّل جميع الموارد (iframe و img). يمكننا أن نرى هنا أنّه يحدث في نفس الوقت تقريبا مع img.onload‏ (img هو آخر الموارد) و window.onload. يعني الانتقال إلى الحالة complete نفس window.onload. الفرق الذي هناك هو أنّ window.onload يشتغل دائما بعد جميع معالجات load اﻷخرى. الملخص أحداث تحميل الصفحة: يقع الحدث DOMContentLoaded على document عندما يكون DOM جاهزا. يمكننا تطبيق جافاسكربت على العناصر في هذه المرحلة. تعيق السكربتات مثل <script>...</script> أو <script src="..."></script> الحدث DOMContentLoaded، إذ ينتظر المتصفّح تنفيذها. يمكن أن تواصل الصور والموارد اﻷخرى التحميل. يقع الحدث load على window عندما تكون الصفحة وجميع الموارد قد حُمّلت. يندر استخدامه، إذ لا حاجة للانتظار كلّ هذا الوقت في العادة. يقع الحدث beforeunload على window عندما يغادر المستخدم في النهاية، يمكننا فعل أشياء بسيطة فقط في المعالج، لا تستلزم تأخّرات ولا تسأل المستخدم. بسبب هذه القيود، يندر استخدامها. يمكننا إرسال طلب عبر الشبكة باستعمال navigator.sendBeacon. document.readyState هي حالة جاهزيّة المستند الحاليّة، يمكن تتبّع تغيّراتها بواسطة الحدث readystatechange: loading -- المستند قيد التحميل. interactive -- قد حُلّل المستند. يحدث في نفس الوقت تقريبا مع DOMContentLoaded، لكن قبله. complete -- قد حُمّل المستند والموارد. يحدث في نفس الوقت تقريبا مع window.onload، لكن قبله. ترجمة -وبتصرف- للمقال Page: DOMContentLoaded, load, beforeunload, unload من سلسلة Browser: Document, Events, Interfaces لصاحبها Ilya Kantor
  7. يقع الحدث 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
  8. لنتناول مختلف اﻷحداث التي ترافق تحديث المُعطيات. الحدث: 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
  9. يمكن للعنصر أن يحصل على التركيز من المستخدم إمّا بالنقر عليه مباشرة أو باستعمال المفتاح 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
  10. تمتلك الاستمارات وعناصر التحكّم فيها، مثل <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
  11. يمكّن الحدث 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
  12. قبل أن نأتي إلى لوحة المفاتيح، يُرجى التنبّه إلى أنّ للأجهزة الحديثة سُبلًا أخرى إلى "إدخال شيء ما". فقد يستخدم الناس مثلا تقنيّة التعرّف على الكلام (خاصّة على اﻷجهزة المحمولة) أو النسخ/اللصق بواسطة الفأرة. ولذلك، إذا أردنا تتبّع جميع المُدخلات في الحقل <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
  13. تُعدّ أحداث المؤشّر طريقة حديثة للتعامل مع المُدخلات النابعة من مختلف أجهزة التأشير، كالفأرة والقلم والشاشات اللمسيّة وغيرها. لمحة تاريخية مختصرة لنأخذ بلمحة قصيرة، لتتمكّن من فهم الصورة العامّة وكذا مكانة أحداث المؤشّر وسط أنواع اﻷحداث اﻷخرى. في القديم، لم تكن هناك سوى أحداث الفأرة. ثمّ انتشرت اﻷجهزة اللمسيّة، مثل الهواتف واﻷجهزة اللوحيّة. ولكي تعمل النصوص البرمجيّة القائمة، كانت هذه اﻷجهزة (ولا زالت) تولّد أحداث الفأرة. فعلى سبيل المثال، يولّد الضغط على شاشة لمسيّة الحدث 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
  14. يُعدّ السحب والإفلات وسيلة جيّدة في واجهة المستخدم. إذ أنّ أخذ الشيء وسحبه ثمّ إفلاته طريقة واضحة وبسيطة لفعل الكثير من اﻷمور، من نسخ المستندات ونقلها (كما في مدير الملفّات) إلى إجراء الطلبات (كوضع السلع في سلّة المشتريات). في مواصفة 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
  15. لنتعمّق في المزيد من التفاصيل حول اﻷحداث التي تقع عندما تتحرّك الفأرة بين العناصر. اﻷحداث 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
  16. سنتناول في هذا المقال أحداث الفأرة وخاصّياتها بمزيد من التفصيل. يُرجى التنبه: لا تأتي هذه الأحداث فقط من "الأجهزة ذوات الفأرة"، ولكن من أجهزة أخرى أيضًا مثل الهواتف والأجهزة اللوحيّة، حيث تُحاكى فيها هذه الأحداث لتحقيق التوافق. أنواع أحداث الفأرة سبق وأن رأينا بعض هذه الأحداث: 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
  17. لا يمكننا فقط إسناد معالجات للأحداث من خلال جافاسكربت، ولكن يمكننا أيضا توليد أحداث مخصّصة. يُمكن استعمال الأحداث المخصّصة لإنشاء "مكوّنات رسوميّة". على سبيل المثال، يمكن للعنصر الجذر في قائمةٍ تعمل بواسطة جافاسكربت افتعال أحداثٍ تُنبئ بما يحصل مع القائمة: 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
  18. تتسبّب الكثير من الأحداث في قيام المتصفّح تلقائيّا بأفعالٍ معيّنة. على سبيل المثال: يتسبّب النقر على رابط في الانتقال إلى عنوانه. يتسبّب النقر على زرّ الإرسال في نموذج في إرساله إلى الخادم. يتسبّب الضغط على زرّ الفأرة فوق نصّ وتحريكها في تحديد ذلك النصّ. في حال معالجة الحدث في جافاسكربت، قد لا نريد حصول فعل المتصفّح المقابل له، ونريد تطبيق سلوكٍ آخر في مكانه. منع أفعال المتصفح هناك طريقتان لإخبار المتصفّح بأنّنا لا نريده أن يعمل: الطريقة الرئيسيّة هي استخدام الكائن 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
  19. يمكّننا انتشار اﻷحداث نحو اﻷسفل واﻷعلى من تطبيق أحد أقوى أنماط معالجة الأحداث، وهو ما يُسمى تفويض الأحداث (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
  20. لنبتدئ بمثال. المعالج التالي مُسندٌ إلى العنصر <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
  21. تمثّل الأحداث (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
  22. لنتناول عُقدَ DOM بمزيدٍ من التعمّق. في هذا المقال، سننظر أكثر في ماهيّتها وسنتعرّف على أكثر خاصّيّاتها استخداما. أصناف العقد في DOM قد تختلف خاصّيات العقد باختلاف العقد نفسها. فعلى سبيل المثال، تملك العقد التي تمثّل الوسم <a> خاصّيّات تتعلّق بالروابط، وتملك العقد التي تمثّل الوسم <input> خاصّيّات تتعلّق بالمُدخلات، وهكذا. لكن بالرغم من اختلاف العقد عن بعضها، فإنّ لها أيضا خاصّيّات وتوابع مشتركة بينها، ﻷنّ جميع أصناف العقد في DOM تُشكّل تسلسلا هرميّا واحدا. تنتمي كلّ عقدة من DOM إلى صنف معيّن من اﻷصناف المسبقة البناء (built-in class). يكون جذر التسلسل الهرميّ هو الصنف EventTarget، الذي يرث عنه الصنفُ Node، الذي يرث عنه بدوره باقي العقد في DOM. هذه صورة للتسلسل الهرمي، وسيتبعها المزيد من الشرح: تتمثّل اﻷصناف في: EventTarget -- هو صنفٌ "مجرّدٌ" (abstract) يكون عند الجذر. لا تُنشأ كائنات من هذا الصنف أبدا. وهو بمثابة القاعدة التي تُمكّن جميع العُقد في DOM من دعم من يُسمّى باﻷحداث (التي سندرسها لاحقا). Node -- هو أيضا صنفٌ "مجرّد" بمثابة القاعدة للعقد في DOM، ويوفّر الوظائف الشجريّة اﻷساسيّة مثل parentNode و nextSibling و childNodes وغيرها (التي تُسمّى جالبة - getters). لا تُنشأ كائنات من هذا الصنف، لكنّ هناك أصنافا حقيقيّة (concrete) من العُقد التي ترث هذا الصنف، منها Text للعقد النصّيّة و Element للعقد العنصريّة وأصنافٌ أخرى أغرب مثل Comment للعقد التعليقيّة. Element -- هو الصنف الأساسيّ للعناصر في DOM. يتيح توابع للتنقّل بين العناصر مثل nextElementSibling و children وتوابع للبحث مثل getElementsByTagName و querySelector. بما أنّ المتصفّح لا يدعم فقط HTML بل XML و SVG كذلك، فإنّ الصنف Element يمثّل قاعدة لأصنافٍ أخصّ مثل SVGElement و XMLElement و HTMLElement. HTMLElement -- هو الصنف اﻷساسيّ لجميع عناصر HTML ، ويرث عنه عناصرُ HTML الحقيقيّة: HTMLInputElement -- هو صنف العناصر <input>. HTMLBodyElement -- هو صنف العناصر <body>. HTMLAnchorElement -- هو صنف العناصر <a>. … إلى غير ذلك، فلكلّ وسمٍ صنفه الخاصّ الذي قد يتيح له خاصّيّات وتوابع تختصّ به. إذًا فمجموع الخاصّيّات والتوابع التي لدى عقدةٍ ما هو ناتج عن الوراثة. على سبيل المثال، لنأخذ كائن DOM لعنصر <input>. ينتمي هذا الكائن إلى صنف HTMLInputElement، ويكتسب خاصّياته وتوابعه من تراكم اﻷصناف التالية (مرتبة بحسب الوراثة): HTMLInputElement -- يتيح هذا الصنف خاصّيّات تتعلّق بالمُدخلات. HTMLElement -- يتيح توابع مشتركة بين عناصر HTML (وجوالب/ضوابط -- getters/setters). Element -- يتيح توابع عامّة للعناصر. Node -- يتيح خاصّيات مشتركة بين عقد DOM. EventTarget -- يتيح الدعم للأحداث (سنتطرق لها لاحقا). … وأخيرا هو يرث من Object ، مما يجعل توابع "الكائنات الصرفة" مثل hasOwnProperty متاحة له كذلك. لمعرفة اسم صنف العقدة في DOM، يكفي تذكّر أنّ للكائنات عادةً خاصّيّة constructor. تشير هذه الخاصّيّة إلى الدالّة البانية (constructor) الخاصّة بالصنف، و constructor.name هو اسمها: alert( document.body.constructor.name ); // HTMLBodyElement See the Pen JS-p2-05-basic-dom-node-properties-ex1 by Hsoub (@Hsoub) on CodePen. … أو يكفي مجرّد تطبيق toString عليه: alert( document.body ); // [object HTMLBodyElement] See the Pen JS-p2-05-basic-dom-node-properties-ex2 by Hsoub (@Hsoub) on CodePen. يمكن كذلك استخدام instanceof للتحقّق من الوراثة: alert( document.body instanceof HTMLBodyElement ); // true alert( document.body instanceof HTMLElement ); // true alert( document.body instanceof Element ); // true alert( document.body instanceof Node ); // true alert( document.body instanceof EventTarget ); // true See the Pen JS-p2-05-basic-dom-node-properties-ex3 by Hsoub (@Hsoub) on CodePen. كما يمكن أن نلاحظ، تُعدّ عقد DOM كائنات جافاسكربت عاديّة، وهي تعتمد في التوريث على كائن prototype. يمكن أيضا ملاحظة ذلك بسهولة من خلال عرض عنصرٍ ما بواسطة (console.dir(elem في المتصفّح. يمكنك ملاحظة كلٍّ من HTMLElement.prototype و Element.prototype وغيرها في الطرفية. (console.dir(elem مقابل (console.log(elem تدعم أدوات المطوّر في معظم المتصفحات اﻷمرين: console.log و console.dir. يعرض كلّ منهما معاملاته في الطرفيّة. بالنسبة لكائنات جافاسكربت فإنّ هذين اﻷمرين متماثلان في العادة. لكن بالنسبة لعناصر DOM فهما مختلفان: تعرض (console.log(elem شجرة DOM الخاصّة بالعنصر. تعرض (console.dir(elem العنصر على شكل كائن DOM، وتساعد على استكشاف خاصيّاته. جرّب ذلك على document.body IDL في المواصفة لا توصف أصناف DOM في المواصفة باستخدام جافاسكربت، لكن باستخدام لغة خاصة تُسمى لغة وصف الواجهة (Interface description language أو IDL باختصار)، وهي سهلة الفهم في العادة. في IDL، تُسبق جميع الخاصّيّات بنوعها. فمثلا DOMString و boolean وهكذا. إليك مقتطفا منها مع التعليقات: // HTMLInputElement تعريف // HTMLElement يرث من HTMLInputElement تعني النقطان الرئسيتان ":" أنّ interface HTMLInputElement: HTMLElement { // <input> هنا توضع خاصّيّات وتوابع العنصر // أنّ قيمة الخاصّيّة هي عبارة عن سلسلة نصيّة "DOMString" تعني attribute DOMString accept; attribute DOMString alt; attribute DOMString autocomplete; attribute DOMString value; // (true/false) خاصّيّة ذات قيمةٍ بوليانية attribute boolean autofocus; ... // أن التابع لايعيد أيّة قيمة "void" وهنا مكان التابع: تعني void select(); ... } خاصية "nodeType" تقدّم خاصّيّة "nodeType" طريقة أخرى "قديمة الطراز" للحصول على "نوع" العقدة في DOM. لهذه الخاصّيّة قيمة عددية: elem.nodeType == 1 للعقد العنصريّة. elem.nodeType == 3 للعقد النصّيّة. elem.nodeType == 9 لكائن document. هناك بعض القيم اﻷخرى في المواصفة. على سبيل المثال: <body> <script> let elem = document.body; // لنرى ما نوع هذه العقدة alert(elem.nodeType); // 1 => عنصر // ... وأوّل ابن لها هو alert(elem.firstChild.nodeType); // 3 => نصّ // 9 النوع هو ،document بالنسبة لكائن alert( document.nodeType ); // 9 </script> </body> See the Pen JS-p2-05-basic-dom-node-properties-ex4 by Hsoub (@Hsoub) on CodePen. في السكربتات الحديثة، يمكننا استخدام instanceof وغيرها من الاختبارات المعتمدة على الصنف لمعرفة نوع العقدة، لكن أحيانا قد تكون nodeType أبسط. يمكننا فقط قراءة nodeType دون التعديل عليها. الوسم: nodeName و tagName إذا كانت لدينا عقدة من DOM، فيمكننا قراءة اسم وسمها من خلال خاصّيّات nodeName و tagName. على سبيل المثال: alert( document.body.nodeName ); // BODY alert( document.body.tagName ); // BODY See the Pen JS-p2-05-basic-dom-node-properties-ex5 by Hsoub (@Hsoub) on CodePen. هل هناك فرق بين tagName و nodeName؟ بالتأكيد، يظهر الفرق في اسميهما لكنّه في الحقيقة طفيف نوعا ما. تكون خاصّيّة tagName للعقد العنصريّة فقط. تكون خاصّية nodeName مُعرّفةً في جميع العقد: بالنسبة للعناصر هي مماثلة لـ tagName. بالنسبة ﻷنواع العقد اﻷخرى (النصوص والتعليقات وغيرها) فهي سلسلة نصّيّة تحمل نوع العقدة. بعبارة أخرى، تُدعم tagName من العقد النصّيّة فقط (إذ أنّ منشأها من الصنف Element)، بينما تستطيع nodeName الإخبار عن أنواع العقد اﻷخرى. على سبيل المثال، لنوازن بين tagName و nodeName بتطبيقهما على كائن document وعلى عقدة تعليقيّة: <body><!-- comment --> <script> // بالنسبة للتعليق alert( document.body.firstChild.tagName ); // undefined (not an element) -- (أي غير معرّف (ليس عنصرا alert( document.body.firstChild.nodeName ); // #comment // document بالنسبة لـ alert( document.tagName ); // undefined (not an element) alert( document.nodeName ); // #document </script> </body> See the Pen JS-p2-05-basic-dom-node-properties-ex6 by Hsoub (@Hsoub) on CodePen. إذا اقتصرنا على التعامل مع العناصر فقط، فيمكننا استعمال كلٍّ من tagName و nodeName -- لا فرق بينهما. يكون اسم الوسم دائما باﻷحرف الكبيرة (uppercase) ماعدا في وضع XML للمتصفّح وضعان لمعالجة المستندات: HTML و XML. يُستخدم وضع HTML عادة مع صفحات الويب. يُفًّعل وضع XML عندما يستقبل المتصفّح مستند XML بالترويسة Content-Type: application/xml+xhtml. في وضع HTML، تكون tagName و nodeName دائما باﻷحرف الكبيرة: فهي BODY سواء بالنسبة لـ <body> أو <BoDy>. في وضع XML، تُترك اﻷحرف "كما هي". في الوقت الحاضر، يندر استخدام وضع XML. innerHTML: المحتويات تُمكّن innerHTML من تحصيل الـ HTML الذي بداخل العنصر على شكل سلسلة نصيّة. كما يمكننا أيضا التعديل عليه، مما يجعله من أقوى الطرق للتعديل على الصفحة. هذا المثال يبيّن كيفيّة عرض محتويات document.body ثم استبدالها كليّة: <body> <p>A paragraph</p> <div>A div</div> <script> alert( document.body.innerHTML ); // قراءة المحتويات الحاليّة document.body.innerHTML = 'The new BODY!'; // ثم استبدالها </script> </body> See the Pen JS-p2-05-basic-dom-node-properties-ex7 by Hsoub (@Hsoub) on CodePen. يمكننا محاولة إدخال HTML غير سليم، ليقوم المتصفّح بتصحيح الأخطاء: <body> <script> document.body.innerHTML = '<b>test'; // نسينا إغلاق الوسم alert( document.body.innerHTML ); // <b>test</b> (صٌلّحت) </script> </body> See the Pen JS-p2-05-basic-dom-node-properties-ex8 by Hsoub (@Hsoub) on CodePen. لا تُنفّذ السكربتات إذا أدرجت innerHTML سكربت <script> في المستند، فإنّه يصير جزءًا من HTML لكنّه لا يُنفّذ. تعيد "=+innerHTML" الكتابة كليّة يمكننا إضافة HTML إلى عنصر ما بواسطة "elem.innerHTML+="more html هكذا: chatDiv.innerHTML += "<div>Hello<img src='smile.gif'/> !</div>"; chatDiv.innerHTML += "How goes?"; لكن يجب أن ننتبه عند فعل ذلك، فما يحدث فعلا ليست مجرّد إضافة، بل هي إعادة كتابة بالكليّة. ظاهريّا، يفعل هذان السطران نفس الشيء: elem.innerHTML += "..."; // :هو طريقة مختصرة لكتابة elem.innerHTML = elem.innerHTML + "..." بعبارة أخرى، ما تقوم به =+innerHTML هو التالي: يُحذف المحتوى القديم. يُكتب في مكانه innerHTML الجديد (الذي هو سلسلة متكوّنة من المحتوى القديم والجديد). نتيجةً "لتصفير" المحتوى وإعادة كتابته من جديد، فإنّه يُعاد تحميل جميع الصّور وغيرها من الموارد. في مثال chatDiv أعلاه، يعيد السطر ?"chatDiv.innerHTML+="How goes إنشاء محتوى HTML ثمّ يعيد تحميل الصّورة smile.gif (يُأمل أن تكون مخبّأةً في الذاكرة المؤقتة - Cache). إذا كان لدى chatDiv الكثير من النصوص والصور اﻷخرى، فإنّ إعادة التحميل تصير ظاهرة للعيان. هناك آثار جانبية أخرى كذلك. على سبيل المثال، إذا كان النصّ الموجود قد حُدّد بواسطة الفأرة، فإنّ معظم المتصفّحات ستزيل التحديد عنه عند إعادة كتابة innerHTML . كما أنّه إذا كان هناك <input> فيها نصّ أدخله الزائر، فإنّ النصّ سيُزال أيضا. إلى غير ذلك. لحسن الحظّ، هناك طرق أخرى لإضافة HTML عدا innerHTML ، سندرسها قريبا. outerHTML: كامل HTML العنصر تحتوي خاصّيّة outerHTML على كامل HTML العنصر. تشبه هذه الخاصّيّة innerHTML لكن بالإضافة إلى العنصر نفسه. إليك مثالا: <div id="elem">Hello <b>World</b></div> <script> alert(elem.outerHTML); // <div id="elem">Hello <b>World</b></div> </script> See the Pen JS-p2-05-basic-dom-node-properties-ex9 by Hsoub (@Hsoub) on CodePen. انتبه: على خلاف innerHTML، لا يؤدي التعديل على outerHTML إلى تعديل العنصر، بل سيؤدي ذلك إلى استبداله في DOM. نعم، يبدو ذلك غريبا، وهو فعلا كذلك. لهذا خصصنا له تنبيها منفصلا هنا. ألقِ نظرة على هذا المثال: <div>Hello, world!</div> <script> let div = document.querySelector('div'); // <p>...</p> بـ div.outerHTML استبدال div.outerHTML = '<p>A new element</p>'; // (*) // على حاله 'div' ياللجعب! بقي alert(div.outerHTML); // <div>Hello, world!</div> (**) </script> See the Pen JS-p2-05-basic-dom-node-properties-ex10 by Hsoub (@Hsoub) on CodePen. يبدو ذلك غريبا، صحيح؟ في السطر (*)، استبدلنا <div> بـ <p>A new element</p>. في المستند الخارجي (DOM)، يمكننا ملاحظة المحتوى الجديد بدل <div>. لكن كما يمكن الملاحظة في السطر (**)، لم تتغيّر قيمة المتغيّر القديم div! لا يغيّر الإسنادُ outerHTML العنصرَ في DOM (في هذا المثال، الكائن الذي يشير إليه المتغير 'div')، لكنّه يحذفه من DOM ويدرج مكانه الـ HTML الجديد. فالذي حصل بفعل ...=div.outerHTML هو ما يلي: حُذف div من المستند. أُدرج في مكانه قطعة HTML أخرى <p>A new element</p> . لا يزال لـ div نفس القيمة. ولم يُحفظ HTML الجديد في أيّ متغيّر. من السهل جدّا ارتكاب خطأ هنا: التعديل على div.outerHTML ثمّ مواصلة العمل بـ div وكأنّ فيه المحتوى الجديد. لكنّه ليس كذلك. ينطبق هذا على innerHTML لكن ليس على outerHTML. بإمكاننا التعديل على elem.outerHTML، لكن ينبغي التنبّه إلى أنّ ذلك لا يغيّر العنصر 'elem'، بل يضع مكانه HTML الجديد. يمكننا الحصول على مراجع للعناصر الجديدة من خلال البحث في DOM. nodeValue/data: محتوى العقد النصية لا تصلح الخاصّيّة innerHTML إلّا مع العقد العنصريّة. بالنسبة للعقد الأخرى، كالعقد النصّيّة، يقابل ذلك خاصّيّات nodeValue و data. هاتان الخاصّيّتان متماثلتان تقريبا في التطبيق، مع بعض الفروق الطفيفة في المواصفة. لذا سنستعمل data ﻷنّها أقصر. هذا مثالٌ لقراءة محتوى عقدة نصّيّة وتعليق: <body> Hello <!-- Comment --> <script> let text = document.body.firstChild; alert(text.data); // Hello let comment = text.nextSibling; alert(comment.data); // Comment </script> </body> See the Pen JS-p2-05-basic-dom-node-properties-ex11 by Hsoub (@Hsoub) on CodePen. يمكننا تصوّر دافعٍ لقراءة وتعديل العقد النصّيّة، لكن لماذا التعليقات؟ يُضمّن فيها المطوّرون أحيانا معلومات أو تعليمات متعلّقة بالقالب في HTML، على هذا النحو: <!-- if isAdmin --> <div>Welcome, Admin!</div> <!-- /if --> … فيمكن لجافاسكربت قراءتها من خلال خاصّيّة data ثمّ معالجة التعليمات التي تتضمّنها. textContent: نص محض تتيح خاصّية textContent الوصول إلى النصّ الذي بداخل العنصر: مجرّد النصّ، دون أيّة وسوم. على سبيل المثال: <div id="news"> <h1>Headline!</h1> <p>Martians attack people!</p> </div> <script> // Headline! Martians attack people! alert(news.textContent); </script> See the Pen JS-p2-05-basic-dom-node-properties-ex12 by Hsoub (@Hsoub) on CodePen. كما يمكن أن نلاحظ، أُعيد النصّ فقط، وكأنّ جميع الـوسوم قُطعت، وبقي النصّ الذي بداخلها. عمليّا، يندر أن يُحتاج إلى قراءة مثل هذا النصّ. يعدّ التعديل على textContent مفيدا أكثر بكثير، لأنّه يمكّن من كتابة النصوص على "النحو الآمن". لنتفرض أن لدينا سلسلة نصّيّة ما، أدخلها مستخدمٌ مثلا، ونريد أن نعرضها. باستخدام innerHTML سندرجها "على شكل HTML"، بجميع وسوم HTML. باستخدام textContent سندرجها "على شكل نصّ"، وتُعامل جميع الرموز معاملة حرفيّة. قارن بين الإثنين: <div id="elem1"></div> <div id="elem2"></div> <script> let name = prompt("What's your name?", "<b>Winnie-the-pooh!</b>"); elem1.innerHTML = name; elem2.textContent = name; </script> See the Pen JS-p2-05-basic-dom-node-properties-ex13 by Hsoub (@Hsoub) on CodePen. يحصُل <div> اﻷوّل على الاسم (name) "على شكل HTML": جميع الوسوم تُعامل كوسوم، ونرى بذلك الاسم بخطّ عريض (bold). يحصُل <div> الثاني على الاسم "على شكل نصّ"، فنرى حرفيًّا <b>Winnie-the-pooh!</b>. في أغلب الحالات، نتوقّع من المستخدم أن يدخل نصًّا، ونريد أن نعامله كنصّ. لا نريد HTML غير متوقّع في موقعنا. وهذا ما يفعله الإسناد إلى textContent بالضبط. خاصية 'hidden' تُحدّد السّمة "hidden" وخاصيّة DOM التي تحمل نفس الاسم ما إذا كان العنصر مرئيّا أو لا. بإمكاننا استخدامها في HTML أو إسنادها في جافاسكربت كالتالي: <div>Both divs below are hidden</div> <div hidden>With the attribute "hidden"</div> <div id="elem">JavaScript assigned the property "hidden"</div> <script> elem.hidden = true; </script> See the Pen JS-p2-05-basic-dom-node-properties-ex14 by Hsoub (@Hsoub) on CodePen. مبدئيّا، تعمل hidden تماما مثل "style="display:none ، لكنّها أقصر في الكتابة. إليك عنصرا وامضًا: <div id="elem">A blinking element</div> <script> setInterval(() => elem.hidden = !elem.hidden, 1000); </script> See the Pen JS-p2-05-basic-dom-node-properties-ex15 by Hsoub (@Hsoub) on CodePen. المزيد من الخاصيات للعناصر في DOM المزيد من الخاصّيّات، خصوصا تلك التي تتعلق باﻷصناف: value -- القيمة التي في <input> و <select> و <textarea> (المتعلقة بـ HTMLInputElement و HTMLSelectElement …). href -- قيمة "href" في <"..."=a href> (المتعلقة بـ HTMLAnchorElement). id -- قيمة السمة "id" لجميع العناصر (المتعلقة بـHTMLElement). … والمزيد من ذلك… على سبيل المثال: <input type="text" id="elem" value="value"> <script> alert(elem.type); // "text" alert(elem.id); // "elem" alert(elem.value); // value </script> See the Pen JS-p2-05-basic-dom-node-properties-ex16 by Hsoub (@Hsoub) on CodePen. تقابل معظم السمات القياسية في HTML خاصّيات في DOM، ويمكننا الوصول إليها بهذه الطريقة. إذا أردنا معرفة جميع الخاصّيّات المدعومة في صنف معيّن، يمكننا الاطلاع عليها في المواصفة. على سبيل المثال، HTMLInputElement موثّقة هنا. أمّا إذا أردنا الحصول عليها بسرعة، أو كنّا مهتمين بمواصفة ملموسة في المتصفّح، يمكننا استعراض العنصر بواسطة (console.dir(elem لقراءة الخاصّيّات. أو يمكن كذلك استكشاف "خاصّيّات DOM" في لسان العناصر (Elements) في أدوات المطوّر في المتصفّح. الملخص تنتمي كلّ عقدة في DOM إلى صنف معيّن. تشكّل اﻷصناف تسلسلا هرميّا. يكون مجموع الخاصّيّات والتوابع التي لدى عقدة ما ناتجا عن الوراثة. خاصّيّات عقد DOM الرئيسيّة هي: nodeType: يمكننا استعمالها لمعرفة ما إذا كانت العقدة نصيّة أو عنصريّة. لها القيمة العددية: 1 للعناصر و 3 للعقد النصّيّة، وبعض القيم اﻷخرى لباقي أنواع العقد. يمكن قراءتها فقط. nodeName/tagName: للعناصر tagName (تُكتب باﻷحرف الكبيرة ماعدا في وضع XML)، وللعقد غير العنصريّة nodeName. تُبيّن ماهية العقدة. يمكن قراءتها فقط. innerHTML: ما يحتويه العنصر من HTML. يمكن التعديل عليها. outerHTML: كامل HTML العنصر. لا تمسّ عمليّة التعديل على elem.outerHTML العنصر elem نفسه، لكن تستبدله بالـ HTML الجديد في DOM. nodeValue/data: محتوى العقد غير العنصريّة (النصوص والتعليقات). هاتان الخاصّيّتان متماثلتان تقريبا، لكن تُستعمل في العادة data . يمكن التعديل عليها. textContent: النصّ الذي بداخل العنصر: HTML منزوع الوسوم. يضع التعديلُ عليه النصَّ بداخل العنصر، مع معاملة جميع المحرّفات الخاصّة والوسوم كمجرّد نصّ. تُمكّن من إدراج النصوص التي يُدخلها المستخدمون بطريقة آمنة تحمي من إدراج HTML غير مرغوب فيه. hidden: عند إعطائها القيمة true تؤدّي نفس وظيفة display:none في CSS. تملك عقد DOM المزيد من الخاصّيّات بحسب الصنف الذي تنتمي إليه. على سبيل المثال، تدعم العناصر <input> (ذات الصنف HTMLInputElement) كلاّ من value و type ، بينما تدعم <a> (ذات الصنف HTMLAnchorElement) الخاصيّة href ، إلى غير ذلك. لمُعظم السمات القياسية في HTML خاصيّة تقابلها في DOM. لكن مع ذلك، لا تتساوى سمات HTML وخاصيّات DOM على الدوام، كما سنرى في المقال التالي. التمارين حساب الفروع اﻷهميّة: 5 هناك شجرة متشكّلة من تفرّع العناصر ul/li. اكتب الشفرة التي من أجل كل <li> تستعرض التالي: النصّ الذي بداخله (دون الشجرة الفرعيّة). عدد الـ <li> المتفرّعة عنه -- جميع العقد السليلة، بما في ذلك العقد المتفرّعة عن فروعه إلى آخر ذلك. افتح المثال في نافذة مستقلة افتح البيئة التجريبيّة لإنجاز التمرين الحل لنعمل حلقة تكراريّة حول جميع الـ <li>: for (let li of document.querySelectorAll('li')) { ... } نحتاج في الحلقة أن نحصل على النصّ بداخل كلّ li. يمكننا قراءة النصّ الذي بداخل أوّل العقد اﻷبناء لـ li ، والتي هي العقدة النصّيّة: for (let li of document.querySelectorAll('li')) { let title = li.firstChild.data; // الذي يأتي قبل باقي العقد <li> هو النصّ داخل title } بعدها يمكننا الحصول على عدد العقد السليلة باستخدام li.getElementsByTagName('li').length. افتح الحلّ في البيئة التجريبيّة مالذي بداخل nodeType؟ الأهميّة: 5 ماذا يعرض السكربت التالي: <html> <body> <script> alert(document.body.lastChild.nodeType); </script> </body> </html> الحل توجد خدعة هنا. في الوقت الذي يُنفّذ فيه <script> ، تكون آخر العقد في DOM هي <script> نفسه، ﻷنّ المتصفّح لم يكن قد عالج بقيّة الصفحة بعد. إذا الجواب هو 1 (عقدة عنصريّة). <html> <body> <script> alert(document.body.lastChild.nodeType); </script> </body> </html> وسم في التعليق اﻷهميّة: 3 ماذا تعرض هذه الشفرة: <script> let body = document.body; body.innerHTML = "<!--" + body.tagName + "-->"; alert( body.firstChild.data ); // ماذا هنا؟ </script> الحل الجواب هو، BODY <script> let body = document.body; body.innerHTML = "<!--" + body.tagName + "-->"; alert( body.firstChild.data ); // BODY </script> الذي يحصل خطوة بخطوة: يُستبدل محتوى <body> بالتعليق. التعليق هو <--BODY--!> ، لأنّ "body.tagName == "BODY. كما نذكر، يٌكتب tagName دائما بالأحرف الكبيرة في HTML. التعليق الآن هو الابن الوحيد، فنحصل عليه بواسطة body.firstChild. تكون خاصّيّة data بالنسبة للتعليق هي محتواه (ما بداخل <--...--!>): "BODY". أين يقع "المستند" في التسلسل الهرمي: اﻷهميّة: 4 ما هو الصنف الذي ينتمي إليه document ؟ ما هو مكانه في التسلسل الهرميّ لـ DOM ؟ هل يرث من Node أو Element أو ربّما HTMLElement ؟ الحل يمكننا معرفة الصنف الذي ينتمي إليه من خلال عرضها هكذا: alert(document); // [object HTMLDocument] أو: alert(document.constructor.name); // HTMLDocument إذًا document هو نسخة من الصنف HTMLDocument. ما هو مكانه في التسلسل الهرميّ ؟ نعم، يمكننا تصفّح المواصفة، لكن سيكون أسرع لو اكتشفنا ذلك يدويّا. لنجتز سلسلة prototype بواسطة __proto__. كما نعلم، توجد توابع الصنف في prototype الدالة البانية. على سبيل المثال، توجد توابع المستندات في HTMLDocument.prototype. بالإضافة إلى ذلك، هناك مرجعٌ إلى الدالة البانية بداخل prototype: alert(HTMLDocument.prototype.constructor === HTMLDocument); // true للحصول على اسم الصنف على شكل سلسلة نصيّة، يمكننا استخدام constructor.name. لنفعل ذلك مع جميع سلسلة prototype الخاصّة بـ document إلى غاية الصنف Node: alert(HTMLDocument.prototype.constructor.name); // HTMLDocument alert(HTMLDocument.prototype.__proto__.constructor.name); // Document alert(HTMLDocument.prototype.__proto__.__proto__.constructor.name); // Node هذا هو التسلسل الهرميّ. يمكننا كذلك تفحّص الكائن باستخدام (console.dir(document ثم معرفة هذه اﻷسماء من خلال فتح __proto__. تأخذ الطرفيّة هذه اﻷسماء من constructor داخليّا. ترجمة وبتصرف لمقال Node properties: type, tag and contents من كتاب The Modern JavaScript Tutorial.
  23. تُفيد خصائص التنقّل في DOM كثيرا عندما تكون العناصر قريبة من بعضها البعض. لكن ماذا لو لم تكن كذلك؟ كيف يمكن تحصيل عنصرٍ ما على الصفحة؟ هناك المزيد من توابع البحث لهذا الغرض. document.getElementById أو فقط id إذا كان للعنصر سمة id، فيمكننا تحصيله باستخدام التابع (document.getElementById(id، أينما وُجد. على سبيل المثال: <div id="elem"> <div id="elem-content">Element</div> </div> <script> // تحصيل العنصر let elem = document.getElementById('elem'); // تلوين خلفيّته باﻷحمر elem.style.background = 'red'; </script> See the Pen JS-p2-04-searching-elements-dom-ex01 by Hsoub (@Hsoub) on CodePen. بالإضافة إلى هذا، يمكن الإشارة إلى العنصر بواسطة متغيّر عامّ (global variable) اسمه نفس قيمة الـ id. <div id="elem"> <div id="elem-content">Element</div> </div> <script> // id="elem" الذي سمته DOM إلى عنصر elem يشير المتغيّر elem.style.background = 'red'; // واصلة في وسطه، لذا فلا يمكن أن يكون اسمًا لمتغير id="elem-content"لدى // window['elem-content'] لكن يمكن الوصول إليه بواسطة اﻷقواس المربعة... </script> See the Pen JS-p2-04-searching-elements-dom-ex02 by Hsoub (@Hsoub) on CodePen. … هذا إذا لم نصرّح بمتغيّر جافاسكربت له نفس الاسم، فإنّ اﻷولويّة حينها تكون له: <div id="elem"></div> <script> let elem = 5; // <div id="elem"> هو 5، وليس إشارة إلى elem يكون الآن alert(elem); // 5 </script> See the Pen JS-p2-04-searching-elements-dom-ex03 by Hsoub (@Hsoub) on CodePen. يُرجى عدم استخدام المتغيّرات العامّة المسمّات على قيم الـ id للوصول إلى العناصر هذا السلوك مُبيّن في المواصفة، مما يجعله وفق المعايير نوعا ما. لكنّه مدعوم في الغالب بغرض التوافق (compatibility). يحاول المتصفّح مساعدتنا بالمزج بين مجالات اﻷسماء (namespaces) في جافاسكربت و DOM. لا بأس بذلك في النصوص البرمجيّة البسيطة المضّمنة في HTML، لكن لا يُنصح به في العموم. فقد يؤدي ذلك إلى تناقضات في التسمية. كما أنّه عند قراءة شفرة جافاسكربت دون النظر إلى HTML، قد لا يتضّح من أين يأتي المتغيّر. نستعمل هنا في هذا المقال id للإشارة مباشرة إلى عنصر ما بغرض الاختصار، عندما يكون واضحا من أين يأتي العنصر. في التطبيقات الواقعيّة، تُعدّ document.getElementById هي الطريقة المفضّلة. يجب أن يكون الـ id فريدًا يجب أن يكون الـ id فريدًا. لا يمكن أن يحمل أكثر من عنصر في المستند نفس الـ id. إذا كانت هناك عدّة عناصر لها نفس الـ id ، فإنّه لا يمكن التنبؤ بسلوك التوابع التي تستخدمها. فقد تُرجع مثلا document.getElementById أيًّا من العناصر عشوائيًّا. لذا يُرجى الالتزام بالقاعدة وإبقاء الـ id فريدا. document.getElementById فقط، وليس anyElem.getElementById يمكن استدعاء التابع getElementById على الكائن document فقط. يبحث هذا التابع عن الـ id المراد في المستند بأكمله. querySelectorAll يُعدّ (css)elem.querySelectorAll أكثر التوابع تنوّعا في الاستخدام على الإطلاق. يعيد هذا التابع جميع العناصر التي ينطبق عليها محدّد selector) CSS) معيّن. نبحث في اﻷسفل مثلا عن جميع العناصر <li> التي هي آخر اﻷبناء: <ul> <li>The</li> <li>test</li> </ul> <ul> <li>has</li> <li>passed</li> </ul> <script> let elements = document.querySelectorAll('ul > li:last-child'); for (let elem of elements) { alert(elem.innerHTML); // "test", "passed" } </script> See the Pen JS-p2-04-searching-elements-dom-ex04 by Hsoub (@Hsoub) on CodePen. هذا التابع قويّ بالفعل، ﻷنّه يمكن معه استخدام أيّ محدّد CSS. يمكن استخدام أشباه اﻷصناف كذلك يدعم محدّد CSS أيضا أشباه اﻷصناف (pseudo-class) مثل hover: و active:. على سبيل المثال، يعيد ('document.querySelectorAll(':hover مجموعة العناصر التي يوجد عليها المؤشّر حاليّا (حسب ترتيب تفرعها: بداية من <html> إلى آخر فرع منها). querySelector يعيد استدعاء (elem.querySelector(css أوّل العناصر التي ينطبق عليها محدّد CSS. بعبارة أخرى، هي نفس نتيجة [elem.querySelectorAll(css)[0 ، لكنّ هذه اﻷخيرة تبحث عن جميع العناصر وتختار بعد ذلك أوّلها، بينما تبحث elem.querySelector عن عنصر واحد فقط. فهي بذلك أسرع في البحث وأقصر في الكتابة. matches تقوم التوابع السابقة بالبحث في DOM. لا يقوم (elem.matches(css بالبحث عن أيّ شيء، بل يتحقّق فقط من أنّ العنصر elem يطابق محدّد CSS المراد، ويعيد إمّا true أو false. يُفيد هذا التابع عند المرور على عدد من العناصر (في مصفوفة أو شيء من هذا القبيل) ونريد ترشيح العناصر التي تهمّنا فقط. على سبيل المثال: <a href="http://example.com/file.zip">...</a> <a href="http://ya.ru">...</a> <script> // document.body.children يمكن تطبيقها على أيّة مجموعة مكان for (let elem of document.body.children) { if (elem.matches('a[href$="zip"]')) { alert("The archive reference: " + elem.href ); } } </script> See the Pen JS-p2-04-searching-elements-dom-ex05 by Hsoub (@Hsoub) on CodePen. closest يتمثّل أسلاف (ancestors) عنصر ما في أبيه، وأب أبيه، وهكذا. يشكّل جميع اﻷسلاف معًا سلسلة الآباء التي تبتدئ من العنصر وتنتهي عند القمّة. يبحث تابع (elem.closest(css عن أقرب سلفٍ ينطبق عليه محدّد CSS. يشمل البحثُ العنصرَ elem نفسَه. بعبارة أخرى، ينطلق التابع closest صعودًا من العنصر المراد ويفحص كلّا من الآباء. فإذا طابق أحد اﻷسلاف المُحدِّد يتوقّف البحث، ويعيدُ السلفَ المطابق. على سبيل المثال: <h1>Contents</h1> <div class="contents"> <ul class="book"> <li class="chapter">Chapter 1</li> <li class="chapter">Chapter 1</li> </ul> </div> <script> let chapter = document.querySelector('.chapter'); // LI alert(chapter.closest('.book')); // UL alert(chapter.closest('.contents')); // DIV alert(chapter.closest('h1')); // (ليس من اﻷسلاف h1 ﻷن) null </script> See the Pen JS-p2-04-searching-elements-dom-ex06 by Hsoub (@Hsoub) on CodePen. *getElementsBy هناك أيضا توابع أخرى للبحث عن العقد حسب الوسم (tag) والصنف (class) وغيرها. تُعدّ هذه التوابع غالبًا من الماضي، إذ أنّ querySelector أقوى وأقصر في الكتابة. إذًا سنذكرها هنا من باب التمام، كما أنّه لا يزال من الممكن إيجادها في النصوص البرمجيّة القديمة. يبحث (elem.getElementsByTagName(tag عن العناصر التي لها نفس الوسم المُراد ويعيدهم جميعا على شكل مجموعة. يمكن للمعامل tag أن يكون نجمة أيضا "*" ليشمل "جميع الوسوم". يعيد (elem.getElementsByClassName(className العناصر التي لها نفس صنف CSS المراد. يعيد (document.getElementsByName(name كل العناصر التي في المستند التي لها نفس السمة name. يندر استعمال هذا التابع. على سبيل المثال: // التي في المستند div حصّل جميع وسوم let divs = document.getElementsByTagName('div'); لنجد جميع وسوم input التي في المستند: <table id="table"> <tr> <td>Your age:</td> <td> <label> <input type="radio" name="age" value="young" checked> less than 18 </label> <label> <input type="radio" name="age" value="mature"> from 18 to 50 </label> <label> <input type="radio" name="age" value="senior"> more than 60 </label> </td> </tr> </table> <script> let inputs = table.getElementsByTagName('input'); for (let input of inputs) { alert( input.value + ': ' + input.checked ); } </script> See the Pen JS-p2-04-searching-elements-dom-ex07 by Hsoub (@Hsoub) on CodePen. لا تنسَ حرف "s" للجمع! ينسى المطوّرون المبتدئون أحيانا الحرف "s"، فيعمدون إلى مناداة getElementByTagName بدل getElement<b>s</b>ByTagName. يخلو getElementByTagName من الحرف "s" ﻷنّه يعيد عنصرا وحيدا. لكنّ getElement<b>s</b>ByTagName يعيد مجموعة من العناصر، لهذا فإنّ بداخله "s". يعيد التابع مجموعة، وليس عنصرًا! من الأخطاء الشائعة لدى المبتدئين أيضا هو كتابة: // لا تعمل document.getElementsByTagName('input').value = 5; لن يعمل ذلك، ﻷنّه يحاول إسناد القيمة إلى مجموعةٍ من المُدخلات بدل إسنادها إلى العناصر التي تحتويها. يجب علينا إمّا التكرار على المجموعة أو تحصيل عنصر بواسطة معامله ثمّ إسناد القيمة له هكذا: // (ًيُفترض أن تعمل (إذا كانت هناك مُدخلات document.getElementsByTagName('input')[0].value = 5; للبحث عن العناصر ذات الصنف article.: <form name="my-form"> <div class="article">Article</div> <div class="long article">Long article</div> </form> <script> // name أوجد حسب السمة let form = document.getElementsByName('my-form')[0]; // أوجد حسب الصنف داخل النموذج let articles = form.getElementsByClassName('article'); alert(articles.length); // 2 - "article" وجدنا عنصران لهما الصنف </script> See the Pen JS-p2-04-searching-elements-dom-ex08 by Hsoub (@Hsoub) on CodePen. المجموعات الحية تُعيد جميع التوابع "*getElementsBy" مجموعة حيّة (live). تعكس هذه المجموعات الوضع الحاليّ الذي عليه المستند و "تُحدَّث تلقائيًّا" كلّما تغيّر. في المثال أدناه نصّان برمجيّان: يُنشئ اﻷوّل مرجعًا إلى مجموعة العناصر <div>؛ طول المجموعة عندها هو 1. يُنفَّذ النصّ الثاني بعدما يقابل المتصفّح عنصر <div> آخر، ويكون طول المجموعة حينها هو 2. <div>First div</div> <script> let divs = document.getElementsByTagName('div'); alert(divs.length); // 1 </script> <div>Second div</div> <script> alert(divs.length); // 2 </script> See the Pen JS-p2-04-searching-elements-dom-ex09 by Hsoub (@Hsoub) on CodePen. في المقابل، يعيد querySelectorAll مجموعة ساكنة (static)، كأنّها مصفوفة ثابتة من العناصر. إذا طبقناه في المثال أعلاه، فسيُخرج كلا النصّان 1: <div>First div</div> <script> let divs = document.querySelectorAll('div'); alert(divs.length); // 1 </script> <div>Second div</div> <script> alert(divs.length); // 1 </script> See the Pen JS-p2-04-searching-elements-dom-ex10 by Hsoub (@Hsoub) on CodePen. يمكننا الآن رؤية الفرق بوضوح. لم تزدد المجموعة الساكنة بظهور عنصر <div> جديد في المستند. الملخص هناك 6 توابع رئيسيّة للبحث عن العقد في DOM: التابع يبحث بواسطة ... يمكن مناداته على عنصر؟ حيّة؟ querySelector محدّد CSS ✔ - querySelectorAll محدّد CSS ✔ - getElementById id - - getElementsByName name - ✔ getElementsByTagName الوسم أو '*' ✔ ✔ getElementsByClassName الصنف ✔ ✔ 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; } تُعدّ querySelector و querySelectorAll أكثرها استخداما على الإطلاق، لكن قد تُفيد *getElementBy أحيانا أو قد توجد في النصوص البرمجيّة القديمة. عدا ذلك: هناك (elem.matches(css للتحقّق من أنّ العنصر elem مطابق لمحدّد CSS المراد. هناك (elem.closest(css للبحث عن أقرب سلفٍ مطابق لمحدّد CSS المراد. يشمل البحثُ العنصرَ نفسه. ولنضف هنا تابعا آخر للتحقّق من العلاقة ابن-أب، إذ قد تفيد أحيانا: يعيد (elemA.contains(elemB القيمة true إذا كان elemB داخلا تحت elemA (أي عنصرا سليلا لـ elemA) أو إذا كان elemA==elemB. التمارين البحث عن العناصر اﻷهميّة: 4 إليك مستندًا يحتوي على جدول ونموذج. كيف يمكن إيجاد؟ … الجدول الذي له "id="age-table. جميع العناصر label بداخل ذلك الجدول (من المفترض أن تكون هناك 3 منها). أوّل td في ذلك الجدول (التي فيها الكلمة "Age"). النموذج form الذي له "name="search. أوّل input في ذلك النموذج. آخر input في ذلك النموذج افتح table.html في نافذة مستقلة واستعمل أدوات المتصفّح لذلك. الحل هناك عدّة طرق لفعل ذلك. هذه إحداها: // id="age-table" الجدول الذي له let table = document.getElementById('age-table') // بداخل ذلك الجدول label جميع العناصر table.getElementsByTagName('label') // أو document.querySelectorAll('#age-table label') // ("Age" في ذلك الجدول (التي فيها الكلمة td أوّل table.rows[0].cells[0] // أو table.getElementsByTagName('td')[0] // أو table.querySelector('td') // name="search" النموذج الذي له // name="search" على افتراض أنّ هناك عنصرا واحدا في المستند له let form = document.getElementsByName('search')[0] // خصّيصا form ،أو document.querySelector('form[name="search"]') // في ذلك النموذج input أوّل form.getElementsByTagName('input')[0] // أو form.querySelector('input') // في ذلك النموذج input آخر let inputs = form.querySelectorAll('input') // inputs أوجد جميع الـ inputs[inputs.length-1] // اختر آخرها ترجمة وبتصرف لمقال *Searching: getElement*, querySelector من كتاب The Modern JavaScript Tutorial
  24. يُمكّننا DOM من فعل أيّ شيء بالعناصر وما تحتويه، لكن نحتاج أوّلا إلى أن نصل إلى الكائن المحدّد من DOM. تبدأ جميع العمليّات على DOM بالكائن document. فهو "نقطة الدخول" الرئيسيّة إلى DOM، ويمكن من خلاله الوصول إلى جميع العقد. تمثّل الصورة التالية الروابط التي يمكن من خلالها التنقّل بين العقد في DOM. لنتناولها بمزيدٍ من التفصيل. في اﻷعلى: documentElement و body تُتاح العقد العلويّة للشجرة مباشرةً على شكل خاصّيّات لكائن document: <html> =‏ document.documentElement: تكون العقدة التي في قمّة الشجرة document.documentElement ، وهي عقدة DOM التي تمثّل وسم <html>. <body> =‏ document.body: من العقد التي يكثر استعمالاها أيضا عنصر <body> الذي يمثّله كائن document.body <head> =‏ document.head: يكون وسم <head> متاحا كـ document.head. هناك خدعة: قد يكون document.body عديم القيمة (أي null) لا يستطيع السكربت الوصول إلى عنصر غير موجود حال تنفيذه. فمثلا، إذا كان السكربت بداخل <head> فإنّ document.body غير متاح له، ﻷن المتصفّح لم يبلغ موضعه بعد حتى يقرأه. ففي المثال أدناه، يعرض لنا الـ alert اﻷوّل القيمة العدميّة null: <html> <head> <script> alert( "From HEAD: " + document.body ); // بعد <body> فليس هناك ،null </script> </head> <body> <script> alert( "From BODY: " + document.body ); // فهو الآن موجود ،HTMLBodyElement </script> </body> </html> في عالم DOM، تعني null أنّه "غير موجود" في DOM، تعني القيمة null أنّه "غير موجود" أو "لا وجود لهذه العقدة". اﻷبناء: lastChild ، firstChild ، childNodes هناك مصطلحان سنستعملهما من الآن فصاعد: العقد اﻷبناء (أو اﻷبناء باختصار) -- العناصر الذين هم أبناء مباشرون، أو بعبارة أخرى، هي العناصر المتفرعة عن العنصر المراد مباشرة. على سبيل المثال، العنصران <head> و <body> هما أبناء لعنصر <html>. العقد السليلة (descendants) -- جميع العناصر المتفرعة عن العنصر المراد، بما ذلك أبناؤه وأبناء أبنائه، إلى آخر ذلك. فمثلا هنا، للعنصر <body> اﻷبناء <div> و <ul> (وبعض العقد المتكوّنة من فراغات): <html> <body> <div>Begin</div> <ul> <li> <b>Information</b> </li> </ul> </body> </html> … وأمّا العقد السليلة لـ <body> فليست أبناءه المباشرين <div> و <ul> فحسب، بل تشمل كلّ العقد المتفرّعة منه، مثل <li> (الذي هو ابنً لـ <ul>) و <b> (الذي هو ابنٌ لـ <div>) -- الشجرة الفرعيّة بأكملها. تضمّ المجموعة childNodes كل العقد اﻷبناء، بما في ذلك العقد النصيّة. يقوم المثال أدناه بعرض أبناء document.body: <html> <body> <div>Begin</div> <ul> <li>Information</li> </ul> <div>End</div> <script> for (let i = 0; i < document.body.childNodes.length; i++) { alert( document.body.childNodes[i] ); // Text, DIV, Text, UL, ..., SCRIPT } </script> ...المزيد من اﻷشياء... </body> </html> See the Pen JS-p2-03-dom-navigation-ex01 by Hsoub (@Hsoub) on CodePen. يُرجى الانتباه ﻷمر دقيق هنا. لو أجرينا المثال السابق، سيكون <script> هو آخر العناصر التي تُعرض. في حقيقة اﻷمر، لاتزال هناك المزيد من اﻷشياء في المستند، لكن السكربت لم يرها، ﻷنّه حال تنفيذه، لم يكن المتصفّح قد اطّلع عليها بعد. تُمكّن الخاصّيات firstChild و lastChild الوصول بسهولة إلى أوّل اﻷبناء وآخرهم. هي فقط بمثابة اختصارات. إذا كان للعنصر elem عقدً أبناء، فإنّ العبارات التالية دائما صحيحة: elem.childNodes[0] === elem.firstChild elem.childNodes[elem.childNodes.length - 1] === elem.lastChild هناك أيضا دالّة خاصّة ()elem.hasChildNodes للتحقّق من أنّ له أبناءً. مجموعات DOM كما نلاحظ، تبدو childNode وكأنّها مصفوفة (array)، لكنّ الحقيقة أنّها ليست كذلك، بل هي باﻷحرى مجموعة (collection) -- كائن خاصّ شبيهً بالمصفوفة وقابل للتكرار عليه (iterable). ينجم عن هذا اﻷمر أثران مهمّان: يمكننا استعمال for..of للتكرار عليه: for (let node of document.body.childNodes) { alert(node); // يعرض هذا جميع العقد التي في المجموعة } See the Pen JS-p2-03-dom-navigation-ex02 by Hsoub (@Hsoub) on CodePen. هذا ﻷنّه قابل للتكرار (أي أنّه يتيح خاصّيّة Symbol.iterator كما يلزم). لا يمكننا استعمال توابع المصفوفة معها، ﻷنّها ليست مصفوفة: alert(document.body.childNodes.filter); // (filter غير معرّف (ليس هناك تابع اسمه الأمر اﻷوّل جيّد. كما أنّه لا بأس باﻷمر الثاني، ﻷنّه يمكننا استعمال Array.from ﻹنشاء مصفوفة "حقيقيّة" من المجموعة، إذا رغبنا في توابع المصفوفة: alert( Array.from(document.body.childNodes).filter ); // function See the Pen JS-p2-03-dom-navigation-ex03 by Hsoub (@Hsoub) on CodePen. مجموعات DOM هي للقراءة فقط مجموعات DOM، بل زد على ذلك، جميع خاصّيّات التنقّل التي ذكرناها في هذا المقال هي للقراءة فقط (read-only). فلا يمكن مثلا استبدال ابنٍ بشيء آخر بواسطة الإسناد ... = [childNodes[i. يتطلب تغيير DOM توابع أخرى سنتناولها في المقال التالي. مجموعات DOM حيّة كلّ مجموعات DOM تقريبا، مع بعض الاستثناءات القليلة، هي حيّة (live). ما يعني ذلك أنّها تعكس الوضع الحاليّ لـ DOM. إذا احتفظنا بمرجع للمجموعة elem.childNodes ، وأضفنا أو أزلنا عقدا من DOM، فإنّها تظهر في المجموعة تلقائيّا. لا تستعمل for..in للتكرار على المجموعات رغم أنّ المجموعات تقبل التكرار بواسطة for..in، لكنّه يُفضّل عدم استعمالها لذلك. تمرّ for..in على جميع الخاصّيّات التي يمكن تعدادها (enumerable)، وللمجموعات بعض الخاصّيّات "الإضافيّة" التي يندر استعمالها ولا نودّ أن نحصل عليها: <body> <script> // وأكثر ،values ،item ،length ،1 ،0 سيعرض هذا كلّا من for (let prop in document.body.childNodes) alert(prop); </script> </body> See the Pen JS-p2-03-dom-navigation-ex04 by Hsoub (@Hsoub) on CodePen. الإخوة واﻷب الإخوة (siblings) هم عقدٌ أبناء لنفس الأب. فمثلا هنا، <head> و <body> هما أخوين: <html> <head>...</head><body>...</body> </html> يُقال عن <body> أنّه اﻷخ "التالي" أو "اﻷيمن" لـ <head>. يُقال عن <head> أنّه اﻷخ "السابق" أو "الأيسر" لـ <body> . يكون اﻷخ التالي مُتاحا في الخاصّيّة nextSibling، والأخ السابق في الخاصّيّة previousSibling. بينما يكون اﻷب مُتاحا في الخاصّيّة parentNode. على سبيل المثال: // <html> هو <body> أب alert( document.body.parentNode === document.documentElement ); // صحيح // <body> يأتي<head> بعد alert( document.head.nextSibling ); // HTMLBodyElement // <head> يكون <body> قبل alert( document.body.previousSibling ); // HTMLHeadElement See the Pen JS-p2-03-dom-navigation-ex05 by Hsoub (@Hsoub) on CodePen. التنقّل بين العناصر فقط تربط خاصّيّات التنقّل المذكورة أعلاه بجميع أنواع العقد. فيمكننا مثلا من خلال childNodes الاطلاع على كلٍّ من العقد النصّيّة والعقد العنصريّة، بل وحتى العقد التعليقيّة إن وُجدت. لكنّنا في العديد من المهامّ لا نرغب في العقد النصّيّة والتعليقيّة. بل نرغب في معالجة العقد العنصريّة التي تمثّل الوسوم وتشكّل بنية الصفحة. لنرى إذًا بعض روابط التنقّل اﻷخرى التي تأخذ في الحسبان العقد النصيّة فقط: هذه الروابط مماثلة لتلك المذكورة أعلاه، مع إضافة كلمة Element في وسطها: children -- الأبناء الذين هم عقد عنصريّة فقط. firstElementChild و lastElementChild -- أوّل اﻷبناء وآخرهم من العقد العنصريّة. previousElementSibling و nextElementSibling -- العناصر المتجاورة. parentElement -- العنصر اﻷب. ما الداعي إلى parentElement ؟ أيمكن للأب ألّا يكون عنصرا؟ تُعيد خاصّيّة parentElement "العنصر" اﻷب، بينما تُعيد parentNode "أيّ عقدةٍ" أب. هاتان الخاصّيّتان متشابهتان في العادة: كلتاهما تُحصّل اﻷب. لكن هناك استثناءً وحيدا مع document.documentElement: alert( document.documentElement.parentNode ); // document alert( document.documentElement.parentElement ); // null See the Pen JS-p2-03-dom-navigation-ex06 by Hsoub (@Hsoub) on CodePen. سبب ذلك أنّ أب العقدة الجذر document.documentElement (التي تمثلّ <html>) هو document، و document ليس عقدة عنصريّة. فلهذا تعيده parentNode ولا تعيده parentElement. قد يفيد هذا التفصيل إذا ما أردنا التنقّل صعودا من عنصرٍ ما elem إلى <html>، لكن دون الوصول إلى document. while(elem = elem.parentElement) { // <html> اصعد إلى غاية alert( elem ); } لنُعدّل أحد اﻷمثلة التي في اﻷعلى. استبدل childNodes بـ children . ستظهر لك الآن العناصر فقط: <html> <body> <div>Begin</div> <ul> <li>Information</li> </ul> <div>End</div> <script> for (let elem of document.body.children) { alert(elem); // DIV, UL, DIV, SCRIPT } </script> ... </body> </html> See the Pen JS-p2-03-dom-navigation-ex07 by Hsoub (@Hsoub) on CodePen. المزيد من الروابط: الجدوال تناولنا إلى حدّ الآن خاصّيّات التنقّل اﻷساسيّة. قد تُتيح بعض أصناف العناصر في DOM خاصّيات إضافيّة تتميّز بها دون غيرها، لمزيدٍ من التسهيل. تُعدّ الجداول مثالا جيّدا لذلك، وتمثّل حالة خاصّة اﻷهميّة: يدعم عنصر <table> (إضافة إلى ما ذُكر في اﻷعلى) الخاصّيّات التالية: table.rows -- مجموعة عناصر <tr> التي في الجدول. table.caption/tHead/tFoot -- تشير إلى العناصر caption و thead و tfoot. table.tBodies -- مجموعة عناصر <tbody> (قد تكون هناك عدّةٌ منها كما ينصّ على ذلك المعيار، لكن سيكون هناك حتمًا واحد على اﻷقل -- حتى وإن لم يوجد في مصدر HTML، فسيضعه المتصفّح في DOM). تُتيح العناصر <thead> و <tfoot> و <tbody> خاصّيّة rows : tbody.rows -- مجموعة عناصر <tr> التي بداخله. <tr>: tr.cells -- مجموعة الخانات <td> و <th> التي بداخل <tr>. tr.sectionRowIndex -- موضع <tr> بداخل الـ <thead> أو <tbody> أو <tfoot> الذي يضمّه. tr.rowIndex -- رقم <tr> بالنسبة للجدول ككلّ (أي بين جميع أسطر الجدول). <td> و <th>: td.cellIndex -- رقم الخانة بداخل السطر الذي يضمّها. هذا مثال عن استعمالها: <table id="table"> <tr> <td>one</td><td>two</td> </tr> <tr> <td>three</td><td>four</td> </tr> </table> <script> // (السطر اﻷوّل، والعمود الثاني) "two" حدّد الخانة التي فيها let td = table.rows[0].cells[1]; td.style.backgroundColor = "red"; // لوّنها </script> See the Pen JS-p2-03-dom-navigation-ex08 by Hsoub (@Hsoub) on CodePen. للاطلاع على المواصفة: tabular data. هناك أيضا المزيد من خاصّيات التنقّل التي تتعلّق بنماذج HTML (أي forms). سنقف عندها لاحقا عندما نبدأ العمل بالنماذج. الملخص إنطلاقًا من أيّ عقدةٍ في DOM، يمكن الذهاب إلى العقد المجاورة لها مباشرة بواسطة خاصّيّات التنقّل. تنقسم هذه الخاصيّات إلى فئتين رئيسيّتن: لجميع العقد: parentNode childNodes firstChild lastChild previousSibling nextSibling للعقد العنصريّة فقط: parentElement children firstElementChild lastElementChild previousElementSibling nextElementSibling تُتيح بعض أصناف العقد في DOM كالجداول مثلا، خاصّيّات ومجموعات إضافيّة تمكّن من الوصول إلى محتواها. التمارين اﻷبناء في DOM اﻷهمية: 5 لاحظ هذه الصفحة: <html> <body> <div>Users:</div> <ul> <li>John</li> <li>Pete</li> </ul> </body> </html> اعط طريقة واحدة على اﻷقلّ للوصول إلى كلٍّ من عقد DOM التالية: العقدة <div>؟ العقدة <ul>؟ عقدة <li> الثانية (التي فيها Pete)؟ الحل هناك عدّة طرق، على سبيل المثال: العقدة <div>: document.body.firstElementChild // أو document.body.children[0] // أو -- لاحظ أن العقدة الأولى فراغ، لذا نأخذ الثانية document.body.childNodes[1] العقدة <ul>: document.body.lastElementChild // أو document.body.children[1] عقدة <li> الثانية (التي فيها Pete): // ومن ثمّ تحصيل آخر أبنائه العناصر <ul> تحصيل document.body.lastElementChild.lastElementChild سؤال عن الإخوة اﻷهميّة: 5 إذا كانت elem عقدةً عنصريّة من DOM … هل صحيح أنّ elem.lastChild.nextSibling دائمًا null؟ هل صحيح أنّ elem.children[0].previousSibling دائمًا null؟ الحل نعم، ذلك صحيح. يكون elem.lastChild دائما آخر اﻷبناء، فليس لديه nextSibling. لا ليس ذلك صحيحا، ﻷنّ [elem.children[0 هو أوّل ابنٍ بين العناصر، وقد تأتي قبله عقدٌ غير عنصريّة. فلا تكون بذلك previousSibling هي null بالضرورة، بل قد تكون عقدةً نصيّة مثلا. يرجى الانتباه إلى أنّه في كلتا الحالتين، إذا لم يكن هناك أبناء فسيُحدث ذلك خطأً. إذا لم يكن هناك أبناء، فإنّ elem.lastChild هو null فلا يمكننا الوصول إلى elem.lastChild.nextSibling، وتكون أيضا المجموعة elem.children فارغة (كمصفوفة فارغة [ ]). اختر الخانات القُطريّة اﻷهميّة: 5 اكتب الشفرة التي تُلوّن جميع الخانات التي على قُطر الجدول باﻷحمر. ستحتاج إلى تحصيل جميع الخانات <td> القطريّة من الجدول <table> وتلوينها باستخدام الشفرة التالية: // إلى خانة الجدول td تشير td.style.backgroundColor = 'red'; يجب أن تكون النتيجة كالتالي: افتح البيئة التجريبيّة لإنجاز التمرين الحل سنستخدم خاصّيّات rows و cells للوصول إلى الخانات القطريّة. شاهد الحلّ في البيئة التجريبيّة ترجمة وبتصرف لمقال Walking the DOM من كتاب The Modern JavaScript Tutorial
  25. تعدّ الوسوم (tags) أساس مستندات HTML. ويُمثَّل كلُّ وسمٍ منها في نموذج كائن المستند (DOM) بكائن. كما يُمثَّل النصّ الذي بداخل الوسم بكائن أيضا. وتعدّ الوسوم المتشعبة عن وسم آخر "أبناءً" لذلك الوسم. يمكننا الوصول لكلّ هذه الكائنات من خلال جافاسكربت، ونستطيع بواسطتها التعديل على الصفحة. فعلى سبيل المثال، يمثّل الكائن document.body وسم <body>، وبتنفيذ الشفرة التالية يصير لون <body> أحمر لمدّة 3 ثوانٍ: document.body.style.background = 'red'; // جعل الخلفية حمراء اللون setTimeout(() => document.body.style.background = '', 3000); // ارجاعها كما كانت See the Pen JS-p2-02-dom-nodes-ex01 by Hsoub (@Hsoub) on CodePen. استعملنا هنا خاصّيّة style.background لتغيير لون خلفيّة document.body، وهناك العديد من الخاصّيّات اﻷخرى مثل: innerHTML -- محتوى HTML الذي بداخل العقدة. offsetWidth -- مقدار عرض العقدة (بالبكسل). … إلى غير ذلك. سنتعلم قريبا المزيد من الطرق لمعالجة DOM، لكن نحتاج أوّلا إلى التعرّف على بنيته. مثال عن DOM لنبدأ بالمستند البسيط التالي: <!DOCTYPE HTML> <html> <head> <title>About elk</title> </head> <body> The truth about elk. </body> </html> See the Pen JS-p2-02-dom-nodes-ex02 by Hsoub (@Hsoub) on CodePen. يمثِّل DOM مستند HTML بواسطة بنية شجريّة من الوسوم، كما في الصورة التالية: كل عقدة من هذه الشجرة هي كائن. تُمثَّل الوسوم بعقدٍ عنصريّة element nodes (أو عناصر elements باختصار) وتشكّل بنية الشّجرة. فعند الجذر توجد<html> ، و<head> و <body> هما أبناؤها، إلى آخر ذلك. يُمثَّل النّص الذي بداخل العناصر بعقدٍ نصّيّة وهي معلّمة في الصّورة بـ text#. لا تحتوي العقد النّصية إلا على سلاسل نصيّة (string)، ولا يمكن أن يكون لها أبناء، وتكون دائما بمثابة ورقةٍ للشجرة. على سبيل المثال، يحوي وسم <title> على النصّ "About elk". لاحظ وجود هذه المحارف الخاصّة (special characters) في العقد النصّيّة: السطر الجديد: ↵ (يُعرف في جافاسكربت بـ n\). الفراغ: ␣. يعدّ السطر الجديد والفراغ محارف صحيحة، تماما كالحروف واﻷرقام، وتُكوّن بدورها عقدا نصّيّة وتصير جزءًا من DOM. ففي المثال أعلاه، يحوي وسم <head> بعض الفراغات قبل <title>، ويكوّن ذلك النّص عقدة نصّيّة (تحوي سطرا جديدا وبعض الفراغات فقط). يُستثنى من ذلك شيئان: تُهمل الفراغات واﻷسطر الجديدة التي قبل <head> لأسباب تاريخيّة. إذا وضعنا شيئا ما بعد <body/>، فإنه يُنقل تلقائيّا إلى آخر body، ﻷنّ مواصفة HTML تشترط أن يكون جميع المحتوى موجودا داخل <body>. لذا فلا يمكن أن تكون هناك أيّة فراغات بعد <body/>. في غير ذلك من الحالات، فاﻷمر واضح؛ إذا كانت هناك فراغات في المستند فإنّها تصير عقدا نصّيّة (كغيرها من المحرّفات) في DOM، وإن أزلناها فلن تكون هناك أيّ منها. هذا مثال لعقد نصيّة لا تحوي أيّة فراغات: <!DOCTYPE HTML> <html><head><title>About elk</title></head><body>The truth about elk.</body></html> لا تُظهر أدوات المتصفّح (التي سنتطرق لها لاحقا) عند تعاملها مع DOM عادةً الفراغات التي في أوّل النّصّ وآخره، ولا العقد النصّيّة الفارغة (كالتي تنجم عن إنهاء السطر) بين الوسوم. توفّر أدوات المتصفّح بذلك المساحة على الشاشة. قد نحذفها أيضا في ما يأتي صورٍ لـ DOM إذا لم يكن في إظهارها فائدة. لا تؤثر هذه الفراغات عادة على كيفيّة عرض المستند. التصحيح التلقائي إذا صادف المتصفّح مستند HTML خاطئ التنسيق، فإنّه يصحّحه تلقائيّا عند بناء DOM. على سبيل المثال، يكون <html> دائما هو الوسم اﻷعلى. حتى وإن لم يوجد في المستند، فإنّه سُيوجد في DOM، لأن المتصفّح سيُنشئه. وكذلك الشأن مع <body>. فإذا كان ملف HTML عبارةً عن كلمة واحدة "Hello"، فإنّ المتصفّح سيلفُّه وسط <html> و <body> ويضيف لهما <head> كما يلزم، ويصير بذلك DOM: عند توليد DOM، تعالج المتصفّحات تلقائيّا الأخطاء التي في المستند، كعدم إغلاق الوسوم وغير ذلك. فإذا كان في المستند وسوم لم تُغلق: <p>Hello <li>Mom <li>and <li>Dad فإنّ المتصفّح يستعيد الأجزاء المفقودة عندما يطّلع على الوسوم: للجداول دائما <tbody> تعدّ الجداول "حالة خاصّة" مثيرة للاهتمام. فوفقًا لمواصفة DOM يجب أن يكون للجداول العنصر <tbody>، لكن يُسمح (رسميًّا) لنصوص HTML أن تسقطه. فينشئ المتصفح حينها <tbody> في DOM تلقائيّا. فتؤدّي مثلا شفرة HTML هذه: <table id="table"><tr><td>1</td></tr></table> إلى بنية DOM التالية: أرأيت؟ ظهر <tbody> من لاشيء. ينبغي الانتباه لهذا عند التعامل مع الجداول لتفادي المفاجآت. أنواع أخرى من العقد هناك أنواع أخرى من العقد ما عدا العقد العنصريّة والنصيّة. فهناك مثلا التعليقات. <!DOCTYPE HTML> <html> <body> The truth about elk. <ol> <li>An elk is a smart</li> <!-- comment --> <li>...and cunning animal!</li> </ol> </body> </html> يمكننا في الصورة أعلاه ملاحظة نوع جديد من العقد -- العقد التعليقيّة ، معلّمة بـ comment# ، بين عقدتين نصّيّتين. لكن قد نتساءل -- لماذا تضاف التعليقات إلى DOM؟ فلا تأثير لها على التمثيل البصريّ بأيّ شكل من اﻷشكال. هذا صحيح، لكن هناك قاعدة تقول: كلُّ ما وُجد في HTML، فلا بدّ أن يوجد أيضا في شجرة DOM. كلّ ما يحويه HTML، حتّى التعليقات، يصير جزءًا من DOM. حتّى تعليمة <...DOCTYPE!> التي في أوّل HTML هي أيضا عقدة من DOM. هي موجودة في شجرة DOM قبل <html> مباشرة. لن نمسّ تلك العقدة، ولن نرسمها حتى في المخططات، لكنّها موجودة. بل إنّ كائن document الذي يمثل المستند بأكمله هو، اصطلاحًا، عقدة من DOM أيضا. هناك 12 نوعا من العقد، لكن نتعامل عادةً مع أربعة منها فقط: document -- "نقطة الدخول" إلى DOM. العقد العنصريّة -- وسوم HTML التي تمثل لبِنات الشجرة اﻷساسيّة. العقد النصّيّة -- تحتوي على نصّ فقط. التعليقات -- نضع فيها بعض المعلومات أحيانا. هي لا تُعرض، ولكن يستطيع جافاسكربت قراءتها من خلال DOM. عاينه بنفسك لمعاينة البنية الآنيّة لـ DOM، جرّب Live DOM Viewer. عليك فقط أن تدخل المستند، وسيظهر لك على شكل DOM حالًا. يمكن أيضا تفحّص DOM باستخدام أدوات المطوّر (developer tools) في المتصفّح، وهي في الحقيقة ما نستعمله عند التطوير. للقيام بذلك، افتح صفحة elk.html، ثمّ فعّل أدوات المطوّر في المتصفّح بالضغط على F12، وانتقل إلى لسان Elements (بمعنى عناصر). يُفترض أن تبدو كما في الصورة: يمكنك مشاهدة DOM، والنقر على على عناصره لاستعراض تفاصيلها وما إلى ذلك. يُرجى التنبّه إلى أن بنية DOM مبسّطة في أدوات المطوّر. فتظهر العقد النصّيّة كمجرّد نصّ، وليست هناك عقد نصّيّة فارغة (أي تتكوّن من فراغات فقط) إطلاقًا. لكن لا بأس بذلك، إذ ما نهتمّ به في الغالب هي العقد العنصريّة. بالنقر على الزرالذي على شكل مؤشر في أقصى الزاوية اليسرى من اﻷعلى يمكننا اختيار عقدة من الصفحة باستخدام الفأرة (أو أيٍّ من أجهزة التأشير اﻷخرى) و "فحصها" (بالانتقال إلى موضعها في لسان العناصر). يفيد هذا كثيرا عندما تكون صفحة HTML (وما يقابلها من DOM) التي لدينا كبيرة جدا، ونود أن نرى مكان عنصر معيّن منها. كما يمكن أيضا فعل ذلك عن طريق النقر بالزر اﻷيمن على صفحة الويب ومن ثمّ اختيار "Inspect" (أي تفحّص) من القائمة المنبثقة. توجد في الجزء اﻷيمن من اﻷدوات اﻷلسنة الفرعية التالية: Styles (بمعنى الأنماط) -- تمكّننا من رؤية قواعد CSS المطبّقة على العنصر الحاليّ قاعدةً بقاعدة، بما في ذلك القواعد المضمّنة (باللون الرمادي). يمكن تقريبًا تعديل كلّ شيء من مكانه، بما في ذلك أبعاد (dimensions) وهوامش (margins) وحشوات (paddings) الصندوق (box) من أسفل اللسان. Computed (بمعنى المحسوبة) -- لرؤية تنسيقات CSS المطبّقة على العنصر مرتّبةً حسب الخاصّيّة (property): كلّ خاصّيّة تقابلها القاعدة التي منحتها (بما في ذلك التنسيقات المورّثة وما إلى ذلك). Event Listeners (بمعنى مستمعي اﻷحداث) -- لرؤية مستمعي اﻷحداث المرفقة مع عناصر DOM (سنتناول ذلك في مقالات لاحقة). أفضل طريقة لدراسة كلّ هذه اﻷقسام هي بالنقر هنا وهناك. معظم القيم قابلة للتعديل من مكانها. التعامل مع الطرفية (console) عند تعاملنا مع DOM قد نودّ تطبيق جافاسكربت عليه، كأن نأخذ إحدى العقد، ونجري عليها شِفرة ما لمعرفة النتيجة. هذه بعض الطرق لكيفيّة التنقل بين لسان العناصر والطرفيّة. بدايةً: حدّد أوّل <li> في لسان العناصر. اضغط زر Esc -- سيفتح ذلك الطرفيّة تحت لسان العناصر مباشرة. يكون آخر العناصر التي حدّدناها محفوظا في المتغيّر 0$ ، والذي حدّدناه قبل ذلك في 1$ وهكذا. يمكننا تنفيذ أوامر عليها. فمثلا، يغيّر اﻷمر ‎‎$0.style.background = 'red'‎ لون عنصر القائمة المحدَّد إلى اﻷحمر كما في الصّورة: بهذه الطريقة تُنقل العقد من لسان العناصر إلى الطرفية. يمكن أيضا فعل العكس. فإذا كان لدينا متغيّر يشير إلى عقدة ما، يمكننا استخدام اﻷمر (inspect(node (حيث node هو اسم المتغيّر) في الطرفيّة لرؤيتها في لسان العناصر. أو يمكننا فقط طباعة عقدة DOM في الطرفية وتفحصّها "من مكانها"، كما هو الحال مع document.body في اﻷسفل: لا تُستخدم هذه الطرق إلا عند تنقيح اﻷخطاء بالطبع. ابتداءً من المقال الموالي، سنستخدم جافاسكربت للوصول إلى DOM والتعديل عليه. تساعد أدوات المطوّر في المتصفّح كثيرا أثناء التطوير، إذ يمكن من خلالها تفحّص DOM، وتجريب أشياء مختلفة لمعرفة ما قد يحدث من أخطاء. خلاصة يُمثًّل مستند HTML/XML في المتصفّح بواسطة شجرة DOM. تصير الوسوم عقدًا عنصريّة وتشكّل بنية الشجرة. تصير النصوص عقدًا نصّيّة. … إلى غير ذلك، فلِكلّ ما يوجد في HTML مكانه في DOM، حتّى التعليقات. يمكننا استخدام أدوات المطوّر لتفحّص DOM وتعديله يدويّا. اقتصرنا هنا كبدايةٍ على اﻷساسيّات من أهمّ العمليّات وأكثرها استخداما. هناك توثيق شامل ﻷدوات المطوّر في متصفّح Chrome على https://developers.google.com/web/tools/chrome-devtools. أفضل طريقة لتعلم هذه اﻷدوات هو النقر هنا وهناك، وقراءة ما في القوائم، فمعظم الخيارات واضحة. لاحقا، عندما تتقن استخدامها عموما، طالع التوثيق والتقط الباقي. لعُقد DOM خاصّيّات وتوابع تمكنّنا من التنقّل بينها والتعديل عليها والتجوّل في الصفحة وغير ذلك. هذا ما سنتناوله في المقال التالي. ترجمة وبتصرف لمقال DOM tree من كتاب The Modern JavaScript Tutorial
×
×
  • أضف...