البحث في الموقع
المحتوى عن 'dom'.
-
في سلسلة فهم شجرة DOM، ناقشنا شجرة DOM وكيفية الوصول والتنقل وإضافة وإزالة وتعديل العقد والعناصر باستخدام وحدة تحكّم أدوات المطوّر. 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; } code { background-color: rgb(250, 250, 250); border-radius: 3px; } بإمكاننا إجراء أي تغيير على شجرة DOM حتى هذه اللحظة، من وجهة نظر المستخدم فإنّه ليسَ مفيدًا للغاية لأننا قمنا بتنفيذ التغييرات يدويًّا فقط. سنتعرَّف الآن على الأحداث وربط كل شيء معها لإنشاء مواقع تفاعلية. ما هي الأحداث؟ الأحداث (event) هي الإجراءات التي تحدث في المتصفّح والتي يبدأها المستخدم أو المتصفّح. فيما يلي عدّة أمثلة عن الأحداث الشائعة التي يمكن أن تحدث على موقع الويب: انتهاء الصفحة من التحميل نقر المستخدم على زر تحويم المستخدم سهم الفأرة فوق قائمة منسدلة إرسال المستخدم بياناته ضغط المستخدم على أحد مفاتيح لوحة مفاتيحه. بإمكان المطورين إظهار رسائل للمستخدم للتحقق من صحة بيانات، والاستجابة إلى ضغط زرٍّ، والعديد من الإجراءات الأخرى وذلك عبر برمجة استجابات جافاسكربت على حدث معيّن. سنتطرق إلى مُعَالِجَاتَ الأحداث ومُسْتمعي الأحداث وكائنات الأحداث. كما سنعرّج لثلاث طرقٍ مختلفة لكتابة التعليمات البرمجيّة للتعامل مع الأحداث، وبعض الأحداث الأكثر شيوعًا. ستتمكّن من تقديم تجربة ويب أكثر تفاعلية للمستخدمين النهائيين، من خلال التعرف على الأحداث. معالجات الأحداث ومستمعي الأحداث عندما ينقر المستخدم على زر أو يضغط المفتاح، يتم قدحُ الحدث، ويُسمّى آنذاك حدث النقر click أو حدث الضغط keypress على التوالي. معالج الأحداث (event handler) هو دالَة جافاسكربت تعمل عند وقوع حدثٍ ما. يقوم مستمع الحدث (event listener) بإرفاق واجهة استجابة لعنصر، ممَّا يسمح لهذا العنصر بالانتظار لإطلاق الحدث المحدّد أو ويكأنه يتنصَّت ويستمع إلى صوت إطلاق ذلك الحدث المتمثِّل بعيار ناري. هناك ثلاث طرق لإسناد الأحداث إلى العناصر: مُعَالِجَات الأحداث المضمّنة (Inline event handlers). خاصيات معالج الحدث (Event handler properties). مستمعو الأحداث (Event listeners). سنتعرف على ثلاث الطرق جَمِيعِها للتأكّد من أنك على دراية كاملة بكلِّ طريقة لإطلاق الحدث، ومناقشة إيجابيات وسلبيات كل طريقة. الخصيّات المضمّنة لمعالج الأحدث لبدء التعرف على معالجات الأحداث، سننظر أولاً في معالج الحدث المضمَّن (inline event handler9). لنبدأ بمثالٍ بسيط للغاية، يتكون من العنصرين زرٍّ button وفقرة p. ونريد من المستخدم أن يضغط على الزر لتغيير محتوى النص في الفقرة. لنبدأ بصفحة HTML تحتوي على زرٍ. سنشير إلى ملف جافاسكربت وسنضيف الشيفرة البرمجية فيما بعد: الملف events.html: <!DOCTYPE html> <html lang="en-US"> <head> <title>Events</title> </head> <body> <!-- إضافة زر --> <button>Click me</button> <p>Try to change me.</p> </body> <!-- ملف جافاسكربت --> <script src="js/events.js"></script> </html> سنضيف خاصية على الزر مباشرةً تسمّى onclick ( أي عند النقر، نفِّذ ...). ستكون قيمة خاصيّة هي دالة ننشئها تسمّى changeText() تمثِّل معالج الحدث. الملف events.html: <!DOCTYPE html> <html lang="en-US"> <head> <title>Events</title> </head> <body> <button onclick="changeText()">Click me</button> <p>Try to change me.</p> </body> <script src="js/events.js"></script> </html> لننشئ الملف events.js ضمن المسار js/. سننشئ الدالة changeText() التي ستعدّل محتوى النص المتمثِّل بقيمة الخاصية textContent على عنصر الفقرة p. الملف js/events.js: // تعديل نصِّ الفقرة عبر هذه الدالّة const changeText = () => { const p = document.querySelector('p'); p.textContent = "I changed because of an inline event handler."; } عند تحميلك الصفحة events.html لأول مرة ستبدو كما يلي: حينما يتمُّ النقر على الزر من قبل المستخدم أو غيره، فإن محتوى نصّ الفقرة سيتغيّير من Try to change me. إلى I changed because of an inline event handler. تعدُّ معالجات الأحداث المضمّنة طريقةً مباشرةً لبدء فهم الأحداث، ولكن يجب ألا تستخدم بشكل عام خارج نطاق الاختبار والأغراض التعليميّة. يمكنك مقارنة معالجات الأحداث المضمَّنة بأنماط CSS المضمَّنة في عنصر HTML. من العمليّ للغاية الحفاظ على ورقة أنماط منفصلة للأصناف بدلًا من إنشاء أنماط مضمّنة في كل عنصر، تمامًا كما هو أكثر جدوى للحفاظ على شيفرات جافاسكربت التي تتم معالجتها بالكامل من خلال ملف منفصل بدلًا من إضافتها إلى كل عنصر. خاصيات معالج الأحداث الخطوة التالية، هي الانتقال من معالج الأحداث المضمّنة إلى خاصية معالج الأحداث. يعمل هذا بشكل مشابه جدًّا لمعالج مضمَّن، باستثناء أننا نقوم بتعيين خاصية العنصر في الجافاسكربت بدلًا من خاصية HTML. سيكون الإعداد هو نفسه هنا، باستثناء أننا لم نعد ندرج onclick="changeText()" في شيفرة العنصر: الملف events.html: ... <body> <button>Click me</button> <p>I will change.</p> </body> ... ستظلُّ وظيفتنا متشابهة أيضًا، إلا أننا نحتاج الآن إلى الوصول إلى العنصر button في جافاسكربت. يمكننا ببساطة الوصول إلى الخاصية onclick مثلما نصل إلى الخاصية style أو id أو أي خاصية عنصر أخرى، ثم نقوم بتعيين مرجع للدالة المراد تنفيذها. الملف js/events.js: // الدالة المراد تنفيذها عند وقوع الحدث والتي تعدِّل محتوى الفقرة const changeText = () => { const p = document.querySelector('p'); p.textContent = "I changed because of an event handler property."; } // button إضافة معالج حدث كخاصية للعنصر const button = document.querySelector('button'); button.onclick = changeText; ملاحظة: لا تتبع معالجات الأحداث عُرْفَ سنام الجمل (camelCase) التي تلتزم بها معظم تعليمات جافاسكربت البرمجية. لاحظ أن الرمز هو onclick، وليس onClick. عند تحميل الصفحة لأول مرة ، سيعرض المتصفح ما يلي: الآن عند النقر فوق الزر، سيكون له تأثير مماثل كما كان من قبل: لاحظ بتمريرنا مرجع الدالة المراد تنفيذها إلى الخاصية onclick، فإننا لا نقوم بتضمين الأقواس، حيث أننا لا نستدعي الدالّة في تلك اللحظة، ولكننا نمرّر مرجعًا إليها. button.onclick = changeText; تعدُّ خاصيَّة معالج الأحداث أكثرَ قابليَّةٍ للصيانة من المعالج المضمَّن، إلَّا أنها تعاني عقباتً مماثلة. فمثلاً، محاولةُ تعييِّن عدّة خصائص onclick منفصلة سيطبق التأثير على آخر محاولة. كما هو موضَّح أدناه. الملف js/events.js: const p = document.querySelector('p'); const button = document.querySelector('button'); const changeText = () => { p.textContent = "Will I change?"; } const alertText = () => { alert('Will I alert?'); } // يمكن تعديل الأحداث button.onclick = changeText; button.onclick = alertText; في المثال أعلاه، لن تعرض النقرة على الـ button سوى تنبيه، ولن تغيّر النصّ p، لأن alert() هو آخر تعليمة برمجية أضيفت إلى الخاصية. من خلال فهم كل من معالجات الأحداث المضمّنة وخصائص معالج الأحداث، دعنا ننتقل إلى مستمعي الأحداث. مستمعي الأحداث أحدث إضافة إلى معالجات الأحداث في جافاسكربت هي مستمعي الحدث. يراقب مستمع الحدث حدثًا على عنصر. بدلاً من إسناد الحدث مباشرةً إلى خاصيّة في العنصر، سنستخدم التابع addEventListener() للاستماع للحدث. يأخذ addEventListener() معاملين إلزاميين - الحدث الذي يجب الاستماع إليه، ودالَّة ردِّ نداء لتنفيذها عند اطلاق الحدث. ملف الـ HTML لمستمع الأحداث الخاص بنا هو نفسه في المثال السابق. الملف events.html: ... <button>Click me</button> <p>I will change.</p> ... سنظلّ نستخدم نفس الدالّة changeText() كما سابقًا ولكن سنربطها مع التابع addEventListener() على الزر. الملف js/events.js: // الدالة المراد تنفيذها عند وقوع الحدث والتي تعدِّل محتوى الفقرة const changeText = () => { const p = document.querySelector('p'); p.textContent = "I changed because of an event listener."; } // الاستماع لحدث النقر const button = document.querySelector('button'); button.addEventListener('click', changeText); لاحظ أنه مع أوّل طريقتين، تمّت الإشارة إلى حدث النقر باسم onclick، ولكن مع مستمعي الأحداث يشار إليه باسم click. تُسْقَطُ on من الكلمة لكل مُسْتَمِع حدث. في القسم التالي، سنأخذ المزيد من الأمثلة على أنواع الأحداث الأخرى. عند إعادة تحميل الصفحة بشيفرة جافاسكربت أعلاه، ستتلقى الخَرْج التالي: للوهلة الأولى، يبدو مستمعو الأحداث متشابهين جدًا مع خصائص معالج الأحداث، لكن لديهم بعض المزايا. يمكننا تعيين عدّة مستمعين على نفس العنصر، كما هو موضَّح في المثال أدناه. الملف js/events.js: const p = document.querySelector('p'); const button = document.querySelector('button'); const changeText = () => { p.textContent = "Will I change?"; } const alertText = () => { alert('Will I alert?'); } // يمكن إضافة عدة مستمعات إلى الحدث نفسه والعنصر نفسه أيضًا button.addEventListener('click', changeText); button.addEventListener('click', alertText); سيتمُّ إطلاق الحدثين في المثال أعلاه، ممّا يوفِّر للمستخدم كلًّ من التنبيه والنصِّ المعدّل بعد النقر فوق التنبيه. ستستخدام غالباً دالّات مجهولة (Anonymous functions) بدلاً من مرجع دالّة على مُسْتَمع الحدث. الدالّات المجهولة هي دالّات غير مُسَمَّاة. // دالة مجهولة يراد تنفيذها عند إطلاق الحدث button.addEventListener('click', () => { p.textContent = "Will I change?"; }); من المُمْكِن أيضًا استخدام دالّة removeEventListener() لإزالة حدث واحد أو جميع الأحداث من عنصر. // button من العنصر alert حذف الدالة button.removeEventListener('click', alertText); يمكنك استخدام addEventListener()على الكائنين document و window. يُعَدُّ مُسْتَمِعُو الأحداث حاليًا الطريقةَ الأكثرَ شيوعًا والمفضَّلة للتعامل مع الأحداث في جافاسكربت. الأحداث المشتركة تعلمّنا عن معالجات الأحداث المضمّنة، وخصائص معالج الأحداث، ومستمعي الأحداث باستخدام حدث النقرات، ولكن هناك العديد من الأحداث في جافاسكربت. سنتطرق لأكثر الأحداث شيوعًا أدناه. أحداث الفأرة أحداث الفأرة هي من بين الأحداث الأكثر استخدامًا. تشير إلى الأحداث التي تنطوي على نقر زر الفأرة أو تحريك المؤشر وتحويمه (hovering). تتوافق هذه الأحداث أيضًا مع الإجراء المماثل على جهاز يعمل باللمس. الحدث الوصف click يُطلَق عند الضغط بزر الفأرة أو عند الإفلات على العنصر dblclick يُطلَق عندما يتم النقر فوق عنصر مرتين mouseenter يُطلَق عندما يدخل المؤشر داخل عنصر mouseleave يُطلَق عندما يترك المؤشر عنصرًا mousemove يُطلَق في كل مرة يتحرك فيها المؤشر داخل عنصر ما`` إن حدث النقر click هو حدثٌ مركَّب يتألف من الجمع بين حدثين هما mousedown و mouseup، ويُطلَق حينَ الضغط على زر الفأرة لأسفل أو رفعه، على التوالي. يؤدي استخدام الحدثين mouseenter و mouseleave بالترادف إلى إعادة إنشاء تأثير التحويم الذي يستمر ما دام مؤشر الفأرة على العنصر. أحداث النموذج أحداث النموذج form هي إجراءات تتعلق بالنماذج، مثل عناصر الإدخال input التي يتم تحديدها أو غير المحددة، والنماذج التي يتم إرسالها. الحدث وصف submit يُطلَق عند إرسال النموذج focus يُطلَق عند تلقِي عنصر (مثل المدخلات) التركيز blur يُطلَق حينَ يفقد العنصر التركيز يتحقق التركيزfoucus عند تحديد عنصر، على سبيل المثال، من خلال النقر بالفأرة أو الانتقال إليه عبر المفتاح TAB غالبًا ما تستخدم جافاسكربت لإرسال النماذج وإرسال القيم إلى لغة الواجهة الخلفية. تتمثّلُ ميزة استخدام جافاسكربت لإرسال النماذج بأنه لا يتطلب إعادة تحميل الصفحة لإرسال النموذج، ويمكن استخدام جافاسكربت للتحقق من حقول الإدخال المطلوبة. أحداث لوحة المفاتيح تُستخدم أحداث لوحة المفاتيح لمعالجة إجراءات لوحة المفاتيح، مثل الضغط على مفتاح ما، ورفع الضغط عن مفتاح، واستمرار ضغط مفتاح. الحدث وصف keydown يُطلَق مرة واحدة عند الضغط على أي مفتاح keyup يُطلَق مرة واحدة عندما يتم تحرير (إفلات الضغط) المفتاح المضغوط keypress يُطلَق بشكل مستمر أثناء الضغط على المفتاح قد يبدو الحدثين keydown و keypress متشابهين إلا أنهما لا يتيحان الوصول إلى جميع المفاتيح بالضبط. في حين أنَّ keydown يقرّ بكل ضغطَة (أي الضغط على أي مفتاح)، يستثني الحدث keypress المفاتيح التي لا يولد الضغط عليها حرفًا، مثل المفاتيح SHIFT و DELETE و ALT وغيرها. أحداث لوحة المفاتيح لها خصائص محددة للوصول إلى المفاتيح الفردية. إذا مُرِّرَ المعامل الأول (الكائن event) عبر مستمع الحدث، فيمكننا الوصول إلى مزيد من المعلومات حول الإجراء الذي حدث أهمها ثلاثة خصائص تتعلق بكائنات لوحة المفاتيح هي: keycode و key و code. إذا ضغط المستخدم على المفتاح a على لوحة المفاتيح، فستظهر الخصائص التالية المتعلقة بهذا المفتاح: خاصية وصف مثال keyCode عدد يتعلق بالمفتاح 65 key يمثل اسم الحرف a code يمثل المفتاح الفعلي الذي يتم الضغط عليه keyA لإظهار كيفية جمع هذه المعلومات عبر وحدة التحكم جافاسكربت، يمكننا كتابة سطور التعليمات البرمجيّة التالية: // keyCode و key و code التحقق من الخاصيات document.addEventListener('keydown', event => { console.log('key: ' + event.keyCode); console.log('key: ' + event.key); console.log('code: ' + event.code); }); بمجرد ضغط ENTER في الطرفية، يمكننا آنذاك الضغط على أي مفتاح من لوحة المفاتيح، في هذا المثال، سنضغط a. Output keyCode: 65 key: a `code: KeyA` إنَّ الخاصيَّةَ keyCode هي عدد يمثلُّ قيمة المفتاح الذي ضُغِطَ بينما تمثِّل خاصيَّةَ key اسم الحرف، التي يمكن أن تتغيّر - على سبيل المثال، ضغط المفتاح a مع SHIFT سيؤدي A. وتمثِّلُ الخاصيِّةُ code المفتاح الفيزيائي على لوحة المفاتيح. لاحظ أنَّ keyCode في طريقها إلى الزوال ويفضل استخدام code في مشاريع جديدة. لمعرفة المزيد، يمكنك عرض القائمة الكاملة للأحداث على شبكة مطوري Mozilla. كائنات الحدث يتكون الكائن Event من الخاصيّات والتوابع التي يمكن لجميع الأحداث الوصول إليها. بالإضافة إلى Event الكائن العام، كل نوع من أنواع الأحداث له امتدادات خاصة به، مثل KeyboardEvent و MouseEvent. سيمرَّر الكائن Event من خلال دالّة المستمع كمعامل. وعادة ما يكتب باسم event أو e. يمكننا الوصول إلى الخاصية code التابعة للـحدث keydown لتكرار عناصر تحكم لوحة المفاتيح في لعبة مثلًا. قم بإنشاء ملف HTML يحوي العنصر <p> وحملّه في المتصفح. الملف event-test-p.html: <!DOCTYPE html> <html lang="en-US"> <head> <title>Events</title> </head> <body> <p></p> </body> </html> ثم اكتب شفرة جافاسكربت التالية في طرفية أدوات المطور . // تمرير حدث إلى مستمع document.addEventListener('keydown', event => { var element = document.querySelector('p'); // إسناد قيمة رموز المفاتيح المراد الضغط عليها إلى متغيرات var a = 'KeyA'; var s = 'KeyS'; var d = 'KeyD'; var w = 'KeyW'; // تحديد الاتجاه لكل مفتاح switch (event.code) { case a: element.textContent = 'Left'; break; case s: element.textContent = 'Down'; break; case d: element.textContent = 'Right'; break; case w: element.textContent = 'Up'; break; } }); عند الضغط على أحد المفاتيح - a، أو s، أو d، أو w سترى خرجًا مشابهًا لما يلي: يمكنك الاستمرار في تطوير كيفية استجابة المتصفح للمستخدم حينَ الضغط على هذه المفاتيح، ويمكن إنشاء موقع ويب أكثر ديناميكية. سنتطرق إلى إحدى خصائص الأحداث الأكثر استخدامًا: الخاصيِّةُ target. في المثال التالي ، لدينا ثلاثة عناصر div ضمن قسم واحد section. الملف event-test-div.html: <!DOCTYPE html> <html lang="en-US"> <head> <title>Events</title> </head> <body> <section> <div id="one">One</div> <div id="two">Two</div> <div id="three">Three</div> </section> </body> </html> باستخدام event.target في جافاسكربت ضمن طرفيّة أدوات المطور (Developer Console)، يمكننا وضع مستمع حدث واحد على العنصر section الخارجي والحصول على العنصر الأكثر تداخلًا. const section = document.querySelector('section'); // طباعة الهدف المحدَّد section.addEventListener('click', event => { console.log(event.target); }); يؤدي النقر فوق أي عنصر من هذه العناصر إلى إعادة إخراج العنصر المحدد ذي الصلة إلى الطرفية باسـتخدام event.target. إتاحة وضع مُسْتَمع حَدَث واحد فقط، يُمَكِّنُ من الوصول إلى العديد من العناصر المتداخلة، وهذا في غاية الأهمية. باستخدام الكائن Event، يمكننا إعداد استجابات متعلقة بجميع الأحداث، بما في ذلك الأحداث العامة والإضافات الأكثر تحديدًا. الخلاصة الأحداث هي الإجراءات التي تحدث على موقع ويب، مثل النقر فوق الفأرة أو تحويم المؤشر أو إرسال نموذج أو تحميل صفحة أو الضغط على مفتاح في لوحة المفاتيح. تصبح جافاسكربت تفاعليًا وديناميكيًا حقًا عندما نتمكن من جعل مواقع الويب تستجيب للإجراءات التي اتخذها المستخدم. تعلمنا -في هذا الدليل ما هي الأحداث، وأمثلة عن الأحداث الشائعة، والفرق بين معالجات الأحداث ومستمعي الأحداث، وكيفية الوصول إلى الكائن Event. ستتمكن من إنشاء مواقع وتطبيقات ديناميكية، باستخدام هذه المعلومات. ترجمة - وبتصرف- للمقال Understanding Events in JavaScript لصاحبته Tania Rascia
- 1 تعليق
-
- 1
-
تعرفنا في المقال السابق مكونات الويب: عناصر HTML المخصصة وقوالبها على عناصر HTML المخصصة وكيفية إنشائها وسنكمل التعرف على موضوع مهم في هذا الصدد وهو شجرة DOM الخفية Shadow DOM -والتي تترجم أيضًا إلى ظل شجرة DOM- تستخدَم للتغليف encapsulation، والتي تسمح لكل مكوِّن ويب مخصص أن يمتلك شجرةً خفيةً لا يمكن للمستند الرئيسي الوصول إليها صدفةً، وقد تمتلك قواعد تنسيق خاصة وغير ذلك من الخواص. شجرة DOM الخفية هل لديك فكرة عن مدى تعقيد إنشاء وتنسيق أدوات تحكم المتصفح؟ لنفترض وجود العنصر <"input type="range> مثلًا: input[type="range" i] { appearance: auto; cursor: default; color: rgb(157, 150, 142); padding: initial; border: initial; margin: 2px; } يستخدم المتصفح ضمنيًا DOM/CSS لرسم أدوات التحكم، ولا تظهر بنية شجرة DOM لنا عادةً، لكن يمكننا رؤيتها باستخدام أدوات التطوير بتفعيل الخيار Show user agent shadow DOM من إعدادات أدوات المطور Dev Tools تحديدًا قسم Elements في المتصفح Chrome مثلًا. ستبدو الشجرة DOM للعنصر السابق كالتالي: ما نراه تحت العنوان shadow-root# يسمَّى شجرة DOM المخفية، ولا يمكن الحصول على عناصر شجرة DOM المخفية المضمنة باستخدام استدعاءات JavaScript النظامية أو المحددات selectors، فهذه العناصر ليست أبناءً نظاميةً، بل هي تقنية تغليف متقدمة. سنرى في المثال السابق السمة المفيدة pseudo، وهي سمة غير قياسية وموجودة لأسباب تاريخية، حيث يمكننا استخدامها لتنسيق العناصر الفرعية باستخدام CSS كالتالي: <style> /* make the slider track red */ input::-webkit-slider-runnable-track { background: red; } </style> <input type="range"> See the Pen JS-p3-06-Shadow-DOM -ex1 by Hsoub (@Hsoub) on CodePen. بدأت المتصفحات باختبار الشيفرة بالاستفادة من بنى DOM الداخلية لتجهيز أدوات التحكم، وبعد فترة من الزمن طوّرت شجرة DOM الخفية وأصبحت معياريةً لتسمح لنا وللمطورين بالقيام بأشياء مماثلة. سنستخدم لاحقًا المعيار الحديث لشجرة DOM الخفية الذي تغطيه مواصفات DOM وغيرها من المواصفات ذات الصلة. الشجرة الخفية Shadow tree يمكن لكائن DOM أن يمتلك نوعين من الأشجار الفرعية: شجرة ظاهرة Light tree: وتمثل شجرةً فرعيةً نظاميةً مكوّنةً من عناصر HTML أبناء، وجميع الأشجار التي رأيناها في المقال السابق من هذا النوع. شجرة خفية Shadow tree: شجرة DOM فرعية لا تظهر على صفحة HTML، بل تكون مخفيةً من الظهور والرؤية. إذا امتلك العنصر الشجرتين السابقتين فسيصيّر المتصفح الشجرة الخفية، لكن يمكن إعداد توليفة بين الشجرتين، وسنرى تفاصيل أكثر في مقالات لاحقة. يمكن استخدام الشجرة الخفية في العناصر المخصصة لإخفاء المكوّنات الداخلية وتطبيق تنسيق خاص بالمكوِّن. يخفي العنصر <show-hello> على سبيل المثال شجرة DOM الداخلية ضمن شجرة خفية: <script> customElements.define('show-hello', class extends HTMLElement { connectedCallback() { const shadow = this.attachShadow({mode: 'open'}); shadow.innerHTML = `<p> Hello, ${this.getAttribute('name')} </p>`; } }); </script> <show-hello name="John"></show-hello> See the Pen JS-p3-06-Shadow-DOM -ex2 by Hsoub (@Hsoub) on CodePen. ستظهر النتيجة في أدوات تطوير متصفح بالشكل التالي، حيث تكون جميع المحتويات تحت "shadow-root#": يُنشئ الاستدعاء ({...:elem.attachShadow({mode شجرةً مخفيةً أولًا، لكن توجد عقبتان: يمكن إنشاء جذر مخفي واحد لكل عنصر. يجب أن يكون العنصر عنصرًا مخصصًا، أو أحد العناصر التالية: "article". "blockquote". "body". "div". "footer". "h1…h6". "header". "main". "nav". "p". "section". "span". ولا يمكن لغير هذه العناصر استضافة شجرة خفية. يضبط الخيار mode مستوى التغليف، ويمكن أن يحمل إحدى القيمتين: "open": يمكن أن نحصل على الجذر الخفي من خلال elem.shadowRoot، ويمكن لأي شيفرة الوصول إلى الشجرة الخفية لــ elem. "closed": ويعيد الأمر elem.shadowRoot القيمة null دائمًا. يمكن الوصول إلى شجرة DOM الخفية عن طريق المرجع الذي يعيده attachShadow فقط، ومن الممكن أن يكون مخفيًا ضمن صنف، وتكون الأشجار الخفية الأصلية المتعلقة بالمتصفح -مثل <input type="range">- من النوع المغلق، ولا يمكن الوصول إليها، ويشابه الجذر الخفي shadow root الذي يعيده attachShadow العناصر، ويمكن إظهاره باستخدام innerHTML أو توابع DOM مثل append. يُدعى العنصر الذي يمتلك جذرًا خفيًا بمضيف الشجرة الخفية shadow tree host، ويتاح للاستخدام ضمن الخاصية host للجذر الخفي: // لن يعيد شيئًا elem.shadowRoot وإلا {mode: "open"} بافتراض أن alert(elem.shadowRoot.host === elem); // محقق التغليف Encapsulation تفصَل شجرة DOM الخفية عن المستند الرئيسي: لا تكون عناصر شجرة DOM الخفية مرئيةً للتابع querySelector من شجرة DOM الظاهرة، وقد تمتلك عناصر شجرة DOM الخفية معرفات id تتعارض مع تلك الموجودة في شجرة DOM الظاهرة، فهي فريدة ضمن الشجرة الخفية فقط. لشجرة DOM الخفية أوراق تنسيق خاصة بها، إذ لا تُطبق قواعد التنسيق الخارجية عليها. إليك مثالًا: <style> /*(1) #elem لن يطبق التنسيق على الشجرة الخفية للعنصر*/ p { color: red; } </style> <div id="elem"></div> <script> elem.attachShadow({mode: 'open'}); // (للشجرة الخفية تنسيقها الخاص (2 elem.shadowRoot.innerHTML = ` <style> p { font-weight: bold; } </style> <p>Hello, John!</p> `; // مرئية فقط للاستعلامات التي تجري داخل الشجرة <p> (3) alert(document.querySelectorAll('p').length); // 0 alert(elem.shadowRoot.querySelectorAll('p').length); // 1 </script> تشير (1) و(2) و(3) أعلاه إلى الآتي: (1) لا يؤثر تنسيق الصفحة على تنسيق الشجرة الخفية. (2) يؤدي التنسيق داخل الشجرة عمله بطريقة صحيحة. (3) للحصول على العناصر من الشجرة الخفية، لا بدّ من الاستعلام عنها من داخل الشجرة. تقليم شجرة DOM الخفية وعملية التركيب تحتاج العديد من المكوّنات إلى محتوىً لتصييره، مثل النوافذ ومعارض الصور والقوائم وغيرها، ويتوقع العنصر المخصص <custom-tabs> محتوى نافذة ليمرَّر إليه، على غرار العناصر الأصلية المضمنة في المتصفح مثل <select> -عدا عناصر <option>-، ويحتاج العنصر <custom-menu> كذلك إلى محتوى على شكل عناصر قائمة. قد تبدو الشيفرة التي تستعمل العنصر <custom-menu> كالتالي: <custom-menu> <title>Candy menu</title> <item>Lollipop</item> <item>Fruit Toast</item> <item>Cup Cake</item> </custom-menu> سيتمكن بعدها مكوّننا من تصيير المحتوى بصورة صحيحة، على شكل قائمة جميلة لها عنوان وعناصر ومعالجات لأحداث القائمة وغيرها، ولكن كيف سننجز ذلك؟ يمكننا أن نحاول تحليل محتوى العناصر وأن ننسخ أو نعيد ترتيب عقد DOM ديناميكيًا، لكن إذا كنا سننقل عناصر إلى شجرة DOM الخفية shadow DOM، فلن يُطبَّق تنسيق CSS الموجود ضمن المستند عليها، وبالتالي سنفقد التنسيق المرئي، وسيحتاج ذلك إلى كتابة بعض الشيفرة، ولحسن الحظ ليس علينا فعل ذلك، إذ تدعم شجرة DOM الخفية عناصر <slot> التي تُملأ تلقائيًا بالمحتوى الموجود في شجرة DOM الظاهرة وشبهنا هذه العملية بعملية "التقليم" التي تجرى على الأشجار من قص وإنشاء فتحة في الشجرة ووضع فرع في الفتحة من شجرة أو نوع آخر لذلك أطلقنا عليها هذا الاسم وسنستعمل اسم "فتحة" المقابل للكلمة الأجنبية slot (اسم العنصر) للسهولة. الفتحات المسماة Named slots لنلق نظرةً على طريقة عمل التقليمات في هذا المثال البسيط، حيث توفر شجرة DOM الخفية للعنصر المخصص <user-card> فتحتين تُملآن من شجرة DOM الظاهرة: <script> customElements.define('user-card', class extends HTMLElement { connectedCallback() { this.attachShadow({mode: 'open'}); this.shadowRoot.innerHTML = ` <div>Name: <slot name="username"></slot> </div> <div>Birthday: <slot name="birthday"></slot> </div> `; } }); </script> <user-card> <span slot="username">John Smith</span> <span slot="birthday">01.01.2001</span> </user-card> See the Pen JS-p3-06-Shadow-DOM -ex3 by Hsoub (@Hsoub) on CodePen. يعرّف العنصر <"slot name="X> في شجرة DOM الخفية نقطة إدراج، وهو المكان الذي تُصيَّر فيه العناصر التي تعرف فيها الخاصية "slot="X، ثم ينفِّذ المتصفح عملية تركيب composition، إذ يأخذ عناصر من شجرة DOM الظاهرة، ويصيّرها ضمن الفتحات الملائمة ضمن شجرة DOM الخفية، وهكذا سنحصل في نهاية الأمر على مكوِّن يمكن ملؤه بالبيانات، وهذا ما نريده. إليك بنية الشجرة DOM بعد تنفيذ السكربت مهملين عملية التركيب: <user-card> #shadow-root <div>Name: <slot name="username"></slot> </div> <div>Birthday: <slot name="birthday"></slot> </div> <span slot="username">John Smith</span> <span slot="birthday">01.01.2001</span> </user-card> لقد أنشأنا شجرة DOM خفيةً، وسنجدها تحت #shadow-root. سيمتلك العنصر الآن شجرتي DOM الظاهرة والخفية. سيبحث المتصفح -لأغراض التصيير- عن السمة "..."=slot لكل <"..."=slot name> التي لها نفس الاسم في شجرة DOM الظاهرة، وستُصيَّر هذه العناصر ضمن الفتحات. وعندها ستسمى النتيجة شجرة DOM المسطّحة flattened DOM. <user-card> #shadow-root <div>Name: <slot name="username"> <!-- slotted element is inserted into the slot --> <span slot="username">John Smith</span> </slot> </div> <div>Birthday: <slot name="birthday"> <span slot="birthday">01.01.2001</span> </slot> </div> </user-card> لقد أوُجِدت شجرة DOM المسطحة لأغراض التصيير ومعالجة الأحداث، قد تبدو الطريقة التي تُعرض بها الأمور افتراضيةً نوعًا ما، لكن لن تُنقَل العقد في المستند فعليًا، فمن السهل التحقق من أنّ العقد لا تزال في مكانها بتنفيذ الأمر querySelectorAll. alert( document.querySelectorAll('user-card span').length ); // 2 تُشتقّ شجرة DOM المسطحة من شجرة DOM الخفية بإدراج الفتحات، حيث يصيّرها المتصفح ويستخدمها في وراثة التنسيق وانتقال الأحداث -وهو ما سنوضّحه لاحقًا-، لكن نظرتها لن تغير JavaScript إلى المستند قبل الوصول إلى الشجرة المسطحة. ملاحظة: يمكن للأبناء من المستوى الأعلى فقط امتلاك السمة "…"=slot، إذ يمتلك الأبناء المباشرون للمضيف الخفي -وهو العنصر <user-card> في مثالنا- السمة "..."=slot فقط، بينما ستُهمل هذه السمة للعناصر الداخلية. وسيتجاهل المتصفح في المثال التالي العنصر <span> الثاني، فهو ليس ابنًا مباشرًا للعنصر <user-card>: <user-card> <span slot="username">John Smith</span> <div> <!-- invalid slot, must be direct child of user-card --> <span slot="birthday">01.01.2001</span> </div> </user-card> إذا وجدت عناصر لها فتحات بنفس الاسم في شجرة DOM الظاهرة، فستوضع في الفتحة واحدةً تلو الأخرى، إليك المثال التالي: <user-card> <span slot="username">John</span> <span slot="username">Smith</span> </user-card> إليك مثالًا عن شجرة DOM مسطحة تمتلك عنصرين ضمن الفتحة <"slot name="username>: <user-card> #shadow-root <div>Name: <slot name="username"> <span slot="username">John</span> <span slot="username">Smith</span> </slot> </div> <div>Birthday: <slot name="birthday"></slot> </div> </user-card> المحتوى الابتدائي للعنصر slot إذا وُضع محتوًى ما ضمن الفتحة <slot>، فسيصبح هو المحتوى الافتراضي حيث سيعرضه المتصفح إذا لم يوجد محتوىً مناسب ضمن شجرة DOM الظاهرة لملء الفتحة. عندها ستكون نتيجة تصيير هذا الجزء مثلًا من شجرة DOM الخفية هي Anonymous إذا لم توجد فتحة باسم "slot="username في شجرة DOM الظاهرة. <div>Name: <slot name="username">Anonymous</slot> </div> الفتحة الافتراضية هي أول فتحة بلا اسم ستكون أول فتحة <slot> في شجرة DOM الخفية غير مالكة لاسم هي الفتحة الافتراضية، وستحصل على كل العقد في شجرة DOM الظاهرة التي لم تُدرج في أي فتحة أخرى. لنضف في المثال التالي فتحةً افتراضيةً للعنصر المخصص <user-card> الذي سيخفي كل معلومات المستخدم غير المدرجة في فتحات: <script> customElements.define('user-card', class extends HTMLElement { connectedCallback() { this.attachShadow({mode: 'open'}); this.shadowRoot.innerHTML = ` <div>Name: <slot name="username"></slot> </div> <div>Birthday: <slot name="birthday"></slot> </div> <fieldset> <legend>Other information</legend> <slot></slot> </fieldset> `; } }); </script> <user-card> <div>I like to swim.</div> <span slot="username">John Smith</span> <span slot="birthday">01.01.2001</span> <div>...And play volleyball too!</div> </user-card> See the Pen JS-p3-06-Shadow-DOM -ex4 by Hsoub (@Hsoub) on CodePen. سيوضع محتوى شجرة DOM الظاهرة غير المُدرج في المثال السابق ضمن العنصر <fieldset> الذي يحمل تسمية Other information، حيث ستُدرج العناصر ضمن الفتحة واحدًا تلو الآخر، ولذا ستُدرج قطعتا المعلومات معًا ضمن الفتحة الافتراضية. وستبدو شجرة DOM المسطحة كالتالي: <user-card> #shadow-root <div>Name: <slot name="username"> <span slot="username">John Smith</span> </slot> </div> <div>Birthday: <slot name="birthday"> <span slot="birthday">01.01.2001</span> </slot> </div> <fieldset> <legend>Other information</legend> <slot> <div>I like to swim.</div> <div>...And play volleyball too!</div> </slot> </fieldset> </user-card> مثال القائمة لنعد الآن إلى العنصر المخصص الذي أشرنا إليه سابقًا في هذا المقال، حيث يمكن استخدام الفتحات لتوزيع العناصر، إليك ترميز HTML للعنصر المخصص <custom-menu>: <custom-menu> <span slot="title">Candy menu</span> <li slot="item">Lollipop</li> <li slot="item">Fruit Toast</li> <li slot="item">Cup Cake</li> </custom-menu> تمثل الشيفرة التالية قالب شجرة DOM الخفية مزوّدةً بالفتحات الملائمة: <template id="tmpl"> <style> /* menu styles */ </style> <div class="menu"> <slot name="title"></slot> <ul><slot name="item"></slot></ul> </div> </template> يُدرج <"span slot="title> ضمن <"slot name="title>. هناك العديد من عناصر القائمة <li slot="item"> في القالب، لكن توجد فتحة مناسبة <slot name="item"> واحدة ضمنه، لذلك ستُدرج كل عناصر القائمة -مثل <li slot="item">- ضمن الفتحة الواحد تلو الآخر، وهذا ما يُشكِّل القائمة. ستصبح شجرة DOM المسطّحة كالتالي: <custom-menu> #shadow-root <style> /* menu styles */ </style> <div class="menu"> <slot name="title"> <span slot="title">Candy menu</span> </slot> <ul> <slot name="item"> <li slot="item">Lollipop</li> <li slot="item">Fruit Toast</li> <li slot="item">Cup Cake</li> </slot> </ul> </div> </custom-menu> قد نُلاحظ تواجد العنصر <li> ضمن القائمة <ul> في شجرة DOM صالحة، لكن الشجرة في الشيفرة السابقة مسطحة تصف آلية تصيير المكوِّن، وهذا الأمر طبيعي، ويبقى علينا إضافة معالج للحدث click لفتح وإغلاق القائمة <custom-menu> لتصبح جاهزةً: customElements.define('custom-menu', class extends HTMLElement { connectedCallback() { this.attachShadow({mode: 'open'}); //هو قالب الشجرة الخفية في الأعلى this.shadowRoot.append( tmpl.content.cloneNode(true) ); // لا يمكن اختيار عقد من الشجرة الظاهرة، لذلك تُعالج النقرات ضمن الفتحة this.shadowRoot.querySelector('slot[name="title"]').onclick = () => { // فتح وإغلاق القائمة this.shadowRoot.querySelector('.menu').classList.toggle('closed'); }; } }); وستكون النتيجة: See the Pen JS-p3-06-Shadow-DOM -ex5 by Hsoub (@Hsoub) on CodePen. يمكن بالطبع تعزيز وظائف القائمة من خلال الأحداث والتوابع وغيرها. تحديث الفتحات ماذا لو أرادت الشيفرة الخارجية إضافة أو إزالة العناصر ديناميكيًا؟ يراقب المتصفح الفتحات، ثم تُحدَّث العناصر المصيّرة إذا تغيرت أو أزيلت العناصر المدرجة، وطالما ستُصيَّر العناصر الظاهرة فقط في الفتحات -ولن تُنسَخ- فستظهر أي تغييرات عليها مباشرةً، فلا حاجة لفعل أي شيء لتحديث نتيجة التصيير، لكن يمكن استخدام الحدث slotchange إذا أرادت شيفرة المكوّن مراقبة التغيرات في الفتحات. يُدرَج في المثال التالي عنصر قائمة ديناميكيًا بعد ثانية، ويتغير العنوان بعد ثانيتين: <custom-menu id="menu"> <span slot="title">Candy menu</span> </custom-menu> <script> customElements.define('custom-menu', class extends HTMLElement { connectedCallback() { this.attachShadow({mode: 'open'}); this.shadowRoot.innerHTML = `<div class="menu"> <slot name="title"></slot> <ul><slot name="item"></slot></ul> </div>`; //لا يمتلك الجذر الخفي معالجات أحداث لذا سنستخدم الابن الأول this.shadowRoot.firstElementChild.addEventListener('slotchange', e => alert("slotchange: " + e.target.name) ); } }); setTimeout(() => { menu.insertAdjacentHTML('beforeEnd', '<li slot="item">Lollipop</li>') }, 1000); setTimeout(() => { menu.querySelector('[slot="title"]').innerHTML = "New menu"; }, 2000); </script> See the Pen JS-p3-06-Shadow-DOM -ex6 by Hsoub (@Hsoub) on CodePen. يُحدَّث تصيير القائمة كل مرة دون تدخلنا، وسنلاحظ في الشيفرة السابقة وجود حدثي slotchange: عند التهيئة: حيث يقع الحدث slotchange: title مباشرةً عندما يُدرج "slot="title من شجرة DOM الظاهرة ضمن الفتحة الموافقة له. بعد ثانية: يقع الحدث slotchange: item عندما يُضاف عنصر قائمة جديد <"li slot="item>. لاحظ أنه لا وجود للحدث slotchange بعد ثانيتين عندما يُعدَّل محتوى "slot="title، والسبب هو عدم وجود أي تغييرات في الفتحة، حيث عدّلنا محتوى عنصر مُدرج في الفتحة، وهذا أمر مختلف. يمكن تتبع التغيرات الداخلية ضمن شجرة DOM الظاهرة باستخدام شيفرة JavaScript من خلال الآلية المعمّمة المعروفة بـ MutationObserver. الواجهة البرمجية للفتحات لنلق نظرةً على توابع JavaScript المتعلقة بالفتحات، إذ تنظر JavaScript كما رأينا سابقًا إلى شجرة DOM الحقيقية دون تسطيح flattening، لكن إذا كانت قيمة الخيار mode في الشجرة الخفية هي "open"، فسنتمكن من معرفة أي العناصر أدرِجت في فتحات، كما يمكننا معرفة الفتحة من العنصر الذي أدرج ضمنه: node.assignedSlot: تعيد الفتحة <slot> التي أسندت إليها عقدة من الشجرة node. ({slot.assignedNodes({flatten: true/false: يعيد عقد DOM التي أسندت إلى فتحة، وستكون قيمة الخيار flatten هي "false" افتراضيًا، لكن إذا ضبطت على القيمة "true" صراحةً، فسيُنظر إلى DOM المسطحة بعمق، كما سيعيد التابع الفتحات المتداخلة nested في حال وجود مكوّنات متداخلة، ويعيد "محتوى التراجع" إذا لم يجد أي عقد أسندت إلى فتحات. ({slot.assignedElements({flatten: true/false: يعيد عناصر DOM التي أسندت إلى فتحة، وهو يشابه التابع السابق إلا أنه يعيد عقد العنصر. تُستخدَم هذه التوابع عندما نريد إظهار محتوى الفتحات وتتبعها باستخدام JavaScript. وإذا أراد المكوّن <custom-menu> مثلًا معرفة ما الذي يعرضه، فيمكنه تعقّب الحدث slotchange والحصول على العناصر من خلال slot.assignedElements: <custom-menu id="menu"> <span slot="title">Candy menu</span> <li slot="item">Lollipop</li> <li slot="item">Fruit Toast</li> </custom-menu> <script> customElements.define('custom-menu', class extends HTMLElement { items = [] connectedCallback() { this.attachShadow({mode: 'open'}); this.shadowRoot.innerHTML = `<div class="menu"> <slot name="title"></slot> <ul><slot name="item"></slot></ul> </div>`; // يقع عندما يتغير محتوى الفتحة this.shadowRoot.firstElementChild.addEventListener('slotchange', e => { let slot = e.target; if (slot.name == 'item') { this.items = slot.assignedElements().map(elem => elem.textContent); alert("Items: " + this.items); } }); } }); // items update after 1 second setTimeout(() => { menu.insertAdjacentHTML('beforeEnd', '<li slot="item">Cup Cake</li>')}, 1000); </script> See the Pen Untitled by Hsoub (@Hsoub) on CodePen. تنسيق شجرة DOM الخفية يمكن أن تحتوي شجرة DOM الخفية على الوسمين <style> و<link rel="stylesheet" href="…">، وتُخزّن أوراق التنسيق في الوسم الأخير في ذاكرة HTTP المؤقتة، لذلك لن يُعاد تحميلها للمكونات المختلفة التي تستخدم نفس القالب، ويمكن أن نعد ما لي قاعدةً عامةً: يعمل التنسيق المحلي داخل الشجرة الخفية، بينما تعمل تنسيقات المستند ككل خارجها، مع وجود بعض الاستثناءات. المحدد host: يسمح هذا المحدد باختيار المضيف الخفي، أي العنصر الذي يحتوي الشجرة الخفية، لنفترض مثلًا أننا سننشئ عنصرًا مخصصًا <custom-dialog> يجب أن يتوسط الصفحة، لذلك لا بدّ من تنسيق المكوّن <custom-dialog> ذاته، وهذا ما يفعله host: تمامًا: <template id="tmpl"> <style> /* the style will be applied from inside to the custom-dialog element */ :host { position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); display: inline-block; border: 1px solid red; padding: 10px; } </style> <slot></slot> </template> <script> customElements.define('custom-dialog', class extends HTMLElement { connectedCallback() { this.attachShadow({mode: 'open'}).append(tmpl.content.cloneNode(true)); } }); </script> <custom-dialog> Hello! </custom-dialog> See the Pen JS-p3-06-Shadow-DOM-styling -ex1 by Hsoub (@Hsoub) on CodePen. توريث التنسيقات Cascading يبقى المضيف الخفي -<custom-dialog> ذاته- ضمن شجرة DOM الظاهرة، لذلك سيتأثر بقواعد CSS التي يخضع لها المستند كاملاً، فإذا وجدت خاصية جرى تنسيقها محليًا ضمن host: وفي المستند بنفس الوقت، فسيُطبق حينها تنسيق المستند. فإن كان المستند مثلًا: <style> custom-dialog { padding: 0; } </style> فسيُعرض العنصر <custom-dialog> دون حاشية padding. من الملائم أن نعّرف تنسيقًا افتراضيًا للمكوّن ضمن القاعدة host:، ثم نعيد التنسيق -إذا أردنا- باستخدام تنسيق المستند، ويبقى الاستثناء في الخاصية التي نعنونها important!،عندها يُطبق التنسيق المحلي. المحدد (host(selector: هو نفسه host: لكنه يُطبّق فقط إذا تطابق اسم المضيف الخفي مع الوسيط selector، فعلى سبيل المثال: نريد توسيط المكوّن <custom-dialog> في الصفحة فقط إذا امتلك السمة centered: <template id="tmpl"> <style> :host([centered]) { position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); border-color: blue; } :host { display: inline-block; border: 1px solid red; padding: 10px; } </style> <slot></slot> </template> <script> customElements.define('custom-dialog', class extends HTMLElement { connectedCallback() { this.attachShadow({mode: 'open'}).append(tmpl.content.cloneNode(true)); } }); </script> <custom-dialog centered> Centered! </custom-dialog> <custom-dialog> Not centered. </custom-dialog> See the Pen JS-p3-06-Shadow-DOM-styling -ex2 by Hsoub (@Hsoub) on CodePen. والآن سيُطبّق التوسيط فقط على المكوّن الأول <custom-dialog centered>. باختصار، يمكن استخدام عائلة المحددات host: لتنسيق العنصر الرئيسي للمكوِّن بناءً على السياق، ويمكن تغيير هذه التنسيقات -ما لم تستخدم السمة important!- من قبل تنسيق المستند. تنسيق محتوى الفتحات لننظر إلى حالة وجود فتحات slots. تأتي المكوّنات المقلمة slotted components من شجرة DOM الظاهرة، لذا ستستخدم تنسيقات المستند، ولن يؤثر التنسيق المحلي على المحتوى المركب. سنرى في المثال التالي أنّ العنصر المركب <span> سيظهر بخط سميك وفق تنسيق المستند، لكنه لن يتأثر بقيمة الخاصية background العائدة للتنسيق المحلي للعنصر: <style> span { font-weight: bold } </style> <user-card> <div slot="username"><span>John Smith</span></div> </user-card> <script> customElements.define('user-card', class extends HTMLElement { connectedCallback() { this.attachShadow({mode: 'open'}); this.shadowRoot.innerHTML = ` <style> span { background: red; } </style> Name: <slot name="username"></slot> `; } }); </script> See the Pen JS-p3-06-Shadow-DOM-styling -ex3 by Hsoub (@Hsoub) on CodePen. سيكون الخط سميكًا لكنه ليس باللون الأحمر. وإذا أردنا تنسيق العناصر المركبة في مكوننا فأمامنا خياران هما: الأول هو تنسيق الفتحة <slot> نفسه اعتمادًا على وراثة تنسيق CSS: <user-card> <div slot="username"><span>John Smith</span></div> </user-card> <script> customElements.define('user-card', class extends HTMLElement { connectedCallback() { this.attachShadow({mode: 'open'}); this.shadowRoot.innerHTML = ` <style> slot[name="username"] { font-weight: bold; } </style> Name: <slot name="username"></slot> `; } }); </script> See the Pen JS-p3-06-Shadow-DOM-styling -ex4 by Hsoub (@Hsoub) on CodePen. سيكون الخط سميكًا في <p>John Smith</p>، وذلك لأن وراثة CSS ستنقل التنسيق من الفتحة <slot> إلى المحتويات، لكن ليست كل خصائص CSS قابلة للوراثة. الخيار الثاني هو استخدام محدد الصنف الوهمي (slotted(selector::، والتي تطابق العناصر بناءً على شرطين: أن يكون العنصر مركبًا وآتيًا من الشجرة الظاهرة، ولا يهم اسم الفتحة هنا، ويطبق على العنصر المركب ذاته وليس على أبنائه. أن يطابق العنصر قيم الوسيط selector. يختار المحدد (slotted(div:: في مثالنا التالي العنصر <"div slot="username> تمامًا وليس أبناءه: <user-card> <div slot="username"> <div>John Smith</div> </div> </user-card> <script> customElements.define('user-card', class extends HTMLElement { connectedCallback() { this.attachShadow({mode: 'open'}); this.shadowRoot.innerHTML = ` <style> ::slotted(div) { border: 1px solid red; } </style> Name: <slot name="username"></slot> `; } }); </script> See the Pen JS-p3-06-Shadow-DOM-styling -ex5 by Hsoub (@Hsoub) on CodePen. لاحظ أنه يمكن للمحدد slotted:: التقدم داخل عناصر الفتحة، لذا تكون المحددات التالية غير صحيحة: ::slotted(div span) { /*مع هذا المحدد div لايتطابق العنصر */ } ::slotted(div) p { /* DOM لا يمكنه التقدم داخل */ } يمكن استخدام slotted:: أيضًا في querySelector. خطافات CSS مع خاصيات مخصصة كيف ننسق العناصر الداخلية للمكوِّن من المستند الرئيسي؟ تطبق المحددات -مثل host:- القواعد على عناصر مخصصة، مثل <custom-dialog> أو <user-card>، لكن كيف ننسق عناصر شجرة DOM الخفية ضمنها؟ لا يوجد محدد قادر على التأثير المباشر على تنسيق شجرة DOM الخفية من المستند الرئيسي، لكن -وكما عرضنا توابع للتفاعل مع مكوننا- يمكن عرض متغيرات CSS -خصائص CSS مخصصة- إلى التنسيق. تتواجد الخصائص المخصصة على كل المستويات وفي كلتا الشجرتين الخفية والظاهرة، حيث يمكن مثلًا استخدام المتغير --user-card-field-color في شجرة DOM الخفية لتنسيق الحقول، ويمكن أن يضبط المستند الخارجي قيمتها: <style> .field { color: var(--user-card-field-color, black); /* معرفًا استخدم اللون الأسود --user-card-field-color */ } </style> <div class="field">Name: <slot name="username"></slot></div> <div class="field">Birthday: <slot name="birthday"></slot></div> عندها يمكن التصريح عن هذه الخاصية في المستند الخارجي للمكوّن <user-card>: user-card { --user-card-field-color: green; } تخترق خاصيات CSS المخصصة شجرة DOM الخفية، وهي مرئية في كل مكان، لذلك يمكن للقاعدة الداخلية field. استخدامها، وإليك مثالًا كاملًا: <style> user-card { --user-card-field-color: green; } </style> <template id="tmpl"> <style> .field { color: var(--user-card-field-color, black); } </style> <div class="field">Name: <slot name="username"></slot></div> <div class="field">Birthday: <slot name="birthday"></slot></div> </template> <script> customElements.define('user-card', class extends HTMLElement { connectedCallback() { this.attachShadow({mode: 'open'}); this.shadowRoot.append(document.getElementById('tmpl').content.cloneNode(true)); } }); </script> <user-card> <span slot="username">John Smith</span> <span slot="birthday">01.01.2001</span> </user-card> See the Pen JS-p3-06-Shadow-DOM-styling -ex6 by Hsoub (@Hsoub) on CodePen. شجرة DOM الخفية والأحداث إنّ الفكرة الأساسية من الشجرة الخفية هي تغليف تفاصيل التنفيذ الداخلي للمكوّن. لنفترض وقوع حدث نقرٍ داخل شجرة DOM الخفية للمكوّن <user-card>، لكن لا تملك السكربتات في المستند الرئيسي أي فكرة عن ما يجري داخل الشجرة، وخاصةً إذا أتى المكون من طرف ثالث، لذا يعيد المتصفح استهداف الحدث للمحافظة على تغليف التفاصيل. تستهدف الأحداث التي تحدث في شجرة DOM الخفية العنصر المضيف عندما تُلتقط خارج إطار المكوِّن. وإليك مثالًا بسيطًا: <user-card></user-card> <script> customElements.define('user-card', class extends HTMLElement { connectedCallback() { this.attachShadow({mode: 'open'}); this.shadowRoot.innerHTML = `<p> <button>Click me</button> </p>`; this.shadowRoot.firstElementChild.onclick = e => alert("Inner target: " + e.target.tagName); } }); document.onclick = e => alert("Outer target: " + e.target.tagName); </script> See the Pen JS-p3-06-Shadow-DOM-and-events -ex1 by Hsoub (@Hsoub) on CodePen. إذا نقرت على الزر فستظهر الرسائل التالية: الهدف الداخلي BUTTON: معالج حدث داخلي يحصل على الهدف الصحيح، وهو عنصر داخل الشجرة الخفية. الهدف الخارجي USER-CARD: معالج حدث المستند، والذي يحصل على المضيف الخفي مثل هدف. إنّ إعادة استهداف retargeting الأحداث أمر مفيد جدًا، إذ يُفترض أن لا يعرف المستند الخارجي ما يوجد داخل المكوّن، فالحدث قد وقع من وجهة نظره على المكوّن <user-card>. لا تحدث إعادة الاستهداف إذا وقع الحدث في عنصر مُركّب، وذلك لأنه موجود فيزيائيًا في شجرة DOM الظاهرة، فلو نقر مستخدم مثلًا على <span slot="username"> في المثال التالي، فسيستهدف الحدث العنصر span هذا بالتحديد، وفي كلا معالجي الحدث الخاصين بالشجرتين الظاهرة والخفية: <user-card id="userCard"> <span slot="username">John Smith</span> </user-card> <script> customElements.define('user-card', class extends HTMLElement { connectedCallback() { this.attachShadow({mode: 'open'}); this.shadowRoot.innerHTML = `<div> <b>Name:</b> <slot name="username"></slot> </div>`; this.shadowRoot.firstElementChild.onclick = e => alert("Inner target: " + e.target.tagName); } }); userCard.onclick = e => alert(`Outer target: ${e.target.tagName}`); </script> See the Pen JS-p3-06-Shadow-DOM-and-events -ex2 by Hsoub (@Hsoub) on CodePen. فإذا نُقر الاسم "John Smith"، فسيكون الهدف بالنسبة لمعالجي الحدث الداخلي والخارجي هو <"span slot="username>، وهنا لن يُعاد الاستهداف لأنه عنصر من DOM الظاهرة. من ناحية أخرى، إذا نُقر عنصر يعود إلى شجرة DOM الخفية مثل العنصر <b>Name</b>، فسُيعاد ضبط هدف الحدث event.target أثناء انسياب الحدث لخارج شجرة DOM الخفية ليصبح <user-card>. التابعان Bubbling و()event.composedPath تُستخدم DOM المسطحة لدفع الأحداث إلى الخارج bubbling، لذا إذا وُجد عنصر مركب slotted في فتحة ووقع حدث في مكان ما بداخله، فسيندفع الحدث إلى خارج العنصر باتجاه الفتحة <slot> وإلى الأعلى، ويمكن الحصول على المسار الكامل للهدف الأصلي للحدث مع كل عناصر شجرته الخفية باستخدام التابع ()event.composedPath، ويؤخذ هذا المسار -كما يدل اسم التابع- بعد عملية التركيب، وإليك شجرة DOM المسطحة للمثال السابق: <user-card id="userCard"> #shadow-root <div> <b>Name:</b> <slot name="username"> <span slot="username">John Smith</span> </slot> </div> </user-card> عند النقر على <span slot="username">، سيعيد استدعاء التابع ()event.composedPath المصفوفة التالية: [span, slot, div, shadow-root, user-card, body, html, document, window]، وهي شجرة العناصر كاملةً ابتداءًا من العنصر المُستهدف في شجرة DOM المسطحة بعد التركيب. التابع event.composed تنساب معظم الأحداث إلى خارج حدود شجرة DOM الخفية، لكن هذا لا ينطبق على بعضها، وذلك بحسب قيمة كائن الحدث composed، فإذا كانت "true" فستعبر الأحداث حدود الشجرة، وإلا فستلتقَط داخل حدود الشجرة فقط. إذا ألقينا نظرةً على توصيفات أحداث واجهة المستخدم UI Events specification، فسنجد أن قيمة composed هي "true" للأحداث: blur وfocus وfocusin وfocusout. click وdblclick. mousedown وmouseup وmousemove وmouseout وmouseover. wheel. beforeinput وinput وkeydown وkeyup. جميع أحداث اللمس والتأشير. أما الأحداث التي يكون فيها composed: false فهي: mouseenter وmouseleave: التي لا تنساب للخارج إطلاقًا. load وunload وabort وerror. select. slotchange. يمكن التقاط هذه الأحداث من قبل العناصر داخل شجرة DOM نفسها، حيث يقيم العنصر المستهدف. الأحداث المخصصة لا بدّ من ضبط الخاصيتين bubbles وcomposed على القيمة "true" إذا أردنا إيفاد أحداث مخصصة تنساب إلى مستوى أعلى وإلى خارج المكوّن، وسننشئ في المثال التالي div#inner في شجرة DOM الخفية ضمن div#outer ونطلق حدثين فيها، وسيصل الحدث الذي يمتلك الخاصية composed: true فقط إلى الخارج، وصولًا إلى المستند: <div id="outer"></div> <script> outer.attachShadow({mode: 'open'}); let inner = document.createElement('div'); outer.shadowRoot.append(inner); /* div(id=outer) #shadow-dom div(id=inner) */ document.addEventListener('test', event => alert(event.detail)); inner.dispatchEvent(new CustomEvent('test', { bubbles: true, composed: true, detail: "composed" })); inner.dispatchEvent(new CustomEvent('test', { bubbles: true, composed: false, detail: "not composed" })); </script> خلاصة شجرة DOM الخفية هي طريقة لإنشاء شجرة محلية خاصة بالمكوِّن. ({shadowRoot = elem.attachShadow({mode: open|closed: ينشئ شجرة DOM خفيةً للعنصر elem، ويمكن الوصول إليها باستخدام الخاصية elem.shadowRoot إذا كانت الخاصية "mode="open. يمكن تعميم الكائن shadowRoot باستخدام innerHTML أو توابع DOM أخرى. عناصر شجرة DOM الخفية: لها فضاء خاص لقيم المعرّفات id. غير مرئية لمحددات JavaScript الموجودة في المستند الرئيسي، مثل querySelector. تستخدم التنسيق الموجود ضمن الشجرة الخفية فقط، وليس تنسيق المستند الرئيسي. تُصيّر شجرة DOM الخفية -إن وجدت- ضمن ما يُسمى شجرة DOM الظاهرة light DOM، مثل عناصر أبناء نظاميين. إذا امتلك عنصر شجرة DOM خفيةً، فلن تُعرض شجرة DOM الظاهرة عادةً، لذلك تسمح الفتحات slots -الناتجة عن عملية تقليم الشجرة- بعرض عناصر من شجرة DOM الظاهرة في أماكن محددة من شجرة DOM الخفية، ويوجد نوعان من الفتحات: الفتحات المسماة <slot name="X">...</slot>: الحصول على أبناء شجرة DOM الظاهرة فتحتها slot="X". الفتحة الافتراضية: وهي أول فتحة <slot> لا تحمل اسمًا، وتهمَل الفتحات غير المسماة التي تأتي لاحقًا، كما يحصل على أبناء شجرة DOM الظاهرة الذين لم يُدرجوا في فتحات. إذا وجدَت عدة عناصر في الفتحة نفسها، فستُدرج واحدًا بعد الآخر. يُستخدَم محتوى الفتحة <slot> محتوىً للتراجع، لذا يظهر هذا المحتوى عند عدم وجود أبناء لشجرة DOM الظاهرة في الفتحة. تُدعى عملية تصيير العناصر المدرجة في فتحات ضمن فتحاتها بالتركيب composition، وتسمى النتيجة شجرة DOM المسطّحة. لا تنقل عملية التركيب العقد في الواقع، فمن وجهة نظر JavaScript ستبقى شجرة DOM نفسها، ويمكن الوصول إلى الفتحات من خلال توابع JavaScript التالية: ()slot.assignedNodes/Elements: يعيد العقد أو العناصر داخل فتحة. node.assignedSlot: وهي الخاصية المعاكسة، وتعيد الفتحة من العقدة التي تشغلها. إذا أردنا معرفة ما نُظهره، فيمكننا تعقب محتوى الفتحة باستخدام: الحدث slotchange: ويقع في المرة الأولى التي تُملأ بها الفتحة، أو أثناء عمليات الإضافة أو الإزالة أو الاستبدال لعنصر مُدرج في الفتحة وليس لأبنائه، وستكون الفتحة event.target. MutationObserver للغوص عميقًا في محتوى الفتحة ومعرفة التغيرات التي حدثت ضمنها. بعد أن تعرفنا على كيفية إظهار عناصر من شجرة DOM الظاهرة، سنتعرف على كيفية تنسيقها بالشكل الصحيح، وتبقى القاعدة الأساسية هي أن عناصر شجرة DOM الخفية تُنسّق من الداخل، والظاهرة من الخارج -مع بعض الاستثناءات-، وهذا ما سنتعرف عليه في المقال القادم. يمكن أن تحوي شجرة DOM الخفية تنسيقات مثل <style> أو <"link rel="stylesheet>، وتؤثر التنسيقات المحلية في: الشجرة الخفية. المضيف الخفي عندما يستعمل عائلة المحددات host:. العناصر المركبة القادمة من شجرة DOM الظاهرة، حيث يسمح المحدد (slotted(selector:: باختيار العناصر المركبة ذاتها وليس أبنائها. بينما تؤثر تنسيقات المستند الرئيسي في: المضيف الخفي، إذا كان حيًّا في المستند الخارجي. العناصر المركبة ومحتوياتها، إذا كانت أيضًا في المستند الخارجي. سيُطبق تنسيق المستند الخارجي عندما يحدث تضارب في تنسيق العنصر، إلا عند استخدام الخاصية مع عنوان important!، حيث يُطبق التنسيق المحلي. تخترق خصائص CSS المخصصة شجرة DOM الخفية، وتستخدم مثل خطافات hooks لتنسيق المكوّن: يستخدم المكوِّن خصائص CSS المخصصة لتنسيق العناصر المفتاحية، مثل (<var(--component-name-title, <default value. ينشر مؤلف المكون هذه الخصائص للمطورين، وهي بأهمية توابع المكوّنات العامة. عندما لا يرغب المطور في قضاء وقت طويل على التنسيق، فسيسند الخاصية --component-name-title إلى المضيف الخفي أو الخاصية التي أشرنا إليها في الأعلى. الربح طبعًا! تتجاوز الأحداث حدود شجرة DOM الخفية إذا كانت قيمة الراية composedهي true، والتي تكون كذلك لمعظم الأحداث المضمنة Built-in، كما هو محدد في التوصيفات: أحداث واجهة المستخدم. أحداث اللمس. أحداث التأشير. من الأحداث المضمنة التي يكون فيها composed: false: mouseenter وmouseleave: لا تنساب للخارج إطلاقًا. load وunload وabort وerror. select. slotchange. يمكن التقاط هذه الأحداث من قِبل العناصر داخل شجرة DOM نفسها. إذا أردنا إيفاد أحداث مخصصة CustomEvent فلا بد من ضبط الخاصية composed على القيمة "true" بشكل صريح. المراجع مراجع DOM. مراجع التوافقية Compatibility ذكرت الشجرة الخفية في الكثير من المواصفات مثل DOM Parsing. ترجمة -وبتصرف- للفصول Shadow DOM and events و Shadow DOM slots, Composition وShadow DOM وShadow DOM Style من سلسلة The Modern JavaScript Tutorial. اقرأ أيضًا إنشاء الحركات عبر جافاسكربت التنقل في شجرة DOM عبر جافاسكربت تعديل الخاصيات والأصناف والأنماط في شجرة DOM إجراء تعديلات على DOM طريقة الوصول للعناصر في DOM
- 1 تعليق
-
- dom
- shadow dom
-
(و 1 أكثر)
موسوم في:
-
يشرح هذا الجزء من سلسلة تعليم CSS كيف تعمل CSS في المتصفّحات وما الهدف من وجود DOM. سنتعلّم أيضًا كيف نُحلّل مستندًا بسيطًا. فهرس السلسلة: مدخل إلى أوراق الأنماط المتتالية (CSS). آلية عمل تعليمات CSS داخل المتصفحات. (هذا الدرس) المحددات (Selectors) في CSS. كيفية كتابة تعليمات CSS يسهل قراءتها. تنسيق نصوص صفحات الويب باستخدام CSS. التعامل مع الألوان في CSS. إضافة محتوى إلى صفحة ويب باستخدام CSS. تنسيق القوائم (Lists) في CSS. تعرف على الصناديق (Boxes) في CSS. رصف العناصر (Layout) في CSS. الجداول (Tables) في CSS. التعامل مع أجهزة العرض المختلفة والمطبوعات في CSS. كيفية عمل CSS عندما يعرض المتصفّح المستند، فإنّه يجمع محتواه مع معلومات التنسيق، ويُعالج المستند على مرحلتين: يحوّل المتصفّح لغة الرّماز وCSS إلى DOM (اختصارًا لـDocument Object Model). يُمثّل DOM المستند في ذاكرة الحاسوب، ويجمع محتوى المستند مع تنسيقه. يعرض المتصفّح محتويات DOM. تستخدم لغات الرّماز عناصر (elements) لتعريف هيكل المستند. يُحدّد العنصر بوسم (tag)، وهو نص يبدأ بالرّمز < وينتهي بالرّمز >. معظم العناصر لها زوجان من الوسوم، وسم في البداية وآخر في النهاية. لكتابة وسم البداية أدرج اسم العنصر بين <>، أما وسم النّهاية فيكتب بإضافة / بعد < وقبل اسم العنصر. قد يكون لبعض العناصر وسم بادئ فقط، أو وسم وحيد فيه تأتي / بعد اسم العنصر، وذلك تبعًا للغة الرّماز. قد يكون العنصر أيضًا حاويًا على عناصر آخرى ضمن وسْمَيه الأول والأخير. عليك التأكد من إغلاق الوسوم ضمن العنصر الحاوي. لـDOM بنية تشبه الشجرة، حيث يصبح كل عنصر أو خاصّة أو سلسلة من النصوص في لغة الرماز عقدة (node) في البنية الشجريّة. تُحدّد العُقد بعلاقتها بالعقد الأخرى، فبعض العناصر هي آباء (parent nodes) لعناصر آخرى (children nodes)، وقد يكون للأبناء إخوة (sibling nodes). يُعينك فهم DOM على تصميم وتنقيح وصيانة CSS، لأنّه يمثّل نقطة التقاء CSS مع محتوى المستند. مثال يُنشئ الوسم <p> ونظيره </p> عنصرًا حاويًا: <p> <strong>C</strong>ascading <strong>S</strong>tyle <strong>S</strong>heets </p> في هيكل DOM، تُعتير العقدة P أبًا، أبناؤه هي العقد STRONG والعقد النصيّة، وبالمثل فإنّ العقد STRONG آباء للعقد النّصيّة. ├─STRONG │ └─"C" ├─"ascading" ├─STRONG │ └─"S" ├─"tyle" ├─STRONG │ └─"S" └─"heets" تدريب: تحليل DOM باستخدام أداة فحص DOM لتحليل هيكل DOM، نحتاج إلى برنامج خاصّ، بإمكانك استخدام إضافة DOM Inspector من Mozilla لتحقيق ذلك. ما عليك إلا تثبيت الإضافة (المزيد من التفاصيل أدناه). استخدم متصفح Mozilla لفتح مستند HTML الذي أنشأته. من قائمة المتصفّح اختر: أدوات > DOM Inspector أو أدوات > تطوير الويب > DOM Inspector. تفاصيل أكثر إن لم يحوِ متصفح Mozilla عندك على أداة فحص DOM، فبإمكانك تثبيت الإضافة من موقع الإضافات وإعادة تشغيل المتصفح ثم العودة لهذا الدّرس. إن لم ترغب بتثبيت هذه الإضافة (أو لم تكن تستعمل متصفح من Mozilla)، بإمكانك استخدام Web X-Ray Goggles كما هو مشروح في القسم التالي. بإمكانك أيضًا تجاوز هذا القسم والمتابعة. في أداة فحص DOM، وسّع عقد المستند بالنقر على الأسهم. ملاحظة: قد تؤدي المسافات في ملف HTML إلى عرض عقد نصية فارغة، يمكنك تجاهلها. يبدو جزء من النتيجة مشابهًا لما يلي، تبعًا للعقد الّتي وسّعتها: │ ▼╴P │ │ │ ▼╴STRONG │ │ └#text │ ├╴#text │ ►╴STRONG │ │ عندما تختار أيّة عقدة، بإمكانك استخدام اللوحة على يمين DOM Inspector للاطّلاع على معلومات إضافية عنها، فمثلًا: يعرض DOM Inspector النص في اللوحة على اليمين عندما تحتار عقدة نصيّة. عندما تختار عقدة تمثّل عنصرًا، تُحلّل الأداة هذه العقدة وتقدّم معلومات كثيرة في اللّوحة على اليمين، يكون من ضمنها معلومات التنسيق. تمرين انقر على عقدة من نوع STRONG في أداة فحص DOM، ثم استخدم اللوحة على اليمين لإيجاد الموضع الّذي يُعيّن فيه لون العقدة إلى أحمر، والموضع الّذي يُجعل فيه خطّها أعرض من النصّ العاديّ. شاهد الحل في القائمة فوق اللوحة على اليمين، اختر CSS Rules، سترى عنصرين مُدرجين، أحدهما هو المصدر الداخلي الّذي ي يُعرّف font-width على أنّها bolder، والآخر آتٍ من ورقة الأنماط الّتي تُحرّرها والّذي يُعرّف color على أنّه red. استخدام Web X-Ray Goggles تعرض الأداة Web X-Ray Goggles معلومات أقل من DOM Inspector، ولكنّها أبسط في التثبيت والاستعمال: اذهب إلى الصّفحة الرئيسيّة للأداة. اسحب رابط العلامة المرجعيّة إلى شريط المتصفّح. افتح مستند HTML الّذي أنشأته. فعّل الأداة بالنقر على العلامة المرجعيّة في الشّريط. اسحب مؤشّر الفأرة فوق العناصر المختلفة في المستند. تتالي الأنماط ووراثتها سنتعلّم الآن كيف تتفاعل الأنماط المتتالية، وكيف ترث العناصر الأنماط عن آبائها، وكيف تضيف الأنماط إلى ورقة الأنماط مستفيدًا من فكرة الوراثة بحيث تغيّر تنسيق عدة أجزاء من المستند مرّة واحدة. قد يُحدّد النّمط النّهائي للعنصر في عدّة مواضع، تتفاعل فيما بينها بطريقة مُعقّدة، وهذا التّفاعل المعقّد يُعطي CSS قدراتها المُميّزة، ولكنّه قد يُعقّد الأمور ويجعل تتبّع الأخطاء عمليّة صعبة. تجتمع المصادر الثلاثة الأساسية لمعلومات التنسيق مشكّلة ما نسمّيه تعاقب الأنماط (cascade)، وهذه الأنماط هي: التنسيقات المبدئيّة الّتي يحدّدها المتصفّح للعناصر في لغة الرّماز. التنسيقات الّتي يحدّدها المستخدم الّذي يقرأ المستند. التنسيقات المرتبطة بالمستند الّتي يحدّدها مؤلّفه، والّتي قد تعيّن في ثلاثة مواضع: في ملفّ خارجيّ: وهذا الموضوع الأساسيّ لهذه الدّروس. في تعريف في بداية المستند: للتنسيقات الخاصّة بصفحة واحدة فقط. في عنصر معيّن ضمن متن المستند: وهي الطريقة الأقل سهولة في الصيانة، ويمكن استخدامها على سبيل التّجربة. تُغيّر أنماط المستخدم الأنماط الّتي يعيّنها المتصفّح، ثمّ تغيّر أنماط مؤلّف المستند تلك الّتي عيّنها المستخدم. في هذه السّلسلة أنت مؤلّف المستند، ولن نتعامل إلّا مع هذا النّوع من الأنماط. مثال عندما تقرأ هذا المستند في متصفّح، يأتي جزء من التنسيق الّذي تراه من الإعدادات المبدئيّة لـHTML في المتصفّح. وقد يأتي جزء آخر من الإعدادات المخصّصة للمتصفّح، إذ يمكن تخصيصها في مربع التفضيلات في Firefox مثلًا، أو يمكن تعيينها في ملف userContent.css في ملفّ المتصفّح الشّخصيّ. ويأتي الجزء الأخير من التنسيق من خادوم موقع الأكاديميّة. عندما تفتح المستند الّذي تتدرّب عليه في المتصفّح، تكون العناصر <strong> أعرض خطًّا من بقيّة النّصّ، هذا التنسيق يأتي من إعدادات المتصفّح المبدئيّة. كما أن العناصر نفسها ذات لون أحمر، وهذا التنسيق يأتي من ورقة الأنماط الّتي كتبناها بأنفسنا. ترث عناصر <strong> أيضًا معظم تنسيقها عن عناصر <p> لأنّها أبناؤها، وبالمثل ترث العناصر <p> معظم تنسيق عنصر <body>. يكون للتنسيقات الّتي يحدّدها مؤلّف المستند الأولويّة في تعاقب الأنماط على تلك الّتي يحدّدها القارئ، وتكون الأولويّة الأقل لتنسيقات المتصفّح المبدئيّة. بالنّسبة للتنسيقات الموروثة، يكون للتنسيقات المعرّفة على العقدة ذاتها الأولويّة على تلك الّتي ترثها عن آبائها. هناك قواعد أخرى لهذه الأولويّات، سنتعرّف عليها لاحقًا. تفاصيل أكثر توفّر CSS أيضًا طريقة للقارئ لحجب التنسيق الّذي يحدّده مؤلّف المستند باستخدام الكلمة !important. وهذا يعني أنّه لا يمكن توقّع الشّكل الّذي سيكون عليه المستند لكلّ قارئ. إذا أردت معرفة المزيد عن موضوع التعاقب والوراثة، انظر فقرة Assigning property values, Cascading and inheritance في تعريف CSS. تمرين: استخدام الوراثة حرّر ملفّ CSS الّذي تتدرّب عليه. أضف هذا السّطر بنسخه ولصقه. لا يهمّ إن كان فوق أو تحت السّطر الّذي أضفته من قبل. إلّا أن إضافته فوقه أكثر منطقيّة لأن العنصر <p> أب للعنصر <strong> في مستندك. p {color: blue; text-decoration: underline;} احفظ الملفّ وحدّث متصفّحك لتشاهد تأثير ذلك على المستند. يؤدّي ذلك إلى تسطير النّص في كامل الفقرة، بما في ذلك الحروف الأولى من كلّ كلمة، وهذا يعني أن العناصر <strong> ورثت عن أبيها نمط تسطير النّصّ. ولكّن هذه العناصر ما تزال حمراء اللّون، لأنّ اللون الأحمر هو نمطها الخاص، ولذا يكون له الأولويّة على اللّون الأزرق لأبيها العنصر <p>. قبل <p> <strong>C</strong>ascading <strong>S</strong>tyle <strong>S</strong>heets </p> strong {color:red} بعد <p> <strong>C</strong>ascading <strong>S</strong>tyle <strong>S</strong>heets </p> p {color:blue; text-decoration:underline} strong {color:red} تمرين عدّل ورقة الأنماط بحيث تكون الحروف الأولى فقط مُسطَّرة: شاهد الحل انقل التصريح الذي يُعنى بتسطير النّص من قاعدة <p> إلى <strong>، كما يلي: p {color: blue; } strong {color: orange; text-decoration: underline;} ما التالي؟ سنتعرّف في الدّرس التالي كيف نستهدف العناصر بطريقة أكثر انتقائيّة. ترجمة -وبتصرف- للمقال How CSS works من سلسلة Getting started with CSS على شبكة مطوّري Mozilla.
-
توفّر jQuery أدوات قويّة لإيجاد العنصر أو العناصر التي تريدها في الصّفحة، ثمّ العمل بهذه العناصر للوصول إلى النّتيجة المرغوبة. تسهّل jQuery بأدواتها هذه عمليّات كانت لتكون أكثر تعقيدًا لو أردنا تنفيذها من خلال وظائف DOM الأصليّة. سنطّلع في هذا الجزء على بعض (لا كلّ) وظائف الانتقال عبر الصّفحة وتّعديل العناصر في jQuery. وقبل أن نبدأ، علينا فهم بعض المصطلحات الضّروريّة. لنفترض أنّ لدينا نصّ HTML التّالي: <ul> <li> <span> <i>Foo</i> </span> </li> <li>Bar</li> </ul> نقول عن عنصر القائمة الأوّل (<li>) أنّه ابن (child) القائمة غير المرتّبة (<ul>). نقول عن القائمة غير المرتّبة (<ul>) أنّها والد (parent) عنصري القائمة الاثنين. نقول عن العنصر <span> أنّه خَلَفُ (descendant) القائمة غير المرتّبة. نقول عن القائمة غير المرتّبة أنّها سَلَفٌ (ancestor) لكلّ ما داخلها. نقول عن عنصري القائمة أنّهما شقيقان (siblings). الانتقال عبر الصّفحة (Traversal) تسمح jQuery لنا بالانتقال عبر عناصر HTML الّتي تكوّن صفحتنا. إذ نُنشئ أوّلًا تحديدًا مبدئيًّا ثمّ ننتقل عبر DOM انطلاقًا منه. وخلال مسيرنا عبر DOM، فإنّنا نُغيّر من تحديدنا الأوّل فنضيف إليه أو نحذف منه بعض العناصر، أو نستبدل به تحديدًا آخر بالكامل في بعض الأحيان. تصفية التّحديدات بإمكانك تصفية تحديد موجودٍ بحيث يتضمّن فقط العناصر الّتي تطابق معاييرَ مُحدّدة. بإمكانك مثلًا إجراء التّصفية بإحدى الطّرق التّالية: var listItems = $( 'li' ); // صفِّ التّحديد ليحوي فقط العناصر ذات الصّنف 'special' var special = listItems.filter( '.special' ); // صفّ التّحديد ليحوي فقط العناصر من غير الصّنف 'special' var notSpecial = listItems.not( '.special' ); // صفّ التّحديد ليحوي فقط العناصر الّتي تتضمّن span var hasSpans = listItems.has( 'span' ); من المهمّ أن تعرف أن الوظيفة .not() ليست عكس .is()، لأنّ .is() تُعيد قيمة منطقيّة (true أو false)، بينما تُعيد .not() كائن jQuery جديدًا. إيجاد العناصر انطلاقًا من تحديد يمكن الاستفادة من تحديد أوّليّ كأساس لإنشاء تحديدات إضافيّة؛ فإذا كان لديك تحديدٌ يحوي عنصر قائمة مُفردًا مثلًا، وأردت التّعامل مع "أشقّائه" أو مع القائمة غير المُرتّبة الّتي تحويه، فبإمكانك إنشاء تحديد جديد انطلاقًا من التّحديد الموجود بسهولة: // اختر أوّل عنصر قائمة في الصّفحة var listItem = $( 'li' ).first(); // أيضًا: .last() // اختر أشقّاء عنصر القائمة var siblings = listItem.siblings(); // اختر الشّقيق التّالي لعنصر القائمة var nextSibling = listItem.next(); // أيضًا: .prev() // اختر والد عنصر القائمة var list = listItem.parent(); // اختر عناصر القائمة الّتي تنحدر مباشرةً من القائمة var listItems = list.children(); // اختر كلّ عناصر القائمة ضمن القائمة، بما في ذلك العناصر الفرعيّة var allListItems = list.find( 'li' ); // اختر كل أسلاف عنصر القائمة ذوي الصّنف "module" var modules = listItem.parents( '.module' ); // اختر أقرب سلفٍ لعنصر القائمة له الصّنف "module" var module = listItem.closest( '.module' ); بإمكانك كذلك الإضافة على التّحديد الحاليّ باستخدام الوظيفة .add()، الّتي تقبل مُحدِّدًا أو مصفوفة عناصر أو نص HTML أو كائن jQuery. var list = $( '#my-unordered-list' ); // افعل شيئًا ما بالقائمة ثم ... var listAndListItems = list.add( '#my-unordered-list li' ); العودة إلى التّحديد الأصليّ تحتفظ jQuery بإشارة إلى تحديد الأصليّ عندما تستخدمه للانتقال إلى تحديدات أخرى انطلاقًا منه، في حال أردت العودة إلى التّحديد الأصليّ. افترض مثلًا أنّك حدّدت قائمة غير مرتّبه، ثمّ أردت التّعديل على عناصر القائمة، ثمّ العودة مجدّدًا للعمل على القائمة غير المرتّبة، عندها بإمكانك استخدام الوظيفة .end() للرّجوع إلى التّحديد الأصليّ: $( '#my-unordered-list' ) .find('li') // نحن الآن نعمل على عناصر القائمة .addClass('special') .end() // عدنا الآن للعمل على القائمة ذاتها .addClass('super-special'); تُسهِّل الوظيفة .end() إجراء تعديلات كثيرة في جملة واحدة، إلّا أنّ هذا الأسلوب لا يُلقي بالًا لوضوح النّصّ البرمجيّ، فهو أشبه بأن تحكي قصّة دون أن تلتقط أنفاسك. لهذا السّبب لا أنصحك بالإكثار من استعماله، فهو يؤدّي في معظم الحالات إلى جعل قراءة النّصّ البرمجيّ وصيانته وتنقيحه أكثر صعوبة. فيما يلي حلّ أفضل للمشكلة ذاتها: var list = $( '#my-unordered-list' ); var listItems = list.find('li'); listItems.addClass( 'special' ); list.addClass( 'super-special' ); توفّر jQuery أيضًا الوظيفة .addBack() إن أردت إضافة تحديدك الأصليّ إلى التّحديد الحاليّ. مثال: $( 'li.special' ) .siblings() // نحن نعمل الآن على أشقّاء التّحديد السّابقة .removeClass( 'important' ) .addBack() // الآن نعمل على عناصر القائمة الأصليّة وأشقائها **معًا** .addClass( 'urgent' ); هل اختلط عليك الأمر؟ الوظيفة .addBack() تشبه الوظيفة .end() في عيوبها، فكلاهما (وإن كان لهما استخدامها) يزيدان تعقيد النّصّ البرمجيّة. الحلّ الأفضل هو استخدام الوظيفة .add() لدمج التّحديدين الأصليين معًا: var specialListItems = $( 'li.special' ); var otherListItems = specialListItems.siblings(); otherListItems.removeClass( 'important' ); specialListItems.add( otherListItems ).addClass( 'urgent' ); هناك وظائف عديدة لم نتطرّق إليها هنا، يمكنك الاطّلاع عليها في وثائق الانتقال عبر الصّفحة. التّعامل مع العناصر (Manipulation) تسمح وظائف التّعامل مع العناصر في jQuery بتغيير DOM الصّفحة بصياغة أكثر بساطة من تلك الّتي توفّرها وظائف DOM الخام. تُعيد وظائف التّعامل مع العناصر في jQuery كائن jQuery الّتي استدعيت للعمل عليه، وهذا يعني إمكانيّة ربطها في سلسلة أو دمجها مع وظائف jQuery أخرى كالّتي ناقشناها في الفقرات السّابقة. تعديل العناصر كثيرةٌ هي طرق تعديل العناصر في jQuery. سنطّلع فيما يلي على طرق إنجاز المهام الأكثر شيوعًا. إضافة أو حذف الأصناف (classes) يمكن الاستفادة من أصناف الكائنات في HTML بأن نستهدفها في CSS بغرض تنسيقها، كما يُستفاد منها في إنشاء تحديدات jQuery. فمثلًا يمكن لعنصر في الصّفحة أن يقع تحت الصّنف hidden، والّذي يُستخدم في CSS لجعل خاصّة display موافقة للقيمة none للعناصر من هذا الصّنف، ثمّ يمكن حذف هذا الصّنف أو إضافته لتغيير حالة ظهور العناصر الموافقة في jQuery: $( 'li' ).addClass( 'hidden' ); $( 'li' ).eq( 1 ).removeClass( 'hidden' ); جرّب المثال في ساحة التّجربة (تأكد من ضغط زر Run with JS في هذا المثال وكلّ الأمثلة التالية) إن تطلّبت حالتك إضافة صنفٍ أو حذفه مرارًا، فبإمكانك استخدام الوظيفة .toggleClass() الّتي تُبدّل حالة الصّنف على العنصر، فتضيفه إن لم يكن موجودًا أو تحذفه إن وُجد: $( 'li' ).eq( 1 ).toggleClass( 'hidden' ); جرّب المثال في ساحة التّجربة تغيير المظهر ملاحظة: يُفضّل دومًا استخدام الأصناف واستهدافها بقواعد CSS لتغيير طريقة عرض العناصر، والاقتصار على استخدام jQuery في إضافة هذه الأصناف أو حذفها كما ورد للتوّ. في هذه الفقرة سنتعرّف كيف نُغيّر مظاهر العناصر مُباشرةً في jQuery، ولكننا نُفضِّل دومًا الأسلوب الأوّل إن كان يُحقِّق النّتائج ذاتها. عندما تعجز عن تحقيق هدفك بإضافة الأصناف أو حذفها، فإنّ jQuery تقدّم الوظيفة .css() الّتي تسمح بتعيين مظهر العناصر مباشرةً، ولعلّ هذا يكون ضروريًّا عادةً عندما تحتاج إلى إسناد قيم عدديّة لا يمكن حسابها إلّا أثناء عمل التّطبيق، كمعلومات توضّع العناصر في الصّفحة. لا يُفضَّل استخدام الوظيفة .css() لإجراء تنسيقات بسيطة مثل display: none، بل يُفضَّل في معظم الحالات إنجاز الغاية ذاتها باستخدام الأصناف وCSS. افترض مثلًا أنّنا نريد تعيين مظهر العنصر بالاعتماد على عرض والده، وربّما يصعب أو يستحيل معرفة عرض الوالد مُسبقًا عند اعتماد تخطيط مرنٍ للصّفحة. في هذه الحالة قد نلجأ إلى الوظيفة .css() لتنسيق العنصر: var list = $( '#my-unordered-list' ); var width = Math.floor( list.width() * 0.1 ); list.find('li').each(function( index, elem ) { var padding = width * index; $( elem ).css( 'padding-right', padding + 'px' ); }); جرّب المثال في ساحة التّجربة إن احتجت إلى تعيين أكثر من خاصّة في وقت واحدٍ، مرّر كائنًا إلى الوظيفة .css() بدلًا من اسم الخاصّة وقيمتها. لاحظ أنّ عليك إحاطة أيّة خاصّة تحوي الرّمز "-" بعلامتي اقتباس: $( 'li' ).eq( 1 ).css({ 'font-size': '20px', 'padding-right': '20px' }); جرّب المثال في ساحة التّجربة تغيير قيم النّماذج (forms) تقدّم jQuery الوظيفة .val() لتعديل قيمة العناصر في النّماج مثل input وselect. بإمكانك تمرير سلسلة نصّيّة لتعيين محتوى حقول input النّصّيّة: $( 'input[type="text"]' ).val( 'new value' ); جرّب المثال في ساحة التّجربة بالنّسبة للعناصر من نوع select، بإمكانك تعيين الخيار المُختار باستخدام .val() أيضًا: $( 'select' ).val( '2' ); جرّب المثال في ساحة التّجربة أمّا لحقول input من نوع checkbox، فعليك تعيين الخاصّة checked على العنصر بالوظيفة .prop(). $( 'input[type="checkbox"]' ).prop( 'checked', 'checked' ); جرّب المثال في ساحة التّجربة ملاحظة: أُضيفت الوظيفة .prop() في الإصدارة 1.6 من jQuery؛ وقبل ذلك كانت تُستخدم الوظيفة .attr() للغرض ذاته، وهي ما تزال تعمل في الإصدارات الحديثة من jQuery، ولكنّها في حالة checked تكتفي باستدعاء .prop(). إن كنت تستخدم إصدارةً أحدث من 1.6، فأنصحك باستخدام .prop() دومًا لتعيين الخاصّة checked وخصائص عناصر DOM الأخرى. اطّلع على الوثائق لتفاصيل أكثر. تغيير الصّفات (attributes) الأخرى بإمكانك استخدام وظيفة .attr() لتغيير صفات العناصر، فيمكنك مثلًا تغيير عنوان رابط (الخاصّة title لعنصر <a>): $( 'a' ).attr( 'href', 'new title'); عند تعيين قيمة لصفة، بإمكانك تمرير دالّة في موضع المُعامل الثّاني للوظيفة، ومثلها مثل كلّ دوالّ الكتابة السّابقة، تتلقّى هذه الدّالّة مُعاملين اثنين: دليل العنصر الّذي تعمل عليه، والقيمة الحاليّة للصّفة. يجب أن تُعيد هذه الدّالة القيمة الجديدة للصّفة: $( 'a' ).attr( 'href', function(index, value) { return value + '?special=true'; }); جرّب المثال في ساحة التّجربة بإمكانك حذف الصّفات أيضًا، وذلك باستخدام .removeAttr(). الحصول على معلومات من العناصر ناقشنا في الجزء السّابق (أساسيّات jQuery) فكرة وظائف القراءة والكتابة. كلّ الوظائف الّتي يمكن استخدامها لتغيير العناصر، يمكن أيضًا استخدامها لقراءة معلومات من تلك العناصر. فيمكن مثلًا استخدام الوظيفة .val() الّتي وصفناها أعلاه كوظيفة قراءة وكتابة معًا: var input = $( 'input[type="text"]' ); input.val( 'new value' ); input.val(); // returns 'new value' وكذلك الأمر بالنّسبة للوظيفة .css()، إذ يمكن استخدامها لقراءة قيمة خصائص CSS مُفردة بإمرار اسم الخاصّة فقط دون قيمة: var listItemColor = $( 'li' ).css( 'color' ); عندما تُستخدم وظائف التّعامل مع العناصر للقراءة، فإنّها تعمل فقط مع العنصر الأول في التّحديد، باستثناء الوظيفة .text() الّتي تقرأ المحتوى النّصيّ لكلّ العناصر المُحدّدة إن لم يُمرّر معامل إليها. إضافة العناصر إلى الصّفحة سواءٌ حدّدت عنصرًا أو أنشأت واحدًا جديدًا، فبإمكانك إضافة هذا العنصر إلى الصّفحة يمكن فعل ذلك بطريقتين: باستدعاء وظيفة تتبع للعنصر (أو العناصر) المطلوب إضافته، أو باستدعاء وظيفة تتبع لعنصر مرتبط بذلك الّذي تريد إضافته. افترض مثلًا أنّك تريد نقل عنصر في قائمة من رأسها إلى ذيلها، هناك عدّة طرق لفعل ذلك. بإمكانك مثلًا إضافة العنصر إلى القائمة باستدعاء الوظيفة .appendTo() على عنصر القائمة ذاته: var listItem = $( '#my-unordered-list li' ).first(); listItem.appendTo( '#my-unordered-list' ); جرّب المثال في ساحة التّجربة وبإمكانك أيضًا إضافة العنصر باستدعاء .append() على القائمة: var listItem = $( '#my-unordered-list li' ).first(); $( '#my-unordered-list' ).append( listItem ); أو إضافته باستدعاء .insertAfter() على العنصر المُراد نقله مُمرّرًا العنصر الأخير في القائمة إلى الوظيفة: var listItems = $( '#my-unordered-list li' ); listItems.first().insertAfter( listItems.last() ); جرّب المثال في ساحة التّجربة أو إضافته باستدعاء .after() على العنصر الأخير في القائمة مُمرًّرا العنصر الأولى في القائمة إلى الوظيفة: var listItems = $( '#my-unordered-list li' ); listItems.last().after( listItems.first() ); جرّب المثال في ساحة التّجربة هناك طرق آخرى كثير لإضافة العناصر، فبإمكانك إضافتها حول عناصر أخرى أو داخلها أو خارجها بحسب حاجتك. تعتمد أكثر الطّرق كفاءة في إضافة عنصر إلى الصّفحة على العناصر الّتي تتوفّر بين يديك بالفعل. فقد ترغب في إضافة العنصر إلى القائمة غير المُرتّبة في المثال السّابق إن كنت قد حدّدت القائمة غير المرتّبة من قبل لغرض آخر؛ أو إن كنت قد حدّدت عناصر القائجة جميعها، فقد يكون إضافة العنصر الأول بعد العنصر الأخير أمرًا أسهل. عندما تختار الطّريقة المناسبة لإضافة العنصر، فلا تكتفِ بالنّظر في سهولة الطّريقة، بل فكّر في إمكانيّة صيانتها لاحقًا. تجنّب الطّرق الّتي تعتمد على افتراض بنية مُحدِّدة بدقَّة لصفحتك، فقد تقرّر تغيير هذه البنية فيما بعد. نسخ العناصر بإمكانك إنشاء نسخة من عنصر أو مجموعة عناصر باستخدام الوظيفة .clone() في jQuery، وستُنشئ النُّسخة في الذّاكرة دون أن تُدرج في الصّفحة، فعليك فعل ذلك بنفسك إن أردته. بإمكانك تعديل العناصر المنسوخة قبل إضافتها: var clones = $( 'li' ).clone(); clones.html(function( index, oldHtml ) { return oldHtml + '!!!'; }); $( '#my-unordered-list' ).append( clones ); جرّب المثال في ساحة التّجربة ملاحظة*: لن تمنعك jQuery من نسخ عنصر ذي مُعرِّف (ID)، ولكن عليك التأكّد من حذف المُعرِّف أو تغييره في العنصر المنسوخ بتعديل الصّفة id قبل إدراجه في المستند، إذ لا ينبغي أ يوجد عنصران بمُعرِّف واحدٍ في الصّفحة. حذف العناصر هناك ثلاث طرق لحذف العناصر من الصّفحة: .remove() و.detach() و.replaceWith()، ولكلّ منها غرضٌ مُختلف. يجب استخدام .remove() عند الحاجة لحذف العناصر بصورة دائمة، فهي ستحذف مع العنصر كلّ مُتوليّات الأحداث المُرتبطة به (event handlers). تُعيد الوظيفة .remove() إشارةً إلى العناصر المُحذوفة، ولكن عند إضافة هذه العناصر مرّة ثانيةً، فلن تكون أيّة أحداث مُرتبطةً بها. $( '#my-unordered-list li' ).click(function() { alert( $( this ).text() ); }); var removedListItem = $( '#my-unordered-list li' ).first().remove(); removedListItem.appendTo( '#my-unordered-list' ); removedListItem.trigger( 'click' ); // لا رسالة تنبيه! جرّب المثال في ساحة التّجربة تُفيد الوظيفة .detach() في حذف العناصر مؤقّتًا من الصّفحة، فمثلًا إن رغب في إجراء تعديلات كبيرة على بنية الصّفحة باستخدام jQuery، فقد يكون حذف العناصر مؤقّتًا من الصّفحة ثمّ إضافتها ثانيةً أفضل أداءً بمراحل. ستحتفظ العناصر المحذوفة بهذه الوظيفة بمُتولّيات الأحداث المُرتبطة بها، ويمكن بعد ذلك إضافتها إلى الصّفحة مُجدّدًا باستخدام .appendTo() أو غيرها من وظائف الإضافة. $( '#my-unordered-list li' ).click(function() { alert( $( this ).text() ); }); var detachedListItem = $( '#my-unordered-list li' ).first().detach(); // افعل شيئًا ما مُعقّدًا بعنصر القائمة detachedListItem.appendTo( '#my-unordered-list' ); detachedListItem.trigger( 'click' ); // alert! أخيرًا لدينا الوظيفة .replaceWith() الّتي تُحلِّ عنصرًا أو نصّ HTML محلّ عنصرٍ أو عناصر أخرى. تُعاد العناصر الّتي أُزيلت من الوظيفة، ولكنّ كلّ متولّيات الأحداث المرتبطة بها تُحذف، تمامًا كالوظيفة .remove(). $( '#my-unordered-list li' ).click(function() { alert( $( this ).text() ); }); var replacedListItem = $( '#my-unordered-list li' ).first() .replaceWith( '<li>new!</li>' ); replacedListItem.appendTo( '#my-unordered-list' ); replacedListItem.trigger( 'click' ); // لا رسالة تنبيه! جرّب المثال في ساحة التّجربة خاتمة تعلّمنا في هذا الجزء الطّرق المختلفة للانتقال بين العناصر في الصّفحة، وكيفيّة نقلها وتغييرها وإضافة عناصر جديدة. سنتعلّم في الجزء القادم كيف نُنصِت لتفاعل المستخدم مع صفحتنا. ترجمة (بشيء من التصرف) للجزء الثالث من سلسلة jQuery Fundamentals لمؤلّفتها Rebecca Murphey.
-
تعلمنا فيما سبق في هذه السلسلة "كيف تجري تغيرات على شجرة DOM" والتعامل مع شجرة DOM من إنشاء، وإدراج، واستبدال وإزالة عناصر من المستند بتوابع مضمنة. أصبحت الآن مؤهلًا لاستخدام قدرات JavaScript التفاعلية وتعديل عناصر الويب لبزيادة كفاءتك في التعامل مع شجرة DOM. table { width: 100%; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; } tr:nth-child(even) { background-color: #dddddd; } سنكمل في هذا المقال موضوع إجراء التعديل على شجرة DOM بتعديل الأنماط والأصناف والخاصيات الأخرى لعقد عناصر HTML. وهذا سيعطيك فهمًا أكبر للتعامل مع العناصر الأساسية لشجرة DOM. استعراض تحديد العناصر استخدمت مكتبة شائعة ضمن الجافاسكربت تسمى jQuery لتحديد وتعديل عناصر شجرة DOM. سهَّلت هذه المكتبة من اختيار عنصر أو أكثر ضمن المستند وتعديلها وتغييرها في آن معًا. راجعنا في مقال "الوصول إلى العناصر في شجرة DOM" التوابع الخاصة بـ DOM التي تحدد العقد وتعدل عليها عبر جافاسكربت الصرفة فقط. لنتذكر أن التابعين document.querySelector() و document.getElementById() يستخدمان للوصول إلى عنصر واحد. نستطيع الوصول العنصر باستخدام العنصر div أو عبر الخاصية id. <div id="demo-id">Demo ID</div> بالإمكان اختيار أي عنصر في الصفحة بأي نوع من المحدِّدات باستخدام التابع querySelector(). // كلا التابعين سيعيد عنصرًا واحدًا const demoId = document.querySelector('#demo-id'); بإمكاننا تحديث جزء من العنصر كالنص داخله بسهولة، حينما نصل إلى العنصر. // تغيير النصّ لعنصر واحد demoId.textContent = 'Demo ID text updated.'; حينما نصل إلى عدة عناصر باستخدام محدد مشترك، مثل صنف محدد، يمكننا إجراء تعديل على جميع العناصر داخل حلقة تكرارية. لدينا عنصرين من div ذوي صنف مشترك. <div class="demo-class">Demo Class 1</div> <div class="demo-class">Demo Class 2</div> للإمساك بجميع العناصر ذات الصنف demo-class مثلًا، سنستخدم querySelectorAll()، وسنستخدم forEach() للوصول إلى العنصر وتطبيق التغيير. كما بالإمكان الوصول إلى عنصر محدد بـ querySelectorAll() بنفس الطريقة التي تستخدمها مع مصفوفة، باستخدام الأقواس المربعة. // demo الحصول على قائمة بجميع العناصر ذات الصنف const demoClasses = document.querySelectorAll('.demo-class'); // تغيير النصّ لعدّة عناصر باستخدام حلقة تكراريّة demoClasses.forEach(element => { element.textContent = 'All demo classes updated.'; }); // الوصول للعنصر الأول ضمن القائمة demoClasses[0]; هذا هو أحد أهم الاختلافات التي يجب معرفتها عند الانتقال من jQuery إلى JavaScript الصرفة. لن تشرح العديد من الأمثلة على تعديل العناصرعملية تطبيق هذه التوابع والخصائص على عناصر متعددة. غالبًا ما يتم إرفاق الخاصيات والتوابع الواردة في هذه المقالة بـ «مسمتعي الأحداث» (event listener) من أجل الاستجابة للنقرات، التحويم (hover)، ومشغلات (triggers) أخرى. ملاحظة: سيعيد التابعان getElementsByClassName() و getElementsByTagName() مجموعة عناصر HTML لا يمكنها استعمال forEach() على عكس العناصر التي يعيدها التابعquerySelectorAll(). في هذه الحالات، ستحتاج إلى استخدام حلقة for الاعتيادية للتكرار عبر مجموعة العناصر تلك. تعديل الخاصيات تحتوي الخاصيات (التي هي قيم) على معلومات إضافية عن عناصر HTML. تأتي عادةً في ثنائيات الاسم/القيمة، وقد تكون أساسية اعتمادًا على العنصر. إن الخاصية src التابعة للعنصرimg، والخاصية href التابعة للعنصر a، وكذلك الخاصيات class و id، و style هي أحد أكثر الخاصيات شيوعًا. للحصول على قائمة كاملة بخاصيات HTML، قم بعرض قائمة الخاصيات على شبكة مطوري Mozilla. ستُسبَق العناصر المخصصة التي ليست جزءًا من معيار HTML بـالبادئة data-. لدينا أربع توابع لتعديل خاصيات العنصر في جافاسكربت: التابع الوصف مثال hasAttribute() يعيد القيمة true أو false إن كان العنصر يملك خاصية محدَّدة أم لا. element.hasAttribute('href'); getAttribute() يعيد قيمة الخاصية المحددة أو null. element.getAttribute('href'); setAttribute() يضيف أو يحدّث قيمة لخاصية محددة. element.setAttribute('href', 'index.html'); removeAttribute() يزيل خاصية محدَّدة من عنصر. element.removeAttribute('href'); لننشئ ملف HTML جديد يحوي العنصر img مع خاصية واحدة. سنربط العنصر بصورة عامة عبر رابط، إلا أنه من الممكن ربط الوسم بصورة محلية في حال عدم توفر اتصال بالإنترنت. attributes.html <!DOCTYPE html> <html lang="en"> <body> <img src="https://js-tutorials.nyc3.digitaloceanspaces.com/shark.png"> </body> </html> افتح الملف السابق على متصفح متطور، وافتح الطرفية في وحدة تحكم المطور (Developer Console)، وستشاهد ما يلي: سنختبر طرق الخاصية (دالات) عبر المثال التالي: // إسناد عنصر صورة const img = document.querySelector('img'); img.hasAttribute('src'); // يعيد القيمة المنطقية صح img.getAttribute('src'); // "...shark.png" يعيد img.removeAttribute('src'); // وقيمتها src يحذف الخاصية أزلت الخاصية src والقيمة المرتبطة بها الموجودة العنصر img بعد المثال السابق، ولكن بإمكانك إعادة تعيين تلك الخاصية وإسناد القيمة لصورة بديلة باستخدام img.setAttribute(): img.setAttribute('src', 'https://js-tutorials.nyc3.digitaloceanspaces.com/octopus.png'); يمكننا تعديل الخاصية مباشرةً بإسناد قيمة جديدة إليها كخاصية للعنصر، وإعادة تعيين src للصورة shark.png بالشكل التالي: img.src = 'https://js-tutorials.nyc3.digitaloceanspaces.com/shark.png'; يمكن تحرير أي خاصيّة بهذا التابع وكذلك مع التوابع المذكورة أعلاه. إن التابعينhasAttribute() و getAttribute() عادةً ما يُستخدمان مع عبارات شرطية، بينما تُستخدَم setAttribute() و removeAttribute() مباشرةً لتعديل شجرة DOM. تعديل الأصناف تتوافق الخاصية "class" مع محددات الصنف في CSS. لا يجب الخلط مع أصناف ES6، وهو نوع خاص من دالات جافاسكربت. تُسْتخدمُ أصناف CSS لتطبيق الأنماط على عناصر متعددة، على عكس المعرفات التي لا يمكن أن توجد إلا مرة واحدة في كل صفحة. في جافاسكربت، لدينا الخاصيتين className و classList للعمل مع الخاصية class. التابع / الخاصية الوصف مثال className قراءة أو كتابة قيمة الخاصية class. element.className; classList.add() إضافة قيمة صنف أو أكثر. element.classList.add('active'); classList.toggle() تبديل صنف محدَّد أي إضافة إن لم يكن موجودًا أو إزالته إن كان موجودًا. element.classList.toggle('active'); classList.contains() تحقق إذا كانت قيمة الصنف موجود. element.classList.contains('active'); classList.replace() استبدال قيمة صنف موجود بقيمة صنف جديد. element.classList.replace('old', 'new'); classList إزالة قيمة صنف. element.classList.remove('active'); سنقوم بإنشاء ملف HTML آخر يحوي عنصرين بعدة أصناف للعمل مع توابع الصنف. الملف classes.html: <!DOCTYPE html> <html lang="en"> <style> body { max-width: 600px; margin: 0 auto; font-family: sans-serif; } .active { border: 2px solid blue; } .warning { border: 2px solid red; } .hidden { display: none; } div { border: 2px dashed lightgray; padding: 15px; margin: 5px; } </style> <body> <div>Div 1</div> <div class="active">Div 2</div> </body> </html> عند فتح الملف classes.html في متصفح الويب، يجب أن تتلقى عرضًا مماثلاً لما يلي: عُرِّفت الخاصيِّة className بهذا الاسم لمنع التعارض مع الكلمة المحجوزة class الموجودة في جافاسكربت وغيرها من اللغات التي لديها إمكانية الوصول إلى شجرة DOM. بإمكانك استخدام className لإسناد قيمة مباشرة إلى الصنف. // div اختيار أول عنصر const div = document.querySelector('div'); // الأول div للعنصر warning إسناد الصنف div.className = 'warning'; أسندنا الصنف warning (تحذير) المعرف في قيم CSS للملف classes.html للعنصر div الأول. ستتلقى المُخْرَجَ التالي: لاحظ أنه في حال وجود أصناف مسبقًا على العنصر، فإن هذا سيؤدي إلى تجاوزها. يمكنك إضافة عدة أصناف بفصلها بمسافة فارغة باستخدام الخاصية className، أو استخدامها دون معاملات الإسناد للحصول على قيمة الصنف في العنصر. الطريقة الأخرى لتعديل الأصناف هي عن طريق الخاصية classList، والتي تأتي مع بعض الدالات التوابع المفيدة. هذه التوابع مشابهة للتوابع addClass، و removeClass، و toggleClass الموجودة في jQuery. // الثاني عن طريق اسم الصنف div اختيار العنصر const activeDiv = document.querySelector('.active'); activeDiv.classList.add('hidden'); // hidden إضافة الصنف activeDiv.classList.remove('hidden'); // hidden إزالة الصنف activeDiv.classList.toggle('hidden'); // hidden إضافة أو إزالة الصنف activeDiv.classList.replace('active', 'warning'); // active مكان الصنف warning تبديل الصنف بعد تنفيذ التوابع أعلاه، ستبدو صحفة الويب الخاصة بك كما يلي: إن استخدام التابع classList.add() سيضيف صنفًا جديدًا إلى قائمة الأصناف الموجودة، على عكس استخدام className. يمكنك أن تضيف عدة أصناف مفصولة بالفاصلة. من الممكن أيضًا استخدام setAttribute لتعديل صنف العنصر. تعديل الأنماط تعتبر الخاصية style من الأنماط المضمنة على عنصر HTML. سيتم تطبيق الأنماط على العناصر عبر ورقة أنماط غالبًا، كما فعلنا سابقًا في هذه المقالة، ولكن في بعض الأحيان يتعين علينا إضافة أو تعديل نمط مضمَّن مباشرة. سنقدِّمُ مثالًا قصيرًا لشرح كيفية تعديل الأنماط باستخدام جافاسكربت. يوجد ملف HTML جديد أدناه بعنصر div يحتوي على بعض الأنماط المضمَّنة فيه مباشرةً. الملف styles.html: <!DOCTYPE html> <html lang="en"> <body> <div style="height: 100px; width: 100px; border: 2px solid black;">Div</div> </body> </html> عند فتح الملف styles.html في المتصفح، سيبدو على الشكل التالي: إحدى طرق تعديل الأنماط هي باستعمال التابع setAttribute(). // div إمساك العنصر const div = document.querySelector('div'); // div تطبيق النمط على العنصر div.setAttribute('style', 'text-align: center'); ومع ذلك، سيؤدي إلى إزالة جميع الأنماط الضمنية من العنصر (أي حذف العنصر style السابق بما يحويه وإضافة العنصر الجديد مع القيمة المعطاة). وبما أن هذا ليس هو التأثير المطلوب، فمن الأفضل استخدام الخاصية style مباشرةً بالشكل التالي: div.style.height = '100px'; div.style.width = '100px'; div.style.border = '2px solid black'; تُكْتَبُ خاصيات الـ CSS في حالة أسياخ الشواء (kebab-case)، وهي كلمات بأحرف صغيرة مفصولة بشرطة. لاحظ أنه من المهم معرفة أن حالة أسياخ الشواء لخاصيات الـ CSS لا يمكن استخدامها مع الخاصية style جافاسكربت. بدلاً من ذلك، ستستبدل بمكافئها من حالة سنام الجمل (الحرف الأول صغير وباقي الكلمات متصلة بأحرف كبيرة). بعبارة أخرى، سنستخدم textAlign بدلًا من text-glign في الخاصية style للجافاسكربت. // إلى دائرة وتوسيط النصِّ عاموديًا div تحويل العنصر div.style.borderRadius = '50%'; div.style.display = 'flex'; div.style.justifyContent = 'center'; div.style.alignItems = 'center'; بعد الانتهاء من تعديلات النمط أعلاه، سيظهر الملف styles.html دائرة على الشكل التالي: إذا تم تطبيق العديد من التغييرات المتعلقة بالنمط على عنصر ما، فإن أفضل مسار للعمل هو تطبيق الأنماط على صنف جديد. ومع ذلك، هناك بعض الحالات التي يكون فيها تعديل خاصية النمط المضمن ضروريًا أو أكثر وضوحًا. الخلاصة تحتوي عناصر HTML غالبًا على معلومات إضافية مسندة إليها عبر الخاصيات. قد تتكون الخاصيات من ثنائية الاسم/القيمة، وعدد قليل من الخاصيات الأكثر شيوعًا هي class و style. تعلمنا في هذا الدليل كيفية الوصول إلى خاصيات عنصر HTML ضمن شجرة DOM وتعديلها وحذفها باستخدام جافاسكربت. كما تعلمنا إضافة أصناف CSS وإزالتها وتبديلها واستبدالها في عنصر، وكيفية تحرير أنماط CSS. ترجمة -وبتصرف- للمقال Understanding the DOM — Document Object Model لصاحبته Lisa Tagliaferri
-
تحدثنا في مقالتين سابقتين عن طريقة الوصول إلى العناصر في DOM و التنقل بين عقد شجرة DOM وتعلمنا كيف يستطيع مطورو الويب استخدام الأصناف، والوسم، والمعرفات لإيجاد عقدة في DOM بالإضافة إلى استخدام خصائص القرابة بين هذه العقد للتنقل فيما بينها بهدف الوصول في النهاية إلى العقدة المطلوبة. الخطوة التالية الآن ستكون احترافية بشكل أكبر حيث سنتعلم طريقة إضافة، وتغيير، واستبدال، وإزالة العقد من DOM. وسنتدرّب على ذلك عبر برنامج قائمة مهام بسيط مع لغة جافاسكربت. سنتعلم في هذه المقالة كيفية إنشاء العقد وإضافتها إلى DOM، واستبدال وإزالة عقد قديمة. إنشاء عقد جديدة في مواقع الويب الثابتة، يتم إضافة العناصر بشكل مباشر من خلال كتابة ملف HTML باللاحقة .html ولكن في تطبيقات الويب الديناميكية المتغيرة باستمرار، تُضَاف العناصر والنصوص بشكل آلي من خلال جافاسكربت باستخدام توابع مثل createElement() و createTextNode() التي تنشئ عقدة جديدة في DOM. table { width: 100%; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; } tr:nth-child(even) { background-color: #dddddd; } الخاصية / التابع التوصيف createElement() إنشاء عقدة جديدة من نوع عنصر createTextNode() إنشاء عقدة جديدة من نوع نص node.textContent الوصول أو تعديل المحتوى النصي لعقدة العنصر node.innerHTML الوصول أو تعديل محتوى HTML في العنصر لنبدأ أولًا بإنشاء الملف index.html ونقوم بحفظه في مجلد المشروع. <!DOCTYPE html> <html lang="en"> <head> <title>Learning the DOM</title> </head> <body> <h1>Document Object Model</h1> </body> </html> نضغط بالزر الأيمن على أي مكان في الصفحة لتظهر لدينا قائمة نختار منها "Inspect" لفتح أدوات التطوير ثم نختار اللسان Console. سنقوم الآن باستخدام التابع ()createElement الموجود في الكائن document من أجل إنشاء عنصر جديد هو p: > const paragraph = document.createElement('p'); للتأكد من أنه تم إنشاء العنصر p، ندخل التعليمة التالية إلى الطرفية: > console.log(paragraph) حيث سيكون الخرج هو: <p></p> نلاحظ أن المتغير paragraph أعطانا كخرج عنصر p فارغ وهو غير مفيد من دون وجود نص فيه، لذلك سنقوم بإضافة النص من خلال تعديل الخاصية textContent بالشكل التالي: > paragraph.textContent = "I'm a brand new paragraph."; > console.log(paragraph) عندها سيكون الخرج هو: <p>I'm a brand new paragraph.</p> إن استخدام ()createElement و textContent معًا يؤدي إلى بناء عقدة عنصر كاملة. ولكن يمكن أيضًا استخدام تابع بديل لتعديل محتوى العنصر من خلال الخاصيةinnerHTML التي تسمح بإضافة تعليمات HTML ونص عادي إلى العنصر بآن واحد: > paragraph.innerHTML = "I'm a paragraph with <strong>bold</strong> text."; تحذير: إن طريقة استخدام innerHTML شائعة لإضافة محتوى إلى العنصر ولكن سيصبح الموقع عرضة لخطر هجوم cross-site scripting (xxs) والذي يتم من خلال إضافة سطر تعليمة جافاسكربت قد تؤدي إلى ضرر بالموقع باستخدام innerHTML غير مرغوب فيه، لذلك ينصح باستخدام textContent وبالتالي سيتم التخلص من وسوم HTML فورًا. كما أنه من الممكن استخدام التابع createTextNode() لإضافة عقدة نص، كما في التعليمة: > const text = document.createTextNode("I'm a new text node."); > console.log(text) وسيكون الخرج هو بالشكل: "I'm a new text node." إذًا، نستطيع من خلال هذه التوابع إنشاء عقد عناصر ونصوص جديدة، ولكن هذه العقد لن تصبح مرئية في واجهة الموقع حتى يتم إدخالها إلى المستند document. إضافة عقد إلى DOM نحتاج الآن بعد بناء عقد العناصر والنصوص إلى إضافتها إلى document ويتم ذلك من خلال استخدام مجموعة من التوابع مثل appendChild() و insertBefore() لإضافة العناصر في بداية، وسط، أو نهاية عنصر الأب، كما يمكن استخدام التابع replaceChild() لاستبدال عقدة قديمة بأخرى جديدة. الخاصية / التابع التوصيف node.appendChild() إضافة عقدة كابن أخير للعنصر الأب node.insertBefore() إضافة عقدة على نفس مستوى عقدة الأب قبل عقدة الأخ المحددة node.replaceChild() استبدال العقدة الحالة بأخرى جديدة للتمرن على هذه التوابع سنقوم بإنشاء قائمة المهام التالية باستخدام html: Buy groceries Feed the cat Do laundry وعند تحميل الملف السابق إلى المتصفح فإنه سيبدو بالشكل: الآن سنقوم بإضافة عنصر جديد إلى نهاية قائمة المهام، وللقيام بذلك سننشئ عنصرًا ونضيف له نصًّا كما فعلنا في المقطع السابق: > // ليمثل قائمة المهام ul إنشاء عنصر > const todoList = document.querySelector('ul'); > // إنشاء مهمة جديدة أي عنصر قائمة > const newTodo = document.createElement('li'); > newTodo.textContent = 'Do homework'; الأن سنقوم بإضافة هذه المهمة إلى نهاية القائمة باستخدام التابع appendChild(): > // إضافة مهمة جديدة إلى نهاية القائمة > todoList.appendChild(newTodo); عند فتح ملف html الخاص بالقائمة، سنلاحظ أنه تم إضافة عنصر li جديد في نهاية ul: <ul> <li>Buy groceries</li> <li>Feed the cat</li> <li>Do laundry</li> <li>Do homework</li> </ul> قد نحتاج لإضافة مهمة جديدة إلى القائمة ولكنها على رأس الأولويات، وبالتالي نحتاج لإضافتها في رأس القائمة. وللقيام بذلك، سنبدأ أولًا بإنشاء عنصر من خلال التابع createElement() ولكنه غير قابل للاستخدام بعد: > // إنشاء مهمة جديدة > const anotherTodo = document.createElement('li'); > anotherTodo.textContent = 'Pay bills'; ولإضافة هذا العنصر إلى رأس القائمة، سنستخدم التابع insertBefore() حيث نمرر لهذا التابع متغيرين الأول هو العقدة الجديدة التي نريد إضافتها والثاني هو العقدة الأخ الذي يأتي مباشرة بعد العقدة الجديدة وسيكون شكل التعليمة كما يلي: parentNode.insertBefore(newNode, nextSibling); الآن، لتطبيق التعليمة السابقة عمليًا على قائمتنا، سنضيف العقدة التي بنيناها سابقًا والمسماة anotherTodo قبل العنصر الأول من أبناء القائمة والذي هو العنصر “Buy groceries”: > // إضافة مهمة جديدة إلى بداية القائمة > todoList.insertBefore(anotherTodo, todoList.firstElementChild); الآن سيصبح شكل ملف todo.html هو: <ul> <li>Pay bills</li> <li>Buy groceries</li> <li>Feed the cat</li> <li>Do laundry</li> <li>Do homework</li> </ul> وعند تحميل الملف السابق على المتصفح سيظهر كمايلي: إذًا تمت إضافة العقدة الجديدة بنجاح لبداية القائمة، وهكذا نكون قد تعلمنا كيفية إضافة عقدة ابن لعقدة أب ولكن يمكننا أيضًا استبدال هذه العقدة الموجودة بعقدة جديدة. لتعديل عقدة موجودة في قائمة المهام، سننشئ أولًا عنصرًا جديدًا: > const modifiedTodo = document.createElement('li'); > modifiedTodo.textContent = 'Feed the dog'; سنقوم بالاستبدال باستخدام التابع replaceChild() الذي يشبه التابع insertBefore()حيث يتم تمرير متحولين له الأول هو العقدة الجديدة والثاني هو العقدة المراد استبدالها، حيث أن الشكل العام لهذه التعليمة هو: parentNode.replaceChild(newNode, oldNode); الآن سنقوم باستبدال العنصر الثالث في القائمة بالعنصر الجديد: > // استبدال مهمة موجودة مسبقًا بأخرى > todoList.replaceChild(modifiedTodo, todoList.children[2]); حيث سيتم تلقائيًا تعديل تعليمات ملف html لتصبح بالشكل: <ul> <li>Pay bills</li> <li>Buy groceries</li> <li>Feed the dog</li> <li>Do laundry</li> <li>Do homework</li> </ul> ويصبح شكل صفحة الويب: باستخدام توابع appendChild()، و insertBefore()، و replaceChild() يمكننا إضافة العناصر إلى أي مكان في DOM. حذف عقد من DOM بما أننا تعرفنا على طريقة إنشاء عناصر وإضافتهم إلى DOM أو تعديل العناصر الموجودة، تبقى لنا الخطوة الأخيرة لإتمامها وهي تعلم كيفية حذف عنصر موجود في DOM. نستطيع حذف العقد الأبناء من العقدة الأب باستعمال التابع removeChild() كما نستطيع حذف العقدة نفسها باستخدام التابع remove(). التابع التوصيف node.removeChild() حذف عقدة ابن node.remove() حذف العقدة نفسها بالعودة إلى مثال قائمة المهام السابق، سنحذف عناصر من القائمة بعد الانتهاء منها. مثلًا سنقوم بحذف المهمة "Do homework" الموجودة في نهاية القائمة (أي هي الابن الأخير للقائمة) وذلك باستخدام التابع removeChild(): >todoList.removeChild(todoList.lastElementChild); بالعودة لملف html نلاحظ أن عنصر li الأخير قد حُذِف: <ul> <li>Pay bills</li> <li>Buy groceries</li> <li>Feed the dog</li> <li>Do laundry</li> </ul> الآن سنستخدم التابع الآخر وهو remove() والذي يقوم بحذف العقدة نفسها مباشرة: > // Remove second element child from todoList > todoList.children[1].remove(); وبالتالي يصبح ملف html بالشكل التالي: <ul> <li>Pay bills</li> <li>Feed the dog</li> <li>Do laundry</li> </ul> باستخدام التابعين removeChild() و remove()، يمكننا إزلة أي عقدة من DOM كما يمكننا استخدام الخاصية innerHTML لإزالة عقدة ابن من العنصر الأب من خلال استبدال الابن بسلسلة نصية فارغة (empty string) بالشكل "" ولكن هذه الطريقة غير مفضلة في الاستخدام. ملخص تعلمنا في هذه المقالة كيفية استخدام الجافا سكربت لإنشاء عقد وعناصر جديدة ثم إضافتها إلى DOM، واستبدال أو حذف العقد والعناصر الموجودة. وبهذا نكون قد تعلمنا طريقة الوصول إلى عناصر DOM والتنقل بين العقد وتعديلها وبالتالي يمكنك البدء بإنشاء واجهة تطيبق ويب باستخدام الجافا سكربت. ترجمة -وبتصرف- للمقالة How To Make Changes to the DOM للكاتبة Tania Rascia
-
مقدمة تحدثنا في المقال السابق عن البنية الشجرية لـ DOM وعن العقد Nodes المكونة لها وهي عبارة عن كائنات objects من الممكن أن تكون نصوص أو تعليقات أو عناصر. في الغالب يمكن الوصول إلى محتوى DOM من خلال عقد عناصر html، ولمعرفة كيفية الوصول إلى عناصر DOM بشكل احترافي من الضروري امتلاك معلومات كافية عن قواعد وأساسيات css، لذلك سنقوم في هذه المقالة بتعلم كيفية الوصول إلى عناصر DOM من خلال ID والصف class والوسم tag، ومحددات الاستعلام query selectors. نظرة عامة سنستعرض في هذا المقال خمس توابع methods هي: الطرق محدد بناء الجملة يحصل على getElementById() #demo ID getElementsByClassName() .demo Class getElementsByTagName() demo Tag querySelector() Selector (single) querySelectorAll() Selector (all) من المهم عند دراستنا ل DOM أن تقوم بتطبيق الأمثلة على حاسبك للتأكد من فهم جميع المعلومات بشكل جيد. سنبدأ بإنشاء ملف access.html في مجلد المشروع والذي سنقوم بتطبيق جميع أمثلتنا عليه في هذه المقالة. <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Accessing Elements in the DOM</title> <style> html { font-family: sans-serif; color: #333; } body { max-width: 500px; margin: 0 auto; padding: 0 15px; } div, article { padding: 10px; margin: 5px; border: 1px solid #dedede; } </style> </head> <body> <h1>Accessing Elements in the DOM</h1> <h2>ID (#demo)</h2> <div id="demo">Access me by ID</div> <h2>Class (.demo)</h2> <div class="demo">Access me by class (1)</div> <div class="demo">Access me by class (2)</div> <h2>Tag (article)</h2> <article>Access me by tag (1)</article> <article>Access me by tag (2)</article> <h2>Query Selector</h2> <div id="demo-query">Access me by query</div> <h2>Query Selector All</h2> <div class="demo-query-all">Access me by query all (1)</div> <div class="demo-query-all">Access me by query all (2)</div> </body> </html> في ملف html هذا نلاحظ أن لدينا العديد من العناصر التي يمكن الوصول إليها من خلال توابع method الخاصة بـ document وعند فتح هذا الملف في المتصفح سيكون مشابه لهذه الصفحة: سنستخدم العديد من التوابع methods التي ذكرناها في المقال سابقًا للوصول إلى العناصر المتاحة في هذا الملف. الوصول للعناصر باستخدام ID أسهل طريقة للوصول إلى عنصر محدد في DOM هو المعرف الخاص بها ID(Identification). حيث يمكن الوصول إلى العنصر من خلال ID باستخدام التابع getElementById() مع العنصر document. document.getElementById(); ولكن للحصول على هذه الخاصية في الوصول إلى العنصر من خلال id يجب أولًا إضافة الصفة id للعنصر. مثلًا سنضيف المعرف demo للعنصر div <div id="demo">Access me by ID</div> الآن في تبويب console سنقوم بالوصول إلى العنصر واسناده للمتحول demoId > const demoId = document.getElementById('demo'); في حال تمرير المتحول demoId إلى تابع log سنحصل على عنصر html كاملًا. > console.log(demoId); سيكون الخرج عندها العنصر كاملًا <div id="demo">Access me by ID</div> للتأكد من وصولنا إلى العنصر الصحيح سنقوم بتغيير حدود العنصر border property إلى اللّون البنفسجي > demoId.style.border = '1px solid purple'; وعند تنفيذ هذه التعليمات سنلاحظ أن حدود العنصر قد تغيرت إلى اللّون البنفسجي: إن الوصول إلى أي عنصر من خلال ID الخاص به هي أكثر طريقة فعالة وسريعة للوصول إلى عنصر ضمن DOM، ومع ذلك هذه الطريقة لديها عدة عيوب وهي أن ID دائمًا يجب أن يكون متفردًا في الصفحة ولذلك لا يمكن الوصول إلا إلى عنصر واحد فقط من خلال التابع getElementById. ولذلك في حال الحاجة إلى إضافة تابع لأكثر من عنصر في الصفحة فإن إضافته باستخدام id سيؤدي إلى تكرار كتابة التابع لكل عنصر على حدى. الوصول للعناصر باستخدام class تستخدم خاصية class للوصول إلى عنصر أو أكثر في DOM وذلك من خلال التابع getElementByClassName() document.getElementsByClassName(); ونستطيع من خلال الخاصية class الوصول إلى أكثر من عنصر، كما في هذا المثال حيث سنعطي لعنصرين نفس اسم الصف demo <div class="demo">Access me by class (1)</div> <div class="demo">Access me by class (2)</div> الآن في تبويبة console سنبني متحول demoClass ونسند له العناصر التي استطعنا الوصول إليها من خلال اسم الصف demo > const demoClass = document.getElementsByClassName('demo'); والآن أصبح بالإمكان الوصول إلى العناصر وتعديلها باستخدام class كما فعلنا في المثال السابق باستخدم ID. سنقوم الآن بتغيير خاصية الحدود border من خلال المتحول demoClass، demoClass.style.border = '1px solid orange'; ولكن ستظهر لنا رسالة خطأ بأنه لا يمكن تحديد العنصر Output Uncaught TypeError: Cannot set property 'border' of undefined والسبب في هذا الخطأ أن المتحول demoClass لايحوي عنصرًا واحدًا فقط وإنما مصفوفة كائنات من هذه العناصر للتأكد من ذلك سنكتب التعليمة التالية لمعرفة عدد عناصر المصفوفة المُسند إلى deomClass console.log(demoClass); والخرج سيكون هو عدد العناصر في المصفوفة Output (2) [div.demo, div.demo] حتى نستطيع الوصول إلى العناصر بشكل صحيح يجب أن نتعامل مع المتحول demoClass على أنه مصفوفة جافا سكربت يتم الوصول إليها من خلال رقم دليل العنصر index number. الآن نستطيع تغيير حدود كل العنصر الأول في المصفوفة باستخدام رقم الدليل 0 demoClass[0].style.border = '1px solid orange'; بشكل عام للوصول إلى العناصر من خلال الصف calss سنقوم بتغيير جميع العناصر التي تمتلك نفس اسم الصف باستخدام حلقة for التي تقوم بالمرور على جميع العناصر التي تحويها المصفوفة for (i = 0; i < demoClass.length; i++) { demoClass[i].style.border = ‘lpx solid orange’; } وعند تنفيذ هذه التعليمات سيصبح شكل صفحة الويب إذًا استطعنا الوصول إلى جميع العناصر التي تمتلك الصف demo وتغيير خاصية حدودها border إلى اللّون البرتقالي. الوصول للعناصر باستخدام Tag طريقة الوصول للعنصر من خلال اسم الوسم html tag name هو أقل الطرق تخصيصًا حيث يمكننا الوصول إلى العديد من العناصر باستخدام تابع getElementsByTagName() document.getElementsByTagName(); سنستخدم كمثال هنا العنصر article للوصول إليه من خلال اسم الوسم، نلاحظ العنصرين الذين يحملان اسم الوسم article <article>Access me by tag (1)</article> <article>Access me by tag (2)</article> وبنفس الطريقة التي وصلنا للعناصر من خلال اسم الصف class فإن التابع getElementsByTagName تعيد لنا مصفوفة array من العناصر حيث يمكن تعديل كل tag في المستند من خلال حلقة for const demoTag = document.getElementsByTagName(‘article’); for (i = 0; i < demoTag.length; i++) { demoTag[i].style.border = ‘1px solid blue’; } وبمجرد تنفيذ هذه التعليمات سيصبح شكل صفحة الويب بالشكل وهكذا قمنا بتعديل خاصية الحدود border لجميع العناصر article إلى اللّون الأزرق. محددات الاستعلام Query Selector في حال كان لديك خبرة سابقة في jQuery API ربما ستكون لديك معرفة سابقة مع توابع jQuery التي تستطيع الوصول لعناصر DOM من خلال محددات css $('#demo'); // returns the demo ID element in jQuery يمكننا الحصول على نفس النتيجة في الجافا سكربت من خلال استخدام التوابع querySelector() و querySelectorAll() document.querySelector(); document.querySelectorAll(); للوصول إلى عنصر واحد نستخدم تابع querySelector. فمثلًا يمكننا الوصول إلى العنصر الذي لديه المعرف demo-query من خلال التابع querySelector() <div id="demo-query">Access me by query</div> حيث نمرر للتابع المعرف id مع إضافة الرمز #، وسنقوم بإسناد العنصر للثابت demoQuery const demoQuery = document.querySelector('#demo-query'); ولكن في حالة استخدمنا التابع السابق مع المحددات التي تعطينا أكثر من عنصر واحد مثل class أو tag فإن querySelector() سيعيد لنا العنصر الأول فقط الذي يتطابق مع الاستعلام، ولكن نستطيع استخدام التابع querySelectorAll() من أجل الحصول على جميع العناصر المتطابقة مع الاستعلام. في مثالنا نلاحظ أن لدينا عنصرين يملكان اسم الصف demo-query-all <div class="demo-query-all">Access me by query all (1)</div> <div class="demo-query-all">Access me by query all (2)</div> إن اسم المحدد للخاصية class يجب أن يسبق بنقطة عند تمريره للتابع querySelectorAll() أي سنمرر في هذا المثال .demo-query-all const demoQueryAll = document.querySelectorAll('.demo-query-all'); باستخدام التابع forEach() يمكننا تطبيق اللّون الأخضر على حدود هذه العناصر deomQueryAll.forEach(query => { query.style.border = ‘1px solid green’; }); تعتبر الفاصلة , في حال وضعناها بين القيم للتابع querySelector() كعملية OR مثلاً إذا قمنا بكتابة التابع بالشكل querySelector(‘div, article’) أي تعني أن يجد لنا العنصر div أو article بغض النظر عمن سيظهر أولًا في الملف. أما في حالة وجود الفاصلة بين القيم للتابع querySelectorAll فإنها تعني العملية AND أي في حال كتبنا التابع querySelectroAll(‘div, article’) فإنها تعني إعادة العناصر التي تحوي القيم articl و div الموجودة ضمن المستند. إن استخدام توابع محددات الاستعلامات يعطينا القدرة العالية في البرمجة للوصول إلى أي عنصر أو مجموعة عناصر ضمن DOM بنفس المحددات التي نستخدمها في ملف css. وفي هذه المقالة يوجد قائمة بجميع المحددات الخاصة بال css التي جمعت بواسطة شبكة مطوري Mozilla. تعليمات الجافا سكربت كاملة سأذكر هنا التعليمات التي كتبناها بلمف access.js بشكل كامل. حيث يمكن إضافة ملف js إلى ملف html قبل إغلاق body. // Assign all elements const demoId = document.getElementById('demo'); const demoClass = document.getElementsByClassName('demo'); const demoTag = document.getElementsByTagName('article'); const demoQuery = document.querySelector('#demo-query'); const demoQueryAll = document.querySelectorAll('.demo-query-all'); // Change border of ID demo to purple demoId.style.border = '1px solid purple'; // Change border of class demo to orange for (i = 0; i < demoClass.length; i++) { demoClass[i].style.border = '1px solid orange'; } // Change border of tag demo to blue for (i = 0; i < demoTag.length; i++) { demoTag[i].style.border = '1px solid blue'; } // Change border of ID demo-query to red demoQuery.style.border = '1px solid red'; // Change border of class query-all to green demoQueryAll.forEach(query => { query.style.border = '1px solid green'; }); وسيكون الشكل النهائي لملف html هو <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Accessing Elements in the DOM</title> <style> html { font-family: sans-serif; color: #333; } body { max-width: 500px; margin: 0 auto; padding: 0 15px; } div, article { padding: 10px; margin: 5px; border: 1px solid #dedede; } </style> </head> <body> <h1>Accessing Elements in the DOM</h1> <h2>ID (#demo)</h2> <div id="demo">Access me by ID</div> <h2>Class (.demo)</h2> <div class="demo">Access me by class (1)</div> <div class="demo">Access me by class (2)</div> <h2>Tag (article)</h2> <article>Access me by tag (1)</article> <article>Access me by tag (2)</article> <h2>Query Selector</h2> <div id="demo-query">Access me by query</div> <h2>Query Selector All</h2> <div class="demo-query-all">Access me by query all (1)</div> <div class="demo-query-all">Access me by query all (2)</div> <script src="access.js"></script> </body> </html> ملخص في هذه المقالة تعلمنا 5 طرق للوصول إلى عناصر html الموجودة في DOM وهي من خلال المعرف ID، الصف class، الوسم html tag name، ومن خلال المحددات selector. التوابع التي يمكن استخدامها للحصول على عنصر أو مجموعة عناصر تعتمد على مدى دعم المتصفح المستخدم لها وعدد العناصر التي تتم معالجتها. والآن يمكنك الوصول إل عناصر html الموجودة في المستند باستخدام الجافا سكربت و DOM. هذه المقالة هي ترجمة للمقالة How To Access Elements in the DOM وكاتبتها Tania Rascia. table { width: 100%; } td, th { border: 1px solid #dddddd; text-align: left; padding: 8px; } tr:nth-child(even) { background-color: #dddddd; }
-
مقدمة تعلمنا في المقال السابق طريقة الوصول إلى العناصر في DOM عن كيفية استخدام التوابع الملحقة بالكائن document بهدف الوصول إلى عناصر html بإحدى الطرق التالية: معرف العنصر ID، صف العنصر class، اسم الوسم tag name، أو محددات الاستعلام query selectors. كما شرحنا في مقال البنية الشجرية لـ DOM عن العقدة الأب لـ DOM الممثلة بالكائن document، كما تطرقنا إلى شرح مفصل عن العقد الأبناء التي تبنى من خلالها الشجرة وعن أنواعها (العناصر، التعليقات، والنصوص). ولكننا نحتاج غالبًا للانتقال بين عقد شجرة DOM بدون المرور على جميع العقد التي تسبق العقدة المطلوبة، لذلك سنتحدث في هذه المقالة عن كيفية التنقل في شجرة DOM من الأعلى إلى الأسفل وبالعكس، إضافة إلى التنقل بين أفرع الشجرة من خلال باستخدام تعليمات الجافا سكربت و html. الإعداد: لنبدأ أولًا بإنشاء ملف nodes.html والذي يحوي التعليمات التالية: <!DOCTYPE html> <html> <head> <title>Learning About Nodes</title> <style> * { border: 2px solid #dedede; padding: 15px; margin: 15px; } html { margin: 0; padding: 0; } body { max-width: 600px; font-family: sans-serif; color: #333; } </style> </head> <body> <h1>Shark World</h1> <p>The world's leading source on <strong>shark</strong> related information.</p> <h2>Types of Sharks</h2> <ul> <li>Hammerhead</li> <li>Tiger</li> <li>Great White</li> </ul> </body> <script> const h1 = document.getElementsByTagName('h1')[0]; const p = document.getElementsByTagName('p')[0]; const ul = document.getElementsByTagName('ul')[0]; </script> </html> عند فتح هذا الملف من خلال متصفح الانترنت، سنشاهد الصفحة التالية: في صفحة الويب هذه لدينا مستند html يحوي على عدد قليل من العناصر وأساسيات css مرفقة به من خلال الوسم style من أجل تحسين طريقة إظهار العناصر، كما قمنا ببناء عدد قليل من المتحولات باستخدام تعليمات الجافا سكربت والتي ذكرت ضمن الوسم script حيث ستساعدنا بالوصول إلى العناصر. كما نلحظ وجود عنصر واحد من العناصر التالية h1، p، ul لذا يمكن الوصول إليها من خلال الدليل الأول index للتابع getElementsByTagName. عقدة الجذر Root Node يعتبر الكائن doucment العقدة الجذر root node لجميع العقد في DOM. ولكن في الحقيقة كائن document هو إحدى خصائص الكائن window والذي يمكن تعريفه بأنه كائن عام global وذو مستوى عالي top-level object حيث يمثل تبويب المتصفح الذي يظهر فيه صفحة الويب. يمكننا من خلال كائن window أن نصل إلى معلومات عن شريط الأدوات، طول وارتفاع النافذة، الرسائل الظاهرة في المتصفح، والإنذارات، في حين أن كائن document يتضمن كل الأحداث والخصائص الأخرى الموجودة داخل window. يتضمن الجدول المرفق عناصر الجذر التي يمتلكها كل document حتى لو كان ملف html المحمل إلى المتصفح فارغ وهي عبارة عن 3 عقد سيتم إضافتها وتفسيرها في DOM. table { width: 100%; } td, th { border: 1px solid #dddddd; text-align: left; padding: 8px; } tr:nth-child(even) { background-color: #dddddd; } الخاصية العقدة نوع العقدة document #document DOCUMENT_NODE document.documentElement html Element_NODE getElementsByTagName() head Element_NODE querySelector() body Element_NODE وبما أن عناصر html، head، body شائعة جدًا فإنها تمتلك الخصائص الخاصة بها في المستند. سنقوم بفتح تبويب console ضمن أدوات المطورين DevTools حتى نختبر هذه الخصائص الأربع من خلال كتابتها في console ومشاهدة النتائج. كما يمكننا أيضًا اختبار العناصر h1، p، و ul. عقد الآباء Parent Nodes لدينا في DOM ثلاث أنواع من العقد معرفة حسب نوع العلاقة فيها بينها وهي الآباء parents، الأبناء children، والشقيقة siblings. تعرف العقدة الأب parent بأنها أي عقدة تمتلك عقدًا أقل في مستوى level أقل منها في التدرّج الهرمي للـ DOM، أو يمكن تعريفها بأنها العقد الأقرب لعقدة الجذر document. يمكن الوصول دومًا للعقدة الأب بواسطة إحدى الخاصيتين parentNode أو parentElement. Gets Property ParentNode parentNode ParentElementNode parentElement في مثالنا nodes.html نلاحظ أن: html هي العقدة الأب لكل من عقد head، body، و script العقدة body هي العقدة الأب لكل من h1، h2، و ul ولكنها ليست العقدة الأب لـ li لأنها في المستوى الأقل منها بمستويين. يمكننا اختبار طريقة الوصول إلى العقدة الأب التي ذكرناها سابقًا، حيث سنقوم بمعرفة أب العنصر p من خلال الخاصية parentNode، وكما نذكر أننا قمنا ببناء المتحول p في التعليمات السابقة وإسناد القيمة document.getElementsByTagName('p')[0] وللوصول للأب نكتب التعليمة التالية في تبويب console > p.parentNode; وسيكون الخرج: Output ► <body>...</body> ويمكن استعمال parentNode مرتين للوصول إلى جد p. يمكن الاستعانة بالعديد من الخصائص التي تمكننا من الوصول إلى العقدة الأب ولكن يوجد فيما بينها القليل من الاختلافات كما سنبينها الآن > // Assign html object to html variable > const html = document.documentElement; > > console.log(html.parentNode); // > #document > console.log(html.parentElement); // > null إن الأب لأي عقدة هو من نوع عقدة العنصر element nodes بينما لايمكن للعقد من نوع نص أو تعليقات أن تكون أب لأي عقدة أخرى وبما أن العقدة الأب لـ html هي العقدة الجذر document node لذلك نلاحظ أن القيمة المرجعة هي Null في السطر الأخير من التعليمات. بشكل عام إن استخدام الخاصية parentNode شائع أكثر في الانتقال ضمن DOM. العقد الأبناء Children Nodes إن أبناء أي عقدة هي جميع العقد الموجودة في المستوى الذي يليه مباشرة أما العقد الموجود في المستوى الذي يلي مستوى الأبناء تسمى العقد الأحفاد. Gets Property Child Nodes childNodes First Child Node firstChild Last Child Node lastChild Element Child Nodes children First Child Element Node firstElementChild Last Child Element Node lastElementChild إن خاصية ChildNode ستعيد قائمة بأبناء العقدة. على سبيل المثال في ملف nodes.html السابق نلاحظ أن عنصر ul لديه 3 عناصر أبناء من li وبالتالي عند إدخال التعليمة: > ul.childNodes; وسيكون الخرج: Output ► (7) [text, li, text, li, text, li, text] نلاحظ أننا حصلنا على قائمة بأبناء العنصر ul والتي تحوي العناصر li الثلاث التي ذكرناها بالإضافة إلى عُقد النصوص text nodes ويعود السبب في ذلك لأننا كتبنا ملف html بطريقة يدوية ولم يتم توليده بواسطة الجافا سكربت. مع العلم أن كل ما يوجد بين العناصر يعتبره DOM عقدة نص text node، ولكن يعمل تبويب Elements الموجودة في أدوات التطوير الخاصة بالمتصفح DevTools بحذف الفراغات البيضاء من النص وبالتالي لا يعتبرها عقد نصوص. مثلًا إذا حاولنا تغيير لون الخلفية للابن الأول باستخدام خاصية firstChild سنفشل لأن أول عقدة هي نص. > ul.firstChild.style.background = 'yellow'; وستكون رسالة الخطأ هي Output > Uncaught TypeError: Cannot set property 'background' of undefined لذلك فإن الخصائص children، firstElementChild، و lastElementChild وجدت لكي نستطيع الحصول على العقدة من نوع عنصر element node فقط، أي في حال كتابة ul.children سنحصل على مصفوفة تحوي 3 عقد من نوع عنصر هي li. باستخدام الخاصية firstElementChild سنقوم بتغيير لون الخلفية للعنصر الأول li التي هي ابن للعقدة ul > ul.firstElementChild.style.background = 'yellow'; وعند تنفيذ هذه التعليمة سيتغير لون الخلفية عند معالجة شجرة DOM تظهر بعض الخصائص المفيدة للعناصر element properties. في تطبيقات الويب التي تم توليدها باستخدام الجافا سكربت الأكثر احتمالًا، هو استخدام الخصائص التي تختار جميع العقد، وفي هذه الحالة الخطوط والمساحات البيضاء لن تكون موجودة. بالعودة للمثال السابق يمكن استخدام حلقة for ... of من أجل تطبيق اللون الأصفر في الخلف على جميع العناصر الأبناء children. for (let element of ul.children) { element.style.background = 'yellow'; } بتطبيق هذه التعليمات سيظهر اللون الأصفر في خلفية جميع الأبناء وبما أن العنصر p يمتلك نوعي عقد من الأبناء وهي العناصر والنصوص في داخله، فإنه يمكن للخاصية childNodes أن تكون مفيدة في الوصول إلى لهذه المعلومات. for (let element of p.childNodes) { console.log(element); } حيث سيكون الخرج عبارة عن ثلاثة أسطر كل سطر يمثل ابن للعقدة p Output "The world's leading source on " <strong>shark</strong> " related information." إن خاصيتي children و childNode تعيد مصفوفة ولكنها لاتمتلك جميع خصائص وتوابع المصفوفات، ولكنها تظهر بشكل مشابه للمصفوفات في الجافا سكربت. ولكن يمكن الوصول إلى العقد من خلال رقم الدليل index أو من خلال خاصية طول المصفوفة length document.body.children[3].lastElementChild.style.background = 'fuchsia'; التعليمة السابقة هي عبارة عن إضافة اللون الفوشي لخلفية العقدة الابن الأخيرة (li) التابعة للعنصر الابن رقم 3 (ul) والتي هي بدورها ابن للعقدة body. إن استخدام خصائص الأب والابن تساعد في الوصول إلى أي عقدة ضمن شجرة DOM. العقد الشقيقة Sibling Nodes العقد الشقيقة sibling of nodes هي العقد الموجودة في نفس المستوى في شجرة DOM ويمكن أن تكون من أنواع مختلفة من العقد سواء عنصر، نص أو تعليق Gets Property Previous Sibling Node previousSibling Next Sibling Node nextSibling Previous Sibling Element Node previousElementSibling Next Sibling Element Node nextElementSibling إن خصائص العقد الشقيقة تعمل بنفس الطريقة التي تعمل فيها عند العقد الأبناء، حيث نجد مجموعة من الخصائص للتنقل بين العقد بكل أنواعها وهي previousSibling و nextSibling ومجموعة أخرى تساعد على التنقل بين العقد من نوع عناصر فقط وهي previousElementSibling و nextElementSibling. في مثالنا nodes.html سنختار الابن الأوسط للعنصر ul const tiger = ul.children[1]; وبما أننا بنينا DOM الخاص بنا من الصفر وليس بناء على تطبيق ويب باستخدام الجافا سكربت فإننا نحتاج لاستخدام خصائص العناصر الأشقاء للوصول إلى عقد العناصر السابقة واللاحقة مع العلم أن الفراغات البيضاء موجودة في DOM كعقد من نوع text tiger.nextElementSibling.style.background = 'coral'; tiger.previousElementSibling.style.background = 'aquamarine'; وبتنفيذ التعليمات السابقة سنلاحظ أن لون الخلفية عند Hammerhead وGreat White قد تغير ملخص تحدثنا في هذه المقالة عن كيفية الوصول إلى جميع عناصر مستند HTML وكيفية التحرك بين العقد في شجرة DOM من خلال خصائص العقد الأب، الابن، والشقيقة. وبهذا ستكون قادرًا على الوصول إلى أي عقدة ضمن شجرة DOM في أي موقع ويب. هذه المقالة ترجمة للمقالة How To Traverse the DOM للكاتبة Tania Rascia
-
مقدمة عندما نتكلم عن DOM فإننا نتكلم عن بنية شجرية تحوي العديد من الكائنات objects التي تدعى العقد nodes. لقد تكلمنا في المقال السابق (مدخل إلى DOM) عن تعريف DOM وكيفية الوصول إلى الكائن document وطريقة تعديل خصائصه من خلال console بالإضافة إلى إيضاح الفرق بين ماهية HTML و DOM. في هذه المقالة فسنتحدث عن مصطلحات HTML التي تعتبر قواعد أساسية للتنسيق برمجة موقع الويب الذي يتضمن html، الجافا سكربت ومفاهيم DOM. حيث سنتعلم من خلال هذا المقال كيفية بناء شجرة DOM و كيفية إضافة العقد الخاصة بها Nodes حسب نوعها، أخيرًا سنقوم ببناء برنامج باستخدام الجافا سكربت بدلًا من استخدام console وذلك لتعديل DOM. مصطلحات HTML إن فهم مصطلحات Html و الجافا سكربت تعتبر أمرًا ضروريًا لفهم كيفية العمل مع DOM. ولذلك سنقوم بمراجعة بعض مصطلحات html سنبدأ أولًا بالنظر إلى عنصر html التالي <a href="index.html">Home</a> إن عنصر html السابق a هو عبارة عن عنصر يُضاف لربط صفحة الويب الخاصة بنا بصفحة أو محتوى آخر، في هذا المثال لتحقيق إمكانية الوصول إلى صفحة index.html a هو الوسم tag href هو الصفة attribute index.html هو قيمة الصف home هو النص الذي نضيفه للعنصر كل ماهو موجود بين علامتي <> هو عبارة عن عنصر html سنعمل على صفحة index.html التي بنيناها بالمقالة السابقة <!DOCTYPE html> <html lang="en"> <head> <title>Learning the DOM</title> </head> <body> <h1>Document Object Model</h1> </body> </html> إن الطريقة الأسهل للوصول إلى عنصر عبر الجافا سكربت هي استخدام الصفة id ويمكننا ربط أي عنصر في html بـ id خاص به، فمثلًا سنقوم بإضافة id خاص بالعنصر a في المثال السابق وقيمتها هي nav ... <body> <h1>Document Object Model</h1> <a id="nav" href="index.html">Home</a> </body> ... الآن سنقوم بتحميل إو إعادة تحميل صفحة index.html للتأكد بتعديل DOM الخاص بصفحتنا التي عدلناها. سنستخدم التابع getElementById() للوصول إلى العنصر كاملًا من خلال كتابة التعليمة التالية في تبويبة console document.getElementById('nav'); عندها سيكون الخرج في تبويبة console بالشكل <a id="nav" href="index.html">Home</a> حيث نلاحظ أننا حصلنا على العنصر كاملآ من خلال استخدام تابع getElementById() ، ولكن يمكننا إسناد عنصر html إلى متحول variable للوصول إليه بشكل أسرع من استخدام التابع السابق. let navLink = document.getElementById('nav'); وهكذا أصبح navLink هو عبارة عن متحول يحوي على عنصر a الخاص وبالتالي يمكننا تعديل الصفات attribute التي يحويها وقيمها بسهولة، فمثلًا يمكننا تعديل قيمة الصفة href بالشكل التالي navLink.href = 'https://www.wikipedia.org'; كما يمكننا تعديل المحتوى النصي للمتحول من خلال إعادة إسناد قيمة نصية جديدة للخاصية textContent navLink.textContent = 'Navigate to Wikipedia'; الآن عندما سنستعرض العنصر a إما من خلال console أو من خلال تمرير الفأة على الوسم a سنشاهد كيف تم تعديل العنصر navLink; وسيكون الخرج هو Output <a id="nav" href="https://www.wikipedia.org/">Navigate to Wikipedia</a> كما سنشاهد أن هذه سينعكس على شكل واجهة الموقع ولكن عند تحديث الصفحة سيتم التراجع عن كل التعديلات التي قمنا بها. وبنهاية هذه الفقرة يجب أن نكون قد تعلمنا كيفية التعامل مع توابع document وطريقة الوصول إليها وإسناد العنصر إلى متحول وتغيير الصفات الخاصة بالعنصر من خلال استخدام المتحول المُسند إليه. شجرة DOM والعقد المكونة لها تعرف جميع العناصر التي تنتمي لل DOM كعقد nodes ولها أنواع متعددة لكن يوجد 3 أنواع هي الأكثر استخداما عقد العنصر Element nodes عقد النص Text nodes عقد التعليقات Comment nodes عندما يتم تمثيل عنصر html كعنصر داخل DOM سنشير له بأنه عقدة العنصر element nodes، وأي نص خارج هذا العنصر سنشير له بأنه عقدة النص text node، وأخيرًا التعليقات التي نكتبها داخل html سنشي لها في DOM بأنها عقد التعليقات Comment node. بالإضافة إلى هذه الأنواع الثلاثة تعتبر document بأنها عقدة قائمة بذاتها تسمى document node والتي تعتبر بأنها جذر العقد الأخرى كافة التي يحتويها ملف DOM. إذًا يتضمن DOM بنية شجرية تحوي العديد من القعد المتداخلة والتي غالبًا ما يشار لها بشجرة DOM أو DOM tree وتشبه بنيتها إلى حد كبير بنية شجرة العائلة التي تضمن الآباء، الأبناء والأشقاء وتلك هي الحال أيضًا بالنسبة للعلاقة بين عقد شجرة DOM. لنرى هذه العلاقات بين عقد شجرة DOM بشكل عملي سنقوم ببناء ملف nodes.html والتي يحوي على أنواع العقد الثلات: عقد النص text، التعليقات comment والعناصر element <!DOCTYPE html> <html> <head> <title>Learning About Nodes</title> </head> <body> <h1>An element node</h1> <!-- a comment node --> A text node. </body> </html> عقدة العنصر html هي العقدة الأب parent node أما عقد العناصر head و body فهي عقد أشقاء وأبناء للقعدة html. أما عقدة body فهي تملك 3 عقد أبناء وهي جميعها عقد أخوة، مع العلم أن نوع العقدة لايؤثر على مستواها. ملاحظة: {.underline} عند العمل مع DOM المولد من قبل html فإن كثرة الفراغات المضافة إلى تعليمات html سيؤدي إلى خلق العديد من عقد النصوص text nodes الفارغة. والتي لاتظهر في تبويبة "Elements" الموجودة في DevTools في chrome. تمييز نوع العقدة كل العقد في المستند document تملك نوع مخصص والتي يمكن الحصول عليها من خلال الخاصية nodeType. قامت شبكة مطوري Mozilla بتحديث قائمتها لـ ثوابت نوع العقدة. وسنذكر هنا قائمة بأكثر أنواع العقد الشائعة وكيفية التعامل معها في هذه المقال نوع العقدة القيمة مثال عقدة العنصر ELEMENT_NODE 1 العنصر < body > عقدة النص TEXT_NODE 3 أي نص ليس جزء من العنصر عقدة التعليق COMMENT_NODE 8 < !--- html comment --- > في تبويبة "Elemnets" الموجودة في أدوات المطوريين قد تلاحظ أنه في حال قمت بالضغط على أي عنصر في DOM وتم تظليله فستلاحظ أنه سيظهر بجانبه القيمة ==$0 ولذلك فإن أحد الطرق السهلة للوصول إلى العنصر النشيط من خلال أدوات التطوير هو كتابة $0. في تبويبة "console" الخاصة بالصفحة nodes.html سنقوم بالضغط على أول عنصر موجود في body وهو h1 الآن سنحصل على نوع العقدة المفعلة حاليًا وذلك من خلال الوصول إليها عبر خاصية nodeType لذلك ندخل التعليمة التالية في console $0.nodeType; وسيكون عندها الخرج هو 1 بالشكل التالي Output 1 وجدنا أن الخرج هو 1 المرتبط بالعنصر المختار h1 والذي يشير إلى نوع ELEMENT_NODE. وبتجربة التعليمة السابقة على نص أو تعليق في ملف HTML فإننا سنحصل على الخرج 3 و 8 بالترتيب. كما يمكننا الحصول على نوع العقدة من دون الحاجة إلى تظليل العنصر في DOM قبل معرفته وذلك من خلال التعليمة document.body.nodeType; حيث سيكون الخرج هو 1 أيضًا. بالإضافة إلى الخاصية nodeType، يمكن استخدام الخاصية nodeValue للحصول على القيمة أو التعليق في عقد النص والتعليق، كما أن الخاصية nodeName تساعدنا في الحصول على اسم الوسم الخاص بالعنصر. تعديل DOM باستخدام الأحداث تعرفنا إلى الآن إلى كيفية تعديل DOM من خلال console ولكن هذا التعديل مؤقت فبمجرد تحديث صفحة الويب ستُلغى هذه التعديلات. في المقالة مدخل إلى DOM استخدمنا console من أجل تعديل لون خلفية الشاشة، الآن يمكن الجمع بين ما تعلمناه سابقًا مع المعلومات في هذه المقالة لبناء زر تفاعلي عندما يتم الضغط عليه. بالعودة إلى ملف index.html سنضيف زر من خلال العنصر button ونضيف له id كما أننا سنضيف رابط جديد لملف جافا سكربت موجود في المجلد js/scripts.js <!DOCTYPE html> <html lang="en"> <head> <title>Learning the DOM</title> </head> <body> <h1>Document Object Model</h1> <button id="changeBackground">Change Background Color</button> <script src="scripts.js"></script> </body> </html> يعرف الحدث event في الجافا سكربت بأنه الفعل الذي يقوم به المستخدم كمثال عندما يقوم المستخدم بتمرير الفأرة فوق عنصر، الضغط على عنصر، أو الضغط على زر معين في لوحة المفاتيح. في هذا المثال نريد للزر الذي بنيناه أن ينتظر ويكون مستعدًا لتلقي الفعل من قبل المستخدم من خلال استخدام event listener للزر. أولًا نقوم ببناء ملف scripts.js ونحفظه في مجلد js ثم سنضيف تعليمات الجافا سكربت التي تقوم بإيجاد العنصر وإسناده إلى متحول let button = document.getElementById('changeBackground'); ثم نضيف التابع addEventListener الذي يعمل على جعل الزر ينتظر حتى يتم الضغط عليه من أجل تنفيذ التابع function الذي يقوم بالفعل في هذا المثال الفعل هو تغيير لون الخلفية. ... button.addEventListener('click', () => { // action will go here }); حيث سنكتب داخل التابع function التعليمة الخاصة بتغيير لون الخلفية إلى لون fuchsia ... document.body.style.backgroundColor = 'fuchsia'; حيث ستكون التعليمات بالترتيب كما يلي let button = document.getElementById('changeBackground'); button.addEventListener('click', () => { document.body.style.backgroundColor = 'fuchsia'; }); وحالما نقوم بحفظ هذا الملف وإعادة تحديث ملف index.html ثم الضغط على زر الموجود في الشاشة فإن الحدث event الذي صممناه لتغير لون الخلفية سوف يعمل مباشرة. ملخص في هذه المقالة قمنا باستذكار المصطلحات التي تساعدنا على فهم وتعديل DOM كما تعرفنا على البنية الشجرية لل DOM وأنواع العقد المستخدمة في إنشائها مثل عناصر html، النصوص، والتعليقات. كما قمنا في النهاية بإنشاء سكربت يسمح للمستخدم بتعديل شكل الموقع من دون الحاجة إلى إدخال التعليمات يدويًا إلى تبويبة console. هذه المقالة هي ترجمة للمقالة Understanding the DOM Tree and Nodes وكاتبتها Tania Rascia
-
مقدمة إن نموذج تمثيل المستند ك جسم أو كائن Document Object Model والذي يشار له اختصارا ب DOM يعتبر أحد الطرق الأساسية في بناء مواقع متفاعلة فيما بينها، حيث تُعتبر هذه الطريقة واجهة تسمح للغات البرمجة بمعالجة محتوى المواقع وهيكليتها. أحد اللغات المرتبطة بالـ DOM في المتصفحات هي الجافا سكربت وذلك لأنها لغة برمجة تعمل في مواقع الويب من جهة المستخدم client-side. حيث يمكن الاستفادة من مفاهيم DOM في بناء مواقع ويب متفاعلة مثل إضافة عرض صور للموقع من خلال شاشة عرض بطريقة تدوير الصور، أو إظهار لافتة تنبّه المستخدم إلى وقوعه في خطأ عند محاولته إدخال معلومات ناقصة ضمن الحقول المطلوبة، أو التبديل في قوائم التصفح الخاصة بالموقع ويتم تنفيذ هذه العمليات الثلاث من خلال لغة الجافاسكربت ولكن من خلال معالجة DOM. في هذا المقال سنتعلم ماهو DOM وكيفية التعامل معه من خلال تعريف كائن object هو document والتمييز بين أسطر التعليمات المكتوبة بلغة HTML وبين DOM. ملاحظة: لاتعتبر DOM لغة برمجة كما الحال في لغات البرمجة الاعتيادية كالجافا أو الجافا سكربت... حيث يتم تنفيذ وتطبيق مفاهيمها من خلال لغات برمجة أخرى وهنا سنستخدم لغة الجافا سكربت. المتطلبات الأساسية لفهم كيفية تطبيق مفاهم DOM بشكل فعال وتنفيذه في تطبيقات الويب من المهم أن تكون لديك معرفة سابقة بـ HTML و CSS بالإضافة إلى معرفة القواعد الأساسية حول طريقة كتابة تعليمات الجافا سكربت وهيكليتها. ما هي DOM للبدء ببناء أي موقع ويب مهما كان بسيط سنبدأ أولًا بإنشاء مستند HTML، حيث يقوم برنامج المتصفح الذي يستخدمه الزائر لمشاهدة الموقع بتفسير تعليمات html و CSS وتحويلهم إلى المحتوى، الهيكلية والتصميم الذي نشاهده أمامنا في الصفحة النهائية. بالإضافة لذلك يقوم المتصفح ببناء تمثيل للمستند يُعرف بأنه نموذج كائن المستند Document Object Model ويسمح هذا النموذج للجافا سكربت بالوصول إلى المحتوى المُعبّر عنه كنص text أو كعناصر element ضمن المستند ورؤيتها على شكل أغراض objects. ولسهولة فهم هذا المفهوم الجديد سنتعرف عليه من خلال مثال عملي يبدأ بتطوير موقع بسيط وذلك بإنشاء ملف جديد يسمى index.html ثم حفظه ضمن مجلد المشروع project directory <!DOCTYPE html> <html lang="en"> <head> <title>Learning the DOM</title> </head> <body> <h1>Document Object Model</h1> </body> </html> إن أسطر التعليمات السابقة هي أساسيات HTML لبناء موقع ويب. سنستخدم هنا متصفح Chrome ولكن يمكنك الوصول إلى نفس الخرج من خلال المتصفحات الحديثة الأخرى. أولًا سنقوم بفتح ملف index.html الذي قمنا ببنائه من خلال متصفح chrome عندها سيظهر أمامك صفحة ويب فارغة وتحوي فقط عنوان هو "Document Object Model" ومن خلال النقر بالزر اليميني في أي مكان بالصفحة ستظهر لدينا قائمة نختار منها "Inspect" والتي ستفتح لنا على جانب الصفحة أدوات التطوير، سيظهر لدينا DOM تحت تبويب "Elements" في هذه الحالة نلاحظ أن DOM الذي قمنا بإظهاره مطابق تمامًا لتعليمات html التي كتبناها سابقًا لبناء الموقع، وعند تمرير السهم فوق العنصر سنلاحظ أن مايقابله من صفحة الويب المفتوحة أمامنا ستُظلّل باللون الأزرق، كما أن السهم الموجود على جانب كل عنصر يسمح بتوسيع العناصر المكتوبة بالداخل. كائن المستند Document كائن المستند document هو عبارة عن كائن مُدمج يملك عدد من الخصائص properties والطرق methods التي يمكن استخدامها للوصول إلى الموقع وتعديله. لفهم كيفية عمل DOM يجب أولًا فهم كيف تعمل الكائنات objects في الجافا سكريبت، ويمكن ذلك من خلال الإطلاع على مقالة برمجة الكائنات Objects في جافاسكريبت في أدوات التطوير التي أظهرناها سابقًا بجانب ملف index.html ننتقل إلى التبويبة "Console" ونكتب document داخل console ثم نضغظ زر Enter حيث نلاحظ أن الخرج هو نفسه الخرج الموجود في تبويبة "Elements" document; Output #document <!DOCTYPE html> <html lang="en"> <head> <title>Learning the DOM</title> </head> <body> <h1>Document Object Model</h1> </body> إن كتابة المستند document أو العمل مباشرة في تبويب console ليست أمرًا شائعًا نقوم به خارج debugging ولكنه يساعد في ترسيخ مفهوم كائن المستند document وكيفية تعديله ومعرفه العناصر الأخرى المدرجة ضمنه. ماهو الفرق بين DOM وتعليمات HTML حاليًا في المثال السابق الذي طرحناه تبدو لنا DOM كما لو أنها تعليمات HTML نفسها ولكن يوجد أمثلة يقوم فيها المتصفح بتوليد DOM مختلف عن تعليمات HTML مثل: تعديل DOM من خلال تعليمات الجافا سكربت والتي يتم تنفيذها في جانب المستخدم. قيام المتصفح بتصحيح الأخطاء الموجودة في التعليمات HTML المكتوبة سابقًا بشكل تلقائي. سنقوم الآن باكتشاف آلية تعديل DOM من خلال استخدام الجافا سكربت التي تُطبق في جانب المستخدم. أولًا ندخل في console التعليمة document.body; عندها سيكون الخرج: Output <body> <h1>Document Object Model</h1> </body> سنلاحظ هنا أن document هو عبارة عن كائن أما body فهو خاصية property لهذا الكائن لذلك من للوصول إلى هذه الخاصية نكتب اسم الكائن ثم نقطة ثم الخاصية document.body عندها سيكون الخرج هو جميع عناصر body الموجودة في المستند document. نستطيع من خلال console تغيير بعض الصفات الظاهرة في الكائن body في هذا الموقع فمثلًا يمكن تغيير صفات style الخاصية بالـ body مثل تغيير لون الخلفية ويتم ذلك من خلال التعليمة document.body.style.backgroundColor = 'fuchsia'; بعد كتابة هذه التعليمة وإدخالها سنلحظ تغير لون خلفية الموقع للون الفوشيا: بالانتقال إلى تبويبة "Elements" أو من خلال كتابة تعليمة document.body في console مرة أخرى سنلاحظ أن DOM قد تغير Output <body style="background-color: fuchsia;"> <h1>Document Object Model</h1> </body> ملاحظة: رأينا في المثال السابق عند تغيير لون الخلفية أن خاصية لون الخلفية في CSS والذي يكتب بالشكل background-color تمت كتابته بالشكل backgroundColor في الجافا سكربت وبنفس هذا الترتيب فإن أي خاصية في CSS يمكن كتابتها في الجافا سكربت بطريقة camelCase والتي تعني أن الحرف الأول من الكلمة الأولى صغير بينما الحرف الأول من الكلمة الثانية كبير. عند الضغط بالزر اليميني على الصفحة واختيار "View Page Source" سنلاحظ أن التعليمات الظاهرة لاتحوي على الصفة attribute الجديدة للون الخلفية الذي أضفناه من خلال الجافا سكربت وذلك لأن التعليمات الأساسية الموجودة في جانب الخادم Server التي تم بناء الموقع من خلالها لاتتأثر بأي تعليمات كتبت في جانب المستخدم بلغة الجافا سكربت، ولذلك في حال قمنا بتحديث الصفحة فإن الخاصية التي أضفناها ستُلغى. بالنسبة للحالة الأخرى التي ذكرناها عن اختلاف DOM و تعليمات HTML وهي عندما يتم تصحيح الأخطاء المكتوبة في التعليمات source code آليًا من قبل المتصفح، كمثال شائع فإن الوسم tbody يجب أن يرافق الوسم table دائمًا ولكن غالبًا لايقوم المطورون بتضمين هذه الوسوم بالشكل الصحيح ضمن HTML وعندها يقوم المتصفح تلقائيًا بتصيح هذا الخطأ وإضافة الوسم tbody وبالتالي يتم تغيير DOM. بالإضافة إلى ذلك دائمًا يصحح DOM الوسوم التي لايتم إغلاقها. ملخص عرضنا في هذه المقالة تعريف للـ DOM واستطعنا الوصول إلى كائن المستند document باستخدام الجافا سكربت وconsole من أجل تغيير خصائص تابعة للكائن document، كما تعرفنا على الفرق بين HTML و DOM. أما في المقالة التالية سنقوم باستعراض معلومات مهمة عن HTML وشجرة DOM وكيفية التعامل مع عقدها Nodes وماهي الأنواع الشائعة منها. ترجمة بتصرف للمقال Introduction to DOM وكاتبتها Tania Rascia