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

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

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

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

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

5 متابعين

آخر الزوار

2993 زيارة للملف الشخصي

إنجازات محمد أمين بوقرة

عضو مساهم

عضو مساهم (2/3)

11

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

  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
×
×
  • أضف...