المحتوى عن 'جافاسكربت في المتصفح'.



مزيد من الخيارات

  • ابحث بالكلمات المفتاحية

    أضف وسومًا وافصل بينها بفواصل ","
  • ابحث باسم الكاتب

نوع المُحتوى


التصنيفات

  • الإدارة والقيادة
  • التخطيط وسير العمل
  • التمويل
  • فريق العمل
  • دراسة حالات
  • التعامل مع العملاء
  • التعهيد الخارجي
  • السلوك التنظيمي في المؤسسات
  • عالم الأعمال
  • التجارة والتجارة الإلكترونية
  • نصائح وإرشادات
  • مقالات ريادة أعمال عامة

التصنيفات

  • PHP
    • Laravel
    • ووردبريس
  • جافاسكريبت
    • Node.js
    • React
    • AngularJS
    • Vue.js
    • jQuery
    • Cordova
  • HTML
    • HTML5
  • CSS
  • SQL
  • لغة C#‎
  • لغة C++‎
  • لغة C
  • بايثون
    • Flask
    • Django
  • لغة روبي
    • Sass
    • إطار عمل Bootstrap
    • إطار العمل Ruby on Rails
  • لغة Go
  • لغة جافا
  • لغة Kotlin
  • برمجة أندرويد
  • لغة Swift
  • لغة R
  • لغة TypeScript
  • ‎.NET
    • ASP.NET
  • الذكاء الاصطناعي
  • صناعة الألعاب
    • Unity3D
    • منصة Xamarin
  • سير العمل
    • Git
  • سهولة الوصول
  • مقالات برمجة عامة

التصنيفات

  • تجربة المستخدم
  • الرسوميات
    • إنكسكيب
    • أدوبي إليستريتور
    • كوريل درو
  • التصميم الجرافيكي
    • أدوبي فوتوشوب
    • أدوبي إن ديزاين
    • جيمب
  • التصميم ثلاثي الأبعاد
    • 3Ds Max
    • Blender
  • نصائح وإرشادات
  • مقالات تصميم عامة

التصنيفات

  • خوادم
    • الويب HTTP
    • قواعد البيانات
    • البريد الإلكتروني
    • DNS
    • Samba
  • الحوسبة السّحابية
    • Docker
  • إدارة الإعدادات والنّشر
    • Chef
    • Puppet
    • Ansible
  • لينكس
    • ريدهات (Red Hat)
  • خواديم ويندوز
  • FreeBSD
  • حماية
    • الجدران النارية
    • VPN
    • SSH
  • شبكات
    • سيسكو (Cisco)
  • مقالات DevOps عامة

التصنيفات

  • التسويق بالأداء
    • أدوات تحليل الزوار
  • تهيئة محركات البحث SEO
  • الشبكات الاجتماعية
  • التسويق بالبريد الالكتروني
  • التسويق الضمني
  • التسويق بالرسائل النصية القصيرة
  • استسراع النمو
  • المبيعات
  • تجارب ونصائح
  • مبادئ علم التسويق

التصنيفات

  • إدارة مالية
  • الإنتاجية
  • تجارب
  • مشاريع جانبية
  • التعامل مع العملاء
  • الحفاظ على الصحة
  • التسويق الذاتي
  • مقالات عمل حر عامة

التصنيفات

  • الإنتاجية وسير العمل
    • مايكروسوفت أوفيس
    • ليبر أوفيس
    • جوجل درايف
    • شيربوينت
    • Evernote
    • Trello
  • تطبيقات الويب
    • ووردبريس
    • ماجنتو
  • أندرويد
  • iOS
  • macOS
  • ويندوز
  • الترجمة بمساعدة الحاسوب
    • omegaT
    • memoQ
  • أساسيات استعمال الحاسوب
  • مقالات عامة

التصنيفات

  • شهادات سيسكو
    • CCNA
  • شهادات مايكروسوفت
  • شهادات Amazon Web Services
  • شهادات ريدهات
    • RHCSA
  • شهادات CompTIA
  • مقالات عامة

أسئلة وأجوبة

  • الأقسام
    • أسئلة ريادة الأعمال
    • أسئلة العمل الحر
    • أسئلة التسويق والمبيعات
    • أسئلة البرمجة
    • أسئلة التصميم
    • أسئلة DevOps
    • أسئلة البرامج والتطبيقات
    • أسئلة الشهادات المتخصصة

التصنيفات

  • ريادة الأعمال
  • العمل الحر
  • التسويق والمبيعات
  • البرمجة
  • التصميم
  • DevOps

تمّ العثور على 14 نتائج

  1. يمكّننا انتشار اﻷحداث نحو اﻷسفل واﻷعلى من تطبيق أحد أقوى أنماط معالجة الأحداث، وهو ما يُسمى تفويض الأحداث (event delegation). تتلخّص الفكرة في أنه إذا كان لدينا الكثير من العناصر التي تُعالج بطريقة متماثلة، فبدل أن نسند معالجا لكلّ منها، فإنّنا نسند معالجا واحدا إلى السلف الذي يلتقون فيه. في داخل المعالج، يمكن أن نتعرّف على مكان وقوع الحدث من خلال event.target، ثم نعالجه. لنرى مثالا على ذلك -- مخطط باكوا الذي يعكس الفلسفة الصينية القديمة، كما هو مبيّن من هنا. ويمكن تمثيله بواسطة HTML كالتالي: <table> <tr> <th colspan="3"><em>Bagua</em> Chart: Direction, Element, Color, Meaning</th> </tr> <tr> <td class="nw"><strong>Northwest</strong><br>Metal<br>Silver<br>Elders</td> <td class="n">...</td> <td class="ne">...</td> </tr> <tr>...2 more lines of this kind...</tr> <tr>...2 more lines of this kind...</tr> </table> See the Pen JS-p2-event-delegation -ex1 by Hsoub (@Hsoub) on CodePen. يحتوي الجدول على 9 خانات، لكنها قد تكون 99 أو 9999، لا يهمّ ذلك. مهمّتنا هي إبراز الخانة عند النقر عليها. بدل إسناد معالج onclick إلى كلّ <td> (قد يكون هناك الكثير منها)، سنسند المعالج "catch-all" إلى العنصر <table>. يستخدم المعالجُ الخاصيّة event.target للحصول على العنصر الذي نُقر عليه ثم يبرزه. إليك الشيفرة: let selectedTd; table.onclick = function(event) { let target = event.target; // أين كان النقر؟ if (target.tagName != 'TD') return; // ؟ إذًا لا يهمّنا ذلك TD ليس في highlight(target); // أبرزه }; function highlight(td) { if (selectedTd) { // أزل الإبراز الحالي إن وُجد selectedTd.classList.remove('highlight'); } selectedTd = td; selectedTd.classList.add('highlight'); // الجديدة td أبرز الـ } لا تكترث هذه الشيفرة بعدد الخانات التي في الجدول. يمكننا إضافة وإزالة الخانات <td> ديناميكيا في أي وقت، وستظل وظيفة الإبراز تعمل. لكن تبقى هناك نقيصة. قد لا يقع النقر على العنصر <td> بعينه ولكن على عنصر آخر بداخله. ففي حالتنا هذه، لو ألقينا نظرة داخل HTML، سنلاحظ أوسمة مدرجة داخل <td>، مثل <strong>: <td> <strong>Northwest</strong> ... </td> فمن الطبيعي أنه لو وقع النقر على <strong>، فسيصير هو القيمة التي يحملها event.target. داخل المعالج table.onclick، ينبغي علينا أن نأخذ event.target ونتحقق إن كان النقر قد وقع داخل <td> أو لا. هذه هي الشيفرة المحسّنة: table.onclick = function(event) { let td = event.target.closest('td'); // (1) if (!td) return; // (2) if (!table.contains(td)) return; // (3) highlight(td); // (4) }; إليك بعض الإيضاحات: يعيد التابع elem.closest(selector)‎ أقرب سلف يطابق المُحدِّد selector. في حالتنا، نبحث عن أقرب <td> نصادفه صعودًا من العنصر المصدري. إذا لم يكن event.target بداخل أيّ <td>، فسيُعاد الاستدعاء مباشرة، إذ ليس هناك شيء لفعله. في حال تداخل الجداول، قد تكون event.target هي <td>، لكنّها موجودة خارج الجدول الحاليّ. فنتحقق إذًا من أنّ <td> خاصّة بجدولنا الحاليّ. وإذا كانت كذلك، نبرزها. وبذلك، تكون لدينا شيفرة سريعة وفعّالة للإبراز، لا تكترث بعدد الخانات التي في الجدول. مثال عن التفويض: الأفعال داخل الترميز (markup) هناك استعمالات أخرى لتفويض الأحداث. لنقُل أننا نود إنشاء قائمة من الأزرار: "حفظ" و "تحميل" و "بحث" وغير ذلك. ويوجد هناك كائن له التوابع save و load و search … فكيف يتم الربط بين هذه التوابع واﻷزرار؟ أوّل ما قد يتبادر إلى الذهن هو إسناد معالج إلى كلّ زر. ولكنّ هناك حلًّا أكثر أناقة. يمكننا إسناد معالج إلى القائمة بأكملها و إضافة سمات data-action للأزرار تحمل التابع الذي سيُستدعى: <button data-action="save">Click to Save</button> يقرأ المعالج السمة، وينفّذ التابع. ألقِ نظرة على المثال أدناه: <div id="menu"> <button data-action="save">Save</button> <button data-action="load">Load</button> <button data-action="search">Search</button> </div> <script> class Menu { constructor(elem) { this._elem = elem; elem.onclick = this.onClick.bind(this); // (*) } save() { alert('saving'); } load() { alert('loading'); } search() { alert('searching'); } onClick(event) { let action = event.target.dataset.action; if (action) { this[action](); } }; } new Menu(menu); </script> See the Pen JS-p2-event-delegation -ex2 by Hsoub (@Hsoub) on CodePen. يُرجى ملاحظة أن this.onClick مرتبط بـ this في (*). هذا مهمّ، لأنّه لو لم يكن كذلك فإن this الذي بداخله سيشير إلى عنصر (elem) ، وليس الكائن Menu، ولا يكون بذلك this[action]‎ هو ما نحتاجه. فما هي إذًا المزايا التي يقدّمها تفويض الأحداث هنا؟ لا نحتاج كتابة شيفرة لإسناد معالج لكل زر. بل ننشئ فقط تابعًا ونضعه في الترميز. تصبح بنية HTML مرنة، فيمكننا إضافة وإزالة أزرار بسهولة. يمكننا أيضا استخدام أصناف مثل ‎.action-save و ‎.action-load، لكن سمة مثل data-action أفضل من الناحية الدلالية، بالإضافة إلى إمكانية استخدامها في قواعد CSS. نمط "السلوك" يمكننا أيضا استخدام تفويض الأحداث لإضافة "سلوكيّات" للعناصر تصريحيًّا، بواسطة سمات وأصناف خاصة. يتألّف هذا النمط من جزأين: نضيف سمة مخصّصة إلى العنصر تعبّر عن سلوكه. يتتبع الأحداثَ معالجٌ على نطاق المستند، فإذا وقع حدث على عنصر له سمة، فإنه يقوم بالفعل المناسب. مثال عن السلوك: العداد على سبيل المثال، تضيف السمة data-counter هنا سلوك "زيادة القيمة عند النقر" للأزرار: Counter: <input type="button" value="1" data-counter> One more counter: <input type="button" value="2" data-counter> <script> document.addEventListener('click', function(event) { if (event.target.dataset.counter != undefined) { // إذا كانت السمة موجودة... event.target.value++; } }); </script> See the Pen JS-p2-event-delegation -ex3 by Hsoub (@Hsoub) on CodePen. إذا نقرنا على أحد الأزرار، فإن القيمة التي عليه ستزداد. بغضّ النظر عن هذه الأزرار، فإن المنهجية العامة المتبعة هنا مهمّة. يمكن أن يكون هناك من السمات مع data-counter بقدر ما نرغب. يمكننا إضافة سمات جديدة إلى HTML في أي وقت. باستخدام تفويض الأحداث، نكون قد "وسّعنا" HTML من خلال إضافة سمة تعبّر عن سلوك جديد. تنبيه: استخدم دائما addEventListener في المعالجات التي على مستوى المستند عند إسناد معالج حدثٍ إلى الكائن document، يجب أن نستخدم دائما addEventListener، وليس document.on<event>‎، لأن هذا الأخير سيؤدي إلى تعارضات: تستبدل المعالجاتُ الجديدة المعالجات القديمة. في المشاريع الواقعية، من الطبيعي أن تكون هناك عدة معالجات قد أُسندت بواسطة أجزاء مختلفة من الشيفرة. مثال عن السلوك: القالِب (toggler) لنرى مثالا آخر عن السلوك. يؤدي النقر على عنصرٍ له السمة data-toggle-id إلى إخفاء وإظهار العنصر الذي له ذاك الـ id. <button data-toggle-id="subscribe-mail"> Show the subscription form </button> <form id="subscribe-mail" hidden> Your mail: <input type="email"> </form> <script> document.addEventListener('click', function(event) { let id = event.target.dataset.toggleId; if (!id) return; let elem = document.getElementById(id); elem.hidden = !elem.hidden; }); </script> See the Pen JS-p2-event-delegation -ex4 by Hsoub (@Hsoub) on CodePen. لنلاحظ مرة أخرى ما قمنا به. لإضافة وظيفة القلب إلى عنصرٍ ما من الآن فصاعدًا، لا حاجة لمعرفة جافاسكربت، يكفي استخدام السمة data-toggle-id. قد يصير هذا ملائما بالفعل -- لا حاجة لكتابة جافاسكربت لكل واحد من هذه العناصر. يكفي استخدام السلوك فقط. يجعل المعالج الذي على مستوى المستند ذلك يعمل مع أي عنصر على الصفحة. يمكننا أن نجمع بين عدّة معالجات في نفس العنصر أيضا. قد يشكل نمط "السلوك" بديلا عن الأجزاء المصغرة (mini-fragments) في جافاسكربت. الملخص تفويض الأحداث رائع حقًّا! إذ يُعدّ واحدا من أنفع الأنماط المتعلّقة بأحداث DOM. كثيرا ما يُستخدم تفويض اﻷحداث لإضافة نفس المعالج لعدّة عناصر متماثلة، لكن لا يقتصر اﻷمر على ذلك. الخوارزمية: أسند معالجًا وحيدًا إلى العنصر الحاوي. في المعالج -- افحص العنصر المصدري event.target. إذا وقع الحدث داخل عنصر يهمّنا، عالج الحدث. المزايا: يبسّط التهيئة ويوفّر الذاكرة: لا حاجة لإضافة عدة معالجات. أقلّ شيفرة: عند إضافة أو إزالة عناصر، لا داعي لإضافة أو إزالة المعالجات. التعديلات على DOM: يمكننا إضافة أو إزالة العناصر جماعيا بواسطة innerHTML وما إلى ذلك. للتفويض حدود أيضا بالطبع: أولًا، يجب أن يكون الحدث منتشرًا نحو اﻷعلى. بعض الأحداث لا تنتشر نحو اﻷعلى. يجب كذلك أن لا تَستخدم المعالجاتُ التي في الأسفل event.stopPropagation()‎. ثانيًا، قد يضيف التفويض عبئًا على وحدة المعالجة المركزية (CPU)، لأنّ المعالج الذي على مستوى الحاوي يستجيب للأحداث في أي مكان في الحاوي، بغضّ النظر عن كونها مهمّة لنا أو لا. لكن العبئ عادةً طفيف، فلا نأخذه بالحسبان. التمارين أخفي الرسائل باستخدام التفويض الأهمية: 5 هناك قائمة من الرسائل لها أزرار لإزالتها [x]. اجعل الأزرار تعمل. كما هو مبيّن هنا. ملاحظة: يجب أن يكون هناك منصت واحد للأحداث على الحاوي. استخدم تفويض الأحداث. أنجز التمرين في البيئة التجريبية الحل افتح الحل في البيئة التجريبية قائمة شجرية الأهمية: 5 أنشئ شجرة يمكن فيها إظهار وإخفاء العقد الأبناء بواسطة النقر: كما هو مبيّن هنا. المتطلبات: معالج واحد للأحداث فقط (استخدم التفويض). يجب ألا يفعل النقر خارج عقدة العنوان (في مساحة فارغة) أي شيء. أنجز التمرين في البيئة التجريبية الحل ينقسم الحل إلى جزئين: ضع كلّ عقدة عنوان في الشجرة داخل <span>. بهذا يمكننا إضافة تنسيقات CSS إلى :‎hover و معالجة النقرات على النص بالضبط، لأن عُرض <span> هو نفس عُرض النص بالضبط (بخلاف ما لو كان بدونه). عيّن معالجًا على العقدة الجذر tree، وعالج النقرات على العنوانين <span> تلك. افتح الحل في البيئة التجريبية جدول قابل للترتيب الأهمية: 4 اجعل الجدول قابلًا للترتيب: يجب أن يؤدي النقر على العناصر <th> إلى ترتيبه حسب العمود الموافق. لكلّ <th> نوع معين موجود بداخل السمة، هكذا: <table id="grid"> <thead> <tr> <th data-type="number">Age</th> <th data-type="string">Name</th> </tr> </thead> <tbody> <tr> <td>5</td> <td>John</td> </tr> <tr> <td>10</td> <td>Ann</td> </tr> ... </tbody> </table> في المثال أعلاه، يحتوي العمود الأول على الأرقام، و العمود الثاني على الحروف. يجب أن تقوم دالة الترتيب بمعالجة الترتيب حسب النوع. يستلزم فقط أن يُدعم النوعان "string" و "number". يمكن مشاهدة المثال يعمل من هنا. ملاحظة: يمكن أن يكون الجدول كبيرا، بأي عدد من الأسطر والأعمدة. أنجز التمرين في البيئة التجريبية الحل افتح الحل في البيئة التجريبية سلوك التلميحات الأهمية: 5 أنشئ شفرة جافاسكربت لأجل سلوك التلميحات (tooltips). عندما يحوم مؤشر الفأرة فوق عنصر له السمة data-tooltip، فيجب أن تظهر التلميحة فوقه، وعندما يفارقه فإنها تختفي. هذا مثال لشفرة HTML مع الشرح: <button data-tooltip="the tooltip is longer than the element">Short button</button> <button data-tooltip="HTML<br>tooltip">One more button</button> يجب أن تعمل كما هنا. سنفترض في هذا التمرين أن جميع العناصر التي لها data-tooltip تحتوي على نص فقط. لا وسوم متداخلة (بعد). التفاصيل: يجب أن تكون المسافة بين العنصر والتلميحة 5px. يجب أن تكون التلميحة في منتصف العنصر، إن أمكن ذلك. يجب ألا تقطع التلميحة حوافّ النافذة. من المفترض أن تكون التلميحة فوق العنصر، فإذا كان العنصر في أعلى الصفحة ولا مكان هناك للتلميحة، فإنها تكون تحته. يُعطى محتوى التلميحة في السمة data-tooltip. يمكنها أن تحوي أي شفرة HTML. ستحتاج إلى حدثين هنا: mouseover يحصل عندما يحوم المؤشر فوق العنصر. mouseout يحصل عندما يفارق المؤشر العنصر. يُرجى استخدام تفويض الأحداث: أسند اثنين من المعالجات إلى document لتتبّع كلّ "الحومان" و "المفارقة" للعناصر التي لها data-tooltip وقم بإدارة التلميحات من هناك. بعد تطبيق السلوك، يمكن ولو لأناس غير متعودين على جافاسكربت إضافة عناصر بتلميحات. ملاحظة: يجب أن تظهر تلميحة واحدة فقط في نفس الوقت. أنجز التمرين في البيئة التجريبية الحل افتح الحل في البيئة التجريبية ترجمة -وبتصرف- للمقال Event delegation من سلسلة Browser: Document, Events, Interfaces لصاحبها Ilya Kantor
  2. لنبتدئ بمثال. المعالج التالي مُسندٌ إلى العنصر <div>، لكنّه أيضًا يشتغل عند النقر على الوسوم الداخلة تحته مثل <em> أو <code>. <div onclick="alert('The handler!')"> <em>If you click on <code>EM</code>, the handler on <code>DIV</code> runs.</em> </div> See the Pen JS-p2-bubbling-and-capturing -ex1 by Hsoub (@Hsoub) on CodePen. أليس هذا غريبًا بعض الشيء؟ لماذا يشتغل المعالج المُسنَد إلى <div> إذا كان النقر في الواقع على <em>؟ انتشار اﻷحداث نحو اﻷعلى مبدأ الانتشار نحو اﻷعلى (bubbling) بسيط. عندما يقع حدثٌ على عنصرٍ ما، فإنّه يُشغّل المعالجات المسندة إليه، ثم المسندة إلى أبيه، وهكذا صعودًا إلى المعالجات المسندة إلى أسلافه الآخرين. لنفترض أنّ لدينا ثلاثة عناصر متداخلة P < DIV < FORM ، مع معالجٍ لكلّ منها: <style> body * { margin: 10px; border: 1px solid blue; } </style> <form onclick="alert('form')">FORM <div onclick="alert('div')">DIV <p onclick="alert('p')">P</p> </div> </form> See the Pen JS-p2-bubbling-and-capturing -ex2 by Hsoub (@Hsoub) on CodePen. يؤدّي النّقر على العنصر <p> الذي بالداخل أوّلًا إلى تشغيل onclick : المُسند إلى <p> ذاك. ثم المُسند إلى <div> الذي خارجه. ثم المُسند إلى <form> الذي خارجه. وهكذا صعودًا إلى الكائن document. فإذا نقرنا على <p> ، سنرى ثلاثة تنبيهات متتالية: form <- div <- p. تُعرف على هذه العمليّة بالانتشار نحو اﻷعلى (bubbling)، لأنّ الأحداث تنتشر من العنصر الداخلي صعودًا عبر آبائه كالفقّاعة في الماء. تنبيه: تنتشر مُعظم اﻷحداث نحو اﻷعلى ينبغي التنبه في هذه الجملة إلى كلمة "مُعظم". على سبيل المثال، لا ينتشر الحدث focus نحو اﻷعلى. وسنرى أيضا أمثلة أخرى. لكنّها تبقى استثناءً عوض القاعدة، فمعظم اﻷحداث تنتشر نحو اﻷعلى. event.target يمكن للمعالج المسند إلى عنصرٍ أبٍ أن يتحصّل دائمًا على تفاصيل مكان وقوع الحدث. يُسمّى العنصر اﻷدنى الذي نشأ عنه الحدث بالعنصر "الهدف"، ويمكن الوصول إليه بواسطة event.target. لاحظ الاختلاف الذي بينه و this (الذي هو نفس event.currentTarget): event.target -- هو العنصر "الهدف" الذي أنشأ الحدث، ولا يتغيّر خلال عمليّة الانتشار نحو اﻷعلى. this -- هو العنصر "الحاليّ"، أي الذي أُسنِد إليه المعالجُ الذي يشتغل حاليّا. على سبيل المثال، إذا كان لدينا معالجٌ وحيدٌ form.onclick مُسندٌ إلى النموذج <form>، فإنّه يمكنه "التقاط" جميع النقرات داخل النموذج. أيّا كان مكان وقوعها، فإنّها تنتشر نحو اﻷعلى إلى <form> وتشغّل المعالج. في المعالج form.onclick: this (الذي هو نفس event.currentTarget) هو العنصر <form>، لأنّ المعالج المُشتغل مسندٌ إليه. event.target هو العنصر الذي نُقر عليه داخل النموذج. يمكنك رؤية ذلك من هنا ، من خلال النقر على مختلف العناصر لإظهار event.target و this في كلّ حالة. قد يكون event.target هو نفسه this، كما لو نقرنا هنا على العنصر <form> مباشرة. إيقاف الانتشار نحو اﻷعلى ينطلق الحدث عند انتشاره نحو اﻷعلى من العنصر الهدف مباشرة. ويواصل الانتشار عادةً إلى أن يصل إلى <html>، ومن ثَمّ إلى الكائن document، بل إنّ بعض اﻷحداث قد تصل إلى window، ويشغّل جميع المعالجات في طريقه إلى هناك. لكن قد يقرّر أحد المعالجات أن الحدث قد تمّت معالجته ويوقف بذلك عمليّة الانتشار. يتوقّف الانتشار نحو اﻷعلى بواسطة التابع event.stopPropagation()‎. على سبيل المثال، لا يشتغل المعالج body.onclick هنا عند النقر على <button>: <body onclick="alert(`the bubbling doesn't reach here`)"> <button onclick="event.stopPropagation()">Click me</button> </body> See the Pen JS-p2-bubbling-and-capturing -ex3 by Hsoub (@Hsoub) on CodePen. ملاحظة: ()event.stopImmediatePropagation إذا أُسنِد إلى عنصرٍ ما عدّةُ معالجات لنفس الحدث، فحتىّ لو أوقف أحدها الانتشار نحو اﻷعلى، ستشتغل المعالجات الأخرى. بعبارة أخرى، يوقِف التابع event.stopPropagation()‎‎ الانتشار نحو اﻷعلى، لكن ستشتغل بقيّة المعالجات المسندة إلى العنصر الحاليّ. لإيقاف الانتشار نحو اﻷعلى، ومنع اشتغال بقيّة المعالجات المُسندة إلى العنصر الحاليّ أيضا، يوجد هناك تابع لذلك event.stopImmediatePropagation()‎‎ لا يشتغل بعده معالج. تنبيه: لا توقف الانتشار نحو اﻷعلى دون الحاجة لذلك! الانتشار نحو اﻷعلى أمرٌ ملائم. لا توقفه دون سبب وجيه، يكون واضحًا ومُمحصًّا هندسيًّا. قد يُحدث التابع event.stopPropagation()‎‎ أحيانًا مزالق تتسبّب لاحقًا في مشاكل. على سبيل المثال: ننشئ قائمة متداخلة. تعالج كلُّ قائمة داخليّة النقرات التي على عناصرها، وتستدعيstopPropagation لتفادي تفعيل القائمة الخارجيّة. نقرّر بعدها أن نلتقط جميع النقرات على النافذة، لتتبع سلوك المستخدمين (أين ينقر الناس). تقوم بعض أنظمة التحليل بذلك، وعادةً ما تستخدم الشيفرةُ التابعَ document.addEventListener('click'…)‎‎ لالتقاط جميع النقرات. لن يعمل نظام التحليل على المساحة التي أوقف فيها انتشار النقرات نحو اﻷعلى بواسطة stopPropagation. فيكون بذلك لدينا "منطقة ميّتة" للأسف. لا توجد في العادة حاجة حقيقيّة لإيقاف الانتشار نحو اﻷعلى. فالمهام التي تبدو أنّها تتطلب ذلك يمكن حلّها بوسائل أخرى. من بين هذه الوسائل، استخدام اﻷحداث المخصّصة (custom events) التي سنتناولها لاحقا. يمكننا أيضا كتابة بياناتٍ على الكائن event في معالج وقراءتها في معالج آخر، ليتسنى بذلك تمرير معلومات إلى المعالجات المسندة إلى الآباء حول المعالجة التي تمت في اﻷسفل. الانتشار نحو الأسفل توجد هناك مرحلة أخرى لمعالجة اﻷحداث يُطلق عليها "الانتشار نحو اﻷسفل" (capturing). من النادر استخدامها في شيفرات واقعيّة، لكنّها قد تكون مفيدة أحيانا. يصِف معيار أحداث DOM ثلاث مراحل لانتشار الأحداث: مرحلة الانتشار نحو اﻷسفل (Capturing phase) - ينزل الحدث إلى العنصر. مرحلة الهدف (Target phase) - يصل الحدث إلى العنصر الهدف. مرحلة الانتشار نحو اﻷعلى (Bubbling phase) - ينتشر الحدث صعودًا من العنصر. هذه صورة لما يحصل عند النقر على <td> داخل جدول، مأخوذة من المواصفة: ما يعني ذلك: بالنقر على <td> ، ينتشر الحدث أوّلا عبر سلسلة الأسلاف نزولًا إلى العنصر (مرحلة الانتشار نحو اﻷسفل)، فيبلغ الهدفَ ويتفعّل هناك (مرحلة الهدف)، ثم ينتشر صعودًا (مرحلة الانتشار نحو اﻷعلى) مستدعيًا المعالجات في طريقه. اقتصرنا في السابق على مرحلة الانتشار نحو اﻷعلى، لأنّه من النادر استخدام مرحلة الانتشار نحو اﻷسفل. لا تظهر لنا عادةً. لا يعلم المعالجون الذين عُيّنوا على شكل خاصيّة on<event>‎‎ ، أو على شكل سمة HTML، أو باستخدام addEventListener(event, handler)‎‎ بوسيطين فقط، شيئًا عن الانتشار نحو اﻷسفل، فهم يشتغلون فقط في المرحلتين الثانية والثالثة. لالتقاط حدثٍ في مرحلة الانتشار نحو اﻷسفل، يجب تغيير قيمة الخيار capture إلى true. elem.addEventListener(..., {capture: true}) // {capture: true} فهو اختصار لـ ،"true" أو فقط elem.addEventListener(..., true) يمكن أن يأخذ الخيار capture قيمتين: إذا كانت false (افتراضيًّا)، فإنّ المعالج يوضع في مرحلة الانتشار نحو اﻷعلى. إذا كانت true، فإنّ المعالج يوضع في مرحلة الانتشار نحو اﻷسفل. لاحظ رغم أنّه يوجد رسميًّا ثلاث مراحل، إلّا أن المرحلة الثانية (مرحلة الهدف: عندما يبلغ الحدثُ الهدف) لا تُعالَج بشكل مستقل، بل تشتغل كلٌّ من المعالجات الموضوعة في مرحلتي الانتشار نحو اﻷسفل واﻷعلى في هذه المرحلة أيضا. لنرى كلًّا من الانتشار نحو اﻷسفل والأعلى حال عملهما: <style> body * { margin: 10px; border: 1px solid blue; } </style> <form>FORM <div>DIV <p>P</p> </div> </form> <script> for(let elem of document.querySelectorAll('*')) { elem.addEventListener("click", e => alert(`Capturing: ${elem.tagName}`), true); elem.addEventListener("click", e => alert(`Bubbling: ${elem.tagName}`)); } </script> See the Pen JS-p2-bubbling-and-capturing -ex4 by Hsoub (@Hsoub) on CodePen. تُسند الشيفرة معالجاتٍ لحدث النّقر إلى جميع العناصر التي في المستند لرؤية أيّها تعمل. عند النقر على <p>، فإنّ التسلسل يكون كالتالي: DIV ‹- FORM ‹- BODY ‹- HTML (مرحلة الانتشار نحو اﻷسفل، أوّل المنصتين). P (مرحلة الهدف، تُفعّل مرّتين لأنّنا وضعنا مُنصتَين اثنين: الانتشار نحو اﻷسفل واﻷعلى). HTML ‹- BODY ‹- FORM ‹- DIV (مرحلة الانتشار نحو اﻷسفل، ثاني المنصتين). توجد هناك الخاصيّة event.eventPhase التي تخبرنا برقم المرحلة التي تمّ فيها التقاط الحدث. لكن يندر استخدامها لأنّنا نعلم ذلك من خلال المعالج عادةً. ملاحظة: لحذف المعالج، يستلزم التابع removeEventListener إعطاء نفس المرحلة عند إضافة معالجٍ بهذا الشكل addEventListener(..., true)‎‎، فيجب ذكر نفس المرحلة أيضًا في removeEventListener(..., true)‎‎ لحذف المعالج بشكل صحيح. ملاحظة: تشتغل المعالجات التي أُسندت إلى نفس العنصر وفي نفس المرحلة حسب الترتيب الذي أُنشئت به إذا كانت لدينا عدّة معالجات للأحداث مُسنَدة إلى نفس العنصر، وفي نفس المرحلة فإنها تشتغل بحسب الترتيب الذي أُنشئت به. elem.addEventListener("click", e => alert(1)); // من المؤكّد أن يشتغل أوّلا elem.addEventListener("click", e => alert(2)); الملخص عندما يقع الحدث، فإن أدنى العناصر الذي وقع فيه الحدث يُعلَّم بالعنصر"الهدف" (event.target). ثم ينتشر الحدث نزولًا من جذر المستند إلى event.target مستدعيًا في طريقه المعالجين المعيّنين بواسطة addEventListener(..., true)‎‎ (القيمة ‎‎ true هي اختصار لـ {capture: true}) ثم نُستدعى المعالجات المُسندة إلى العنصر الهدف نفسه. ثم ينتشر الهدف صعودًا من event.target إلى الجذر، مناديًا المعالجات المعيّنة بواسطة on<event>‎‎ و addEventListener دون الوسيط الثالث false/{capture:false}‎‎. يمكن لأيّ معالجٍ أن يستخدم خاصيّات الكائن event التالية: event.target -- أدنى العناصر الذي نشأ عنه الحدث. event.currentTarget (هو نفس this) -- العنصر الذي يعالج الحدث حاليًّا (الذي أُسند إليه المعالج). event.eventPhase -- المرحلة الحاليّة (1=الانتشار نحو اﻷسفل، 2=الهدف، 3=الانتشار نحو اﻷعلى). يمكن لأيّ معالجٍ أن يوقف انتشار الحدث من خلال استدعاء event.stopPropagation()‎‎، لكن لا يُنصح بذلك، لأنّه لا يمكن التأكّد حقًّا من عدم الحاجة إليه في الأعلى، ربّما في أمورٍ مختلفة تماما. يندر جدًّا استخدام مرحلة الانتشار نحو اﻷسفل، إذ تُعالَج الأحداث عادةً عند انتشارها نحو اﻷعلى. هناك سبب منطقي وراء ذلك. في الواقع، عندما يقع حادث ما، تُبلَّغ السلطات المحليّة أوّلًا. فهم أفضل من يعرف المنطقة التي وقع فيها الحادث. ثمّ تُبلّغ السلطات الأعلى عند الحاجة لذلك. ينطبق الأمر على معالجات الأحداث. تكون الشيفرة المسؤولة عن إسناد المعالج إلى عنصرٍ ما أعلم بالتفاصيل المتعلّقة بذلك العنصر ومالذي يفعله. فيكون المعالج المسند إلى العنصر<td> خصيصا أنسب بتولي أمر ذلك العنصر بالذات، إذ يعلم كلّ شيء بخصوصه، وينبغي أن تُمنح له الفرصة أوّلا. ثم يأتي أبوه المباشر، الذي يكون له اطّلاع على السياق لكنّه أقلّ معرفةً به. وهكذا إلى أعلى العناصر، الذي يعالج الأمور العامّة ويكون آخرهم اشتغالا. يرسي مفهوم الانتشار نحو اﻷسفل واﻷعلى اﻷساس لموضوع "تفويض الأحداث"، الذي يُعدّ نمط معالجةٍ للأحداث قويًّا للغاية. سندرسه في المقال التالي. ترجمة -وبتصرف- للمقال Bubbling and capturing من سلسلة Browser: Document, Events, Interfaces لصاحبها Ilya Kantor
  3. تمثّل الأحداث (events) إشاراتٍ إلى أنّ شيئًا ما قد حصل. يمكن أن تنشأ هذه الإشارات من أيّ عقدة في DOM (لكنّها لا تقتصر فقط على DOM). على سبيل المثال، هذه قائمة لأكثر الأحداث فائدةً: أحداث الفأرة: click -- عند النقر بالفأرة على عنصرٍ ما (أو عند الضغط عليه باستخدام الأجهزة اللمسية) contextmenu -- عند النقر بالزرّ الأيمن للفأرة على عنصرٍ ما. mouseout \ mouseover -- عندما يبلغ / يغادر مؤشّر الفأرة عنصرًا ما. mouseup \ mousedown -- عند ضغط / تحرير زرّ الفأرة. mousemove -- عند تحريك مؤشّر الفأرة. أحداث لوحة المفاتيح: keyup \ keydown -- عند ضغط / إرسال أحد أزرار لوحة المفاتيح. أحداث النماذج: submit -- عندما يرسل المستخدم النموذج <form>. focus -- عندما يحدّد المستخدم عنصرًا ما في النموذج، كتحديده عنصر <input> مثلا. أحداث المستند: DOMContentLoaded -- عند الفراغ من تحميل ملف HTML ومعالجتِه، وبناء كامل شجرة DOM. الأحداث المتعلقة بـ CSS: transitionend - عند انتهاء تحريكCSS (animation) ‎. وهناك العديد من الأحداث الأخرى، سنتناول بعضها بمزيدٍ من التفصيل في مقالاتٍ لاحقة. معالجات الأحداث من أجل الاستجابة للأحداث، يمكننا تعيين معالجٍ (handler) على شكل دالّة تُنفَّذ عند وقوع الحدث. وتُعدّ بذلك المعالجات وسيلةً لتنفيذ شيفرات جافاسكربت وفقًا لما يقوم به المستخدم. توجد عدة طرق لتعيين معالجٍ للحدث. سنتناولها بدءًا بأبسطها. على شكل سمة HTML يمكن تعيين المعالج في HTML على شكل سمةٍ (attribute) يكون اسمها على النحو on<event>‎ أي on متبوعة باسم الحدث. على سبيل المثال، لتعيين معالجٍ لحدث click على عنصر input، يمكننا استخدام السمة onclick كالتالي: <input value="Click me!" onclick="alert('Click!')" type="button"> See the Pen JS-p2-introduction-browser-events-ex1 by Hsoub (@Hsoub) on CodePen. عند النقر بالفأرة، تُنفّذ الشيفرة التي بداخل onclick. يرجى التنبّه هنا إلى أنّنا استخدمنا علامات الاقتباس غير المزدوجة داخل onclick، لأنّ السّمة نفسها محاطة بعلامات اقتباس مزدوجة. فلو غفلنا عن أنّ الشيفرة موجودة داخل السّمة واستخدمنا داخلها علامات الاقتباس المزدوجة هكذا onclick="alert("Click!")"‎ فلن تعمل الشيفرة بشكل صحيح. لا تعدّ سمة HTML مكانا مناسبا لكتابة الكثير من الشيفرة، فيحسُن إذًا أن ننشئ دالّة جافاسكربت ونستدعيها هناك. في المثال أدناه، يؤدي النقر إلى تنفيذ الدالّة countRabbits()‎ <script> function countRabbits() { for(let i=1; i<=3; i++) { alert("Rabbit number " + i); } } </script> <input type="button" onclick="countRabbits()" value="Count rabbits!"> See the Pen JS-p2-introduction-browser-events-ex2 by Hsoub (@Hsoub) on CodePen. كما هو معلوم، لا فرق بين الأحرف الكبيرة والصغيرة في تسمية سمات HTML، فتمثل كلّ من ONCLICK و onClick و onCLICK نفس السّمة، لكن في الغالب تُكتب السّمات بأحرف صغيرة هكذا onclick. على شكل خاصيّة DOM يمكننا تعيين معالجٍ على شكل خاصيّة DOM يكون اسمها على النحو on<event>‎. على سبيل المثال، elem.onclick: <input id="elem" type="button" value="Click me"> <script> elem.onclick = function() { alert('Thank you'); }; </script> See the Pen JS-p2-introduction-browser-events-ex3 by Hsoub (@Hsoub) on CodePen. عند تعيين المعالج على شكل سمة HTML، فإنّ المتصفّح يقرأها ثمّ يُنشئ من محتواها دالّة، ويكتبها على شكل خاصيّة في DOM. فتكون بذلك هذه الطريقة لتعيين المعالج مساوية للتي قبلها. تؤدي هاتان الشيفرتان الوظيفة نفسها: مجرّد HTML: <input type="button" onclick="alert('Click!')" value="Button"> HTML + JS: <input type="button" id="button" value="Button"> <script> button.onclick = function() { alert('Click!'); }; </script> استخدمنا في المثال الأوّل سمة HTML لتهيئة button.onclick، بينما في المثال الثاني استخدمنا سكربتًا لذلك. هذا الفرق الذي هناك. بما أنّ هناك خاصيّة واحدة فقط تحمل الاسم onclick، فلا يمكننا تعيين أكثر من معالج واحد لنفس الحدث. في المثال أدناه، تؤدي إضافة معالجٍ بواسطة جافاسكربت إلى استبدال المعالج الموجود مسبقًا. <input type="button" id="elem" onclick="alert('Before')" value="Click me"> <script> elem.onclick = function() { // يستبدل المعالج الموجود alert('After'); // هذا ما سيظهر فقط }; </script> See the Pen JS-p2-introduction-browser-events-ex4 by Hsoub (@Hsoub) on CodePen. لحذف المعالج، يكفي إعطاء الخاصيّة القيمة null هكذا: elem.onclick = null. الوصول إلى العنصر بواسطة this تشير الكلمة this داخل المعالج إلى نفس العنصر الذي أُسند إليه المعالج. ففي الشيفرة أدناه، يُظهر العنصر button محتواه بواسطة this.innerHTML <button onclick="alert(this.innerHTML)">Click me</button> See the Pen JS-p2-introduction-browser-events-ex5 by Hsoub (@Hsoub) on CodePen. أخطاء محتملة في بداية التعامل مع معالجات الأحداث، يُرجى التنبه لعدد من الأمور الدقيقة. يمكن تعيين معالج من دالة موجودة مسبقا: function sayThanks() { alert('Thanks!'); } elem.onclick = sayThanks; لكن انتبه، يجب إسناد الدالة كـ sayThanks وليس sayThanks()‎. // صحيح button.onclick = sayThanks; // خطأ button.onclick = sayThanks(); بإضافة الأقواس، تصير sayThanks()‎ استدعاءًا للدالة. وبالتالي، يأخذ السطر الأخير ناتج تنفيذ الدالّة (الذي هو undefined بحكم أنّ الدالة لا تعيد أيّ شيء) ويضعه في onclick. هذا لا يصلح. … في المقابل، نحتاج في HTML إلى تضمين الأقواس: <input type="button" id="button" onclick="sayThanks()"> يمكن توضيح سبب ذلك كالتالي: عندما يقرأ المتصفّح السّمة، فإنّه ينشئ معالجًا على شكل دالّة لها نفس محتوى هذه السّمة. فيقوم HTML الذي لدينا بإنشاء الخاصيّة التالية: button.onclick = function() { sayThanks(); // <-- يصير محتوى السّمة هنا }; لا تستخدم setAttribute لتعيين المعالجات. هذه الشيفرة لا تصلح: // إلى توليد أخطاء <body> يؤدي النفر على // بحكم أنّ السّمات هي سلاسل نصيّة، فتصير الدالة سلسلة نصيّة ايضا document.body.setAttribute('onclick', function() { alert(1) }); تفرّق خاصيّات DOM بين الأحرف الكبيرة والصغيرة. يجب تعيين المعالج في elem.onclick بدل elem.ONCLICK ، لأنّ خاصيّات DOM تفرُق معها الأحرف الكبيرة والصغيرة. addEventListener تكمن المشكلة الأساسية في طرق تعيين المعالج السالفة الذكر، في عدم إمكانيّة تعيين عدّة معالجات لحدث واحد. لنفترض أن جزءًا من الشيفرة التي لدينا يهدف إلى إبراز أحد الأزرار عند النقر عليه، بينما يهدف جزء آخر من الشيفرة إلى إظهار رسالة ما عند نفس النقرة. قد نودّ تعيين عدّة معالجات للحدث لتحقيق ذلك، لكن بإضافة خاصيّة جديدة إلى DOM، تُستبدل الخاصيّة الموجودة مسبقًا. input.onclick = function() { alert(1); } // ... input.onclick = function() { alert(2); } // يستبدل المعالج السابق أدرك العاملون على معايير الويب هذه المشكلة منذ القدم، واقترحوا طريقة بديلة لإدارة معالجات الأحداث، وذلك بواسطة التوابع الخاصة addEventListener و removeEventListener. تكون صيغة إضافة معالجٍ فيها كالتالي: element.addEventListener(event, handler, [options]); حيث أن: event: هو اسم الحدث، كـ "click" مثلًا. handler: هي دالّة المعالج. options: هو كائن إضافي اختياري، وله الخاصيّات التالية: once: إذا كانت قيمتها true، فإن منصت الحدث (event listener) يزول تلقائيا بعد حصول الحدث. capture المرحلة التي يُعالَج فيها الحدث، وسنتطرّق إليها لاحقا في مقال انتشار اﻷحداث. لأسباب تاريخية، يمكن أن تحمل options القيمة true \ false ويكون لذلك نفس معنى {capture: false/true}. passive: إذا كانت قيمتها true، فلن يستدعي المعالجُ التابعَ preventDefault()‎، وسنشرح ذلك في مقال أفعال المتصفّح الافتراضية. يمكن حذف المعالج بواسطة: element.removeEventListener(event, handler, [options]); تنبيه: يتطلّب الحذفُ الدالةَ نفسَها لحذف المعالج يجب تمرير نفس الدالة التي عُيّنت من قبل. فلو جرّبنا مثلًا: elem.addEventListener( "click" , () => alert('Thanks!')); // .... elem.removeEventListener( "click", () => alert('Thanks!')); لن يحُذف المعالج، لأن removeEventListener قد تلقّى دالة أخرى -- لها نفس الشيفرة، لكن لا يهم ذلك لأنها كائن دالة آخر. هذه هي الطريقة الصحيحة لحذف المعالج: function handler() { alert( 'Thanks!' ); } input.addEventListener("click", handler); // .... input.removeEventListener("click", handler); يرُجى التنبّه هنا إلى أنّه إذا لم نحفظ الدالة في متغيّر، فلا يمكننا حذفها. إذ لا سبيل إلى "إعادة قراءة" المعالجات المُعيّنة بواسطة addEventListener. يمكّن الاستدعاء المتكرّر لـ addEventListener من تعيين عدّة معالجات كالتالي: <input id="elem" type="button" value="Click me"/> <script> function handler1() { alert('Thanks!'); }; function handler2() { alert('Thanks again!'); } elem.onclick = () => alert("Hello"); elem.addEventListener("click", handler1); // Thanks! elem.addEventListener("click", handler2); // Thanks again! </script> See the Pen JS-p2-introduction-browser-events-ex6 by Hsoub (@Hsoub) on CodePen. كما هو مبيّن في المثال أعلاه، يمكن تعيين معالجات باستخدام كلٍّ من خاصيّة DOM وaddEventListener معًا، لكن في الغالب تُستخدم إحداهما فقط. تنبيه: لا يمكن تعيين معالجات لبعض الأحداث إلا بواسطة addEventListener توجد أحداثٌ لا يمكن تعيين معالجات لها عن طريق خاصيّة DOM ، بل تشترط استخدام addEventListener. على سبيل المثال، الحدث DOMContentLoaded، الذي يحصل حين الانتهاء من تحميل المستند وبناء DOM. // لن يتم تنفيذ هذا أبدا document.onDOMContentLoaded = function() { alert("DOM built"); }; // هذه الطريقة أصحّ document.addEventListener("DOMContentLoaded", function() { alert("DOM built"); }); بذلك يكونaddEventListener أشمل، رغم أنّ هذه الأحداث تُعدّ استثناءًا وليست القاعدة. كائن الحدث لمعالجة الحدث كما ينبغي، قد يلزمنا معرفة المزيد عمّا حصل بالضبط. فليس مجرّد "النقر" أو "الضغط"، بل أيضا ما هي إحداثيات المؤشر؟ أو ما هو الزر التي ضُغط؟ إلى غير ذلك. عند وقوع حدثٍ ما، يُنشئ المتصفّحُ كائن حدث ويضع فيه التفاصيل، ثم يمرّره على شكل وسيط للمعالج. هذا مثال لكيفية الحصول على إحداثيات المؤشر من كائن الحدث: <input type="button" value="Click me" id="elem"> <script> elem.onclick = function(event) { // يظهر نوع الحدث والعنصر وإحداثيات النقر alert(event.type + " at " + event.currentTarget); alert("Coordinates: " + event.clientX + ":" + event.clientY); }; </script> See the Pen JS-p2-introduction-browser-events-ex7 by Hsoub (@Hsoub) on CodePen. هذه بعض خاصيّات كائن الحدث event: event.type: نوع الحدث، و هو في هذا المثال "click" event.currentTarget: العنصر الذي عالج الحدث. وهو نفس this، إلا إذا كان المعالج دالة سهمية أو أن this الخاصّ به مرتبط (bound) بشيء آخر، فعندها يمكن الحصول على العنصر بواسطة event.currentTarget. event.clientX / event.clientY: هي إحداثيات المؤشّر بالنسبة للنافذة، عند الأحداث المتعلقة بالمؤشّر. هناك المزيد من الخاصيّات، والكثير منها متعلق بنوع الحدث. فأحداث لوحة المفاتيح لها مجموعة من الخاصيّات، وأحداث المؤشر لها مجموعة أخرى. سندرس ذلك لاحقا عندما نتطرّق لمختلف الأحداث بالتفصيل. ملاحظة: كائن الحدث متوفر أيضا للمعالجات في HTML إذا عينّا معالجًا في HTML، فإنّه من الممكن أيضا استخدام الكائن event، كالتالي: <input type="button" onclick="alert(event.type)" value="Event type"> See the Pen JS-p2-introduction-browser-events-ex8 by Hsoub (@Hsoub) on CodePen. يمكننا ذلك لأن المتصفّح حينما يقرأ السمة، فإنّه ينشئ معالجًا بهذا الشكل: function(event) { alert(event.type) }‎، فيكون اسم وسيطه الأول "event"، ومحتواه مأخوذا من السمة. الكائنات المعالجة: handleEvent يمكن بواسطة addEventListener تعيين معالجٍ على شكل كائن أيضا، وعند وقوع الحدث، يُستدعى التابع handleEvent. على سبيل المثال: <button id="elem">Click me</button> <script> let obj = { handleEvent(event) { alert(event.type + " at " + event.currentTarget); } }; elem.addEventListener('click', obj); </script> See the Pen JS-p2-introduction-browser-events-ex8 by Hsoub (@Hsoub) on CodePen. كما نرى، عندما يستقبل addEventListener كائنًا، فإنه يستدعي obj.handleEvent(event)‎ في حال وقوع الحدث. يمكننا أيضا استخدام صنفٍ لذلك: <button id="elem">Click me</button> <script> class Menu { handleEvent(event) { switch(event.type) { case 'mousedown': elem.innerHTML = "Mouse button pressed"; break; case 'mouseup': elem.innerHTML += "...and released."; break; } } } let menu = new Menu(); elem.addEventListener('mousedown', menu); elem.addEventListener('mouseup', menu); </script> See the Pen JS-p2-introduction-browser-events-ex9 by Hsoub (@Hsoub) on CodePen. يقوم نفس الكائن هنا بمعالجة كلا الحدثين. لكن ينبغي التنبه إلى أنه يجب تحديد الأحداث المراد الإنصات إليها باستخدام addEventListener صراحةً. يتلقى الكائن menu هنا الحدثين mousedown و mouseup دون غيرهما من أنواع الأحداث الأخرى. لا يلزم التابع handleEvent أن يقوم بكامل العمل، بل يمكنه استدعاء توابع أخرى مختصة بأحداث معيّنة، كما يلي: <button id="elem">Click me</button> <script> class Menu { handleEvent(event) { // mousedown -> onMousedown let method = 'on' + event.type[0].toUpperCase() + event.type.slice(1); this[method](event); } onMousedown() { elem.innerHTML = "Mouse button pressed"; } onMouseup() { elem.innerHTML += "...and released."; } } let menu = new Menu(); elem.addEventListener('mousedown', menu); elem.addEventListener('mouseup', menu); </script> See the Pen JS-p2-introduction-browser-events-ex10 by Hsoub (@Hsoub) on CodePen. هكذا يتم فصل المعالجات على حدة، مما قد يجعل دعمها أسهل. الملخص توجد ثلاث طرق لتعيين معالجات للأحداث: سمة HTML: ‏ onclick="..."‎. خاصيّة DOM:‏ elem.onclick = function. توابع: elem.addEventListener(event, handler[, phase])‎ للإضافة، و removeEventListener للحذف. يندر استخدام سمات HTML، لأن جافاسكربت تبدو غريبة وسط وسم HTML. بالإضافة إلى أنه لا يمكن كتابة الكثير من الشيفرة هناك. لا بأس باستخدام خاصيّات DOM، غير أنه لا يمكن تعيين أكثر من معالج لنفس الحدث. في الكثير من الأحيان قد لا يمثّل هذا القيد مشكلة تُذكر. تُعدّ الطريقة الأخيرة أكثرها مرونة، لكنها أيضا أطولها كتابة. هناك بعض الأحداث التي لا تعمل إلا بواسطتها، على سبيل المثال transitionend و DOMContentLoaded (ستُدرس لاحقًا). بالإضافة إلى ذلك، تدعم addEventListener تعيين معالجات للأحداث على شكل كائنات، ويُستدعى حينها التابع handleEvent عند وقوع الحدث. أيّا كانت طريقة تعيين المعالج، فإنّه يستقبل كائن حدث كوسيط أول. يحتوي هذا الكائن تفاصيل ما قد حصل. سنتعلم المزيد عن الأحداث عموما، وعن مختلف أنواع الأحداث في المقالات القادمة. التمارين الإخفاء عند النقر الأهمية: 5 أضف جافاسكربت إلى الزر button لجعل <div id="text"‎> يختفي عند النقر عليه، كما هنا افتح البيئة التجريبيّة لإنجاز التمرين الحل افتح البيئة التجريبيّة لمشاهدة الحلّ الاختفاء عند النقر الأهمية: 5 أنشئ زرّا يخفي نفسه عند النقر عليه. الحل يمكن استخدام this للإشارة إلى "العنصر نفسه" هنا: <input type="button" onclick="this.hidden=true" value="Click to hide"> أيّ المعالجات ستُنفّذ؟ اﻷهمية: 5 يحتوي المتغير button على زرّ، وليس عليه معالجات. أيّ من المعالجات ستُنفّذ بعد تشغيل الشيفرة أدناه؟ ما هي التنبيهات التي تظهر؟ button.addEventListener("click", () => alert("1")); button.removeEventListener("click", () => alert("1")); button.onclick = () => alert(2); الحل الجواب هو 1 و 2. يستجيب المعالج الأول لأنه لم يُحذف بواسطة removeEventListener. لحذف المعالج، يجب تمرير نفس الدالة التي عُيّنت بالضبط. وفي الكود أعلاه، مُررت دالة جديدة، تبدو مثلها تماما لكنها تبقى دالة أخرى. لحذف كائن دالة، نحتاج أن نحفظ مرجعا لها، كما يلي: function handler() { alert(1); } button.addEventListener("click", handler); button.removeEventListener("click", handler); أما المعالج button.onclick فهو يعمل بشكل مستقل وبالإضافة إلى addEventListener. حرّك الكرة في الملعب حرّك الكرة في الملعب بواسطة النقر، كما هنا المطلوب: يجب أن ينتقل مركز الكرة إلى موضع المؤشر عند النقر (حبذا دون الخروج عن حافة الملعب). تحريكات CSS مُرحّب بها. يجب ألا تخرج الكرة عن حدود الملعب. يجب ألا يؤدي تمرير الصفحة إلى اختلاط الأمور . ملاحظات: يجب أن تشتغل الشيفرة مع مختلف أحجام الكرة والملعب، وألا تكون مرتبطة بقيم معينة. استخدم الخاصيّات event.clientX/event.clientY للحصول على إحداثيات النقر. افتح البيئة التجريبيّة لإنجاز التمرين الحل أولا، علينا أن نختار طريقة لتغيير موضع الكرة. لا يمكننا استخدام position:fixed لذلك، لأن تمرير الصفحة قد يخرج الكرة عن الملعب. لذا يلزمنا استخدام position:absolute، ولكي يكون التموضع جيد الإحكام، علينا أن نعطي للملعب نفسه وضعية، لتكون بذلك الكرة مُموضَعة بالنسبة إلى الملعب: #field { width: 200px; height: 150px; position: relative; } #ball { position: absolute; left: 0; /* (بالنسبة إلى أقرب سلف مموضَع (الملعب */ top: 0; transition: 1s all; /* الكرة تطير leftو top المتعلقة بـ CSS تجعل تحريكات */ } بعدها، علينا أن نعطي القيم المناسبة لـ ball.style.left/top ، إذ هي الآن تمثل إحداثيات الكرة بالنسبة إلى الملعب. هذه هي الصورة: تمثل event.clientX/clientY إحداثيات موضع النقر بالنسبة إلى النافذة. للحصول على الإحداثية left لموضع النقر بالنسبة إلى الملعب، يمكننا أن نطرح كلّا من الحافة اليسرى الملعب وسمك الحد: let left = event.clientX - fieldCoords.left - field.clientLeft; عادةً، يشير ball.style.left إلى "الحافة اليسرى للعنصر" (الكرة)، فإذا أعطيناه قيمة المتغير left، فإن حافة الكرة هي التي ستكون تحت المؤشر وليس مركزها. علينا إذًا أن نزيح الكرة بمقدار نصف عرضها إلى اليسار، وبمقدار نصف طولها إلى الأعلى كي نجعلها في المنتصف. فتكون القيمة النهائية لـ left هي: let left = event.clientX - fieldCoords.left - field.clientLeft - ball.offsetWidth/2; تُحسب الإحداثية العمودية بنفس الطريقة. يُرجى التنبه إلى أنه يجب أن يكون عرض الكرة وطولها معلومين عند قراءة ball.offsetWidth. يجب أن يُحدد ذلك في HTML أو CSS. افتح البيئة التجريبيّة لمشاهدة الحلّ أنشئ قائمة منحدرة اﻷهمية: 5 أنشئ قائمة تُفتح وتُغلق عند النقر كما هنا ملاحظة: ينبغي التعديل على الملف المصدري لـ HTML/CSS. افتح البيئة التجريبيّة لإنجاز التمرين الحل HTML/CSS لننشئ أولا HTML/CSS. تُعدّ القائمة مُكوّنا رسوميا مستقلا بذاته في الصفحة، لذا فيفضّل وضعها في عنصر DOM واحد. يمكن تنسيق عناصر القائمة على شكل ul/li. هذا مثال عن البنية: <div class="menu"> <span class="title">Sweeties (click me)!</span> <ul> <li>Cake</li> <li>Donut</li> <li>Honey</li> </ul> </div> استخدمنا <span> للعنوان، لأن <div> له ضمنيّا الخاصيّة display:block ، مما سيجعله يحتل 100% من المساحة الأفقية، هكذا: <div style="border: solid red 1px" onclick="alert(1)">Sweeties (click me)!</div> فإذا وضعنا عليه onclick ، فإنه سيستجيب للنقرات التي على يمين النص أيضا. وبما أن <span> له ضمنيّا الخاصيّة display: inline، فإنه سيحتل من المساحة فقط ما يكفي لاحتواء النص: <span style="border: solid red 1px" onclick="alert(1)">Sweeties (click me)!</span> إقلاب القائمة يؤدي إقلاب (toggling) القائمة إلى تغيير السهم وإظهار أو إخفاء عناصرها. يمكن القيام بكل هذه التغيرات من خلال CSS. في جافاسكربت علينا فقط تمييز الوضع الحالي للقائمة من خلال إضافة أو إزالة الصنف open.. بدون open. تكون القائمة منقبضة: .menu ul { margin: 0; list-style: none; padding-left: 20px; display: none; } .menu .title::before { content: '▶ '; font-size: 80%; color: green; } … ومع وجوده يتغير السهم وتظهر عناصر القائمة: .menu.open .title::before { content: '▼ '; } .menu.open ul { display: block; } افتح البيئة التجريبيّة لمشاهدة الحلّ أضف زرّا للإغلاق اﻷهمية: 5 هناك مجموعة من الرسائل. باستخدام جافاسكربت، أضف زرا في أقصى اليمين العلوي للرسائل لإغلاقها. يجب أن تبدو النتيجة كما هنا. افتح البيئة التجريبيّة لإنجاز التمرين الحل لإضافة الزر، يمكن استخدام كلّ من position:absolute (وجعل وضعية اللوحة نسبية position:relative) أو float:right. تتميز طريقة float:right بأنها تضمن عدم تقاطع الزر مع النص، لكن position:absolute تمنح المزيد من الحرية. فالخيار لك. بذلك تكون الشيفرة لكل من الألواح كالتالي: pane.insertAdjacentHTML("afterbegin", '<button class="remove-button">[x]</button>'); و يصير <button> بذلك pane.firstChild، مما يمكّننا من تعيين معالج له هكذا: pane.firstChild.onclick = () => pane.remove(); افتح البيئة التجريبيّة لمشاهدة الحلّ الدوّار اﻷهمية: 4 أنجز دوّارا -- شريط من الصور يمكن تمريره بواسطة أسهم، كما هنا يمكننا لاحقا إضافة المزيد من المزايا كالتمرير اللامتناهي، والتحميل الديناميكي وغير ذلك. ملاحظة: في هذا التمرين، تُمثل بنية HTML/CSS في الحقيقة 90% من الحل. افتح البيئة التجريبيّة لإنجاز التمرين الحل يمكن تمثيل شريط الصور بقائمة ul/li من الصور <img>. من المفترض أن هذا الشريط واسع، لكننا نحدّه بـ <div> ثابت الحجم لقطعه، فيبدو بذلك جزء من الشريط فقط: لعرض القائمة أفقيا، يجب تطبيق الخاصيّات المناسبة لـ <li> في CSS، مثل display: inline-block. بالنسبة لـ <img> يجب أيضا تعديل display، لأنها تكون افتراضيا inline. توجد هناك مساحة تحت العناصر inline مخصصة لأذيال الحروف، فيمكن استخدام display:block لإزالتها. للقيام بالتمرير يمكن إزاحة <ul>. هناك عدة طرق لفعل ذلك، مثل تغيير margin-left أو (لأداء أفضل) استخدام translateX()‎: بما أن العرض الخارجي لـ <div> محدود، فإن الصور "الزائدة" ستُخفى. يُعد الدوّار بأكمله مكونا "رسوميا" مستقلا بذاته، فيُفضل وضعه في <div class="carousel"> واحد وتنسيق جميع الأمور بداخله. افتح البيئة التجريبيّة لمشاهدة الحلّ ترجمة -وبتصرف- للمقال Introduction to browser events من سلسلة Browser: Document, Events, Interfaces لصاحبها Ilya Kantor
  4. يتطلّب تحريك العناصر داخل الصفحة معرفة إحداثياتها. وتعمل معظم الدوالّ في لغة جافاسكربت وفق أحد نظامي الإحداثيات التاليين: إحداثيات بالنسبة للنافذة: تشبه الموضع الثابت (position:fixed)، حيث تُحسب الإحداثيات إنطلاقًا من الركن العلوي الأيسر للنافذة. نسمي هذه الإحداثيات clientX وclientY، وسنوضح السبب من وراء ذلك فيما بعد عند التطرق لخاصيات الأحداث (event properties). إحداثيات بالنسبة للصفحة: تشبه الموضع المطلق (position:absolute)، حيث تُحسب الإحداثيات إنطلاقًا من الركن العلوي الأيسر للصفحة. نسمي هذه الإحداثيات pageX وpageY. عندما يكون أعلى الصفحة ظاهرًا في النافذة، يكون الركن العلوي الأيسر للصفحة متطابقا مع الركن العلوي الأيسر للنافذة، وتكون الإحداثيات أيضًا متساوية. ولكن بعد تحريك الصفحة تتغيّر إحداثيات العناصر بالنسبة للنافذة كلما تحركت العناصر ضمن النافذة. أما الإحداثيات بالنسبة للصفحة فلا تتغيّر، بل تبقى دائمًا ثابتة. نأخذ في هذه الصورة نقطة من الصفحة لإظهار إحداثياتها قبل التمرير (الصورة على اليسار) وبعد التمرير (الصورة على اليمين). عند تمرير الصفحة: بقيت الترتيبة pageY بالنسبة للصفحة على حالها ولم تتغير، حيث تُحسب إنطلاقًا من أعلى الصفحة (الذي لم يعد ظاهرًا في النافذة بعد التحريك). تغيّرت الترتيبة clientY بالنسبة للنافذة (أصبح السهم أقصر) لأن النقطة نفسها أصبحت أقرب لأعلى النافذة. إحداثيات عنصر ما: getBoundingClientRect تعيد الدالّة elem.getBoundingClientRect() الإحداثيات (بالنسبة للنافذة) الخاصة بأصغر مستطيل يكون العنصر elem محصورًا فيه بصفته كائنا من الصنف المدمج DOMRect أهم خاصيات الصنف DOMRect: x وy: إحداثيات نقطة بداية المستطيل بالنسبة للنافذة (X وY). width وheight: عرض وطول المستطيل (يمكن أن تكون القيم سالبة). كما يملك الصنف DOMRect خاصيات مشتقة: top وbottom: ترتيبتي ركني المستطيل العلوي والسفلي. left وright: فاصلتي ركني المستطيل الأيسر والأيمن. فعلى سبيل المثال، عند استعمال الدالّةbutton.getBoundingClientRect()‎للحصول على إحداثيات زرٍ ما مع تمرير الصفحة نزولًا وصعودًا، ستلاحظ أنه كلما تغيّر موضع الزر بالنسبة للنافذة تغيّرت إحداثياته بالنسبة للنافذة (yوtopوbottom` إذا كان التمرير عموديًا). إليك صورة تمثيلية تعرض مخرجات (output) الدالّة elem.getBoundingClientRect()‎: لاحظ أن الخاصيات x وy وwidth وheight تصف المستطيل وصفًا وافيًا، حيث يمكن حساب بقية الخاصيات المشتقة إنطلاقًا منها: left = x top = y right = x + width bottom = y + height لاحظ هنا أنه: يمكن للإحداثيات أن تكون أعدادًا عشريةً مثل العدد 10.5. يُعد هذا الأمر عاديًا، حيث يستعمل المتصفح في عمليات الحساب عمليات القسمة، كما أنه ليس علينا تقريب هذه القيم (to round) عند إسنادها للخاصيتين style.left وstyle.top. يمكن أن تكون قيم الإحداثيات سالبة. في حالة تمرير الصفحة مثلا بحيث يكون العنصر elem تمامًا فوق النافذة، تكون قيمة الخاصية elem.getBoundingClientRect().top سالبة. ملاحظة: لماذا نحتاج إلى الخاصيات المشتقة؟ وما الهدف من وجود الخاصيتين left وtop في وجود الإحداثيات x وy؟ يُعرّف المستطيل في الرياضيات بإحداثيات نقطة البداية (x,y) وشعاع الاتجاه (width,height)، وبذلك تُستخدَم الخاصيات المشتقة لتسهيل الأمر فقط. من ناحية تقنية، يمكن أن تكون قيم الخاصيتينwidth وheight سالبة، فهي تسمح بوصف المستطيلات "الموجّهة" وصفًا دقيقًا بما فيها نقطتي البداية والنهاية، كالمستطيلات المتكوّنة عند النقر على الفأرة وسحبها مسافة معيّنة. وتعني القيم السالبة للخاصيتين width وheight أن المستطيل يبدأ من الركن الأيمن السفلي ليتشكّل عن طريق سحب الفأرة في الاتجاه الأيسر نحو الأعلى. وتمثل الصورة الموالية مستطيلًا بقيم سالبة لكل من الخاصيتين width وheight (مثال: width=-200, height=-100) وكما ترى، لا تتطابق قيم الخاصيتين left وtop مع قيم الخاصيتين x وy في هذه الحالة. ولكن عمليًا، تُعيد دائمًا الدالّة elem.getBoundingClientRect()‎ قيمًا موجبةً للعرض والطول. غير أن السبب الذي حملنا على ذكر القيم السالبة هنا، هو أن هذه القيم التي تبدو لك مكرّرةً، ليست مكرّرةً في الحقيقة. ملاحظة: لا يدعم المتصفح Internet Explorer الإحداثيات x وy لا يدعم المتصفح Internet Explorer خاصيتي الإحداثيات x وy لاسباب تاريخية. يمكنك إذًا إمّا استعمال مكتبة بديلة (polyfill) (إضافة جالبات الخاصيات (getters) للخاصية DomRect.prototype) أو استعمال الخاصيتين left وtop فقط بما أنهما دائمًا متطابقتين مع الإحداثيات x وy في الحالات التي تكون فيها قيم الخاصيتين width وheight موجبة، وخاصة في القيم التي تعيدها الدالّة elem.getBoundingClientRect()‎. ملاحظة: تختلف الإحداثيات right وbottom عن خاصيات التموضع المعتمدة في شيفرة CSS هناك أوجه تشابه جليّة بين الإحداثيات بالنسبة للنافذة، والموضع (position:fixed) المعتمد في شيفرة CSS. ولكن إذا تحدثنا عن التموضع في شيفرة CSS، تمثّل الخاصية right المسافة إنطلاقًا من الركن الأيمن، وتمثّل الخاصية bottom المسافة إنطلاقًا من الركن السفلي. أمّا إذا تمعّنا في الصورة أعلاه، نُدرك أن الأمر ليس كذلك في لغة جافاسكربت، حيث تُحسب كافة الإحداثيات بالنسبة للنافذة إنطلاقًا من الركن العلوي الأيسر، بما فيها إحداثيات الخاصيتين المذكورتين. elementFromPoint(x, y)‎ تعيد الدالّة document.elementFromPoint(x, y)‎ العنصر الأكثر تداخلًا مع إحداثيات النافذة (x, y)، وصيغتها كالآتي: let elem = document.elementFromPoint(x, y); فعلى سبيل المثال تُلقي هذه الشيفرة الضوء أو تُبرِز وسم العنصر المتموضع وسط النافذة أثناء معاينة الشيفرة: let centerX = document.documentElement.clientWidth / 2; let centerY = document.documentElement.clientHeight / 2; let elem = document.elementFromPoint(centerX, centerY); elem.style.background = "red"; alert(elem.tagName); See the Pen JS-p2-11-Coordinates-ex1 by Hsoub (@Hsoub) on CodePen. وبما أن الشيفرة تستعمل الإحداثيات بالنسبة للنافذة، يختلف العنصر الذي تُبرزه الشيفرة كلّما جرى تمرير للصفحة. ملاحظة: تعيد الدالّةelementFromPoint القيمة null إذا مُرِّرَت لها إحداثيات خارجة عن نطاق النافذة تعمل الدالّة document.elementFromPoint(x,y)‎ فقط إذا كانت الإحداثيات (x,y) المُمَرّرة لها داخل نطاق الجزء من الصفحة الظاهر في النافذة. وإذا كانت إحدى الإحداثيات سالبةً أو خارج نطاق طول أوعرض النافذة، تُعيد الدالّة القيمة null. وإليك نوع الخطأ الذي ينجر عن ذلك: let elem = document.elementFromPoint(x, y); //null القيمة elementFromPoint إذا كانت الإحداثيات خارج نطاق النافذة تعيد الدالّة (elem = null) elem.style.background = ''; // Error! See the Pen JS-p2-11-Coordinates-ex2 by Hsoub (@Hsoub) on CodePen. استعمال الإحداثيات للتموضع الثابت (fixed) غالبا ما نحتاج للإحداثيات من أجل إضافة عنصرٍ ما في موضعٍ ما. ولإظهار عنصرٍ بمحاذاة عنصرٍ آخر، يمكننا استعمال الدالّة getBoundingClientRect للحصول على إحداثياته، ثم الاستعانة بالخاصية position مع الخاصيتين left وtop (أو right وbottom). فعلى سبيل المثال، تُظهر الدالّة createMessageUnder(elem, html)‎ في الشيفرة الموالية رسالةً تحت العنصر elem: let elem = document.getElementById("coords-show-mark"); function createMessageUnder(elem, html) { // إنشاء العنصر الذي يضم الرسالة let message = document.createElement('div'); //يستحسن استعمال صنف CSS لتحديد النمط التنسيقي message.style.cssText = "position:fixed; color: red"; //إسناد الإحداثيات دون أن ننسى الحرفين 'px' let coords = elem.getBoundingClientRect(); message.style.left = coords.left + "px"; message.style.top = coords.bottom + "px"; message.innerHTML = html; return message; } // :الاستعمال // إضافته للصفحة لمدّة خمس ثوان let message = createMessageUnder(elem, 'Hello, world!'); document.body.append(message); setTimeout(() => message.remove(), 5000); See the Pen JS-p2-11-Coordinates-ex3 by Hsoub (@Hsoub) on CodePen. عند الضغط على الزر صاحب الخاصية id="coords-show-mark"‎ تظهر الرسالة تحته. يمكن التعديل على الشيفرة لإظهار الرسالة على اليسار، أو على اليمين، أو في الأسفل، أو تطبيق شيفرة حركات CSS (أي CSS animation) عليه لجعله يختفي تدريجيا، وهكذا دواليك. سيكون ذلك سهلًا طالما نملك كافة إحداثيات ومقاسات العنصر. لاحظ هذا التفصيل المهم: في حالة ما إذا قمنا بتمرير الصفحة تبتعد الرسالة عن الزر. والسبب واضح: موضع الرسالة ثابت (position:fixed)، لذا تبقى في نفس الموضع من النافذة عند تمرير الصفحة. ولتغيير ذلك، علينا استعمال الإحداثيات بالنسبة للصفحة، والموضع المطلق (position:absolute). إحداثيات الصفحة يبدأ احتساب الإحداثيات بالنسبة للصفحة من الركن العلوي الأيسر للصفحة، لا النافذة. ويقابِل الإحداثيات بالنسبة للنافذة في لغة CSS الموضع position:fixed، أما الإحداثيات بالنسبة للصفحة فيقابلها في لغة CSS الموضع position:absolute من الأعلى. يمكننا استعمال الموضع position:absolute والخاصيتين left وtop لوضع عنصرٍ ما في موضعٍ ما من الصفحة ، بحيث يبقى في نفس الموضع عند تمرير الصفحة. ولكن علينا أولا إيجاد الإحداثيات الصحيحة. لا يوجد أيّ دالّة في المعايير تسمح بالحصول على إحداثيات عنصرٍ ما بالنسبة للصفحة، ولكن يمكننا كتابتها. يعد نظامي الإحداثيات مرتبطين ببعضهما البعض من خلال المعادلة التالية: pageY=clientY+ طول الجزء العمودي من الصفحة غير الظاهر في النافذة. pageX=clientX+ عرض الجزء الأفقي من الصفحة غير الظاهر في النافذة. تأخذ الدالّة getCoords(elem)‎ الإحداثيات بالنسبة للنافذة باستخدام الدالّة elem.getBoundingClientRect()‎ وتضيف مسافة التمرير لها. // الحصول على إحداثيات العنصر بالنسبة للصفحة function getCoords(elem) { let box = elem.getBoundingClientRect(); return { top: box.top + window.pageYOffset, right: box.right + window.pageXOffset, bottom: box.bottom + window.pageYOffset, left: box.left + window.pageXOffset }; } في المثال أعلاه، استعملنا الموضع position:absolute، وبذلك تبقى الرسالة بمحاذاة العنصر عند تمرير الصفحة وتصبح الدالّة createMessageUnder بعد التعديل كالآتي: function createMessageUnder(elem, html) { let message = document.createElement('div'); message.style.cssText = "position:absolute; color: red"; let coords = getCoords(elem); message.style.left = coords.left + "px"; message.style.top = coords.bottom + "px"; message.innerHTML = html; return message; } الخلاصة لكلّ نقطة من الصفحة إحداثيات: بالنسبة للنافذة: تُساوي elem.getBoundingClientRect()‎ بالنسبة للصفحة: تُساوي elem.getBoundingClientRect()‎ + مقدار تمرير الصفحة. يُنصح باستخدام الإحداثيات بالنسبة للنافذة مع الموضع position:fixed، والإحداثيات بالنسبة للصفحة مع الموضع position:absolute. ولِكِلَا نظامي الإحداثيات إيجابيات وسلبيات، حيث يُستحسن استعمال أحدهما في حالات معيّنة، والآخر في حالات أخرى، كما هو الحال بالنسبة للمواضع absolute وfixed في لغة CSS. تمارين إيجاد إحداثيات المستطيل بالنسبة للنافذة درجة الأهمية:5 لدينا في الصورة الموالية صفحة ويب بها مستطيل أخضر “field”. استعمل لغة جافاسكربت للحصول على إحداثيات الأركان المؤشر عليها بالأسهم الحمراء بالنسبة للنافذة. أضيفت للصفحة إمكانية الحصول على إحداثيات أيّ نقطة منها بمجرد النقر عليها لتسهيل الأمر عليك. يمكنك الاطلاع على شيفرة التمرين عبر هذا الرابط عليك استعمال نموذج تمثيل الصفحة ككائن DOM للحصول على الإحداثيات بالنسبة للنافذة لكل من: الركن الخارجي العلوي الأيسر (وهو سهل). الركن الخارجي السفلي الأيمن (وهو سهل أيضا). الركن الداخلي العلوي الأيسر (صعب بعض الشيء). الركن الداخلي السفلي الأيمن ( هناك عدة طرائق للحصول عليه. اختر واحدة منها) ينبغي أن تكون الإحداثيات التي تحسبها الشيفرة متطابقةً مع الإحداثيات التي تظهر عند النقر على الركن بالفأرة. ينبغي أيضًا أن تعمل الشيفرة مهما كانت قيم المقاسات والأطر، ليس فقط من أجل القيم المعطاة في نص التمرين. الحل الركنان الخارجيان تكون إحداثيات الركنين الخارجيين عادةً هي الإحداثيات التي تعيدها الدالّة elem.getBoundingClientRect()‎. في الشيفرة التالية، تمثّل answer1 إحداثيات الركن العلوي الأيسر، وتمثّل answer2 إحداثيات الركن السفلي الأيمن. let coords = elem.getBoundingClientRect(); let answer1 = [coords.left, coords.top]; let answer2 = [coords.right, coords.bottom]; الركن الداخلي العلوي الأيسر الفرق بينه وبين الركن الخارجي هو عرض الإطار. والطريقة المثلى للحصول على هذه المسافة هي باستعمال الخاصيتين clientLeft وclientTop. let answer3 = [coords.left + field.clientLeft, coords.top + field.clientTop]; الركن الداخلي السفلي الأيمن علينا في هذه الحالة طرح عرض الإطار من الإحداثيات الخارجية. ويمكننا استعمال لغة CSS كالآتي: let answer4 = [ coords.right - parseInt(getComputedStyle(field).borderRightWidth), coords.bottom - parseInt(getComputedStyle(field).borderBottomWidth) ]; هناك أيضًا طريقةٌ بديلةٌ، وربما هي أحسن، تكون بإضافة قيم الخاصيتين clientWidth وclientHeight لإحداثيات الركن العلوي الأيسر. let answer4 = [ coords.left + elem.clientLeft + elem.clientWidth, coords.top + elem.clientTop + elem.clientHeight ]; يمكنك الاطلاع على شيفرة الحل عبر هذا الرابط إظهار ملاحظة بمحاذاة عنصرٍ ما درجة الأهمية: 5 أنشئ الدالّة positionAt(anchor, position, elem)‎ التي تضع العنصرelem في موضعٍ position قريبٍ من العنصرanchor. يكون الموضع position عبارة عن سلسلة نصية ويأخذ إحدى القيم التالية: "top": تضع العنصر elem فوق العنصرanchor مباشرة. "right": تضع العنصرelem على يمين العنصرanchor مباشرة. "bottom": تضع العنصرelem أسفل العنصر anchor مباشرة. تُستعمل هذه الدالّة بداخل الدالّة showNote(anchor, position, html)‎ (الشيفرة الخاصة بالدالّة معطاة في نص التمرين) التي تُنشئ عنصرًا يحتوي على ملاحظة من خلال الشيفرة html المُمَرّرة لها، وتظهر هذه الملاحظة في الموضع position المُمَرر لها بمحاذاة العنصر anchor، وهذه صورة تمثيلية للنتيجة التي نريد الوصول اليها: يمكنك الاطلاع على شيفرة التمرين عبر هذا الرابط. الحل في هذا التمرين نحتاج فقط حسابَ الإحداثيات بشكلٍ دقيقٍ. ولتفاصيل أكثر، اطلع على شيفرة الحل. لاحظ أن العناصر ينبغي أن تكون موجودةً في الصفحة حتى يتسنى لنا قراءة الخاصية offsetHeight وخاصيات أخرى. حيث أن العنصر المخفي (display:none) أو غير الموجود بالصفحة لا يملك مقاسات. يمكنك الاطلاع على الحل عبر هذا الرابط. إظهار ملاحظة بمحاذاة عنصرٍ ما (مطلق) درجة الأهمية: 5 عدِّل على حل التمرين السابق بحيث تستعمل الملاحظة موضعًا مطلقًا (position:absolute) بدلًا من الموضع الثابت (position:fixed). هذا ما يُبقِي الملاحظة بمحاذاة العنصر عند تمرير الصفحة. استعمل حل التمرين السابق كنقطة انطلاق، ولمعاينة التمرين أضف النمط التنسيقي التالي: <body style="height: 2000px"‎> الحل الحل بسيطٌ جدًا. غيّر الخاصية position الخاصة بالمحدِّد .note من position:fixed إلىposition:absolute في شيفرة CSS. استعمل الدالّة getCoords()‎ المعرّفة أعلاه للحصول على الإحداثيات بالنسبة للصفحة. يمكنك الإطلاع على الحل عبر هذا الرابط. وضع الملاحظة داخل العنصر (مطلق) درجة الأهمية: 5 استعمل التمرين السابق (إظهار ملاحظة بمحاذاة عنصرٍ ما [مطلق]) وعدّل على الدالّة positionAt(anchor, position, elem)‎ لتسمح بإضافة عنصر elem داخل العنصرanchor. والقيم الجديدة للموضع position هي: bottom-out ،right-out ،top-out: هي نفس المواضع المعرّفة في الحل الأول والتي تسمح بإضافة العنصرelem فوق أو على يمين أو تحت العنصر anchor. bottom-in، right-in، top-in: تسمح بإضافة العنصر elem داخل العنصر anchor بحيث يلتصق بالركن العلوي أو الأيمن أو السفلي. على سبيل المثال: // تظهر الملاحظة فوق إطار المقولة positionAt(blockquote, "top-out", note); // تظهر الملاحظة داخل إطار المقولة في الأعلى positionAt(blockquote, "top-in", note); والنتيجة تكون كالآتي: خذ شيفرة حل التمرين السابق (إظهار ملاحظة بمحاذاة عنصرٍ ما [مطلق]) واستعن بها كشيفرة نص لهذا التمرين. الحل يمكنك الاطلاع على الحل عبر هذا الرابط. ترجمة -وبتصرف- للفصل Coordinates من كتاب Browser: Document, Events, Interfaces
  5. هل تتساءل عن كيفية الحصول على طول وعرض نافذة المتصفح؟ وكيف لك أن تعرف الطول والعرض الكلي للصفحة بما فيه الجزء الذي لا يُؤشِر عليه شريط التمرير (الجزء من الصفحة غير الظاهر في النافذة)؟ وكيف يمكن تمرير محتوى الصفحة باستعمال لغة جافاسكربت؟ فيما يلي الأجوبة الشافية لكل هذه التساؤلات. يمكن، في أغلب هذه الحالات، استعمال العنصر الجذر للصفحة document.documentElement الذي يقابِل الوسم <html>، بالإضافة إلى بعض الدوالّ والخاصيات التي يُستحسن أخذها في الحسبان نظرًا لأهميتها. طول وعرض النافذة تُستخدم الخاصيتان clientHeight وclientWidth لمعرفة طول وعرض النافذة. حيث تُظهِرالتعليمة، في المثال التالي، طول نافذة المتصفح: alert( document.documentElement.clientHeight ); ملاحظة: لا تستعمل window.innerWidth وwindow.innerHeight تدعم المتصفحات الخاصيتين window.innerWidth وwindow.innerHeight، ويبدو أنهما تتوافقان مع ما نريده، فلماذا لا نستخدمهما بدلًا من الخاصيتين اللّتان ذكرناهما سابقا؟ في حالة وجود شريط تمرير في الصفحة، تعطي الخاصيتان clientHeight وclientWidth طول وعرض النافذة بعد طرح عرض شريط التمرير منه. أو بعبارة أخرى، تعطيان طول وعرض الجزء الظاهر من الصفحة والمخصص للمحتوى. وتعطي الخاصيتان window.innerWidth وwindow.innerHeight الطول والعرض الكليين للنافذة بما فيهما عرض شريط التمرير. في حالة ما إذا كان لدينا شريط تمرير يشغل مساحة ما من النافذة، تعطي التعليمتان التاليتان نتائج مختلفة: alert( window.innerWidth ); // العرض الكلي للنافذة alert( document.documentElement.clientWidth ); // عرض النافذة بعد طرح عرض شريط التمرير منه See the Pen JS-p2-10-Window sizes and scrolling-ex1 by Hsoub (@Hsoub) on CodePen. نحتاج، في أغلب الحالات، معرفة عرض النافذة المتاح (المخصص للمحتوى)، لرسم شيء ما أو وضعه، أي دون احتساب عرض شريط التمرير إن وُجد، لذا علينا استعمال الخاصيتين documentElement.clientHeight وdocumentElement.clientWidth. ملاحظة: التعليمة DOCTYPE مهمة لاحظ أن الخاصيات رفيعة المستوى المتعلقة بالهندسة يمكن أن لا تعمل بالشكل المطلوب إذا لم يحوي ملف HTML التعليمة <!DOCTYPE HTML> ، فمن الممكن أن تحدث أشياء غريبة بسبب ذلك. أضف دائمًا إذًا التعليمة DOCTYPE عند كتابة شيفرة HTML على الطريقة العصرية. طول وعرض الصفحة نظريا، وبما أن العنصر الجذر للصفحة هو document.documentElement، ويضُم كلّ المحتوى، يمكن قياس العرض والطول الكليين للصفحة باستعمال الخاصيتين document.documentElement.scrollWidth وdocument.documentElement.scrollHeight، ولكن هاتين الخاصيتين، عند استعمالهما على هذا العنصر، لا تعملان بالشكل المطلوب إن لم يكن هناك شريط تمرير في كلّ من المتصفحات Chrome/Safari/Opera، حيث يمكن أن تعطي الخاصية documentElement.scrollHeight قيمةً أصغر من القيمة التي تعطيها الخاصية documentElement.clientHeight. يبدو الأمر غريبا نوعا ما، أليس كذلك؟ للحصول إذًا على القيمة الصحيحة للطول الكلي للصفحة، علينا أخذ أكبر قيمة من بين القيم التالية: let scrollHeight = Math.max( document.body.scrollHeight, document.documentElement.scrollHeight, document.body.offsetHeight, document.documentElement.offsetHeight, document.body.clientHeight, document.documentElement.clientHeight ); alert('Full document height, with scrolled out part: ' + scrollHeight); See the Pen JS-p2-10-Window sizes and scrolling-ex2 by Hsoub (@Hsoub) on CodePen. لماذا يحدث ذلك؟ يُستحسن عدم طرح هذا السؤال لأن هذه التناقضات موجودة منذ زمن ولا تخضع لتفكير منطقي "ذكي". حساب إحداثيات الموضع الحالي لعنصر ما تملك عناصر تمثيل الصفحة ككائن DOM خاصيتين تحملان إحداثيات موضعها من الصفحة وهما: elem.scrollLeft وelem.scrollTop. فبالنسبة لموضِع النافذة، تعمل الخاصيتان document.documentElement.scrollLeft وdocument.documentElement.scrollTop في أغلب المتصفحات، عدا تلك التي تعتمد على محرك WebKit مثل المتصفح Safari (يوّلد استعمالها الخطأ البرمجي 5991)، حيث ينبغي في هذه الحالة استعمال document.body بدلا من document.documentElement. ومن حسن الحظ أنه ليس علينا على الإطلاق تذكُّر كلّ هذه الخاصيات، حيث توجد خاصيتان غيرهما تعطيان الموضع الحالي للنافدة، إنهما الخاصيتان window.pageXOffset وwindow.pageYOffset، وهما خاصيتان قابلتان فقط للقراءة. alert('Current scroll from the top: ' + window.pageYOffset); alert('Current scroll from the left: ' + window.pageXOffset); See the Pen JS-p2-10-Window sizes and scrolling-ex3 by Hsoub (@Hsoub) on CodePen. التمرير باستعمال الدوال scrollTo وscrollBy وscrollIntoView ملاحظة هامة لا يمكن تمرير الصفحة إلّا بعد البناء الكلي لنموذج تمثيل الصفحة ككائن (DOM)، فمن غير الممكن كتابة شيفرة لتمرير الصفحة ووضعها في الجزء <head> أي تنفيذها قبل أن تجهز الصفحة فلن يعمل ذلك حتما. ويمكن تمرير العناصر العادية بتغيير قيم الخاصيتين scrollTop وscrollLeft، كما يمكن عمل الشيء نفسه للصفحة باستعمال الخاصيتين document.documentElement.scrollLeft وdocument.documentElement.scrollTop، عدا في المتصفح Safari (حيث ينبغي استخدام الخاصيتين document.body.scrollLeft وdocument.body.scrollTop بدلا منها). وهناك أيضا طريقةٌ بديلةٌ، أبسط من الأولى، تعمل في كافة المتصفحات، وتكون باستعمال الدالّتين window.scrollBy(x,y)‎ وwindow.scrollTo(pageX,pageY)‎. تحرّك الدالّة scrollBy(x,y)‎ النافذة من مكانها الحالي بمقدار معين. حيث تُحرِك التعليمة window.scrollBy(0,10)‎ مثلا الصفحة إلى الأسفل بمقدار عشرة بكسل. وتحرِّك الدالّة scrollTo(pageX,pageY)‎ الصفحة إلى موضعٍ ذي إحداثياتٍ مطلقةٍ. بحيث تكون إحداثيات الركن العلوي الأيسر للجزء الظاهر من الصفحة هي (pageX, pageY) إنطلاقا من الركن العلوي الأيسر للصفحة. تعمل هذه الدالّة تماما كما لو أننا نُسنِد قِيَمًا للخاصيتين scrollLeft وscrollTop. مثلًا لتحريك النافذة إلى نقطة البداية (العودة إلى أعلى الصفحة)، يمكن استعمال التعليمة window.scrollTo(0,0)‎. وتعمل هذه الدوالّ بنفس الطريقة (تعطي النتائج نفسها) على كلّ المتصفحات. الدالة scrollIntoView دعنا نكمل ما بدأناه بشرح الدالّة elem.scrollIntoView(top)‎. ينتج عن مناداة هذه الدالّة تمرير الصفحة بحيث تجعل العنصر elem ظاهرًا، ولها وسيط واحد: إذا كانت قيمة الوسيط top تساوي true (وهي القيمة المبدئية -الافتراضية)، تُمرَّر الصفحة بحيث يظهر العنصر elem أعلى النافذة فتتطابق حافته العلوية مع أعلى النافذة. وإذا كانت قيمة الوسيط top تساوي false، تُمرَّر الصفحة بحيث يظهر العنصر elem أسفل النافذة فتتطابق حافته السفلى مع أسفل النافذة. مثال: إذا كان لدينا زر، فإن هذه التعليمة this.scrollIntoView()‎ تُحرِك الصفحة بحيث يظهر هذا الزر أعلى النافذة. أما هذه التعليمة this.scrollIntoView(false)‎، فتُحرِك الصفحة بحيث يظهر الزر أسفل النافذة. تثبيط قابلية التمرير نحتاج أحيانا إلى جعل الصفحة غير قابلة للتمرير، فنرغب مثلًا في إخفائها وراء رسالة طويلة للفت انتباه الزائر وحمله على التفاعل مع الرسالة وليس مع الصفحة. وهنا يكفي إسناد القيمة "hidden" للخاصية document.body.style.overflow حيث تثبِّت (تجمِّد) هذه التعليمة الصفحة في موضعها الحالي. فلتجرب ذلك باستعمال التعليمتين document.body.style.overflow="hidden" و document.body.style.overflow="". هنا، تثبِّط التعليمة الأولى قابلية التحريك، أما التعليمة الثانية فتعاود تفعيلها. ويمكننا استعمال هذه الطريقة لتجميد/تثبيت عناصر أخرى أيضا، ليس العنصر document.body فقط. غير أن لهذه الدالّة نقطة سلبية، حيث يختفي شريط التمرير تماما بعد مناداتها، وبما أنه كان يشغل مساحة ما من الصفحة، فعند اختفائه، يتحرك محتوى الصفحة بعض الشيء لملء الفراغ الذي نتج عن اختفاء شريط التمرير. يبدو هذا غريبا بعض الشئ، ولكن يمكننا تفادي حدوث ذلك بموازنة قيمة الخاصية clientWidth قبل وبعد التثبيط، فإذا ازداد العرض بعد اختفاء شريط التمرير، نضيف حاشية padding للعنصرdocument.body مكان شريط التمرير للإبقاء على عرض محتوى الصفحة كما هو. الخلاصة لمعرفة طول/عرض الجزء الظاهر من الصفحة (طول/عرض مساحة المحتوى) نستعمل الخاصيتين document.documentElement.clientWidth/Height. *لمعرفة الطول/العرض الكلي للصفحة بما فيها الجزء غير الظاهر في النافذة نبحث عن أكبر قيمة من بين القيم التالية: let scrollHeight = Math.max( document.body.scrollHeight, document.documentElement.scrollHeight, document.body.offsetHeight, document.documentElement.offsetHeight, document.body.clientHeight, document.documentElement.clientHeight ); لقراءة إحداثيات موضع الجزء الظاهر من الصفحة إنطلاقا من الركن العلوي الأيسر للصفحة نستعمل الخاصيتين window.pageYOffset/pageXOffset. ولتغيير الموضع الحالي للنافذة نستعمل الدوالّ التالية: window.scrollTo(pageX,pageY): التحريك نحو الموضع ذي الإحداثيات المطلقة (pageX,pageY). window.scrollBy(x,y) : التحريك بمقدار (x,y) انطلاقا من الموضع الحالي. elem.scrollIntoView(top): التحريك لجعل العنصر elem ظاهرًا، أي تحريكه بحيث يتطابق مع أعلى أو أسفل النافذة حسب قيمة الوسيط top. ترجمة -وبتصرف- للفصل Window sizes and scrolling من كتاب Browser: Document, Events, Interfaces
  6. تضم جافاسكربت العديد من الخاصيات التي تسمح بقراءة معلومات طول عنصر ما (height)، وعرضه (width) وجوانب أخرى متعلقة بهندسته (geometry). هي معلومات نحتاجها عادة متى ما أردنا تحريك أو وضع عناصر ما على صفحة ويب باستعمال لغة جافاسكربت. العنصر النموذجي نستعين فيما يلي بالعنصر النموذجي التالي من أجل عرض الخاصيات: <div id="example"> ...Text... </div> <style> #example { width: 300px; height: 200px; border: 25px solid #E8C48F; padding: 20px; overflow: auto; } </style> See the Pen JS-p2-09-Element size and scrolling-ex1 by Hsoub (@Hsoub) on CodePen. يملك هذا العنصر إطارًا (border)، وحاشية (padding) وإحداثيات تمرير (scrolling)؛ أي مجموعة كاملة من الخاصيات. في حين أنه لا يملك هوامش (margins) لأنها لا تُعدّ جزءًا منه وليست لها خاصيات. ويبدو العنصر على الشكل التالي: يمكنك الاطلاع على الشيفرة ومعاينتها على هذا الرابط. ملاحظة: شريط التمرير تُظهر الصورة السابقة أكثر الحالات تعقيدًا، أي حين يكون للعنصر شريط تمرير،حيث تُخصِص بعض المتصفحات ( وليس كلّها) مساحةً لشريط التمرير من المساحة المخصصة للمحتوى، والمؤشَّر عليها في الصورة بعبارة 'عرض المحتوى' (content width). وبذلك يكون عرض المحتوى هنا 300 بكسل في حالة عدم وجود شريط تمرير. ولكن شَغلَ شريط التمرير لمساحةٍ عرضها 16 بكسل (يتغيّر هذا العرض من متصفح لآخر ومن جهاز لآخر) يُصغِّر من عرض المحتوى ليصبح 300-16=284 بكسل. وعليه فإنه من الأهمية بمكان أخذ ذلك في الحسبان. يُسهِّل عدم وجود شريط التمرير العمليات الحسابية، ولكننا نفترض في الأمثلة الخاصة بهذا الفصل أنه موجود. ملاحظة: يمكن لمساحة الحاشية السفلى padding-bottom أن تحتوي نصا عادة ما تَظهر الحواشي فارغةً من أي محتوى. غير أن نص العنصر قد يكون طويلا جدًا بحيث لا تسعه مساحة المحتوى كاملا، فتُظهِره المتصفحات عادة في الحاشية السفلى للعنصر. الخاصيات الهندسية لعنصر ما فيما يلي صورة شاملة تُظهِر الخاصيات الهندسية لعنصر ما. تكون قيم هذه الخاصيات عبارة عن أرقام، ولكنها تُمثِّل في الحقيقة عدد البكسلات، فهي إذا مقاسات بالبكسل. فلنستعرض هذه الخاصيات إنطلاقًا من خارج العنصر نحو داخله. الخاصيات offsetParent وoffsetLeft وoffsetTop نادرًا ما نحتاج إلى هذه الخاصيات ولكنها تصف العنصر من الخارج ، لذا سنبدأ العرض بها: تمثل الخاصية offsetParent السلف الأقرب (ancestor) للعنصر ويستعملها المتصفح لحساب الإحداثيات أثناء ترجمة الشيفرات إلى صفحات ويب، ويكون السلف الأقرب إمّا: متموضع باستعمال لغة CSS: يكون الموضع (position) إمّا مطلقا (absolute)، أو نسبي (relative)، أو ثابت (fixed) أو لاصق (sticky) أو، أحد العناصر: <td> ،<th> ،<table>، أو، العنصر <body>. وتعطي، في المثال التالي، الخاصيتان offsetLeft وoffsetTop الإحداثيات x وy إنطلاقًا من الركن الأيسر العلوي للسلف الأقرب offsetParent. ويُعدّ العنصر <main> السلف الأقرب offsetParent للعنصر <div> المحصور داخله، وتحسب الخاصيتان offsetLeft وoffsetTop إنطلاقًا من الركن العلوي الأيسر للعنصر <main>: <main style="position: relative" id="main"> <article> <div id="example" style="position: absolute; left: 180px; top: 180px">...</div> </article> </main> <script> alert(example.offsetParent.id); // main alert(example.offsetLeft); // 180 (*) alert(example.offsetTop); // 180 </script> See the Pen JS-p2-09-Element size and scrolling-ex2 by Hsoub (@Hsoub) on CodePen. (*): لاحظ أن القيمة هنا عبارة عن رقم (180) وليس سلسلة نصية “180px”. هناك عدة حالات تحمل فيها الخاصية offsetParent القيمة null: إذا كان العنصر غير ظاهر (display:none أو غير ظاهرٍ في الصفحة) إذا كان العنصر هو <body> أو <html>، إذا كان موضع العنصر يحمل القيمة position:fixed. offsetWidth وoffsetHeight نباشر الآن في عرض خاصيات العنصر نفسه. تُعدّ الخاصيتان offsetHeight وoffsetWidth أبسط الخاصيات، حيث تعطيان الطول والعرض الخارجي الكلي للعنصر، بما في ذلك الأطر. وفيما يتعلّق بالعنصر النموذجي لدينا: offsetWidth = 390: العرض الكلي ويمكن حسابه بجمع العرض الداخلي المحدَّد في شيفرة CSS (أي 300 بكسل)، والحاشيتين (220 بكسل) و الإطارين (225 بكسل). offsetHeight = 290: الطول الخارجي. ملاحظة: تأخذ الخاصيات الهندسية للعناصر غير الظاهرة على الصفحة إحدى القيمتان 0 أو null لا تُحسب الخاصيات الهندسية إلا إذا تعلّق الأمر بعناصر ظاهرة على الصفحة. إذا كان العنصر أو أيًا من أسلافه يملك الخاصية display:none أو لم يكن ظاهرًا في الصفحة، تكون قيم كل الخاصيات الهندسية الخاصة به معدومة ( أو null، إذا تحدثنا عن الخاصية offsetParent). فعلى سبيل المثال، تحمل الخاصية offsetParent القيمة null، والخاصيتان offsetWidth وoffsetHeight القيمة 0 إذا أنشأنا عنصرًا ما ولم نُضفه إلى الصفحة أو إذا كانت الخاصية display الخاصة به أو بأسلافه محدّدة بقيمة none. ويمكن التحقّق ممّا إذا كان العنصر ظاهرًا أو مخفيًا كالآتي: function isHidden(elem) { return !elem.offsetWidth && !elem.offsetHeight; } لاحظ هنا أن الدالّة isHidden تعيد القيمة true أيضًا إذا كان العنصر ظاهرًا على الصفحة ولكن مقاساته منعدمة، كالعنصر <div> مثلا إذا كان فارغًا. clientTop وclientLeft داخل العنصر، توجد الأطر، ولقياسها لدينا الخاصيتان clientTop وclientLeft، حيث تكون قيم كلُّ من الخاصيتين في المثال السابق كالآتي: clientLeft = 25، عرض الإطار الأيسر. clientTop = 25، عرض الإطار العلوي. وحتى نكون دقيقين علينا أن ننبّه لأمر ما، وهو أن هاتين الخاصيتين لا تمثلان طول وعرض الإطار، بل الإحداثيات النسبية للجانب الداخلي إنطلاقًا من الجانب الخارجي. ما الفرق بينهما؟ يَظهر الفرق عندما يكون اتجاه الصفحة من اليمين إلى اليسار (إذا كانت لغة المتصفح هي العربية أو العبرية)، حيث يَظهر شريط التمرير في هذه الحالة على الجهة اليسرى بدل اليمنى، وبذلك تضم الخاصية clientLeft عرض شريط التمرير أيضا. وهنا لا تكون قيمة الخاصية clientLeft هي 25، بل 25 زائد عرض شريط التمرير (25 + 16 = 41) . وفيما يلي مثال باللغة العبرية: clientWidth وclientHeight تعطي هاتان الخاصيتان مقاسات المساحة المحصورة بين إطاري العنصر، وتضمّان عرض المحتوى بما فيه الحاشيتين، ولكن دون عرض شريط التمرير. فلنتحدث أولا عن الخاصية clientHeight المذكورة في الصورة أعلاه. لا وجود في الصورة لشريط تمرير أفقي، فقيمتها إذًا تساوي بالتدقيق مجموع ماهو محصور بين الإطارين، أي الطول المحدَّد في شيفرة CSS (أي 200 بكسل) زائد الحاشيتين السفلية والعلوية (2*20 بكسل) وهو ما يعطي القيمة 240 بكسل. أما بالنسبة للخاصية clientWidth، فلا يساوي عرض المحتوى 300 بكسل، بل 240 بكسل، ذلك لأن شريط التمرير يشغَل مساحةً عرضها 16 بكسل. فالنتيجة تكون 284 بكسل زائد عرض الحاشيتين اليمنى واليسرى. المجموع إذا 324 بكسل. وفي حالة عدم وجود الحاشيتين تساوي قيمة الخاصيتين clientWidth وclientHeight بالتدقيق عرض وطول مساحة المحتوى المحصورة بين الأطر وشريط التمرير إن وجد. يمكننا إذا استعمال الخاصيتين clientWidth وclientHeight للحصول على مقاسات مساحة المحتوى إن لم يكن للعنصر حواشي. scrollWidth وscrollHeight تشبه هاتان الخاصيتان الخاصيتين clientWidth وclientHeight، ولكنهما تضمان أيضًا الأجزاء غير الظاهرة. لدينا في الصورة أعلاه: scrollHeight = 723، وتمثل الطول الداخلي الكلي لمساحة المحتوى بما فيه الأجزاء غير الظاهرة. scrollWidth = 324، وتمثل العرض الداخلي الكلي، وبما أنه لا وجود لشريط تمرير أفقي، فهي تساوي clientWidth. ويمكننا استخدام هاتين الخاصيتين لتوسيع العنصر إلى طوله أو عرضه الكلي كالآتي: // توسيع محتوى العنصر إلى طوله الكلي element.style.height = `${element.scrollHeight}px`; حيث يختفي هنا شريط التمرير، ويظهر محتوى العنصر كاملا. scrollLeft وscrollTop تمثل الخاصيتان scrollLeft وscrollTop عرض وطول الجزء غير الظاهر من العنصر (الذي جرى المُرور عليه). لاحظ في الصورة الموالية الخاصيتين scrollHeight وscrollTop لمقطع يضم شريط تمرير عمودي. بعبارة أخرى، تمثل الخاصية scrollTop مقدار تمرير العنصر إنطلاقًا من أعلى الصفحة. ملاحظة: يمكن التعديل على الخاصيتين scrollLeft وscrollTop تُعدّ أغلب الخاصيات التي استعرضناها هنا قابلة للقراءة فقط (read-only)، ولكن الخاصيتين scrollLeft وscrollTop يمكن تغييرهما ليحرِّك المتصفح العنصر. إذا كان لدينا عنصرًا ما، وليكن elem، فإن التعليمة elem.scrollTop += 10 تُحرِّكه بمقدار 10 بكسل نحو الأسفل. ويؤدي إسناد القيمة 0 أو أيّ قيمةٍ كبيرةٍ جدًا (مثلا 1e9) للخاصية scrollTop إلى تحريك العنصر إلى أقصى أعلى أو أسفل الصفحة. لا تأخذ طول وعرض العنصر من شيفرة CSS تحدثنا أعلاه عن الخاصيات الهندسية لعناصر DOM التي يمكن استخدامها للحصول على الطول والعرض ولحساب المسافات. وسبق لك وأن تعرفت في فصل الأنماط التنسيقية والأصناف، أنه يمكن قراءة الطول والعرض المحدَّدّين في شيفرة CSS لعنصر ما باستعمال الدالّة getComputedStyle، فلماذا لا نقرأ عرض عنصرٍ ما باستعمال الدالّة getComputedStyle، كما في المثال التالي؟ let elem = document.body; alert( getComputedStyle(elem).width ); // إظهار عرض العنصر المحدَّدّ في شيفرة CSS See the Pen JS-p2-09-Element size and scrolling-ex3 by Hsoub (@Hsoub) on CodePen. لماذا عليك استعمال الخاصيات الهندسية بدلًا من هذه الدالّة؟ هناك سببان لذلك: الأول، هو أن الطول والعرض المحدَّدين في شيفرة CSS يتعلّقان بخاصية أخرى وهي box-sizing، التي تحدِّد الطول والعرض في شيفرة CSS. ويمكن لأيّ تغيير في الخاصية box-sizing أن يوقِف عمل شيفرة جافاسكربت. والثاني، هو أن الطول والعرض المحدَّدين في شيفرة CSS، قد يحملان القيمة "auto" كما هو الحال بالنسبة للعناصر السطرية (inline element). مثال: <span id="elem">Hello!</span> <script> alert( getComputedStyle(elem).width ); // auto </script> See the Pen JS-p2-09-Element size and scrolling-ex4 by Hsoub (@Hsoub) on CodePen. يُعدّ إسناد القيمة "auto" للخاصية width أمرًا عاديًا جدًا في لغة CSS، ولكن جافاسكربت يتطلّب مقاسا دقيقا بالبكسل حتى يستخدمه في العمليات الحسابية. لذلك لن يفيدك العمل بالعرض المحدَّد في شيفرة CSS في هذه الحالة. هناك أيضا سببٌ آخر يتعلّق بشريط التمرير. حيث يحدث أن يعمل سكربت معين بصفة عادية إن لم يكن هناك شريط تمرير، في حين يُظهِر العديدَ من الأخطاء البرمجية في حالة وجوده. هذا لأن شريط التمرير يَشغَل مساحةً من المحتوى في بعض المتصفحات، فيكون العرض الحقيقي المتاح للمحتوى أقل من العرض الذي تحدِّده شيفرة CSS، وهو ما تأخذه الخاصيتان clientWidth وclientHeight في الحسبان. ولكن الأمر يختلف عند استخدام التعليمة getComputedStyle(elem).width، حيث تُعيد بعض المتصفحات (مثل المتصفح Chrome) العرض الداخلي الحقيقي بعد طرح عرض شريط التمرير منه، وتُعيد بعض المتصفحات الأخرى (مثل المتصفح Firefox) العرض المحدَّد في شيفرة CSS (حيث يُهمِل هذا المتصفح شريط التمرير). ويُعدّ هذا الاختلاف من متصفح لآخر هو السبب الذي يحمِلنا على عدم استعمال الدالّة getComputedStyle بل الاعتماد على الخاصيات الهندسية بدلا منها. وإذا كان المتصفح المثبَّت على حاسوبك يُخصِص مساحةً لشريط التمرير (وهو ما تُوفره معظم متصفحات نظام التشغيل Windows) ، يمكنك معاينة السكربت من خلال هذا الرابط هنا، يساوي عرض العنصر المحدَّد في شيفرة CSS الذي يضم النص 300 بكسل. تُخصِص المتصفحات Chrome وFirefox وEdge على نظام التشغيل Windows المكتبي مساحةً لشريط التمرير. ولكن المتصفح Firefox يُظهر القيمة 300 بكسل، في حين يُظهر المتصفح Chrome والمتصفح Edge قيمةً أقل، لأن المتصفح Firefox يُظهر العرض المحدَّد في شيفرة CSS، أمّا بقية المتصفحات فتُعيد العرض الحقيقي للعنصر (بعد اقتطاع عرض شريط التمرير). لاحظ أن الفرق يظهر فقط عند قراءة القيمة باستعمال الدالّة getComputedStyle(...).width في شيفرة جافاسكربت، حيث يكون الشكل الذي يَظهر على الشاشة صحيحًا. الخلاصة تملك العناصر الخاصيات الهندسية التالية: offsetParent: تمثل السلف الأقرب للعنصر من حيث التموضع أو إحدى القيم التالية: td أو th أوtable أوbody. offsetLeft وoffsetTop: تمثل إحداثيات العنصر إنطلاقًا من الحافة العلوية اليسرى للعنصر السلف offsetParent الخاص به. offsetWidth وoffsetHeight: تمثل العرض والطول الخارجي لعنصر ما بما فيه الأطر. clientLeft وclientTop: تمثلان المسافتين بين الركن العلوي الأيسر الخارجي والركن العلوي الأيسر الداخلي (المحتوى + الحاشية). بالنسبة لأنظمة التشغيل التي تعمل باللّغات التي تُكتَب من اليسار إلى اليمين، تساوي هاتان الخاصيتان عرض الإطارين العلوي والأيسر، أما بالنسبة لأنظمة التشغيل التي تعمل باللّغات التي تُكتَب من اليمين إلى اليسار، يكون شريط التمرير من الجهة اليسرى، وبذلك تضم الخاصية clientLeft عرض شريط التمرير أيضًا. clientWidth وclientHeight: تمثلان عرض وطول المحتوى بما فيه الحواشي دون احتساب عرض شريط التمرير. scrollWidth وscrollHeight: تمثلان عرض وطول المحتوى، تماما مثل ما هو الحال بالنسبة للخاصيتين clientWidth وclientHeight، ولكن مع احتساب الأجزاء غير الظاهرة من العنصر. scrollLeft وscrollTop: تمثل عرض وطول الجزء العلوي للعنصر الذي جرى المُرور عليه إنطلاقًا من الركن العلوي الأيسر للعنصر. وتُعد كلّ هذه الخاصيات قابلة للقراءة فقط (read-only) عدا الخاصيتين scrollLeft وscrollTop اللّتين يؤدي تغيير قيمتهما إلى تحريك المتصفح للعنصر. تمارين مقاس الجزء الذي لم نمر عليه بعد درجة الأهمية: 5 تمثل الخاصية elem.scrollTop مقاس الجزء الذي جرى المُرور عليه إنطلاقًا من الأعلى. كيف يمكننا إذًا الحصول على مقاس الجزء الذي لم نمر عليه بعد (ولنسمه scrollBottom)؟ اكتب الشيفرة التي تسمح بالحصول على مقاس الجزء (من عنصر ما) الذي لم نمر عليه بعد، وليكن هذا العنصر elem. ملاحظة: عليك التحقّق من الشيفرة التي تكتبها، حيث ينبغي أن تعطي القيمة 0 إذا لم يكن هناك شريط تمرير، أو إذا كان العنصر متموضعًا أساسا في الأسفل. الحل الحل هو كالآتي: let scrollBottom = elem.scrollHeight - elem.scrollTop - elem.clientHeight; أو بعبارة أخرى، (الطول الكلي) ناقص (الجزء العلوي الذي جرى المُرور عليه) ناقص (الجزء الظاهر). هذا ما يسمح بالحصول على الجزء الذي لم نمر عليه بعد. ما هو عرض شريط التمرير؟ _درجة الأهمية: _3 اكتب الشيفرة التي تُعيد عرض شريط تمرير عادي. يكون هذا العرض بالنسبة لنظام التشغيل Windows بين 12 و20 بكسل. وفي حالة ما لم يُخصِص المتصفح مساحةَ لشريط التمرير (يحدث أن يكون الشريط نصف شفاف ويتموضع فوق النص)، يكون عرضه منعدما. ملاحظة: ينبغي أن تعمل الشيفرة على أيّ صفحة HTML كانت وأن لا تكون مرتبطة بمحتوى هذه الصفحة. الحل للحصول على عرض شريط التمرير، يمكن إنشاء عنصرٍ يملك شريط تمرير لكن دون أطر ولا حواشي. وبذلك يكون الفرق بين العرض الكلي للعنصر offsetWidth وعرض المساحة الداخلية للمحتوى clientWidth هو عرض شريط التمرير. // إنشاء حاوية تملك شريط تمرير let div = document.createElement('div'); div.style.overflowY = 'scroll'; div.style.width = '50px'; div.style.height = '50px'; // إضافتها للصفحة، وإلا فسوف نحصل على مقاسات منعدمة (0 بكسل) document.body.append(div); let scrollWidth = div.offsetWidth - div.clientWidth; div.remove(); alert(scrollWidth); وضع الكرة في مركز المساحة الخضراء درجة الأهمية: 5 تكون الصفحة مبدئيا كالآتي: ما هي إحداثيات مركز المساحة الخضراء؟ احسبها واتبِّع ما يلي لوضع الكرة في مركز المساحة الخضراء: حرِّك العنصر باستعمال لغة جافاسكربت، وليس لغة CSS. ينبغي أن تعمل الشيفرة مهما كان مقاس الكرة ومهما كان مقاس المساحة الخضراء وليس فقط مع المقاسات المعطاة. ملاحظة: يمكن وضع الكرة في المركز باستعمال لغة CSS، ولكننا نريد تجسيد ذلك باستعمال لغة جافاسكربت. سوف تُصادف مستقبلا مواضيع أخرى ووضعيات أكثر تعقيدًا، حيث يكون استعمال جافاسكربت ضرورة حتمية. يُعدّ هذا المثال مجرّد تمرين بسيط للتسخين. يمكنك الاطلاع على شيفرة التمرين من هنا الحل موضع الكرة هو position:absolute، وهو ما يعني أن إحداثياتها بالنسبة للأعلى ولليسار تُحسب بناءً على أقرب عنصر متموضع، أي العنصر ‎#field (لأنه يملك الموضع position:relative). يكون مبدأ الإحداثيات (0،0) متطابقا مع الركن العلوي الأيسر الداخلي للمساحة الخضراء. تُمثِل قيم الخاصيتين clientWidthوclientHeight العرض والطول الداخليين للمساحة الخضراء، وبالتالي تكون إحداثيات المركز كالآتي: (clientWidth/2, clientHeight/2). ولكننا حين نسند هذه القيم للخاصيتين ball.style.left وball.style.top نجعل الحافة العلوية اليسرى للكرة، وليس الكرة بأكملها، تتحرك نحو المركز، فتظهر على هذا الشكل: ball.style.left = Math.round(field.clientWidth / 2) + 'px'; ball.style.top = Math.round(field.clientHeight / 2) + 'px'; ومن أجل مطابقة مركز الكرة مع مركز المساحة الخضراء، ينبغي تحريك الكرة بمقدار نصف طولها للأعلى وبمقدار نصف عرضها نحو الجهة اليسرى. وبذلك تتموضع الكرة في مركز المساحة الخضراء. ball.style.left = Math.round(field.clientWidth / 2 - ball.offsetWidth / 2) + 'px'; ball.style.top = Math.round(field.clientHeight / 2 - ball.offsetHeight / 2) + 'px'; تحذير لا تعمل الشيفرة بالشكل المطلوب إذا لم يكن للكرة طول أو عرض. <img src="ball.png" id="ball"> إذا لم يجد المتصفح طول وعرض صورةٍ ما ضمن سمات الوسوم أو في شيفرة CSS، يعتدّها منعدمة إلى غاية التحميل الكلي للصورة ، وبالتالي تكون قيمة ball.offsetWidth منعدمة إلى غاية تحميل الصورة بشكل كامل. وهذا ما ينتج عنه إحداثيات خاطئة عند استعمال الشيفرة أعلاه. بعد عملية التحميل الأولى للصورة، يضع المتصفح الصورة في الذاكرة المؤقتة (cache)، وعند إعادة التحميل، يحصل المتصفح على المقاسات في الحين. لكن المشكل هو أن قيمة الخاصية ball.offsetWidth تكون منعدمة عند عملية التحميل الأولى. يمكن تفادي حصول ذلك بإسناد قيمة للسمتين width وheight الخاصتين بالصورة <img>: <img src="ball.png" width="40" height="40" id="ball"> أو إضافة المقاسات في شيفرة CSS كالآتي: #ball { width: 40px; height: 40px; } يمكنك الاطلاع على الحل من خلال هذا الرابط الفرق بين العرض المحدَّد في شيفرة CSS وclientWidth درجة الأهمية 5 ما هو الفرق بين getComputedStyle(elem).width و elem.clientWidth؟ أعط ثلاث فوارق على الأقل، ويستحسن أكثر. الحل الفوارق هي كالآتي: تُعدّ قيمة الخاصية clientWidth قيمة عددية، بينما تكون قيمة getComputedStyle(elem).width عبارة عن سلسلة نصية تحمل في نهايتها الحرفينpx. يمكن أن تُعيد الدالّة getComputedStyle قيمًا غير عددية كالقيمة "auto" مثلا بالنسبة للعناصر السطرية. تمثل الخاصية clientWidth مقاس المساحة الداخلية للمحتوى بالإضافة إلى الحواشي، بينما لا يضم العرض المحدَّد في شيفرة CSS (مع خاصية box-sizing عادية) الحواشي (يتمثل في مقاس المساحة الداخلية للمحتوى دون الحاشيتين). إذا كان هناك شريط تمرير وخَصص المتصفح مساحةً لهذا الشريط، قد تطرح بعض المتصفحات هذه المسافة من العرض المحدَّد في شيفرة CSS (لأنه لم يَعُد متاحا ليضم محتوى ما)، ولكن بعض المتصفحات الأخرى لا تسلك السلوك نفسه. أما الخاصية clientWidth فهي دائما تمثل نفس القيمة، حيث يُطرَح عرض شريط التمرير إذا خُصصت له مساحةً ما. ترجمة -وبتصرف- للفصل Element size and scrolling من كتاب Browser: Document, Events, Interfaces
  7. تجدر بنا الإشارة إلى قاعدة مهمة قبل الشروع في الحديث عن كيفية تعامل جافاسكربت مع الأنماط والأصناف. القاعدة بديهية جدًا ولكننا سنذكرها للإفادة. هناك طريقتان تُستخدمان لتنسيق عنصرٍ ما: إنشاء صنف في ملف CSS وإضافته للعنصر على الشكل التالي: <div class="..."‎> كتابة خاصيات السمة style مباشرة بين تسلسلات التهريب الخاصة بالعنصر على الشكل التالي: <div style="..."‎> تستطيع لغة جافاسكربت التعديل على خاصيات الأصناف وخاصيات الخاصية style. ويُستحسن استعمال أصناف CSS للتنسيق بدلا من السمة style، حيث نلجأ إلى الطريقة الثانية فقط إذا تعذّرعلينا إضافة التنسيق باستعمال الطريقة الأولى. فعلى سبيل المثال، يمكن استعمال الخاصية style إذا كان عليك حساب إحداثيات عنصر ما ديناميكيًا وتحديد قيمها باستعمال جافاسكربت كالآتي: let top = /* عمليات حسابية معقّدة */; let left = /* عمليات حسابية معقّدة */; elem.style.left = left; // e.g '123px', تُحسب أثناء التنفيذ elem.style.top = top; // e.g '456px' وفي حالات أخرى مثل تلوين النص بالأحمر، أو إضافة صورة للخلفية، يُستحسن وصف التنسيق باستعمال CSS ثم إضافة الصنف للعنصر (يمكن عمل ذلك باستعمال جافاسكربت)، حيث يمنحك ذلك أكثر مرونة وسهولة في البرمجة. اسم الصنف (className) وقائمة الأصناف (classList) يُعدّ تعديل الصنف أكثر عمليةٍ نُصادفها في السكربتات. وكانت جافاسكربت فيما مضى تتّسمُ بالمحدودية حين يتعلّق الأمر بالكلمة المحجوزة "class"، حيث لم تكن تسمح بأن تحمل خاصيةٌ من خواص الكائن (object) اسم "class" كالآتي : elem.class. حينها جاء التفكير في استحداث خاصية مشابهة تسمى "className" تُطبّق على الأصناف. حيث يمثِّل elem.className اسم السمة "class" كما هو مبين في المثال التالي: <body class="main page"> <script> alert(document.body.className); // main page </script> </body> See the Pen JS-p2-08-Styles and classes-ex1 by Hsoub (@Hsoub) on CodePen. في حالة ما إذا أسندنا قيمةً معينةً للخاصية elem.className، تعوِّض هذه القيمة مُجمل سلسلة الأصناف. هذا ما نحتاج إليه أحيانا، ولكننا نحتاج في غالبية الحالات إلى إضافة أو حذف صنفٍ واحدٍ فقط. ولهذا وُجدت خاصية أخرى؛ إنها خاصية elem.classList. تُعدّ هذه الخاصية كائنًا خاصًا بحد ذاته له دوالّه الخاصة (methods) لإضافة صنف ما (add) أو حذفه (remove) أو إما إضافته إن لم يكن موجودًا أو حذفه إن وُجد (toggle).كما هو مبين في المثال التالي: <body class="main page"> <script> // إضافة صنف document.body.classList.add('article'); alert(document.body.className); // main page article </script> </body> See the Pen JS-p2-08-Styles and classes-ex2 by Hsoub (@Hsoub) on CodePen. وبذلك يمكننا إجراء عمليات على مجمل سلسلة الأصناف دفعة واحدة باستعمال className أو على الأصناف، كلٌّ على حدى، باستعمال classList. اختيارنا لهذا أو ذاك مرتبط بما نحتاج القيام به. الدوالّ الخاصة بـالخاصية classList هي: *elem.classList.add/remove("class")‎: إضافة الصنف المذكور كوسيط للدلّة/حذف الصنف المذكور كوسيط للدلّة. *elem.classList.toggle("classe")‎: إضافة الصنف المذكور كوسيط للدلّة إن لم يكن موجودًا أو حذفه إن وُجد. *elem.classList.contains("class")‎: البحث عن الصنف المذكور كوسيط للدلّة، والنتيجة تكون صحيح أو خطأ (true/false). وتقبل الخاصية classList الإدماج داخل حلقة التكرار for....of لإظهار قائمة الأصناف كما في المثال التالي: <body class="main page"> <script> for (let name of document.body.classList) { alert(name); // main, and then page } </script> </body> See the Pen JS-p2-08-Styles and classes-ex3 by Hsoub (@Hsoub) on CodePen. تنسيق العنصر باستعمال الخاصية style تُعدّ الخاصية elem.style كائنًا يحمل محتوى السمة style. ويؤدي إسناد القيمة "100px" للخاصية elem.style.width على الشكل التالي: elem.style.width="100px" إلى النتيجة نفسها لو كانت السمة style تحمل السلسلة النصية "width:100px". إذا كان اسم السمة يتكون من عدة كلمات، يُشكَّل اسم الخاصية بجعل الحرف الأول من كل كلمة حرفا كبيرًا ماعدا الكلمة الأولى كما في المثال التالي: background-color => elem.style.backgroundColor z-index => elem.style.zIndex border-left-width => elem.style.borderLeftWidth ملاحظة: الخاصيات التي تبدأ ببادئة تتبع الخاصيات التي تبدأ ببادئة تُحدّد المتصفح نفس القاعدة، كالخاصيتين -moz-border-radius و-webkit-border-radius، حيث تُترجَم الشرطة إلى حرفٍ كبيرٍ كالآتي: button.style.MozBorderRadius = '5px'; button.style.WebkitBorderRadius = '5px'; تغيير قيمة خاصية التنسيق style يحدث أن تَرغب في إسناد قيمةٍ للخاصية style ثم حذفها لاحقا. يمكننا على سبيل المثال إسناد القيمة "none" للخاصية elem.style.display كالأتي elem.style.display = "none" ثم حذفها وكأننا لم نحدّد لها قيمةً من قبل. هنا، ينبغي إسناد سلسلة نصية فارغة للخاصية elem.style.display كالآتي elem.style.display = ""‎ بدلا من حذفها (delete). //عند تنفيذ هذا السكربت يختفي العنصر <body> ثمّ يُعاود الظهور document.body.style.display = "none"; // يختفي setTimeout(() => document.body.style.display = "", 1000); // يُعاود الظهور إذا أسندنا سلسلة نصية فارغة للخاصية style.display، يُطبِّق المتصفح أصناف CSS والأنماط التنسيقية المتضمَّنة بداخلها بطريقة عاديةٍ جدًا وكأن الخاصية style.display غير موجودة تماما. ملاحظة: التعديل على خاصيات الخاصية style جملةً واحدةً باستعمال الخاصية style.cssStyle تُستعمل عادة الخاصية *.style للتعديل على قيم خاصيات التنسيق، كلٌ على حدى، ولا يمكننا التعديل عليها دفعة واحدة كالآتي: div.style="color:red; width:100px"‎، لأن div.style هو كائنٌ لا يمكن التعديل عليه (ِread-only) بهذه الطريقة. يمكن تغيير التنسيق كاملا دفعة واحدة بإسناد سلسلة نصية (تحمل وصف التنسيق) للخاصية style.cssStyle كما في المثال التالي: <div id="div">Button</div> <script> // يمكننا استعمال رايات تنسيقية خاصة مثل الراية “important” div.style.cssText=`color: red !important; background-color: yellow; width: 100px; text-align: center; `; alert(div.style.cssText); </script> See the Pen JS-p2-08-Styles and classes-ex4 by Hsoub (@Hsoub) on CodePen. غير أنه من النادر استعمال هذه الخاصية كونها تحذف الأنماط التنسيقية السابقة وتستبدلها بالقيم الجديدة، أي أنها قد تحذف أشياء مازلنا بحاجتها. فيما يمكن أن تُستخدم لتنسيق العناصر الجديدة، فلن يؤدي إسناد القيم بهذه الطريقة إلى أيّ عملية حذف (بما أن العناصر الجديدة لا تملك تنسيقات بعد). ويمكننا عمل ذلك أيضا باستعمال الدالّة div.setAttribute('style', 'color: red...')‎. الوحدات لا تنس إضافة الوحدات للقيم في شيفرة CSS، فلا يصِحُّ إسناد القيمة "10" للخاصية elem.style.top بل القيمة 10px هي الأصح، وإلا فلن يعمل السكربت بالشكل المطلوب. <body> <script> // لا يعمل document.body.style.margin = 20; alert(document.body.style.margin); // '' (سلسلة نصية فارغة، إهمال عملية الإسناد) // يعمل بعد إضافة الوحدة document.body.style.margin = '20px'; alert(document.body.style.margin); // 20px alert(document.body.style.marginTop); // 20px alert(document.body.style.marginLeft); // 20px </script> </body> See the Pen JS-p2-08-Styles and classes-ex5 by Hsoub (@Hsoub) on CodePen. لاحظ في السطرين الأخيرين أن المتصفح يفكّك الخاصية style.margin إلى خاصيتين وهما: style.marginLeft وstyle.marginTop. الأنماط المحسوبة باستعمال الدالة getComputedStyle يُعدّ التعديل على الأنماط عمليةً سهلةً ولكن كيف تُقرأ الأنماط؟ نريد مثلا معرفة مقاس، هوامش ولون عنصرٍ ما، كيف نتحصل عليها؟ تعمل الخاصية style على تعديل قيمة السمة style فقط دون الوصول إلى الأنماط الموصوفة في الأوراق التنسيقية المتتالية CSS. وبالتالي لا يمكننا قراءة أيّ قيمٍ من أصناف CSS باستعمال الخاصية elem.style. فعلى سبيل المثال لا يمكن أن تصل الخاصية style في هذا المثال إلى الهامش. <head> <style> body { color: red; margin: 5px } </style> </head> <body> The red text <script> alert(document.body.style.color); // فارغة alert(document.body.style.marginTop); // فارغة </script> </body> See the Pen JS-p2-08-Styles and classes-ex6 by Hsoub (@Hsoub) on CodePen. ماذا لو أردنا على سبيل المثال إضافة 20px للهامش؟ سيكون علينا أولا الوصول إلى القيمة الحالية له حتى يتسنى لنا تعديلها. وهنا لدينا طريقة أخرى للحصول على ذلك وتكون باستعمال الدالة getComputedStyle وبنيتها كالآتي: getComputedStyle(element, [pseudo]) حيث يمثّل العنصر element العنصر الذي سنحسب قيمه ويمثل pseudo العنصر الزائف، مثلا before::. إذا كانت قيمة pseudo عبارة عن سلسلة نصية فارغة أو غير موجودة أصلا فهذا يعني أننا نقصد العنصر نفسه. وتكون مخرجات الدالّة (output) عبارة عن كائن يحوي أنماط تنسيقية مثله مثل elem.style ولكن يأخذ في الحسبان هذه المرة كلّ الأصناف الموجودة في ملف CSS. وفيما يلي مثال على ذلك: <head> <style> body { color: red; margin: 5px } </style> </head> <body> <script> let computedStyle = getComputedStyle(document.body); // يمكننا الآن قراءة اللون والهامش alert( computedStyle.marginTop ); // 5px alert( computedStyle.color ); // rgb(255, 0, 0) </script> </body> See the Pen JS-p2-08-Styles and classes-ex7 by Hsoub (@Hsoub) on CodePen. ملاحظة: القيم المحسوبة والقيم النهائية (المُحدَّدة) هناك مفهومان في لغة CSS هما: قيمة تنسيقية محسوبة وهي القيمة المتحصّل عليها بعد تطبيق مجمل القواعد التنسيقية وقواعد الوراثة المُتضمَّنة في ملف CSS. قد تكون على شكل height:1em أو font-size:125% . قيمة تنسيقية نهائية (مُحدَّدة) وهي القيمة التي يقع عليها الاختيار في آخر المطاف وتُطبَّق على العنصر. القيمتان 1em و125% هما قيمتان نسبيتان. يأخذ المتصفح كافة القيم المحسوبة ويجعل كافة الوحدات مطلقة كما في المثال التالي: height:20px ،font-size:16px. ويمكن للقيم النهائية الخاصة بالخاصيات الهندسية أن تكون عشرية مثل: width:50.5px. لقد اُستحدثت الدالّة getComputedStyle أساسا للحصول على قيم محسوبة ولكن تبيّن فيما بعد أن القيم النهائية (المُحدَّدة) أحسن، فتغيرت المعايير، وأصبحت الدالّة getComputedStyle تُخرِج القيم النهائية للخاصية والتي تكون غالبا بالبكسل px بالنسبة للخاصيات الهندسية. ملاحظة: تتطلب الدالة getComputedStyle ذكر الاسم الكامل للخاصية علينا البحث على الدوام عن الخاصية التي نحتاج إليها بدقة مثل: padingLeft، أو marginTop أو borderTopWidth وإلا فلن نتمكن من ضمان صحة النتيجة المتحصّل عليها. فمع وُجود، على سبيل المثال، الخاصيتين padingLeft/padingTop، على ماذا سوف نحصل عند تنفيذ الدالّة getComputedStyle(elem, pading)‎؟ لن نحصل على شيئ؟ أو ربما سنحصل على قيمة "مستوحاة" من قيم معرّفة مسبقا للحاشية (pading)؟ في الحقيقة لا يوجد أيّ معايير تتحدث عن هذا الموضوع. وهناك بعض التناقضات الأخرى، حيث تُظهِر بعض المتصفحات (Chrome مثلا) في مثال الموالي القيمة 10px، ولا تُظهِرها متصفحات أخرى (كالمتصفح Firefox). مثال: <style> body { margin: 10px; } </style> <script> let style = getComputedStyle(document.body); alert(style.margin); // نحصل على سلسلة فارغة عند استعمال المتصفح Firefox </script> See the Pen JS-p2-08-Styles and classes-ex8 by Hsoub (@Hsoub) on CodePen. ملاحظة: الأنماط التي تُطبَّق على الروابط ‎:visited تكون مخفية يمكن تلوين الروابط التي سبق وأن زيرت باستخدام الصنف الزائف ‎:visited في ملف CSS. لكن الدالّة getComputedStyle لا يمكنها الوصول إلى هذا اللون، لأن ذلك يمكِّن أيّ صفحة كانت من إنشاء الرابط على الصفحة والإطلاع على الأنماط وبالتالي معرفة ما إذا كان المستخدم قد زار الرابط من قبل. لا يمكن للغة جافاسكربت الإطلاع على الأنماط المعرّفة باستخدام الصنف الزائف ‎:visited، كما تمنع لغة CSS تطبيق تنسيقاتِ تغيير الشكل والأبعاد (geometry-changing styles) ضمن الصنف الزائف ‎:visited وذلك لغلق الطريق أمام أيّ صفحةٍ مشبوهةٍ تسعى لمعرفة ما إذا زار المستخدم الرابط أم لا، وبالتالي التعدي على خصوصيته. الخلاصة هناك خاصيتان تُستخدمان للعمل على الأصناف وهما: className: وهي سلسلة نصية تُستخدم للعمل على كافة الأصناف دفعةً واحدةً. classList: هي عبارة عن كائن له دوالّه الخاصة (add/delete/toggle/contains) وتستخدم للعمل على الأصناف، كلُ على حدى. ولتغيير التنسيق لدينا: الخاصية style؛ وهي عبارة عن كائن تُشكَّل خواصه بجعل الحرف الأول من كل كلمة حرفا كبيرًا ما عدا الكلمة الأولى. تُعدّ قراءته والتعديل عليه تماما كالتعديل على خاصيات السمة style، كلٌ على حدى. وللاطلاع على كيفية إضافة الراية important وغيرها، يمكنك زيارة موقع MDN حيث تجد قائمة من الدوالّ التي تُستخدم لذلك. الخاصية style.cssText: هي الخاصية التي تقابِل السمة "style" في مجملها، أي السلسلة النصية التي تحمل كافة الأنماط التنسيقية دفعةً واحدةً. ولقراءة الأنماط التنسيقية النهائية (التي تأخذ في الحسبان كافة الأصناف بعد تطبيق تنسيقات CSS وحساب القيم النهائية) وُجدَت الدالة getComputedStyle(elem, [pseudo])‎ والتي تخرِج/تعيد كائنا يحمل التنسيقات وهو قابلٌ للقراءة فقط. تمرين إنشاء إشعار درجة الأهمية: 5 اكتب شيفرة الدالّة showNotification(options)‎ التي تُنشِئ إشعارًا كالآتي: بالمحتوى الذي يُمرَّر لها كوسيط، حيث يختفي الإشعار بعد ثانية ونصف من إظهاره. وتُوفّر الخيارات التالية: // أظهِر عنصرًا يحمل النص "Hello" بالقرب من الركن العلوي الأيمن للنافذة showNotification({ top: 10, // عشرة بكسل بدءًا من أعلى النافذة والذي فاصلته 0 right: 10, // عشرة بكسل بدءًا من الحافة اليمنى للنافذة والتي ترتيبتها 0 html: "Hello!", // شيفرة HTML الخاصة بالإشعار className: "welcome" // صنف إضافي للحاوية ‘div’ (اختياري) }); استعمل تنسيقات CSS لتحديد موضع إظهار العنصر حسب الإحداثيات المعطاة بافتراض أن الصفحة تحتوي مسبقا على الأنماط التنسيقية الضرورية. الحل يمكنك الإطلاع على الحل من هنا ترجمة -وبتصرف- للفصل Styles and classes من كتاب Browser: Document, Events, Interfaces
  8. يُعدّ التعديل على نموذج تمثيل المستند ككائن (DOM) مفتاح الحصول على صفحات "حيّة" (ديناميكية). سوف تتعرف فيما يلي على كيفية إنشاء عناصر جديدة على الصفحات في لمح البصر (بطريقة آنية دون الحاجة إلى إعادة تحميل الصفحات). مثال: إظهار رسالة فلنعرض ذلك على شكل مثالٍ تطبيقيٍ، حيث نُضيف رسالةً للصفحة تكون أكثر تنسيقًا من رسالة alert كالآتي: <style> .alert { padding: 15px; border: 1px solid #d6e9c6; border-radius: 4px; color: #3c763d; background-color: #dff0d8; } </style> <div class="alert"> <strong>Hi there!</strong> You've read an important message. </div> See the Pen JS-p2-07-Modifying the document-ex1 by Hsoub (@Hsoub) on CodePen. استعملنا في هذا المثال لغة HTML. فلنُنشئ نفس الحاوية div باستخدام جافاسكربت (على افتراض أن شيفرة HTML/CSS تتضمّن الأنماط التنسيقية). إنشاء عنصر ما توجد طريقتان لإنشاء عناصر (DOM): الدالّة (document.createElement(tag: تُستخدم لإنشاء عقدة عناصرية جديدة باستعمال الوسم الذي يُمرَّر لها كالآتي: let div = document.createElement('div'); الدالّة (document.createTextNode(text: تُستخدم لإنشاء عقدة نصية جديدة باستعمال النص الذي يُمرَّر لها كالآتي: let textNode = document.createTextNode('Here I am'); غالبا ما نحتاج إلى إنشاء عقدٍ عناصريةٍ، مثل الحاوية div، من أجل إظهار الرسالة. إنشاء الرسالة يتطلّب إنشاء حاوية الرسالة div ثلاث مراحل: // 1. إنشاء العنصر <div> let div = document.createElement('div'); // 2. إسناد القيمة "alert" لصنف الحاوية div.className = "alert"; // 3. وضع المحتوى داخل الحاوية div.innerHTML = "<strong>Hi there!</strong> You've read an important message."; صحيحٌ أنّنا أنشأنا العنصر ولكنه لحد الآن غير ظاهرٍ في الصفحة لأنّنا أنشأناه داخل المتغيّر div ولم نُضفه بعد للصفحة. دوالّ إضافة العناصر للصفحة من أجل إظهار الحاوية div، نحتاج إلى إضافتها في موضع ما من الصفحة، داخل العنصر <body> مثلا والمشار إليه بالكائن document.body. توجد دالّة خاصة تسمّى append تساعد على ذلك كما هو موضّح في المثال الآتي (انظر السطر الموسوم بـ (*)): <style> .alert { padding: 15px; border: 1px solid #d6e9c6; border-radius: 4px; color: #3c763d; background-color: #dff0d8; } </style> <script> let div = document.createElement('div'); div.className = "alert"; div.innerHTML = "<strong>Hi there!</strong> You've read an important message."; document.body.append(div);//(*) </script> See the Pen JS-p2-07-Modifying the document-ex2 by Hsoub (@Hsoub) on CodePen. نادينا، في هذا المثال، الدالّة append وطبقناها على الكائن document.body. كان يمكن أن نُطبقها على أيّ عنصرٍ كان من أجل إضافة عنصرٍ آخر له، حيث يمكننا على سبيل المثال إضافة عنصر ما للعنصر <div> باستعمال الدالّة div.append(anotherElement)‎. وهذه قائمة دوالّ غيرها تُستعمل لإضافة عنصرٍ ما لعنصرٍ آخرٍ في موضعٍ معيّنٍ من الصفحة: node.append(...nodes or strings)‎: إضافة عقد أو سلاسل نصية في نهاية العقدة node. node.prepend(...nodes or strings)‎: إضافة عقد أو سلاسل نصية في بداية العقدة node. node.before(...nodes or strings)‎: إضافة عقد أو سلاسل نصية قبل العقدة node. node.after(...nodes or strings)‎: إضافة عقد أو سلاسل نصية بعد العقدة node. node.replaceWith(...nodes or strings)‎: وضع عقد أو سلاسل نصية مكان العقدة node. وتكون وسائط هذه الدوالّ عبارة عن قائمة عشوائية من عقد DOM أو سلاسل نصية (التي تصبح آليا عقدًا نصية). دعنا نرى ذلك عبر المثال الحيّ التالي، حيث تُستخدم فيه هذه الدوالّ لإضافة عناصر li لقائمة ما ونصين، أحدهما قبلها والآخر بعدها. <ol id="ol"> <li>0</li> <li>1</li> <li>2</li> </ol> <script> ol.before('before'); // إضافة السلسلة "before" قبل العنصر <ol> ol.after('after'); // إضافة السلسلة "after" بعد العنصر <ol> let liFirst = document.createElement('li'); liFirst.innerHTML = 'prepend'; ol.prepend(liFirst); // إضافة العنصر liFirst في بداية القائمة <ol> let liLast = document.createElement('li'); liLast.innerHTML = 'append'; ol.append(liLast); // إضافة العنصر liLast في نهاية القائمة <ol> </script> See the Pen JS-p2-07-Modifying the document-ex3 by Hsoub (@Hsoub) on CodePen. وفيما يلي صورةٌ توضيحيةٌ لما تقوم به هذه الدوالّ: وبالتالي تصبح القائمة في شكلها النهائي كما يلي: before <ol id="ol"> <li>prepend</li> <li>0</li> <li>1</li> <li>2</li> <li>append</li> </ol> after وكما سبق أن ذكرنا، يُمكننا إضافة العديد من العقد والسلاسل النصية بمناداة الدالّة مرّةً واحدةً فقط. في المثال التالي، أُضيفت كلٌّ من السلسلة النصية والعنصر دفعةً واحدةً: <div id="div"></div> <script> div.before('<p>Hello</p>', document.createElement('hr')); </script> See the Pen JS-p2-07-Modifying the document-ex4 by Hsoub (@Hsoub) on CodePen. لاحظ هنا أن النص أُضيف بمثابة "نصٍ" وليس بمثابة شيفرة HTML، حيث تظهر التسلسلات التهريبية مثل < و>. وبالتالي تصبح شيفرة HTML النهائية كالآتي: <p>Hello</p> <hr> <div id="div"></div> وبعبارة أخرى، تُضاف السلاسل النصية بطريقة آمنة كأنها أُضيفت باستعمال الخاصية textContent. تُستخدم هذه الدوالّ إذا فقط لإضافة عقد DOM أو مقاطع نصية. ولكن ماذا لو أردنا إضافة سلسلةٍ نصيةٍ تحوي شيفرة HTML بما فيها الوسوم وغيرها، مثل ما هو معمول به مع الخاصية innerHTML؟ الدوالّ insertAdjacentHTML/Text/Element والجوابٍ على السؤال السابق، هو استخدام دالّةٍ أخرى متعدّدة الاستعمالات، إنها الدالّة elem.insertAdjacentHTML(where, html)‎. الوسيط الأوّل where للدالّة عبارة عن كلمة رمز (code word) تُحدِّد في أيّ موضع بالضبط من العنصر سيُضاف الوسيط الثاني، وتأخذ هذه الكلمة الرمزواحدةً من القيم التالية: "beforebegin": إضافة شيفرة HTML مباشرةً قبل العنصرelem. "afterbegin": إضافة شيفرة HTML في بداية العنصرelem. "beforeend": إضافة شيفرة HTML في نهاية العنصرelem. "afterend": إضافة شيفرة HTML مباشرةً بعد العنصرelem. ويكون الوسيط الثاني html للدالّة عبارة عن سلسلة نصية تُضاف على شكل شيفرة HTML. لاحظ المثال التالي: <div id="div"></div> <script> div.insertAdjacentHTML('beforebegin', '<p>Hello</p>'); div.insertAdjacentHTML('afterend', '<p>Bye</p>'); </script> See the Pen JS-p2-07-Modifying the document-ex5 by Hsoub (@Hsoub) on CodePen. الذي يولّد الشيفرة التالية: <p>Hello</p> <div id="div"></div> <p>Bye</p> كانت هذه هي الطريقة التي تُمكّننا من إضافة شيفرة HTML، أيًّا كانت، للصفحة. وفيما يلي صورة توضيحية لطرائق الإضافة في مختلف المواضع: ويمكننا بسهولة ملاحظة أوجه الشبه بين هذه الصورة والصورة التي قبلها، حيث أن مواضع الإضافة هي نفسها لكن هذه الدالّة تُضيف شيفرة HTML في حين تُضيف السابقة عقدًا أو سلاسل نصيةً. ولهذه الدالّة دالّتين "شقيقتين": elem.insertAdjacentText(where, text)‎: لها نفس البنية ولكن السلسلة النصية تُضاف هنا كنص وليس كشيفرة HTML. elem.insertAdjacentElement(where, elem)‎: لها أيضًا نفس البنية ولكنها تُستعمل لإضافة عنصرٍ ما. وُجدت هاتين الدالّتين فقط لضرورة توحيد البنية، ولكن تُستخدم فقط الدالّة elem.insertAdjacentHTML على أرض الواقع، طالما وُجدت الدوالّ append/prepend/before/after التي تُستخدم لإضافة كلٍّ من العقد والنصوص بالإضافة إلى أنها أقصر من حيث عدد الأحرف. وفيما يلي طريقةٌ بديلةٌ لإظهار رسالةٍ ما: <style> .alert { padding: 15px; border: 1px solid #d6e9c6; border-radius: 4px; color: #3c763d; background-color: #dff0d8; } </style> <script> document.body.insertAdjacentHTML("afterbegin", `<div class="alert"> <strong>Hi there!</strong> You've read an important message. </div>`); </script> See the Pen JS-p2-07-Modifying the document-ex6 by Hsoub (@Hsoub) on CodePen. إزالة/حذف عقدة ما اُستحدثت الدالّة node.remove()‎ لإزالة/حذف عقدةٍ ما. فلنجعل الرسالة التي أظهرناها في المثال السابق تختفي بعد ثانية واحدة من إظهارها: <style> .alert { padding: 15px; border: 1px solid #d6e9c6; border-radius: 4px; color: #3c763d; background-color: #dff0d8; } </style> <script> let div = document.createElement('div'); div.className = "alert"; div.innerHTML = "<strong>Hi there!</strong> You've read an important message."; document.body.append(div); setTimeout(() => div.remove(), 1000); </script> See the Pen JS-p2-07-Modifying the document-ex7 by Hsoub (@Hsoub) on CodePen. لاحظ هنا أنه لا داعيَ لتحويل عنصرٍ ما من الموضع الذي كان فيه إذا أردت وضعه في مكان آخر، هذا لأنّ كافة دوالّ الإضافة تُحوِّل آليا العنصر من مكانه السابق. فلنُحوّل من خلال المثال الآتي موضعي العنصرين التاليين: <div id="first">First</div> <div id="second">Second</div> <script> // لا داعيَ لمناداة الدالّة remove second.after(first); //تُضيف العنصر second بعد العنصر first </script> See the Pen JS-p2-07-Modifying the document-ex8 by Hsoub (@Hsoub) on CodePen. نسخ العُقد باستعمال الدالة cloneNode كيف يمكننا إضافة رسالة مطابقة للرسالة السابقة؟ نستطيع إنشاء دالّة ووضع الشيفرة بداخلها، ولكن هناك طريقة بديلة تنسخ العقدة div وتعدّل النص المتضمّن بداخلها (إن أردنا ذلك). وتسمح لنا هذه الطريقة باختصار الوقت والجهد سيما لو كانت شيفرة العنصر محلّ النسخ طويلةً بعض الشيء. تُنشِئ الدالّة elem.cloneNode(true)‎ عند مناداتها نسخةً كاملةً للعنصر، بما فيه من سمات وتوابع (العناصر الوليدة). أما إذا نادينا الدالّة elem.cloneNode(false)‎- مع تغيير الوسيط من true إلى false- تُنشِئ الدالّة العنصر وحده دون العناصر التابعة له (العناصر الوليدة). وهذا مثال عن نسخ الرسالة: <style> .alert { padding: 15px; border: 1px solid #d6e9c6; border-radius: 4px; color: #3c763d; background-color: #dff0d8; } </style> <div class="alert" id="div"> <strong>Hi there!</strong> You've read an important message. </div> <script> let div2 = div.cloneNode(true); // نسخ الرسالة div2.querySelector('strong').innerHTML = 'Bye there!'; // تغيير محتوى الرسالة المنسوخة div.after(div2); // إظهار النسخة بعد الحاوية الأولى </script> See the Pen JS-p2-07-Modifying the document-ex9 by Hsoub (@Hsoub) on CodePen. العقدة الخاصة DocumentFragment تُعدّ DocumentFragment عقدة DOM خاصة تُستعمل كمُغلِّف (wrapper) لتمرير قوائم تحوي عددًا من العقد. ويمكننا إضافة عقدٍ لها باستعمال الدالّة append، ولكن عند إضافتها هي (أي العقدة الخاصة DocumentFragment) في موضعٍ ما، يكون محتواها هو ما يُضاف في هذا الموضع بدلا عنها. تُنشِئ الدالّة getListContent في هذا المثال مقطعا يتضمّن عناصر <li> تُضاف لاحقا إلى العنصر <ul>: <ul id="ul"></ul> <script> function getListContent() { let fragment = new DocumentFragment(); for(let i=1; i<=3; i++) { let li = document.createElement('li'); li.append(i); fragment.append(li); } return fragment; } ul.append(getListContent()); // (*) </script> See the Pen JS-p2-07-Modifying the document-ex10 by Hsoub (@Hsoub) on CodePen. لاحظ هنا في السطر الأخير الموسوم بالرمز (*) أنّنا أضفنا العقدة الخاصة DocumentFragment باستعمال الدالّة append (التي تضيف عنصرًا في نهاية عنصرٍ آخر) لكنها تخلّلت الشيفرة فحصلنا على البنية التالية: <ul> <li>1</li> <li>2</li> <li>3</li> </ul> ونادرًا ما تُستخدم عقدة DOM الخاصية DocumentFragment، ذلك لأنه لا يوجد سببٌ يحملنا على إضافة عنصرٍ أو عناصرَ إلى عقدة من نوعٍ خاصٍ في حين يمكننا وضعه/وضعها في مصفوفة من العقد كالآتي: <ul id="ul"></ul> <script> function getListContent() { let result = []; for(let i=1; i<=3; i++) { let li = document.createElement('li'); li.append(i); result.push(li); } return result; } ul.append(...getListContent()); </script> See the Pen JS-p2-07-Modifying the document-ex11 by Hsoub (@Hsoub) on CodePen. أتينا على ذكر العقدة الخاصة DocumentFragment لأنها تندرج ضمن بعض المفاهيم، كالعنصر template (أي القالب) مثلا، التي سنتطرّق إليها لاحقا. دوال الإضافة والحذف على الطريقة القديمة ملاحظة: تساعد هذه المعلومة على فهم الشيفرات القديمة ولكنك لن تحتاج إليها أثناء التطوير باستعمال الطرائق العصرية. تَضمّ لغة جافاسكربت أيضًا دوالًا قديمةً تُطبّق على كائنات DOM والتي مازالت موجودة لضرورة التأريخ. تعود هذه الدوالّ إلى زمنٍ بعيدٍ جدًا ولا نحتاج لاستعمالها في الوقت الحالي طالما وَضعت لغة جافاسكربت بين أيدينا دوالًا أكثر مرونة مثل الدوالّ: append prepend before after remove replaceWith السبب الوحيد الذي جعلنا نذكر لك هنا هذه الدوالّ هو أنك يمكن أن تصادفها في الكثير من السكربتات القديمة. الدالة appendChild‎ تضيف الدالّة parentElem.appendChild(node)‎ العقدة node كآخر عنصرٍ وليدٍ للعنصر parentElem. سنضيف في المثال التالي عنصرًا جديدًا <li> في نهاية العنصر الوالد <ol>: <ol id="list"> <li>0</li> <li>1</li> <li>2</li> </ol> <script> let newLi = document.createElement('li'); newLi.innerHTML = 'Hello, world!'; list.appendChild(newLi); </script> See the Pen JS-p2-07-Modifying the document-ex12 by Hsoub (@Hsoub) on CodePen. الدالة insertBefore وتضيف الدالّة parentElem.insertBefore(node, nextSibling)‎ العقدة node قبل العقدة nextSibling ضمن العقدة parentElem. تُضيف الشيفرة التالية عنصرًا جديدًا للقائمة <ol> مباشرة قبل العنصر<li> الثاني في القائمة: <ol id="list"> <li>0</li> <li>1</li> <li>2</li> </ol> <script> let newLi = document.createElement('li'); newLi.innerHTML = 'Hello, world!'; list.insertBefore(newLi, list.children[1]); </script> See the Pen JS-p2-07-Modifying the document-ex13 by Hsoub (@Hsoub) on CodePen. يمكننا إضافة العقدة newLi في المرتبة الأولى كالآتي: list.insertBefore(newLi, list.firstChild); الدالة replaceWith تَستبدلُ هذه الدالّة العقدة node بالعقدة oldChild التي تُعدّ وليدة العقدة parentElem. الدالة removeChild تحذِف هذه الدالّة العقدة node من العقدة parentElem (حيث نفترض هنا أن العقدة node وليدة العقدة parentElem). يُحذَف العنصر الأول <li> من القائمة <ol> في المثال الموالي: <ol id="list"> <li>0</li> <li>1</li> <li>2</li> </ol> <script> let li = list.firstElementChild; list.removeChild(li); </script> See the Pen JS-p2-07-Modifying the document-ex14 by Hsoub (@Hsoub) on CodePen. تكون مخرجات (output) هذه الدوالّ عبارة عن عُقد (العقد التي حُذفت/التي أُضيفت)، حيث تُوّلِد الدالّة parentElement.appendChild(node)‎ عقدةً ولكن غالبًا ما لا تُستعمل القيمة التي توّلدت من مناداة هذه الدالّة، نحتاج فقط إلى تطبيق الدالّة/تنفيذها. نبذة عن الدالة "document.write" هناك أيضا الدالّة document.write، وهي دالّة قديمة جدًا تُستخدم لإضافة جزءٍ ما لصفحة الويب، وبنيتها كالآتي: <p>Somewhere in the page...</p> <script> document.write('<b>Hello from JS</b>'); </script> <p>The end</p> See the Pen JS-p2-07-Modifying the document-ex15 by Hsoub (@Hsoub) on CodePen. تؤدي مناداة الدالّة document.write(html)‎ إلى كتابة الشيفرة html في الصفحة 'آنذاك وفي هذا الموضع بالضبط'. وبذلك تُولَّد السلسلة النصية html بطريقة ديناميكية (آنية) مرنة. يمكن إذًا استعمال لغة جافاسكربت لإنشاء وكتابة صفحة ويب كاملة. اِستُحدثت هذه الدالّة في وقت لم تكن فيه كائنات DOM موجودة ولم تكن هناك معايير أيضًا….إلخ، أي منذ زمنٍ بعيدٍ جدًا، ومازالت 'حيّة' ما دام البعض يستعملها لحد الآن في سكربتاته، ولكن نادرًا ما نصادفها في السكربتات العصرية الدارجة حاليا، نظرًا لمحدوديتها في الجوانب التالية: تُنفَّذ مناداة الدالّة document.write فقط أثناء تحميل الصفحة وفي حال نُوديَت بعد ذلك، يُحذف محتوى الصفحة كما في المثال التالي: <p>After one second the contents of this page will be replaced...</p> <script> // مناداة الدالّة document.write بعد مرور ثانية واحدة // أي بعد تحميل الصفحة، فتَحذِف هذه الدالّة المحتوى الأصلي للصفحة setTimeout(() => document.write('<b>...By this.</b>'), 1000); </script> See the Pen JS-p2-07-Modifying the document-ex16 by Hsoub (@Hsoub) on CodePen. لا ينبغي إذًا استعمال هذه الدالّة بعد تحميل الصفحة، على عكس غيرها من دوالّ DOM التي تحدثنا عنها سابقا. تحدثنا عن الجانب السلبي لهذه الدالّة، لكنها تملك جانبا إيجابيًا أيضًا؛ فمن وجهة نظر تقنية، وفي حال ما نوديت الدالّة document.write أثناء تحليل/قراءة المتصفّح لشيفرة HTML تدريجيا، وكتبت الدالّة بعض الأسطر، يَعتبر المتصفّح أن هذه الأسطر كانت موجودةً منذ البداية في شيفرة HTML، يُسرِّع ذلك كثيرًا من عملية القراءة حيث لا تشمل هذه العملية أيّ تعديلات على DOM، فتكتب الدالّة مباشرةً على الصفحة قبل أن يُنشَأ نموذج تمثيل المستند ككائن. فإذا أردنا مثلا إضافة سلسلةٍ كبيرةٍ تحوي شيفرة HTML بطريقة ديناميكية في مرحلة تحميل الصفحة، وكان عامل السرعة يصنع الفارق بالنسبة لنا، يمكن أن تكون هذه الطريقة فعالة جدًا. ولكننا نادرًا ما نصادف هذه السيناريوهات على أرض الواقع. بل غالبا ما نصادف هذه الدالّة في بعض السكربتات القديمة (سكربتات كُتبت في وقت لم تكن تُستعمل هذه الدالّة لأغراضٍ كهذه). الخلاصة الدوال التي تُستخدم في إنشاء عقدٍ جديدةٍ هي: document.createElement(tag)‎: تُنشِئ عنصرًا بناءً على الوسم tag. document.createTextNode(value)‎: تُنشِئ عقدةً نصيةً (نادرًا ما تُستخدم). elem.cloneNode(deep)‎: تنسخ العنصر، مع كل توابعه (أي العناصر الوليدة) إذا كانت قيمة المعامل deep تُساوي true. الدوالّ التي تستخدم للإضافة والحذف هي: node.append(...nodes or strings)‎: تُضيف عقدة أو سلسلة نصية في نهاية العقدة node. -node.prepend(...nodes or strings)‎: تُضيف عقدة أو سلسلة نصية في بداية العقدة node. -node.before(...nodes or strings)‎: تُضيف عقدة أو سلسلة نصية مباشرة قبل العقدة node. -node.after(...nodes or strings)‎: تُضيف عقدة أو سلسلة نصية مباشرة بعد العقدة node. -node.replaceWith(...nodes or strings)‎: تستبدل بالعقدة node ما مُرِّر إليها من عقدة/عقد أو محتوى نصي. -node.remove()‎: تحذف العقدة node. وهي تُضيف السلاسل النصية كنص وليس كشيفرة HTML. الدوالّ القديمة هي: rent.appendChild(node)‎ parent.insertBefore(node, nextSibling)‎ parent.removeChild(node)‎ parent.replaceChild(newElem, node)‎ وتكون مُخرجات (output) هذه الدوالّ عبارة عن عُقد. تضيف الدالّة elem.insertAdjacentHTML(where, html)‎ شيفرة HTML، التي تُمرّر لها عبر الوسيط html، في المواضع التالية بناءً على قيمة الوسيط where: "beforebegin": تُضاف الشيفرة html مباشرة قبل العنصر elem. "afterbegin": تُضاف الشيفرة html في بداية العنصر elem. "beforeend": تُضاف الشيفرة html في نهاية العنصر elem. "afterend": تُضاف الشيفرة html مباشرة بعد العنصر elem. هناك أيضًا دوالٌّ مماثلةٌ لها وهي elem.insertAdjacentText وelem.insertAdjacentElement ولكن نادرًا ما تٌستخدم. إذا أردت إضافة شيفرة HTML إلى الصفحة قبل نهاية عملية تحميلها اِستعمل الدالّة document.write(html)‎. تؤدي مناداة هذه الدالّة بعد انتهاء عملية تحميل الصفحة إلى مسح محتوى الصفحة، ونُصادفها خاصة في السكربتات القديمة. تمارين createTextNode/innerHTML/textContent درجة الأهمية: 5 لدينا عنصر DOM فارغ يسمّى elem وسلسلة نصية text. حدِّد، من بين التعليمات الثلاث التالية، تلك التي لها نفس الوظيفة. elem.append(document.createTextNode(text))‎ elem.innerHTML = text elem.textContent = text الحل التعليمتان 1 و3 لهما نفس الوظيفة، حيث تضيف كلتا التعليمتين السلسلة النصية text للعنصر elem كنص وليس كشيفرة HTML. وفيما يلي مثال على ذلك: <div id="elem1"></div> <div id="elem2"></div> <div id="elem3"></div> <script> let text = '<b>text</b>'; elem1.append(document.createTextNode(text)); elem2.innerHTML = text; elem3.textContent = text; </script> مسح عنصر ما درجة الأهمية: 5 أنشئ الدالّة clear(elem)‎ التي تحذف محتوى العنصر elem بالكامل. <ol id="elem"> <li>Hello</li> <li>World</li> </ol> <script> function clear(elem) { /*اكتب الشيفرة هنا */ } clear(elem); // clears the list </script> الحل فلنبدأ بالحل الذي لا ينبغي أن تقترحه، لأنه ببساطة حلٌ خاطئ. function clear(elem) { for (let i=0; i < elem.childNodes.length; i++) { elem.childNodes[i].remove(); } } طريقة الحل هذه لا تنفع، لأن مناداة الدالّة remove()‎ تؤدي إلى تغيير مواضع العُقد ضِمن المجموعة elem.childNodes، فبعد حذف العقدة الأولى مثلا (التي كانت في الموضع 0 من المجموعة) تُسحب المجموعة كاملةً للخلف لتصبح العقدة ذات الموضع 1 مكان العقدة ذات الموضع 0، لكن المؤشر i في الدورة الثانية من الحلقة يأخذ القيمة 1 وبالتالي تُحذف العقدة ذات الموضع 1 وتبقى العقدة ذات الموضع 0 موجودة، وهكذا دواليك. لن تُحذف إذا بعض العناصر من المجموعة. ويحدث نفس الشيء عند استعمال الحلقة for...of. الحل الصحيح يكون مثلا كالآتي: function clear(elem) { while (elem.firstChild) { elem.firstChild.remove(); } } أو بطريقة أبسط: function clear(elem) { elem.innerHTML = ''; } لماذا لا تختفي السلسلة 'aaa'؟ درجة الأهمية: 1 عند مناداة الدالّة table.remove()‎، في المثال التالي، يُحذف الجدول من الصفحة ولكن السلسلة 'aaa' تبقى ظاهرة. فلماذا يحدث ذلك؟ <table id="table"> aaa <tr> <td>Test</td> </tr> </table> <script> alert(table); // الجدول موجود table.remove(); // لماذا مازالت السلسلة 'aaa' ؟ ظاهرة </script> الحل يعود السبب في حدوث ذلك إلى خطأ في شيفرة HTML لهذا التمرين. اِعلم أن من مهام المتصفّح تصحيح الأخطاء آليًا. وبما أن المواصفة التقنية تنصّ على أن الجداول لا تضمّ أيّ سلاسل نصية، بل فقط الوسوم الخاصة بالجداول، يُضيف المتصفح السلسلة "aaa" قبل الجدول table مباشرة. وعندما يُحذف الجدول، تبقى السلسلة "aaa" ظاهرة. للإجابة على سؤال هذا التمرين، يمكنك الإستعانة بعرض نموذج تمثيل المستند ككائن DOM باستعمال المتصفح، وستظهر لك السلسلة "aaa" قبل الجدول table. تُحدِّد معايير HTML، وبالتفصيل، كيف يُعالِج المتصفح الشيفرة التي تحتوي على أخطاء، ويُعدّ تعامل المتصفح بهذه الطريقة مع الخطأ المتضمَّن في شيفرة HTML لهذا التمرين صحيحا. إنشاء قائمة درجة الأهمية: 4 اكتب شيفرة تُنشِئ واجهةً تسمح للمستخدم بإدخال محتوى ما على مراحل وتجمع هذا المحتوى في شكل قائمة. ولكل عنصر من القائمة عليك: مطالبة المستخدِم بإدخال المحتوى باستعمال الدالّة prompt. إنشاء العنصر<li> بناءً على المحتوى الذي أدخله المستخدِم. مواصلة العملية إلى غاية فراغ المستخدِم من إدخال المعطيات (بالضغط على المفتاح esc على لوحة المفاتيح، أو الضغط على زر الالغاء الظاهر على النافذة). يجب أن تُنشَئ العناصر ديناميكيًا وإذا أدخل المستخدِم وسوم HTML، تُعالَج على أنها نصوص، لا شيفرة HTML. الحل ملاحظة: لاحظ استعمال الخاصية textContent لإسناد محتوى للعنصر <li>. يمكنك الإطلاع على الحل من هنا إنشاء شجرة اعتمادًا على كائن درجة الأهمية: 5 اكتب الدالّة createTree()‎ التي تُنشئ قائمةً متفرعةً من العناصر ul/li بناءً على كائن متفرع: let data = { "Fish": { "trout": {}, "salmon": {} }, "Tree": { "Huge": { "sequoia": {}, "oak": {} }, "Flowering": { "apple tree": {}, "magnolia": {} } } }; وتكون بنيتها كالآتي: let container = document.getElementById('container'); createTree(container, data); // تُنشِئ الشجرة بداخل الحاوية ‘container’ على أن تكون النتيجة المتحصل عليها كالآتي: اِختر واحدةً من بين الطريقتين التاليتين لحل التمرين: أنشئ الشجرة في شكل شيفرة HTML ثم أسندها للخاصية Container.innerHTML أو أنشئ عُقد الشجرة وأضفها باستعمال دوالّ DOM. ويُستحسن أن تَحُل التمرين بالطريقين. ملاحظة: لا ينبغي أن تَضمّ الشجرة في أوراقها عناصر إضافية فارغة <ul></ul>. الحل ملاحظة: أسهل طريقة لتصفح الكائن (المرور بعناصره الواحد تلوى الآخر) تكون باستعمال التعاود (recursion). حل التمرين باستعمال الخاصية innerHTML حل التمرين باستعمال دوالّ DOM إظهار العناصر الوليدة لشجرة ما درجة الأهمية: 5 لدينا هذه الشجرة وهي مبنية على شكل عناصر ul/li متفرعة. اكتب شيفرة تُضيف من خلالها لكل عنصر <li> عدد عناصره الوليدة مع إهمال الأوراق (العُقد التي ليس لها عناصر وليدة)، والنتيجة تكون على الشكل التالي: الحل لإضافة نصٍ لكلّ عنصر من العناصر <li> يمكن التعديل على العُقدة النصية data. يمكنك الإطلاع على الحل من هنا إنشاء يومية الأهمية:4 اكتب الدالّة createCalendar(elem, year, month)‎ التي تُنشئ يومية الشهر month من السنة year، وتسندها للعنصر elem. تكون اليومية على شكل جدول تُمثِّل فيه العناصر <tr> الأسابيع والعناصر <td> الأيام، وتكون خانات السطر الأول عبارة عن عناصر <th> تحوي أسماء أيام الأسبوع مُرتّبةً من الإثنين إلى الأحد. فعلى سبيل المثال، تؤدي مناداة الدالّة createCalendar(cal, 2012, 9)‎ إلى إنشاء اليومية التالية وإسنادها للعنصر cal. ملاحظة: يكفي في هذه المرحلة إظهار اليومية، لا نحتاج أن تكون تفاعلية. يمكنك الإطلاع على شيفرة التمرين من هنا الحل نُنشئ الجدول على شكل سلسلة نصية "<table>....</table>" ثم نسندها إلى الخاصية innerHTML، باتباع خطوات الخوارزمية التالية: إنشاء خانات السطر الأول من الجدول (table header) باستعمال عناصر<th> تحوي أسماء أيام الأسبوع. إنشاء الكائنdate باستعمال الدالّة البانية d=newDate(year, month-1)‎ والذي يُمثِّل أول يوم في الشهر month (مع الأخذ في الحسبان أن ترتيب الأشهر في جافاسكربت يبدأ من 0 وليس من 1). ملء الخانات التي تسبق خانة اليوم الأول من الشهر (أي اليوم d.getDay()‎)، والتي تكون فارغة في اليومية، بإضافة عناصر <td></td> فارغة للجدول. الانتقال تصاعديا عبر الأيام باستعمال الدالّة d.setDate(d.getDate()+1)‎ وإضافة الخانات <td> لليومية مادامت قيمة d.getMonth()‎ تشير إلى الشهر نفسه (لم نصل بعد للشهر الموالي). وإذا كان اليوم يوم أحدٍ نضيف سطرًا جديدًا <tr></tr>. إذا وصلنا إلى نهاية الشهر، ومازالت في السطر خانات فارغة، نملؤها بعناصر <td></td> فارغة لنجعل الجدول مستطيلا. يمكنك الإطلاع على الحل من هنا إنشاء ساعة ملونة باستعمال الدالّة setInterval الأهمية: 4 أنشئ ساعة ملونة تشبه الساعة في الشكل التالي: استعمل HTML/CSS للتنسيق، وجافاسكربت فقط لتحديث القيم الزمنية للعناصر. الحل فلنكتب أولا شيفرة التنسيق باستعمال HTML/CSS. نضع كلَّ مكون زمني في حاوية خاصة به (الساعات في حاوية، الدقائق في حاوية أخرى والثواني أيضا في حاوية) ونلونها باستعمال CSS. <div id="clock"> <span class="hour">hh</span>:<span class="min">mm</span>:<span class="sec">ss</span> </div> تحدِّث الدالّة update()‎ الوقت، وتنادي الدالّة setInterval الدالّة update()‎ كل ثانية لتحيين الوقت. function update() { let clock = document.getElementById('clock'); let date = new Date(); // (*) let hours = date.getHours(); if (hours < 10) hours = '0' + hours; clock.children[0].innerHTML = hours; let minutes = date.getMinutes(); if (minutes < 10) minutes = '0' + minutes; clock.children[1].innerHTML = minutes; let seconds = date.getSeconds(); if (seconds < 10) seconds = '0' + seconds; clock.children[2].innerHTML = seconds; } نتحقّق في السطر الموسوم بالرمز (*) من الوقت آنيا لأن القيم التي تَنتُج من مناداة الدالّة setInterval غير موثوقة، حيث يمكن أن يكون فيها بعض التأخير. فيما يلي الدوالّ التي تُستعمل لإدارة الساعة: let timerId; function clockStart() { // run the clock timerId = setInterval(update, 1000); update(); // (*) } function clockStop() { clearInterval(timerId); timerId = null; } لاحظ أن الدالّة update()‎، بالإضافة لكونها مُبرمَجةً لتُنفَّذ دوريًا كلّ ثانية داخل الدالّة clockStart()‎، فهي تُنفَّذُ في السطر الموسوم بالرمز (*) في الحين ولا تنتظر حتى تنادي الدالّة setInterval الدالّة update()‎، وذلك لتفادي بقاء الساعة فارغةً طول مدّة تنفيذ الدالّةsetInterval (أي مدّة ثانية واحدة) عند مناداتها أول مرة. يمكنك الإطلاع على الحل من هنا إضافة شيفرة HTML إلى قائمة _الأهمية: 5 _ اكتب الشيفرة التي تُضيف المقطع <li>2</li><li>3</li> بين العنصرين <li> فيما يلي: <ul id="ul"> <li id="one">1</li> <li id="two">4</li> </ul> الحل تُعدّ الدالّة insertAdjacentHTML أحسن اختيار حين يتعلق الأمر بإضافة شيفرة HTML في موضع ما من الصفحة. one.insertAdjacentHTML('afterend', '<li>2</li><li>3</li>'); فرز جدول ما الأهمية: 5 لدينا الجدول التالي (قد يحوي عددًا أكبر من السطور): <table> <thead> <tr> <th>Name</th><th>Surname</th><th>Age</th> </tr> </thead> <tbody> <tr> <td>John</td><td>Smith</td><td>10</td> </tr> <tr> <td>Pete</td><td>Brown</td><td>15</td> </tr> <tr> <td>Ann</td><td>Lee</td><td>5</td> </tr> <tr> <td>...</td><td>...</td><td>...</td> </tr> </tbody> </table> اكتب الشيفرة التي تعيدُ ترتيبه ترتيبًا أبجديًا حسب محتوى العمود name. الحل الحل عبارة عن بضعة سطور فقط، ولكننا نحتاج إلى شرح الحيل التي اُستعمِلت فيه. ويكون باتباع خطوات الخوارزمية التالية: استخراج سطور الجدول من العنصر tbody. ترتيبها حسب محتوى العمود الأول name ترتيبًا أبجديًا. إضافة العُقد حسب الترتيب الجديد باستعمال append(sortedRows)‎. لستَ مضطرًا لحذف العناصر<tr>، عليك فقط إعادة إضافتها حيث تنتقل آليًا من مواضعها الأصلية إلى المواضع الجديدة بعد ترتيبها. ملاحظة: العنصر tbody من الجدول مذكورٌ صراحةً في شيفرة HTML ولكن حتى وإن لم يُذكر صراحةً، فهو دائمًا موجود في بنية DOM. يمكنك الإطلاع على الحل من هنا ترجمة -وبتصرف- للفصل Modifying the document من كتاب Browser: Document, Events, Interfaces
  9. عندما يُحمِّل المتصفّح الصّفحة، فهو يقرأ/يحلّل شيفرة HTML ويُنشئ من خلالها كائنات DOM. وحين يتعلّق الأمر بالعُقد العناصرية (element nodes)، فإن غالبية سمات HTML المعيارية تصبح خاصيات للكائنات DOM. فعلى سبيل المثال، إذا كانت السمة كالتالي: <"body id="page>، يكتسب كائن DOM الخاصية "body.id="page. لكن عملية الربط بين السمات والخاصيات لا تتم تقابليا (كلّ سمة لا يقابلها بالضرورة خاصية). فيما يلي ستتعرف على كيفية التمييز بين الْمفهومَيْن، لتتعلم طريقة استخدامهما حين يتطابقان وحين يختلفان. خاصيات DOM سبق وأن تعرفت على خاصيات نموذج تمثيل المستند ككائن (أي خاصيات DOM) المبنيّة مسبقًا. هناك العديد منها ولكن ذلك لا يمنع المبرمج من إضافة/إنشاء خاصيات أخرى إذا لم تكن الخاصيات المبنيّة مسبقًا كافية لتلبية حاجاته. تُعدّ عُقد DOM كائنات جافاسكربت عادية يمكنك التعديل عليها. فلننشئ على سبيل المثال خاصيّة جديدة للكائن document.body: document.body.myData = { name: 'Caesar', title: 'Imperator' }; alert(document.body.myData.title); // Imperator كما يمكن إضافة دالّة أيضًا: document.body.sayTagName = function() { alert(this.tagName); }; document.body.sayTagName(); // BODY (قيمة this في الدالة هي document.body) See the Pen JS-p2-06-Attributes and properties-ex2 by Hsoub (@Hsoub) on CodePen. يمكن أيضًا تعديل النماذج المبنيّة مسبقًا مثل Element.prototype وإضافة دوالّ جديدة تُطبّق على كلّ العناصر. Element.prototype.sayHi = function() { alert(`Hello, I'm ${this.tagName}`); }; document.documentElement.sayHi(); // Hello, I'm HTML document.body.sayHi(); // Hello, I'm BODY See the Pen JS-p2-06-Attributes and properties-ex3 by Hsoub (@Hsoub) on CodePen. تسلُك خاصيات ودوالّ DOM إذا سلوكًا مماثلًا لسلوك كائنات جافاسكربت العاديّة ويمكنها استقبال أيّ قيمةٍ كانت. فهي تخضع لشرط التمييز بين الحروف الكبيرة والحروف الصغيرة (فنكتب مثلا elem.nodeType، ولا نكتب elem.NodeType). سمات HTML يمكن أن تملك وسوم HTML سمات، ويتعرف المتصفّح على السمات المعيارية عندما يقرأ الوسوم ويحلّلها لإنشاء خاصيات لكائنات DOM المقابلة لها. وبذلك، عندما يكون للعنصر السمة id أوأيّ سمة معيارية أخرى، تُنشَأُ الخاصية الموافقة لها. ولكن ذلك لن يحدث إذا كانت الخاصية غير معيارية. انظر المثال التالي: <body id="test" something="non-standard"> <script> alert(document.body.id); // test // السمة غير المعيارية لا تولّد خاصية مقابلة لها alert(document.body.something); // undefined </script> </body> See the Pen JS-p2-06-Attributes and properties-ex4 by Hsoub (@Hsoub) on CodePen. لاحظ أن سمةً ما قد تكون معياريةً لعناصر دون غيرها وغير مُعرّفةٍ إذا تعلّق الأمر بعناصر أخرى. فمثلًا تُعدّ السمة "type" سمةً معياريةً للعنصر <input>‏ (HTMLInputElement) وغير معيارية للعنصر <body>‏ (HTMLBodyElement). وستجد وصفًا للسمات المعيارية في مواصفات صنف كل عنصر. والمثال التالي يوضّح ذلك: <body id="body" type="..."> <input id="input" type="text"> <script> alert(input.type); // text alert(body.type); // undefined لم تٌنشأ خاصية DOM لأنها غير معيارية // </script> </body> See the Pen JS-p2-06-Attributes and properties-ex5 by Hsoub (@Hsoub) on CodePen. وبالتالي إذا كانت السمة غير معيارية، لا تٌنشأ خاصية DOM الموافقة لها. فهل من طريقة تسمح بالوصول إلى هذه السمات؟ بالتأكيد، يمكن الوصول إلى جميع السمات باستعمال الدوالّ التالية: (elem.hasAttribute(name: للتحقّق من وجود السمة. (elem.getAttribute(name: للحصول على قيمة السمة. (elem.setAttribute(name, value: إسناد القيمة value للسمة name. (elem.removeAttribute(name: حذف السمة. وتُستخدم هذه الدوالّ مع السمات غير المعيارية كما هي مكتوبة بالضبط في شيفرة HTML. يمكن أيضًا قراءة كافة السمات باستعمال elem.attributes؛ وهو مجموعة من الكائنات التي تنتمي إلى صنف Attr المبني مسبقًا، تملك خاصيتي الاسم name والقيمة value. فيما يلي عرض لكيفية قراءة سمة غير معيارية: <body something="non-standard"> <script> alert(document.body.getAttribute('something')); // non-standard </script> </body> See the Pen JS-p2-06-Attributes and properties-ex6 by Hsoub (@Hsoub) on CodePen. تملك سمات HTML الميزات التالية: لا تخضع لشرط التمييز بين الحروف الصغيرة والحروف الكبيرة (id هو نفسه ID). تكون قيم هذه السمات دائما من نوع سلاسل نصّية (أي من النوع string). فيما يلي عرضٌ موسّعٌ يوضّح طريقة التعامل مع السمات: <body> <div id="elem" about="Elephant"></div> <script> alert( elem.getAttribute('About') ); // (1) 'Elephant', جلب elem.setAttribute('Test', 123); // (2), ضبط alert( elem.outerHTML ); // (3), التحقق من وجود السمة في شيفرة HTML (نعم) for (let attr of elem.attributes) { // (4) عرض القائمة كاملة alert( `${attr.name} = ${attr.value}` ); } </script> </body> See the Pen JS-p2-06-Attributes and properties-ex7 by Hsoub (@Hsoub) on CodePen. لاحظ ما يلي: (1).('getAttribute('About: الحرف الأول من كلمة About هو حرف كبير مع أنه صغير في شيفرة HTML. هذا لا يُسبّب أيّ مشكلةٍ طالما أن أسماء السمات لا تخضع لشرط التمييز بين الحروف الكبيرة والحروف الصغيرة. (2). يمكن إسناد قيمة من أيّ نوعٍ كان للسمة، ولكن هذه القيمة تصبح سلسلةً نصّيةً. قيمة السمة Test اذًا هنا هي "123". (3). تظهر كافة السمات بما فيها تلك التي أضفناها (أي غير المعيارية مثل تلك التي كانت موجودة في الشيفرة كالسمة 'About'، وتلك التي أضفناها كالسمة 'Test') في outerHTML. (4). تُعدّ مجموعة السمات attributes قابلة للتمرير ضمن حلقة تكرار، وتضمّ كافة سمات العنصر، المعيارية منها وغير المعيارية، في شكل كائنات تملك خاصيتي الاسم name والقيمة value. المزامنة (synchronization) بين الخاصية والسمة حين تتغيّر سمة معيارية ما، يجري تحديث الخاصية الموافقة لها آليًا والعكس كذلك صحيح (ما عدا في بعض الحالات الاستثنائية). عُدّلت في المثال الموالي السمة "id"، فلاحظ أن ذلك أدّى الى تعديل الخاصية أيضًا. وحدث الشّيء نفسه في المثال العكسي الذي جاء بعده، حيث عُدّلت الخاصية وهو ما أدّى إلى تعديل السمة آليًا. <input> <script> let input = document.querySelector('input'); // سمة => خاصّية input.setAttribute('id', 'id'); alert(input.id); // id (تحديث) // خاصية => سمة input.id = 'newId'; alert(input.getAttribute('id')); // newId (تحديث) </script> See the Pen JS-p2-06-Attributes and properties-ex8 by Hsoub (@Hsoub) on CodePen. غير أن هناك بعض الاستثناءات، فعلى سبيل المثال، لا يمكن مزامنة input.value سوى في اتجاه واحد، من السمة إلى الخاصية، وليس العكس. <input> <script> let input = document.querySelector('input'); // سمة ⇐ خاصية input.setAttribute('value', 'text'); alert(input.value); // text //خاصية ⇍ سمة input.value = 'newValue'; alert(input.getAttribute('value')); // text (لم يتم التحديث) </script> See the Pen JS-p2-06-Attributes and properties-ex9 by Hsoub (@Hsoub) on CodePen. في المثال أعلاه: يُؤدّي تعديل قيمة السمة value إلى تعديل الخاصية آليًا. ولكن التعديل على الخاصية مباشرةً لا يُؤدّي إلى أيّ أثر على السمة. هذه "الميزة" يمكن أن تكون فعلا مفيدة، حيث يمكن للمستخدم أن يغيّر القيمة value، وبعدها، إذا أردت استعادة القيمة الأولية من HTML، تجدها في السمة. خاصيات DOM لها أنواع لا تكون خاصيات DOM دائمًا عبارة عن سلاسل نصّية (أي من النوع "string")، فمثلا خاصية input.checked (في مربعات الاختيار (checkbox)) تكون عبارة عن قيمة بوليانية/منطقية. <input id="input" type="checkbox" checked> checkbox <script> alert(input.getAttribute('checked')); // قيمة السمة من نوع سلسلة نصّية فارغة alert(input.checked); // قيمة الخاصية هي “true” </script> See the Pen JS-p2-06-Attributes and properties-ex10 by Hsoub (@Hsoub) on CodePen. وهناك أمثلة أخرى؛ تكون السمة style عبارة عن سلسلة نصّية ولكن الخاصية style هي كائن. <div id="div" style="color:red;font-size:120%">Hello</div> <script> // سلسلة نصّية alert(div.getAttribute('style')); // color:red;font-size:120% // كائن alert(div.style); // [object CSSStyleDeclaration] alert(div.style.color); // red </script> See the Pen JS-p2-06-Attributes and properties-ex11 by Hsoub (@Hsoub) on CodePen. غير أن غالبية الخاصيات تكون من نوع سلاسل نصّية. وفي حالات نادرة جدًا، حتى وإن كانت خاصية DOM من نوع سلسلة نصّية، يمكن أن تختلف عن السمة. فمثلا، خاصية href في DOM تكون دائما عبارة عن عنوان URL مطلق كامل، حتى وإن تضمّنت السمة عنوان URL نسبي أو إشارةً إلى أحد العناصر في الصفحة عبر الصياغة hash#. وفيما يلي مثالٌ على ذلك. <a id="a" href="#hello">link</a> <script> // attribute alert(a.getAttribute('href')); // #hello // property alert(a.href ); // ‫ URL كامل على شكل http://site.com/page#hello </script> See the Pen JS-p2-06-Attributes and properties-ex12 by Hsoub (@Hsoub) on CodePen. إذا ما أردت الوصول إلى قيمة href، أو أيّ سمةٍ غيرها، كما هي مدّونة بالضبط في شيفرة HTML، استعمل الخاصية getAttribute. السمات غير المعيارية والخاصية dataset نستخدم الكثير من السمات المعيارية عند الكتابة بلغة جافاسكربت ولكن ماذا عن السمات غير المعيارية وهل هي مفيدة؟ فلنرى أولا إن كانت مفيدة أم لا وما هو دورها. تُستخدم أحيانا السمات غير المعيارية في تمرير البيانات من شيفرة HTML إلى جافاسكربت أو لـ : "وسم" عناصر HTML حتى تتعرّف عليها جافاسكربت. <!-- وسم العنصر div لإظهار الاسم هنا--> <div show-info="name"></div> <!-- والعمر هنا --> <div show-info="age"></div> <script> // تجد الشيفرة العنصر الموسوم وتقوم بإظهار المطلوب let user = { name: "Pete", age: 25 }; for(let div of document.querySelectorAll('[show-info]')) { // ملئ الحقل بالمعلومات الموافقة له let field = div.getAttribute('show-info'); div.innerHTML = user[field]; //(*) } </script> See the Pen JS-p2-06-Attributes and properties-ex13 by Hsoub (@Hsoub) on CodePen. (*): إظهارالقيمة "Pete" داخل العنصر div الموسوم بـ name وإظهار القيمة 25 داخل العنصر div الموسوم بـage. كما يمكن استعمالها لتنسيق عنصر ما. فمثلا، تُستخدم هنا السمة order-state للتعبير عن حالة الطلب (إن كان جديدًا، معلّقًا أو ملغى). <style> /* تعتمد الأنماط على السمة غير المعيارية 'order-state' */ .order[order-state="new"] { color: green; } .order[order-state="pending"] { color: blue; } .order[order-state="canceled"] { color: red; } </style> <div class="order" order-state="new"> A new order. </div> <div class="order" order-state="pending"> A pending order. </div> <div class="order" order-state="canceled"> A canceled order. </div> See the Pen JS-p2-06-Attributes and properties-ex14 by Hsoub (@Hsoub) on CodePen. لماذا يُستحسن استخدام السمة بدل استخدام الأصناف order-state-canceled. وorder-state-pending. و order-state-new.؟ ذلك لأنه يَسهُل استعمال الخاصيات موازنةً مع الأصناف حيث يمكن تعديل الحالة بمنتهى السهولة كما في المثال التالي: // أسهل من حذف/إضافة صنف جديد div.setAttribute('order-state', 'canceled'); ولكن السمات غير المعيارية قد تتسبّب في المشكل التالي: ماذا لو استُخدمت سمةٌ غير معياريةٍ لغرضٍ ما وبعدها اِستُحدِثت كسمةٍ معياريةٍ تقوم بوظيفةٍ ما؟ تُعد لغة HTML حيّةً، ومتطورةً، حيث تَستحدِث باستمرار سماتٍ جديدةٍ تلبّي حاجات المطوّرين. قد ينجرّ عن هذا الأمر انعكاساتٍ غير متوقعة. ولتفادي حصول مثل هذا التضارب وُجدت السمات *-data. كلّ السمات التي تبدأ بـ "-data" هي سمات محجوزة يستعملها المطوّرون فقط. ويمكن الوصول إليها عبر الخاصية dataset. فمثلا، إذا كانت السمة المسمّاة "data-about" تابعة للعنصر elem ، يمكن الوصول إليها باستعمال elem.dataset.about كالآتي: <body data-about="Elephants"> <script> alert(document.body.dataset.about); // Elephants </script> See the Pen JS-p2-06-Attributes and properties-ex15 by Hsoub (@Hsoub) on CodePen. تُصبح أسماء السمات التي تتكوّن من عدّة كلمات مثل data-order-state على الشكل: dataset.orderState، حيث تُكتب -عند استعمالها كخاصيات- بنمط سنام الجمل أي بجعل أول حرف من كلّ كلمة كبيرًا عدا الكلمة الأولى. نعيد فيما يلي كتابة المثال السابق (المثال الخاص بـ "حالة الطلب") بطريقة أخرى. <style> .order[data-order-state="new"] { color: green; } .order[data-order-state="pending"] { color: blue; } .order[data-order-state="canceled"] { color: red; } </style> <div id="order" class="order" data-order-state="new"> A new order. </div> <script> // قراءة alert(order.dataset.orderState); // new // تعديل order.dataset.orderState = "pending"; // (*) </script> See the Pen JS-p2-06-Attributes and properties-ex16 by Hsoub (@Hsoub) on CodePen. يُعدّ استعمال السمات التي تبدأ بـ *-data طريقةً فعالةً وآمنةً لتمرير البيانات غير المعيارية. لاحظ هنا أنه لا يمكنك قراءة السمات *-data فحسب، بل حتى تعديلها، ومن ثمّ تُطبَّق تنسيقات CSS بناءً عليها. في المثال أعلاه، غيّرت تعليمة السطر الأخير (الموسوم بالرمز (*)) اللّون ليصبح أزرقًا. الخلاصة السمة هي ما يدوّن على شيفرة HTML الخاصية هي ما يندرج ضمن الكائن 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; } دوالّ العمليات على السمات هي: (elem.hasAttribute(name: للتحقق من وجود السمة. (elem.getAttribute(name: للحصول على قيمة السمة. (elem.setAttribute(name, value: إسناد القيمة value للسمة name. (elem.removeAttribute(name: حذف السمة. elem.attributes: يمثّل مجموعة السمات الخاصة بالعنصر المحدد. ويُستحسن استعمال خاصيات DOM في أغلب الحالات، وعليك اللجوء إلى استعمال السمات فقط حين تحتاج إلى ذلك وإذا كان استعمال خاصيات DOM لا يفي بالغرض. مثلا: *تحتاج إلى سمةٍ غير معيارية، ولكن إن كانت تبدأ بـ -data عليك استعمالdataset. *تُريد قراءة القيمة كما هي مكتوبة بالضبط في شيفرة HTML. قد تكون قيمة الخاصية DOM مختلفةً عنها. فمثلا، تكون الخاصية href دائما عبارة عن عنوان URL مطلق كامل، وقد تحتاج أنت الوصول إلى القيمة الأصلية/الأولى. تمارين الوصول إلى سمةٍ ما درجة الأهمية: 5 اُكتب الشيفرة التي تسمح باختيار العنصر من المستند باستعمال السمة data-widget-name وقراءة قيمتها. <!DOCTYPE html> <html> <body> <div data-widget-name="menu">Choose the genre</div> <script> /*أكتب الشيفرة هنا */ </script> </body> </html> الحل <!DOCTYPE html> <html> <body> <div data-widget-name="menu">Choose the genre</div> <script> // الحصول عليه let elem = document.querySelector('[data-widget-name]'); // قراءة القيمة alert(elem.dataset.widgetName); // أو alert(elem.getAttribute('data-widget-name')); </script> </body> </html> تغيير لون الروابط الخارجية إلى البرتقالي درجة الأهمية: 3 غيّر لون جميع الروابط الخارجية إلى البرتقالي من خلال التعديل على خاصية style . يُعدّ الرابط خارجيًا إذا: كانت السمة href الخاصة به تضمّ السلسلة النصّية '//:' لم تكن تبدأ بـالسلسلة النصّية 'http://internal.com' مثال: <a name="list">the list</a> <ul> <li><a href="http://google.com">http://google.com</a></li> <li><a href="/tutorial">/tutorial.html</a></li> <li><a href="local/path">local/path</a></li> <li><a href="ftp://ftp.com/my.zip">ftp://ftp.com/my.zip</a></li> <li><a href="http://nodejs.org">http://nodejs.org</a></li> <li><a href="http://internal.com/test">http://internal.com/test</a></li> </ul> <script> // تنسيق رابط واحد let link = document.querySelector('a'); link.style.color = 'orange'; </script> ينبغي أن تكون النتيجة كالآتي: الحل عليك أولا إيجاد كلّ الروابط الخارجية. وهناك طريقتان: الأولى من خلال إيجاد كافة الروابط باستعمال ('document.querySelectorAll('a وبعدها أخذ ما تحتاج إليه فقط. let links = document.querySelectorAll('a'); for (let link of links) { let href = link.getAttribute('href'); if (!href) continue; // لا وجود للسمة if (!href.includes('://')) continue; // لا وجود للسلسلة النصية '://' if (href.startsWith('http://internal.com')) continue; // رابط داخلي link.style.color = 'orange'; } لاحظ هنا أنّنا نستعمل ('link.getAttribute('href وليس link.href لأنّنا نريد استخراج القيمة من شيفرة HTML . والطريقة الثانية، أبسط من الأولى، تكون بإضافة عمليات التحقّق لمحدّد CSS: البحث في href عن كلّ الروابط التي تضمّ السلسلة النصّية '//:' // // ولا تبدأ بـالسلسلة النصّية 'http://internal.com' let selector = 'a[href*="://"]:not([href^="http://internal.com"])'; let links = document.querySelectorAll(selector); links.forEach(link => link.style.color = 'orange'); ترجمة -وبتصرف- للفصل Attributes and properties من كتاب Browser: Document, Events, Interfaces
  10. لنتناول عُقدَ DOM بمزيدٍ من التعمّق. في هذا المقال، سننظر أكثر في ماهيّتها وسنتعرّف على أكثر خاصّيّاتها استخداما. أصناف العقد في DOM قد تختلف خاصّيات العقد باختلاف العقد نفسها. فعلى سبيل المثال، تملك العقد التي تمثّل الوسم <a> خاصّيّات تتعلّق بالروابط، وتملك العقد التي تمثّل الوسم <input> خاصّيّات تتعلّق بالمُدخلات، وهكذا. لكن بالرغم من اختلاف العقد عن بعضها، فإنّ لها أيضا خاصّيّات وتوابع مشتركة بينها، ﻷنّ جميع أصناف العقد في DOM تُشكّل تسلسلا هرميّا واحدا. تنتمي كلّ عقدة من DOM إلى صنف معيّن من اﻷصناف المسبقة البناء (built-in class). يكون جذر التسلسل الهرميّ هو الصنف EventTarget، الذي يرث عنه الصنفُ Node، الذي يرث عنه بدوره باقي العقد في DOM. هذه صورة للتسلسل الهرمي، وسيتبعها المزيد من الشرح: تتمثّل اﻷصناف في: EventTarget -- هو صنفٌ "مجرّدٌ" (abstract) يكون عند الجذر. لا تُنشأ كائنات من هذا الصنف أبدا. وهو بمثابة القاعدة التي تُمكّن جميع العُقد في DOM من دعم من يُسمّى باﻷحداث (التي سندرسها لاحقا). Node -- هو أيضا صنفٌ "مجرّد" بمثابة القاعدة للعقد في DOM، ويوفّر الوظائف الشجريّة اﻷساسيّة مثل parentNode و nextSibling و childNodes وغيرها (التي تُسمّى جالبة - getters). لا تُنشأ كائنات من هذا الصنف، لكنّ هناك أصنافا حقيقيّة (concrete) من العُقد التي ترث هذا الصنف، منها Text للعقد النصّيّة و Element للعقد العنصريّة وأصنافٌ أخرى أغرب مثل Comment للعقد التعليقيّة. Element -- هو الصنف الأساسيّ للعناصر في DOM. يتيح توابع للتنقّل بين العناصر مثل nextElementSibling و children وتوابع للبحث مثل getElementsByTagName و querySelector. بما أنّ المتصفّح لا يدعم فقط HTML بل XML و SVG كذلك، فإنّ الصنف Element يمثّل قاعدة لأصنافٍ أخصّ مثل SVGElement و XMLElement و HTMLElement. HTMLElement -- هو الصنف اﻷساسيّ لجميع عناصر HTML ، ويرث عنه عناصرُ HTML الحقيقيّة: HTMLInputElement -- هو صنف العناصر <input>. HTMLBodyElement -- هو صنف العناصر <body>. HTMLAnchorElement -- هو صنف العناصر <a>. … إلى غير ذلك، فلكلّ وسمٍ صنفه الخاصّ الذي قد يتيح له خاصّيّات وتوابع تختصّ به. إذًا فمجموع الخاصّيّات والتوابع التي لدى عقدةٍ ما هو ناتج عن الوراثة. على سبيل المثال، لنأخذ كائن DOM لعنصر <input>. ينتمي هذا الكائن إلى صنف HTMLInputElement، ويكتسب خاصّياته وتوابعه من تراكم اﻷصناف التالية (مرتبة بحسب الوراثة): HTMLInputElement -- يتيح هذا الصنف خاصّيّات تتعلّق بالمُدخلات. HTMLElement -- يتيح توابع مشتركة بين عناصر HTML (وجوالب/ضوابط -- getters/setters). Element -- يتيح توابع عامّة للعناصر. Node -- يتيح خاصّيات مشتركة بين عقد DOM. EventTarget -- يتيح الدعم للأحداث (سنتطرق لها لاحقا). … وأخيرا هو يرث من Object ، مما يجعل توابع "الكائنات الصرفة" مثل hasOwnProperty متاحة له كذلك. لمعرفة اسم صنف العقدة في DOM، يكفي تذكّر أنّ للكائنات عادةً خاصّيّة constructor. تشير هذه الخاصّيّة إلى الدالّة البانية (constructor) الخاصّة بالصنف، و constructor.name هو اسمها: alert( document.body.constructor.name ); // HTMLBodyElement See the Pen JS-p2-05-basic-dom-node-properties-ex1 by Hsoub (@Hsoub) on CodePen. … أو يكفي مجرّد تطبيق toString عليه: alert( document.body ); // [object HTMLBodyElement] See the Pen JS-p2-05-basic-dom-node-properties-ex2 by Hsoub (@Hsoub) on CodePen. يمكن كذلك استخدام instanceof للتحقّق من الوراثة: alert( document.body instanceof HTMLBodyElement ); // true alert( document.body instanceof HTMLElement ); // true alert( document.body instanceof Element ); // true alert( document.body instanceof Node ); // true alert( document.body instanceof EventTarget ); // true See the Pen JS-p2-05-basic-dom-node-properties-ex3 by Hsoub (@Hsoub) on CodePen. كما يمكن أن نلاحظ، تُعدّ عقد DOM كائنات جافاسكربت عاديّة، وهي تعتمد في التوريث على كائن prototype. يمكن أيضا ملاحظة ذلك بسهولة من خلال عرض عنصرٍ ما بواسطة (console.dir(elem في المتصفّح. يمكنك ملاحظة كلٍّ من HTMLElement.prototype و Element.prototype وغيرها في الطرفية. (console.dir(elem مقابل (console.log(elem تدعم أدوات المطوّر في معظم المتصفحات اﻷمرين: console.log و console.dir. يعرض كلّ منهما معاملاته في الطرفيّة. بالنسبة لكائنات جافاسكربت فإنّ هذين اﻷمرين متماثلان في العادة. لكن بالنسبة لعناصر DOM فهما مختلفان: تعرض (console.log(elem شجرة DOM الخاصّة بالعنصر. تعرض (console.dir(elem العنصر على شكل كائن DOM، وتساعد على استكشاف خاصيّاته. جرّب ذلك على document.body IDL في المواصفة لا توصف أصناف DOM في المواصفة باستخدام جافاسكربت، لكن باستخدام لغة خاصة تُسمى لغة وصف الواجهة (Interface description language أو IDL باختصار)، وهي سهلة الفهم في العادة. في IDL، تُسبق جميع الخاصّيّات بنوعها. فمثلا DOMString و boolean وهكذا. إليك مقتطفا منها مع التعليقات: // HTMLInputElement تعريف // HTMLElement يرث من HTMLInputElement تعني النقطان الرئسيتان ":" أنّ interface HTMLInputElement: HTMLElement { // <input> هنا توضع خاصّيّات وتوابع العنصر // أنّ قيمة الخاصّيّة هي عبارة عن سلسلة نصيّة "DOMString" تعني attribute DOMString accept; attribute DOMString alt; attribute DOMString autocomplete; attribute DOMString value; // (true/false) خاصّيّة ذات قيمةٍ بوليانية attribute boolean autofocus; ... // أن التابع لايعيد أيّة قيمة "void" وهنا مكان التابع: تعني void select(); ... } خاصية "nodeType" تقدّم خاصّيّة "nodeType" طريقة أخرى "قديمة الطراز" للحصول على "نوع" العقدة في DOM. لهذه الخاصّيّة قيمة عددية: elem.nodeType == 1 للعقد العنصريّة. elem.nodeType == 3 للعقد النصّيّة. elem.nodeType == 9 لكائن document. هناك بعض القيم اﻷخرى في المواصفة. على سبيل المثال: <body> <script> let elem = document.body; // لنرى ما نوع هذه العقدة alert(elem.nodeType); // 1 => عنصر // ... وأوّل ابن لها هو alert(elem.firstChild.nodeType); // 3 => نصّ // 9 النوع هو ،document بالنسبة لكائن alert( document.nodeType ); // 9 </script> </body> See the Pen JS-p2-05-basic-dom-node-properties-ex4 by Hsoub (@Hsoub) on CodePen. في السكربتات الحديثة، يمكننا استخدام instanceof وغيرها من الاختبارات المعتمدة على الصنف لمعرفة نوع العقدة، لكن أحيانا قد تكون nodeType أبسط. يمكننا فقط قراءة nodeType دون التعديل عليها. الوسم: nodeName و tagName إذا كانت لدينا عقدة من DOM، فيمكننا قراءة اسم وسمها من خلال خاصّيّات nodeName و tagName. على سبيل المثال: alert( document.body.nodeName ); // BODY alert( document.body.tagName ); // BODY See the Pen JS-p2-05-basic-dom-node-properties-ex5 by Hsoub (@Hsoub) on CodePen. هل هناك فرق بين tagName و nodeName؟ بالتأكيد، يظهر الفرق في اسميهما لكنّه في الحقيقة طفيف نوعا ما. تكون خاصّيّة tagName للعقد العنصريّة فقط. تكون خاصّية nodeName مُعرّفةً في جميع العقد: بالنسبة للعناصر هي مماثلة لـ tagName. بالنسبة ﻷنواع العقد اﻷخرى (النصوص والتعليقات وغيرها) فهي سلسلة نصّيّة تحمل نوع العقدة. بعبارة أخرى، تُدعم tagName من العقد النصّيّة فقط (إذ أنّ منشأها من الصنف Element)، بينما تستطيع nodeName الإخبار عن أنواع العقد اﻷخرى. على سبيل المثال، لنوازن بين tagName و nodeName بتطبيقهما على كائن document وعلى عقدة تعليقيّة: <body><!-- comment --> <script> // بالنسبة للتعليق alert( document.body.firstChild.tagName ); // undefined (not an element) -- (أي غير معرّف (ليس عنصرا alert( document.body.firstChild.nodeName ); // #comment // document بالنسبة لـ alert( document.tagName ); // undefined (not an element) alert( document.nodeName ); // #document </script> </body> See the Pen JS-p2-05-basic-dom-node-properties-ex6 by Hsoub (@Hsoub) on CodePen. إذا اقتصرنا على التعامل مع العناصر فقط، فيمكننا استعمال كلٍّ من tagName و nodeName -- لا فرق بينهما. يكون اسم الوسم دائما باﻷحرف الكبيرة (uppercase) ماعدا في وضع XML للمتصفّح وضعان لمعالجة المستندات: HTML و XML. يُستخدم وضع HTML عادة مع صفحات الويب. يُفًّعل وضع XML عندما يستقبل المتصفّح مستند XML بالترويسة Content-Type: application/xml+xhtml. في وضع HTML، تكون tagName و nodeName دائما باﻷحرف الكبيرة: فهي BODY سواء بالنسبة لـ <body> أو <BoDy>. في وضع XML، تُترك اﻷحرف "كما هي". في الوقت الحاضر، يندر استخدام وضع XML. innerHTML: المحتويات تُمكّن innerHTML من تحصيل الـ HTML الذي بداخل العنصر على شكل سلسلة نصيّة. كما يمكننا أيضا التعديل عليه، مما يجعله من أقوى الطرق للتعديل على الصفحة. هذا المثال يبيّن كيفيّة عرض محتويات document.body ثم استبدالها كليّة: <body> <p>A paragraph</p> <div>A div</div> <script> alert( document.body.innerHTML ); // قراءة المحتويات الحاليّة document.body.innerHTML = 'The new BODY!'; // ثم استبدالها </script> </body> See the Pen JS-p2-05-basic-dom-node-properties-ex7 by Hsoub (@Hsoub) on CodePen. يمكننا محاولة إدخال HTML غير سليم، ليقوم المتصفّح بتصحيح الأخطاء: <body> <script> document.body.innerHTML = '<b>test'; // نسينا إغلاق الوسم alert( document.body.innerHTML ); // <b>test</b> (صٌلّحت) </script> </body> See the Pen JS-p2-05-basic-dom-node-properties-ex8 by Hsoub (@Hsoub) on CodePen. لا تُنفّذ السكربتات إذا أدرجت innerHTML سكربت <script> في المستند، فإنّه يصير جزءًا من HTML لكنّه لا يُنفّذ. تعيد "=+innerHTML" الكتابة كليّة يمكننا إضافة HTML إلى عنصر ما بواسطة "elem.innerHTML+="more html هكذا: chatDiv.innerHTML += "<div>Hello<img src='smile.gif'/> !</div>"; chatDiv.innerHTML += "How goes?"; لكن يجب أن ننتبه عند فعل ذلك، فما يحدث فعلا ليست مجرّد إضافة، بل هي إعادة كتابة بالكليّة. ظاهريّا، يفعل هذان السطران نفس الشيء: elem.innerHTML += "..."; // :هو طريقة مختصرة لكتابة elem.innerHTML = elem.innerHTML + "..." بعبارة أخرى، ما تقوم به =+innerHTML هو التالي: يُحذف المحتوى القديم. يُكتب في مكانه innerHTML الجديد (الذي هو سلسلة متكوّنة من المحتوى القديم والجديد). نتيجةً "لتصفير" المحتوى وإعادة كتابته من جديد، فإنّه يُعاد تحميل جميع الصّور وغيرها من الموارد. في مثال chatDiv أعلاه، يعيد السطر ?"chatDiv.innerHTML+="How goes إنشاء محتوى HTML ثمّ يعيد تحميل الصّورة smile.gif (يُأمل أن تكون مخبّأةً في الذاكرة المؤقتة - Cache). إذا كان لدى chatDiv الكثير من النصوص والصور اﻷخرى، فإنّ إعادة التحميل تصير ظاهرة للعيان. هناك آثار جانبية أخرى كذلك. على سبيل المثال، إذا كان النصّ الموجود قد حُدّد بواسطة الفأرة، فإنّ معظم المتصفّحات ستزيل التحديد عنه عند إعادة كتابة innerHTML . كما أنّه إذا كان هناك <input> فيها نصّ أدخله الزائر، فإنّ النصّ سيُزال أيضا. إلى غير ذلك. لحسن الحظّ، هناك طرق أخرى لإضافة HTML عدا innerHTML ، سندرسها قريبا. outerHTML: كامل HTML العنصر تحتوي خاصّيّة outerHTML على كامل HTML العنصر. تشبه هذه الخاصّيّة innerHTML لكن بالإضافة إلى العنصر نفسه. إليك مثالا: <div id="elem">Hello <b>World</b></div> <script> alert(elem.outerHTML); // <div id="elem">Hello <b>World</b></div> </script> See the Pen JS-p2-05-basic-dom-node-properties-ex9 by Hsoub (@Hsoub) on CodePen. انتبه: على خلاف innerHTML، لا يؤدي التعديل على outerHTML إلى تعديل العنصر، بل سيؤدي ذلك إلى استبداله في DOM. نعم، يبدو ذلك غريبا، وهو فعلا كذلك. لهذا خصصنا له تنبيها منفصلا هنا. ألقِ نظرة على هذا المثال: <div>Hello, world!</div> <script> let div = document.querySelector('div'); // <p>...</p> بـ div.outerHTML استبدال div.outerHTML = '<p>A new element</p>'; // (*) // على حاله 'div' ياللجعب! بقي alert(div.outerHTML); // <div>Hello, world!</div> (**) </script> See the Pen JS-p2-05-basic-dom-node-properties-ex10 by Hsoub (@Hsoub) on CodePen. يبدو ذلك غريبا، صحيح؟ في السطر (*)، استبدلنا <div> بـ <p>A new element</p>. في المستند الخارجي (DOM)، يمكننا ملاحظة المحتوى الجديد بدل <div>. لكن كما يمكن الملاحظة في السطر (**)، لم تتغيّر قيمة المتغيّر القديم div! لا يغيّر الإسنادُ outerHTML العنصرَ في DOM (في هذا المثال، الكائن الذي يشير إليه المتغير 'div')، لكنّه يحذفه من DOM ويدرج مكانه الـ HTML الجديد. فالذي حصل بفعل ...=div.outerHTML هو ما يلي: حُذف div من المستند. أُدرج في مكانه قطعة HTML أخرى <p>A new element</p> . لا يزال لـ div نفس القيمة. ولم يُحفظ HTML الجديد في أيّ متغيّر. من السهل جدّا ارتكاب خطأ هنا: التعديل على div.outerHTML ثمّ مواصلة العمل بـ div وكأنّ فيه المحتوى الجديد. لكنّه ليس كذلك. ينطبق هذا على innerHTML لكن ليس على outerHTML. بإمكاننا التعديل على elem.outerHTML، لكن ينبغي التنبّه إلى أنّ ذلك لا يغيّر العنصر 'elem'، بل يضع مكانه HTML الجديد. يمكننا الحصول على مراجع للعناصر الجديدة من خلال البحث في DOM. nodeValue/data: محتوى العقد النصية لا تصلح الخاصّيّة innerHTML إلّا مع العقد العنصريّة. بالنسبة للعقد الأخرى، كالعقد النصّيّة، يقابل ذلك خاصّيّات nodeValue و data. هاتان الخاصّيّتان متماثلتان تقريبا في التطبيق، مع بعض الفروق الطفيفة في المواصفة. لذا سنستعمل data ﻷنّها أقصر. هذا مثالٌ لقراءة محتوى عقدة نصّيّة وتعليق: <body> Hello <!-- Comment --> <script> let text = document.body.firstChild; alert(text.data); // Hello let comment = text.nextSibling; alert(comment.data); // Comment </script> </body> See the Pen JS-p2-05-basic-dom-node-properties-ex11 by Hsoub (@Hsoub) on CodePen. يمكننا تصوّر دافعٍ لقراءة وتعديل العقد النصّيّة، لكن لماذا التعليقات؟ يُضمّن فيها المطوّرون أحيانا معلومات أو تعليمات متعلّقة بالقالب في HTML، على هذا النحو: <!-- if isAdmin --> <div>Welcome, Admin!</div> <!-- /if --> … فيمكن لجافاسكربت قراءتها من خلال خاصّيّة data ثمّ معالجة التعليمات التي تتضمّنها. textContent: نص محض تتيح خاصّية textContent الوصول إلى النصّ الذي بداخل العنصر: مجرّد النصّ، دون أيّة وسوم. على سبيل المثال: <div id="news"> <h1>Headline!</h1> <p>Martians attack people!</p> </div> <script> // Headline! Martians attack people! alert(news.textContent); </script> See the Pen JS-p2-05-basic-dom-node-properties-ex12 by Hsoub (@Hsoub) on CodePen. كما يمكن أن نلاحظ، أُعيد النصّ فقط، وكأنّ جميع الـوسوم قُطعت، وبقي النصّ الذي بداخلها. عمليّا، يندر أن يُحتاج إلى قراءة مثل هذا النصّ. يعدّ التعديل على textContent مفيدا أكثر بكثير، لأنّه يمكّن من كتابة النصوص على "النحو الآمن". لنتفرض أن لدينا سلسلة نصّيّة ما، أدخلها مستخدمٌ مثلا، ونريد أن نعرضها. باستخدام innerHTML سندرجها "على شكل HTML"، بجميع وسوم HTML. باستخدام textContent سندرجها "على شكل نصّ"، وتُعامل جميع الرموز معاملة حرفيّة. قارن بين الإثنين: <div id="elem1"></div> <div id="elem2"></div> <script> let name = prompt("What's your name?", "<b>Winnie-the-pooh!</b>"); elem1.innerHTML = name; elem2.textContent = name; </script> See the Pen JS-p2-05-basic-dom-node-properties-ex13 by Hsoub (@Hsoub) on CodePen. يحصُل <div> اﻷوّل على الاسم (name) "على شكل HTML": جميع الوسوم تُعامل كوسوم، ونرى بذلك الاسم بخطّ عريض (bold). يحصُل <div> الثاني على الاسم "على شكل نصّ"، فنرى حرفيًّا <b>Winnie-the-pooh!</b>. في أغلب الحالات، نتوقّع من المستخدم أن يدخل نصًّا، ونريد أن نعامله كنصّ. لا نريد HTML غير متوقّع في موقعنا. وهذا ما يفعله الإسناد إلى textContent بالضبط. خاصية 'hidden' تُحدّد السّمة "hidden" وخاصيّة DOM التي تحمل نفس الاسم ما إذا كان العنصر مرئيّا أو لا. بإمكاننا استخدامها في HTML أو إسنادها في جافاسكربت كالتالي: <div>Both divs below are hidden</div> <div hidden>With the attribute "hidden"</div> <div id="elem">JavaScript assigned the property "hidden"</div> <script> elem.hidden = true; </script> See the Pen JS-p2-05-basic-dom-node-properties-ex14 by Hsoub (@Hsoub) on CodePen. مبدئيّا، تعمل hidden تماما مثل "style="display:none ، لكنّها أقصر في الكتابة. إليك عنصرا وامضًا: <div id="elem">A blinking element</div> <script> setInterval(() => elem.hidden = !elem.hidden, 1000); </script> See the Pen JS-p2-05-basic-dom-node-properties-ex15 by Hsoub (@Hsoub) on CodePen. المزيد من الخاصيات للعناصر في DOM المزيد من الخاصّيّات، خصوصا تلك التي تتعلق باﻷصناف: value -- القيمة التي في <input> و <select> و <textarea> (المتعلقة بـ HTMLInputElement و HTMLSelectElement …). href -- قيمة "href" في <"..."=a href> (المتعلقة بـ HTMLAnchorElement). id -- قيمة السمة "id" لجميع العناصر (المتعلقة بـHTMLElement). … والمزيد من ذلك… على سبيل المثال: <input type="text" id="elem" value="value"> <script> alert(elem.type); // "text" alert(elem.id); // "elem" alert(elem.value); // value </script> See the Pen JS-p2-05-basic-dom-node-properties-ex16 by Hsoub (@Hsoub) on CodePen. تقابل معظم السمات القياسية في HTML خاصّيات في DOM، ويمكننا الوصول إليها بهذه الطريقة. إذا أردنا معرفة جميع الخاصّيّات المدعومة في صنف معيّن، يمكننا الاطلاع عليها في المواصفة. على سبيل المثال، HTMLInputElement موثّقة هنا. أمّا إذا أردنا الحصول عليها بسرعة، أو كنّا مهتمين بمواصفة ملموسة في المتصفّح، يمكننا استعراض العنصر بواسطة (console.dir(elem لقراءة الخاصّيّات. أو يمكن كذلك استكشاف "خاصّيّات DOM" في لسان العناصر (Elements) في أدوات المطوّر في المتصفّح. الملخص تنتمي كلّ عقدة في DOM إلى صنف معيّن. تشكّل اﻷصناف تسلسلا هرميّا. يكون مجموع الخاصّيّات والتوابع التي لدى عقدة ما ناتجا عن الوراثة. خاصّيّات عقد DOM الرئيسيّة هي: nodeType: يمكننا استعمالها لمعرفة ما إذا كانت العقدة نصيّة أو عنصريّة. لها القيمة العددية: 1 للعناصر و 3 للعقد النصّيّة، وبعض القيم اﻷخرى لباقي أنواع العقد. يمكن قراءتها فقط. nodeName/tagName: للعناصر tagName (تُكتب باﻷحرف الكبيرة ماعدا في وضع XML)، وللعقد غير العنصريّة nodeName. تُبيّن ماهية العقدة. يمكن قراءتها فقط. innerHTML: ما يحتويه العنصر من HTML. يمكن التعديل عليها. outerHTML: كامل HTML العنصر. لا تمسّ عمليّة التعديل على elem.outerHTML العنصر elem نفسه، لكن تستبدله بالـ HTML الجديد في DOM. nodeValue/data: محتوى العقد غير العنصريّة (النصوص والتعليقات). هاتان الخاصّيّتان متماثلتان تقريبا، لكن تُستعمل في العادة data . يمكن التعديل عليها. textContent: النصّ الذي بداخل العنصر: HTML منزوع الوسوم. يضع التعديلُ عليه النصَّ بداخل العنصر، مع معاملة جميع المحرّفات الخاصّة والوسوم كمجرّد نصّ. تُمكّن من إدراج النصوص التي يُدخلها المستخدمون بطريقة آمنة تحمي من إدراج HTML غير مرغوب فيه. hidden: عند إعطائها القيمة true تؤدّي نفس وظيفة display:none في CSS. تملك عقد DOM المزيد من الخاصّيّات بحسب الصنف الذي تنتمي إليه. على سبيل المثال، تدعم العناصر <input> (ذات الصنف HTMLInputElement) كلاّ من value و type ، بينما تدعم <a> (ذات الصنف HTMLAnchorElement) الخاصيّة href ، إلى غير ذلك. لمُعظم السمات القياسية في HTML خاصيّة تقابلها في DOM. لكن مع ذلك، لا تتساوى سمات HTML وخاصيّات DOM على الدوام، كما سنرى في المقال التالي. التمارين حساب الفروع اﻷهميّة: 5 هناك شجرة متشكّلة من تفرّع العناصر ul/li. اكتب الشفرة التي من أجل كل <li> تستعرض التالي: النصّ الذي بداخله (دون الشجرة الفرعيّة). عدد الـ <li> المتفرّعة عنه -- جميع العقد السليلة، بما في ذلك العقد المتفرّعة عن فروعه إلى آخر ذلك. افتح المثال في نافذة مستقلة افتح البيئة التجريبيّة لإنجاز التمرين الحل لنعمل حلقة تكراريّة حول جميع الـ <li>: for (let li of document.querySelectorAll('li')) { ... } نحتاج في الحلقة أن نحصل على النصّ بداخل كلّ li. يمكننا قراءة النصّ الذي بداخل أوّل العقد اﻷبناء لـ li ، والتي هي العقدة النصّيّة: for (let li of document.querySelectorAll('li')) { let title = li.firstChild.data; // الذي يأتي قبل باقي العقد <li> هو النصّ داخل title } بعدها يمكننا الحصول على عدد العقد السليلة باستخدام li.getElementsByTagName('li').length. افتح الحلّ في البيئة التجريبيّة مالذي بداخل nodeType؟ الأهميّة: 5 ماذا يعرض السكربت التالي: <html> <body> <script> alert(document.body.lastChild.nodeType); </script> </body> </html> الحل توجد خدعة هنا. في الوقت الذي يُنفّذ فيه <script> ، تكون آخر العقد في DOM هي <script> نفسه، ﻷنّ المتصفّح لم يكن قد عالج بقيّة الصفحة بعد. إذا الجواب هو 1 (عقدة عنصريّة). <html> <body> <script> alert(document.body.lastChild.nodeType); </script> </body> </html> وسم في التعليق اﻷهميّة: 3 ماذا تعرض هذه الشفرة: <script> let body = document.body; body.innerHTML = "<!--" + body.tagName + "-->"; alert( body.firstChild.data ); // ماذا هنا؟ </script> الحل الجواب هو، BODY <script> let body = document.body; body.innerHTML = "<!--" + body.tagName + "-->"; alert( body.firstChild.data ); // BODY </script> الذي يحصل خطوة بخطوة: يُستبدل محتوى <body> بالتعليق. التعليق هو <--BODY--!> ، لأنّ "body.tagName == "BODY. كما نذكر، يٌكتب tagName دائما بالأحرف الكبيرة في HTML. التعليق الآن هو الابن الوحيد، فنحصل عليه بواسطة body.firstChild. تكون خاصّيّة data بالنسبة للتعليق هي محتواه (ما بداخل <--...--!>): "BODY". أين يقع "المستند" في التسلسل الهرمي: اﻷهميّة: 4 ما هو الصنف الذي ينتمي إليه document ؟ ما هو مكانه في التسلسل الهرميّ لـ DOM ؟ هل يرث من Node أو Element أو ربّما HTMLElement ؟ الحل يمكننا معرفة الصنف الذي ينتمي إليه من خلال عرضها هكذا: alert(document); // [object HTMLDocument] أو: alert(document.constructor.name); // HTMLDocument إذًا document هو نسخة من الصنف HTMLDocument. ما هو مكانه في التسلسل الهرميّ ؟ نعم، يمكننا تصفّح المواصفة، لكن سيكون أسرع لو اكتشفنا ذلك يدويّا. لنجتز سلسلة prototype بواسطة __proto__. كما نعلم، توجد توابع الصنف في prototype الدالة البانية. على سبيل المثال، توجد توابع المستندات في HTMLDocument.prototype. بالإضافة إلى ذلك، هناك مرجعٌ إلى الدالة البانية بداخل prototype: alert(HTMLDocument.prototype.constructor === HTMLDocument); // true للحصول على اسم الصنف على شكل سلسلة نصيّة، يمكننا استخدام constructor.name. لنفعل ذلك مع جميع سلسلة prototype الخاصّة بـ document إلى غاية الصنف Node: alert(HTMLDocument.prototype.constructor.name); // HTMLDocument alert(HTMLDocument.prototype.__proto__.constructor.name); // Document alert(HTMLDocument.prototype.__proto__.__proto__.constructor.name); // Node هذا هو التسلسل الهرميّ. يمكننا كذلك تفحّص الكائن باستخدام (console.dir(document ثم معرفة هذه اﻷسماء من خلال فتح __proto__. تأخذ الطرفيّة هذه اﻷسماء من constructor داخليّا. ترجمة وبتصرف لمقال Node properties: type, tag and contents من كتاب The Modern JavaScript Tutorial.
  11. تُفيد خصائص التنقّل في DOM كثيرا عندما تكون العناصر قريبة من بعضها البعض. لكن ماذا لو لم تكن كذلك؟ كيف يمكن تحصيل عنصرٍ ما على الصفحة؟ هناك المزيد من توابع البحث لهذا الغرض. document.getElementById أو فقط id إذا كان للعنصر سمة id، فيمكننا تحصيله باستخدام التابع (document.getElementById(id، أينما وُجد. على سبيل المثال: <div id="elem"> <div id="elem-content">Element</div> </div> <script> // تحصيل العنصر let elem = document.getElementById('elem'); // تلوين خلفيّته باﻷحمر elem.style.background = 'red'; </script> See the Pen JS-p2-04-searching-elements-dom-ex01 by Hsoub (@Hsoub) on CodePen. بالإضافة إلى هذا، يمكن الإشارة إلى العنصر بواسطة متغيّر عامّ (global variable) اسمه نفس قيمة الـ id. <div id="elem"> <div id="elem-content">Element</div> </div> <script> // id="elem" الذي سمته DOM إلى عنصر elem يشير المتغيّر elem.style.background = 'red'; // واصلة في وسطه، لذا فلا يمكن أن يكون اسمًا لمتغير id="elem-content"لدى // window['elem-content'] لكن يمكن الوصول إليه بواسطة اﻷقواس المربعة... </script> See the Pen JS-p2-04-searching-elements-dom-ex02 by Hsoub (@Hsoub) on CodePen. … هذا إذا لم نصرّح بمتغيّر جافاسكربت له نفس الاسم، فإنّ اﻷولويّة حينها تكون له: <div id="elem"></div> <script> let elem = 5; // <div id="elem"> هو 5، وليس إشارة إلى elem يكون الآن alert(elem); // 5 </script> See the Pen JS-p2-04-searching-elements-dom-ex03 by Hsoub (@Hsoub) on CodePen. يُرجى عدم استخدام المتغيّرات العامّة المسمّات على قيم الـ id للوصول إلى العناصر هذا السلوك مُبيّن في المواصفة، مما يجعله وفق المعايير نوعا ما. لكنّه مدعوم في الغالب بغرض التوافق (compatibility). يحاول المتصفّح مساعدتنا بالمزج بين مجالات اﻷسماء (namespaces) في جافاسكربت و DOM. لا بأس بذلك في النصوص البرمجيّة البسيطة المضّمنة في HTML، لكن لا يُنصح به في العموم. فقد يؤدي ذلك إلى تناقضات في التسمية. كما أنّه عند قراءة شفرة جافاسكربت دون النظر إلى HTML، قد لا يتضّح من أين يأتي المتغيّر. نستعمل هنا في هذا المقال id للإشارة مباشرة إلى عنصر ما بغرض الاختصار، عندما يكون واضحا من أين يأتي العنصر. في التطبيقات الواقعيّة، تُعدّ document.getElementById هي الطريقة المفضّلة. يجب أن يكون الـ id فريدًا يجب أن يكون الـ id فريدًا. لا يمكن أن يحمل أكثر من عنصر في المستند نفس الـ id. إذا كانت هناك عدّة عناصر لها نفس الـ id ، فإنّه لا يمكن التنبؤ بسلوك التوابع التي تستخدمها. فقد تُرجع مثلا document.getElementById أيًّا من العناصر عشوائيًّا. لذا يُرجى الالتزام بالقاعدة وإبقاء الـ id فريدا. document.getElementById فقط، وليس anyElem.getElementById يمكن استدعاء التابع getElementById على الكائن document فقط. يبحث هذا التابع عن الـ id المراد في المستند بأكمله. querySelectorAll يُعدّ (css)elem.querySelectorAll أكثر التوابع تنوّعا في الاستخدام على الإطلاق. يعيد هذا التابع جميع العناصر التي ينطبق عليها محدّد selector) CSS) معيّن. نبحث في اﻷسفل مثلا عن جميع العناصر <li> التي هي آخر اﻷبناء: <ul> <li>The</li> <li>test</li> </ul> <ul> <li>has</li> <li>passed</li> </ul> <script> let elements = document.querySelectorAll('ul > li:last-child'); for (let elem of elements) { alert(elem.innerHTML); // "test", "passed" } </script> See the Pen JS-p2-04-searching-elements-dom-ex04 by Hsoub (@Hsoub) on CodePen. هذا التابع قويّ بالفعل، ﻷنّه يمكن معه استخدام أيّ محدّد CSS. يمكن استخدام أشباه اﻷصناف كذلك يدعم محدّد CSS أيضا أشباه اﻷصناف (pseudo-class) مثل hover: و active:. على سبيل المثال، يعيد ('document.querySelectorAll(':hover مجموعة العناصر التي يوجد عليها المؤشّر حاليّا (حسب ترتيب تفرعها: بداية من <html> إلى آخر فرع منها). querySelector يعيد استدعاء (elem.querySelector(css أوّل العناصر التي ينطبق عليها محدّد CSS. بعبارة أخرى، هي نفس نتيجة [elem.querySelectorAll(css)[0 ، لكنّ هذه اﻷخيرة تبحث عن جميع العناصر وتختار بعد ذلك أوّلها، بينما تبحث elem.querySelector عن عنصر واحد فقط. فهي بذلك أسرع في البحث وأقصر في الكتابة. matches تقوم التوابع السابقة بالبحث في DOM. لا يقوم (elem.matches(css بالبحث عن أيّ شيء، بل يتحقّق فقط من أنّ العنصر elem يطابق محدّد CSS المراد، ويعيد إمّا true أو false. يُفيد هذا التابع عند المرور على عدد من العناصر (في مصفوفة أو شيء من هذا القبيل) ونريد ترشيح العناصر التي تهمّنا فقط. على سبيل المثال: <a href="http://example.com/file.zip">...</a> <a href="http://ya.ru">...</a> <script> // document.body.children يمكن تطبيقها على أيّة مجموعة مكان for (let elem of document.body.children) { if (elem.matches('a[href$="zip"]')) { alert("The archive reference: " + elem.href ); } } </script> See the Pen JS-p2-04-searching-elements-dom-ex05 by Hsoub (@Hsoub) on CodePen. closest يتمثّل أسلاف (ancestors) عنصر ما في أبيه، وأب أبيه، وهكذا. يشكّل جميع اﻷسلاف معًا سلسلة الآباء التي تبتدئ من العنصر وتنتهي عند القمّة. يبحث تابع (elem.closest(css عن أقرب سلفٍ ينطبق عليه محدّد CSS. يشمل البحثُ العنصرَ elem نفسَه. بعبارة أخرى، ينطلق التابع closest صعودًا من العنصر المراد ويفحص كلّا من الآباء. فإذا طابق أحد اﻷسلاف المُحدِّد يتوقّف البحث، ويعيدُ السلفَ المطابق. على سبيل المثال: <h1>Contents</h1> <div class="contents"> <ul class="book"> <li class="chapter">Chapter 1</li> <li class="chapter">Chapter 1</li> </ul> </div> <script> let chapter = document.querySelector('.chapter'); // LI alert(chapter.closest('.book')); // UL alert(chapter.closest('.contents')); // DIV alert(chapter.closest('h1')); // (ليس من اﻷسلاف h1 ﻷن) null </script> See the Pen JS-p2-04-searching-elements-dom-ex06 by Hsoub (@Hsoub) on CodePen. *getElementsBy هناك أيضا توابع أخرى للبحث عن العقد حسب الوسم (tag) والصنف (class) وغيرها. تُعدّ هذه التوابع غالبًا من الماضي، إذ أنّ querySelector أقوى وأقصر في الكتابة. إذًا سنذكرها هنا من باب التمام، كما أنّه لا يزال من الممكن إيجادها في النصوص البرمجيّة القديمة. يبحث (elem.getElementsByTagName(tag عن العناصر التي لها نفس الوسم المُراد ويعيدهم جميعا على شكل مجموعة. يمكن للمعامل tag أن يكون نجمة أيضا "*" ليشمل "جميع الوسوم". يعيد (elem.getElementsByClassName(className العناصر التي لها نفس صنف CSS المراد. يعيد (document.getElementsByName(name كل العناصر التي في المستند التي لها نفس السمة name. يندر استعمال هذا التابع. على سبيل المثال: // التي في المستند div حصّل جميع وسوم let divs = document.getElementsByTagName('div'); لنجد جميع وسوم input التي في المستند: <table id="table"> <tr> <td>Your age:</td> <td> <label> <input type="radio" name="age" value="young" checked> less than 18 </label> <label> <input type="radio" name="age" value="mature"> from 18 to 50 </label> <label> <input type="radio" name="age" value="senior"> more than 60 </label> </td> </tr> </table> <script> let inputs = table.getElementsByTagName('input'); for (let input of inputs) { alert( input.value + ': ' + input.checked ); } </script> See the Pen JS-p2-04-searching-elements-dom-ex07 by Hsoub (@Hsoub) on CodePen. لا تنسَ حرف "s" للجمع! ينسى المطوّرون المبتدئون أحيانا الحرف "s"، فيعمدون إلى مناداة getElementByTagName بدل getElement<b>s</b>ByTagName. يخلو getElementByTagName من الحرف "s" ﻷنّه يعيد عنصرا وحيدا. لكنّ getElement<b>s</b>ByTagName يعيد مجموعة من العناصر، لهذا فإنّ بداخله "s". يعيد التابع مجموعة، وليس عنصرًا! من الأخطاء الشائعة لدى المبتدئين أيضا هو كتابة: // لا تعمل document.getElementsByTagName('input').value = 5; لن يعمل ذلك، ﻷنّه يحاول إسناد القيمة إلى مجموعةٍ من المُدخلات بدل إسنادها إلى العناصر التي تحتويها. يجب علينا إمّا التكرار على المجموعة أو تحصيل عنصر بواسطة معامله ثمّ إسناد القيمة له هكذا: // (ًيُفترض أن تعمل (إذا كانت هناك مُدخلات document.getElementsByTagName('input')[0].value = 5; للبحث عن العناصر ذات الصنف article.: <form name="my-form"> <div class="article">Article</div> <div class="long article">Long article</div> </form> <script> // name أوجد حسب السمة let form = document.getElementsByName('my-form')[0]; // أوجد حسب الصنف داخل النموذج let articles = form.getElementsByClassName('article'); alert(articles.length); // 2 - "article" وجدنا عنصران لهما الصنف </script> See the Pen JS-p2-04-searching-elements-dom-ex08 by Hsoub (@Hsoub) on CodePen. المجموعات الحية تُعيد جميع التوابع "*getElementsBy" مجموعة حيّة (live). تعكس هذه المجموعات الوضع الحاليّ الذي عليه المستند و "تُحدَّث تلقائيًّا" كلّما تغيّر. في المثال أدناه نصّان برمجيّان: يُنشئ اﻷوّل مرجعًا إلى مجموعة العناصر <div>؛ طول المجموعة عندها هو 1. يُنفَّذ النصّ الثاني بعدما يقابل المتصفّح عنصر <div> آخر، ويكون طول المجموعة حينها هو 2. <div>First div</div> <script> let divs = document.getElementsByTagName('div'); alert(divs.length); // 1 </script> <div>Second div</div> <script> alert(divs.length); // 2 </script> See the Pen JS-p2-04-searching-elements-dom-ex09 by Hsoub (@Hsoub) on CodePen. في المقابل، يعيد querySelectorAll مجموعة ساكنة (static)، كأنّها مصفوفة ثابتة من العناصر. إذا طبقناه في المثال أعلاه، فسيُخرج كلا النصّان 1: <div>First div</div> <script> let divs = document.querySelectorAll('div'); alert(divs.length); // 1 </script> <div>Second div</div> <script> alert(divs.length); // 1 </script> See the Pen JS-p2-04-searching-elements-dom-ex10 by Hsoub (@Hsoub) on CodePen. يمكننا الآن رؤية الفرق بوضوح. لم تزدد المجموعة الساكنة بظهور عنصر <div> جديد في المستند. الملخص هناك 6 توابع رئيسيّة للبحث عن العقد في DOM: التابع يبحث بواسطة ... يمكن مناداته على عنصر؟ حيّة؟ querySelector محدّد CSS ✔ - querySelectorAll محدّد CSS ✔ - getElementById id - - getElementsByName name - ✔ getElementsByTagName الوسم أو '*' ✔ ✔ getElementsByClassName الصنف ✔ ✔ table { width: 100%; } thead { vertical-align: middle; text-align: center;} td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } تُعدّ querySelector و querySelectorAll أكثرها استخداما على الإطلاق، لكن قد تُفيد *getElementBy أحيانا أو قد توجد في النصوص البرمجيّة القديمة. عدا ذلك: هناك (elem.matches(css للتحقّق من أنّ العنصر elem مطابق لمحدّد CSS المراد. هناك (elem.closest(css للبحث عن أقرب سلفٍ مطابق لمحدّد CSS المراد. يشمل البحثُ العنصرَ نفسه. ولنضف هنا تابعا آخر للتحقّق من العلاقة ابن-أب، إذ قد تفيد أحيانا: يعيد (elemA.contains(elemB القيمة true إذا كان elemB داخلا تحت elemA (أي عنصرا سليلا لـ elemA) أو إذا كان elemA==elemB. التمارين البحث عن العناصر اﻷهميّة: 4 إليك مستندًا يحتوي على جدول ونموذج. كيف يمكن إيجاد؟ … الجدول الذي له "id="age-table. جميع العناصر label بداخل ذلك الجدول (من المفترض أن تكون هناك 3 منها). أوّل td في ذلك الجدول (التي فيها الكلمة "Age"). النموذج form الذي له "name="search. أوّل input في ذلك النموذج. آخر input في ذلك النموذج افتح table.html في نافذة مستقلة واستعمل أدوات المتصفّح لذلك. الحل هناك عدّة طرق لفعل ذلك. هذه إحداها: // id="age-table" الجدول الذي له let table = document.getElementById('age-table') // بداخل ذلك الجدول label جميع العناصر table.getElementsByTagName('label') // أو document.querySelectorAll('#age-table label') // ("Age" في ذلك الجدول (التي فيها الكلمة td أوّل table.rows[0].cells[0] // أو table.getElementsByTagName('td')[0] // أو table.querySelector('td') // name="search" النموذج الذي له // name="search" على افتراض أنّ هناك عنصرا واحدا في المستند له let form = document.getElementsByName('search')[0] // خصّيصا form ،أو document.querySelector('form[name="search"]') // في ذلك النموذج input أوّل form.getElementsByTagName('input')[0] // أو form.querySelector('input') // في ذلك النموذج input آخر let inputs = form.querySelectorAll('input') // inputs أوجد جميع الـ inputs[inputs.length-1] // اختر آخرها ترجمة وبتصرف لمقال *Searching: getElement*, querySelector من كتاب The Modern JavaScript Tutorial
  12. يُمكّننا DOM من فعل أيّ شيء بالعناصر وما تحتويه، لكن نحتاج أوّلا إلى أن نصل إلى الكائن المحدّد من DOM. تبدأ جميع العمليّات على DOM بالكائن document. فهو "نقطة الدخول" الرئيسيّة إلى DOM، ويمكن من خلاله الوصول إلى جميع العقد. تمثّل الصورة التالية الروابط التي يمكن من خلالها التنقّل بين العقد في DOM. لنتناولها بمزيدٍ من التفصيل. في اﻷعلى: documentElement و body تُتاح العقد العلويّة للشجرة مباشرةً على شكل خاصّيّات لكائن document: <html> =‏ document.documentElement: تكون العقدة التي في قمّة الشجرة document.documentElement ، وهي عقدة DOM التي تمثّل وسم <html>. <body> =‏ document.body: من العقد التي يكثر استعمالاها أيضا عنصر <body> الذي يمثّله كائن document.body <head> =‏ document.head: يكون وسم <head> متاحا كـ document.head. هناك خدعة: قد يكون document.body عديم القيمة (أي null) لا يستطيع السكربت الوصول إلى عنصر غير موجود حال تنفيذه. فمثلا، إذا كان السكربت بداخل <head> فإنّ document.body غير متاح له، ﻷن المتصفّح لم يبلغ موضعه بعد حتى يقرأه. ففي المثال أدناه، يعرض لنا الـ alert اﻷوّل القيمة العدميّة null: <html> <head> <script> alert( "From HEAD: " + document.body ); // بعد <body> فليس هناك ،null </script> </head> <body> <script> alert( "From BODY: " + document.body ); // فهو الآن موجود ،HTMLBodyElement </script> </body> </html> في عالم DOM، تعني null أنّه "غير موجود" في DOM، تعني القيمة null أنّه "غير موجود" أو "لا وجود لهذه العقدة". اﻷبناء: lastChild ، firstChild ، childNodes هناك مصطلحان سنستعملهما من الآن فصاعد: العقد اﻷبناء (أو اﻷبناء باختصار) -- العناصر الذين هم أبناء مباشرون، أو بعبارة أخرى، هي العناصر المتفرعة عن العنصر المراد مباشرة. على سبيل المثال، العنصران <head> و <body> هما أبناء لعنصر <html>. العقد السليلة (descendants) -- جميع العناصر المتفرعة عن العنصر المراد، بما ذلك أبناؤه وأبناء أبنائه، إلى آخر ذلك. فمثلا هنا، للعنصر <body> اﻷبناء <div> و <ul> (وبعض العقد المتكوّنة من فراغات): <html> <body> <div>Begin</div> <ul> <li> <b>Information</b> </li> </ul> </body> </html> … وأمّا العقد السليلة لـ <body> فليست أبناءه المباشرين <div> و <ul> فحسب، بل تشمل كلّ العقد المتفرّعة منه، مثل <li> (الذي هو ابنً لـ <ul>) و <b> (الذي هو ابنٌ لـ <div>) -- الشجرة الفرعيّة بأكملها. تضمّ المجموعة childNodes كل العقد اﻷبناء، بما في ذلك العقد النصيّة. يقوم المثال أدناه بعرض أبناء document.body: <html> <body> <div>Begin</div> <ul> <li>Information</li> </ul> <div>End</div> <script> for (let i = 0; i < document.body.childNodes.length; i++) { alert( document.body.childNodes[i] ); // Text, DIV, Text, UL, ..., SCRIPT } </script> ...المزيد من اﻷشياء... </body> </html> See the Pen JS-p2-03-dom-navigation-ex01 by Hsoub (@Hsoub) on CodePen. يُرجى الانتباه ﻷمر دقيق هنا. لو أجرينا المثال السابق، سيكون <script> هو آخر العناصر التي تُعرض. في حقيقة اﻷمر، لاتزال هناك المزيد من اﻷشياء في المستند، لكن السكربت لم يرها، ﻷنّه حال تنفيذه، لم يكن المتصفّح قد اطّلع عليها بعد. تُمكّن الخاصّيات firstChild و lastChild الوصول بسهولة إلى أوّل اﻷبناء وآخرهم. هي فقط بمثابة اختصارات. إذا كان للعنصر elem عقدً أبناء، فإنّ العبارات التالية دائما صحيحة: elem.childNodes[0] === elem.firstChild elem.childNodes[elem.childNodes.length - 1] === elem.lastChild هناك أيضا دالّة خاصّة ()elem.hasChildNodes للتحقّق من أنّ له أبناءً. مجموعات DOM كما نلاحظ، تبدو childNode وكأنّها مصفوفة (array)، لكنّ الحقيقة أنّها ليست كذلك، بل هي باﻷحرى مجموعة (collection) -- كائن خاصّ شبيهً بالمصفوفة وقابل للتكرار عليه (iterable). ينجم عن هذا اﻷمر أثران مهمّان: يمكننا استعمال for..of للتكرار عليه: for (let node of document.body.childNodes) { alert(node); // يعرض هذا جميع العقد التي في المجموعة } See the Pen JS-p2-03-dom-navigation-ex02 by Hsoub (@Hsoub) on CodePen. هذا ﻷنّه قابل للتكرار (أي أنّه يتيح خاصّيّة Symbol.iterator كما يلزم). لا يمكننا استعمال توابع المصفوفة معها، ﻷنّها ليست مصفوفة: alert(document.body.childNodes.filter); // (filter غير معرّف (ليس هناك تابع اسمه الأمر اﻷوّل جيّد. كما أنّه لا بأس باﻷمر الثاني، ﻷنّه يمكننا استعمال Array.from ﻹنشاء مصفوفة "حقيقيّة" من المجموعة، إذا رغبنا في توابع المصفوفة: alert( Array.from(document.body.childNodes).filter ); // function See the Pen JS-p2-03-dom-navigation-ex03 by Hsoub (@Hsoub) on CodePen. مجموعات DOM هي للقراءة فقط مجموعات DOM، بل زد على ذلك، جميع خاصّيّات التنقّل التي ذكرناها في هذا المقال هي للقراءة فقط (read-only). فلا يمكن مثلا استبدال ابنٍ بشيء آخر بواسطة الإسناد ... = [childNodes[i. يتطلب تغيير DOM توابع أخرى سنتناولها في المقال التالي. مجموعات DOM حيّة كلّ مجموعات DOM تقريبا، مع بعض الاستثناءات القليلة، هي حيّة (live). ما يعني ذلك أنّها تعكس الوضع الحاليّ لـ DOM. إذا احتفظنا بمرجع للمجموعة elem.childNodes ، وأضفنا أو أزلنا عقدا من DOM، فإنّها تظهر في المجموعة تلقائيّا. لا تستعمل for..in للتكرار على المجموعات رغم أنّ المجموعات تقبل التكرار بواسطة for..in، لكنّه يُفضّل عدم استعمالها لذلك. تمرّ for..in على جميع الخاصّيّات التي يمكن تعدادها (enumerable)، وللمجموعات بعض الخاصّيّات "الإضافيّة" التي يندر استعمالها ولا نودّ أن نحصل عليها: <body> <script> // وأكثر ،values ،item ،length ،1 ،0 سيعرض هذا كلّا من for (let prop in document.body.childNodes) alert(prop); </script> </body> See the Pen JS-p2-03-dom-navigation-ex04 by Hsoub (@Hsoub) on CodePen. الإخوة واﻷب الإخوة (siblings) هم عقدٌ أبناء لنفس الأب. فمثلا هنا، <head> و <body> هما أخوين: <html> <head>...</head><body>...</body> </html> يُقال عن <body> أنّه اﻷخ "التالي" أو "اﻷيمن" لـ <head>. يُقال عن <head> أنّه اﻷخ "السابق" أو "الأيسر" لـ <body> . يكون اﻷخ التالي مُتاحا في الخاصّيّة nextSibling، والأخ السابق في الخاصّيّة previousSibling. بينما يكون اﻷب مُتاحا في الخاصّيّة parentNode. على سبيل المثال: // <html> هو <body> أب alert( document.body.parentNode === document.documentElement ); // صحيح // <body> يأتي<head> بعد alert( document.head.nextSibling ); // HTMLBodyElement // <head> يكون <body> قبل alert( document.body.previousSibling ); // HTMLHeadElement See the Pen JS-p2-03-dom-navigation-ex05 by Hsoub (@Hsoub) on CodePen. التنقّل بين العناصر فقط تربط خاصّيّات التنقّل المذكورة أعلاه بجميع أنواع العقد. فيمكننا مثلا من خلال childNodes الاطلاع على كلٍّ من العقد النصّيّة والعقد العنصريّة، بل وحتى العقد التعليقيّة إن وُجدت. لكنّنا في العديد من المهامّ لا نرغب في العقد النصّيّة والتعليقيّة. بل نرغب في معالجة العقد العنصريّة التي تمثّل الوسوم وتشكّل بنية الصفحة. لنرى إذًا بعض روابط التنقّل اﻷخرى التي تأخذ في الحسبان العقد النصيّة فقط: هذه الروابط مماثلة لتلك المذكورة أعلاه، مع إضافة كلمة Element في وسطها: children -- الأبناء الذين هم عقد عنصريّة فقط. firstElementChild و lastElementChild -- أوّل اﻷبناء وآخرهم من العقد العنصريّة. previousElementSibling و nextElementSibling -- العناصر المتجاورة. parentElement -- العنصر اﻷب. ما الداعي إلى parentElement ؟ أيمكن للأب ألّا يكون عنصرا؟ تُعيد خاصّيّة parentElement "العنصر" اﻷب، بينما تُعيد parentNode "أيّ عقدةٍ" أب. هاتان الخاصّيّتان متشابهتان في العادة: كلتاهما تُحصّل اﻷب. لكن هناك استثناءً وحيدا مع document.documentElement: alert( document.documentElement.parentNode ); // document alert( document.documentElement.parentElement ); // null See the Pen JS-p2-03-dom-navigation-ex06 by Hsoub (@Hsoub) on CodePen. سبب ذلك أنّ أب العقدة الجذر document.documentElement (التي تمثلّ <html>) هو document، و document ليس عقدة عنصريّة. فلهذا تعيده parentNode ولا تعيده parentElement. قد يفيد هذا التفصيل إذا ما أردنا التنقّل صعودا من عنصرٍ ما elem إلى <html>، لكن دون الوصول إلى document. while(elem = elem.parentElement) { // <html> اصعد إلى غاية alert( elem ); } لنُعدّل أحد اﻷمثلة التي في اﻷعلى. استبدل childNodes بـ children . ستظهر لك الآن العناصر فقط: <html> <body> <div>Begin</div> <ul> <li>Information</li> </ul> <div>End</div> <script> for (let elem of document.body.children) { alert(elem); // DIV, UL, DIV, SCRIPT } </script> ... </body> </html> See the Pen JS-p2-03-dom-navigation-ex07 by Hsoub (@Hsoub) on CodePen. المزيد من الروابط: الجدوال تناولنا إلى حدّ الآن خاصّيّات التنقّل اﻷساسيّة. قد تُتيح بعض أصناف العناصر في DOM خاصّيات إضافيّة تتميّز بها دون غيرها، لمزيدٍ من التسهيل. تُعدّ الجداول مثالا جيّدا لذلك، وتمثّل حالة خاصّة اﻷهميّة: يدعم عنصر <table> (إضافة إلى ما ذُكر في اﻷعلى) الخاصّيّات التالية: table.rows -- مجموعة عناصر <tr> التي في الجدول. table.caption/tHead/tFoot -- تشير إلى العناصر caption و thead و tfoot. table.tBodies -- مجموعة عناصر <tbody> (قد تكون هناك عدّةٌ منها كما ينصّ على ذلك المعيار، لكن سيكون هناك حتمًا واحد على اﻷقل -- حتى وإن لم يوجد في مصدر HTML، فسيضعه المتصفّح في DOM). تُتيح العناصر <thead> و <tfoot> و <tbody> خاصّيّة rows : tbody.rows -- مجموعة عناصر <tr> التي بداخله. <tr>: tr.cells -- مجموعة الخانات <td> و <th> التي بداخل <tr>. tr.sectionRowIndex -- موضع <tr> بداخل الـ <thead> أو <tbody> أو <tfoot> الذي يضمّه. tr.rowIndex -- رقم <tr> بالنسبة للجدول ككلّ (أي بين جميع أسطر الجدول). <td> و <th>: td.cellIndex -- رقم الخانة بداخل السطر الذي يضمّها. هذا مثال عن استعمالها: <table id="table"> <tr> <td>one</td><td>two</td> </tr> <tr> <td>three</td><td>four</td> </tr> </table> <script> // (السطر اﻷوّل، والعمود الثاني) "two" حدّد الخانة التي فيها let td = table.rows[0].cells[1]; td.style.backgroundColor = "red"; // لوّنها </script> See the Pen JS-p2-03-dom-navigation-ex08 by Hsoub (@Hsoub) on CodePen. للاطلاع على المواصفة: tabular data. هناك أيضا المزيد من خاصّيات التنقّل التي تتعلّق بنماذج HTML (أي forms). سنقف عندها لاحقا عندما نبدأ العمل بالنماذج. الملخص إنطلاقًا من أيّ عقدةٍ في DOM، يمكن الذهاب إلى العقد المجاورة لها مباشرة بواسطة خاصّيّات التنقّل. تنقسم هذه الخاصيّات إلى فئتين رئيسيّتن: لجميع العقد: parentNode childNodes firstChild lastChild previousSibling nextSibling للعقد العنصريّة فقط: parentElement children firstElementChild lastElementChild previousElementSibling nextElementSibling تُتيح بعض أصناف العقد في DOM كالجداول مثلا، خاصّيّات ومجموعات إضافيّة تمكّن من الوصول إلى محتواها. التمارين اﻷبناء في DOM اﻷهمية: 5 لاحظ هذه الصفحة: <html> <body> <div>Users:</div> <ul> <li>John</li> <li>Pete</li> </ul> </body> </html> اعط طريقة واحدة على اﻷقلّ للوصول إلى كلٍّ من عقد DOM التالية: العقدة <div>؟ العقدة <ul>؟ عقدة <li> الثانية (التي فيها Pete)؟ الحل هناك عدّة طرق، على سبيل المثال: العقدة <div>: document.body.firstElementChild // أو document.body.children[0] // أو -- لاحظ أن العقدة الأولى فراغ، لذا نأخذ الثانية document.body.childNodes[1] العقدة <ul>: document.body.lastElementChild // أو document.body.children[1] عقدة <li> الثانية (التي فيها Pete): // ومن ثمّ تحصيل آخر أبنائه العناصر <ul> تحصيل document.body.lastElementChild.lastElementChild سؤال عن الإخوة اﻷهميّة: 5 إذا كانت elem عقدةً عنصريّة من DOM … هل صحيح أنّ elem.lastChild.nextSibling دائمًا null؟ هل صحيح أنّ elem.children[0].previousSibling دائمًا null؟ الحل نعم، ذلك صحيح. يكون elem.lastChild دائما آخر اﻷبناء، فليس لديه nextSibling. لا ليس ذلك صحيحا، ﻷنّ [elem.children[0 هو أوّل ابنٍ بين العناصر، وقد تأتي قبله عقدٌ غير عنصريّة. فلا تكون بذلك previousSibling هي null بالضرورة، بل قد تكون عقدةً نصيّة مثلا. يرجى الانتباه إلى أنّه في كلتا الحالتين، إذا لم يكن هناك أبناء فسيُحدث ذلك خطأً. إذا لم يكن هناك أبناء، فإنّ elem.lastChild هو null فلا يمكننا الوصول إلى elem.lastChild.nextSibling، وتكون أيضا المجموعة elem.children فارغة (كمصفوفة فارغة [ ]). اختر الخانات القُطريّة اﻷهميّة: 5 اكتب الشفرة التي تُلوّن جميع الخانات التي على قُطر الجدول باﻷحمر. ستحتاج إلى تحصيل جميع الخانات <td> القطريّة من الجدول <table> وتلوينها باستخدام الشفرة التالية: // إلى خانة الجدول td تشير td.style.backgroundColor = 'red'; يجب أن تكون النتيجة كالتالي: افتح البيئة التجريبيّة لإنجاز التمرين الحل سنستخدم خاصّيّات rows و cells للوصول إلى الخانات القطريّة. شاهد الحلّ في البيئة التجريبيّة ترجمة وبتصرف لمقال Walking the DOM من كتاب The Modern JavaScript Tutorial
  13. تعدّ الوسوم (tags) أساس مستندات HTML. ويُمثَّل كلُّ وسمٍ منها في نموذج كائن المستند (DOM) بكائن. كما يُمثَّل النصّ الذي بداخل الوسم بكائن أيضا. وتعدّ الوسوم المتشعبة عن وسم آخر "أبناءً" لذلك الوسم. يمكننا الوصول لكلّ هذه الكائنات من خلال جافاسكربت، ونستطيع بواسطتها التعديل على الصفحة. فعلى سبيل المثال، يمثّل الكائن document.body وسم <body>، وبتنفيذ الشفرة التالية يصير لون <body> أحمر لمدّة 3 ثوانٍ: document.body.style.background = 'red'; // جعل الخلفية حمراء اللون setTimeout(() => document.body.style.background = '', 3000); // ارجاعها كما كانت See the Pen JS-p2-02-dom-nodes-ex01 by Hsoub (@Hsoub) on CodePen. استعملنا هنا خاصّيّة style.background لتغيير لون خلفيّة document.body، وهناك العديد من الخاصّيّات اﻷخرى مثل: innerHTML -- محتوى HTML الذي بداخل العقدة. offsetWidth -- مقدار عرض العقدة (بالبكسل). … إلى غير ذلك. سنتعلم قريبا المزيد من الطرق لمعالجة DOM، لكن نحتاج أوّلا إلى التعرّف على بنيته. مثال عن DOM لنبدأ بالمستند البسيط التالي: <!DOCTYPE HTML> <html> <head> <title>About elk</title> </head> <body> The truth about elk. </body> </html> See the Pen JS-p2-02-dom-nodes-ex02 by Hsoub (@Hsoub) on CodePen. يمثِّل DOM مستند HTML بواسطة بنية شجريّة من الوسوم، كما في الصورة التالية: كل عقدة من هذه الشجرة هي كائن. تُمثَّل الوسوم بعقدٍ عنصريّة element nodes (أو عناصر elements باختصار) وتشكّل بنية الشّجرة. فعند الجذر توجد<html> ، و<head> و <body> هما أبناؤها، إلى آخر ذلك. يُمثَّل النّص الذي بداخل العناصر بعقدٍ نصّيّة وهي معلّمة في الصّورة بـ text#. لا تحتوي العقد النّصية إلا على سلاسل نصيّة (string)، ولا يمكن أن يكون لها أبناء، وتكون دائما بمثابة ورقةٍ للشجرة. على سبيل المثال، يحوي وسم <title> على النصّ "About elk". لاحظ وجود هذه المحارف الخاصّة (special characters) في العقد النصّيّة: السطر الجديد: ↵ (يُعرف في جافاسكربت بـ n\). الفراغ: ␣. يعدّ السطر الجديد والفراغ محارف صحيحة، تماما كالحروف واﻷرقام، وتُكوّن بدورها عقدا نصّيّة وتصير جزءًا من DOM. ففي المثال أعلاه، يحوي وسم <head> بعض الفراغات قبل <title>، ويكوّن ذلك النّص عقدة نصّيّة (تحوي سطرا جديدا وبعض الفراغات فقط). يُستثنى من ذلك شيئان: تُهمل الفراغات واﻷسطر الجديدة التي قبل <head> لأسباب تاريخيّة. إذا وضعنا شيئا ما بعد <body/>، فإنه يُنقل تلقائيّا إلى آخر body، ﻷنّ مواصفة HTML تشترط أن يكون جميع المحتوى موجودا داخل <body>. لذا فلا يمكن أن تكون هناك أيّة فراغات بعد <body/>. في غير ذلك من الحالات، فاﻷمر واضح؛ إذا كانت هناك فراغات في المستند فإنّها تصير عقدا نصّيّة (كغيرها من المحرّفات) في DOM، وإن أزلناها فلن تكون هناك أيّ منها. هذا مثال لعقد نصيّة لا تحوي أيّة فراغات: <!DOCTYPE HTML> <html><head><title>About elk</title></head><body>The truth about elk.</body></html> لا تُظهر أدوات المتصفّح (التي سنتطرق لها لاحقا) عند تعاملها مع DOM عادةً الفراغات التي في أوّل النّصّ وآخره، ولا العقد النصّيّة الفارغة (كالتي تنجم عن إنهاء السطر) بين الوسوم. توفّر أدوات المتصفّح بذلك المساحة على الشاشة. قد نحذفها أيضا في ما يأتي صورٍ لـ DOM إذا لم يكن في إظهارها فائدة. لا تؤثر هذه الفراغات عادة على كيفيّة عرض المستند. التصحيح التلقائي إذا صادف المتصفّح مستند HTML خاطئ التنسيق، فإنّه يصحّحه تلقائيّا عند بناء DOM. على سبيل المثال، يكون <html> دائما هو الوسم اﻷعلى. حتى وإن لم يوجد في المستند، فإنّه سُيوجد في DOM، لأن المتصفّح سيُنشئه. وكذلك الشأن مع <body>. فإذا كان ملف HTML عبارةً عن كلمة واحدة "Hello"، فإنّ المتصفّح سيلفُّه وسط <html> و <body> ويضيف لهما <head> كما يلزم، ويصير بذلك DOM: عند توليد DOM، تعالج المتصفّحات تلقائيّا الأخطاء التي في المستند، كعدم إغلاق الوسوم وغير ذلك. فإذا كان في المستند وسوم لم تُغلق: <p>Hello <li>Mom <li>and <li>Dad فإنّ المتصفّح يستعيد الأجزاء المفقودة عندما يطّلع على الوسوم: للجداول دائما <tbody> تعدّ الجداول "حالة خاصّة" مثيرة للاهتمام. فوفقًا لمواصفة DOM يجب أن يكون للجداول العنصر <tbody>، لكن يُسمح (رسميًّا) لنصوص HTML أن تسقطه. فينشئ المتصفح حينها <tbody> في DOM تلقائيّا. فتؤدّي مثلا شفرة HTML هذه: <table id="table"><tr><td>1</td></tr></table> إلى بنية DOM التالية: أرأيت؟ ظهر <tbody> من لاشيء. ينبغي الانتباه لهذا عند التعامل مع الجداول لتفادي المفاجآت. أنواع أخرى من العقد هناك أنواع أخرى من العقد ما عدا العقد العنصريّة والنصيّة. فهناك مثلا التعليقات. <!DOCTYPE HTML> <html> <body> The truth about elk. <ol> <li>An elk is a smart</li> <!-- comment --> <li>...and cunning animal!</li> </ol> </body> </html> يمكننا في الصورة أعلاه ملاحظة نوع جديد من العقد -- العقد التعليقيّة ، معلّمة بـ comment# ، بين عقدتين نصّيّتين. لكن قد نتساءل -- لماذا تضاف التعليقات إلى DOM؟ فلا تأثير لها على التمثيل البصريّ بأيّ شكل من اﻷشكال. هذا صحيح، لكن هناك قاعدة تقول: كلُّ ما وُجد في HTML، فلا بدّ أن يوجد أيضا في شجرة DOM. كلّ ما يحويه HTML، حتّى التعليقات، يصير جزءًا من DOM. حتّى تعليمة <...DOCTYPE!> التي في أوّل HTML هي أيضا عقدة من DOM. هي موجودة في شجرة DOM قبل <html> مباشرة. لن نمسّ تلك العقدة، ولن نرسمها حتى في المخططات، لكنّها موجودة. بل إنّ كائن document الذي يمثل المستند بأكمله هو، اصطلاحًا، عقدة من DOM أيضا. هناك 12 نوعا من العقد، لكن نتعامل عادةً مع أربعة منها فقط: document -- "نقطة الدخول" إلى DOM. العقد العنصريّة -- وسوم HTML التي تمثل لبِنات الشجرة اﻷساسيّة. العقد النصّيّة -- تحتوي على نصّ فقط. التعليقات -- نضع فيها بعض المعلومات أحيانا. هي لا تُعرض، ولكن يستطيع جافاسكربت قراءتها من خلال DOM. عاينه بنفسك لمعاينة البنية الآنيّة لـ DOM، جرّب Live DOM Viewer. عليك فقط أن تدخل المستند، وسيظهر لك على شكل DOM حالًا. يمكن أيضا تفحّص DOM باستخدام أدوات المطوّر (developer tools) في المتصفّح، وهي في الحقيقة ما نستعمله عند التطوير. للقيام بذلك، افتح صفحة elk.html، ثمّ فعّل أدوات المطوّر في المتصفّح بالضغط على F12، وانتقل إلى لسان Elements (بمعنى عناصر). يُفترض أن تبدو كما في الصورة: يمكنك مشاهدة DOM، والنقر على على عناصره لاستعراض تفاصيلها وما إلى ذلك. يُرجى التنبّه إلى أن بنية DOM مبسّطة في أدوات المطوّر. فتظهر العقد النصّيّة كمجرّد نصّ، وليست هناك عقد نصّيّة فارغة (أي تتكوّن من فراغات فقط) إطلاقًا. لكن لا بأس بذلك، إذ ما نهتمّ به في الغالب هي العقد العنصريّة. بالنقر على الزرالذي على شكل مؤشر في أقصى الزاوية اليسرى من اﻷعلى يمكننا اختيار عقدة من الصفحة باستخدام الفأرة (أو أيٍّ من أجهزة التأشير اﻷخرى) و "فحصها" (بالانتقال إلى موضعها في لسان العناصر). يفيد هذا كثيرا عندما تكون صفحة HTML (وما يقابلها من DOM) التي لدينا كبيرة جدا، ونود أن نرى مكان عنصر معيّن منها. كما يمكن أيضا فعل ذلك عن طريق النقر بالزر اﻷيمن على صفحة الويب ومن ثمّ اختيار "Inspect" (أي تفحّص) من القائمة المنبثقة. توجد في الجزء اﻷيمن من اﻷدوات اﻷلسنة الفرعية التالية: Styles (بمعنى الأنماط) -- تمكّننا من رؤية قواعد CSS المطبّقة على العنصر الحاليّ قاعدةً بقاعدة، بما في ذلك القواعد المضمّنة (باللون الرمادي). يمكن تقريبًا تعديل كلّ شيء من مكانه، بما في ذلك أبعاد (dimensions) وهوامش (margins) وحشوات (paddings) الصندوق (box) من أسفل اللسان. Computed (بمعنى المحسوبة) -- لرؤية تنسيقات CSS المطبّقة على العنصر مرتّبةً حسب الخاصّيّة (property): كلّ خاصّيّة تقابلها القاعدة التي منحتها (بما في ذلك التنسيقات المورّثة وما إلى ذلك). Event Listeners (بمعنى مستمعي اﻷحداث) -- لرؤية مستمعي اﻷحداث المرفقة مع عناصر DOM (سنتناول ذلك في مقالات لاحقة). أفضل طريقة لدراسة كلّ هذه اﻷقسام هي بالنقر هنا وهناك. معظم القيم قابلة للتعديل من مكانها. التعامل مع الطرفية (console) عند تعاملنا مع DOM قد نودّ تطبيق جافاسكربت عليه، كأن نأخذ إحدى العقد، ونجري عليها شِفرة ما لمعرفة النتيجة. هذه بعض الطرق لكيفيّة التنقل بين لسان العناصر والطرفيّة. بدايةً: حدّد أوّل <li> في لسان العناصر. اضغط زر Esc -- سيفتح ذلك الطرفيّة تحت لسان العناصر مباشرة. يكون آخر العناصر التي حدّدناها محفوظا في المتغيّر 0$ ، والذي حدّدناه قبل ذلك في 1$ وهكذا. يمكننا تنفيذ أوامر عليها. فمثلا، يغيّر اﻷمر ‎‎$0.style.background = 'red'‎ لون عنصر القائمة المحدَّد إلى اﻷحمر كما في الصّورة: بهذه الطريقة تُنقل العقد من لسان العناصر إلى الطرفية. يمكن أيضا فعل العكس. فإذا كان لدينا متغيّر يشير إلى عقدة ما، يمكننا استخدام اﻷمر (inspect(node (حيث node هو اسم المتغيّر) في الطرفيّة لرؤيتها في لسان العناصر. أو يمكننا فقط طباعة عقدة DOM في الطرفية وتفحصّها "من مكانها"، كما هو الحال مع document.body في اﻷسفل: لا تُستخدم هذه الطرق إلا عند تنقيح اﻷخطاء بالطبع. ابتداءً من المقال الموالي، سنستخدم جافاسكربت للوصول إلى DOM والتعديل عليه. تساعد أدوات المطوّر في المتصفّح كثيرا أثناء التطوير، إذ يمكن من خلالها تفحّص DOM، وتجريب أشياء مختلفة لمعرفة ما قد يحدث من أخطاء. خلاصة يُمثًّل مستند HTML/XML في المتصفّح بواسطة شجرة DOM. تصير الوسوم عقدًا عنصريّة وتشكّل بنية الشجرة. تصير النصوص عقدًا نصّيّة. … إلى غير ذلك، فلِكلّ ما يوجد في HTML مكانه في DOM، حتّى التعليقات. يمكننا استخدام أدوات المطوّر لتفحّص DOM وتعديله يدويّا. اقتصرنا هنا كبدايةٍ على اﻷساسيّات من أهمّ العمليّات وأكثرها استخداما. هناك توثيق شامل ﻷدوات المطوّر في متصفّح Chrome على https://developers.google.com/web/tools/chrome-devtools. أفضل طريقة لتعلم هذه اﻷدوات هو النقر هنا وهناك، وقراءة ما في القوائم، فمعظم الخيارات واضحة. لاحقا، عندما تتقن استخدامها عموما، طالع التوثيق والتقط الباقي. لعُقد DOM خاصّيّات وتوابع تمكنّنا من التنقّل بينها والتعديل عليها والتجوّل في الصفحة وغير ذلك. هذا ما سنتناوله في المقال التالي. ترجمة وبتصرف لمقال DOM tree من كتاب The Modern JavaScript Tutorial
  14. كانت لغة جافاسكربت عند إنشائها موجّهةً للعمل على متصفّحات الويب، لكنّها تطوّرت مع مرور الوقت وصارت لغة متعدّدة الاستخدامات والمنصّات. قد تكون المنصّة التي تستضيف جافاسكربت متصفّحا أو خادم ويب أو حتى آلة صنع القهوة، وتوفّر كلّ منها وظائف خاصّة بها. تسمّى هذه المنصّة وفقًا لمواصفة لغة جافاسكربت بالبيئة المضيفة (host environment). توفّر البيئة المضيفة كائنات ودوالَّ خاصّة بها زيادةً على ما يوجد في اللغة نفسها. فيٌتيح المتصفّح مثلا وسائل للتحكّم في صفحات الويب، وتقدّم Node.js مزايا لإنشاء تطبيقات تعمل في جانب الخادم، إلى غير ذلك. هذه نظرة عامّة عمّا يكون لدينا عندما تعمل جافاسكربت على متصفّح ويب: في أعلى الصّورة كائن "جذر" اسمه window (بمعنى نافذة)، وله دوران: اﻷوّل أنّه كائن عامّ، كما عٌرّف ذلك في مقال الكائن العامّ. و الثاني أنّه يمثّل "نافذة المتصفّح" ويوفّر توابع للتحكّم فيها. هذا مثال عن استعماله ككائن عامّ: function sayHi() { alert("Hello"); } // الدوالّ العامّة هي توابع للكائن العامّ window.sayHi(); See the Pen JS-P2-01-browser-environment-ex01 by Hsoub (@Hsoub) on CodePen. وهذا مثال عن استعماله كنافذة متصفّح، لمعرفة مقدار ارتفاع النافذة: alert(window.innerHeight); // ارتفاع النافذة الداخلي See the Pen JS-P2-01-browser-environment-ex02 by Hsoub (@Hsoub) on CodePen. هناك المزيد من الخاصّيّات والتوابع المتعلّقة بكائن window، سنستعرضها لاحقًا. نموذج كائن المستند (DOM) يمثّل نموذج كائن المستند (Document Object Model أو DOM باختصار) جميع ما تحتويه الصفحة بواسطة كائنات قابلة للتعديل. يٌعدّ كائن document (أو المستند) "نقطة الدخول" الرئيسية للصفحة، إذ يمكن من خلاله تغيير أو إنشاء أي شيء عليها. على سبيل المثال: // جعل الخلفية حمراء اللون document.body.style.background = "red"; // إرجاعها كما كانت بعد ثانية واحدة setTimeout(() => document.body.style.background = "", 1000); See the Pen JS-p2-01-browser-environment-ex03 by Hsoub (@Hsoub) on CodePen. استعملنا هنا خاصيّة document.body.style، غير أنّ هناك المزيد والمزيد من الخاصّيّات والتوابع التي يمكن الاطلاع على جميعها في مواصفة DOM: DOM Living Standard على https://dom.spec.whatwg.org/ لا يختص DOM بالمتصفحات تبيّن مواصفة DOM بنية المستند وتوفّر كائنات لمعالجته. ويجدر بالذكر أنه يمكن ﻷدواتٍ أخرى غير المتصفّح أن تستخدم DOM. فعلى سبيل المثال، يمكن لسكربتات جانب الخادم التي تحمّل صفحات HTML وتعالجها أن تستخدم DOM أيضًا، غير أنّها قد لا تدعم إلاّ جزءًا من المواصفة فقط. CSSOM هو للتنسيق تٌنظَّم قواعد CSS وصفحات اﻷنماط (stylesheets) تنظيمًا مختلفًا عن تنظيم HTML. لذا فإن لها مواصفة منفصلة، تُسمّى نموذج كائن CSS (أو CSSOM اختصارًا لـ CSS Object Model)، تبيّن كيفيّة تمثيلها بكائنات وكذا كيفيّة كتابتها وقرائتها. يستعمل CSSOM و DOM معًا لتغيير قواعد تنسيق المستند، غير أنّه عمليًّا، قلّ ما يُحتاج إلى CSSOM بحكم أنّ قواعد التنسيق ساكنة في العادة. فبالرغم من أنّه يندر أن يُحتاج لإضافة أو إزالة قواعد CSS من خلال جافاسكربت إلا أنّ ذلك ممكن. نموذج كائن المتصفح (BOM) يمثّل نموذج كائن المتصفّح (Browser Object Model أو BOM باختصار) كائنات أخرى يوفّرها المتصفّح (البيئة المضيفة) للتعامل مع كلّ ما عدا المستند. على سبيل المثال: يعطي كائن navigator (بمعنى ملّاح) معلومات أساسيّة عن المتصفّح ونظام التشغيل، وله عدّة خاصّيات لكنّ أشهرها: navigator.userAgent المتعلّقة بالمتصفّح المستخدَم، و navigator.platform المتعلّقة بنظام التشغيل (قد تساعد في التمييز بين ويندوز ولينكس وماك وغيرها). يمكّن كائن location (بمعنى موقع) من معرفة الرابط (URL) الحاليّ، كما يُمكنه أيضا إعادة توجيه المتصفّح إلى رابط آخر. يمكن مثلا استعمال كائن location كالتالي: alert(location.href); // عرض الرابط الحالي if (confirm("هل تودّ زيارة ويكيبيديا؟")) { location.href = "https://wikipedia.org"; // إعادة توجيه المتصفّح لرابط آخر } See the Pen JS-p2-01-browser-environment-ex04 by Hsoub (@Hsoub) on CodePen. تُعدّ كلٌّ من دوالّ alert و confirm و prompt كذلك جزءا من BOM، فهي لا تتعلّق بالمستند تعلّقًا مباشرًا، بل تمثّل توابع متصفّح خالصة للتواصل مع المستخدم. عن المواصفات يعدّ BOM جزءا من مواصفة HTML العامّة. نعم، لم تتوهّم سماع ذلك. لا تقتصر مواصفة HTML على "لغة HTML" (من وسوم وسمات)، بل تشمل كذلك عدّة كائنات و توابع وامتدادات DOM خاصّة بالمتصفّح، وهو " HTML بمعناه العامّ". كما أنّ لبعض اﻷجزاء الأخرى مواصفات مستقلّة كذلك وهي مدرجة هنا https://spec.whatwg.org. الخلاصة بالحديث عن المعايير، يمكن الخلوص إلى ما يلي: مواصفة DOM: تبيّن بنية المستند، وطرق معالجته، وما يتعلّق بالأحداث (events). للاطلاع عليها https://dom.spec.whatwg.org/. مواصفة CSSOM: تبيّن صفحات اﻷنماط وقواعد التنسيق، و كيفيّة استخدامها للمعالجة، وكذا ارتباطها بالمستند. للاطلاع عليها https://www.w3.org/TR/cssom-1/. مواصفة HTML: تبيّن لغة HTML (كالوسوم مثلا)، كما تبيّن نموذج كائن المتصفّح BOM وما يوفّره من مختلف الدوالّ مثل setTimeout و alert و location وغيرها. تعدّ هذه المواصفة توسعة لمواصفة DOM بمزيد من الخاصيّات والتوابع. للاطلاع عليها https://html.spec.whatwg.org. إضافة لما سبق، بعض اﻷصناف لها مواصفاتً مستقلّة تُبيّنها. للاطلاع عليها https://spec.whatwg.org. يُرجى الاعتناء بهذه الروابط، إذ هناك الكثير من اﻷمور لتعلمّها، ويستحيل الوقوف عند جميعها فضلا عن حفظها. لدراسة خاصّية أو تابع ما، يعدّ دليل موزيلا https://developer.mozilla.org/ar/docs/Learn من أحسن المصادر لذلك، لكن قراءة المواصفة أفضل رغم تعقيدها وطولها، إذ من شأنها أن تورث معرفة تامّة وسليمة. لإيجاد شيء ما، يُستحسن البحث في الانترنت على النّحو التالي: "[مصطلح البحث] WHATWG" أو "[مصطلح البحث] MDN" ،على سبيل المثال: https://google.com?q=whatwg+localstorage ،https://google.com?q=mdn+localstorage. في ما يلي، سنشرع في تعلّم DOM، فللصفحة (أي document) دور محوري في واجهة المستخدم. ترجمة -وبتصرف- للفصل Browser environment, specs من كتاب Browser: Document, Events, Interfaces