البحث في الموقع
المحتوى عن 'جافاسكربت في المتصفح'.
-
عندما يُحمّل المتصفّح ملف 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
- 1 تعليق
-
- 2
-
يعتمد مجرى تنفيذ جافاسكربت في المتصفّح، وكذلك في 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.
-
الكائن 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. اقرأ أيضًا تحميل الموارد الخارجية في صفحات الويب وتتبع حالتها عبر جافاسكربت. هياكل البيانات: الكائنات والمصفوفات في جافاسكريبت. التركيز على عناصر الاستمارات الإلكترونية والتنقل بينها في جافاسكربت.
-
سنتناول في هذا المقال التحديد داخل المستند، وكذلك التحديد داخل حقول الاستمارات، مثل <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
-
تتسبّب الكثير من الأحداث في قيام المتصفّح تلقائيّا بأفعالٍ معيّنة. على سبيل المثال: يتسبّب النقر على رابط في الانتقال إلى عنوانه. يتسبّب النقر على زرّ الإرسال في نموذج في إرساله إلى الخادم. يتسبّب الضغط على زرّ الفأرة فوق نصّ وتحريكها في تحديد ذلك النصّ. في حال معالجة الحدث في جافاسكربت، قد لا نريد حصول فعل المتصفّح المقابل له، ونريد تطبيق سلوكٍ آخر في مكانه. منع أفعال المتصفح هناك طريقتان لإخبار المتصفّح بأنّنا لا نريده أن يعمل: الطريقة الرئيسيّة هي استخدام الكائن event، الذي لديه التابع event.preventDefault(). إذا أٌسند المعالج باستخدام on<event> (لا باستخدام addEventListener) ، فإنّ إعادة القيمة false تؤدي نفس الغرض أيضا. في شيفرة HTML أسفله، لا يتسبّب النقر على الرابط في الانتقال إلى عنوانه، لا يفعل المتصفّح أيّ شيء. <a href="/" onclick="return false">Click here</a> or <a href="/" onclick="event.preventDefault()">here</a> See the Pen JS-p2-browser-default-actions -ex1 by Hsoub (@Hsoub) on CodePen. في المثال التالي، سنستخدم هذه الطريقة لإنشاء قائمة تعمل بواسطة جافاسكربت. تنبيه: إعادة false من المعالج هو استثناء تُهمل عادةً القيمة المُعادة من طرف المعالج. الاستثناء الوحيد هو return false من المعالج المسنَد باستخدام on<event>. في جميع الحالات الأخرى، تُهمل قيمة return. على وجه الخصوص، لا معنى لإعادة true. مثال: القائمة لنفترض قائمة موقع كهذه: <ul id="menu" class="menu"> <li><a href="/html">HTML</a></li> <li><a href="/javascript">JavaScript</a></li> <li><a href="/css">CSS</a></li> </ul> بإضافة بعض تنسيقات CSS ستبدو كما هنا. تُنشأ عناصر القائمة على شكل روابط HTML أي <a>، لا على شكل أزرار <button>. هناك عدّة أسباب لفعل ذلك، فمثلًا: يحبّذ كثيرٌ من الناس استخدام "النقر باليمين" ثم "افتح في نافذة جديدة". لو استخدمنا <button> أو <span> فلن يعمل ذلك. تتتبّع محرّكات البحث الروابط <a href="..."> عند الفهرسة. بالتالي سنستخدم <a> عند إضافة الروابط في HTML. لكنّّنا نودّ عادةً معالجة النقرات على الروابط بواسطة جافاسكربت. فيجب إذًا أن نمنع فعل المتصفّح الافتراضي.كما هو مبيّن هنا: menu.onclick = function(event) { if (event.target.nodeName != 'A') return; let href = event.target.getAttribute('href'); alert( href ); // قد يُحمّل من الخادم أو تُولّد واجهة المستخدم إلى غير ذلك ... return false; // (لا تذهب إلى العنوان) امنع فعل المتصفّح }; إذا حذفنا return false، فسيقوم المتصفّح بعد تنفيذ الشيفرة "بفعله الافتراضي" -- الانتقال إلى العنوان الذي في href. ولا نحتاج ذلك هنا، إذ نعالج النقرات بأنفسنا. بالمناسبة، يساعد استخدام تفويض الحدث هنا في جعل القائمة مرنة. يمكننا إضافة قوائم متداخلة وتنسيقها باستخدام CSS حتى "تنزلق إلى الأسفل". ملاحظة: الأحداث المتتابعة تجري بعض الأحداث الواحد تلو الآخر. فإذا منعنا الحدث الأوّل، لن يكون الثاني. على سبيل المثال، يؤدّي وقوع الحدث mousedown على حقل <input> إلى التركيز عليه، ووقوع حدث focus. فإذا منعنا الحدث mousedown، لن يكون هناك تركيز. جرّب النقر على <input> الأوّل من هنا -- يقع الحدث focus. لكن لو نقرت على الثاني، لن يكون هناك تركيز. <input value="Focus works" onfocus="this.value=''"> <input onmousedown="return false" onfocus="this.value=''" value="Click me"> See the Pen JS-p2-browser-default-actions -ex2 by Hsoub (@Hsoub) on CodePen. هذا لأنّ فعل المتصفّح قد أُلغي عند mousedown. يبقى وقوع التركيز ممكنا إذا استخدمنا طريقة أخرى للوصول إلى المُدخَل. على سبيل المثال، بواسطة المفتاح Tab للانتقال من المُدخَل الأوّل إلى الثاني. لكن ليس باستخدام النقر بالفأرة. خيار المعالج "السلبي (passive)" يشير الخيار الاختياري passive: true لـ addEventListener إلى المتصفّح بأنّ المعالج لن يستدعي preventDefault(). لكن لماذا قد يُحتاج لذلك؟ تتسبّب بعض الأحداث مثل touchmove على الأجهزة اللمسية (عندما يحرّك المستخدم اصبعه على الشاشة) في تمرير (scroll) الصفحة افتراضيًّا، لكن قد يُمنع ذلك التمرير باستخدام preventDefault() في المعالج. فإذا تحسّس المتصفّح مثل هذه الأحداث، عليه أوّلًا دراسة جميع المعالجات للتحقّق من عدم استدعاء preventDefault في أيٍّ منها، ليتمكّن عندها من الشروع في التمرير. قد يُسبّب هذا تأخيرات و "ارتجاجات" غير ضروريّة في واجهة المستخدم. يُعلِم الخيارُ passive: true المتصفّحَ بأنّ المعالج لن يلغي التمرير. فيقوم المتصفّح بالتمرير مباشرة مقدّمًا بذلك تجربة سلسلة للغاية. تكون قيمة passive في بعض المتصفّحات (Firefox, Chrome) هي true افتراضيًّا للأحداث touchstart و touchmove. vent.defaultPrevented تكون قيمة الخاصيّة event.defaultPrevented هي true إذا مُنع الفعل الافتراضي، وإلّا فهي false. هناك حالة استخدامٍ لذلك مثيرة للاهتمام. تذكر أنّنا تحدّثنا في مقال انتشار الأحداث عن event.stopPropagation()، وكيف أنّ إيقاف انتشار الأحداث نحو الأعلى أمرٌ سيّء. يمكن أحيانًا استخدام event.defaultPrevented بدل ذلك، لإعلام المعالجات الأخرى بأنّ الحدث قد تمّت معالجته. لنرى مثالًا عمليّا لذلك. يُظهر المتصفّح افتراضيًّا عند الحدث contextmenu (النقر بزرّ الفأرة الأيمن) قائمة منبثقة تضمّ خيارات اعتياديّة. يمكننا منع ذلك، وإظهار قائمتنا نحن بدلها، هكذا: <button>Right-click shows browser context menu</button> <button oncontextmenu="alert('Draw our menu'); return false"> Right-click shows our context menu </button> See the Pen JS-p2-browser-default-actions -ex3 by Hsoub (@Hsoub) on CodePen. بالإضافة إلى تلك القائمة المنبثقة، نودّ الآن إنشاء قائمة منبثقة تعمل على مستوى المستند. عند النقر باليمين، يجب أن تظهر أقرب قائمة. <p>Right-click here for the document context menu</p> <button id="elem">Right-click here for the button context menu</button> <script> elem.oncontextmenu = function(event) { event.preventDefault(); alert("Button context menu"); }; document.oncontextmenu = function(event) { event.preventDefault(); alert("Document context menu"); }; </script> See the Pen JS-p2-browser-default-actions -ex4 by Hsoub (@Hsoub) on CodePen. المشكلة أنّه عندما ننقر على elem، فإننا نحصل على قائمتين: التي على مستوى الزرّ و (بانتشار الحدث نحو الأعلى) التي على مستوى المستند. كيف يمكن إصلاح ذلك؟ يعتمد أحد الحلول على هذه الفكرة: "عند معالجة حدث النقر باليمين من طرف المعالج المسند إلى الزرّ، لنقم بإيقاف انتشاره نحو الأعلى باستخدام event.stopPropagation(): <p>Right-click for the document menu</p> <button id="elem">Right-click for the button menu (fixed with event.stopPropagation)</button> <script> elem.oncontextmenu = function(event) { event.preventDefault(); event.stopPropagation(); alert("Button context menu"); }; document.oncontextmenu = function(event) { event.preventDefault(); alert("Document context menu"); }; </script> See the Pen JS-p2-browser-default-actions -ex5 by Hsoub (@Hsoub) on CodePen. تعمل الآن القائمة التي على مستوى الزرّ كما ينبغي. لكن الثمن باهظ. سنحرم جميع الشيفرات الخارجية من الوصول إلى المعلومات الخاصة بالنقرات اليمنى إلى الأبد، بما في ذلك العدّادات التي تحصي وما إلى ذلك. ليس هذا من الحكمة على الإطلاق. الحلّ البديل هو التحقّق في معالج document ممّا إذا كان الفعل الافتراضي قد مُنع. وإذا كان كذلك، فإنّ الحدث قد عولج، ولا حاجة لكي نستجيب له. <p>Right-click for the document menu (added a check for event.defaultPrevented)</p> <button id="elem">Right-click for the button menu</button> <script> elem.oncontextmenu = function(event) { event.preventDefault(); alert("Button context menu"); }; document.oncontextmenu = function(event) { if (event.defaultPrevented) return; event.preventDefault(); alert("Document context menu"); }; </script> See the Pen JS-p2-browser-default-actions -ex6 by Hsoub (@Hsoub) on CodePen. كل شيء يعمل الآن بشكل صحيح. إذا كانت لدينا عناصر متداخلة، ولكلٍّ منها قائمته المنبثقة، فإنّ ذلك سيعمل أيضا. احرص فقط على التحقّق من وجود event.defaultPrevented في كلّ معالجات contextmenu. ملاحظة: ()event.stopPropagation و ()event.preventDefault كما يمكننا رؤية ذلك بوضوح، event.stopPropagation() و event.preventDefault() (أو ما يُعرف أيضا بـ return false) هما شيئان مختلفان. لا علاقة لهما ببعضهما البعض. ملاحظة: هندسة القوائم المنبثقة المتداخلة هناك أيضا طرق بديلة لتطبيق قوائم منبثقة متداخلة. إحداها هو أخذ كائن عامّ وحيد له المعالج document.oncontextmenu، وله كذلك توابع تمكّننا من تخزين معالجات أخرى داخله. سيعمل الكائن على التقاط كلّ النقرات اليمنى، ثم ينظر في المعالجات المخزَّنة ويُشغّل المعالج المناسب. لكن عندها، بجب على كلّ قطعة شيفرة تريد قائمة منبثقة أن تكون على علم بذلك المعالج، وتستعين به بدل معالج contextmenu خاصّ بها. الملخص هناك عدة أفعال افتراضيًّة للمتصفّح: mousedown -- يبدأ عمليّة التحديد (تُحرٌك الفأرة للتحديد). click على <input type="checkbox"> -- إضافة أو إزالة التأشير على الخانة input. submit -- يتسبّب النقر على <input type="submit"> أو الضغط على مفتاح Enter داخل حقل في نموذج بوقوع هذا الحدث، ويرسل المتصفّحُ النموذجَ بعد ذلك. keydown -- قد يتسبّب الضغط على مفتاحٍ في إضافة حرف داخل حقل، أو إلى فعل آخر. contextmenu -- يقع الحدث عند النقر باليمين، ويكون الفعل هو إظهار قائمة المتصفّح المنبثقة. … و يوجد هناك المزيد … يُمكن منع جميع الأفعال الافتراضية إذا أردنا معالجة الحدث حصريّا من خلال جافاسكربت. لمنع الفعل الافتراضيّ، استخدم إمّا event.preventDefault() أو return false. لا تعمل الطريقة الثانية إلّا مع المعالجات المسندة بواسطة on<event>. يُعلِم الخيارُ passive: true المتصفّحَ بأنّ الفعل الافتراضيّ لن يُمنع. هذا مفيد لبعض أحداث الأجهزة المحمولة، مثل touchstart و touchmove، لإعلام المتصفّح بأنّ ليس عليه انتظار الانتهاء من تشغيل جميع المعالجات قبل التمرير. إذا تمّ منع الفعل الافتراضيّ، فإنّ قيمة event.defaultPrevented تصير true، وإلّا فهي false. تنبيه: حافظ على الدلاليّة، لا تفرط فنيّا، بمنع الأفعال الافتراضية واستخدام جافاسكربت يمكننا تخصيص سلوك أيّ عنصر. على سبيل المثال، يمكننا جعل رابط <a> يعمل مثل زرّ، وجعل زرّ <button> يتصرّف مثل رابط (ينقل إلى عنوان آخر ونحوه). لكن يجب عمومًا المحافظة على على المعاني الدلاليّة لعناصر HTML. على سبيل المثال، يجب أن يعمل على الانتقال الرابط، وليس الزرّ. إلى جانب كون ذلك "مجرّد أمر جيّد"، يجعل هذا شيفرة HTML أفضل من ناحية إمكانية الوصول (accessibility). بالإضافة إلى ذلك، يُرجى التنبّه إلى أن المتصفّح يتيح لنا إمكانيّة فتح الروابط <a> في نافذة مستقلّة (عن طريق النقر عليها باليمين وغير ذلك). ويودّ الناس ذلك. لكن لو جعلنا مكانها زرّا يعمل كرابط بواسطة جافاسكربت بل وجعلناه أيضا يبدو كرابط بواسطة CSS، فإنّ المزايا المختصّة بـ <a> التي يتيحها المتصفّح لن تعمل. التمارين لماذا لا تعمل "return false"؟ الأهمية: 3 لماذا لا تعمل return false إطلاقًا في هذه الشيفرة: <script> function handler() { alert( "..." ); return false; } </script> <a href="https://w3.org" onclick="handler()">the browser will go to w3.org</a> يتّبع المتصفّح العنوان عند النقر، لكنّنا لا نريد ذلك. كيف يمكن إصلاحه؟ الحل عندما يقرأ المتصفّح الخاصيّة on* مثل onclick، فإنّه ينشئ معالجًا من محتواها. فمن أجل onclick="handler()" تصير الدالّة: function(event) { handler() // onclick محتوى } نستطيع الآن أن نرى أن القيمة المعادة من طرف handler() غير مستعملة ولا تؤثر في النتيجة. طريقة إصلاح ذلك بسيطة: <script> function handler() { alert("..."); return false; } </script> <a href="https://w3.org" onclick="return handler()">w3.org</a> يمكننا أيضا استخدام event.preventDefault()، هكذا: <script> function handler(event) { alert("..."); event.preventDefault(); } </script> <a href="https://w3.org" onclick="handler(event)">w3.org</a> التقط الروابط داخل العنصر الأهمية: 5 اجعل جميع الروابط داخل العنصر الذي له id="contents" تسأل المستخدم أوّلا إن كان يودّ المغادرة حقّا. فإذا كان لا يرغب، لا تذهب إلى العنوان. كما هو مبيّن هنا. التفاصيل: يمكن أن تُحمّل شيفرة HTML داخل العنصر يدويّا، أو يمكن أن تُولّد ديناميكيّا في أيّ وقت، فلا سبيل لإيجاد جميع الروابط وإسناد معالجات إليها. استخدم تفويض الأحداث. قد يكون داخل المحتوى وسوم متداخلة. وداخل الروابط أيضا، مثل <a href=".."><i>...</i></a>. افتح البيئة التجريبية لإنجاز التمرين الحل هذا استعمال جيّد لنمط تفويض الأحداث. في الحالات الواقعية، بدل سؤال المستخدم، يمكننا إرسال طلب "التسجيل" (logging" request") إلى الخادم الذي يحفظ مكان مغادرة المستخدم. أو يمكننا تحميل المحتوى وعرضه في نفس الصفحة (إذا كان ذلك مسموحًا). كلّ ما نحتاجه هو التقاط الأحداث contents.onclick واستخدام confirm لسؤال المستخدم. قد يمثّل استخدام link.getAttribute('href') بدل link.href فكرة جيدة. انظر في الحل للتفاصيل. افتح الحل في البيئة التجريبية. معرض الصور الأهمية: 5 أنشئ معرضًا للصور بحيث تتغير الصورة الرئيسيّة بالنقر على الصور المصغّرة. كما هو مبيّن هنا. ملاحظة: استخدم تفويض الأحداث. أنجز التمرين في البيئة التجريبية. الحل يكون الحل بإسناد معالج إلى الحاوي وتتبّع النقرات. فإذا كان النقر على الرابط <a>، فنغير src الخاصّة بـ #largeImg إلى href الخاصة بتلك الصورة المصغرة. افتح الحل في البيئة التجريبية. ترجمة -وبتصرف- للمقال Browser default actions من سلسلة Browser: Document, Events, Interfaces لصاحبها Ilya Kantor
-
لا يمكننا فقط إسناد معالجات للأحداث من خلال جافاسكربت، ولكن يمكننا أيضا توليد أحداث مخصّصة. يُمكن استعمال الأحداث المخصّصة لإنشاء "مكوّنات رسوميّة". على سبيل المثال، يمكن للعنصر الجذر في قائمةٍ تعمل بواسطة جافاسكربت افتعال أحداثٍ تُنبئ بما يحصل مع القائمة: open (عند فتح القائمة)، select (عند تحديد عنصر) وهكذا. يمكن أن تنصت شيفرة أخرى لهذه الأحداث وتراقب ماذا يحصل مع القائمة. لا يمكننا فقط توليد أحداث جديدة كليّا، نخترعها لأغراضنا الخاصّة، ولكن يمكن أيضا توليد اﻷحداث المضمّنة، مثل click و mousedown إلى غير ذلك. قد يساعد ذلك عند إجراء الاختبارات الآليّة. باني الأحداث تشكّل أصناف الأحداث المضمّنة سُلّميّة مشابهة لسلّميّة أصناف عناصر DOM. يكون الجذر فيها هو الصنف المُضمّن Event. ويمكننا إنشاء كائنات منها بهذه الطريقة: let event = new Event(type[, options]); الوسائط: type -- نوع الحدث، ويكون سلسلة نصيّة مثل "click" أو إذا كان خاصّا بنا مثل "my-event". options -- كائن بخاصيتين اختياريتين: bubbles: true/false -- إذا كانت true، فإنّ الحدث ينتشر نحو الأعلى. cancelable: true/false: إذا كانت true، فمن الممكن منع "الفعل الافتراضي". سنرى لاحقًا ما يعني ذلك للأحداث المخصّصة. تكون قيمة كلتيهما false افتراضيّا: {bubbles: false, cancelable: false}. dispatchEvent بعد إنشاء كائن الحدث، نستطيع أن "نجريه" على عنصرٍ ما بواسطة الاستدعاء elem.dispatchEvent(event). تستجيب المعالجات له حينها كما لو كان حدث متصفّح عادي. وإذا كان الحدث قد أنشئ وله الراية bubbles، فإنّه ينتشر نحو الأعلى. ابتُدِئ الحدث click في المثال أدناه من خلال جافاسكربت. يعمل المعالج بنفس الطريقة كما لو أن الزرّ قد نُقر بالفعل: <button id="elem" onclick="alert('Click!');">Autoclick</button> <script> let event = new Event("click"); elem.dispatchEvent(event); </script> See the Pen JS-p2-dispatch-events -ex1 by Hsoub (@Hsoub) on CodePen. ملاحظة: event.isTrusted هناك طريقة لمعرفة إذا كان الحدث "حقيقيّا" من المستخدم أو مولّدًا بواسطة سكربت. تكون قيمة الخاصيّة event.isTrusted هي true للأحداث التي تأتي من أفعال حقيقيّة للمستخدم و تكون false للأحداث المولّدة بواسطة سكربت. مثال عن الانتشار نحو الأعلى يمكننا إنشاء حدث منتشر نحو الأعلى باسم "hello" والتقاطه في document. كلّ ما نحتاجه هو إعطاء bubbles القيمة true: <h1 id="elem">Hello from the script!</h1> <script> // ... document الالتقاط على مستوى document.addEventListener("hello", function(event) { // (1) alert("Hello from " + event.target.tagName); // Hello from H1 }); // elem الإرسال من ... let event = new Event("hello", {bubbles: true}); // (2) elem.dispatchEvent(event); // و يظهر الرسالة document سيشتغل المعالج المسند إلى </script> See the Pen JS-p2-dispatch-events -ex2 by Hsoub (@Hsoub) on CodePen. ملاحظات: يجب أن نستخدم addEventListener لأحداثنا المخصصة، لأن on<event> توجد فقط للأحداث المضمّنة، فلا تعمل document.onhello مثلا. يجب وضع bubbles:true، وإلا فلن ينتشر الحدث نحو الأعلى. آلية الانتشار نحو الأعلى هي نفسها للأحداث المُضمّنة (click) والمخصصة (hello). هناك أيضا مرحلتا الانتشار نحو الأعلى والانتشار نحو الأسفل. MouseEvent و KeyboardEvent وغيرهما هذه قائمة قصيرة لأصناف أحداث واجهة المستخدم مأخوذة من مواصفة أحداث واجهة المستخدم: UIEvent FocusEvent MouseEvent WheelEvent KeyboardEvent … ينبغي أن نستخدمها عوضا عن new Event إذا أردنا إنشاء هذه الأحداث. على سبيل المثال، new MouseEvent("click"). يمكّن الباني المناسب من تحديد خاصيّات قياسية تتعلّق بنوع الحدث ذاك. مثل clientX/clientY لأحداث المؤشر: let event = new MouseEvent("click", { bubbles: true, cancelable: true, clientX: 100, clientY: 100 }); alert(event.clientX); // 100 يرجى التنبه: لا يتيح الباني العام Event ذلك. لنجرّب: let event = new Event("click", { bubbles: true, // cancelable و bubbles فقط cancelable: true, // Event تعملان في الباني clientX: 100, clientY: 100 }); alert(event.clientX); // تُهمل الخاصّية غير المعروفة ،undefined في الحقيقة، يمكننا الاحتيال على ذلك بإسناد event.clientX=100 مباشرة بعد إنشائه. فيؤول الأمر إلى المناسبة والالتزام بالقواعد. يكون نوع الأحداث التي يولّدها المتصفّح صحيحًا على الدوام. توجد القائمة الشاملة لمختلف أحداث واجهة المستخدم في المواصفة، على سبيل المثال، MouseEvent. الأحداث المخصصة بالنسبة لأنواع الأحداث الخاصّة بنا والجديدة كليّا مثل "hello" علينا أن نستخدم new CustomEvent. فنيّا، CustomEvent هي نفس Event لكن مع استثناء وحيد. في الوسيط الثاني (object) يمكننا إضافة خاصيّة أخرى detail من أجل أيّة معلومات مخصصّة نودّ تمريرها مع الحدث. على سبيل المثال: <h1 id="elem">Hello for John!</h1> <script> // تأتي المزيد من التفاصيل مع الحدث إلى المعالج elem.addEventListener("hello", function(event) { alert(event.detail.name); }); elem.dispatchEvent(new CustomEvent("hello", { detail: { name: "John" } })); </script> See the Pen JS-p2-dispatch-events -ex3 by Hsoub (@Hsoub) on CodePen. يمكن أن تحوي هذه الخاصيّة أي معطيات. في الحقيقة، من الممكن أن نعمل بدونها، لأننا نستطيع أن نسند أيّ خاصيّة إلى كائن new Event عاديّ بعد إنشائه. لكن CustomEvent تزوّده بحقل detail الخاص لتفادي التعارض مع خاصيّات الحدث الأخرى. إلى جانب ذلك، يبيّن صنف الحدث "أيّ نوع حدث" هو، وإذا كان الحدث مخصّصا، فينبغي استخدام CustomEvent لنكون فقط واضحين بخصوصه. ()event.preventDefault لدى العديد من أحداث المتصفّح "أفعال افتراضيّة"، كالانتقال إلى رابط، بداية تحديد، وما إلى ذلك. أمّا الأحداث المخصّصة الجديدة، فليس لديها بالطبع أيّة أفعال افتراضيًّة، لكن قد تكون للشيفرة التي ترسل مثل هذه الأحداث خُططًا خاصّة تقوم بها بعد افتعال الحدث. باستدعاء event.preventDefault() ، يمكن لمعالج الحدث أن يرسل إشارة بإنّ تلك الأفعال المخطّط لها يجب أن تُلغى. في تلك الحالة يعيد الاستدعاء elem.dispatchEvent(event) القيمة false. وتعلم الشيفرةُ التي أرسلت الحدث بأنّ عليها ألا تكمل. لنرى مثالًا تطبيقيّا لذلك -- أرنب متخفّي (قد يكون قائمة أيضا منغلقة أو أيّ شيئ آخر). يمكن أن ترى في الأسفل الأرنب rabbit# وعليه الدالة hide() التي ترسل الحدث "hide"، لتعلم جميع الأطراف المعنيّة بأنّ الأرنب سيختفي. يمكن لأي معالج أن يستمع لذلك الحدث بواسطة rabbit.addEventListener('hide',...) ، وإن تتطلّب الأمر، يمكنه إلغاء الفعل باستخدام event.preventDefault(). فعندها لن يختفي الأرنب: <pre id="rabbit"> |\ /| \|_|/ /. .\ =\_Y_/= {>o<} </pre> <button onclick="hide()">Hide()</button> <script> function hide() { let event = new CustomEvent("hide", { cancelable: true // preventDefault بدون تلك الراية لا يعمل }); if (!rabbit.dispatchEvent(event)) { alert('The action was prevented by a handler'); } else { rabbit.hidden = true; } } rabbit.addEventListener('hide', function(event) { if (confirm("Call preventDefault?")) { event.preventDefault(); } }); </script> See the Pen JS-p2-dispatch-events -ex4 by Hsoub (@Hsoub) on CodePen. يرجى التنبه هنا: يجب أن يكون للحدث الراية cancelable: true، وإلّا فسيُهمل event.preventDefault() . تعمل الأحداث التي داخل أحداث أخرى بشكل متزامن تُعالَج الأحداث عادةً على شكل طابور. بعبارة أخرى، إذا كان المتصفّح يقوم بمعالجة onclick ووقع حدث جديد، كتحريك الفأرة مثلًا، فإن معالجته تُصفّ في طابور الانتظار، وتُستدعى المعالجات المتعلّقة بـ mousemove بعد الانتهاء من معالجة onclick. الاستثناء الجدير بالانتباه هنا هو عندما ينشأ حدث من داخل حدث آخر، بواسطة dispatchEvent مثلا. تُعالج تلك الأحداث فورًا: تُستدعى معالجات الحدث الجديد ثم تستمر معالجة الحدث الحالية. على سبيل المثال، في الشيفرة أدناه يُفتعل الحدث menu-open خلال الحدث onclick. ويُعالَج فورًا دون انتظار معالج onclick من الانتهاء: <button id="menu">Menu (click me)</button> <script> menu.onclick = function() { alert(1); menu.dispatchEvent(new CustomEvent("menu-open", { bubbles: true })); alert(2); }; // يفتعل بين 1 و 2 document.addEventListener('menu-open', () => alert('nested')); </script> See the Pen JS-p2-dispatch-events -ex5 by Hsoub (@Hsoub) on CodePen. يكون ترتيب المخرجات كما يلي: 1 -› الحدث الداخلي -› 2. يُرجى التنبّه إلى أنّ الحدث الداخليّ menu-open قد تم ألتقاطه على مستوى document. يتمّ انتشار الحدث الداخلي ومعالجته قبل عودة المعالجة إلى الشيفرة الخارجية (onclick). لا يخصّ هذا فقط dispatchEvent، بل هناك حالات أخرى. إذا استدعى معالجٌ توابعَ تفتعل أحداثًا أخرى، فإنّها تُعالج تزامنيًّا أيضًا، بشكلٍ متداخل. لكن لنفترض أنّنا لا نريد ذلك. نريد أن تتمّ معالجة onclick أوّلًا، باستقلالٍ عن menu-open أو غيره من اﻷحداث المتداخلة. يمكننا عندها إمّا أن نضع dispatchEvent (أو نداءَ افتعال أحداثٍ آخر) في آخر onclick، أو ربّما أفضل، أن نلفّه بـ setTimeout منعدمة التأخير: <button id="menu">Menu (click me)</button> <script> menu.onclick = function() { alert(1); setTimeout(() => menu.dispatchEvent(new CustomEvent("menu-open", { bubbles: true }))); alert(2); }; document.addEventListener('menu-open', () => alert('nested')); </script> See the Pen JS-p2-dispatch-events -ex6 by Hsoub (@Hsoub) on CodePen. تعمل الآن dispatchEvent لا تزامنيًّا بعد الانتهاء من تنفيذ الشيفرة الحاليّة، بما في ذلك menu.onclick، وتصير بذلك معالجات الأحداث منفصلة تمامًا. ويصير ترتيب المخرجات كالتالي: 1 -> 2 -> الحدث الداخلي. الملخص لتوليد حدثٍ من خلال الشيفرة، نحتاج أوّلًا أن ننشئ كائن حدث. يقبل الباني العام Event(name, options) اسمًا للحدث والكائن options مع الخاصّيّتين: bubbles: true إذا كان يجب أن ينتشر الحدث نحو اﻷعلى. cancelable: true إذا كان يجب أن يعمل event.preventDefault(). يقبل بانو الأحداث اﻷصليّة الآخرون مثل MouseEvent و KeyboardEvent و ما إلى ذلك، خاصّيّات مختصّة بنوع الحدث ذلك. على سبيل المثال، clientX لأحداث المؤشر. ينبغي أن نستخدم الباني CustomEvent للأحداث المخصّصة. فلديه خيار إضافيّ اسمه detail، يمكن أن نسند إليه المعطيات المختصّة بالحدث. وبذلك يمكن لجميع المعالجات الوصول إليها بواسطة event.detail. بالرغم من الإمكانيّة التقنيّة لتوليد أحداث المتصفّح مثل click أو keydown، فينبغي استخدامها بحذرٍ شديد. لا ينبغي أن نعمد إلى توليد أحداث المتصفّح، إذ يعدّ ذلك طريقة مبتذلة لتشغيل المعالجات. وهي هندسة سيّئة في أغلب اﻷحيان. يمكن أن تُولّد اﻷحداث اﻷصليّة: كطريقة مبتذلة لجعل مكتبات الطرف الثالث تعمل كما يجب، إذا لم تكن توفّر وسائل أخرى للتفاعل معها. عند إجراء الاختبارات الآليّة، مثل "النقر على الزرّ" من خلال السكربت لمعرفة ما إذا كانت الواجهة تستجيب بشكل صحيح. تُولَّد الأحداث المخصّصة بأسمائنا الخاصّة لأغراض هندسيّة في الغالب، للإشارة إلى ما يحصل داخل القوائم و الدوّارات وما إلى ذلك. ترجمة -وبتصرف- للمقال Dispatching custom events من سلسلة Browser: Document, Events, Interfaces لصاحبها Ilya Kantor
-
سنتناول في هذا المقال أحداث الفأرة وخاصّياتها بمزيد من التفصيل. يُرجى التنبه: لا تأتي هذه الأحداث فقط من "الأجهزة ذوات الفأرة"، ولكن من أجهزة أخرى أيضًا مثل الهواتف والأجهزة اللوحيّة، حيث تُحاكى فيها هذه الأحداث لتحقيق التوافق. أنواع أحداث الفأرة سبق وأن رأينا بعض هذه الأحداث: mousedown/mouseup: يحدث عندما يُنقر / يُحرَّر زرّ الفأرة فوق عنصر ما. mouseover/mouseout: يحدث عندما يبلغ / يغادر مؤشّر الفأرة عنصرا ما. mousemove: يحدث كلّما تحرّك مؤشّر الفأرة فوق عنصر ما. click: يحدث بعد حدوث mousedown ثم mouseup فوق نفس العنصر باستخدام الزرّ الأيسر للفأرة. dblclick؛ يحدث بعد النقر مرّتين على نفس العنصر خلال إطار زمنيّ قصير. يندر استخدامه هذه الأيام. contextmenu: يحدث عندما يُضغط الزرّ الأيمن للفأرة. وبما أنّ هناك طرقا أخرى لفتح القائمة المنبثقة، كاستخدام زرّ خاصّ في لوحة المفاتيح مثلا، فإنّه يحدث عند ذلك أيضا، ولا يُعدّ بذلك من الأحداث المختصّة بالفأرة على وجه الدقّة. هناك العديد من الأحداث الأخرى أيضا، سنتطرّق إليها لاحقا. ترتيب الأحداث مثلما يمكن أن تلاحظ في القائمة أعلاه، قد يؤدّي فعل واحد للمستخدم إلى وقوع عدّة أحداث. على سبيل المثال، يؤدّي النقر بالزرّ اﻷيسر أوّلا إلى حدوث mousedown عند الضغط على الزرّ، ثم إلى حدوث mouseup وclick عند تحريره. في الحالات التي يبتدئُ فيها فعلٌ واحد عدّةَ أحداث، فإنّ ترتيبها يكون ثابتا. بمعنى أن المعالجات نُستدعى هنا حسب الترتيب mousedown -> mouseup -> click. إليك التجربة الحيّة التالية لمثال تُسجَّل فيه جميع أحداث الفأرة، وإذا كان هناك فارق بأكثر من ثانية بينها ستُفصل بخطّ أفقيّ. يمكنك أيضا رؤية الخاصّيّة button التي تمكّن من اكتشاف زرّ الفأرة، وسيُشرح ذلك في أسفله: See the Pen JS-p2-mouse-events-basics-ex01 by Hsoub (@Hsoub) on CodePen. زر الفأرة تكون للأحداث المتعلّقة بالنقر دائما الخاصّيّة button، التي تمكّن من معرفة الزرّ الذي ضُغط بالضبط. قد لا نحتاج إلى استعمالها في العادة مع اﻷحداث click وcontextmenu، لأّن اﻷوّل يحدث فقط عند النقر باليسار، بينما يحدث اﻷخير فقط عند النقر باليمين. لكن في المقابل، قد تحتاج معالجات الأحداث mousedown وmouseup إلى الخاصّيّة event.button، لأنّ تلك اﻷحداث قد تقع عند النقر على أيٍّ من الأزرار، فتمكّن button من التمييز بين "mousedown اﻷيمن" و"mousedown اﻷيسر". يمكن أن تأخذ الخاصّيّة button القيم التالية: حالة الزرّ event.button الزرّ اﻷيسر (اﻷوليّ) 0 الزرّ اﻷوسط (الملحق) 1 الزرّ اﻷيمن (الثانويّ) 2 الزرّ X1 (الخلف) 3 الزرّ X2 (اﻷمام) 4 table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } ليس لدى معظم أجهزة الفأرة سوى الزرّين اﻷيمن واﻷيسر، فتكون القيم الممكنة 0 أو 2. تولّد اﻷجهزة اللمسيّة أيضا مثل هذه اﻷحداث عندما ينقر أحدهم عليها. ثم إنّ هناك الخاصّيّة event.buttons التي تعبّر عن جميع الأزرار المضغوطة حاليّا بعدد صحيح، يكون ناتج مجموع قيم تلك اﻷزرار. يندر جدّا استخدام هذه الخاصّيّة في الواقع، لكن يمكنك أن تجد المزيد من التفاصيل حولها من MDN إن حصل واحتجت لها. تنبيه: خاصّيّة event.which المندثرة قد تستعمل الشيفراتُ القديمة الخاصّيّة event.which وهي طريقة قديمة وغير قياسية للحصول على الزرّ، ويمكن أن تأخذ القيم التالية: event.which == 1 -- الزرّ اﻷيسر، event.which == 2 -- الزرّ اﻷوسط، event.which == 3 -- الزرّ اﻷيمن. حاليّا، تُعدّ event.which مهملة (deprecated)، ولا ينبغي استعمالها. مفاتيح التبديل: shift و alt و ctrl و meta تحتوي جميع أحداث الفأرة على المعلومات الخاصّة بمفاتيح التبديل المضغوطة. فللكائن event الخاصّيّات التالية: shiftKey: المفتاح Shift altKey: المفتاح Alt (أو المفتاح Opt في ماك) ctrlKey: المفتاح Ctrl metaKey: المفتاح Cmd في ماك تكون قيم هذه الخاصّيّات true إذا كان المفتاح المقابل لها مضغوطا عند وقوع الحدث. فعلى سبيل المثال، لا يعمل الزرّ أسفله إلا عند النقر مع الضغط على المفاتيح Alt وShift معا: <button id="button">Alt+Shift+Click on me!</button> <script> button.onclick = function(event) { if (event.altKey && event.shiftKey) { alert('Hooray!'); } }; </script> See the Pen JS-p2-mouse-events-basics-ex02 by Hsoub (@Hsoub) on CodePen. تنبيه: عادة ما يُستعمل المفتاح Cmd بدل Ctrl في ماك توجد في ويندوز ولينكس مفاتيح التبديل Alt وShift وCtrl. كما يوجد في ماك المفتاح الإضافيّ Cmd الذي تقابله الخاصّيّة metaKey. في معظم التطبيقات، عندما يُستعمل في ويندوز ولينكس المفتاح Ctrl فإنّ في ماك يُستعمل المفتاح Cmd. بمعنى أنّه عندما يضغط مستخدم ويندوز على المفتاحين Ctrl وEnter أو Ctrl وA، فإنّ من المفترض أن يضغط مستخدم ماك على المفتاحين Cmd وEnter أو Cmd وA، وهكذا. فإذا أردت أن تعتمد الجمع بين Ctrl والنقر مثلا، فمن المعقول استخدام الجمع بين Cmd والنقر في ماك. فذلك أريح لمستخدمي ماك. حتى وإن فضّلنا اضطرار مستخدمي ماك إلى الجمع بين Ctrl والنقر -- ذلك صعب بعض الشيء، فإنّ المشكلة أن النقر باليسار مع الضغط على Ctrl في نظام التشغيل ماك يُفهم على أنّه نقر باليمين، ويؤدّي إلى توليد الحدث contextmenu، وليس click كما في ويندوز ولينكس. فإذا أردنا أن نحرص على راحة مستخدمي جميع أنظمة التشغيل، فينبغي أن نفحص كلتى الخاصّيتين ctrlKey وmetaKey. ما يعني ذلك في شيفرة جافاسكربت هو أن نتحقّق if (event.ctrlKey || event.metaKey). تنبيه: هناك أيضا أجهزة محمولة يُعدّ الجمع بين المفاتيح إضافة جيّدة في سير العمل. فإذا كان الزائر يستخدم لوحة المفاتيح، فستعمل بشكل جيّد. لكن إذ لم يكن جهازه متضمّنا لها، فلابدّ إذا من وجود سبيل للعمل دون مفاتيح التبديل. الإحداثيات: clientX/Y وpageX/Y تقدّم جميع أحداث الفأرة نوعين من الإحداثيّات: بالنسبة إلى النافذة: clientX وclientY. بالنسبة إلى المستند: pageX وpageY. باختصار، تُحتسب الإحداثيّات بالنسبة إلى النافذة pageX/Y ابتداءً من الزاوية العليا من اليسار للمستند، ولا تتغيّر عند تمرير (scroll) الصفحة، بينما تُحتسب clientX/Y ابتداءً من الزاوية العليا من اليسار للنافذة الحاليّة وتتغيّر عند تمرير الصفحة. على سبيل المثال، إذا كانت لدينا نافذة قياسها 500x500، والفأرة موجودة في الزاوية العليا من اليسار، فإنّ قيمتا clientX وclientY هي 0، مهما مُرّرت الصفحة. بينما إذا كانت الفأرة في المنتصف، فإن قيمتا clientX وclientY هي 250، أيًّا كان مكانها في المستند. وهما بهذا الاعتبار متشابهتان مع position:fixed. حرّك الفأرة فوق حقل المُدخل لرؤية clientX/clientY (المثال موجود داخل iframe، لذا فإنّ الإحداثيّات منسوبة إلى iframe). <input onmousemove="this.value=event.clientX+':'+event.clientY" value="Mouse over me"> See the Pen JS-p2-mouse-events-basics-ex03 by Hsoub (@Hsoub) on CodePen. تجنب التحديد مع mousedown للنقر المزدوج بالفأرة أثر جانبيّ قد يكون مزعجا في بعض الواجهات، ألا وهو تحديد النصّ. على سبيل المثال، يؤدّي النقر المزدوج على النصّ أدناه إلى تحديده بالإضافة تشغيل المعالج المسند إليه: <span ondblclick="alert('dblclick')">Double-click me</span> See the Pen JS-p2-mouse-events-basics-ex04 by Hsoub (@Hsoub) on CodePen. وإذا ضغط أحدهم زرّ الفأرة اﻷيمن، وحرك الفأرة دون تحريره، فسيعمل ذلك أيضا على تحديد النص، ولا يُرغب في ذلك غالبا. هناك عدّة طرق لتجنُّب التحديد، يمكنك الاطّلاع عليها في مقال التحديد والمدى. لكن في هذه الحالة خاصّة، أنسب طريقة لذلك هي منع فعل المتصفّح عند mousedown، ليحول ذلك دون حصول هذه التحديدات: Before... <b ondblclick="alert('Click!')" onmousedown="return false"> Double-click me </b> ...After See the Pen JS-p2-mouse-events-basics-ex05 by Hsoub (@Hsoub) on CodePen. لا يُحدَّد العنصر ذو الخطّ العريض الآن عند النقر المزدوج عليه، ولا يؤدّي النقر عليه باليسار إلى ابتداء تحديده أيضا. لكن يُرجى التنبّه: يبقى النصّ داخله قابلا للتحديد، إذا ابتُدئ التحديد قبل النصّ أو بعده، لا عليه مباشرة، ولا بأس في ذلك بالنسبة للمستخدمين عادة. ملاحظة: منع النسخ إذا أردنا أن نعطّل تحديد النصّ لحماية محتوى صفحتنا من النسخ واللصق، فيمكننا عندها استعمال حدث آخر: oncopy. <div oncopy="alert('Copying forbidden!');return false"> Dear user, The copying is forbidden for you. If you know JS or HTML, then you can get everything from the page source though. </div> See the Pen JS-p2-mouse-events-basics-ex06 by Hsoub (@Hsoub) on CodePen. فلو حاولت نسخ جزء من النصّ الذي بداخل <div>، فلن يعمل ذلك، ﻷن الفعل الافتراضيّ oncopy قد مُنع. يمكن للمستخدم بالتأكيد الوصول إلى مصدر HTML، وأخذ المحتوى من هناك، لكن لا يُحسن الجميع فعل ذلك. الملخص لأحداث الفأرة الخاصّيّات التالية: الزرّ: button. مفاتيح التبديل: (true عند الضغط عليها): altKey وctrlKey وshiftKey وmetaKey (ماك). إذا كنت تودّ اعتماد Ctrl، لا تنس مستخدمي ماك، إذ يستعملون عادة Cmd، فيُفضّل إذًا أن نتحقّق if (e.metaKey || e.ctrlKey). الإحداثيّات بالنسبة للنافذة: clientX/clientY. الإحداثيّات بالنسبة للمستند: pageX/pageY. فعل المتصفح الافتراضيّ عند mousedown هو تحديد النصّ، فإذا لم يكن مناسبا في الواجهة، ينبغي منعه. في المقال التالي سنتناول المزيد من التفاصيل حول اﻷحداث التي تلي حركة المؤشّر وكيفية تتبّع تغيير العناصر التي تحته. التمارين قائمة قابلة للتحديد اﻷهمية: 5 أنشئ قائمة تكون عناصرها قابلة للتحديد، كما في مديري الملفّات. بالنقر على عنصر من القائمة يُحدَّد ذلك العنصر فقط (بإضافة الصنف selected.) ويزال التحديد عن بقيّة العناصر. إذا تمّ النقر مع المفتاح Ctrl (Cmd بالنسبة لماك)، فإنّ التحديد ينقلب (toggled) في ذلك العنصر، ولا تتغيّر بقيّة العناصر. يمكن مشاهدة المثال من هنا. ملاحظة: في هذا التمرين، يمكننا افتراض أنّ عناصر القائمة مكوّنة من نصّ فقط. ليست هناك وسوم متداخلة. ملاحظة أخرى: امنع تحديد المتصفّح اﻷصيل للنصوص عند النقر عليها. أنجز التمرين في البيئة التجريبية الحل افتح الحل في البيئة التجريبية ترجمة -وبتصرف- للمقال Mouse events من سلسلة Browser: Document, Events, Interfaces لصاحبها Ilya Kantor
-
لنتعمّق في المزيد من التفاصيل حول اﻷحداث التي تقع عندما تتحرّك الفأرة بين العناصر. اﻷحداث 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
-
يُعدّ السحب والإفلات وسيلة جيّدة في واجهة المستخدم. إذ أنّ أخذ الشيء وسحبه ثمّ إفلاته طريقة واضحة وبسيطة لفعل الكثير من اﻷمور، من نسخ المستندات ونقلها (كما في مدير الملفّات) إلى إجراء الطلبات (كوضع السلع في سلّة المشتريات). في مواصفة 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
-
تُعدّ أحداث المؤشّر طريقة حديثة للتعامل مع المُدخلات النابعة من مختلف أجهزة التأشير، كالفأرة والقلم والشاشات اللمسيّة وغيرها. لمحة تاريخية مختصرة لنأخذ بلمحة قصيرة، لتتمكّن من فهم الصورة العامّة وكذا مكانة أحداث المؤشّر وسط أنواع اﻷحداث اﻷخرى. في القديم، لم تكن هناك سوى أحداث الفأرة. ثمّ انتشرت اﻷجهزة اللمسيّة، مثل الهواتف واﻷجهزة اللوحيّة. ولكي تعمل النصوص البرمجيّة القائمة، كانت هذه اﻷجهزة (ولا زالت) تولّد أحداث الفأرة. فعلى سبيل المثال، يولّد الضغط على شاشة لمسيّة الحدث 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
-
قبل أن نأتي إلى لوحة المفاتيح، يُرجى التنبّه إلى أنّ للأجهزة الحديثة سُبلًا أخرى إلى "إدخال شيء ما". فقد يستخدم الناس مثلا تقنيّة التعرّف على الكلام (خاصّة على اﻷجهزة المحمولة) أو النسخ/اللصق بواسطة الفأرة. ولذلك، إذا أردنا تتبّع جميع المُدخلات في الحقل <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
-
يمكّن الحدث 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
-
تمتلك الاستمارات وعناصر التحكّم فيها، مثل <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
-
يمكن للعنصر أن يحصل على التركيز من المستخدم إمّا بالنقر عليه مباشرة أو باستعمال المفتاح 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
-
لنتناول مختلف اﻷحداث التي ترافق تحديث المُعطيات. الحدث: 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
-
يقع الحدث 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
-
لدورة حياة صفحة 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
-
يمكّننا المتصفّح من تتبّع تحميل الموارد الخارجيّة، مثل السكربتات والعناصر 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
-
تجدر بنا الإشارة إلى قاعدة مهمة قبل الشروع في الحديث عن كيفية تعامل جافاسكربت مع الأنماط والأصناف. القاعدة بديهية جدًا ولكننا سنذكرها للإفادة. هناك طريقتان تُستخدمان لتنسيق عنصرٍ ما: إنشاء صنف في ملف CSS وإضافته للعنصر على الشكل التالي: <div class="..."> كتابة خاصيات السمة style مباشرة بين تسلسلات التهريب الخاصة بالعنصر على الشكل التالي: <div style="..."> تستطيع لغة جافاسكربت التعديل على خاصيات الأصناف وخاصيات الخاصية style. ويُستحسن استعمال أصناف CSS للتنسيق بدلا من السمة style، حيث نلجأ إلى الطريقة الثانية فقط إذا تعذّرعلينا إضافة التنسيق باستعمال الطريقة الأولى. فعلى سبيل المثال، يمكن استعمال الخاصية style إذا كان عليك حساب إحداثيات عنصر ما ديناميكيًا وتحديد قيمها باستعمال جافاسكربت كالآتي: let top = /* عمليات حسابية معقّدة */; let left = /* عمليات حسابية معقّدة */; elem.style.left = left; // e.g '123px', تُحسب أثناء التنفيذ elem.style.top = top; // e.g '456px' وفي حالات أخرى مثل تلوين النص بالأحمر، أو إضافة صورة للخلفية، يُستحسن وصف التنسيق باستعمال CSS ثم إضافة الصنف للعنصر (يمكن عمل ذلك باستعمال جافاسكربت)، حيث يمنحك ذلك أكثر مرونة وسهولة في البرمجة. اسم الصنف (className) وقائمة الأصناف (classList) يُعدّ تعديل الصنف أكثر عمليةٍ نُصادفها في السكربتات. وكانت جافاسكربت فيما مضى تتّسمُ بالمحدودية حين يتعلّق الأمر بالكلمة المحجوزة "class"، حيث لم تكن تسمح بأن تحمل خاصيةٌ من خواص الكائن (object) اسم "class" كالآتي : elem.class. حينها جاء التفكير في استحداث خاصية مشابهة تسمى "className" تُطبّق على الأصناف. حيث يمثِّل elem.className اسم السمة "class" كما هو مبين في المثال التالي: <body class="main page"> <script> alert(document.body.className); // main page </script> </body> See the Pen JS-p2-08-Styles and classes-ex1 by Hsoub (@Hsoub) on CodePen. في حالة ما إذا أسندنا قيمةً معينةً للخاصية elem.className، تعوِّض هذه القيمة مُجمل سلسلة الأصناف. هذا ما نحتاج إليه أحيانا، ولكننا نحتاج في غالبية الحالات إلى إضافة أو حذف صنفٍ واحدٍ فقط. ولهذا وُجدت خاصية أخرى؛ إنها خاصية elem.classList. تُعدّ هذه الخاصية كائنًا خاصًا بحد ذاته له دوالّه الخاصة (methods) لإضافة صنف ما (add) أو حذفه (remove) أو إما إضافته إن لم يكن موجودًا أو حذفه إن وُجد (toggle).كما هو مبين في المثال التالي: <body class="main page"> <script> // إضافة صنف document.body.classList.add('article'); alert(document.body.className); // main page article </script> </body> See the Pen JS-p2-08-Styles and classes-ex2 by Hsoub (@Hsoub) on CodePen. وبذلك يمكننا إجراء عمليات على مجمل سلسلة الأصناف دفعة واحدة باستعمال className أو على الأصناف، كلٌّ على حدى، باستعمال classList. اختيارنا لهذا أو ذاك مرتبط بما نحتاج القيام به. الدوالّ الخاصة بـالخاصية classList هي: *elem.classList.add/remove("class"): إضافة الصنف المذكور كوسيط للدلّة/حذف الصنف المذكور كوسيط للدلّة. *elem.classList.toggle("classe"): إضافة الصنف المذكور كوسيط للدلّة إن لم يكن موجودًا أو حذفه إن وُجد. *elem.classList.contains("class"): البحث عن الصنف المذكور كوسيط للدلّة، والنتيجة تكون صحيح أو خطأ (true/false). وتقبل الخاصية classList الإدماج داخل حلقة التكرار for....of لإظهار قائمة الأصناف كما في المثال التالي: <body class="main page"> <script> for (let name of document.body.classList) { alert(name); // main, and then page } </script> </body> See the Pen JS-p2-08-Styles and classes-ex3 by Hsoub (@Hsoub) on CodePen. تنسيق العنصر باستعمال الخاصية style تُعدّ الخاصية elem.style كائنًا يحمل محتوى السمة style. ويؤدي إسناد القيمة "100px" للخاصية elem.style.width على الشكل التالي: elem.style.width="100px" إلى النتيجة نفسها لو كانت السمة style تحمل السلسلة النصية "width:100px". إذا كان اسم السمة يتكون من عدة كلمات، يُشكَّل اسم الخاصية بجعل الحرف الأول من كل كلمة حرفا كبيرًا ماعدا الكلمة الأولى كما في المثال التالي: background-color => elem.style.backgroundColor z-index => elem.style.zIndex border-left-width => elem.style.borderLeftWidth ملاحظة: الخاصيات التي تبدأ ببادئة تتبع الخاصيات التي تبدأ ببادئة تُحدّد المتصفح نفس القاعدة، كالخاصيتين -moz-border-radius و-webkit-border-radius، حيث تُترجَم الشرطة إلى حرفٍ كبيرٍ كالآتي: button.style.MozBorderRadius = '5px'; button.style.WebkitBorderRadius = '5px'; تغيير قيمة خاصية التنسيق style يحدث أن تَرغب في إسناد قيمةٍ للخاصية style ثم حذفها لاحقا. يمكننا على سبيل المثال إسناد القيمة "none" للخاصية elem.style.display كالأتي elem.style.display = "none" ثم حذفها وكأننا لم نحدّد لها قيمةً من قبل. هنا، ينبغي إسناد سلسلة نصية فارغة للخاصية elem.style.display كالآتي elem.style.display = "" بدلا من حذفها (delete). //عند تنفيذ هذا السكربت يختفي العنصر <body> ثمّ يُعاود الظهور document.body.style.display = "none"; // يختفي setTimeout(() => document.body.style.display = "", 1000); // يُعاود الظهور إذا أسندنا سلسلة نصية فارغة للخاصية style.display، يُطبِّق المتصفح أصناف CSS والأنماط التنسيقية المتضمَّنة بداخلها بطريقة عاديةٍ جدًا وكأن الخاصية style.display غير موجودة تماما. ملاحظة: التعديل على خاصيات الخاصية style جملةً واحدةً باستعمال الخاصية style.cssStyle تُستعمل عادة الخاصية *.style للتعديل على قيم خاصيات التنسيق، كلٌ على حدى، ولا يمكننا التعديل عليها دفعة واحدة كالآتي: div.style="color:red; width:100px"، لأن div.style هو كائنٌ لا يمكن التعديل عليه (ِread-only) بهذه الطريقة. يمكن تغيير التنسيق كاملا دفعة واحدة بإسناد سلسلة نصية (تحمل وصف التنسيق) للخاصية style.cssStyle كما في المثال التالي: <div id="div">Button</div> <script> // يمكننا استعمال رايات تنسيقية خاصة مثل الراية “important” div.style.cssText=`color: red !important; background-color: yellow; width: 100px; text-align: center; `; alert(div.style.cssText); </script> See the Pen JS-p2-08-Styles and classes-ex4 by Hsoub (@Hsoub) on CodePen. غير أنه من النادر استعمال هذه الخاصية كونها تحذف الأنماط التنسيقية السابقة وتستبدلها بالقيم الجديدة، أي أنها قد تحذف أشياء مازلنا بحاجتها. فيما يمكن أن تُستخدم لتنسيق العناصر الجديدة، فلن يؤدي إسناد القيم بهذه الطريقة إلى أيّ عملية حذف (بما أن العناصر الجديدة لا تملك تنسيقات بعد). ويمكننا عمل ذلك أيضا باستعمال الدالّة div.setAttribute('style', 'color: red...'). الوحدات لا تنس إضافة الوحدات للقيم في شيفرة CSS، فلا يصِحُّ إسناد القيمة "10" للخاصية elem.style.top بل القيمة 10px هي الأصح، وإلا فلن يعمل السكربت بالشكل المطلوب. <body> <script> // لا يعمل document.body.style.margin = 20; alert(document.body.style.margin); // '' (سلسلة نصية فارغة، إهمال عملية الإسناد) // يعمل بعد إضافة الوحدة document.body.style.margin = '20px'; alert(document.body.style.margin); // 20px alert(document.body.style.marginTop); // 20px alert(document.body.style.marginLeft); // 20px </script> </body> See the Pen JS-p2-08-Styles and classes-ex5 by Hsoub (@Hsoub) on CodePen. لاحظ في السطرين الأخيرين أن المتصفح يفكّك الخاصية style.margin إلى خاصيتين وهما: style.marginLeft وstyle.marginTop. الأنماط المحسوبة باستعمال الدالة getComputedStyle يُعدّ التعديل على الأنماط عمليةً سهلةً ولكن كيف تُقرأ الأنماط؟ نريد مثلا معرفة مقاس، هوامش ولون عنصرٍ ما، كيف نتحصل عليها؟ تعمل الخاصية style على تعديل قيمة السمة style فقط دون الوصول إلى الأنماط الموصوفة في الأوراق التنسيقية المتتالية CSS. وبالتالي لا يمكننا قراءة أيّ قيمٍ من أصناف CSS باستعمال الخاصية elem.style. فعلى سبيل المثال لا يمكن أن تصل الخاصية style في هذا المثال إلى الهامش. <head> <style> body { color: red; margin: 5px } </style> </head> <body> The red text <script> alert(document.body.style.color); // فارغة alert(document.body.style.marginTop); // فارغة </script> </body> See the Pen JS-p2-08-Styles and classes-ex6 by Hsoub (@Hsoub) on CodePen. ماذا لو أردنا على سبيل المثال إضافة 20px للهامش؟ سيكون علينا أولا الوصول إلى القيمة الحالية له حتى يتسنى لنا تعديلها. وهنا لدينا طريقة أخرى للحصول على ذلك وتكون باستعمال الدالة getComputedStyle وبنيتها كالآتي: getComputedStyle(element, [pseudo]) حيث يمثّل العنصر element العنصر الذي سنحسب قيمه ويمثل pseudo العنصر الزائف، مثلا before::. إذا كانت قيمة pseudo عبارة عن سلسلة نصية فارغة أو غير موجودة أصلا فهذا يعني أننا نقصد العنصر نفسه. وتكون مخرجات الدالّة (output) عبارة عن كائن يحوي أنماط تنسيقية مثله مثل elem.style ولكن يأخذ في الحسبان هذه المرة كلّ الأصناف الموجودة في ملف CSS. وفيما يلي مثال على ذلك: <head> <style> body { color: red; margin: 5px } </style> </head> <body> <script> let computedStyle = getComputedStyle(document.body); // يمكننا الآن قراءة اللون والهامش alert( computedStyle.marginTop ); // 5px alert( computedStyle.color ); // rgb(255, 0, 0) </script> </body> See the Pen JS-p2-08-Styles and classes-ex7 by Hsoub (@Hsoub) on CodePen. ملاحظة: القيم المحسوبة والقيم النهائية (المُحدَّدة) هناك مفهومان في لغة CSS هما: قيمة تنسيقية محسوبة وهي القيمة المتحصّل عليها بعد تطبيق مجمل القواعد التنسيقية وقواعد الوراثة المُتضمَّنة في ملف CSS. قد تكون على شكل height:1em أو font-size:125% . قيمة تنسيقية نهائية (مُحدَّدة) وهي القيمة التي يقع عليها الاختيار في آخر المطاف وتُطبَّق على العنصر. القيمتان 1em و125% هما قيمتان نسبيتان. يأخذ المتصفح كافة القيم المحسوبة ويجعل كافة الوحدات مطلقة كما في المثال التالي: height:20px ،font-size:16px. ويمكن للقيم النهائية الخاصة بالخاصيات الهندسية أن تكون عشرية مثل: width:50.5px. لقد اُستحدثت الدالّة getComputedStyle أساسا للحصول على قيم محسوبة ولكن تبيّن فيما بعد أن القيم النهائية (المُحدَّدة) أحسن، فتغيرت المعايير، وأصبحت الدالّة getComputedStyle تُخرِج القيم النهائية للخاصية والتي تكون غالبا بالبكسل px بالنسبة للخاصيات الهندسية. ملاحظة: تتطلب الدالة getComputedStyle ذكر الاسم الكامل للخاصية علينا البحث على الدوام عن الخاصية التي نحتاج إليها بدقة مثل: padingLeft، أو marginTop أو borderTopWidth وإلا فلن نتمكن من ضمان صحة النتيجة المتحصّل عليها. فمع وُجود، على سبيل المثال، الخاصيتين padingLeft/padingTop، على ماذا سوف نحصل عند تنفيذ الدالّة getComputedStyle(elem, pading)؟ لن نحصل على شيئ؟ أو ربما سنحصل على قيمة "مستوحاة" من قيم معرّفة مسبقا للحاشية (pading)؟ في الحقيقة لا يوجد أيّ معايير تتحدث عن هذا الموضوع. وهناك بعض التناقضات الأخرى، حيث تُظهِر بعض المتصفحات (Chrome مثلا) في مثال الموالي القيمة 10px، ولا تُظهِرها متصفحات أخرى (كالمتصفح Firefox). مثال: <style> body { margin: 10px; } </style> <script> let style = getComputedStyle(document.body); alert(style.margin); // نحصل على سلسلة فارغة عند استعمال المتصفح Firefox </script> See the Pen JS-p2-08-Styles and classes-ex8 by Hsoub (@Hsoub) on CodePen. ملاحظة: الأنماط التي تُطبَّق على الروابط :visited تكون مخفية يمكن تلوين الروابط التي سبق وأن زيرت باستخدام الصنف الزائف :visited في ملف CSS. لكن الدالّة getComputedStyle لا يمكنها الوصول إلى هذا اللون، لأن ذلك يمكِّن أيّ صفحة كانت من إنشاء الرابط على الصفحة والإطلاع على الأنماط وبالتالي معرفة ما إذا كان المستخدم قد زار الرابط من قبل. لا يمكن للغة جافاسكربت الإطلاع على الأنماط المعرّفة باستخدام الصنف الزائف :visited، كما تمنع لغة CSS تطبيق تنسيقاتِ تغيير الشكل والأبعاد (geometry-changing styles) ضمن الصنف الزائف :visited وذلك لغلق الطريق أمام أيّ صفحةٍ مشبوهةٍ تسعى لمعرفة ما إذا زار المستخدم الرابط أم لا، وبالتالي التعدي على خصوصيته. الخلاصة هناك خاصيتان تُستخدمان للعمل على الأصناف وهما: className: وهي سلسلة نصية تُستخدم للعمل على كافة الأصناف دفعةً واحدةً. classList: هي عبارة عن كائن له دوالّه الخاصة (add/delete/toggle/contains) وتستخدم للعمل على الأصناف، كلُ على حدى. ولتغيير التنسيق لدينا: الخاصية style؛ وهي عبارة عن كائن تُشكَّل خواصه بجعل الحرف الأول من كل كلمة حرفا كبيرًا ما عدا الكلمة الأولى. تُعدّ قراءته والتعديل عليه تماما كالتعديل على خاصيات السمة style، كلٌ على حدى. وللاطلاع على كيفية إضافة الراية important وغيرها، يمكنك زيارة موقع MDN حيث تجد قائمة من الدوالّ التي تُستخدم لذلك. الخاصية style.cssText: هي الخاصية التي تقابِل السمة "style" في مجملها، أي السلسلة النصية التي تحمل كافة الأنماط التنسيقية دفعةً واحدةً. ولقراءة الأنماط التنسيقية النهائية (التي تأخذ في الحسبان كافة الأصناف بعد تطبيق تنسيقات CSS وحساب القيم النهائية) وُجدَت الدالة getComputedStyle(elem, [pseudo]) والتي تخرِج/تعيد كائنا يحمل التنسيقات وهو قابلٌ للقراءة فقط. تمرين إنشاء إشعار درجة الأهمية: 5 اكتب شيفرة الدالّة showNotification(options) التي تُنشِئ إشعارًا كالآتي: بالمحتوى الذي يُمرَّر لها كوسيط، حيث يختفي الإشعار بعد ثانية ونصف من إظهاره. وتُوفّر الخيارات التالية: // أظهِر عنصرًا يحمل النص "Hello" بالقرب من الركن العلوي الأيمن للنافذة showNotification({ top: 10, // عشرة بكسل بدءًا من أعلى النافذة والذي فاصلته 0 right: 10, // عشرة بكسل بدءًا من الحافة اليمنى للنافذة والتي ترتيبتها 0 html: "Hello!", // شيفرة HTML الخاصة بالإشعار className: "welcome" // صنف إضافي للحاوية ‘div’ (اختياري) }); استعمل تنسيقات CSS لتحديد موضع إظهار العنصر حسب الإحداثيات المعطاة بافتراض أن الصفحة تحتوي مسبقا على الأنماط التنسيقية الضرورية. الحل يمكنك الإطلاع على الحل من هنا ترجمة -وبتصرف- للفصل Styles and classes من كتاب Browser: Document, Events, Interfaces
- 1 تعليق
-
- 2
-
يمكّننا انتشار اﻷحداث نحو اﻷسفل واﻷعلى من تطبيق أحد أقوى أنماط معالجة الأحداث، وهو ما يُسمى تفويض الأحداث (event delegation). تتلخّص الفكرة في أنه إذا كان لدينا الكثير من العناصر التي تُعالج بطريقة متماثلة، فبدل أن نسند معالجا لكلّ منها، فإنّنا نسند معالجا واحدا إلى السلف الذي يلتقون فيه. في داخل المعالج، يمكن أن نتعرّف على مكان وقوع الحدث من خلال event.target، ثم نعالجه. لنرى مثالا على ذلك -- مخطط باكوا الذي يعكس الفلسفة الصينية القديمة، كما هو مبيّن من هنا. ويمكن تمثيله بواسطة HTML كالتالي: <table> <tr> <th colspan="3"><em>Bagua</em> Chart: Direction, Element, Color, Meaning</th> </tr> <tr> <td class="nw"><strong>Northwest</strong><br>Metal<br>Silver<br>Elders</td> <td class="n">...</td> <td class="ne">...</td> </tr> <tr>...2 more lines of this kind...</tr> <tr>...2 more lines of this kind...</tr> </table> See the Pen JS-p2-event-delegation -ex1 by Hsoub (@Hsoub) on CodePen. يحتوي الجدول على 9 خانات، لكنها قد تكون 99 أو 9999، لا يهمّ ذلك. مهمّتنا هي إبراز الخانة عند النقر عليها. بدل إسناد معالج onclick إلى كلّ <td> (قد يكون هناك الكثير منها)، سنسند المعالج "catch-all" إلى العنصر <table>. يستخدم المعالجُ الخاصيّة event.target للحصول على العنصر الذي نُقر عليه ثم يبرزه. إليك الشيفرة: let selectedTd; table.onclick = function(event) { let target = event.target; // أين كان النقر؟ if (target.tagName != 'TD') return; // ؟ إذًا لا يهمّنا ذلك TD ليس في highlight(target); // أبرزه }; function highlight(td) { if (selectedTd) { // أزل الإبراز الحالي إن وُجد selectedTd.classList.remove('highlight'); } selectedTd = td; selectedTd.classList.add('highlight'); // الجديدة td أبرز الـ } لا تكترث هذه الشيفرة بعدد الخانات التي في الجدول. يمكننا إضافة وإزالة الخانات <td> ديناميكيا في أي وقت، وستظل وظيفة الإبراز تعمل. لكن تبقى هناك نقيصة. قد لا يقع النقر على العنصر <td> بعينه ولكن على عنصر آخر بداخله. ففي حالتنا هذه، لو ألقينا نظرة داخل HTML، سنلاحظ أوسمة مدرجة داخل <td>، مثل <strong>: <td> <strong>Northwest</strong> ... </td> فمن الطبيعي أنه لو وقع النقر على <strong>، فسيصير هو القيمة التي يحملها event.target. داخل المعالج table.onclick، ينبغي علينا أن نأخذ event.target ونتحقق إن كان النقر قد وقع داخل <td> أو لا. هذه هي الشيفرة المحسّنة: table.onclick = function(event) { let td = event.target.closest('td'); // (1) if (!td) return; // (2) if (!table.contains(td)) return; // (3) highlight(td); // (4) }; إليك بعض الإيضاحات: يعيد التابع elem.closest(selector) أقرب سلف يطابق المُحدِّد selector. في حالتنا، نبحث عن أقرب <td> نصادفه صعودًا من العنصر المصدري. إذا لم يكن event.target بداخل أيّ <td>، فسيُعاد الاستدعاء مباشرة، إذ ليس هناك شيء لفعله. في حال تداخل الجداول، قد تكون event.target هي <td>، لكنّها موجودة خارج الجدول الحاليّ. فنتحقق إذًا من أنّ <td> خاصّة بجدولنا الحاليّ. وإذا كانت كذلك، نبرزها. وبذلك، تكون لدينا شيفرة سريعة وفعّالة للإبراز، لا تكترث بعدد الخانات التي في الجدول. مثال عن التفويض: الأفعال داخل الترميز (markup) هناك استعمالات أخرى لتفويض الأحداث. لنقُل أننا نود إنشاء قائمة من الأزرار: "حفظ" و "تحميل" و "بحث" وغير ذلك. ويوجد هناك كائن له التوابع save و load و search … فكيف يتم الربط بين هذه التوابع واﻷزرار؟ أوّل ما قد يتبادر إلى الذهن هو إسناد معالج إلى كلّ زر. ولكنّ هناك حلًّا أكثر أناقة. يمكننا إسناد معالج إلى القائمة بأكملها و إضافة سمات data-action للأزرار تحمل التابع الذي سيُستدعى: <button data-action="save">Click to Save</button> يقرأ المعالج السمة، وينفّذ التابع. ألقِ نظرة على المثال أدناه: <div id="menu"> <button data-action="save">Save</button> <button data-action="load">Load</button> <button data-action="search">Search</button> </div> <script> class Menu { constructor(elem) { this._elem = elem; elem.onclick = this.onClick.bind(this); // (*) } save() { alert('saving'); } load() { alert('loading'); } search() { alert('searching'); } onClick(event) { let action = event.target.dataset.action; if (action) { this[action](); } }; } new Menu(menu); </script> See the Pen JS-p2-event-delegation -ex2 by Hsoub (@Hsoub) on CodePen. يُرجى ملاحظة أن this.onClick مرتبط بـ this في (*). هذا مهمّ، لأنّه لو لم يكن كذلك فإن this الذي بداخله سيشير إلى عنصر (elem) ، وليس الكائن Menu، ولا يكون بذلك this[action] هو ما نحتاجه. فما هي إذًا المزايا التي يقدّمها تفويض الأحداث هنا؟ لا نحتاج كتابة شيفرة لإسناد معالج لكل زر. بل ننشئ فقط تابعًا ونضعه في الترميز. تصبح بنية HTML مرنة، فيمكننا إضافة وإزالة أزرار بسهولة. يمكننا أيضا استخدام أصناف مثل .action-save و .action-load، لكن سمة مثل data-action أفضل من الناحية الدلالية، بالإضافة إلى إمكانية استخدامها في قواعد CSS. نمط "السلوك" يمكننا أيضا استخدام تفويض الأحداث لإضافة "سلوكيّات" للعناصر تصريحيًّا، بواسطة سمات وأصناف خاصة. يتألّف هذا النمط من جزأين: نضيف سمة مخصّصة إلى العنصر تعبّر عن سلوكه. يتتبع الأحداثَ معالجٌ على نطاق المستند، فإذا وقع حدث على عنصر له سمة، فإنه يقوم بالفعل المناسب. مثال عن السلوك: العداد على سبيل المثال، تضيف السمة data-counter هنا سلوك "زيادة القيمة عند النقر" للأزرار: Counter: <input type="button" value="1" data-counter> One more counter: <input type="button" value="2" data-counter> <script> document.addEventListener('click', function(event) { if (event.target.dataset.counter != undefined) { // إذا كانت السمة موجودة... event.target.value++; } }); </script> See the Pen JS-p2-event-delegation -ex3 by Hsoub (@Hsoub) on CodePen. إذا نقرنا على أحد الأزرار، فإن القيمة التي عليه ستزداد. بغضّ النظر عن هذه الأزرار، فإن المنهجية العامة المتبعة هنا مهمّة. يمكن أن يكون هناك من السمات مع data-counter بقدر ما نرغب. يمكننا إضافة سمات جديدة إلى HTML في أي وقت. باستخدام تفويض الأحداث، نكون قد "وسّعنا" HTML من خلال إضافة سمة تعبّر عن سلوك جديد. تنبيه: استخدم دائما addEventListener في المعالجات التي على مستوى المستند عند إسناد معالج حدثٍ إلى الكائن document، يجب أن نستخدم دائما addEventListener، وليس document.on<event>، لأن هذا الأخير سيؤدي إلى تعارضات: تستبدل المعالجاتُ الجديدة المعالجات القديمة. في المشاريع الواقعية، من الطبيعي أن تكون هناك عدة معالجات قد أُسندت بواسطة أجزاء مختلفة من الشيفرة. مثال عن السلوك: القالِب (toggler) لنرى مثالا آخر عن السلوك. يؤدي النقر على عنصرٍ له السمة data-toggle-id إلى إخفاء وإظهار العنصر الذي له ذاك الـ id. <button data-toggle-id="subscribe-mail"> Show the subscription form </button> <form id="subscribe-mail" hidden> Your mail: <input type="email"> </form> <script> document.addEventListener('click', function(event) { let id = event.target.dataset.toggleId; if (!id) return; let elem = document.getElementById(id); elem.hidden = !elem.hidden; }); </script> See the Pen JS-p2-event-delegation -ex4 by Hsoub (@Hsoub) on CodePen. لنلاحظ مرة أخرى ما قمنا به. لإضافة وظيفة القلب إلى عنصرٍ ما من الآن فصاعدًا، لا حاجة لمعرفة جافاسكربت، يكفي استخدام السمة data-toggle-id. قد يصير هذا ملائما بالفعل -- لا حاجة لكتابة جافاسكربت لكل واحد من هذه العناصر. يكفي استخدام السلوك فقط. يجعل المعالج الذي على مستوى المستند ذلك يعمل مع أي عنصر على الصفحة. يمكننا أن نجمع بين عدّة معالجات في نفس العنصر أيضا. قد يشكل نمط "السلوك" بديلا عن الأجزاء المصغرة (mini-fragments) في جافاسكربت. الملخص تفويض الأحداث رائع حقًّا! إذ يُعدّ واحدا من أنفع الأنماط المتعلّقة بأحداث DOM. كثيرا ما يُستخدم تفويض اﻷحداث لإضافة نفس المعالج لعدّة عناصر متماثلة، لكن لا يقتصر اﻷمر على ذلك. الخوارزمية: أسند معالجًا وحيدًا إلى العنصر الحاوي. في المعالج -- افحص العنصر المصدري event.target. إذا وقع الحدث داخل عنصر يهمّنا، عالج الحدث. المزايا: يبسّط التهيئة ويوفّر الذاكرة: لا حاجة لإضافة عدة معالجات. أقلّ شيفرة: عند إضافة أو إزالة عناصر، لا داعي لإضافة أو إزالة المعالجات. التعديلات على DOM: يمكننا إضافة أو إزالة العناصر جماعيا بواسطة innerHTML وما إلى ذلك. للتفويض حدود أيضا بالطبع: أولًا، يجب أن يكون الحدث منتشرًا نحو اﻷعلى. بعض الأحداث لا تنتشر نحو اﻷعلى. يجب كذلك أن لا تَستخدم المعالجاتُ التي في الأسفل event.stopPropagation(). ثانيًا، قد يضيف التفويض عبئًا على وحدة المعالجة المركزية (CPU)، لأنّ المعالج الذي على مستوى الحاوي يستجيب للأحداث في أي مكان في الحاوي، بغضّ النظر عن كونها مهمّة لنا أو لا. لكن العبئ عادةً طفيف، فلا نأخذه بالحسبان. التمارين أخفي الرسائل باستخدام التفويض الأهمية: 5 هناك قائمة من الرسائل لها أزرار لإزالتها [x]. اجعل الأزرار تعمل. كما هو مبيّن هنا. ملاحظة: يجب أن يكون هناك منصت واحد للأحداث على الحاوي. استخدم تفويض الأحداث. أنجز التمرين في البيئة التجريبية الحل افتح الحل في البيئة التجريبية قائمة شجرية الأهمية: 5 أنشئ شجرة يمكن فيها إظهار وإخفاء العقد الأبناء بواسطة النقر: كما هو مبيّن هنا. المتطلبات: معالج واحد للأحداث فقط (استخدم التفويض). يجب ألا يفعل النقر خارج عقدة العنوان (في مساحة فارغة) أي شيء. أنجز التمرين في البيئة التجريبية الحل ينقسم الحل إلى جزئين: ضع كلّ عقدة عنوان في الشجرة داخل <span>. بهذا يمكننا إضافة تنسيقات CSS إلى :hover و معالجة النقرات على النص بالضبط، لأن عُرض <span> هو نفس عُرض النص بالضبط (بخلاف ما لو كان بدونه). عيّن معالجًا على العقدة الجذر tree، وعالج النقرات على العنوانين <span> تلك. افتح الحل في البيئة التجريبية جدول قابل للترتيب الأهمية: 4 اجعل الجدول قابلًا للترتيب: يجب أن يؤدي النقر على العناصر <th> إلى ترتيبه حسب العمود الموافق. لكلّ <th> نوع معين موجود بداخل السمة، هكذا: <table id="grid"> <thead> <tr> <th data-type="number">Age</th> <th data-type="string">Name</th> </tr> </thead> <tbody> <tr> <td>5</td> <td>John</td> </tr> <tr> <td>10</td> <td>Ann</td> </tr> ... </tbody> </table> في المثال أعلاه، يحتوي العمود الأول على الأرقام، و العمود الثاني على الحروف. يجب أن تقوم دالة الترتيب بمعالجة الترتيب حسب النوع. يستلزم فقط أن يُدعم النوعان "string" و "number". يمكن مشاهدة المثال يعمل من هنا. ملاحظة: يمكن أن يكون الجدول كبيرا، بأي عدد من الأسطر والأعمدة. أنجز التمرين في البيئة التجريبية الحل افتح الحل في البيئة التجريبية سلوك التلميحات الأهمية: 5 أنشئ شفرة جافاسكربت لأجل سلوك التلميحات (tooltips). عندما يحوم مؤشر الفأرة فوق عنصر له السمة data-tooltip، فيجب أن تظهر التلميحة فوقه، وعندما يفارقه فإنها تختفي. هذا مثال لشفرة HTML مع الشرح: <button data-tooltip="the tooltip is longer than the element">Short button</button> <button data-tooltip="HTML<br>tooltip">One more button</button> يجب أن تعمل كما هنا. سنفترض في هذا التمرين أن جميع العناصر التي لها data-tooltip تحتوي على نص فقط. لا وسوم متداخلة (بعد). التفاصيل: يجب أن تكون المسافة بين العنصر والتلميحة 5px. يجب أن تكون التلميحة في منتصف العنصر، إن أمكن ذلك. يجب ألا تقطع التلميحة حوافّ النافذة. من المفترض أن تكون التلميحة فوق العنصر، فإذا كان العنصر في أعلى الصفحة ولا مكان هناك للتلميحة، فإنها تكون تحته. يُعطى محتوى التلميحة في السمة data-tooltip. يمكنها أن تحوي أي شفرة HTML. ستحتاج إلى حدثين هنا: mouseover يحصل عندما يحوم المؤشر فوق العنصر. mouseout يحصل عندما يفارق المؤشر العنصر. يُرجى استخدام تفويض الأحداث: أسند اثنين من المعالجات إلى document لتتبّع كلّ "الحومان" و "المفارقة" للعناصر التي لها data-tooltip وقم بإدارة التلميحات من هناك. بعد تطبيق السلوك، يمكن ولو لأناس غير متعودين على جافاسكربت إضافة عناصر بتلميحات. ملاحظة: يجب أن تظهر تلميحة واحدة فقط في نفس الوقت. أنجز التمرين في البيئة التجريبية الحل افتح الحل في البيئة التجريبية ترجمة -وبتصرف- للمقال Event delegation من سلسلة Browser: Document, Events, Interfaces لصاحبها Ilya Kantor
-
لنبتدئ بمثال. المعالج التالي مُسندٌ إلى العنصر <div>، لكنّه أيضًا يشتغل عند النقر على الوسوم الداخلة تحته مثل <em> أو <code>. <div onclick="alert('The handler!')"> <em>If you click on <code>EM</code>, the handler on <code>DIV</code> runs.</em> </div> See the Pen JS-p2-bubbling-and-capturing -ex1 by Hsoub (@Hsoub) on CodePen. أليس هذا غريبًا بعض الشيء؟ لماذا يشتغل المعالج المُسنَد إلى <div> إذا كان النقر في الواقع على <em>؟ انتشار اﻷحداث نحو اﻷعلى مبدأ الانتشار نحو اﻷعلى (bubbling) بسيط. عندما يقع حدثٌ على عنصرٍ ما، فإنّه يُشغّل المعالجات المسندة إليه، ثم المسندة إلى أبيه، وهكذا صعودًا إلى المعالجات المسندة إلى أسلافه الآخرين. لنفترض أنّ لدينا ثلاثة عناصر متداخلة P < DIV < FORM ، مع معالجٍ لكلّ منها: <style> body * { margin: 10px; border: 1px solid blue; } </style> <form onclick="alert('form')">FORM <div onclick="alert('div')">DIV <p onclick="alert('p')">P</p> </div> </form> See the Pen JS-p2-bubbling-and-capturing -ex2 by Hsoub (@Hsoub) on CodePen. يؤدّي النّقر على العنصر <p> الذي بالداخل أوّلًا إلى تشغيل onclick : المُسند إلى <p> ذاك. ثم المُسند إلى <div> الذي خارجه. ثم المُسند إلى <form> الذي خارجه. وهكذا صعودًا إلى الكائن document. فإذا نقرنا على <p> ، سنرى ثلاثة تنبيهات متتالية: form <- div <- p. تُعرف على هذه العمليّة بالانتشار نحو اﻷعلى (bubbling)، لأنّ الأحداث تنتشر من العنصر الداخلي صعودًا عبر آبائه كالفقّاعة في الماء. تنبيه: تنتشر مُعظم اﻷحداث نحو اﻷعلى ينبغي التنبه في هذه الجملة إلى كلمة "مُعظم". على سبيل المثال، لا ينتشر الحدث focus نحو اﻷعلى. وسنرى أيضا أمثلة أخرى. لكنّها تبقى استثناءً عوض القاعدة، فمعظم اﻷحداث تنتشر نحو اﻷعلى. event.target يمكن للمعالج المسند إلى عنصرٍ أبٍ أن يتحصّل دائمًا على تفاصيل مكان وقوع الحدث. يُسمّى العنصر اﻷدنى الذي نشأ عنه الحدث بالعنصر "الهدف"، ويمكن الوصول إليه بواسطة event.target. لاحظ الاختلاف الذي بينه و this (الذي هو نفس event.currentTarget): event.target -- هو العنصر "الهدف" الذي أنشأ الحدث، ولا يتغيّر خلال عمليّة الانتشار نحو اﻷعلى. this -- هو العنصر "الحاليّ"، أي الذي أُسنِد إليه المعالجُ الذي يشتغل حاليّا. على سبيل المثال، إذا كان لدينا معالجٌ وحيدٌ form.onclick مُسندٌ إلى النموذج <form>، فإنّه يمكنه "التقاط" جميع النقرات داخل النموذج. أيّا كان مكان وقوعها، فإنّها تنتشر نحو اﻷعلى إلى <form> وتشغّل المعالج. في المعالج form.onclick: this (الذي هو نفس event.currentTarget) هو العنصر <form>، لأنّ المعالج المُشتغل مسندٌ إليه. event.target هو العنصر الذي نُقر عليه داخل النموذج. يمكنك رؤية ذلك من هنا ، من خلال النقر على مختلف العناصر لإظهار event.target و this في كلّ حالة. قد يكون event.target هو نفسه this، كما لو نقرنا هنا على العنصر <form> مباشرة. إيقاف الانتشار نحو اﻷعلى ينطلق الحدث عند انتشاره نحو اﻷعلى من العنصر الهدف مباشرة. ويواصل الانتشار عادةً إلى أن يصل إلى <html>، ومن ثَمّ إلى الكائن document، بل إنّ بعض اﻷحداث قد تصل إلى window، ويشغّل جميع المعالجات في طريقه إلى هناك. لكن قد يقرّر أحد المعالجات أن الحدث قد تمّت معالجته ويوقف بذلك عمليّة الانتشار. يتوقّف الانتشار نحو اﻷعلى بواسطة التابع event.stopPropagation(). على سبيل المثال، لا يشتغل المعالج body.onclick هنا عند النقر على <button>: <body onclick="alert(`the bubbling doesn't reach here`)"> <button onclick="event.stopPropagation()">Click me</button> </body> See the Pen JS-p2-bubbling-and-capturing -ex3 by Hsoub (@Hsoub) on CodePen. ملاحظة: ()event.stopImmediatePropagation إذا أُسنِد إلى عنصرٍ ما عدّةُ معالجات لنفس الحدث، فحتىّ لو أوقف أحدها الانتشار نحو اﻷعلى، ستشتغل المعالجات الأخرى. بعبارة أخرى، يوقِف التابع event.stopPropagation() الانتشار نحو اﻷعلى، لكن ستشتغل بقيّة المعالجات المسندة إلى العنصر الحاليّ. لإيقاف الانتشار نحو اﻷعلى، ومنع اشتغال بقيّة المعالجات المُسندة إلى العنصر الحاليّ أيضا، يوجد هناك تابع لذلك event.stopImmediatePropagation() لا يشتغل بعده معالج. تنبيه: لا توقف الانتشار نحو اﻷعلى دون الحاجة لذلك! الانتشار نحو اﻷعلى أمرٌ ملائم. لا توقفه دون سبب وجيه، يكون واضحًا ومُمحصًّا هندسيًّا. قد يُحدث التابع event.stopPropagation() أحيانًا مزالق تتسبّب لاحقًا في مشاكل. على سبيل المثال: ننشئ قائمة متداخلة. تعالج كلُّ قائمة داخليّة النقرات التي على عناصرها، وتستدعيstopPropagation لتفادي تفعيل القائمة الخارجيّة. نقرّر بعدها أن نلتقط جميع النقرات على النافذة، لتتبع سلوك المستخدمين (أين ينقر الناس). تقوم بعض أنظمة التحليل بذلك، وعادةً ما تستخدم الشيفرةُ التابعَ document.addEventListener('click'…) لالتقاط جميع النقرات. لن يعمل نظام التحليل على المساحة التي أوقف فيها انتشار النقرات نحو اﻷعلى بواسطة stopPropagation. فيكون بذلك لدينا "منطقة ميّتة" للأسف. لا توجد في العادة حاجة حقيقيّة لإيقاف الانتشار نحو اﻷعلى. فالمهام التي تبدو أنّها تتطلب ذلك يمكن حلّها بوسائل أخرى. من بين هذه الوسائل، استخدام اﻷحداث المخصّصة (custom events) التي سنتناولها لاحقا. يمكننا أيضا كتابة بياناتٍ على الكائن event في معالج وقراءتها في معالج آخر، ليتسنى بذلك تمرير معلومات إلى المعالجات المسندة إلى الآباء حول المعالجة التي تمت في اﻷسفل. الانتشار نحو الأسفل توجد هناك مرحلة أخرى لمعالجة اﻷحداث يُطلق عليها "الانتشار نحو اﻷسفل" (capturing). من النادر استخدامها في شيفرات واقعيّة، لكنّها قد تكون مفيدة أحيانا. يصِف معيار أحداث DOM ثلاث مراحل لانتشار الأحداث: مرحلة الانتشار نحو اﻷسفل (Capturing phase) - ينزل الحدث إلى العنصر. مرحلة الهدف (Target phase) - يصل الحدث إلى العنصر الهدف. مرحلة الانتشار نحو اﻷعلى (Bubbling phase) - ينتشر الحدث صعودًا من العنصر. هذه صورة لما يحصل عند النقر على <td> داخل جدول، مأخوذة من المواصفة: ما يعني ذلك: بالنقر على <td> ، ينتشر الحدث أوّلا عبر سلسلة الأسلاف نزولًا إلى العنصر (مرحلة الانتشار نحو اﻷسفل)، فيبلغ الهدفَ ويتفعّل هناك (مرحلة الهدف)، ثم ينتشر صعودًا (مرحلة الانتشار نحو اﻷعلى) مستدعيًا المعالجات في طريقه. اقتصرنا في السابق على مرحلة الانتشار نحو اﻷعلى، لأنّه من النادر استخدام مرحلة الانتشار نحو اﻷسفل. لا تظهر لنا عادةً. لا يعلم المعالجون الذين عُيّنوا على شكل خاصيّة on<event> ، أو على شكل سمة HTML، أو باستخدام addEventListener(event, handler) بوسيطين فقط، شيئًا عن الانتشار نحو اﻷسفل، فهم يشتغلون فقط في المرحلتين الثانية والثالثة. لالتقاط حدثٍ في مرحلة الانتشار نحو اﻷسفل، يجب تغيير قيمة الخيار capture إلى true. elem.addEventListener(..., {capture: true}) // {capture: true} فهو اختصار لـ ،"true" أو فقط elem.addEventListener(..., true) يمكن أن يأخذ الخيار capture قيمتين: إذا كانت false (افتراضيًّا)، فإنّ المعالج يوضع في مرحلة الانتشار نحو اﻷعلى. إذا كانت true، فإنّ المعالج يوضع في مرحلة الانتشار نحو اﻷسفل. لاحظ رغم أنّه يوجد رسميًّا ثلاث مراحل، إلّا أن المرحلة الثانية (مرحلة الهدف: عندما يبلغ الحدثُ الهدف) لا تُعالَج بشكل مستقل، بل تشتغل كلٌّ من المعالجات الموضوعة في مرحلتي الانتشار نحو اﻷسفل واﻷعلى في هذه المرحلة أيضا. لنرى كلًّا من الانتشار نحو اﻷسفل والأعلى حال عملهما: <style> body * { margin: 10px; border: 1px solid blue; } </style> <form>FORM <div>DIV <p>P</p> </div> </form> <script> for(let elem of document.querySelectorAll('*')) { elem.addEventListener("click", e => alert(`Capturing: ${elem.tagName}`), true); elem.addEventListener("click", e => alert(`Bubbling: ${elem.tagName}`)); } </script> See the Pen JS-p2-bubbling-and-capturing -ex4 by Hsoub (@Hsoub) on CodePen. تُسند الشيفرة معالجاتٍ لحدث النّقر إلى جميع العناصر التي في المستند لرؤية أيّها تعمل. عند النقر على <p>، فإنّ التسلسل يكون كالتالي: DIV ‹- FORM ‹- BODY ‹- HTML (مرحلة الانتشار نحو اﻷسفل، أوّل المنصتين). P (مرحلة الهدف، تُفعّل مرّتين لأنّنا وضعنا مُنصتَين اثنين: الانتشار نحو اﻷسفل واﻷعلى). HTML ‹- BODY ‹- FORM ‹- DIV (مرحلة الانتشار نحو اﻷسفل، ثاني المنصتين). توجد هناك الخاصيّة event.eventPhase التي تخبرنا برقم المرحلة التي تمّ فيها التقاط الحدث. لكن يندر استخدامها لأنّنا نعلم ذلك من خلال المعالج عادةً. ملاحظة: لحذف المعالج، يستلزم التابع removeEventListener إعطاء نفس المرحلة عند إضافة معالجٍ بهذا الشكل addEventListener(..., true)، فيجب ذكر نفس المرحلة أيضًا في removeEventListener(..., true) لحذف المعالج بشكل صحيح. ملاحظة: تشتغل المعالجات التي أُسندت إلى نفس العنصر وفي نفس المرحلة حسب الترتيب الذي أُنشئت به إذا كانت لدينا عدّة معالجات للأحداث مُسنَدة إلى نفس العنصر، وفي نفس المرحلة فإنها تشتغل بحسب الترتيب الذي أُنشئت به. elem.addEventListener("click", e => alert(1)); // من المؤكّد أن يشتغل أوّلا elem.addEventListener("click", e => alert(2)); الملخص عندما يقع الحدث، فإن أدنى العناصر الذي وقع فيه الحدث يُعلَّم بالعنصر"الهدف" (event.target). ثم ينتشر الحدث نزولًا من جذر المستند إلى event.target مستدعيًا في طريقه المعالجين المعيّنين بواسطة addEventListener(..., true) (القيمة true هي اختصار لـ {capture: true}) ثم نُستدعى المعالجات المُسندة إلى العنصر الهدف نفسه. ثم ينتشر الهدف صعودًا من event.target إلى الجذر، مناديًا المعالجات المعيّنة بواسطة on<event> و addEventListener دون الوسيط الثالث false/{capture:false}. يمكن لأيّ معالجٍ أن يستخدم خاصيّات الكائن event التالية: event.target -- أدنى العناصر الذي نشأ عنه الحدث. event.currentTarget (هو نفس this) -- العنصر الذي يعالج الحدث حاليًّا (الذي أُسند إليه المعالج). event.eventPhase -- المرحلة الحاليّة (1=الانتشار نحو اﻷسفل، 2=الهدف، 3=الانتشار نحو اﻷعلى). يمكن لأيّ معالجٍ أن يوقف انتشار الحدث من خلال استدعاء event.stopPropagation()، لكن لا يُنصح بذلك، لأنّه لا يمكن التأكّد حقًّا من عدم الحاجة إليه في الأعلى، ربّما في أمورٍ مختلفة تماما. يندر جدًّا استخدام مرحلة الانتشار نحو اﻷسفل، إذ تُعالَج الأحداث عادةً عند انتشارها نحو اﻷعلى. هناك سبب منطقي وراء ذلك. في الواقع، عندما يقع حادث ما، تُبلَّغ السلطات المحليّة أوّلًا. فهم أفضل من يعرف المنطقة التي وقع فيها الحادث. ثمّ تُبلّغ السلطات الأعلى عند الحاجة لذلك. ينطبق الأمر على معالجات الأحداث. تكون الشيفرة المسؤولة عن إسناد المعالج إلى عنصرٍ ما أعلم بالتفاصيل المتعلّقة بذلك العنصر ومالذي يفعله. فيكون المعالج المسند إلى العنصر<td> خصيصا أنسب بتولي أمر ذلك العنصر بالذات، إذ يعلم كلّ شيء بخصوصه، وينبغي أن تُمنح له الفرصة أوّلا. ثم يأتي أبوه المباشر، الذي يكون له اطّلاع على السياق لكنّه أقلّ معرفةً به. وهكذا إلى أعلى العناصر، الذي يعالج الأمور العامّة ويكون آخرهم اشتغالا. يرسي مفهوم الانتشار نحو اﻷسفل واﻷعلى اﻷساس لموضوع "تفويض الأحداث"، الذي يُعدّ نمط معالجةٍ للأحداث قويًّا للغاية. سندرسه في المقال التالي. ترجمة -وبتصرف- للمقال Bubbling and capturing من سلسلة Browser: Document, Events, Interfaces لصاحبها Ilya Kantor
-
تمثّل الأحداث (events) إشاراتٍ إلى أنّ شيئًا ما قد حصل. يمكن أن تنشأ هذه الإشارات من أيّ عقدة في DOM (لكنّها لا تقتصر فقط على DOM). على سبيل المثال، هذه قائمة لأكثر الأحداث فائدةً: أحداث الفأرة: click -- عند النقر بالفأرة على عنصرٍ ما (أو عند الضغط عليه باستخدام الأجهزة اللمسية) contextmenu -- عند النقر بالزرّ الأيمن للفأرة على عنصرٍ ما. mouseout \ mouseover -- عندما يبلغ / يغادر مؤشّر الفأرة عنصرًا ما. mouseup \ mousedown -- عند ضغط / تحرير زرّ الفأرة. mousemove -- عند تحريك مؤشّر الفأرة. أحداث لوحة المفاتيح: keyup \ keydown -- عند ضغط / إرسال أحد أزرار لوحة المفاتيح. أحداث النماذج: submit -- عندما يرسل المستخدم النموذج <form>. focus -- عندما يحدّد المستخدم عنصرًا ما في النموذج، كتحديده عنصر <input> مثلا. أحداث المستند: DOMContentLoaded -- عند الفراغ من تحميل ملف HTML ومعالجتِه، وبناء كامل شجرة DOM. الأحداث المتعلقة بـ CSS: transitionend - عند انتهاء تحريكCSS (animation) . وهناك العديد من الأحداث الأخرى، سنتناول بعضها بمزيدٍ من التفصيل في مقالاتٍ لاحقة. معالجات الأحداث من أجل الاستجابة للأحداث، يمكننا تعيين معالجٍ (handler) على شكل دالّة تُنفَّذ عند وقوع الحدث. وتُعدّ بذلك المعالجات وسيلةً لتنفيذ شيفرات جافاسكربت وفقًا لما يقوم به المستخدم. توجد عدة طرق لتعيين معالجٍ للحدث. سنتناولها بدءًا بأبسطها. على شكل سمة HTML يمكن تعيين المعالج في HTML على شكل سمةٍ (attribute) يكون اسمها على النحو on<event> أي on متبوعة باسم الحدث. على سبيل المثال، لتعيين معالجٍ لحدث click على عنصر input، يمكننا استخدام السمة onclick كالتالي: <input value="Click me!" onclick="alert('Click!')" type="button"> See the Pen JS-p2-introduction-browser-events-ex1 by Hsoub (@Hsoub) on CodePen. عند النقر بالفأرة، تُنفّذ الشيفرة التي بداخل onclick. يرجى التنبّه هنا إلى أنّنا استخدمنا علامات الاقتباس غير المزدوجة داخل onclick، لأنّ السّمة نفسها محاطة بعلامات اقتباس مزدوجة. فلو غفلنا عن أنّ الشيفرة موجودة داخل السّمة واستخدمنا داخلها علامات الاقتباس المزدوجة هكذا onclick="alert("Click!")" فلن تعمل الشيفرة بشكل صحيح. لا تعدّ سمة HTML مكانا مناسبا لكتابة الكثير من الشيفرة، فيحسُن إذًا أن ننشئ دالّة جافاسكربت ونستدعيها هناك. في المثال أدناه، يؤدي النقر إلى تنفيذ الدالّة countRabbits() <script> function countRabbits() { for(let i=1; i<=3; i++) { alert("Rabbit number " + i); } } </script> <input type="button" onclick="countRabbits()" value="Count rabbits!"> See the Pen JS-p2-introduction-browser-events-ex2 by Hsoub (@Hsoub) on CodePen. كما هو معلوم، لا فرق بين الأحرف الكبيرة والصغيرة في تسمية سمات HTML، فتمثل كلّ من ONCLICK و onClick و onCLICK نفس السّمة، لكن في الغالب تُكتب السّمات بأحرف صغيرة هكذا onclick. على شكل خاصيّة DOM يمكننا تعيين معالجٍ على شكل خاصيّة DOM يكون اسمها على النحو on<event>. على سبيل المثال، elem.onclick: <input id="elem" type="button" value="Click me"> <script> elem.onclick = function() { alert('Thank you'); }; </script> See the Pen JS-p2-introduction-browser-events-ex3 by Hsoub (@Hsoub) on CodePen. عند تعيين المعالج على شكل سمة HTML، فإنّ المتصفّح يقرأها ثمّ يُنشئ من محتواها دالّة، ويكتبها على شكل خاصيّة في DOM. فتكون بذلك هذه الطريقة لتعيين المعالج مساوية للتي قبلها. تؤدي هاتان الشيفرتان الوظيفة نفسها: مجرّد HTML: <input type="button" onclick="alert('Click!')" value="Button"> HTML + JS: <input type="button" id="button" value="Button"> <script> button.onclick = function() { alert('Click!'); }; </script> استخدمنا في المثال الأوّل سمة HTML لتهيئة button.onclick، بينما في المثال الثاني استخدمنا سكربتًا لذلك. هذا الفرق الذي هناك. بما أنّ هناك خاصيّة واحدة فقط تحمل الاسم onclick، فلا يمكننا تعيين أكثر من معالج واحد لنفس الحدث. في المثال أدناه، تؤدي إضافة معالجٍ بواسطة جافاسكربت إلى استبدال المعالج الموجود مسبقًا. <input type="button" id="elem" onclick="alert('Before')" value="Click me"> <script> elem.onclick = function() { // يستبدل المعالج الموجود alert('After'); // هذا ما سيظهر فقط }; </script> See the Pen JS-p2-introduction-browser-events-ex4 by Hsoub (@Hsoub) on CodePen. لحذف المعالج، يكفي إعطاء الخاصيّة القيمة null هكذا: elem.onclick = null. الوصول إلى العنصر بواسطة this تشير الكلمة this داخل المعالج إلى نفس العنصر الذي أُسند إليه المعالج. ففي الشيفرة أدناه، يُظهر العنصر button محتواه بواسطة this.innerHTML <button onclick="alert(this.innerHTML)">Click me</button> See the Pen JS-p2-introduction-browser-events-ex5 by Hsoub (@Hsoub) on CodePen. أخطاء محتملة في بداية التعامل مع معالجات الأحداث، يُرجى التنبه لعدد من الأمور الدقيقة. يمكن تعيين معالج من دالة موجودة مسبقا: function sayThanks() { alert('Thanks!'); } elem.onclick = sayThanks; لكن انتبه، يجب إسناد الدالة كـ sayThanks وليس sayThanks(). // صحيح button.onclick = sayThanks; // خطأ button.onclick = sayThanks(); بإضافة الأقواس، تصير sayThanks() استدعاءًا للدالة. وبالتالي، يأخذ السطر الأخير ناتج تنفيذ الدالّة (الذي هو undefined بحكم أنّ الدالة لا تعيد أيّ شيء) ويضعه في onclick. هذا لا يصلح. … في المقابل، نحتاج في HTML إلى تضمين الأقواس: <input type="button" id="button" onclick="sayThanks()"> يمكن توضيح سبب ذلك كالتالي: عندما يقرأ المتصفّح السّمة، فإنّه ينشئ معالجًا على شكل دالّة لها نفس محتوى هذه السّمة. فيقوم HTML الذي لدينا بإنشاء الخاصيّة التالية: button.onclick = function() { sayThanks(); // <-- يصير محتوى السّمة هنا }; لا تستخدم setAttribute لتعيين المعالجات. هذه الشيفرة لا تصلح: // إلى توليد أخطاء <body> يؤدي النفر على // بحكم أنّ السّمات هي سلاسل نصيّة، فتصير الدالة سلسلة نصيّة ايضا document.body.setAttribute('onclick', function() { alert(1) }); تفرّق خاصيّات DOM بين الأحرف الكبيرة والصغيرة. يجب تعيين المعالج في elem.onclick بدل elem.ONCLICK ، لأنّ خاصيّات DOM تفرُق معها الأحرف الكبيرة والصغيرة. addEventListener تكمن المشكلة الأساسية في طرق تعيين المعالج السالفة الذكر، في عدم إمكانيّة تعيين عدّة معالجات لحدث واحد. لنفترض أن جزءًا من الشيفرة التي لدينا يهدف إلى إبراز أحد الأزرار عند النقر عليه، بينما يهدف جزء آخر من الشيفرة إلى إظهار رسالة ما عند نفس النقرة. قد نودّ تعيين عدّة معالجات للحدث لتحقيق ذلك، لكن بإضافة خاصيّة جديدة إلى DOM، تُستبدل الخاصيّة الموجودة مسبقًا. input.onclick = function() { alert(1); } // ... input.onclick = function() { alert(2); } // يستبدل المعالج السابق أدرك العاملون على معايير الويب هذه المشكلة منذ القدم، واقترحوا طريقة بديلة لإدارة معالجات الأحداث، وذلك بواسطة التوابع الخاصة addEventListener و removeEventListener. تكون صيغة إضافة معالجٍ فيها كالتالي: element.addEventListener(event, handler, [options]); حيث أن: event: هو اسم الحدث، كـ "click" مثلًا. handler: هي دالّة المعالج. options: هو كائن إضافي اختياري، وله الخاصيّات التالية: once: إذا كانت قيمتها true، فإن منصت الحدث (event listener) يزول تلقائيا بعد حصول الحدث. capture المرحلة التي يُعالَج فيها الحدث، وسنتطرّق إليها لاحقا في مقال انتشار اﻷحداث. لأسباب تاريخية، يمكن أن تحمل options القيمة true \ false ويكون لذلك نفس معنى {capture: false/true}. passive: إذا كانت قيمتها true، فلن يستدعي المعالجُ التابعَ preventDefault()، وسنشرح ذلك في مقال أفعال المتصفّح الافتراضية. يمكن حذف المعالج بواسطة: element.removeEventListener(event, handler, [options]); تنبيه: يتطلّب الحذفُ الدالةَ نفسَها لحذف المعالج يجب تمرير نفس الدالة التي عُيّنت من قبل. فلو جرّبنا مثلًا: elem.addEventListener( "click" , () => alert('Thanks!')); // .... elem.removeEventListener( "click", () => alert('Thanks!')); لن يحُذف المعالج، لأن removeEventListener قد تلقّى دالة أخرى -- لها نفس الشيفرة، لكن لا يهم ذلك لأنها كائن دالة آخر. هذه هي الطريقة الصحيحة لحذف المعالج: function handler() { alert( 'Thanks!' ); } input.addEventListener("click", handler); // .... input.removeEventListener("click", handler); يرُجى التنبّه هنا إلى أنّه إذا لم نحفظ الدالة في متغيّر، فلا يمكننا حذفها. إذ لا سبيل إلى "إعادة قراءة" المعالجات المُعيّنة بواسطة addEventListener. يمكّن الاستدعاء المتكرّر لـ addEventListener من تعيين عدّة معالجات كالتالي: <input id="elem" type="button" value="Click me"/> <script> function handler1() { alert('Thanks!'); }; function handler2() { alert('Thanks again!'); } elem.onclick = () => alert("Hello"); elem.addEventListener("click", handler1); // Thanks! elem.addEventListener("click", handler2); // Thanks again! </script> See the Pen JS-p2-introduction-browser-events-ex6 by Hsoub (@Hsoub) on CodePen. كما هو مبيّن في المثال أعلاه، يمكن تعيين معالجات باستخدام كلٍّ من خاصيّة DOM وaddEventListener معًا، لكن في الغالب تُستخدم إحداهما فقط. تنبيه: لا يمكن تعيين معالجات لبعض الأحداث إلا بواسطة addEventListener توجد أحداثٌ لا يمكن تعيين معالجات لها عن طريق خاصيّة DOM ، بل تشترط استخدام addEventListener. على سبيل المثال، الحدث DOMContentLoaded، الذي يحصل حين الانتهاء من تحميل المستند وبناء DOM. // لن يتم تنفيذ هذا أبدا document.onDOMContentLoaded = function() { alert("DOM built"); }; // هذه الطريقة أصحّ document.addEventListener("DOMContentLoaded", function() { alert("DOM built"); }); بذلك يكونaddEventListener أشمل، رغم أنّ هذه الأحداث تُعدّ استثناءًا وليست القاعدة. كائن الحدث لمعالجة الحدث كما ينبغي، قد يلزمنا معرفة المزيد عمّا حصل بالضبط. فليس مجرّد "النقر" أو "الضغط"، بل أيضا ما هي إحداثيات المؤشر؟ أو ما هو الزر التي ضُغط؟ إلى غير ذلك. عند وقوع حدثٍ ما، يُنشئ المتصفّحُ كائن حدث ويضع فيه التفاصيل، ثم يمرّره على شكل وسيط للمعالج. هذا مثال لكيفية الحصول على إحداثيات المؤشر من كائن الحدث: <input type="button" value="Click me" id="elem"> <script> elem.onclick = function(event) { // يظهر نوع الحدث والعنصر وإحداثيات النقر alert(event.type + " at " + event.currentTarget); alert("Coordinates: " + event.clientX + ":" + event.clientY); }; </script> See the Pen JS-p2-introduction-browser-events-ex7 by Hsoub (@Hsoub) on CodePen. هذه بعض خاصيّات كائن الحدث event: event.type: نوع الحدث، و هو في هذا المثال "click" event.currentTarget: العنصر الذي عالج الحدث. وهو نفس this، إلا إذا كان المعالج دالة سهمية أو أن this الخاصّ به مرتبط (bound) بشيء آخر، فعندها يمكن الحصول على العنصر بواسطة event.currentTarget. event.clientX / event.clientY: هي إحداثيات المؤشّر بالنسبة للنافذة، عند الأحداث المتعلقة بالمؤشّر. هناك المزيد من الخاصيّات، والكثير منها متعلق بنوع الحدث. فأحداث لوحة المفاتيح لها مجموعة من الخاصيّات، وأحداث المؤشر لها مجموعة أخرى. سندرس ذلك لاحقا عندما نتطرّق لمختلف الأحداث بالتفصيل. ملاحظة: كائن الحدث متوفر أيضا للمعالجات في HTML إذا عينّا معالجًا في HTML، فإنّه من الممكن أيضا استخدام الكائن event، كالتالي: <input type="button" onclick="alert(event.type)" value="Event type"> See the Pen JS-p2-introduction-browser-events-ex8 by Hsoub (@Hsoub) on CodePen. يمكننا ذلك لأن المتصفّح حينما يقرأ السمة، فإنّه ينشئ معالجًا بهذا الشكل: function(event) { alert(event.type) }، فيكون اسم وسيطه الأول "event"، ومحتواه مأخوذا من السمة. الكائنات المعالجة: handleEvent يمكن بواسطة addEventListener تعيين معالجٍ على شكل كائن أيضا، وعند وقوع الحدث، يُستدعى التابع handleEvent. على سبيل المثال: <button id="elem">Click me</button> <script> let obj = { handleEvent(event) { alert(event.type + " at " + event.currentTarget); } }; elem.addEventListener('click', obj); </script> See the Pen JS-p2-introduction-browser-events-ex8 by Hsoub (@Hsoub) on CodePen. كما نرى، عندما يستقبل addEventListener كائنًا، فإنه يستدعي obj.handleEvent(event) في حال وقوع الحدث. يمكننا أيضا استخدام صنفٍ لذلك: <button id="elem">Click me</button> <script> class Menu { handleEvent(event) { switch(event.type) { case 'mousedown': elem.innerHTML = "Mouse button pressed"; break; case 'mouseup': elem.innerHTML += "...and released."; break; } } } let menu = new Menu(); elem.addEventListener('mousedown', menu); elem.addEventListener('mouseup', menu); </script> See the Pen JS-p2-introduction-browser-events-ex9 by Hsoub (@Hsoub) on CodePen. يقوم نفس الكائن هنا بمعالجة كلا الحدثين. لكن ينبغي التنبه إلى أنه يجب تحديد الأحداث المراد الإنصات إليها باستخدام addEventListener صراحةً. يتلقى الكائن menu هنا الحدثين mousedown و mouseup دون غيرهما من أنواع الأحداث الأخرى. لا يلزم التابع handleEvent أن يقوم بكامل العمل، بل يمكنه استدعاء توابع أخرى مختصة بأحداث معيّنة، كما يلي: <button id="elem">Click me</button> <script> class Menu { handleEvent(event) { // mousedown -> onMousedown let method = 'on' + event.type[0].toUpperCase() + event.type.slice(1); this[method](event); } onMousedown() { elem.innerHTML = "Mouse button pressed"; } onMouseup() { elem.innerHTML += "...and released."; } } let menu = new Menu(); elem.addEventListener('mousedown', menu); elem.addEventListener('mouseup', menu); </script> See the Pen JS-p2-introduction-browser-events-ex10 by Hsoub (@Hsoub) on CodePen. هكذا يتم فصل المعالجات على حدة، مما قد يجعل دعمها أسهل. الملخص توجد ثلاث طرق لتعيين معالجات للأحداث: سمة HTML: onclick="...". خاصيّة DOM: elem.onclick = function. توابع: elem.addEventListener(event, handler[, phase]) للإضافة، و removeEventListener للحذف. يندر استخدام سمات HTML، لأن جافاسكربت تبدو غريبة وسط وسم HTML. بالإضافة إلى أنه لا يمكن كتابة الكثير من الشيفرة هناك. لا بأس باستخدام خاصيّات DOM، غير أنه لا يمكن تعيين أكثر من معالج لنفس الحدث. في الكثير من الأحيان قد لا يمثّل هذا القيد مشكلة تُذكر. تُعدّ الطريقة الأخيرة أكثرها مرونة، لكنها أيضا أطولها كتابة. هناك بعض الأحداث التي لا تعمل إلا بواسطتها، على سبيل المثال transitionend و DOMContentLoaded (ستُدرس لاحقًا). بالإضافة إلى ذلك، تدعم addEventListener تعيين معالجات للأحداث على شكل كائنات، ويُستدعى حينها التابع handleEvent عند وقوع الحدث. أيّا كانت طريقة تعيين المعالج، فإنّه يستقبل كائن حدث كوسيط أول. يحتوي هذا الكائن تفاصيل ما قد حصل. سنتعلم المزيد عن الأحداث عموما، وعن مختلف أنواع الأحداث في المقالات القادمة. التمارين الإخفاء عند النقر الأهمية: 5 أضف جافاسكربت إلى الزر button لجعل <div id="text"> يختفي عند النقر عليه، كما هنا افتح البيئة التجريبيّة لإنجاز التمرين الحل افتح البيئة التجريبيّة لمشاهدة الحلّ الاختفاء عند النقر الأهمية: 5 أنشئ زرّا يخفي نفسه عند النقر عليه. الحل يمكن استخدام this للإشارة إلى "العنصر نفسه" هنا: <input type="button" onclick="this.hidden=true" value="Click to hide"> أيّ المعالجات ستُنفّذ؟ اﻷهمية: 5 يحتوي المتغير button على زرّ، وليس عليه معالجات. أيّ من المعالجات ستُنفّذ بعد تشغيل الشيفرة أدناه؟ ما هي التنبيهات التي تظهر؟ button.addEventListener("click", () => alert("1")); button.removeEventListener("click", () => alert("1")); button.onclick = () => alert(2); الحل الجواب هو 1 و 2. يستجيب المعالج الأول لأنه لم يُحذف بواسطة removeEventListener. لحذف المعالج، يجب تمرير نفس الدالة التي عُيّنت بالضبط. وفي الكود أعلاه، مُررت دالة جديدة، تبدو مثلها تماما لكنها تبقى دالة أخرى. لحذف كائن دالة، نحتاج أن نحفظ مرجعا لها، كما يلي: function handler() { alert(1); } button.addEventListener("click", handler); button.removeEventListener("click", handler); أما المعالج button.onclick فهو يعمل بشكل مستقل وبالإضافة إلى addEventListener. حرّك الكرة في الملعب حرّك الكرة في الملعب بواسطة النقر، كما هنا المطلوب: يجب أن ينتقل مركز الكرة إلى موضع المؤشر عند النقر (حبذا دون الخروج عن حافة الملعب). تحريكات CSS مُرحّب بها. يجب ألا تخرج الكرة عن حدود الملعب. يجب ألا يؤدي تمرير الصفحة إلى اختلاط الأمور . ملاحظات: يجب أن تشتغل الشيفرة مع مختلف أحجام الكرة والملعب، وألا تكون مرتبطة بقيم معينة. استخدم الخاصيّات event.clientX/event.clientY للحصول على إحداثيات النقر. افتح البيئة التجريبيّة لإنجاز التمرين الحل أولا، علينا أن نختار طريقة لتغيير موضع الكرة. لا يمكننا استخدام position:fixed لذلك، لأن تمرير الصفحة قد يخرج الكرة عن الملعب. لذا يلزمنا استخدام position:absolute، ولكي يكون التموضع جيد الإحكام، علينا أن نعطي للملعب نفسه وضعية، لتكون بذلك الكرة مُموضَعة بالنسبة إلى الملعب: #field { width: 200px; height: 150px; position: relative; } #ball { position: absolute; left: 0; /* (بالنسبة إلى أقرب سلف مموضَع (الملعب */ top: 0; transition: 1s all; /* الكرة تطير leftو top المتعلقة بـ CSS تجعل تحريكات */ } بعدها، علينا أن نعطي القيم المناسبة لـ ball.style.left/top ، إذ هي الآن تمثل إحداثيات الكرة بالنسبة إلى الملعب. هذه هي الصورة: تمثل event.clientX/clientY إحداثيات موضع النقر بالنسبة إلى النافذة. للحصول على الإحداثية left لموضع النقر بالنسبة إلى الملعب، يمكننا أن نطرح كلّا من الحافة اليسرى الملعب وسمك الحد: let left = event.clientX - fieldCoords.left - field.clientLeft; عادةً، يشير ball.style.left إلى "الحافة اليسرى للعنصر" (الكرة)، فإذا أعطيناه قيمة المتغير left، فإن حافة الكرة هي التي ستكون تحت المؤشر وليس مركزها. علينا إذًا أن نزيح الكرة بمقدار نصف عرضها إلى اليسار، وبمقدار نصف طولها إلى الأعلى كي نجعلها في المنتصف. فتكون القيمة النهائية لـ left هي: let left = event.clientX - fieldCoords.left - field.clientLeft - ball.offsetWidth/2; تُحسب الإحداثية العمودية بنفس الطريقة. يُرجى التنبه إلى أنه يجب أن يكون عرض الكرة وطولها معلومين عند قراءة ball.offsetWidth. يجب أن يُحدد ذلك في HTML أو CSS. افتح البيئة التجريبيّة لمشاهدة الحلّ أنشئ قائمة منحدرة اﻷهمية: 5 أنشئ قائمة تُفتح وتُغلق عند النقر كما هنا ملاحظة: ينبغي التعديل على الملف المصدري لـ HTML/CSS. افتح البيئة التجريبيّة لإنجاز التمرين الحل HTML/CSS لننشئ أولا HTML/CSS. تُعدّ القائمة مُكوّنا رسوميا مستقلا بذاته في الصفحة، لذا فيفضّل وضعها في عنصر DOM واحد. يمكن تنسيق عناصر القائمة على شكل ul/li. هذا مثال عن البنية: <div class="menu"> <span class="title">Sweeties (click me)!</span> <ul> <li>Cake</li> <li>Donut</li> <li>Honey</li> </ul> </div> استخدمنا <span> للعنوان، لأن <div> له ضمنيّا الخاصيّة display:block ، مما سيجعله يحتل 100% من المساحة الأفقية، هكذا: <div style="border: solid red 1px" onclick="alert(1)">Sweeties (click me)!</div> فإذا وضعنا عليه onclick ، فإنه سيستجيب للنقرات التي على يمين النص أيضا. وبما أن <span> له ضمنيّا الخاصيّة display: inline، فإنه سيحتل من المساحة فقط ما يكفي لاحتواء النص: <span style="border: solid red 1px" onclick="alert(1)">Sweeties (click me)!</span> إقلاب القائمة يؤدي إقلاب (toggling) القائمة إلى تغيير السهم وإظهار أو إخفاء عناصرها. يمكن القيام بكل هذه التغيرات من خلال CSS. في جافاسكربت علينا فقط تمييز الوضع الحالي للقائمة من خلال إضافة أو إزالة الصنف open.. بدون open. تكون القائمة منقبضة: .menu ul { margin: 0; list-style: none; padding-left: 20px; display: none; } .menu .title::before { content: '▶ '; font-size: 80%; color: green; } … ومع وجوده يتغير السهم وتظهر عناصر القائمة: .menu.open .title::before { content: '▼ '; } .menu.open ul { display: block; } افتح البيئة التجريبيّة لمشاهدة الحلّ أضف زرّا للإغلاق اﻷهمية: 5 هناك مجموعة من الرسائل. باستخدام جافاسكربت، أضف زرا في أقصى اليمين العلوي للرسائل لإغلاقها. يجب أن تبدو النتيجة كما هنا. افتح البيئة التجريبيّة لإنجاز التمرين الحل لإضافة الزر، يمكن استخدام كلّ من position:absolute (وجعل وضعية اللوحة نسبية position:relative) أو float:right. تتميز طريقة float:right بأنها تضمن عدم تقاطع الزر مع النص، لكن position:absolute تمنح المزيد من الحرية. فالخيار لك. بذلك تكون الشيفرة لكل من الألواح كالتالي: pane.insertAdjacentHTML("afterbegin", '<button class="remove-button">[x]</button>'); و يصير <button> بذلك pane.firstChild، مما يمكّننا من تعيين معالج له هكذا: pane.firstChild.onclick = () => pane.remove(); افتح البيئة التجريبيّة لمشاهدة الحلّ الدوّار اﻷهمية: 4 أنجز دوّارا -- شريط من الصور يمكن تمريره بواسطة أسهم، كما هنا يمكننا لاحقا إضافة المزيد من المزايا كالتمرير اللامتناهي، والتحميل الديناميكي وغير ذلك. ملاحظة: في هذا التمرين، تُمثل بنية HTML/CSS في الحقيقة 90% من الحل. افتح البيئة التجريبيّة لإنجاز التمرين الحل يمكن تمثيل شريط الصور بقائمة ul/li من الصور <img>. من المفترض أن هذا الشريط واسع، لكننا نحدّه بـ <div> ثابت الحجم لقطعه، فيبدو بذلك جزء من الشريط فقط: لعرض القائمة أفقيا، يجب تطبيق الخاصيّات المناسبة لـ <li> في CSS، مثل display: inline-block. بالنسبة لـ <img> يجب أيضا تعديل display، لأنها تكون افتراضيا inline. توجد هناك مساحة تحت العناصر inline مخصصة لأذيال الحروف، فيمكن استخدام display:block لإزالتها. للقيام بالتمرير يمكن إزاحة <ul>. هناك عدة طرق لفعل ذلك، مثل تغيير margin-left أو (لأداء أفضل) استخدام translateX(): بما أن العرض الخارجي لـ <div> محدود، فإن الصور "الزائدة" ستُخفى. يُعد الدوّار بأكمله مكونا "رسوميا" مستقلا بذاته، فيُفضل وضعه في <div class="carousel"> واحد وتنسيق جميع الأمور بداخله. افتح البيئة التجريبيّة لمشاهدة الحلّ ترجمة -وبتصرف- للمقال Introduction to browser events من سلسلة Browser: Document, Events, Interfaces لصاحبها Ilya Kantor
-
يتطلّب تحريك العناصر داخل الصفحة معرفة إحداثياتها. وتعمل معظم الدوالّ في لغة جافاسكربت وفق أحد نظامي الإحداثيات التاليين: إحداثيات بالنسبة للنافذة: تشبه الموضع الثابت (position:fixed)، حيث تُحسب الإحداثيات إنطلاقًا من الركن العلوي الأيسر للنافذة. نسمي هذه الإحداثيات clientX وclientY، وسنوضح السبب من وراء ذلك فيما بعد عند التطرق لخاصيات الأحداث (event properties). إحداثيات بالنسبة للصفحة: تشبه الموضع المطلق (position:absolute)، حيث تُحسب الإحداثيات إنطلاقًا من الركن العلوي الأيسر للصفحة. نسمي هذه الإحداثيات pageX وpageY. عندما يكون أعلى الصفحة ظاهرًا في النافذة، يكون الركن العلوي الأيسر للصفحة متطابقا مع الركن العلوي الأيسر للنافذة، وتكون الإحداثيات أيضًا متساوية. ولكن بعد تحريك الصفحة تتغيّر إحداثيات العناصر بالنسبة للنافذة كلما تحركت العناصر ضمن النافذة. أما الإحداثيات بالنسبة للصفحة فلا تتغيّر، بل تبقى دائمًا ثابتة. نأخذ في هذه الصورة نقطة من الصفحة لإظهار إحداثياتها قبل التمرير (الصورة على اليسار) وبعد التمرير (الصورة على اليمين). عند تمرير الصفحة: بقيت الترتيبة pageY بالنسبة للصفحة على حالها ولم تتغير، حيث تُحسب إنطلاقًا من أعلى الصفحة (الذي لم يعد ظاهرًا في النافذة بعد التحريك). تغيّرت الترتيبة clientY بالنسبة للنافذة (أصبح السهم أقصر) لأن النقطة نفسها أصبحت أقرب لأعلى النافذة. إحداثيات عنصر ما: getBoundingClientRect تعيد الدالّة elem.getBoundingClientRect() الإحداثيات (بالنسبة للنافذة) الخاصة بأصغر مستطيل يكون العنصر elem محصورًا فيه بصفته كائنا من الصنف المدمج DOMRect أهم خاصيات الصنف DOMRect: x وy: إحداثيات نقطة بداية المستطيل بالنسبة للنافذة (X وY). width وheight: عرض وطول المستطيل (يمكن أن تكون القيم سالبة). كما يملك الصنف DOMRect خاصيات مشتقة: top وbottom: ترتيبتي ركني المستطيل العلوي والسفلي. left وright: فاصلتي ركني المستطيل الأيسر والأيمن. فعلى سبيل المثال، عند استعمال الدالّةbutton.getBoundingClientRect()للحصول على إحداثيات زرٍ ما مع تمرير الصفحة نزولًا وصعودًا، ستلاحظ أنه كلما تغيّر موضع الزر بالنسبة للنافذة تغيّرت إحداثياته بالنسبة للنافذة (yوtopوbottom` إذا كان التمرير عموديًا). إليك صورة تمثيلية تعرض مخرجات (output) الدالّة elem.getBoundingClientRect(): لاحظ أن الخاصيات x وy وwidth وheight تصف المستطيل وصفًا وافيًا، حيث يمكن حساب بقية الخاصيات المشتقة إنطلاقًا منها: left = x top = y right = x + width bottom = y + height لاحظ هنا أنه: يمكن للإحداثيات أن تكون أعدادًا عشريةً مثل العدد 10.5. يُعد هذا الأمر عاديًا، حيث يستعمل المتصفح في عمليات الحساب عمليات القسمة، كما أنه ليس علينا تقريب هذه القيم (to round) عند إسنادها للخاصيتين style.left وstyle.top. يمكن أن تكون قيم الإحداثيات سالبة. في حالة تمرير الصفحة مثلا بحيث يكون العنصر elem تمامًا فوق النافذة، تكون قيمة الخاصية elem.getBoundingClientRect().top سالبة. ملاحظة: لماذا نحتاج إلى الخاصيات المشتقة؟ وما الهدف من وجود الخاصيتين left وtop في وجود الإحداثيات x وy؟ يُعرّف المستطيل في الرياضيات بإحداثيات نقطة البداية (x,y) وشعاع الاتجاه (width,height)، وبذلك تُستخدَم الخاصيات المشتقة لتسهيل الأمر فقط. من ناحية تقنية، يمكن أن تكون قيم الخاصيتينwidth وheight سالبة، فهي تسمح بوصف المستطيلات "الموجّهة" وصفًا دقيقًا بما فيها نقطتي البداية والنهاية، كالمستطيلات المتكوّنة عند النقر على الفأرة وسحبها مسافة معيّنة. وتعني القيم السالبة للخاصيتين width وheight أن المستطيل يبدأ من الركن الأيمن السفلي ليتشكّل عن طريق سحب الفأرة في الاتجاه الأيسر نحو الأعلى. وتمثل الصورة الموالية مستطيلًا بقيم سالبة لكل من الخاصيتين width وheight (مثال: width=-200, height=-100) وكما ترى، لا تتطابق قيم الخاصيتين left وtop مع قيم الخاصيتين x وy في هذه الحالة. ولكن عمليًا، تُعيد دائمًا الدالّة elem.getBoundingClientRect() قيمًا موجبةً للعرض والطول. غير أن السبب الذي حملنا على ذكر القيم السالبة هنا، هو أن هذه القيم التي تبدو لك مكرّرةً، ليست مكرّرةً في الحقيقة. ملاحظة: لا يدعم المتصفح Internet Explorer الإحداثيات x وy لا يدعم المتصفح Internet Explorer خاصيتي الإحداثيات x وy لاسباب تاريخية. يمكنك إذًا إمّا استعمال مكتبة بديلة (polyfill) (إضافة جالبات الخاصيات (getters) للخاصية DomRect.prototype) أو استعمال الخاصيتين left وtop فقط بما أنهما دائمًا متطابقتين مع الإحداثيات x وy في الحالات التي تكون فيها قيم الخاصيتين width وheight موجبة، وخاصة في القيم التي تعيدها الدالّة elem.getBoundingClientRect(). ملاحظة: تختلف الإحداثيات right وbottom عن خاصيات التموضع المعتمدة في شيفرة CSS هناك أوجه تشابه جليّة بين الإحداثيات بالنسبة للنافذة، والموضع (position:fixed) المعتمد في شيفرة CSS. ولكن إذا تحدثنا عن التموضع في شيفرة CSS، تمثّل الخاصية right المسافة إنطلاقًا من الركن الأيمن، وتمثّل الخاصية bottom المسافة إنطلاقًا من الركن السفلي. أمّا إذا تمعّنا في الصورة أعلاه، نُدرك أن الأمر ليس كذلك في لغة جافاسكربت، حيث تُحسب كافة الإحداثيات بالنسبة للنافذة إنطلاقًا من الركن العلوي الأيسر، بما فيها إحداثيات الخاصيتين المذكورتين. elementFromPoint(x, y) تعيد الدالّة document.elementFromPoint(x, y) العنصر الأكثر تداخلًا مع إحداثيات النافذة (x, y)، وصيغتها كالآتي: let elem = document.elementFromPoint(x, y); فعلى سبيل المثال تُلقي هذه الشيفرة الضوء أو تُبرِز وسم العنصر المتموضع وسط النافذة أثناء معاينة الشيفرة: let centerX = document.documentElement.clientWidth / 2; let centerY = document.documentElement.clientHeight / 2; let elem = document.elementFromPoint(centerX, centerY); elem.style.background = "red"; alert(elem.tagName); See the Pen JS-p2-11-Coordinates-ex1 by Hsoub (@Hsoub) on CodePen. وبما أن الشيفرة تستعمل الإحداثيات بالنسبة للنافذة، يختلف العنصر الذي تُبرزه الشيفرة كلّما جرى تمرير للصفحة. ملاحظة: تعيد الدالّةelementFromPoint القيمة null إذا مُرِّرَت لها إحداثيات خارجة عن نطاق النافذة تعمل الدالّة document.elementFromPoint(x,y) فقط إذا كانت الإحداثيات (x,y) المُمَرّرة لها داخل نطاق الجزء من الصفحة الظاهر في النافذة. وإذا كانت إحدى الإحداثيات سالبةً أو خارج نطاق طول أوعرض النافذة، تُعيد الدالّة القيمة null. وإليك نوع الخطأ الذي ينجر عن ذلك: let elem = document.elementFromPoint(x, y); //null القيمة elementFromPoint إذا كانت الإحداثيات خارج نطاق النافذة تعيد الدالّة (elem = null) elem.style.background = ''; // Error! See the Pen JS-p2-11-Coordinates-ex2 by Hsoub (@Hsoub) on CodePen. استعمال الإحداثيات للتموضع الثابت (fixed) غالبا ما نحتاج للإحداثيات من أجل إضافة عنصرٍ ما في موضعٍ ما. ولإظهار عنصرٍ بمحاذاة عنصرٍ آخر، يمكننا استعمال الدالّة getBoundingClientRect للحصول على إحداثياته، ثم الاستعانة بالخاصية position مع الخاصيتين left وtop (أو right وbottom). فعلى سبيل المثال، تُظهر الدالّة createMessageUnder(elem, html) في الشيفرة الموالية رسالةً تحت العنصر elem: let elem = document.getElementById("coords-show-mark"); function createMessageUnder(elem, html) { // إنشاء العنصر الذي يضم الرسالة let message = document.createElement('div'); //يستحسن استعمال صنف CSS لتحديد النمط التنسيقي message.style.cssText = "position:fixed; color: red"; //إسناد الإحداثيات دون أن ننسى الحرفين 'px' let coords = elem.getBoundingClientRect(); message.style.left = coords.left + "px"; message.style.top = coords.bottom + "px"; message.innerHTML = html; return message; } // :الاستعمال // إضافته للصفحة لمدّة خمس ثوان let message = createMessageUnder(elem, 'Hello, world!'); document.body.append(message); setTimeout(() => message.remove(), 5000); See the Pen JS-p2-11-Coordinates-ex3 by Hsoub (@Hsoub) on CodePen. عند الضغط على الزر صاحب الخاصية id="coords-show-mark" تظهر الرسالة تحته. يمكن التعديل على الشيفرة لإظهار الرسالة على اليسار، أو على اليمين، أو في الأسفل، أو تطبيق شيفرة حركات CSS (أي CSS animation) عليه لجعله يختفي تدريجيا، وهكذا دواليك. سيكون ذلك سهلًا طالما نملك كافة إحداثيات ومقاسات العنصر. لاحظ هذا التفصيل المهم: في حالة ما إذا قمنا بتمرير الصفحة تبتعد الرسالة عن الزر. والسبب واضح: موضع الرسالة ثابت (position:fixed)، لذا تبقى في نفس الموضع من النافذة عند تمرير الصفحة. ولتغيير ذلك، علينا استعمال الإحداثيات بالنسبة للصفحة، والموضع المطلق (position:absolute). إحداثيات الصفحة يبدأ احتساب الإحداثيات بالنسبة للصفحة من الركن العلوي الأيسر للصفحة، لا النافذة. ويقابِل الإحداثيات بالنسبة للنافذة في لغة CSS الموضع position:fixed، أما الإحداثيات بالنسبة للصفحة فيقابلها في لغة CSS الموضع position:absolute من الأعلى. يمكننا استعمال الموضع position:absolute والخاصيتين left وtop لوضع عنصرٍ ما في موضعٍ ما من الصفحة ، بحيث يبقى في نفس الموضع عند تمرير الصفحة. ولكن علينا أولا إيجاد الإحداثيات الصحيحة. لا يوجد أيّ دالّة في المعايير تسمح بالحصول على إحداثيات عنصرٍ ما بالنسبة للصفحة، ولكن يمكننا كتابتها. يعد نظامي الإحداثيات مرتبطين ببعضهما البعض من خلال المعادلة التالية: pageY=clientY+ طول الجزء العمودي من الصفحة غير الظاهر في النافذة. pageX=clientX+ عرض الجزء الأفقي من الصفحة غير الظاهر في النافذة. تأخذ الدالّة getCoords(elem) الإحداثيات بالنسبة للنافذة باستخدام الدالّة elem.getBoundingClientRect() وتضيف مسافة التمرير لها. // الحصول على إحداثيات العنصر بالنسبة للصفحة function getCoords(elem) { let box = elem.getBoundingClientRect(); return { top: box.top + window.pageYOffset, right: box.right + window.pageXOffset, bottom: box.bottom + window.pageYOffset, left: box.left + window.pageXOffset }; } في المثال أعلاه، استعملنا الموضع position:absolute، وبذلك تبقى الرسالة بمحاذاة العنصر عند تمرير الصفحة وتصبح الدالّة createMessageUnder بعد التعديل كالآتي: function createMessageUnder(elem, html) { let message = document.createElement('div'); message.style.cssText = "position:absolute; color: red"; let coords = getCoords(elem); message.style.left = coords.left + "px"; message.style.top = coords.bottom + "px"; message.innerHTML = html; return message; } الخلاصة لكلّ نقطة من الصفحة إحداثيات: بالنسبة للنافذة: تُساوي elem.getBoundingClientRect() بالنسبة للصفحة: تُساوي elem.getBoundingClientRect() + مقدار تمرير الصفحة. يُنصح باستخدام الإحداثيات بالنسبة للنافذة مع الموضع position:fixed، والإحداثيات بالنسبة للصفحة مع الموضع position:absolute. ولِكِلَا نظامي الإحداثيات إيجابيات وسلبيات، حيث يُستحسن استعمال أحدهما في حالات معيّنة، والآخر في حالات أخرى، كما هو الحال بالنسبة للمواضع absolute وfixed في لغة CSS. تمارين إيجاد إحداثيات المستطيل بالنسبة للنافذة درجة الأهمية:5 لدينا في الصورة الموالية صفحة ويب بها مستطيل أخضر “field”. استعمل لغة جافاسكربت للحصول على إحداثيات الأركان المؤشر عليها بالأسهم الحمراء بالنسبة للنافذة. أضيفت للصفحة إمكانية الحصول على إحداثيات أيّ نقطة منها بمجرد النقر عليها لتسهيل الأمر عليك. يمكنك الاطلاع على شيفرة التمرين عبر هذا الرابط عليك استعمال نموذج تمثيل الصفحة ككائن DOM للحصول على الإحداثيات بالنسبة للنافذة لكل من: الركن الخارجي العلوي الأيسر (وهو سهل). الركن الخارجي السفلي الأيمن (وهو سهل أيضا). الركن الداخلي العلوي الأيسر (صعب بعض الشيء). الركن الداخلي السفلي الأيمن ( هناك عدة طرائق للحصول عليه. اختر واحدة منها) ينبغي أن تكون الإحداثيات التي تحسبها الشيفرة متطابقةً مع الإحداثيات التي تظهر عند النقر على الركن بالفأرة. ينبغي أيضًا أن تعمل الشيفرة مهما كانت قيم المقاسات والأطر، ليس فقط من أجل القيم المعطاة في نص التمرين. الحل الركنان الخارجيان تكون إحداثيات الركنين الخارجيين عادةً هي الإحداثيات التي تعيدها الدالّة elem.getBoundingClientRect(). في الشيفرة التالية، تمثّل answer1 إحداثيات الركن العلوي الأيسر، وتمثّل answer2 إحداثيات الركن السفلي الأيمن. let coords = elem.getBoundingClientRect(); let answer1 = [coords.left, coords.top]; let answer2 = [coords.right, coords.bottom]; الركن الداخلي العلوي الأيسر الفرق بينه وبين الركن الخارجي هو عرض الإطار. والطريقة المثلى للحصول على هذه المسافة هي باستعمال الخاصيتين clientLeft وclientTop. let answer3 = [coords.left + field.clientLeft, coords.top + field.clientTop]; الركن الداخلي السفلي الأيمن علينا في هذه الحالة طرح عرض الإطار من الإحداثيات الخارجية. ويمكننا استعمال لغة CSS كالآتي: let answer4 = [ coords.right - parseInt(getComputedStyle(field).borderRightWidth), coords.bottom - parseInt(getComputedStyle(field).borderBottomWidth) ]; هناك أيضًا طريقةٌ بديلةٌ، وربما هي أحسن، تكون بإضافة قيم الخاصيتين clientWidth وclientHeight لإحداثيات الركن العلوي الأيسر. let answer4 = [ coords.left + elem.clientLeft + elem.clientWidth, coords.top + elem.clientTop + elem.clientHeight ]; يمكنك الاطلاع على شيفرة الحل عبر هذا الرابط إظهار ملاحظة بمحاذاة عنصرٍ ما درجة الأهمية: 5 أنشئ الدالّة positionAt(anchor, position, elem) التي تضع العنصرelem في موضعٍ position قريبٍ من العنصرanchor. يكون الموضع position عبارة عن سلسلة نصية ويأخذ إحدى القيم التالية: "top": تضع العنصر elem فوق العنصرanchor مباشرة. "right": تضع العنصرelem على يمين العنصرanchor مباشرة. "bottom": تضع العنصرelem أسفل العنصر anchor مباشرة. تُستعمل هذه الدالّة بداخل الدالّة showNote(anchor, position, html) (الشيفرة الخاصة بالدالّة معطاة في نص التمرين) التي تُنشئ عنصرًا يحتوي على ملاحظة من خلال الشيفرة html المُمَرّرة لها، وتظهر هذه الملاحظة في الموضع position المُمَرر لها بمحاذاة العنصر anchor، وهذه صورة تمثيلية للنتيجة التي نريد الوصول اليها: يمكنك الاطلاع على شيفرة التمرين عبر هذا الرابط. الحل في هذا التمرين نحتاج فقط حسابَ الإحداثيات بشكلٍ دقيقٍ. ولتفاصيل أكثر، اطلع على شيفرة الحل. لاحظ أن العناصر ينبغي أن تكون موجودةً في الصفحة حتى يتسنى لنا قراءة الخاصية offsetHeight وخاصيات أخرى. حيث أن العنصر المخفي (display:none) أو غير الموجود بالصفحة لا يملك مقاسات. يمكنك الاطلاع على الحل عبر هذا الرابط. إظهار ملاحظة بمحاذاة عنصرٍ ما (مطلق) درجة الأهمية: 5 عدِّل على حل التمرين السابق بحيث تستعمل الملاحظة موضعًا مطلقًا (position:absolute) بدلًا من الموضع الثابت (position:fixed). هذا ما يُبقِي الملاحظة بمحاذاة العنصر عند تمرير الصفحة. استعمل حل التمرين السابق كنقطة انطلاق، ولمعاينة التمرين أضف النمط التنسيقي التالي: <body style="height: 2000px"> الحل الحل بسيطٌ جدًا. غيّر الخاصية position الخاصة بالمحدِّد .note من position:fixed إلىposition:absolute في شيفرة CSS. استعمل الدالّة getCoords() المعرّفة أعلاه للحصول على الإحداثيات بالنسبة للصفحة. يمكنك الإطلاع على الحل عبر هذا الرابط. وضع الملاحظة داخل العنصر (مطلق) درجة الأهمية: 5 استعمل التمرين السابق (إظهار ملاحظة بمحاذاة عنصرٍ ما [مطلق]) وعدّل على الدالّة positionAt(anchor, position, elem) لتسمح بإضافة عنصر elem داخل العنصرanchor. والقيم الجديدة للموضع position هي: bottom-out ،right-out ،top-out: هي نفس المواضع المعرّفة في الحل الأول والتي تسمح بإضافة العنصرelem فوق أو على يمين أو تحت العنصر anchor. bottom-in، right-in، top-in: تسمح بإضافة العنصر elem داخل العنصر anchor بحيث يلتصق بالركن العلوي أو الأيمن أو السفلي. على سبيل المثال: // تظهر الملاحظة فوق إطار المقولة positionAt(blockquote, "top-out", note); // تظهر الملاحظة داخل إطار المقولة في الأعلى positionAt(blockquote, "top-in", note); والنتيجة تكون كالآتي: خذ شيفرة حل التمرين السابق (إظهار ملاحظة بمحاذاة عنصرٍ ما [مطلق]) واستعن بها كشيفرة نص لهذا التمرين. الحل يمكنك الاطلاع على الحل عبر هذا الرابط. ترجمة -وبتصرف- للفصل Coordinates من كتاب Browser: Document, Events, Interfaces
-
هل تتساءل عن كيفية الحصول على طول وعرض نافذة المتصفح؟ وكيف لك أن تعرف الطول والعرض الكلي للصفحة بما فيه الجزء الذي لا يُؤشِر عليه شريط التمرير (الجزء من الصفحة غير الظاهر في النافذة)؟ وكيف يمكن تمرير محتوى الصفحة باستعمال لغة جافاسكربت؟ فيما يلي الأجوبة الشافية لكل هذه التساؤلات. يمكن، في أغلب هذه الحالات، استعمال العنصر الجذر للصفحة document.documentElement الذي يقابِل الوسم <html>، بالإضافة إلى بعض الدوالّ والخاصيات التي يُستحسن أخذها في الحسبان نظرًا لأهميتها. طول وعرض النافذة تُستخدم الخاصيتان clientHeight وclientWidth لمعرفة طول وعرض النافذة. حيث تُظهِرالتعليمة، في المثال التالي، طول نافذة المتصفح: alert( document.documentElement.clientHeight ); ملاحظة: لا تستعمل window.innerWidth وwindow.innerHeight تدعم المتصفحات الخاصيتين window.innerWidth وwindow.innerHeight، ويبدو أنهما تتوافقان مع ما نريده، فلماذا لا نستخدمهما بدلًا من الخاصيتين اللّتان ذكرناهما سابقا؟ في حالة وجود شريط تمرير في الصفحة، تعطي الخاصيتان clientHeight وclientWidth طول وعرض النافذة بعد طرح عرض شريط التمرير منه. أو بعبارة أخرى، تعطيان طول وعرض الجزء الظاهر من الصفحة والمخصص للمحتوى. وتعطي الخاصيتان window.innerWidth وwindow.innerHeight الطول والعرض الكليين للنافذة بما فيهما عرض شريط التمرير. في حالة ما إذا كان لدينا شريط تمرير يشغل مساحة ما من النافذة، تعطي التعليمتان التاليتان نتائج مختلفة: alert( window.innerWidth ); // العرض الكلي للنافذة alert( document.documentElement.clientWidth ); // عرض النافذة بعد طرح عرض شريط التمرير منه See the Pen JS-p2-10-Window sizes and scrolling-ex1 by Hsoub (@Hsoub) on CodePen. نحتاج، في أغلب الحالات، معرفة عرض النافذة المتاح (المخصص للمحتوى)، لرسم شيء ما أو وضعه، أي دون احتساب عرض شريط التمرير إن وُجد، لذا علينا استعمال الخاصيتين documentElement.clientHeight وdocumentElement.clientWidth. ملاحظة: التعليمة DOCTYPE مهمة لاحظ أن الخاصيات رفيعة المستوى المتعلقة بالهندسة يمكن أن لا تعمل بالشكل المطلوب إذا لم يحوي ملف HTML التعليمة <!DOCTYPE HTML> ، فمن الممكن أن تحدث أشياء غريبة بسبب ذلك. أضف دائمًا إذًا التعليمة DOCTYPE عند كتابة شيفرة HTML على الطريقة العصرية. طول وعرض الصفحة نظريا، وبما أن العنصر الجذر للصفحة هو document.documentElement، ويضُم كلّ المحتوى، يمكن قياس العرض والطول الكليين للصفحة باستعمال الخاصيتين document.documentElement.scrollWidth وdocument.documentElement.scrollHeight، ولكن هاتين الخاصيتين، عند استعمالهما على هذا العنصر، لا تعملان بالشكل المطلوب إن لم يكن هناك شريط تمرير في كلّ من المتصفحات Chrome/Safari/Opera، حيث يمكن أن تعطي الخاصية documentElement.scrollHeight قيمةً أصغر من القيمة التي تعطيها الخاصية documentElement.clientHeight. يبدو الأمر غريبا نوعا ما، أليس كذلك؟ للحصول إذًا على القيمة الصحيحة للطول الكلي للصفحة، علينا أخذ أكبر قيمة من بين القيم التالية: let scrollHeight = Math.max( document.body.scrollHeight, document.documentElement.scrollHeight, document.body.offsetHeight, document.documentElement.offsetHeight, document.body.clientHeight, document.documentElement.clientHeight ); alert('Full document height, with scrolled out part: ' + scrollHeight); See the Pen JS-p2-10-Window sizes and scrolling-ex2 by Hsoub (@Hsoub) on CodePen. لماذا يحدث ذلك؟ يُستحسن عدم طرح هذا السؤال لأن هذه التناقضات موجودة منذ زمن ولا تخضع لتفكير منطقي "ذكي". حساب إحداثيات الموضع الحالي لعنصر ما تملك عناصر تمثيل الصفحة ككائن DOM خاصيتين تحملان إحداثيات موضعها من الصفحة وهما: elem.scrollLeft وelem.scrollTop. فبالنسبة لموضِع النافذة، تعمل الخاصيتان document.documentElement.scrollLeft وdocument.documentElement.scrollTop في أغلب المتصفحات، عدا تلك التي تعتمد على محرك WebKit مثل المتصفح Safari (يوّلد استعمالها الخطأ البرمجي 5991)، حيث ينبغي في هذه الحالة استعمال document.body بدلا من document.documentElement. ومن حسن الحظ أنه ليس علينا على الإطلاق تذكُّر كلّ هذه الخاصيات، حيث توجد خاصيتان غيرهما تعطيان الموضع الحالي للنافدة، إنهما الخاصيتان window.pageXOffset وwindow.pageYOffset، وهما خاصيتان قابلتان فقط للقراءة. alert('Current scroll from the top: ' + window.pageYOffset); alert('Current scroll from the left: ' + window.pageXOffset); See the Pen JS-p2-10-Window sizes and scrolling-ex3 by Hsoub (@Hsoub) on CodePen. التمرير باستعمال الدوال scrollTo وscrollBy وscrollIntoView ملاحظة هامة لا يمكن تمرير الصفحة إلّا بعد البناء الكلي لنموذج تمثيل الصفحة ككائن (DOM)، فمن غير الممكن كتابة شيفرة لتمرير الصفحة ووضعها في الجزء <head> أي تنفيذها قبل أن تجهز الصفحة فلن يعمل ذلك حتما. ويمكن تمرير العناصر العادية بتغيير قيم الخاصيتين scrollTop وscrollLeft، كما يمكن عمل الشيء نفسه للصفحة باستعمال الخاصيتين document.documentElement.scrollLeft وdocument.documentElement.scrollTop، عدا في المتصفح Safari (حيث ينبغي استخدام الخاصيتين document.body.scrollLeft وdocument.body.scrollTop بدلا منها). وهناك أيضا طريقةٌ بديلةٌ، أبسط من الأولى، تعمل في كافة المتصفحات، وتكون باستعمال الدالّتين window.scrollBy(x,y) وwindow.scrollTo(pageX,pageY). تحرّك الدالّة scrollBy(x,y) النافذة من مكانها الحالي بمقدار معين. حيث تُحرِك التعليمة window.scrollBy(0,10) مثلا الصفحة إلى الأسفل بمقدار عشرة بكسل. وتحرِّك الدالّة scrollTo(pageX,pageY) الصفحة إلى موضعٍ ذي إحداثياتٍ مطلقةٍ. بحيث تكون إحداثيات الركن العلوي الأيسر للجزء الظاهر من الصفحة هي (pageX, pageY) إنطلاقا من الركن العلوي الأيسر للصفحة. تعمل هذه الدالّة تماما كما لو أننا نُسنِد قِيَمًا للخاصيتين scrollLeft وscrollTop. مثلًا لتحريك النافذة إلى نقطة البداية (العودة إلى أعلى الصفحة)، يمكن استعمال التعليمة window.scrollTo(0,0). وتعمل هذه الدوالّ بنفس الطريقة (تعطي النتائج نفسها) على كلّ المتصفحات. الدالة scrollIntoView دعنا نكمل ما بدأناه بشرح الدالّة elem.scrollIntoView(top). ينتج عن مناداة هذه الدالّة تمرير الصفحة بحيث تجعل العنصر elem ظاهرًا، ولها وسيط واحد: إذا كانت قيمة الوسيط top تساوي true (وهي القيمة المبدئية -الافتراضية)، تُمرَّر الصفحة بحيث يظهر العنصر elem أعلى النافذة فتتطابق حافته العلوية مع أعلى النافذة. وإذا كانت قيمة الوسيط top تساوي false، تُمرَّر الصفحة بحيث يظهر العنصر elem أسفل النافذة فتتطابق حافته السفلى مع أسفل النافذة. مثال: إذا كان لدينا زر، فإن هذه التعليمة this.scrollIntoView() تُحرِك الصفحة بحيث يظهر هذا الزر أعلى النافذة. أما هذه التعليمة this.scrollIntoView(false)، فتُحرِك الصفحة بحيث يظهر الزر أسفل النافذة. تثبيط قابلية التمرير نحتاج أحيانا إلى جعل الصفحة غير قابلة للتمرير، فنرغب مثلًا في إخفائها وراء رسالة طويلة للفت انتباه الزائر وحمله على التفاعل مع الرسالة وليس مع الصفحة. وهنا يكفي إسناد القيمة "hidden" للخاصية document.body.style.overflow حيث تثبِّت (تجمِّد) هذه التعليمة الصفحة في موضعها الحالي. فلتجرب ذلك باستعمال التعليمتين document.body.style.overflow="hidden" و document.body.style.overflow="". هنا، تثبِّط التعليمة الأولى قابلية التحريك، أما التعليمة الثانية فتعاود تفعيلها. ويمكننا استعمال هذه الطريقة لتجميد/تثبيت عناصر أخرى أيضا، ليس العنصر document.body فقط. غير أن لهذه الدالّة نقطة سلبية، حيث يختفي شريط التمرير تماما بعد مناداتها، وبما أنه كان يشغل مساحة ما من الصفحة، فعند اختفائه، يتحرك محتوى الصفحة بعض الشيء لملء الفراغ الذي نتج عن اختفاء شريط التمرير. يبدو هذا غريبا بعض الشئ، ولكن يمكننا تفادي حدوث ذلك بموازنة قيمة الخاصية clientWidth قبل وبعد التثبيط، فإذا ازداد العرض بعد اختفاء شريط التمرير، نضيف حاشية padding للعنصرdocument.body مكان شريط التمرير للإبقاء على عرض محتوى الصفحة كما هو. الخلاصة لمعرفة طول/عرض الجزء الظاهر من الصفحة (طول/عرض مساحة المحتوى) نستعمل الخاصيتين document.documentElement.clientWidth/Height. *لمعرفة الطول/العرض الكلي للصفحة بما فيها الجزء غير الظاهر في النافذة نبحث عن أكبر قيمة من بين القيم التالية: let scrollHeight = Math.max( document.body.scrollHeight, document.documentElement.scrollHeight, document.body.offsetHeight, document.documentElement.offsetHeight, document.body.clientHeight, document.documentElement.clientHeight ); لقراءة إحداثيات موضع الجزء الظاهر من الصفحة إنطلاقا من الركن العلوي الأيسر للصفحة نستعمل الخاصيتين window.pageYOffset/pageXOffset. ولتغيير الموضع الحالي للنافذة نستعمل الدوالّ التالية: window.scrollTo(pageX,pageY): التحريك نحو الموضع ذي الإحداثيات المطلقة (pageX,pageY). window.scrollBy(x,y) : التحريك بمقدار (x,y) انطلاقا من الموضع الحالي. elem.scrollIntoView(top): التحريك لجعل العنصر elem ظاهرًا، أي تحريكه بحيث يتطابق مع أعلى أو أسفل النافذة حسب قيمة الوسيط top. ترجمة -وبتصرف- للفصل Window sizes and scrolling من كتاب Browser: Document, Events, Interfaces
-
تضم جافاسكربت العديد من الخاصيات التي تسمح بقراءة معلومات طول عنصر ما (height)، وعرضه (width) وجوانب أخرى متعلقة بهندسته (geometry). هي معلومات نحتاجها عادة متى ما أردنا تحريك أو وضع عناصر ما على صفحة ويب باستعمال لغة جافاسكربت. العنصر النموذجي نستعين فيما يلي بالعنصر النموذجي التالي من أجل عرض الخاصيات: <div id="example"> ...Text... </div> <style> #example { width: 300px; height: 200px; border: 25px solid #E8C48F; padding: 20px; overflow: auto; } </style> See the Pen JS-p2-09-Element size and scrolling-ex1 by Hsoub (@Hsoub) on CodePen. يملك هذا العنصر إطارًا (border)، وحاشية (padding) وإحداثيات تمرير (scrolling)؛ أي مجموعة كاملة من الخاصيات. في حين أنه لا يملك هوامش (margins) لأنها لا تُعدّ جزءًا منه وليست لها خاصيات. ويبدو العنصر على الشكل التالي: يمكنك الاطلاع على الشيفرة ومعاينتها على هذا الرابط. ملاحظة: شريط التمرير تُظهر الصورة السابقة أكثر الحالات تعقيدًا، أي حين يكون للعنصر شريط تمرير،حيث تُخصِص بعض المتصفحات ( وليس كلّها) مساحةً لشريط التمرير من المساحة المخصصة للمحتوى، والمؤشَّر عليها في الصورة بعبارة 'عرض المحتوى' (content width). وبذلك يكون عرض المحتوى هنا 300 بكسل في حالة عدم وجود شريط تمرير. ولكن شَغلَ شريط التمرير لمساحةٍ عرضها 16 بكسل (يتغيّر هذا العرض من متصفح لآخر ومن جهاز لآخر) يُصغِّر من عرض المحتوى ليصبح 300-16=284 بكسل. وعليه فإنه من الأهمية بمكان أخذ ذلك في الحسبان. يُسهِّل عدم وجود شريط التمرير العمليات الحسابية، ولكننا نفترض في الأمثلة الخاصة بهذا الفصل أنه موجود. ملاحظة: يمكن لمساحة الحاشية السفلى padding-bottom أن تحتوي نصا عادة ما تَظهر الحواشي فارغةً من أي محتوى. غير أن نص العنصر قد يكون طويلا جدًا بحيث لا تسعه مساحة المحتوى كاملا، فتُظهِره المتصفحات عادة في الحاشية السفلى للعنصر. الخاصيات الهندسية لعنصر ما فيما يلي صورة شاملة تُظهِر الخاصيات الهندسية لعنصر ما. تكون قيم هذه الخاصيات عبارة عن أرقام، ولكنها تُمثِّل في الحقيقة عدد البكسلات، فهي إذا مقاسات بالبكسل. فلنستعرض هذه الخاصيات إنطلاقًا من خارج العنصر نحو داخله. الخاصيات offsetParent وoffsetLeft وoffsetTop نادرًا ما نحتاج إلى هذه الخاصيات ولكنها تصف العنصر من الخارج ، لذا سنبدأ العرض بها: تمثل الخاصية offsetParent السلف الأقرب (ancestor) للعنصر ويستعملها المتصفح لحساب الإحداثيات أثناء ترجمة الشيفرات إلى صفحات ويب، ويكون السلف الأقرب إمّا: متموضع باستعمال لغة CSS: يكون الموضع (position) إمّا مطلقا (absolute)، أو نسبي (relative)، أو ثابت (fixed) أو لاصق (sticky) أو، أحد العناصر: <td> ،<th> ،<table>، أو، العنصر <body>. وتعطي، في المثال التالي، الخاصيتان offsetLeft وoffsetTop الإحداثيات x وy إنطلاقًا من الركن الأيسر العلوي للسلف الأقرب offsetParent. ويُعدّ العنصر <main> السلف الأقرب offsetParent للعنصر <div> المحصور داخله، وتحسب الخاصيتان offsetLeft وoffsetTop إنطلاقًا من الركن العلوي الأيسر للعنصر <main>: <main style="position: relative" id="main"> <article> <div id="example" style="position: absolute; left: 180px; top: 180px">...</div> </article> </main> <script> alert(example.offsetParent.id); // main alert(example.offsetLeft); // 180 (*) alert(example.offsetTop); // 180 </script> See the Pen JS-p2-09-Element size and scrolling-ex2 by Hsoub (@Hsoub) on CodePen. (*): لاحظ أن القيمة هنا عبارة عن رقم (180) وليس سلسلة نصية “180px”. هناك عدة حالات تحمل فيها الخاصية offsetParent القيمة null: إذا كان العنصر غير ظاهر (display:none أو غير ظاهرٍ في الصفحة) إذا كان العنصر هو <body> أو <html>، إذا كان موضع العنصر يحمل القيمة position:fixed. offsetWidth وoffsetHeight نباشر الآن في عرض خاصيات العنصر نفسه. تُعدّ الخاصيتان offsetHeight وoffsetWidth أبسط الخاصيات، حيث تعطيان الطول والعرض الخارجي الكلي للعنصر، بما في ذلك الأطر. وفيما يتعلّق بالعنصر النموذجي لدينا: offsetWidth = 390: العرض الكلي ويمكن حسابه بجمع العرض الداخلي المحدَّد في شيفرة CSS (أي 300 بكسل)، والحاشيتين (220 بكسل) و الإطارين (225 بكسل). offsetHeight = 290: الطول الخارجي. ملاحظة: تأخذ الخاصيات الهندسية للعناصر غير الظاهرة على الصفحة إحدى القيمتان 0 أو null لا تُحسب الخاصيات الهندسية إلا إذا تعلّق الأمر بعناصر ظاهرة على الصفحة. إذا كان العنصر أو أيًا من أسلافه يملك الخاصية display:none أو لم يكن ظاهرًا في الصفحة، تكون قيم كل الخاصيات الهندسية الخاصة به معدومة ( أو null، إذا تحدثنا عن الخاصية offsetParent). فعلى سبيل المثال، تحمل الخاصية offsetParent القيمة null، والخاصيتان offsetWidth وoffsetHeight القيمة 0 إذا أنشأنا عنصرًا ما ولم نُضفه إلى الصفحة أو إذا كانت الخاصية display الخاصة به أو بأسلافه محدّدة بقيمة none. ويمكن التحقّق ممّا إذا كان العنصر ظاهرًا أو مخفيًا كالآتي: function isHidden(elem) { return !elem.offsetWidth && !elem.offsetHeight; } لاحظ هنا أن الدالّة isHidden تعيد القيمة true أيضًا إذا كان العنصر ظاهرًا على الصفحة ولكن مقاساته منعدمة، كالعنصر <div> مثلا إذا كان فارغًا. clientTop وclientLeft داخل العنصر، توجد الأطر، ولقياسها لدينا الخاصيتان clientTop وclientLeft، حيث تكون قيم كلُّ من الخاصيتين في المثال السابق كالآتي: clientLeft = 25، عرض الإطار الأيسر. clientTop = 25، عرض الإطار العلوي. وحتى نكون دقيقين علينا أن ننبّه لأمر ما، وهو أن هاتين الخاصيتين لا تمثلان طول وعرض الإطار، بل الإحداثيات النسبية للجانب الداخلي إنطلاقًا من الجانب الخارجي. ما الفرق بينهما؟ يَظهر الفرق عندما يكون اتجاه الصفحة من اليمين إلى اليسار (إذا كانت لغة المتصفح هي العربية أو العبرية)، حيث يَظهر شريط التمرير في هذه الحالة على الجهة اليسرى بدل اليمنى، وبذلك تضم الخاصية clientLeft عرض شريط التمرير أيضا. وهنا لا تكون قيمة الخاصية clientLeft هي 25، بل 25 زائد عرض شريط التمرير (25 + 16 = 41) . وفيما يلي مثال باللغة العبرية: clientWidth وclientHeight تعطي هاتان الخاصيتان مقاسات المساحة المحصورة بين إطاري العنصر، وتضمّان عرض المحتوى بما فيه الحاشيتين، ولكن دون عرض شريط التمرير. فلنتحدث أولا عن الخاصية clientHeight المذكورة في الصورة أعلاه. لا وجود في الصورة لشريط تمرير أفقي، فقيمتها إذًا تساوي بالتدقيق مجموع ماهو محصور بين الإطارين، أي الطول المحدَّد في شيفرة CSS (أي 200 بكسل) زائد الحاشيتين السفلية والعلوية (2*20 بكسل) وهو ما يعطي القيمة 240 بكسل. أما بالنسبة للخاصية clientWidth، فلا يساوي عرض المحتوى 300 بكسل، بل 240 بكسل، ذلك لأن شريط التمرير يشغَل مساحةً عرضها 16 بكسل. فالنتيجة تكون 284 بكسل زائد عرض الحاشيتين اليمنى واليسرى. المجموع إذا 324 بكسل. وفي حالة عدم وجود الحاشيتين تساوي قيمة الخاصيتين clientWidth وclientHeight بالتدقيق عرض وطول مساحة المحتوى المحصورة بين الأطر وشريط التمرير إن وجد. يمكننا إذا استعمال الخاصيتين clientWidth وclientHeight للحصول على مقاسات مساحة المحتوى إن لم يكن للعنصر حواشي. scrollWidth وscrollHeight تشبه هاتان الخاصيتان الخاصيتين clientWidth وclientHeight، ولكنهما تضمان أيضًا الأجزاء غير الظاهرة. لدينا في الصورة أعلاه: scrollHeight = 723، وتمثل الطول الداخلي الكلي لمساحة المحتوى بما فيه الأجزاء غير الظاهرة. scrollWidth = 324، وتمثل العرض الداخلي الكلي، وبما أنه لا وجود لشريط تمرير أفقي، فهي تساوي clientWidth. ويمكننا استخدام هاتين الخاصيتين لتوسيع العنصر إلى طوله أو عرضه الكلي كالآتي: // توسيع محتوى العنصر إلى طوله الكلي element.style.height = `${element.scrollHeight}px`; حيث يختفي هنا شريط التمرير، ويظهر محتوى العنصر كاملا. scrollLeft وscrollTop تمثل الخاصيتان scrollLeft وscrollTop عرض وطول الجزء غير الظاهر من العنصر (الذي جرى المُرور عليه). لاحظ في الصورة الموالية الخاصيتين scrollHeight وscrollTop لمقطع يضم شريط تمرير عمودي. بعبارة أخرى، تمثل الخاصية scrollTop مقدار تمرير العنصر إنطلاقًا من أعلى الصفحة. ملاحظة: يمكن التعديل على الخاصيتين scrollLeft وscrollTop تُعدّ أغلب الخاصيات التي استعرضناها هنا قابلة للقراءة فقط (read-only)، ولكن الخاصيتين scrollLeft وscrollTop يمكن تغييرهما ليحرِّك المتصفح العنصر. إذا كان لدينا عنصرًا ما، وليكن elem، فإن التعليمة elem.scrollTop += 10 تُحرِّكه بمقدار 10 بكسل نحو الأسفل. ويؤدي إسناد القيمة 0 أو أيّ قيمةٍ كبيرةٍ جدًا (مثلا 1e9) للخاصية scrollTop إلى تحريك العنصر إلى أقصى أعلى أو أسفل الصفحة. لا تأخذ طول وعرض العنصر من شيفرة CSS تحدثنا أعلاه عن الخاصيات الهندسية لعناصر DOM التي يمكن استخدامها للحصول على الطول والعرض ولحساب المسافات. وسبق لك وأن تعرفت في فصل الأنماط التنسيقية والأصناف، أنه يمكن قراءة الطول والعرض المحدَّدّين في شيفرة CSS لعنصر ما باستعمال الدالّة getComputedStyle، فلماذا لا نقرأ عرض عنصرٍ ما باستعمال الدالّة getComputedStyle، كما في المثال التالي؟ let elem = document.body; alert( getComputedStyle(elem).width ); // إظهار عرض العنصر المحدَّدّ في شيفرة CSS See the Pen JS-p2-09-Element size and scrolling-ex3 by Hsoub (@Hsoub) on CodePen. لماذا عليك استعمال الخاصيات الهندسية بدلًا من هذه الدالّة؟ هناك سببان لذلك: الأول، هو أن الطول والعرض المحدَّدين في شيفرة CSS يتعلّقان بخاصية أخرى وهي box-sizing، التي تحدِّد الطول والعرض في شيفرة CSS. ويمكن لأيّ تغيير في الخاصية box-sizing أن يوقِف عمل شيفرة جافاسكربت. والثاني، هو أن الطول والعرض المحدَّدين في شيفرة CSS، قد يحملان القيمة "auto" كما هو الحال بالنسبة للعناصر السطرية (inline element). مثال: <span id="elem">Hello!</span> <script> alert( getComputedStyle(elem).width ); // auto </script> See the Pen JS-p2-09-Element size and scrolling-ex4 by Hsoub (@Hsoub) on CodePen. يُعدّ إسناد القيمة "auto" للخاصية width أمرًا عاديًا جدًا في لغة CSS، ولكن جافاسكربت يتطلّب مقاسا دقيقا بالبكسل حتى يستخدمه في العمليات الحسابية. لذلك لن يفيدك العمل بالعرض المحدَّد في شيفرة CSS في هذه الحالة. هناك أيضا سببٌ آخر يتعلّق بشريط التمرير. حيث يحدث أن يعمل سكربت معين بصفة عادية إن لم يكن هناك شريط تمرير، في حين يُظهِر العديدَ من الأخطاء البرمجية في حالة وجوده. هذا لأن شريط التمرير يَشغَل مساحةً من المحتوى في بعض المتصفحات، فيكون العرض الحقيقي المتاح للمحتوى أقل من العرض الذي تحدِّده شيفرة CSS، وهو ما تأخذه الخاصيتان clientWidth وclientHeight في الحسبان. ولكن الأمر يختلف عند استخدام التعليمة getComputedStyle(elem).width، حيث تُعيد بعض المتصفحات (مثل المتصفح Chrome) العرض الداخلي الحقيقي بعد طرح عرض شريط التمرير منه، وتُعيد بعض المتصفحات الأخرى (مثل المتصفح Firefox) العرض المحدَّد في شيفرة CSS (حيث يُهمِل هذا المتصفح شريط التمرير). ويُعدّ هذا الاختلاف من متصفح لآخر هو السبب الذي يحمِلنا على عدم استعمال الدالّة getComputedStyle بل الاعتماد على الخاصيات الهندسية بدلا منها. وإذا كان المتصفح المثبَّت على حاسوبك يُخصِص مساحةً لشريط التمرير (وهو ما تُوفره معظم متصفحات نظام التشغيل Windows) ، يمكنك معاينة السكربت من خلال هذا الرابط هنا، يساوي عرض العنصر المحدَّد في شيفرة CSS الذي يضم النص 300 بكسل. تُخصِص المتصفحات Chrome وFirefox وEdge على نظام التشغيل Windows المكتبي مساحةً لشريط التمرير. ولكن المتصفح Firefox يُظهر القيمة 300 بكسل، في حين يُظهر المتصفح Chrome والمتصفح Edge قيمةً أقل، لأن المتصفح Firefox يُظهر العرض المحدَّد في شيفرة CSS، أمّا بقية المتصفحات فتُعيد العرض الحقيقي للعنصر (بعد اقتطاع عرض شريط التمرير). لاحظ أن الفرق يظهر فقط عند قراءة القيمة باستعمال الدالّة getComputedStyle(...).width في شيفرة جافاسكربت، حيث يكون الشكل الذي يَظهر على الشاشة صحيحًا. الخلاصة تملك العناصر الخاصيات الهندسية التالية: offsetParent: تمثل السلف الأقرب للعنصر من حيث التموضع أو إحدى القيم التالية: td أو th أوtable أوbody. offsetLeft وoffsetTop: تمثل إحداثيات العنصر إنطلاقًا من الحافة العلوية اليسرى للعنصر السلف offsetParent الخاص به. offsetWidth وoffsetHeight: تمثل العرض والطول الخارجي لعنصر ما بما فيه الأطر. clientLeft وclientTop: تمثلان المسافتين بين الركن العلوي الأيسر الخارجي والركن العلوي الأيسر الداخلي (المحتوى + الحاشية). بالنسبة لأنظمة التشغيل التي تعمل باللّغات التي تُكتَب من اليسار إلى اليمين، تساوي هاتان الخاصيتان عرض الإطارين العلوي والأيسر، أما بالنسبة لأنظمة التشغيل التي تعمل باللّغات التي تُكتَب من اليمين إلى اليسار، يكون شريط التمرير من الجهة اليسرى، وبذلك تضم الخاصية clientLeft عرض شريط التمرير أيضًا. clientWidth وclientHeight: تمثلان عرض وطول المحتوى بما فيه الحواشي دون احتساب عرض شريط التمرير. scrollWidth وscrollHeight: تمثلان عرض وطول المحتوى، تماما مثل ما هو الحال بالنسبة للخاصيتين clientWidth وclientHeight، ولكن مع احتساب الأجزاء غير الظاهرة من العنصر. scrollLeft وscrollTop: تمثل عرض وطول الجزء العلوي للعنصر الذي جرى المُرور عليه إنطلاقًا من الركن العلوي الأيسر للعنصر. وتُعد كلّ هذه الخاصيات قابلة للقراءة فقط (read-only) عدا الخاصيتين scrollLeft وscrollTop اللّتين يؤدي تغيير قيمتهما إلى تحريك المتصفح للعنصر. تمارين مقاس الجزء الذي لم نمر عليه بعد درجة الأهمية: 5 تمثل الخاصية elem.scrollTop مقاس الجزء الذي جرى المُرور عليه إنطلاقًا من الأعلى. كيف يمكننا إذًا الحصول على مقاس الجزء الذي لم نمر عليه بعد (ولنسمه scrollBottom)؟ اكتب الشيفرة التي تسمح بالحصول على مقاس الجزء (من عنصر ما) الذي لم نمر عليه بعد، وليكن هذا العنصر elem. ملاحظة: عليك التحقّق من الشيفرة التي تكتبها، حيث ينبغي أن تعطي القيمة 0 إذا لم يكن هناك شريط تمرير، أو إذا كان العنصر متموضعًا أساسا في الأسفل. الحل الحل هو كالآتي: let scrollBottom = elem.scrollHeight - elem.scrollTop - elem.clientHeight; أو بعبارة أخرى، (الطول الكلي) ناقص (الجزء العلوي الذي جرى المُرور عليه) ناقص (الجزء الظاهر). هذا ما يسمح بالحصول على الجزء الذي لم نمر عليه بعد. ما هو عرض شريط التمرير؟ _درجة الأهمية: _3 اكتب الشيفرة التي تُعيد عرض شريط تمرير عادي. يكون هذا العرض بالنسبة لنظام التشغيل Windows بين 12 و20 بكسل. وفي حالة ما لم يُخصِص المتصفح مساحةَ لشريط التمرير (يحدث أن يكون الشريط نصف شفاف ويتموضع فوق النص)، يكون عرضه منعدما. ملاحظة: ينبغي أن تعمل الشيفرة على أيّ صفحة HTML كانت وأن لا تكون مرتبطة بمحتوى هذه الصفحة. الحل للحصول على عرض شريط التمرير، يمكن إنشاء عنصرٍ يملك شريط تمرير لكن دون أطر ولا حواشي. وبذلك يكون الفرق بين العرض الكلي للعنصر offsetWidth وعرض المساحة الداخلية للمحتوى clientWidth هو عرض شريط التمرير. // إنشاء حاوية تملك شريط تمرير let div = document.createElement('div'); div.style.overflowY = 'scroll'; div.style.width = '50px'; div.style.height = '50px'; // إضافتها للصفحة، وإلا فسوف نحصل على مقاسات منعدمة (0 بكسل) document.body.append(div); let scrollWidth = div.offsetWidth - div.clientWidth; div.remove(); alert(scrollWidth); وضع الكرة في مركز المساحة الخضراء درجة الأهمية: 5 تكون الصفحة مبدئيا كالآتي: ما هي إحداثيات مركز المساحة الخضراء؟ احسبها واتبِّع ما يلي لوضع الكرة في مركز المساحة الخضراء: حرِّك العنصر باستعمال لغة جافاسكربت، وليس لغة CSS. ينبغي أن تعمل الشيفرة مهما كان مقاس الكرة ومهما كان مقاس المساحة الخضراء وليس فقط مع المقاسات المعطاة. ملاحظة: يمكن وضع الكرة في المركز باستعمال لغة CSS، ولكننا نريد تجسيد ذلك باستعمال لغة جافاسكربت. سوف تُصادف مستقبلا مواضيع أخرى ووضعيات أكثر تعقيدًا، حيث يكون استعمال جافاسكربت ضرورة حتمية. يُعدّ هذا المثال مجرّد تمرين بسيط للتسخين. يمكنك الاطلاع على شيفرة التمرين من هنا الحل موضع الكرة هو position:absolute، وهو ما يعني أن إحداثياتها بالنسبة للأعلى ولليسار تُحسب بناءً على أقرب عنصر متموضع، أي العنصر #field (لأنه يملك الموضع position:relative). يكون مبدأ الإحداثيات (0،0) متطابقا مع الركن العلوي الأيسر الداخلي للمساحة الخضراء. تُمثِل قيم الخاصيتين clientWidthوclientHeight العرض والطول الداخليين للمساحة الخضراء، وبالتالي تكون إحداثيات المركز كالآتي: (clientWidth/2, clientHeight/2). ولكننا حين نسند هذه القيم للخاصيتين ball.style.left وball.style.top نجعل الحافة العلوية اليسرى للكرة، وليس الكرة بأكملها، تتحرك نحو المركز، فتظهر على هذا الشكل: ball.style.left = Math.round(field.clientWidth / 2) + 'px'; ball.style.top = Math.round(field.clientHeight / 2) + 'px'; ومن أجل مطابقة مركز الكرة مع مركز المساحة الخضراء، ينبغي تحريك الكرة بمقدار نصف طولها للأعلى وبمقدار نصف عرضها نحو الجهة اليسرى. وبذلك تتموضع الكرة في مركز المساحة الخضراء. ball.style.left = Math.round(field.clientWidth / 2 - ball.offsetWidth / 2) + 'px'; ball.style.top = Math.round(field.clientHeight / 2 - ball.offsetHeight / 2) + 'px'; تحذير لا تعمل الشيفرة بالشكل المطلوب إذا لم يكن للكرة طول أو عرض. <img src="ball.png" id="ball"> إذا لم يجد المتصفح طول وعرض صورةٍ ما ضمن سمات الوسوم أو في شيفرة CSS، يعتدّها منعدمة إلى غاية التحميل الكلي للصورة ، وبالتالي تكون قيمة ball.offsetWidth منعدمة إلى غاية تحميل الصورة بشكل كامل. وهذا ما ينتج عنه إحداثيات خاطئة عند استعمال الشيفرة أعلاه. بعد عملية التحميل الأولى للصورة، يضع المتصفح الصورة في الذاكرة المؤقتة (cache)، وعند إعادة التحميل، يحصل المتصفح على المقاسات في الحين. لكن المشكل هو أن قيمة الخاصية ball.offsetWidth تكون منعدمة عند عملية التحميل الأولى. يمكن تفادي حصول ذلك بإسناد قيمة للسمتين width وheight الخاصتين بالصورة <img>: <img src="ball.png" width="40" height="40" id="ball"> أو إضافة المقاسات في شيفرة CSS كالآتي: #ball { width: 40px; height: 40px; } يمكنك الاطلاع على الحل من خلال هذا الرابط الفرق بين العرض المحدَّد في شيفرة CSS وclientWidth درجة الأهمية 5 ما هو الفرق بين getComputedStyle(elem).width و elem.clientWidth؟ أعط ثلاث فوارق على الأقل، ويستحسن أكثر. الحل الفوارق هي كالآتي: تُعدّ قيمة الخاصية clientWidth قيمة عددية، بينما تكون قيمة getComputedStyle(elem).width عبارة عن سلسلة نصية تحمل في نهايتها الحرفينpx. يمكن أن تُعيد الدالّة getComputedStyle قيمًا غير عددية كالقيمة "auto" مثلا بالنسبة للعناصر السطرية. تمثل الخاصية clientWidth مقاس المساحة الداخلية للمحتوى بالإضافة إلى الحواشي، بينما لا يضم العرض المحدَّد في شيفرة CSS (مع خاصية box-sizing عادية) الحواشي (يتمثل في مقاس المساحة الداخلية للمحتوى دون الحاشيتين). إذا كان هناك شريط تمرير وخَصص المتصفح مساحةً لهذا الشريط، قد تطرح بعض المتصفحات هذه المسافة من العرض المحدَّد في شيفرة CSS (لأنه لم يَعُد متاحا ليضم محتوى ما)، ولكن بعض المتصفحات الأخرى لا تسلك السلوك نفسه. أما الخاصية clientWidth فهي دائما تمثل نفس القيمة، حيث يُطرَح عرض شريط التمرير إذا خُصصت له مساحةً ما. ترجمة -وبتصرف- للفصل Element size and scrolling من كتاب Browser: Document, Events, Interfaces