فقاعات الاحداث والالتقاط والتفويض
فهم انتشار الاحداث
عندما تنقر على زر في صفحة ويب، لا يقوم المتصفح ببساطة باطلاق الحدث على ذلك الزر فقط. بدلا من ذلك، ينتقل الحدث عبر شجرة DOM بالكامل بترتيب محدد. هذه الرحلة تسمى انتشار الاحداث، وفهمها ضروري لكتابة JavaScript فعال وخال من الاخطاء. انتشار الاحداث له ثلاث مراحل مميزة: مرحلة الالتقاط ومرحلة الهدف ومرحلة الفقاعات. اتقان هذه المراحل يمنحك تحكما دقيقا في كيفية التعامل مع الاحداث عبر العناصر المتداخلة.
تخيل بنية صفحة بسيطة حيث يوجد زر داخل div، الذي يوجد داخل body، الذي يوجد داخل عنصر html، الذي يوجد داخل document. عندما تنقر على ذلك الزر، لا يظهر الحدث فقط على الزر -- بل ينتقل من اعلى DOM نزولا الى الزر، ثم يعود صعودا مرة اخرى. هذه الرحلة ذهابا وايابا هي جوهر انتشار الاحداث.
المراحل الثلاث لانتشار الاحداث
كل حدث في DOM يمر بثلاث مراحل بالتسلسل:
- مرحلة الالتقاط (المرحلة 1) -- يبدا الحدث من كائن
windowوينتقل نزولا عبر كل عنصر اب حتى يصل الى العنصر الاب للهدف. خلال هذه المرحلة، اي مستمعات احداث مسجلة لمرحلة الالتقاط على العناصر الاب ستنطلق بالترتيب من العنصر الخارجي الى الداخلي. - مرحلة الهدف (المرحلة 2) -- يصل الحدث الى العنصر الذي تم النقر عليه فعليا. هذا هو
event.target. المستمعات المسجلة على هذا العنصر تنطلق بغض النظر عما اذا كانت مضبوطة للالتقاط او الفقاعات. - مرحلة الفقاعات (المرحلة 3) -- بعد مرحلة الهدف، يعكس الحدث اتجاهه وينتقل صعودا عبر جميع العناصر الاب الى
window. اي مستمعات احداث مسجلة لمرحلة الفقاعات ستنطلق بالترتيب من اب الهدف الى الخارج.
مثال: تصور المراحل الثلاث
<!DOCTYPE html>
<html>
<body>
<div id="outer">
<div id="inner">
<button id="btn">انقر هنا</button>
</div>
</div>
<script>
const outer = document.getElementById('outer');
const inner = document.getElementById('inner');
const btn = document.getElementById('btn');
// مستمعات مرحلة الالتقاط (المعامل الثالث true)
outer.addEventListener('click', function() {
console.log('1. Outer DIV - مرحلة الالتقاط');
}, true);
inner.addEventListener('click', function() {
console.log('2. Inner DIV - مرحلة الالتقاط');
}, true);
// مرحلة الهدف
btn.addEventListener('click', function() {
console.log('3. الزر - مرحلة الهدف');
});
// مستمعات مرحلة الفقاعات (المعامل الثالث false او محذوف)
inner.addEventListener('click', function() {
console.log('4. Inner DIV - مرحلة الفقاعات');
});
outer.addEventListener('click', function() {
console.log('5. Outer DIV - مرحلة الفقاعات');
});
</script>
</body>
</html>
// عند النقر على الزر، تظهر في وحدة التحكم:
// 1. Outer DIV - مرحلة الالتقاط
// 2. Inner DIV - مرحلة الالتقاط
// 3. الزر - مرحلة الهدف
// 4. Inner DIV - مرحلة الفقاعات
// 5. Outer DIV - مرحلة الفقاعات
addEventListener المستمعات لمرحلة الفقاعات. يجب عليك تمرير true صراحة كمعامل ثالث او استخدام { capture: true } في كائن الخيارات للاستماع خلال مرحلة الالتقاط. معظم الكود في العالم الحقيقي يعتمد على الفقاعات لانها تتوافق مع الترتيب البديهي للتعامل مع الاحداث من الداخل الى الخارج.معامل الالتقاط في addEventListener
يقبل تابع addEventListener معاملا ثالثا اختياريا يتحكم في المرحلة التي يستجيب لها المستمع. يمكن ان يكون هذا المعامل قيمة منطقية بسيطة او كائن خيارات مع تحكم اكثر دقة.
مثال: استخدام معامل الالتقاط
// الشكل المنطقي: true = مرحلة الالتقاط، false = مرحلة الفقاعات (افتراضي)
element.addEventListener('click', handler, true); // الالتقاط
element.addEventListener('click', handler, false); // الفقاعات
element.addEventListener('click', handler); // الفقاعات (افتراضي)
// شكل كائن الخيارات (موصى به للوضوح)
element.addEventListener('click', handler, {
capture: true, // الاستماع خلال مرحلة الالتقاط
once: true, // الازالة تلقائيا بعد اول استدعاء
passive: true // الوعد بعدم استدعاء preventDefault()
});
// ازالة مستمع الالتقاط يتطلب مطابقة علامة الالتقاط
element.removeEventListener('click', handler, true);
removeEventListener، يجب مطابقة علامة الالتقاط بالضبط. المستمع المسجل بـ capture: true لن يتم ازالته اذا استدعيت removeEventListener بدون علامة true. هذا مصدر شائع لتسريب الذاكرة.فقاعات الاحداث بالتفصيل
الفقاعات هي المرحلة الاكثر استخداما لانها تتوافق مع النموذج الذهني الطبيعي: حدث على عنصر ابن "يتصاعد" بشكل طبيعي الى عناصره الاب. عندما تنقر على <span> داخل <button> داخل <form>، ينطلق حدث النقر على span اولا، ثم الزر، ثم النموذج، وهكذا صعودا الى document و window.
مثال: كيف تؤثر الفقاعات على العناصر المتداخلة
<div id="grandparent" style="padding: 30px; background: #e0e0e0;">
الجد
<div id="parent" style="padding: 30px; background: #b0b0b0;">
الاب
<div id="child" style="padding: 30px; background: #808080;">
الابن
</div>
</div>
</div>
<script>
document.getElementById('grandparent').addEventListener('click', function(e) {
console.log('تم النقر على الجد');
console.log('الهدف:', e.target.id);
console.log('الهدف الحالي:', e.currentTarget.id);
});
document.getElementById('parent').addEventListener('click', function(e) {
console.log('تم النقر على الاب');
console.log('الهدف:', e.target.id);
console.log('الهدف الحالي:', e.currentTarget.id);
});
document.getElementById('child').addEventListener('click', function(e) {
console.log('تم النقر على الابن');
console.log('الهدف:', e.target.id);
console.log('الهدف الحالي:', e.currentTarget.id);
});
// عند النقر على "الابن":
// تم النقر على الابن | الهدف: child | الهدف الحالي: child
// تم النقر على الاب | الهدف: child | الهدف الحالي: parent
// تم النقر على الجد | الهدف: child | الهدف الحالي: grandparent
</script>
event.target يشير دائما الى العنصر الذي تم النقر عليه اصلا (مصدر الحدث)، بينما event.currentTarget يشير الى العنصر الذي يتم تنفيذ المستمع المرتبط به حاليا. فهم هذا الفرق ضروري لتفويض الاحداث.ايقاف الانتشار
احيانا تحتاج الى منع الحدث من الاستمرار في رحلته عبر DOM. يوفر JavaScript تابعين لهذا: stopPropagation() وstopImmediatePropagation().
stopPropagation()
استدعاء event.stopPropagation() يمنع الحدث من الانتقال الى العنصر التالي في سلسلة الانتشار. اذا تم استدعاؤه خلال مرحلة الالتقاط، لن يصل الحدث الى الهدف او يتصاعد. اذا تم استدعاؤه خلال مرحلة الفقاعات، لن يستمر الى العناصر الاب. لكن المستمعات الاخرى على نفس العنصر ستظل تنطلق.
مثال: استخدام stopPropagation
<div id="container">
<button id="myBtn">انقر هنا</button>
</div>
<script>
document.getElementById('container').addEventListener('click', function() {
console.log('تم النقر على الحاوية'); // هذا لن ينطلق
});
document.getElementById('myBtn').addEventListener('click', function(e) {
e.stopPropagation();
console.log('تم النقر على الزر'); // هذا ينطلق
});
// مستمع ثان على نفس الزر لا يزال ينطلق
document.getElementById('myBtn').addEventListener('click', function(e) {
console.log('المستمع الثاني للزر'); // هذا ايضا ينطلق
});
// الناتج عند النقر على الزر:
// تم النقر على الزر
// المستمع الثاني للزر
// (مستمع الحاوية لا ينطلق)
</script>
stopImmediatePropagation()
استدعاء event.stopImmediatePropagation() اكثر صرامة. فهو لا يوقف الحدث من الانتشار الى عناصر اخرى فحسب، بل يمنع ايضا اي مستمعات متبقية على نفس العنصر من الانطلاق. استخدم هذا عندما تحتاج الى ايقاف جميع عمليات معالجة الاحداث بالكامل.
مثال: استخدام stopImmediatePropagation
<button id="actionBtn">اجراء</button>
<script>
const btn = document.getElementById('actionBtn');
btn.addEventListener('click', function(e) {
console.log('المستمع الاول ينطلق');
e.stopImmediatePropagation();
});
btn.addEventListener('click', function(e) {
console.log('المستمع الثاني -- لن ينطلق ابدا');
});
document.body.addEventListener('click', function() {
console.log('مستمع body -- لن ينطلق ابدا');
});
// الناتج: المستمع الاول ينطلق
</script>
stopPropagation() وstopImmediatePropagation() بحذر. ايقاف الانتشار يمكن ان يكسر اجزاء اخرى من تطبيقك تعتمد على تصاعد الاحداث، مثل تتبع التحليلات واختصارات لوحة المفاتيح العامة او المكتبات الخارجية. كلما امكن، استخدم المنطق الشرطي داخل معالجاتك بدلا من ايقاف الانتشار بالكامل.ما هو تفويض الاحداث؟
تفويض الاحداث هو نمط قوي يستفيد من فقاعات الاحداث. بدلا من ربط مستمعات الاحداث بكل عنصر ابن على حدة، تربط مستمعا واحدا بعنصر اب مشترك وتستخدم event.target لتحديد اي عنصر ابن تم النقر عليه فعلا. هذا النمط هو واحد من اهم التقنيات في JavaScript لبناء تطبيقات قابلة للتوسع وعالية الاداء.
الفكرة الاساسية بسيطة: بما ان الاحداث تتصاعد من الابن الى الاب، يمكنك "التقاط" الاحداث على مستوى اعلى في DOM وفحص هدف الحدث لتقرر اي اجراء تتخذه. هذا يلغي الحاجة لربط مستمعات بكل عنصر ابن على حدة.
مثال: بدون تفويض (غير فعال)
<ul id="todo-list">
<li>شراء البقالة</li>
<li>تنظيف المنزل</li>
<li>كتابة الكود</li>
<li>قراءة كتاب</li>
<li>ممارسة الرياضة</li>
</ul>
<script>
// سيء: ربط مستمع بكل عنصر li
const items = document.querySelectorAll('#todo-list li');
items.forEach(function(item) {
item.addEventListener('click', function() {
this.classList.toggle('completed');
});
});
// المشكلة 1: 5 مستمعات احداث منفصلة تستهلك ذاكرة اكثر
// المشكلة 2: العناصر المضافة ديناميكيا لن يكون لها مستمعات
</script>
مثال: مع التفويض (فعال)
<ul id="todo-list">
<li>شراء البقالة</li>
<li>تنظيف المنزل</li>
<li>كتابة الكود</li>
<li>قراءة كتاب</li>
<li>ممارسة الرياضة</li>
</ul>
<script>
// جيد: مستمع واحد على الاب يتعامل مع جميع الابناء
document.getElementById('todo-list').addEventListener('click', function(e) {
if (e.target.tagName === 'LI') {
e.target.classList.toggle('completed');
}
});
// الفائدة 1: مستمع حدث واحد فقط بغض النظر عن حجم القائمة
// الفائدة 2: يعمل مع العناصر المضافة ديناميكيا تلقائيا
</script>
فوائد تفويض الاحداث
يوفر تفويض الاحداث عدة مزايا مهمة مقارنة بربط مستمعات فردية:
- تقليل استخدام الذاكرة -- بدلا من انشاء مئات او الاف كائنات مستمعات الاحداث، تنشئ واحدا فقط. لجدول يحتوي على 1000 صف و10 اعمدة، هذا يعني مستمعا واحدا بدلا من 10,000. التوفير في الذاكرة كبير في التطبيقات الضخمة.
- التعامل التلقائي مع العناصر الديناميكية -- عند اضافة عناصر ابناء جديدة الى DOM ديناميكيا (عبر JavaScript او استجابات AJAX او تفاعل المستخدم)، ترث تلقائيا سلوك الحدث المفوض بدون اي اعداد اضافي. هذه هي الفائدة الاكثر قيمة.
- صيانة كود ابسط -- مع التفويض، يعيش منطق التعامل مع الاحداث في مكان واحد بدلا من ان يكون مبعثرا عبر عناصر فردية كثيرة. هذا يجعل تصحيح الاخطاء وتحديث السلوك اسهل بكثير.
- تهيئة اسرع -- اعداد مستمع واحد اسرع من التكرار عبر عناصر كثيرة وربط مستمعات بكل واحد. هذا يحسن اداء تحميل الصفحة خاصة على الاجهزة البطيئة.
- لا حاجة لتنظيف العناصر المحذوفة -- عند ازالة عناصر ابناء من DOM، لا توجد مستمعات احداث يتيمة لتنظيفها لان المستمع على الاب وليس على الابناء.
تطبيق تفويض الاحداث
يتبع نمط تطبيق تفويض الاحداث بنية متسقة. تربط مستمعا بعنصر اب مستقر، وتتحقق من هدف الحدث لتحديد ما اذا كان يتطابق مع العنصر المطلوب، ثم تنفذ الاجراء المناسب.
مثال: نمط التفويض الاساسي
// الخطوة 1: اختيار عنصر اب مستقر
const parentElement = document.getElementById('container');
// الخطوة 2: ربط مستمع حدث واحد بالاب
parentElement.addEventListener('click', function(e) {
// الخطوة 3: التحقق مما اذا كان العنصر المنقور يتطابق مع معاييرك
if (e.target.matches('.action-button')) {
handleAction(e.target);
}
// يمكنك التعامل مع انواع عناصر متعددة في مستمع واحد
if (e.target.matches('.delete-button')) {
handleDelete(e.target);
}
if (e.target.matches('.edit-button')) {
handleEdit(e.target);
}
});
// تابع matches() يتحقق مما اذا كان العنصر يتطابق مع محدد CSS
// وهو الطريقة المفضلة لتصفية الاهداف في المعالجات المفوضة
التعامل مع العناصر المتداخلة باستخدام closest()
تحد شائع في التفويض هو ان المستخدم قد ينقر على عنصر ابن داخل هدفك. مثلا اذا كان الزر يحتوي على ايقونة او span، فان event.target سيكون الايقونة او span وليس الزر نفسه. تابع closest() يحل هذا بالتنقل صعودا في شجرة DOM للعثور على اقرب عنصر اب يتطابق مع محدد.
مثال: استخدام closest() لتفويض موثوق
<ul id="nav-menu">
<li class="nav-item">
<a href="#home"><span class="icon">🏠</span> الرئيسية</a>
</li>
<li class="nav-item">
<a href="#about"><span class="icon">ℹ️</span> من نحن</a>
</li>
<li class="nav-item">
<a href="#contact"><span class="icon">✉️</span> اتصل بنا</a>
</li>
</ul>
<script>
document.getElementById('nav-menu').addEventListener('click', function(e) {
// اذا نقر المستخدم على ايقونة span، فان e.target هو span وليس li
// closest() يتنقل صعودا من الهدف للعثور على العنصر الاب المطابق
const navItem = e.target.closest('.nav-item');
if (navItem) {
// ازالة الفئة النشطة من جميع العناصر
this.querySelectorAll('.nav-item').forEach(function(item) {
item.classList.remove('active');
});
// اضافة الفئة النشطة للعنصر المنقور
navItem.classList.add('active');
const link = navItem.querySelector('a');
console.log('الانتقال الى:', link.getAttribute('href'));
}
});
</script>
closest() بدلا من التحقق من event.target مباشرة عندما تحتوي عناصرك القابلة للنقر على عقد ابناء. تابع closest() يعيد اول عنصر اب (بدءا من العنصر نفسه) يتطابق مع محدد CSS، او null اذا لم يوجد تطابق. تحقق دائما من null قبل استخدام النتيجة.استخدام سمات البيانات للتفويض
سمات بيانات HTML5 (data-*) لا تقدر بثمن لتفويض الاحداث لانها تتيح لك تضمين بيانات وصفية مباشرة في عناصر HTML. بدلا من الاعتماد فقط على اسماء الفئات او اسماء الوسوم لتحديد السلوك، يمكنك استخدام سمات البيانات لتمرير المعلومات الى معالج الاحداث. هذا ينشئ فصلا نظيفا بين التنسيق (الفئات) والسلوك (سمات البيانات).
مثال: سمات البيانات لتوجيه الاجراءات
<div id="toolbar">
<button data-action="bold" data-shortcut="Ctrl+B">غامق</button>
<button data-action="italic" data-shortcut="Ctrl+I">مائل</button>
<button data-action="underline" data-shortcut="Ctrl+U">تحته خط</button>
<button data-action="copy">نسخ</button>
<button data-action="paste">لصق</button>
</div>
<script>
document.getElementById('toolbar').addEventListener('click', function(e) {
const button = e.target.closest('[data-action]');
if (!button) return;
const action = button.dataset.action;
const shortcut = button.dataset.shortcut || 'بدون اختصار';
console.log('الاجراء:', action, '| الاختصار:', shortcut);
switch (action) {
case 'bold':
document.execCommand('bold');
break;
case 'italic':
document.execCommand('italic');
break;
case 'underline':
document.execCommand('underline');
break;
case 'copy':
document.execCommand('copy');
break;
case 'paste':
navigator.clipboard.readText().then(function(text) {
document.execCommand('insertText', false, text);
});
break;
}
});
</script>
مثال: سمات البيانات مع بيانات ديناميكية
<table id="users-table">
<thead>
<tr>
<th>الاسم</th>
<th>البريد الالكتروني</th>
<th>الاجراءات</th>
</tr>
</thead>
<tbody>
<tr data-user-id="101">
<td>احمد محمد</td>
<td>ahmed@example.com</td>
<td>
<button data-action="edit">تعديل</button>
<button data-action="delete">حذف</button>
</td>
</tr>
<tr data-user-id="102">
<td>سارة علي</td>
<td>sara@example.com</td>
<td>
<button data-action="edit">تعديل</button>
<button data-action="delete">حذف</button>
</td>
</tr>
</tbody>
</table>
<script>
document.getElementById('users-table').addEventListener('click', function(e) {
const button = e.target.closest('[data-action]');
if (!button) return;
const row = button.closest('tr[data-user-id]');
if (!row) return;
const userId = row.dataset.userId;
const action = button.dataset.action;
const userName = row.querySelector('td').textContent;
if (action === 'edit') {
console.log('تعديل المستخدم:', userName, '(المعرف:', userId, ')');
openEditModal(userId);
} else if (action === 'delete') {
if (confirm('حذف ' + userName + '؟')) {
console.log('حذف المستخدم:', userName, '(المعرف:', userId, ')');
deleteUser(userId);
row.remove();
}
}
});
</script>
مثال واقعي: قائمة مهام تفاعلية
لنبني تطبيق قائمة مهام كامل وعملي يوضح تفويض الاحداث في سيناريو حقيقي. هذا المثال يتعامل مع اضافة عناصر جديدة وتبديل حالة الاكمال وحذف العناصر -- كل ذلك بمستمع حدث مفوض واحد على حاوية القائمة.
مثال: قائمة مهام كاملة مع التفويض
<div id="todo-app">
<h2>قائمة مهامي</h2>
<form id="todo-form">
<input type="text" id="todo-input" placeholder="اضف مهمة جديدة...">
<button type="submit">اضافة</button>
</form>
<ul id="todo-list">
<li data-id="1">
<span class="todo-text">تعلم فقاعات الاحداث</span>
<button class="complete-btn" data-action="toggle">اكمال</button>
<button class="delete-btn" data-action="delete">حذف</button>
</li>
<li data-id="2">
<span class="todo-text">ممارسة تفويض الاحداث</span>
<button class="complete-btn" data-action="toggle">اكمال</button>
<button class="delete-btn" data-action="delete">حذف</button>
</li>
</ul>
<p id="todo-count"></p>
</div>
<script>
let nextId = 3;
// مستمع مفوض واحد يتعامل مع جميع تفاعلات القائمة
document.getElementById('todo-list').addEventListener('click', function(e) {
const button = e.target.closest('[data-action]');
if (!button) return;
const listItem = button.closest('li');
const action = button.dataset.action;
if (action === 'toggle') {
listItem.classList.toggle('completed');
const text = listItem.querySelector('.todo-text');
if (listItem.classList.contains('completed')) {
text.style.textDecoration = 'line-through';
button.textContent = 'تراجع';
} else {
text.style.textDecoration = 'none';
button.textContent = 'اكمال';
}
}
if (action === 'delete') {
listItem.style.opacity = '0';
setTimeout(function() {
listItem.remove();
updateCount();
}, 300);
}
updateCount();
});
// اضافة مهام جديدة -- تحصل تلقائيا على تغطية التفويض
document.getElementById('todo-form').addEventListener('submit', function(e) {
e.preventDefault();
const input = document.getElementById('todo-input');
const text = input.value.trim();
if (!text) return;
const li = document.createElement('li');
li.setAttribute('data-id', nextId++);
li.innerHTML =
'<span class="todo-text">' + escapeHtml(text) + '</span>' +
'<button class="complete-btn" data-action="toggle">اكمال</button>' +
'<button class="delete-btn" data-action="delete">حذف</button>';
document.getElementById('todo-list').appendChild(li);
input.value = '';
updateCount();
});
function updateCount() {
const total = document.querySelectorAll('#todo-list li').length;
const completed = document.querySelectorAll('#todo-list li.completed').length;
document.getElementById('todo-count').textContent =
completed + ' من ' + total + ' مهمة مكتملة';
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
updateCount();
</script>
<ul> الاب. هذه هي القوة الاساسية للتفويض -- العناصر الديناميكية تعمل فورا بدون اعداد اضافي.مثال واقعي: صفوف جدول ديناميكية
الجداول ذات الصفوف التفاعلية هي واحدة من اكثر الانماط شيوعا في تطبيقات الويب -- لوحات المعلومات ولوحات الادارة وشبكات البيانات والمزيد. تفويض الاحداث يجعل التعامل مع التفاعلات عبر مئات او الاف الصفوف عمليا وعالي الاداء.
مثال: جدول ديناميكي مع اجراءات الصفوف
<table id="products-table">
<thead>
<tr>
<th data-sort="name">اسم المنتج</th>
<th data-sort="price">السعر</th>
<th data-sort="stock">المخزون</th>
<th>الاجراءات</th>
</tr>
</thead>
<tbody id="products-body"></tbody>
</table>
<script>
const products = [
{ id: 1, name: 'حاسوب محمول', price: 999, stock: 15 },
{ id: 2, name: 'لوحة مفاتيح', price: 79, stock: 42 },
{ id: 3, name: 'فارة', price: 29, stock: 88 }
];
function renderProducts() {
const tbody = document.getElementById('products-body');
tbody.innerHTML = products.map(function(product) {
return '<tr data-product-id="' + product.id + '">' +
'<td>' + product.name + '</td>' +
'<td>$' + product.price + '</td>' +
'<td>' + product.stock + '</td>' +
'<td>' +
'<button data-action="view">عرض</button>' +
'<button data-action="restock">تعبئة</button>' +
'<button data-action="remove">ازالة</button>' +
'</td>' +
'</tr>';
}).join('');
}
// مستمع واحد لجميع تفاعلات الجدول
document.getElementById('products-table').addEventListener('click', function(e) {
// التعامل مع رؤوس الترتيب
const header = e.target.closest('[data-sort]');
if (header) {
const sortKey = header.dataset.sort;
products.sort(function(a, b) {
if (typeof a[sortKey] === 'string') {
return a[sortKey].localeCompare(b[sortKey]);
}
return a[sortKey] - b[sortKey];
});
renderProducts();
return;
}
// التعامل مع ازرار اجراءات الصفوف
const button = e.target.closest('[data-action]');
if (!button) return;
const row = button.closest('tr[data-product-id]');
if (!row) return;
const productId = parseInt(row.dataset.productId);
const product = products.find(function(p) { return p.id === productId; });
const action = button.dataset.action;
if (action === 'view') {
alert('المنتج: ' + product.name + ' | السعر: $' + product.price +
' | المخزون: ' + product.stock);
} else if (action === 'restock') {
product.stock += 10;
renderProducts();
} else if (action === 'remove') {
const index = products.findIndex(function(p) { return p.id === productId; });
if (index !== -1) {
products.splice(index, 1);
renderProducts();
}
}
});
renderProducts();
</script>
مثال واقعي: قائمة تنقل مع قوائم فرعية
قوائم التنقل غالبا ما تحتوي على قوائم فرعية متداخلة تحتاج للفتح والاغلاق عند النقر. التفويض مثالي لهذا لان بنية القائمة يمكن ان تتغير ويمكن اضافة عناصر جديدة بدون اعادة ربط الاحداث.
مثال: قائمة تنقل مفوضة
<nav id="main-nav">
<ul class="menu">
<li class="menu-item" data-has-submenu="true">
<a href="#" class="menu-link">المنتجات</a>
<ul class="submenu">
<li><a href="/laptops" data-category="laptops">الحواسيب</a></li>
<li><a href="/phones" data-category="phones">الهواتف</a></li>
<li><a href="/tablets" data-category="tablets">الاجهزة اللوحية</a></li>
</ul>
</li>
<li class="menu-item" data-has-submenu="true">
<a href="#" class="menu-link">الخدمات</a>
<ul class="submenu">
<li><a href="/consulting" data-category="consulting">الاستشارات</a></li>
<li><a href="/training" data-category="training">التدريب</a></li>
</ul>
</li>
<li class="menu-item">
<a href="/about" class="menu-link">من نحن</a>
</li>
</ul>
</nav>
<script>
document.getElementById('main-nav').addEventListener('click', function(e) {
const menuLink = e.target.closest('.menu-link');
if (menuLink) {
const parentItem = menuLink.closest('.menu-item');
// التعامل مع تبديل القائمة الفرعية فقط للعناصر التي لها قوائم فرعية
if (parentItem && parentItem.dataset.hasSubmenu === 'true') {
e.preventDefault();
// اغلاق جميع القوائم الفرعية المفتوحة الاخرى
const allItems = this.querySelectorAll('.menu-item[data-has-submenu="true"]');
allItems.forEach(function(item) {
if (item !== parentItem) {
item.classList.remove('open');
}
});
// تبديل القائمة الفرعية المنقورة
parentItem.classList.toggle('open');
}
}
// التعامل مع روابط الفئات في القوائم الفرعية
const categoryLink = e.target.closest('[data-category]');
if (categoryLink) {
console.log('الفئة المحددة:', categoryLink.dataset.category);
}
});
// اغلاق القوائم عند النقر خارجها
document.addEventListener('click', function(e) {
if (!e.target.closest('#main-nav')) {
document.querySelectorAll('.menu-item.open').forEach(function(item) {
item.classList.remove('open');
});
}
});
</script>
الاخطاء الشائعة وافضل الممارسات
بينما تفويض الاحداث قوي، هناك اخطاء شائعة يجب تجنبها وافضل ممارسات يجب اتباعها:
- تحقق دائما من null مع closest() -- تابع
closest()يعيدnullاذا لم يوجد تطابق. تحقق دائما من القيمة المعادة قبل استخدامها. - اختر عناصر اب مستقرة -- اربط مستمعك المفوض بعنصر موجود في DOM عند تحميل الصفحة ولن تتم ازالته. body متاح دائما لكن كن محددا قدر الامكان لتحسين الاداء.
- لا تفرط في التفويض -- تفويض كل شيء الى
document.bodyيمكن ان يبطئ تطبيقك لان كل نقرة يجب ان تمر عبر معالجك. فوض الى اقرب عنصر اب مستقر يحتوي على جميع عناصرك الديناميكية. - كن حذرا مع الاحداث التي لا تتصاعد -- ليست كل الاحداث تتصاعد. احداث مثل
focusوblurوloadوunloadوscrollوmouseenter/mouseleaveلا تتصاعد. استخدمfocusin/focusoutكبدائل للتفويض المتعلق بالتركيز. - فضل سمات البيانات على اسماء الفئات للسلوك -- استخدم فئات CSS للتنسيق وسمات البيانات لسلوك JavaScript. هذا يفصل الاهتمامات ويمنع اعادة هيكلة CSS من كسر معالجات الاحداث.
تمرين عملي
ابنِ لوحة مهام تفاعلية بثلاثة اعمدة: "للتنفيذ" و"قيد التنفيذ" و"مكتمل". كل عمود يجب ان يكون <div> يحتوي على بطاقات مهام كعناصر ابناء. كل بطاقة مهمة يجب ان تحتوي على عنوان وزر "نقل لليمين" (لنقل البطاقة الى العمود التالي) وزر "حذف". استخدم تفويض الاحداث بربط مستمع نقر واحد فقط بحاوية اب تغلف جميع الاعمدة الثلاثة. استخدم سمات البيانات (data-action على الازرار وdata-task-id على البطاقات) لتوجيه الاجراءات. اضف نموذجا في الاعلى لانشاء مهام جديدة تظهر في عمود "للتنفيذ". تحقق من ان المهام المنشاة حديثا تستجيب للنقرات بدون اضافة اي مستمعات جديدة. اضف عدادا اسفل كل عمود يعرض عدد المهام التي يحتويها. كتحد اضافي، اضف دعم لوحة المفاتيح بحيث يؤدي الضغط على Enter على بطاقة مهمة مركزة الى تشغيل اجراء "نقل لليمين" باستخدام نفس المستمع المفوض. سجل جميع مراحل الانتشار في وحدة التحكم للتحقق من فهمك لتدفق الاحداث.