التنقل في DOM: الاباء والابناء والاشقاء
ما هو التنقل في DOM؟
التنقل في DOM هو عملية الانتقال بين العقد في شجرة نموذج كائن المستند. كل مستند HTML يُمثَّل كشجرة من العقد، وفهم كيفية التحرك بين تلك العقد امر اساسي لكتابة جافاسكريبت فعال. بدلا من استخدام querySelector او getElementById دائما للبحث عن العناصر من الصفر، يتيح لك التنقل البدء من عقدة واحدة والمشي الى العقد المرتبطة بها -- الاباء والابناء والاشقاء. هذا اسرع واكثر كفاءة وغالبا ضروري عند العمل مع محتوى يُنشا ديناميكيا حيث قد لا تكون المعرفات والفئات معروفة مسبقا.
فكر في DOM مثل شجرة العائلة. كل عنصر له اب (العنصر الذي يحتويه)، وقد يكون له ابناء (عناصر متداخلة بداخله)، وقد يكون له اشقاء (عناصر في نفس المستوى). اتقان التنقل يعني انك تستطيع الوصول الى اي عقدة في المستند بدءا من اي عقدة اخرى، مما يمنحك تحكما كاملا في معالجة الصفحة.
التنقل الى الاب: parentNode مقابل parentElement
عندما تحتاج للصعود في شجرة DOM من عقدة معينة، لديك خاصيتان: parentNode وparentElement. قد تبدوان متطابقتين لكن هناك فرق مهم.
parentNode تُرجع اب اي نوع من العقد، بما في ذلك العقد النصية وعقد التعليقات وعقد المستند. parentElement تُرجع الاب فقط اذا كان ذلك الاب عقدة عنصر. في معظم حالات الاستخدام اليومية، كلاهما يُرجع نفس النتيجة لان العناصر عادة تكون متداخلة داخل عناصر اخرى. الفرق يهم فقط في اعلى الشجرة: اب عنصر <html> هو عقدة المستند، وهي ليست عقدة عنصر. لذا document.documentElement.parentNode تُرجع المستند، لكن document.documentElement.parentElement تُرجع null.
مثال: parentNode مقابل parentElement
<div id="container">
<p id="message">مرحبا بالعالم</p>
</div>
<script>
const message = document.getElementById('message');
// كلاهما يُرجع div#container
console.log(message.parentNode); // <div id="container">
console.log(message.parentElement); // <div id="container">
// في اعلى الشجرة يختلفان
const html = document.documentElement;
console.log(html.parentNode); // #document
console.log(html.parentElement); // null
</script>
parentElement هو الخيار الاكثر امانا لانك غالبا تريد العمل مع عقد العناصر. استخدم parentNode فقط عندما تحتاج تحديدا للتعامل مع عقد غير عنصرية مثل عقدة المستند نفسها.الصعود عدة مستويات
يمكنك تسلسل خصائص الاب للصعود عدة مستويات في شجرة DOM. هذا مفيد عندما تحتاج للوصول الى جد او سلف اعلى من عنصر متداخل بعمق.
مثال: الصعود عدة مستويات
<section id="main">
<div class="card">
<p class="card-text">
<span id="highlight">مهم</span>
</p>
</div>
</section>
<script>
const highlight = document.getElementById('highlight');
// صعود مستوى واحد: <p class="card-text">
console.log(highlight.parentElement);
// صعود مستويين: <div class="card">
console.log(highlight.parentElement.parentElement);
// صعود ثلاثة مستويات: <section id="main">
console.log(highlight.parentElement.parentElement.parentElement);
</script>
.parentElement يجعل الكود هشا. اذا تغيرت بنية HTML واُضيف او اُزيل غلاف، تنكسر سلسلتك. استخدم closest() بدلا من ذلك للبحث الموثوق عن الاسلاف -- سنغطي ذلك لاحقا في هذا الدرس.التنقل الى الابناء: childNodes مقابل children
عندما تريد النزول في الشجرة والوصول الى العقد المتداخلة داخل عنصر، لديك خاصيتان: childNodes وchildren. فهم الفرق بينهما امر بالغ الاهمية لانهما يُرجعان مجموعات مختلفة جدا.
childNodes تُرجع NodeList حية تحتوي على كل عقدة ابن، بما في ذلك العقد النصية (المسافات البيضاء وفواصل الاسطر) وعقد التعليقات وعقد العناصر. children تُرجع HTMLCollection حية تحتوي فقط على عقد العناصر الابناء. في معظم الحالات، تريد children لان العقد النصية للمسافات البيضاء نادرا ما تكون مفيدة.
مثال: childNodes مقابل children
<ul id="menu">
<li>الرئيسية</li>
<li>من نحن</li>
<li>اتصل بنا</li>
</ul>
<script>
const menu = document.getElementById('menu');
// childNodes تشمل العقد النصية (المسافات البيضاء بين الوسوم)
console.log(menu.childNodes); // NodeList(7) [text, li, text, li, text, li, text]
console.log(menu.childNodes.length); // 7
// children تشمل فقط عقد العناصر
console.log(menu.children); // HTMLCollection(3) [li, li, li]
console.log(menu.children.length); // 3
</script>
childNodes تُرجع 7 عقد لـ 3 عناصر قائمة -- هناك 4 عقد نصية تحتوي على مسافات بيضاء (قبل اول li، وبين كل li، وبعد اخر li). هذا احد اكثر مصادر الارتباك شيوعا عند العمل مع DOM.الابن الاول والابن الاخير
للوصول السريع الى اول او اخر ابن لعنصر، لديك زوجان من الخصائص. خصائص firstChild وlastChild تشمل جميع انواع العقد. خصائص firstElementChild وlastElementChild تشمل فقط عقد العناصر.
مثال: خصائص الابن الاول والاخير
<div id="wrapper">
<h2>العنوان</h2>
<p>الفقرة الاولى</p>
<p>الفقرة الثانية</p>
</div>
<script>
const wrapper = document.getElementById('wrapper');
// firstChild هو عقدة نصية (مسافة بيضاء قبل <h2>)
console.log(wrapper.firstChild); // #text
console.log(wrapper.firstChild.nodeType); // 3 (عقدة نصية)
// firstElementChild هو <h2>
console.log(wrapper.firstElementChild); // <h2>العنوان</h2>
console.log(wrapper.firstElementChild.nodeType); // 1 (عقدة عنصر)
// lastChild هو عقدة نصية (مسافة بيضاء بعد اخر <p>)
console.log(wrapper.lastChild); // #text
// lastElementChild هو <p> الثاني
console.log(wrapper.lastElementChild); // <p>الفقرة الثانية</p>
</script>
firstElementChild، lastElementChild) ما لم يكن لديك سبب محدد للعمل مع العقد النصية او عقد التعليقات. هذا يمنع السلوك غير المتوقع الناتج عن العقد النصية للمسافات البيضاء.التنقل بين الاشقاء: التحرك جانبيا في DOM
الاشقاء هم عقد تشترك في نفس الاب. مثل خصائص الاباء والابناء، تاتي خصائص الاشقاء في نوعين: تلك التي تشمل جميع انواع العقد وتلك التي تشمل فقط عقد العناصر.
nextSibling وpreviousSibling تُرجعان العقدة التالية او السابقة من اي نوع. nextElementSibling وpreviousElementSibling تُرجعان فقط عقدة العنصر التالي او السابق. مرة اخرى، نسخ العناصر فقط هي ما تريده عادة.
مثال: التنقل بين الاشقاء
<ul id="nav">
<li id="home">الرئيسية</li>
<li id="about">من نحن</li>
<li id="services">الخدمات</li>
<li id="contact">اتصل بنا</li>
</ul>
<script>
const about = document.getElementById('about');
// nextSibling هو عقدة نصية (مسافة بيضاء)
console.log(about.nextSibling); // #text
// nextElementSibling هو <li id="services">
console.log(about.nextElementSibling); // <li id="services">
// previousSibling هو عقدة نصية (مسافة بيضاء)
console.log(about.previousSibling); // #text
// previousElementSibling هو <li id="home">
console.log(about.previousElementSibling); // <li id="home">
// العنصر الاول ليس له شقيق سابق
const home = document.getElementById('home');
console.log(home.previousElementSibling); // null
// العنصر الاخير ليس له شقيق تالي
const contact = document.getElementById('contact');
console.log(contact.nextElementSibling); // null
</script>
دالة closest()
دالة closest() هي واحدة من اقوى ادوات التنقل المتاحة. تصعد في شجرة DOM من العنصر الحالي وتُرجع اول سلف (او العنصر نفسه) يطابق محدد CSS معين. اذا لم يُعثر على سلف مطابق، تُرجع null. هذا اكثر موثوقية بكثير من تسلسل parentElement لانه لا يعتمد على العدد الدقيق للمستويات بين العناصر.
مثال: استخدام closest()
<div class="card" data-id="42">
<div class="card-body">
<h3 class="card-title">اسم المنتج</h3>
<p class="card-text">الوصف هنا</p>
<button class="btn-delete">حذف</button>
</div>
</div>
<script>
const deleteBtn = document.querySelector('.btn-delete');
// البحث عن اقرب سلف بالفئة "card"
const card = deleteBtn.closest('.card');
console.log(card); // <div class="card" data-id="42">
console.log(card.dataset.id); // "42"
// البحث عن اقرب سلف بالفئة "card-body"
const body = deleteBtn.closest('.card-body');
console.log(body); // <div class="card-body">
// عدم وجود تطابق يُرجع null
const form = deleteBtn.closest('form');
console.log(form); // null
</script>
closest() تتحقق من العنصر نفسه اولا، ثم تصعد عبر اسلافه. لذا اذا كان العنصر نفسه يطابق المحدد، فانها تُرجع العنصر نفسه. هذا يختلف عن parentElement التي تبدا دائما من الاب.دالة contains()
دالة contains() تتحقق مما اذا كانت عقدة هي سليل لعقدة اخرى. تُرجع true اذا كانت العقدة المُمررة ابنا او حفيدا او اي سليل اعمق للعقدة التي استدعيتها عليها. كما تُرجع true اذا مررت العقدة نفسها. هذا مفيد للغاية للتحقق مما اذا حدثت نقرة داخل او خارج عنصر معين.
مثال: استخدام contains()
<div id="dropdown">
<button id="toggle">القائمة</button>
<ul id="menu-list">
<li>الخيار 1</li>
<li>الخيار 2</li>
</ul>
</div>
<script>
const dropdown = document.getElementById('dropdown');
const toggle = document.getElementById('toggle');
const menuList = document.getElementById('menu-list');
// التحقق مما اذا كان toggle داخل dropdown
console.log(dropdown.contains(toggle)); // true
// التحقق مما اذا كان dropdown يحتوي على نفسه
console.log(dropdown.contains(dropdown)); // true
// التحقق مما اذا كان toggle يحتوي على dropdown (العكس)
console.log(toggle.contains(dropdown)); // false
// استخدام عملي: اغلاق القائمة المنسدلة عند النقر خارجها
document.addEventListener('click', function(event) {
if (!dropdown.contains(event.target)) {
menuList.style.display = 'none';
}
});
</script>
المشي في شجرة DOM بالتكرار
احيانا تحتاج لزيارة كل عقدة في شجرة فرعية -- مثلا للعثور على كل المحتوى النصي، او تمييز عناصر معينة، او عد انواع عقد محددة. يمكنك كتابة دالة تكرارية تزور عنصرا ثم تستدعي نفسها على كل من ابناء ذلك العنصر. هذه التقنية تُسمى المشي في شجرة DOM.
مثال: ماشي DOM التكراري
<div id="content">
<h2>العنوان</h2>
<div>
<p>فقرة <strong>بخط عريض</strong> نص</p>
<ul>
<li>العنصر 1</li>
<li>العنصر 2</li>
</ul>
</div>
</div>
<script>
function walkDOM(node, callback) {
// استدعاء callback على العقدة الحالية
callback(node);
// الحصول على الابن الاول
let child = node.firstElementChild;
// زيارة كل ابن بالتكرار
while (child) {
walkDOM(child, callback);
child = child.nextElementSibling;
}
}
// مثال: طباعة اسم وسم كل عنصر
const content = document.getElementById('content');
walkDOM(content, function(element) {
console.log(element.tagName);
});
// الناتج: DIV, H2, DIV, P, STRONG, UL, LI, LI
</script>
التكرار على NodeList و HTMLCollection
عندما تستخدم خصائص مثل childNodes او دوال مثل querySelectorAll، تحصل على NodeList. عندما تستخدم children، تحصل على HTMLCollection. كلاهما كائنات شبيهة بالمصفوفة لكنهما ليسا مصفوفات فعلية. معرفة كيفية التكرار عليهما امر اساسي للتنقل في DOM.
مثال: التكرار على NodeList و HTMLCollection
<ul id="list">
<li class="item">تفاح</li>
<li class="item">موز</li>
<li class="item">كرز</li>
</ul>
<script>
const list = document.getElementById('list');
// الطريقة 1: حلقة for (تعمل مع NodeList و HTMLCollection)
const children = list.children;
for (let i = 0; i < children.length; i++) {
console.log(children[i].textContent);
}
// الطريقة 2: حلقة for...of (تعمل مع NodeList من querySelectorAll)
const items = document.querySelectorAll('.item');
for (const item of items) {
console.log(item.textContent);
}
// الطريقة 3: forEach (تعمل مع NodeList من querySelectorAll)
items.forEach(function(item, index) {
console.log(index + ': ' + item.textContent);
});
// الطريقة 4: تحويل HTMLCollection الى مصفوفة لاستخدام دوال المصفوفة
const childArray = Array.from(list.children);
childArray.forEach(function(child) {
console.log(child.textContent);
});
// الطريقة 5: عامل الانتشار للتحويل الى مصفوفة
const spreadArray = [...list.children];
spreadArray.map(function(child) {
return child.textContent.toUpperCase();
});
</script>
.forEach() مباشرة على HTMLCollection (من .children) سيطرح خطا لان HTMLCollection ليس لديها دالة forEach. حولها دائما الى مصفوفة اولا باستخدام Array.from() او عامل الانتشار، او استخدم حلقة for عادية.تصفية الابناء حسب المعايير
غالبا ما تحتاج للعثور على ابناء محددين يطابقون معايير معينة -- فئة معينة او اسم وسم او سمة او محتوى نصي. يمكنك الجمع بين التنقل والتصفية لتحقيق ذلك بكفاءة.
مثال: تصفية الابناء
<div id="toolbar">
<button class="btn primary">حفظ</button>
<button class="btn">الغاء</button>
<span class="separator">|</span>
<button class="btn danger">حذف</button>
<button class="btn" disabled>ارشيف</button>
</div>
<script>
const toolbar = document.getElementById('toolbar');
// تصفية الابناء للازرار فقط
const buttons = Array.from(toolbar.children).filter(function(child) {
return child.tagName === 'BUTTON';
});
console.log(buttons.length); // 4
// تصفية للازرار المُفعَّلة فقط
const enabledButtons = Array.from(toolbar.children).filter(function(child) {
return child.tagName === 'BUTTON' && !child.disabled;
});
console.log(enabledButtons.length); // 3
// البحث عن اول زر بفئة "danger"
const dangerBtn = Array.from(toolbar.children).find(function(child) {
return child.classList.contains('danger');
});
console.log(dangerBtn.textContent); // "حذف"
// التحقق مما اذا كان اي ابن لديه فئة "primary"
const hasPrimary = Array.from(toolbar.children).some(function(child) {
return child.classList.contains('primary');
});
console.log(hasPrimary); // true
</script>
مثال عملي: قائمة تنقل ديناميكية
لنبني قائمة تنقل عملية تستخدم التنقل في DOM للتعامل مع تفاعلات المستخدم. هذا المثال يوضح كيف يُستخدم التنقل في التطبيقات الحقيقية لادارة الحالات النشطة وتوسيع القوائم الفرعية والتعامل مع التنقل بلوحة المفاتيح.
مثال: تنقل تفاعلي مع التنقل في DOM
<nav id="main-nav">
<ul class="nav-list">
<li class="nav-item active">
<a href="#home">الرئيسية</a>
</li>
<li class="nav-item has-submenu">
<a href="#products">المنتجات</a>
<ul class="submenu">
<li><a href="#software">البرامج</a></li>
<li><a href="#hardware">الاجهزة</a></li>
<li><a href="#services">الخدمات</a></li>
</ul>
</li>
<li class="nav-item">
<a href="#about">من نحن</a>
</li>
<li class="nav-item">
<a href="#contact">اتصل بنا</a>
</li>
</ul>
</nav>
<script>
const navList = document.querySelector('.nav-list');
// التعامل مع النقر على اي رابط تنقل
navList.addEventListener('click', function(event) {
const link = event.target.closest('a');
if (!link) return;
event.preventDefault();
// استخدام closest() للعثور على عنصر nav-item الاب
const navItem = link.closest('.nav-item');
// استخدام parentElement للحصول على مستوى nav-list
const parentList = navItem.parentElement;
// ازالة النشط من جميع الاشقاء في نفس المستوى
Array.from(parentList.children).forEach(function(sibling) {
sibling.classList.remove('active');
});
// تعيين العنصر الحالي كنشط
navItem.classList.add('active');
// تبديل القائمة الفرعية اذا كان هذا العنصر يحتوي على واحدة
if (navItem.classList.contains('has-submenu')) {
const submenu = navItem.querySelector('.submenu');
const isVisible = submenu.style.display === 'block';
submenu.style.display = isVisible ? 'none' : 'block';
}
// اغلاق القوائم الفرعية الاخرى باستخدام التنقل بين الاشقاء
let sibling = navItem.previousElementSibling;
while (sibling) {
const sub = sibling.querySelector('.submenu');
if (sub) sub.style.display = 'none';
sibling = sibling.previousElementSibling;
}
sibling = navItem.nextElementSibling;
while (sibling) {
const sub = sibling.querySelector('.submenu');
if (sub) sub.style.display = 'none';
sibling = sibling.nextElementSibling;
}
});
</script>
مثال عملي: تمييز صفوف الجدول
نمط شائع اخر هو التنقل في صفوف وخلايا الجدول. عندما ينقر المستخدم على خلية، قد ترغب في تمييز الصف بالكامل، او العثور على العنوان لذلك العمود، او التنقل بين الصفوف.
مثال: التنقل في الجدول والتمييز
<table id="data-table">
<thead>
<tr>
<th>الاسم</th>
<th>البريد الالكتروني</th>
<th>الدور</th>
</tr>
</thead>
<tbody>
<tr>
<td>سارة</td>
<td>sara@example.com</td>
<td>مسؤول</td>
</tr>
<tr>
<td>احمد</td>
<td>ahmed@example.com</td>
<td>محرر</td>
</tr>
<tr>
<td>منى</td>
<td>mona@example.com</td>
<td>مشاهد</td>
</tr>
</tbody>
</table>
<script>
const table = document.getElementById('data-table');
const tbody = table.querySelector('tbody');
tbody.addEventListener('click', function(event) {
const cell = event.target.closest('td');
if (!cell) return;
// الحصول على الصف الاب باستخدام parentElement
const row = cell.parentElement;
// ازالة التمييز من جميع صفوف الاشقاء
Array.from(tbody.children).forEach(function(tr) {
tr.style.backgroundColor = '';
});
// تمييز الصف المنقور عليه
row.style.backgroundColor = '#e8f4fd';
// الحصول على فهرس الخلية للعثور على عنوان العمود
const cellIndex = Array.from(row.children).indexOf(cell);
const headerRow = table.querySelector('thead tr');
const header = headerRow.children[cellIndex];
console.log('العمود: ' + header.textContent);
console.log('القيمة: ' + cell.textContent);
// التنقل الى الصف التالي (اذا وُجد)
const nextRow = row.nextElementSibling;
if (nextRow) {
const sameColumnCell = nextRow.children[cellIndex];
console.log('قيمة الصف التالي: ' + sameColumnCell.textContent);
}
});
</script>
مثال عملي: مكون الاكورديون
الاكورديون هو نمط واجهة مستخدم شائع حيث يؤدي النقر على عنوان الى تبديل ظهور المحتوى اسفله. التنقل في DOM يجعل هذا النمط نظيفا وقابلا للصيانة بدون الحاجة لتعيين معرفات فريدة لكل لوحة.
مثال: اكورديون باستخدام التنقل بين الاشقاء
<div class="accordion">
<div class="accordion-item">
<button class="accordion-header">القسم 1</button>
<div class="accordion-panel">
<p>محتوى القسم 1 هنا.</p>
</div>
</div>
<div class="accordion-item">
<button class="accordion-header">القسم 2</button>
<div class="accordion-panel">
<p>محتوى القسم 2 هنا.</p>
</div>
</div>
<div class="accordion-item">
<button class="accordion-header">القسم 3</button>
<div class="accordion-panel">
<p>محتوى القسم 3 هنا.</p>
</div>
</div>
</div>
<script>
const accordion = document.querySelector('.accordion');
accordion.addEventListener('click', function(event) {
const header = event.target.closest('.accordion-header');
if (!header) return;
// الحصول على اللوحة (العنصر الشقيق التالي للعنوان)
const panel = header.nextElementSibling;
// الحصول على عنصر accordion-item الاب
const item = header.parentElement;
// اغلاق جميع اللوحات الاخرى باستخدام التنقل الى الاب
const allItems = item.parentElement.children;
Array.from(allItems).forEach(function(otherItem) {
if (otherItem !== item) {
const otherPanel = otherItem.querySelector('.accordion-panel');
otherPanel.style.display = 'none';
otherItem.querySelector('.accordion-header').classList.remove('active');
}
});
// تبديل اللوحة الحالية
const isOpen = panel.style.display === 'block';
panel.style.display = isOpen ? 'none' : 'block';
header.classList.toggle('active');
});
</script>
جدول ملخص التنقل
اليك جدول مرجعي يلخص جميع خصائص ودوال التنقل التي غطيناها في هذا الدرس. احتفظ بهذا الجدول في متناول يدك اثناء عملك على مهام معالجة DOM.
مرجع سريع: جميع خصائص التنقل
// التنقل الى الاب
element.parentNode // العقدة الاب (اي نوع)
element.parentElement // عقدة عنصر الاب فقط
// التنقل الى الابناء
element.childNodes // جميع العقد الابناء (NodeList)
element.children // عقد العناصر الابناء فقط (HTMLCollection)
element.firstChild // اول عقدة ابن (اي نوع)
element.firstElementChild // اول عقدة عنصر ابن
element.lastChild // اخر عقدة ابن (اي نوع)
element.lastElementChild // اخر عقدة عنصر ابن
// التنقل بين الاشقاء
element.nextSibling // العقدة التالية (اي نوع)
element.nextElementSibling // عقدة العنصر التالي
element.previousSibling // العقدة السابقة (اي نوع)
element.previousElementSibling // عقدة العنصر السابق
// دوال التنقل
element.closest(selector) // اقرب سلف يطابق المحدد
element.contains(node) // التحقق مما اذا كانت العقدة سليلا
// خصائص مفيدة
element.childElementCount // عدد العناصر الابناء
element.hasChildNodes() // تُرجع true اذا كان لديه عقد ابناء
تمرين عملي
انشئ صفحة HTML بهيكل سلسلة تعليقات متداخلة -- قائمة تعليقات حيث يمكن لكل تعليق ان يحتوي على ردود متداخلة بداخله. يجب ان يحتوي كل تعليق على اسم الكاتب ونص التعليق وزر "رد". باستخدام التنقل في DOM فقط (بدون استدعاءات querySelector في معالجات الاحداث)، اكتب جافاسكريبت يفعل ما يلي: (1) عند النقر على زر "رد"، استخدم closest() للعثور على عنصر التعليق الاب وparentElement للعثور على حاوية الردود. (2) استخدم children لعد كم رد لدى ذلك التعليق بالفعل وعرض العدد. (3) استخدم nextElementSibling وpreviousElementSibling لاضافة ازرار "التعليق التالي" و"التعليق السابق" التي تميز تعليقات الاشقاء. (4) اكتب دالة walkDOM تكرارية تعد العدد الاجمالي للتعليقات والردود في كامل السلسلة. (5) استخدم contains() لاكتشاف النقرات خارج سلسلة التعليقات وطي جميع اقسام الردود الموسعة. اختبر حلك بدقة وتحقق من ان كل عملية تنقل تُرجع العنصر المتوقع.