أساسيات JavaScript

الاحداث: addEventListener وكائن الحدث

45 دقيقة الدرس 24 من 60

ما هي الاحداث في جافاسكريبت؟

الاحداث هي اشارات بان شيئا ما حدث في المتصفح. عندما ينقر المستخدم على زر، او يرسل نموذجا، او يمرر الصفحة، او يضغط مفتاحا، او حتى عندما تنتهي الصفحة من التحميل -- كل من هذه الاجراءات يُطلق حدثا. جافاسكريبت يمنحك القدرة على الاستماع لهذه الاحداث وتشغيل كود استجابة لها. هذا هو اساس كل تطبيقات الويب التفاعلية. بدون الاحداث، ستكون صفحات الويب مستندات ثابتة بدون طريقة للاستجابة لاجراءات المستخدم.

نظام الاحداث في جافاسكريبت يتبع نمطا بسيطا: تختار عنصرا، وتخبره باي حدث يستمع له، وتوفر دالة تعمل عندما يحدث ذلك الحدث. هذه الدالة تُسمى معالج الحدث او مستمع الحدث. الطريقة الحديثة والموصى بها لاعداد معالجة الاحداث هي من خلال دالة addEventListener.

صيغة addEventListener

دالة addEventListener هي الطريقة القياسية لربط معالجات الاحداث بعناصر DOM. تقبل وسيطين مطلوبين ووسيط اختياري واحد. الوسيط الاول هو نوع الحدث كنص. الوسيط الثاني هو دالة callback التي تعمل عندما يُطلق الحدث. الوسيط الثالث الاختياري هو كائن خيارات او قيمة منطقية لوضع الالتقاط.

مثال: صيغة addEventListener الاساسية

<button id="myBtn">انقر هنا</button>

<script>
    const button = document.getElementById('myBtn');

    // الصيغة: element.addEventListener(eventType, handlerFunction, options)
    button.addEventListener('click', function() {
        console.log('تم النقر على الزر!');
    });

    // يمكنك ايضا استخدام دالة مسماة
    function handleClick() {
        console.log('تم النقر على الزر بدالة مسماة!');
    }
    button.addEventListener('click', handleClick);

    // صيغة الدالة السهمية
    button.addEventListener('click', () => {
        console.log('تم النقر على الزر بدالة سهمية!');
    });
</script>
ملاحظة: يمكنك ربط عدة مستمعي احداث من نفس النوع بنفس العنصر. جميع معالجات النقر الثلاثة في المثال اعلاه ستعمل عند النقر على الزر، بالترتيب الذي تم تسجيلها به. هذه واحدة من المزايا الرئيسية لـ addEventListener مقارنة بالطرق القديمة.

انماط معالجة الاحداث: مضمنة مقابل addEventListener

هناك ثلاث طرق لمعالجة الاحداث في جافاسكريبت، لكن واحدة فقط موصى بها للتطوير الحديث. فهم الثلاثة يساعدك على قراءة الكود القديم وفهم لماذا addEventListener هو النهج القياسي.

مثال: ثلاث طرق لمعالجة الاحداث

<!-- الطريقة 1: سمة HTML مضمنة (غير موصى بها) -->
<button onclick="alert('تم النقر!')">معالج مضمن</button>

<!-- الطريقة 2: خاصية DOM (محدودة) -->
<button id="btn2">معالج خاصية</button>

<!-- الطريقة 3: addEventListener (الموصى بها) -->
<button id="btn3">addEventListener</button>

<script>
    // الطريقة 2: خاصية DOM -- معالج واحد فقط لكل نوع حدث
    const btn2 = document.getElementById('btn2');
    btn2.onclick = function() {
        console.log('المعالج الاول');
    };
    // هذا يستبدل المعالج الاول!
    btn2.onclick = function() {
        console.log('المعالج الثاني -- الاول اختفى!');
    };

    // الطريقة 3: addEventListener -- معالجات متعددة لكل نوع حدث
    const btn3 = document.getElementById('btn3');
    btn3.addEventListener('click', function() {
        console.log('المعالج الاول');
    });
    // هذا يضيف معالجا ثانيا -- كلاهما سيعملان!
    btn3.addEventListener('click', function() {
        console.log('المعالج الثاني -- الاول لا يزال يعمل!');
    });
</script>
خطا شائع: استخدام سمات احداث HTML المضمنة مثل onclick="..." يخلط جافاسكريبت مع HTML، مما يجعل الكود اصعب في الصيانة والتصحيح والتامين. كما انه يحدك بتعبير معالج واحد. استخدم دائما addEventListener في كود جافاسكريبت. الاستثناء الوحيد هو عند قراءة او صيانة كود قديم جدا يستخدم بالفعل معالجات مضمنة.

انواع الاحداث الشائعة

جافاسكريبت يدعم عشرات انواع الاحداث. اليك الاكثر استخداما مجمعة حسب الفئة. فهم انواع الاحداث هذه امر اساسي لبناء تطبيقات تفاعلية.

احداث الفارة

مثال: احداث الفارة

<div id="box" style="width:200px;height:200px;background:#eee;padding:20px;">
    مرر وانقر هنا
</div>

<script>
    const box = document.getElementById('box');

    // click -- يُطلق عند النقر على العنصر
    box.addEventListener('click', function() {
        console.log('تم النقر');
    });

    // dblclick -- يُطلق عند النقر المزدوج
    box.addEventListener('dblclick', function() {
        console.log('نقر مزدوج');
    });

    // mouseenter -- يُطلق عند دخول الفارة للعنصر (لا يتصاعد)
    box.addEventListener('mouseenter', function() {
        box.style.background = '#cde';
    });

    // mouseleave -- يُطلق عند خروج الفارة من العنصر (لا يتصاعد)
    box.addEventListener('mouseleave', function() {
        box.style.background = '#eee';
    });

    // mousemove -- يُطلق باستمرار اثناء تحرك الفارة فوق العنصر
    box.addEventListener('mousemove', function(event) {
        console.log('الفارة عند: ' + event.clientX + '، ' + event.clientY);
    });
</script>

احداث لوحة المفاتيح

مثال: احداث لوحة المفاتيح

<input type="text" id="searchInput" placeholder="اكتب شيئا...">

<script>
    const input = document.getElementById('searchInput');

    // keydown -- يُطلق عند الضغط على مفتاح
    input.addEventListener('keydown', function(event) {
        console.log('مفتاح مضغوط: ' + event.key);
    });

    // keyup -- يُطلق عند تحرير المفتاح
    input.addEventListener('keyup', function(event) {
        console.log('مفتاح محرر: ' + event.key);
        console.log('القيمة الحالية: ' + input.value);
    });

    // الاستماع لمفاتيح محددة
    input.addEventListener('keydown', function(event) {
        if (event.key === 'Enter') {
            console.log('تم الضغط على Enter! ابحث عن: ' + input.value);
        }
        if (event.key === 'Escape') {
            input.value = '';
            input.blur();
        }
    });
</script>

احداث النماذج

مثال: احداث النماذج

<form id="myForm">
    <input type="text" id="username" placeholder="اسم المستخدم">
    <select id="role">
        <option value="user">مستخدم</option>
        <option value="admin">مسؤول</option>
    </select>
    <input type="checkbox" id="agree"> اوافق
    <button type="submit">ارسال</button>
</form>

<script>
    const form = document.getElementById('myForm');
    const username = document.getElementById('username');
    const role = document.getElementById('role');
    const agree = document.getElementById('agree');

    // submit -- يُطلق عند ارسال النموذج
    form.addEventListener('submit', function(event) {
        event.preventDefault(); // منع اعادة تحميل الصفحة
        console.log('تم ارسال النموذج!');
        console.log('اسم المستخدم: ' + username.value);
        console.log('الدور: ' + role.value);
        console.log('الموافقة: ' + agree.checked);
    });

    // input -- يُطلق عند كل ضغطة مفتاح في حقول النص
    username.addEventListener('input', function(event) {
        console.log('الكتابة: ' + event.target.value);
    });

    // change -- يُطلق عند تغيير القيمة وفقدان العنصر للتركيز
    role.addEventListener('change', function(event) {
        console.log('تغير الدور الى: ' + event.target.value);
    });

    // change على خانة الاختيار -- يُطلق عند التحديد/الغاء التحديد
    agree.addEventListener('change', function(event) {
        console.log('الموافقة: ' + event.target.checked);
    });

    // focus و blur -- يُطلقان عند حصول/فقدان العنصر للتركيز
    username.addEventListener('focus', function() {
        username.style.borderColor = 'blue';
    });
    username.addEventListener('blur', function() {
        username.style.borderColor = '';
    });
</script>
نصيحة احترافية: حدث input يُطلق عند كل ضغطة مفتاح او لصق او اي تغيير في القيمة، مما يجعله مثاليا للتحقق في الوقت الحقيقي وميزات البحث اثناء الكتابة. حدث change يُطلق فقط عندما ينتهي المستخدم من التعديل وينتقل بعيدا عن الحقل (او يبدل خانة اختيار/زر راديو). اختر الحدث المناسب بناء على متى تريد ان تحدث الملاحظات.

احداث المستند والنافذة

مثال: احداث المستند والنافذة

<script>
    // DOMContentLoaded -- يُطلق عند تحليل HTML (قبل انتهاء تحميل الصور/CSS)
    document.addEventListener('DOMContentLoaded', function() {
        console.log('DOM جاهز!');
        // امن للاستعلام ومعالجة عناصر DOM هنا
        const heading = document.querySelector('h1');
        if (heading) {
            heading.textContent = 'تم تحميل الصفحة!';
        }
    });

    // load -- يُطلق عند تحميل جميع الموارد بالكامل (الصور، CSS، الاطارات)
    window.addEventListener('load', function() {
        console.log('كل شيء محمل بالكامل!');
    });

    // resize -- يُطلق عند تغيير حجم نافذة المتصفح
    window.addEventListener('resize', function() {
        console.log('حجم النافذة: ' + window.innerWidth + 'x' + window.innerHeight);
    });

    // scroll -- يُطلق عند تمرير الصفحة
    window.addEventListener('scroll', function() {
        console.log('موضع التمرير: ' + window.scrollY);
    });

    // beforeunload -- يُطلق قبل مغادرة المستخدم للصفحة
    window.addEventListener('beforeunload', function(event) {
        // عرض مربع حوار تاكيد (المتصفح يتحكم في الرسالة)
        event.preventDefault();
    });
</script>
ملاحظة: الفرق بين DOMContentLoaded وload مهم. DOMContentLoaded يُطلق بمجرد ان يتم تحليل مستند HTML بالكامل، وهذا يحدث قبل انتهاء تحميل الصور واوراق الانماط والاطارات الفرعية. هذا يجعله الحدث المثالي لتهيئة جافاسكريبت. حدث load ينتظر انتهاء كل شيء، وهو ما قد يكون ابطا بكثير في الصفحات ذات الصور الكثيرة.

كائن الحدث (Event Object)

في كل مرة يُطلق فيها حدث، ينشئ جافاسكريبت تلقائيا كائن حدث ويمرره الى دالة المعالج. هذا الكائن يحتوي على معلومات مفصلة عن الحدث: ما نوعه، واي عنصر اطلقه، وموضع الفارة، واي مفتاح تم الضغط عليه، وغير ذلك الكثير. الوصول الى هذا الكائن بسيط مثل اضافة معامل الى دالة المعالج.

مثال: الوصول الى كائن الحدث

<button id="infoBtn">انقر لمعلومات الحدث</button>

<script>
    const infoBtn = document.getElementById('infoBtn');

    infoBtn.addEventListener('click', function(event) {
        // معامل event يُوفر تلقائيا
        console.log('نوع الحدث: ' + event.type);           // "click"
        console.log('الطابع الزمني: ' + event.timeStamp);   // ملي ثانية منذ تحميل الصفحة
        console.log('هل موثوق: ' + event.isTrusted);        // true (اجراء مستخدم) او false (سكريبت)
        console.log('الزر المستخدم: ' + event.button);      // 0=ايسر، 1=اوسط، 2=ايمن
        console.log('Client X: ' + event.clientX);           // موضع X في نافذة العرض
        console.log('Client Y: ' + event.clientY);           // موضع Y في نافذة العرض
        console.log('Page X: ' + event.pageX);               // موضع X في المستند
        console.log('Page Y: ' + event.pageY);               // موضع Y في المستند
    });
</script>

event.target مقابل event.currentTarget

اثنتان من اهم الخصائص في كائن الحدث هما target وcurrentTarget. قد تبدوان متشابهتين لكنهما تخدمان اغراضا مختلفة جدا، والخلط بينهما مصدر شائع للاخطاء.

event.target هو العنصر الذي اطلق الحدث فعليا -- العنصر الاعمق الذي تم النقر عليه او الكتابة فيه او التفاعل معه. event.currentTarget هو العنصر الذي تم ربط مستمع الحدث به. عند استخدام تفويض الاحداث (ربط مستمع بعنصر اب)، غالبا ما يكون هذان العنصران مختلفين.

مثال: target مقابل currentTarget

<ul id="taskList">
    <li><span class="task-name">شراء البقالة</span> <button class="delete">X</button></li>
    <li><span class="task-name">تمشية الكلب</span> <button class="delete">X</button></li>
    <li><span class="task-name">قراءة كتاب</span> <button class="delete">X</button></li>
</ul>

<script>
    const taskList = document.getElementById('taskList');

    // المستمع على <ul>، لكن النقرات تحدث على العناصر الابناء
    taskList.addEventListener('click', function(event) {
        // currentTarget دائما <ul> (حيث تم ربط المستمع)
        console.log('currentTarget: ' + event.currentTarget.tagName); // "UL"

        // target هو ما تم النقر عليه فعليا
        console.log('target: ' + event.target.tagName);
        // قد يكون "BUTTON" او "SPAN" او "LI" حسب ما تم النقر عليه

        // استخدم target لتحديد الاجراء المطلوب
        if (event.target.classList.contains('delete')) {
            const listItem = event.target.closest('li');
            listItem.remove();
            console.log('تم حذف المهمة!');
        }
    });
</script>
نصيحة احترافية: عند العمل مع تفويض الاحداث، استخدم دائما event.target لتحديد ما تم النقر عليه وevent.currentTarget عندما تحتاج للاشارة الى العنصر المرتبط به المستمع. نمط شائع هو استخدام event.target.closest(selector) للعثور على السلف المناسب للعنصر المنقور عليه، مما يتعامل مع حالات نقر المستخدم على عنصر ابن داخل هدفك.

preventDefault: ايقاف السلوك الافتراضي للمتصفح

العديد من الاحداث لها سلوك افتراضي مبني في المتصفح. النقر على رابط ينتقل الى صفحة جديدة. ارسال نموذج يعيد تحميل الصفحة. الضغط على مفتاح في حقل ادخال يكتب حرفا. دالة event.preventDefault() توقف هذا السلوك الافتراضي مع السماح لكود جافاسكريبت بالعمل.

مثال: استخدام preventDefault

<!-- منع التنقل بالرابط -->
<a href="https://example.com" id="myLink">اذهب الى المثال</a>

<!-- منع ارسال النموذج -->
<form id="loginForm">
    <input type="text" id="user" placeholder="اسم المستخدم" required>
    <input type="password" id="pass" placeholder="كلمة المرور" required>
    <button type="submit">تسجيل الدخول</button>
</form>

<script>
    // منع الرابط من التنقل
    const link = document.getElementById('myLink');
    link.addEventListener('click', function(event) {
        event.preventDefault();
        console.log('تم منع النقر على الرابط. كان سينتقل الى: ' + link.href);
        // يمكنك الان التعامل مع التنقل بجافاسكريبت
    });

    // منع النموذج من اعادة تحميل الصفحة
    const loginForm = document.getElementById('loginForm');
    loginForm.addEventListener('submit', function(event) {
        event.preventDefault();

        const user = document.getElementById('user').value;
        const pass = document.getElementById('pass').value;

        // التحقق والارسال عبر جافاسكريبت بدلا من ذلك
        if (user.length < 3) {
            console.log('اسم المستخدم قصير جدا!');
            return;
        }

        console.log('الارسال عبر fetch...');
        // fetch('/api/login', { method: 'POST', body: ... })
    });
</script>

stopPropagation: التحكم في تدفق الاحداث

الاحداث في DOM تنتشر -- تنتقل عبر شجرة DOM في عملية تُسمى تصاعد الاحداث. عندما تنقر على زر داخل div داخل body، حدث النقر يُطلق على الزر اولا، ثم يتصاعد الى div، ثم الى body، ثم الى document. دالة event.stopPropagation() توقف هذا التصاعد، مما يمنع العناصر الاباء من استقبال الحدث.

مثال: stopPropagation

<div id="outer" style="padding:30px;background:#f0f0f0;">
    القسم الخارجي
    <div id="inner" style="padding:30px;background:#ddd;">
        القسم الداخلي
        <button id="deepBtn">زر عميق</button>
    </div>
</div>

<script>
    const outer = document.getElementById('outer');
    const inner = document.getElementById('inner');
    const deepBtn = document.getElementById('deepBtn');

    outer.addEventListener('click', function() {
        console.log('تم النقر على الخارجي');
    });

    inner.addEventListener('click', function() {
        console.log('تم النقر على الداخلي');
    });

    // بدون stopPropagation: النقر على الزر يطبع الثلاثة
    // مع stopPropagation: النقر على الزر يطبع فقط "تم النقر على الزر"
    deepBtn.addEventListener('click', function(event) {
        event.stopPropagation();
        console.log('تم النقر على الزر');
    });
</script>
خطا شائع: الافراط في استخدام stopPropagation() يمكن ان يكسر معالجات احداث اخرى اعلى في شجرة DOM التي تعتمد على تصاعد الاحداث. مثلا، القائمة المنسدلة التي تُغلق عند النقر خارجها تعتمد على تصاعد حدث النقر الى المستند. اذا اوقفت الانتشار على عنصر، قد لا تُغلق تلك القائمة ابدا. استخدم stopPropagation باعتدال وفقط عندما يكون لديك سبب واضح لمنع التصاعد.

ازالة مستمعي الاحداث

لازالة مستمع حدث، تستخدم دالة removeEventListener. المتطلب الاساسي هو انك يجب ان تمرر نفس مرجع الدالة بالضبط الذي استُخدم في addEventListener. هذا يعني ان الدوال المجهولة لا يمكن ازالتها لانه ليس لديك مرجع لتمريره. استخدم دائما دوال مسماة عندما تخطط لازالة المستمعين لاحقا.

مثال: ازالة مستمعي الاحداث

<button id="startBtn">بدء</button>
<button id="stopBtn">ايقاف</button>
<div id="output"></div>

<script>
    const startBtn = document.getElementById('startBtn');
    const stopBtn = document.getElementById('stopBtn');
    const output = document.getElementById('output');

    // دالة مسماة -- يمكن ازالتها لاحقا
    function handleMouseMove(event) {
        output.textContent = 'الفارة: ' + event.clientX + '، ' + event.clientY;
    }

    // اضافة المستمع
    startBtn.addEventListener('click', function() {
        document.addEventListener('mousemove', handleMouseMove);
        console.log('بدء تتبع الفارة');
    });

    // ازالة نفس مرجع الدالة بالضبط
    stopBtn.addEventListener('click', function() {
        document.removeEventListener('mousemove', handleMouseMove);
        output.textContent = 'تم ايقاف التتبع';
        console.log('تم ايقاف تتبع الفارة');
    });

    // تحذير: هذا لن يعمل!
    // document.addEventListener('click', function() { console.log('A'); });
    // document.removeEventListener('click', function() { console.log('A'); });
    // هاتان دالتان مجهولتان مختلفتان، حتى لو كان الكود متطابقا
</script>
ملاحظة: حتى لو كان لدالتين مجهولتين كود متطابق، فهما كائنان مختلفان في الذاكرة. جافاسكريبت يقارن الدوال بالمرجع وليس بكود المصدر. لهذا السبب removeEventListener يعمل فقط مع مراجع دوال مسماة. عرّف دائما معالجك كدالة مسماة او خزّنه في متغير اذا كنت تحتاج لازالته لاحقا.

خيار once

احيانا تريد ان يعمل معالج الحدث مرة واحدة فقط ثم يزيل نفسه تلقائيا. بدلا من استدعاء removeEventListener يدويا داخل المعالج، يمكنك تمرير خيار once. هذا انظف واقل عرضة للاخطاء.

مثال: استخدام خيار once

<button id="welcomeBtn">عرض رسالة الترحيب</button>
<button id="loadMore">تحميل المزيد من البيانات</button>

<script>
    // هذا المعالج يعمل مرة واحدة فقط ثم يزيل نفسه تلقائيا
    const welcomeBtn = document.getElementById('welcomeBtn');
    welcomeBtn.addEventListener('click', function() {
        alert('مرحبا! هذه الرسالة تظهر مرة واحدة فقط.');
    }, { once: true });
    // النقر على الزر مرة اخرى بعد المرة الاولى لا يفعل شيئا

    // استخدام عملي: تحميل بيانات لمرة واحدة
    const loadMore = document.getElementById('loadMore');
    loadMore.addEventListener('click', function() {
        console.log('تحميل البيانات الاولية...');
        loadMore.textContent = 'تم تحميل البيانات';
        loadMore.disabled = true;
    }, { once: true });
</script>

خيار passive

خيار passive يخبر المتصفح ان معالج الحدث لن يستدعي ابدا preventDefault(). هذا تحسين في الاداء، مهم بشكل خاص لاحداث scroll وtouchmove. عندما يعرف المتصفح انك لن تمنع السلوك الافتراضي، يمكنه بدء التمرير فورا دون انتظار تنفيذ جافاسكريبت، مما ينتج عنه اداء تمرير اكثر سلاسة.

مثال: استخدام خيار passive

<script>
    // مستمع تمرير سلبي -- المتصفح يمكنه التمرير فورا
    window.addEventListener('scroll', function() {
        // فعل شيء خفيف مثل تحديث شريط تقدم
        const scrollPercent = (window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100;
        console.log('التمرير: ' + Math.round(scrollPercent) + '%');
    }, { passive: true });

    // مستمع لمس سلبي للجوال -- تمرير لمسي اكثر سلاسة
    document.addEventListener('touchmove', function(event) {
        // تتبع موضع اللمس بدون حظر التمرير
        console.log('اللمس عند: ' + event.touches[0].clientY);
    }, { passive: true });

    // يمكنك الجمع بين الخيارات
    document.addEventListener('wheel', function(event) {
        console.log('دلتا العجلة: ' + event.deltaY);
    }, { passive: true, capture: false });

    // تحذير: اذا حاولت استدعاء preventDefault() في مستمع سلبي،
    // المتصفح سيتجاهله ويسجل تحذيرا في وحدة التحكم
</script>
نصيحة احترافية: المتصفحات الحديثة تعين تلقائيا passive: true لاحداث touchstart وtouchmove على مستوى المستند. ومع ذلك، تعيين { passive: true } بشكل صريح على مستمعي التمرير واللمس هو ممارسة افضل لانه يوثق نيتك ويضمن سلوكا متسقا عبر جميع المتصفحات وجميع العناصر.

مستمعون متعددون على نفس العنصر

احدى اعظم مزايا addEventListener هي انك تستطيع ربط عدة مستمعين من نفس نوع الحدث بنفس العنصر. كل مستمع يعمل بشكل مستقل وبالترتيب الذي تم تسجيله به. هذا يسمح لاجزاء مختلفة من كودك بالاستجابة لنفس الحدث دون تعارض مع بعضها.

مثال: مستمعون متعددون

<button id="multiBtn">زر متعدد المعالجات</button>

<script>
    const multiBtn = document.getElementById('multiBtn');

    // المعالج 1: تسجيل الحدث
    multiBtn.addEventListener('click', function(event) {
        console.log('المعالج 1: تسجيل النقر عند ' + event.timeStamp);
    });

    // المعالج 2: تحديث واجهة المستخدم
    multiBtn.addEventListener('click', function() {
        multiBtn.textContent = 'تم النقر!';
    });

    // المعالج 3: تتبع التحليلات
    multiBtn.addEventListener('click', function() {
        console.log('المعالج 3: ارسال حدث تحليلات');
        // sendAnalytics('button_click', { button: 'multiBtn' });
    });

    // المعالج 4: ملاحظات بصرية
    multiBtn.addEventListener('click', function() {
        multiBtn.style.transform = 'scale(0.95)';
        setTimeout(function() {
            multiBtn.style.transform = 'scale(1)';
        }, 150);
    });

    // جميع المعالجات الاربعة تعمل بالترتيب: 1، 2، 3، 4
</script>

مثال عملي: نقر الزر مع حالة التحميل

نمط شائع في تطبيقات الويب هو عرض حالة تحميل عند النقر على زر، وتنفيذ عملية غير متزامنة، ثم استعادة الزر. هذا يوضح عدة مفاهيم احداث تعمل معا.

مثال: نقر الزر مع حالة التحميل

<button id="saveBtn" class="btn-save">حفظ التغييرات</button>
<div id="status"></div>

<script>
    const saveBtn = document.getElementById('saveBtn');
    const status = document.getElementById('status');

    saveBtn.addEventListener('click', function(event) {
        // منع النقرات المزدوجة
        if (saveBtn.disabled) return;

        // عرض حالة التحميل
        const originalText = saveBtn.textContent;
        saveBtn.textContent = 'جاري الحفظ...';
        saveBtn.disabled = true;

        // محاكاة عملية غير متزامنة (مثل طلب fetch)
        setTimeout(function() {
            // استعادة حالة الزر
            saveBtn.textContent = originalText;
            saveBtn.disabled = false;

            // عرض رسالة نجاح
            status.textContent = 'تم حفظ التغييرات بنجاح!';
            status.style.color = 'green';

            // مسح الحالة بعد 3 ثوان
            setTimeout(function() {
                status.textContent = '';
            }, 3000);
        }, 2000);
    });
</script>

مثال عملي: التحقق من النموذج وارساله

معالجة النماذج هي احد اكثر استخدامات الاحداث شيوعا. هذا المثال يوضح كيفية التحقق من نموذج في الوقت الحقيقي باستخدام احداث input ومعالجة الارسال باستخدام حدث submit، مع منع اعادة تحميل الصفحة الافتراضية.

مثال: معالجة نموذج كاملة

<form id="registrationForm">
    <div class="form-group">
        <label for="regEmail">البريد الالكتروني</label>
        <input type="email" id="regEmail" required>
        <span id="emailError" class="error"></span>
    </div>
    <div class="form-group">
        <label for="regPassword">كلمة المرور</label>
        <input type="password" id="regPassword" required minlength="8">
        <span id="passError" class="error"></span>
    </div>
    <button type="submit" id="regSubmit">تسجيل</button>
</form>

<script>
    const regForm = document.getElementById('registrationForm');
    const regEmail = document.getElementById('regEmail');
    const regPassword = document.getElementById('regPassword');
    const emailError = document.getElementById('emailError');
    const passError = document.getElementById('passError');

    // التحقق من البريد الالكتروني في الوقت الحقيقي باستخدام حدث input
    regEmail.addEventListener('input', function(event) {
        const value = event.target.value;
        if (value && !value.includes('@')) {
            emailError.textContent = 'الرجاء تضمين رمز @';
        } else if (value && !value.includes('.')) {
            emailError.textContent = 'الرجاء تضمين نطاق صالح';
        } else {
            emailError.textContent = '';
        }
    });

    // التحقق من كلمة المرور في الوقت الحقيقي
    regPassword.addEventListener('input', function(event) {
        const value = event.target.value;
        if (value.length > 0 && value.length < 8) {
            passError.textContent = 'يجب ان تكون كلمة المرور 8 احرف على الاقل';
        } else {
            passError.textContent = '';
        }
    });

    // ارسال النموذج
    regForm.addEventListener('submit', function(event) {
        event.preventDefault();

        // التحقق النهائي
        const email = regEmail.value.trim();
        const password = regPassword.value;
        let isValid = true;

        if (!email.includes('@') || !email.includes('.')) {
            emailError.textContent = 'الرجاء ادخال بريد الكتروني صالح';
            isValid = false;
        }

        if (password.length < 8) {
            passError.textContent = 'يجب ان تكون كلمة المرور 8 احرف على الاقل';
            isValid = false;
        }

        if (isValid) {
            console.log('النموذج صالح! جاري الارسال...');
            console.log('البريد الالكتروني: ' + email);
            // في تطبيق حقيقي: fetch('/api/register', { ... })
        }
    });
</script>

مثال عملي: تهيئة تحميل الصفحة

تهيئة تطبيقك بشكل صحيح عند تحميل الصفحة امر بالغ الاهمية. استخدام DOMContentLoaded يضمن ان جافاسكريبت يعمل في الوقت المناسب، بعد جاهزية HTML ولكن قبل انتهاء تحميل جميع الموارد الخارجية.

مثال: تهيئة تحميل الصفحة

<script>
    // تهيئة التطبيق عندما يكون DOM جاهزا
    document.addEventListener('DOMContentLoaded', function() {
        console.log('جاري تهيئة التطبيق...');

        // اعداد التنقل
        const navLinks = document.querySelectorAll('.nav-link');
        navLinks.forEach(function(link) {
            link.addEventListener('click', function(event) {
                event.preventDefault();
                navLinks.forEach(function(l) { l.classList.remove('active'); });
                link.classList.add('active');
                console.log('التنقل الى: ' + link.getAttribute('href'));
            });
        });

        // اعداد زر العودة للاعلى
        const scrollTopBtn = document.getElementById('scrollTop');
        if (scrollTopBtn) {
            window.addEventListener('scroll', function() {
                if (window.scrollY > 300) {
                    scrollTopBtn.style.display = 'block';
                } else {
                    scrollTopBtn.style.display = 'none';
                }
            }, { passive: true });

            scrollTopBtn.addEventListener('click', function() {
                window.scrollTo({ top: 0, behavior: 'smooth' });
            });
        }

        // اعداد معالج تغيير الحجم مع debounce
        let resizeTimer;
        window.addEventListener('resize', function() {
            clearTimeout(resizeTimer);
            resizeTimer = setTimeout(function() {
                console.log('استقر الحجم عند: ' + window.innerWidth + 'x' + window.innerHeight);
                // تحديث حسابات التخطيط هنا
            }, 250);
        });

        console.log('تمت تهيئة التطبيق بنجاح!');
    });
</script>
نصيحة احترافية: لاحظ نمط debounce على حدث resize. احداث مثل resize وscroll تُطلق عدة مرات في الثانية. تشغيل عمليات مكلفة عند كل اطلاق سيسبب مشاكل في الاداء. نمط debounce ينتظر حتى يتوقف المستخدم عن تغيير الحجم لمدة محددة قبل تشغيل كودك. هذا نمط اداء اساسي لتطبيقات الانتاج.

مثال عملي: محتوى ديناميكي مع تفويض الاحداث

تفويض الاحداث هو نمط حيث تربط مستمع حدث واحد بعنصر اب للتعامل مع الاحداث من ابنائه، بما في ذلك الابناء الذين يُضافون ديناميكيا بعد اعداد المستمع. هذا يعمل بسبب تصاعد الاحداث -- الاحداث المُطلقة على العناصر الابناء تتصاعد الى العناصر الاباء.

مثال: قائمة مهام مع تفويض الاحداث

<div id="todoApp">
    <input type="text" id="todoInput" placeholder="اضف مهمة جديدة...">
    <button id="addTodo">اضافة</button>
    <ul id="todoList">
        <li>
            <span class="todo-text">تعلم احداث جافاسكريبت</span>
            <button class="complete-btn">تم</button>
            <button class="delete-btn">حذف</button>
        </li>
    </ul>
</div>

<script>
    const todoInput = document.getElementById('todoInput');
    const addTodo = document.getElementById('addTodo');
    const todoList = document.getElementById('todoList');

    // اضافة مهمة جديدة
    addTodo.addEventListener('click', function() {
        const text = todoInput.value.trim();
        if (!text) return;

        const li = document.createElement('li');
        li.innerHTML = '<span class="todo-text">' + text + '</span> ' +
                       '<button class="complete-btn">تم</button> ' +
                       '<button class="delete-btn">حذف</button>';
        todoList.appendChild(li);
        todoInput.value = '';
        todoInput.focus();
    });

    // التعامل مع مفتاح Enter في الادخال
    todoInput.addEventListener('keydown', function(event) {
        if (event.key === 'Enter') {
            addTodo.click();
        }
    });

    // تفويض الاحداث: مستمع واحد يتعامل مع جميع ازرار عناصر المهام
    // هذا يعمل للعناصر الحالية والمستقبلية
    todoList.addEventListener('click', function(event) {
        // التعامل مع زر الاكمال
        if (event.target.classList.contains('complete-btn')) {
            const todoText = event.target.previousElementSibling;
            todoText.style.textDecoration = 'line-through';
            todoText.style.opacity = '0.5';
            event.target.disabled = true;
        }

        // التعامل مع زر الحذف
        if (event.target.classList.contains('delete-btn')) {
            const listItem = event.target.closest('li');
            listItem.remove();
        }
    });
</script>

ملخص خيارات الاحداث

اليك مرجع كامل لجميع الخيارات التي يمكنك تمريرها الى addEventListener والخصائص الرئيسية لكائن الحدث.

مرجع سريع: خيارات addEventListener وخصائص الحدث

// خيارات addEventListener
element.addEventListener('click', handler, {
    once: true,      // المعالج يعمل مرة واحدة ثم يُزال تلقائيا
    passive: true,   // وعد بعدم استدعاء preventDefault()
    capture: true    // الاستماع اثناء مرحلة الالتقاط (من الاعلى للاسفل) بدلا من مرحلة التصاعد
});

// خصائص كائن الحدث الرئيسية
event.type              // نص نوع الحدث: "click"، "submit"، "keydown"، الخ
event.target            // العنصر الذي اطلق الحدث
event.currentTarget     // العنصر المرتبط به المستمع
event.timeStamp         // متى حدث الحدث (ملي ثانية منذ تحميل الصفحة)
event.isTrusted         // true اذا بادر به المستخدم، false اذا بادر به السكريبت

// دوال كائن الحدث الرئيسية
event.preventDefault()     // ايقاف السلوك الافتراضي للمتصفح
event.stopPropagation()    // ايقاف الحدث من التصاعد

// خصائص حدث الفارة
event.clientX / event.clientY   // الموضع بالنسبة لنافذة العرض
event.pageX / event.pageY       // الموضع بالنسبة للمستند
event.button                    // اي زر فارة (0=ايسر، 1=اوسط، 2=ايمن)

// خصائص حدث لوحة المفاتيح
event.key           // قيمة المفتاح: "Enter"، "Escape"، "a"، "A"، الخ
event.code          // رمز المفتاح الفيزيائي: "KeyA"، "Enter"، "Space"، الخ
event.altKey        // true اذا مفتاح Alt مضغوط
event.ctrlKey       // true اذا مفتاح Ctrl مضغوط
event.shiftKey      // true اذا مفتاح Shift مضغوط
event.metaKey       // true اذا مفتاح Meta (Cmd/Windows) مضغوط

// ازالة المستمعين
element.removeEventListener('click', namedFunction);
// يجب تمرير نفس مرجع الدالة بالضبط المستخدم في addEventListener

تمرين عملي

ابنِ صفحة تفاعلية كاملة توضح اتقانك للاحداث. انشئ ما يلي: (1) عداد احرف لمنطقة نص يتحدث في الوقت الحقيقي باستخدام حدث input، يعرض كم حرف متبقي من حد 280 حرف، مع تحول العداد للون الاحمر عندما يتبقى اقل من 20 حرف. (2) نموذج اتصال بثلاثة حقول (الاسم، البريد الالكتروني، الرسالة) يتحقق من كل حقل باستخدام احداث input للملاحظات في الوقت الحقيقي وحدث submit للتحقق النهائي، باستخدام preventDefault لمنع النموذج من اعادة تحميل الصفحة. (3) قائمة عناصر حيث يمكن اضافة عناصر جديدة عبر حقل نص ومفتاح Enter، وكل عنصر له زر حذف -- استخدم تفويض الاحداث بمستمع واحد على القائمة الاب. (4) زر "العودة للاعلى" يظهر فقط بعد التمرير لاسفل 200 بكسل، باستخدام حدث scroll مع خيار passive. (5) غلّف كل كود التهيئة داخل مستمع DOMContentLoaded. (6) اضف اشعار ترحيب لمرة واحدة باستخدام خيار once يظهر عندما ينقر المستخدم لاول مرة في اي مكان على الصفحة. اختبر جميع التفاعلات، وتحقق من ان event.target وevent.currentTarget تعملان كما هو متوقع في معالج التفويض، وتاكد من ان ازالة مستمع حدث توقف السلوك المرتبط بشكل صحيح.