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

التنقل في شجرة DOM عبر جافاسكربت


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

يُمكّننا DOM من فعل أيّ شيء بالعناصر وما تحتويه، لكن نحتاج أوّلا إلى أن نصل إلى الكائن المحدّد من DOM.

تبدأ جميع العمليّات على DOM بالكائن document. فهو "نقطة الدخول" الرئيسيّة إلى DOM، ويمكن من خلاله الوصول إلى جميع العقد.

تمثّل الصورة التالية الروابط التي يمكن من خلالها التنقّل بين العقد في DOM.

dom-links.png

لنتناولها بمزيدٍ من التفصيل.

في اﻷعلى: 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>

يُرجى الانتباه ﻷمر دقيق هنا. لو أجرينا المثال السابق، سيكون <script> هو آخر العناصر التي تُعرض. في حقيقة اﻷمر، لاتزال هناك المزيد من اﻷشياء في المستند، لكن السكربت لم يرها، ﻷنّه حال تنفيذه، لم يكن المتصفّح قد اطّلع عليها بعد.

تُمكّن الخاصّيات firstChild و lastChild الوصول بسهولة إلى أوّل اﻷبناء وآخرهم.

هي فقط بمثابة اختصارات. إذا كان للعنصر elem عقدً أبناء، فإنّ العبارات التالية دائما صحيحة:

elem.childNodes[0] === elem.firstChild
elem.childNodes[elem.childNodes.length - 1] === elem.lastChild

هناك أيضا دالّة خاصّة ()elem.hasChildNodes للتحقّق من أنّ له أبناءً.

مجموعات DOM

كما نلاحظ، تبدو childNode وكأنّها مصفوفة (array)، لكنّ الحقيقة أنّها ليست كذلك، بل هي باﻷحرى مجموعة (collection) -- كائن خاصّ شبيهً بالمصفوفة وقابل للتكرار عليه (iterable).

ينجم عن هذا اﻷمر أثران مهمّان:

  1. يمكننا استعمال for..of للتكرار عليه:
     for (let node of document.body.childNodes) {
       alert(node); //  يعرض هذا جميع العقد التي في المجموعة
     }

هذا ﻷنّه قابل للتكرار (أي أنّه يتيح خاصّيّة Symbol.iterator كما يلزم).

  1. لا يمكننا استعمال توابع المصفوفة معها، ﻷنّها ليست مصفوفة:
   alert(document.body.childNodes.filter); //  (filter غير معرّف (ليس هناك تابع اسمه  

الأمر اﻷوّل جيّد. كما أنّه لا بأس باﻷمر الثاني، ﻷنّه يمكننا استعمال Array.from ﻹنشاء مصفوفة "حقيقيّة" من المجموعة، إذا رغبنا في توابع المصفوفة:

alert( Array.from(document.body.childNodes).filter ); // function


مجموعات 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>


الإخوة واﻷب

الإخوة (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

التنقّل بين العناصر فقط

تربط خاصّيّات التنقّل المذكورة أعلاه بجميع أنواع العقد. فيمكننا مثلا من خلال childNodes الاطلاع على كلٍّ من العقد النصّيّة والعقد العنصريّة، بل وحتى العقد التعليقيّة إن وُجدت.

لكنّنا في العديد من المهامّ لا نرغب في العقد النصّيّة والتعليقيّة. بل نرغب في معالجة العقد العنصريّة التي تمثّل الوسوم وتشكّل بنية الصفحة.

لنرى إذًا بعض روابط التنقّل اﻷخرى التي تأخذ في الحسبان العقد النصيّة فقط:

dom-links-elements.png

هذه الروابط مماثلة لتلك المذكورة أعلاه، مع إضافة كلمة Element في وسطها:

  • children -- الأبناء الذين هم عقد عنصريّة فقط.
  • firstElementChild و lastElementChild -- أوّل اﻷبناء وآخرهم من العقد العنصريّة.
  • previousElementSibling و nextElementSibling -- العناصر المتجاورة.
  • parentElement -- العنصر اﻷب.

ما الداعي إلى parentElement ؟ أيمكن للأب ألّا يكون عنصرا؟

تُعيد خاصّيّة parentElement "العنصر" اﻷب، بينما تُعيد parentNode "أيّ عقدةٍ" أب. هاتان الخاصّيّتان متشابهتان في العادة: كلتاهما تُحصّل اﻷب.

لكن هناك استثناءً وحيدا مع document.documentElement:

alert( document.documentElement.parentNode ); // document
alert( document.documentElement.parentElement ); // null

سبب ذلك أنّ أب العقدة الجذر 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>

المزيد من الروابط: الجدوال

تناولنا إلى حدّ الآن خاصّيّات التنقّل اﻷساسيّة.

قد تُتيح بعض أصناف العناصر في 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>

للاطلاع على المواصفة: 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؟

الحل

  1. نعم، ذلك صحيح. يكون elem.lastChild دائما آخر اﻷبناء، فليس لديه nextSibling.
  2. لا ليس ذلك صحيحا، ﻷنّ [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


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...