jQuery والتعامل مع DOM

تفويض الأحداث

15 دقيقة الدرس 21 من 30

فهم تفويض الأحداث

تفويض الأحداث هو نمط قوي يسمح لك بالتعامل مع الأحداث للعناصر المضافة ديناميكياً بكفاءة. بدلاً من إرفاق معالجات الأحداث بالعناصر الفردية، تقوم بإرفاق معالج واحد بعنصر أصلي.

لماذا تفويض الأحداث؟

  • محتوى ديناميكي: يعمل مع العناصر المضافة بعد تحميل الصفحة
  • الأداء: معالجات أحداث أقل = استخدام أفضل للذاكرة
  • البساطة: معالج واحد بدلاً من العديد
  • الصيانة: أسهل في الإدارة والتحديث

المشكلة: الربط المباشر للأحداث

هذا لن يعمل للعناصر الجديدة:
<ul id="itemList">
    <li>عنصر 1</li>
    <li>عنصر 2</li>
</ul>
<button id="addItem">إضافة عنصر</button>

<script>
// هذا يعمل فقط للعناصر الموجودة
$("li").on("click", function() {
    alert($(this).text());
});

// إضافة عنصر جديد
$("#addItem").on("click", function() {
    $("#itemList").append("<li>عنصر جديد</li>");
    // ❌ العناصر الجديدة لن تستجيب للنقرات!
});
</script>
المشكلة: معالجات الأحداث المرفقة بـ $(selector).on("event", handler) ترتبط فقط بالعناصر الموجودة في وقت تشغيل الكود.

الحل: تفويض الأحداث

استخدام تفويض الأحداث:
<ul id="itemList">
    <li>عنصر 1</li>
    <li>عنصر 2</li>
</ul>
<button id="addItem">إضافة عنصر</button>

<script>
// إرفاق المعالج بالعنصر الأصلي
$("#itemList").on("click", "li", function() {
    alert($(this).text());
});

// إضافة عناصر جديدة
let count = 3;
$("#addItem").on("click", function() {
    $("#itemList").append("<li>عنصر " + count + "</li>");
    count++;
    // ✓ العناصر الجديدة تستجيب تلقائياً للنقرات!
});
</script>

صيغة تفويض الأحداث

نمط التفويض:
// $(parent).on(event, childSelector, handler)

// أمثلة:
$("#container").on("click", ".button", function() {
    // التعامل مع النقرات على عناصر .button داخل #container
});

$(document).on("change", "select.category", function() {
    // التعامل مع التغييرات على عناصر select.category في أي مكان في المستند
});

$(".list").on("mouseenter", "li", function() {
    // التعامل مع دخول الماوس على عناصر li داخل .list
});
أفضل ممارسة: استخدم أقرب عنصر أصلي مستقر، وليس دائماً document. هذا يحسن الأداء بتقليل مسار فقاعات الحدث.

كيف يعمل تفويض الأحداث

يستفيد تفويض الأحداث من فقاعات الأحداث - عندما يحدث حدث على عنصر، يتصاعد عبر أسلافه:

مثال على فقاعات الأحداث:
<div id="outer">
    <div id="middle">
        <button id="inner">انقر هنا</button>
    </div>
</div>

<script>
// عند النقر على الزر، يتصاعد الحدث:
// 1. button#inner
// 2. div#middle
// 3. div#outer
// 4. body
// 5. html
// 6. document
// 7. window

$("#outer").on("click", function(event) {
    console.log("تم النقر على Outer");
    console.log("الهدف:", event.target.id);        // العنصر الذي أطلق
    console.log("الهدف الحالي:", event.currentTarget.id); // العنصر الذي يعالج
});
</script>

مثال عملي: قائمة مهام ديناميكية

قائمة مهام كاملة مع تفويض الأحداث:
<div id="todoApp">
    <input type="text" id="todoInput" placeholder="أدخل مهمة...">
    <button id="addTodo">إضافة مهمة</button>
    <ul id="todoList"></ul>
</div>

<style>
.todo-item {
    display: flex;
    align-items: center;
    padding: 10px;
    margin: 5px 0;
    background: var(--bg-light);
    border-radius: 5px;
}
.todo-item.completed {
    text-decoration: line-through;
    opacity: 0.6;
}
.todo-item button {
    margin-left: auto;
    padding: 5px 10px;
    cursor: pointer;
}
</style>

<script>
$(document).ready(function() {
    // إضافة مهمة جديدة
    $("#addTodo").on("click", function() {
        let text = $("#todoInput").val().trim();

        if (text !== "") {
            let todoHtml = `
                <li class="todo-item">
                    <input type="checkbox" class="todo-checkbox">
                    <span class="todo-text">${text}</span>
                    <button class="edit-btn">تعديل</button>
                    <button class="delete-btn">حذف</button>
                </li>
            `;
            $("#todoList").append(todoHtml);
            $("#todoInput").val("");
        }
    });

    // مفتاح Enter للإضافة
    $("#todoInput").on("keypress", function(event) {
        if (event.key === "Enter") {
            $("#addTodo").click();
        }
    });

    // ✓ تفويض الحدث لمربع الاختيار (تبديل الإكمال)
    $("#todoList").on("change", ".todo-checkbox", function() {
        $(this).closest(".todo-item").toggleClass("completed");
    });

    // ✓ تفويض الحدث لزر الحذف
    $("#todoList").on("click", ".delete-btn", function() {
        $(this).closest(".todo-item").fadeOut(300, function() {
            $(this).remove();
        });
    });

    // ✓ تفويض الحدث لزر التعديل
    $("#todoList").on("click", ".edit-btn", function() {
        let todoItem = $(this).closest(".todo-item");
        let textSpan = todoItem.find(".todo-text");
        let currentText = textSpan.text();

        let newText = prompt("تعديل المهمة:", currentText);
        if (newText !== null && newText.trim() !== "") {
            textSpan.text(newText);
        }
    });
});
</script>

أنواع أحداث متعددة مع التفويض

التعامل مع أحداث متعددة:
<div id="gallery"></div>
<button id="addImage">إضافة صورة</button>

<script>
// إضافة صور ديناميكياً
let imageCount = 1;
$("#addImage").on("click", function() {
    let imgHtml = `<img src="image${imageCount}.jpg" class="gallery-img">`;
    $("#gallery").append(imgHtml);
    imageCount++;
});

// أحداث مفوضة متعددة
$("#gallery")
    .on("mouseenter", ".gallery-img", function() {
        $(this).css("transform", "scale(1.1)");
    })
    .on("mouseleave", ".gallery-img", function() {
        $(this).css("transform", "scale(1)");
    })
    .on("click", ".gallery-img", function() {
        alert("تم النقر على الصورة: " + $(this).attr("src"));
    })
    .on("dblclick", ".gallery-img", function() {
        $(this).remove();
    });
</script>

التفويض مع مساحات أسماء الأحداث

استخدام مساحات أسماء الأحداث:
// إضافة أحداث بمساحات أسماء
$("#container").on("click.myNamespace", ".button", function() {
    console.log("تم النقر على الزر");
});

$("#container").on("mouseenter.myNamespace", ".button", function() {
    console.log("تمرير على الزر");
});

// إزالة أحداث مساحة الأسماء فقط
$("#container").off(".myNamespace");

// حدث مساحة اسم محدد
$("#container").off("click.myNamespace");

مقارنة الأداء

الربط المباشر مقابل التفويض:
// ❌ سيء: ربط مباشر للعديد من العناصر
$(".item").on("click", function() {
    // 1000 عنصر = 1000 معالج حدث في الذاكرة
});

// ✓ جيد: تفويض الأحداث
$("#itemList").on("click", ".item", function() {
    // 1000 عنصر = 1 معالج حدث في الذاكرة
});
نصيحة أداء: للقوائم الكبيرة (100+ عنصر)، يمكن لتفويض الأحداث تحسين الأداء بمقدار 10-50 مرة وتقليل استخدام الذاكرة بشكل كبير.

إيقاف انتشار الأحداث

التحكم في فقاعات الأحداث:
<div id="parent">
    الأصل
    <div id="child">
        الطفل
        <button id="button">زر</button>
    </div>
</div>

<script>
$("#parent").on("click", function() {
    console.log("تم النقر على الأصل");
});

$("#child").on("click", function(event) {
    console.log("تم النقر على الطفل");
    // منع الحدث من التصاعد إلى الأصل
    event.stopPropagation();
});

$("#button").on("click", function(event) {
    console.log("تم النقر على الزر");
    // هذا يوقف كل الانتشار
    event.stopImmediatePropagation();
});
</script>
تحذير: استخدم stopPropagation() باعتدال لأنه يمكن أن يكسر أنماط تفويض الأحداث الأعلى في شجرة DOM.

مثال متقدم: نظام تعليقات متداخلة

نظام تعليقات مع التفويض:
<div id="comments">
    <div class="comment" data-id="1">
        <p>تعليق أول</p>
        <button class="reply-btn">رد</button>
        <button class="like-btn">إعجاب (<span class="like-count">0</span>)</button>
        <div class="replies"></div>
    </div>
</div>

<script>
let commentIdCounter = 2;

// تفويض: زر الرد
$("#comments").on("click", ".reply-btn", function() {
    let comment = $(this).closest(".comment");
    let replies = comment.find("> .replies");

    let replyText = prompt("أدخل ردك:");
    if (replyText) {
        let replyHtml = `
            <div class="comment reply" data-id="${commentIdCounter++}"
                 style="margin-left: 30px;">
                <p>${replyText}</p>
                <button class="reply-btn">رد</button>
                <button class="like-btn">إعجاب (<span class="like-count">0</span>)</button>
                <div class="replies"></div>
            </div>
        `;
        replies.append(replyHtml);
    }
});

// تفويض: زر الإعجاب
$("#comments").on("click", ".like-btn", function() {
    let likeCount = $(this).find(".like-count");
    let count = parseInt(likeCount.text());
    likeCount.text(count + 1);

    $(this).prop("disabled", true).css("opacity", "0.5");
});

// تفويض: عرض معرف التعليق عند التمرير
$("#comments").on("mouseenter", ".comment", function() {
    let id = $(this).data("id");
    $(this).css("background-color", "var(--bg-light)");
    console.log("التمرير على معرف التعليق:", id);
}).on("mouseleave", ".comment", function() {
    $(this).css("background-color", "transparent");
});
</script>

التفويض مع سمات البيانات المخصصة

استخدام سمات البيانات:
<div id="productList">
    <div class="product" data-id="101" data-price="29.99">
        منتج 1
        <button class="add-to-cart">إضافة إلى السلة</button>
    </div>
    <div class="product" data-id="102" data-price="49.99">
        منتج 2
        <button class="add-to-cart">إضافة إلى السلة</button>
    </div>
</div>
<div id="cart">السلة: $0.00</div>

<script>
let cartTotal = 0;

$("#productList").on("click", ".add-to-cart", function() {
    let product = $(this).closest(".product");
    let productId = product.data("id");
    let productPrice = parseFloat(product.data("price"));

    cartTotal += productPrice;
    $("#cart").text("السلة: $" + cartTotal.toFixed(2));

    console.log("تمت إضافة معرف المنتج:", productId);
});
</script>

تمرين تطبيقي:

المهمة: بناء نظام إدارة جدول ديناميكي مع تفويض الأحداث:

  1. إنشاء جدول بأعمدة "الاسم"، "البريد الإلكتروني"، "الإجراءات"
  2. زر إضافة لإدراج صفوف جديدة ديناميكياً
  3. كل صف يحتوي على أزرار "تعديل"، "حذف"، و "تكرار"
  4. زر التعديل: جعل الصف قابلاً للتحرير مباشرة
  5. زر الحذف: إزالة الصف مع تحريك التلاشي
  6. زر التكرار: إنشاء نسخة من الصف أسفل الصف الحالي
  7. إضافة تمييز للصف عند التمرير
  8. إضافة عداد نقرات لكل صف

مكافأة: نفذ إعادة ترتيب الصفوف بالسحب والإفلات باستخدام تفويض الأحداث. أضف وظيفة التراجع للحذف.

الأخطاء الشائعة

أخطاء يجب تجنبها:
// ❌ خطأ: استخدام this في دالة سهمية
$("#list").on("click", "li", () => {
    $(this).addClass("active"); // 'this' غير معرف!
});

// ✓ صحيح: استخدم دالة عادية
$("#list").on("click", "li", function() {
    $(this).addClass("active");
});

// ❌ خطأ: تفويض عام جداً
$(document).on("click", "div", function() {
    // يُطلق لجميع divs في المستند!
});

// ✓ صحيح: أصل ومحدد محددان
$("#container").on("click", ".specific-div", function() {
    // يُطلق فقط لـ .specific-div داخل #container
});
</script>

النقاط الرئيسية

  • تفويض الأحداث يعمل مع العناصر المضافة ديناميكياً
  • الصيغة: $(parent).on(event, childSelector, handler)
  • يستخدم فقاعات الأحداث للتعامل مع الأحداث على الأسلاف
  • أداء أفضل لعدد كبير من العناصر
  • اختر أقرب أصل مستقر، وليس دائماً document
  • استخدم دوال عادية، وليس دوال سهمية، عندما تحتاج this