أساسيات JavaScript

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

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

مقدمة في احداث ادخال المستخدم

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

نظرة عامة على احداث لوحة المفاتيح

يوفر JavaScript ثلاثة احداث للوحة المفاتيح تنطلق بترتيب محدد عندما يضغط المستخدم على مفتاح:

  • keydown -- ينطلق عند الضغط على مفتاح. هذا هو حدث لوحة المفاتيح الاكثر استخداما. ينطلق بشكل متكرر اذا تم الضغط المستمر على المفتاح.
  • keypress -- ينطلق بعد keydown للمفاتيح التي تنتج قيمة حرف. هذا الحدث مهمل ويجب عدم استخدامه في الكود الجديد. لا ينطلق لمفاتيح غير الاحرف مثل Shift و Ctrl ومفاتيح الاسهم.
  • keyup -- ينطلق عند تحرير المفتاح. ينطلق مرة واحدة لكل تحرير مفتاح بغض النظر عن مدة الضغط.

ترتيب الانطلاق هو: keydown اولا ثم keypress (اذا كان قابلا للتطبيق) ثم keyup عند تحرير المفتاح. للتطوير الحديث يجب استخدام keydown وkeyup حصريا وتجنب keypress تماما.

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

const input = document.getElementById('search-input');

input.addEventListener('keydown', function(event) {
    console.log('keydown:', event.key, 'code:', event.code);
});

// مهمل -- تجنبه في الكود الجديد
input.addEventListener('keypress', function(event) {
    console.log('keypress:', event.key);
});

input.addEventListener('keyup', function(event) {
    console.log('keyup:', event.key, 'code:', event.code);
});
مهم: حدث keypress مهمل رسميا في مواصفات DOM. يتصرف بشكل غير متسق عبر المتصفحات ولا ينطلق للعديد من المفاتيح مثل Enter و Escape و Backspace ومفاتيح الاسهم في بعض المتصفحات. استخدم دائما keydown او keyup بدلا منه.

خصائص KeyboardEvent

يوفر كائن KeyboardEvent عدة خصائص مهمة لتحديد المفتاح الذي تم ضغطه والمفاتيح المعدلة التي كانت نشطة وقت الحدث:

خاصية key

خاصية event.key تعيد قيمة المفتاح الذي تم ضغطه. للاحرف القابلة للطباعة تعيد الحرف نفسه (مثل "a" او "A" او "1" او "!"). للمفاتيح الخاصة تعيد سلسلة وصفية مثل "Enter" او "Escape" او "ArrowUp" او "Shift" او "Control" او "Tab". قيمة key تاخذ بعين الاعتبار المفاتيح المعدلة -- الضغط على Shift + a يعطي "A" وليس "a".

خاصية code

خاصية event.code تعيد سلسلة تمثل المفتاح الفعلي على لوحة المفاتيح بغض النظر عن تخطيط لوحة المفاتيح الحالي او حالة المعدل. على سبيل المثال الضغط على المفتاح المسمى "A" على لوحة مفاتيح QWERTY يعيد دائما "KeyA" سواء تم الضغط على Shift ام لا. هذا مفيد للالعاب واختصارات لوحة المفاتيح حيث تريد موضع المفتاح الفعلي بدلا من الحرف الذي ينتجه.

مثال: الفرق بين key و code

document.addEventListener('keydown', function(event) {
    console.log('key:', event.key);   // "a" او "A" حسب Shift
    console.log('code:', event.code); // دائما "KeyA" لمفتاح A

    // رموز المفاتيح الفعلية للمفاتيح الخاصة
    // "Enter"      -> code: "Enter"
    // "Space"      -> code: "Space"
    // "ArrowUp"    -> code: "ArrowUp"
    // "Digit1"     -> code: "Digit1"
    // "ShiftLeft"  -> code: "ShiftLeft"
    // "ShiftRight" -> code: "ShiftRight"
});
ملاحظة: الخصائص القديمة event.keyCode وevent.which مهملة. تعيد رموزا رقمية تختلف عبر المتصفحات وتخطيطات لوحة المفاتيح. استخدم دائما event.key او event.code في JavaScript الحديث.

خصائص المفاتيح المعدلة

يتضمن كائن KeyboardEvent خصائص منطقية للتحقق مما اذا كانت مفاتيح التعديل مضغوطة اثناء الحدث:

  • event.ctrlKey -- true اذا كان مفتاح Ctrl مضغوطا.
  • event.shiftKey -- true اذا كان مفتاح Shift مضغوطا.
  • event.altKey -- true اذا كان مفتاح Alt (او Option على Mac) مضغوطا.
  • event.metaKey -- true اذا كان مفتاح Meta مضغوطا (مفتاح Windows على الكمبيوتر او مفتاح Command على Mac).

مثال: اكتشاف المفاتيح المعدلة

document.addEventListener('keydown', function(event) {
    if (event.ctrlKey) {
        console.log('Ctrl مضغوط');
    }
    if (event.shiftKey) {
        console.log('Shift مضغوط');
    }
    if (event.altKey) {
        console.log('Alt/Option مضغوط');
    }
    if (event.metaKey) {
        console.log('Meta/Command مضغوط');
    }
    // مثال مجمع: Ctrl + Shift + S
    if (event.ctrlKey && event.shiftKey && event.key === 'S') {
        console.log('تم ضغط Ctrl+Shift+S');
    }
});

خاصية repeat

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

مثال: اكتشاف تكرار المفتاح

document.addEventListener('keydown', function(event) {
    if (event.repeat) {
        console.log('المفتاح مضغوط باستمرار:', event.key);
        return; // تخطي الاحداث المتكررة
    }
    console.log('ضغطة مفتاح اولية:', event.key);
});

بناء اختصارات لوحة المفاتيح

تحسن اختصارات لوحة المفاتيح تجربة المستخدم بالسماح للمستخدمين المتقدمين بتنفيذ الاجراءات بسرعة. عند بناء اختصارات لوحة المفاتيح يجب دمج فحوصات المفاتيح المعدلة مع قيم مفاتيح محددة. استدعِ دائما event.preventDefault() لمنع المتصفح من تنفيذ الاجراء الافتراضي لتلك المجموعة.

مثال: تنفيذ اختصارات لوحة المفاتيح

document.addEventListener('keydown', function(event) {
    // Ctrl+S (او Cmd+S على Mac) للحفظ
    if ((event.ctrlKey || event.metaKey) && event.key === 's') {
        event.preventDefault();
        saveDocument();
        console.log('تم حفظ المستند!');
    }

    // Ctrl+K لفتح البحث
    if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
        event.preventDefault();
        openSearchModal();
    }

    // Escape لاغلاق النوافذ المنبثقة
    if (event.key === 'Escape') {
        closeAllModals();
    }

    // Ctrl+Z للتراجع
    if ((event.ctrlKey || event.metaKey) && event.key === 'z') {
        event.preventDefault();
        if (event.shiftKey) {
            redo(); // Ctrl+Shift+Z للاعادة
        } else {
            undo();
        }
    }

    // Ctrl+Enter لارسال النموذج
    if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
        event.preventDefault();
        submitForm();
    }
});

function saveDocument() { /* منطق الحفظ */ }
function openSearchModal() { /* منطق النافذة */ }
function closeAllModals() { /* منطق الاغلاق */ }
function undo() { /* منطق التراجع */ }
function redo() { /* منطق الاعادة */ }
function submitForm() { /* منطق الارسال */ }
نصيحة احترافية: عند تنفيذ اختصارات لوحة المفاتيح تحقق دائما من event.ctrlKey وevent.metaKey معا لدعم مستخدمي Windows/Linux (الذين يستخدمون Ctrl) و macOS (الذين يستخدمون Command). هذا يضمن عمل اختصاراتك بشكل متسق عبر جميع انظمة التشغيل.

التنقل بلوحة المفاتيح

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

مثال: التنقل بمفاتيح الاسهم في قائمة

const menuItems = document.querySelectorAll('.menu-item');
let currentIndex = 0;

document.addEventListener('keydown', function(event) {
    if (event.key === 'ArrowDown') {
        event.preventDefault();
        currentIndex = (currentIndex + 1) % menuItems.length;
        menuItems[currentIndex].focus();
    }

    if (event.key === 'ArrowUp') {
        event.preventDefault();
        currentIndex = (currentIndex - 1 + menuItems.length) % menuItems.length;
        menuItems[currentIndex].focus();
    }

    if (event.key === 'Home') {
        event.preventDefault();
        currentIndex = 0;
        menuItems[currentIndex].focus();
    }

    if (event.key === 'End') {
        event.preventDefault();
        currentIndex = menuItems.length - 1;
        menuItems[currentIndex].focus();
    }

    if (event.key === 'Enter' || event.key === ' ') {
        event.preventDefault();
        menuItems[currentIndex].click();
    }
});

نظرة عامة على احداث الفارة

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

  • click -- ينطلق عندما ينقر المستخدم (ضغط وتحرير) بزر الفارة الاساسي على عنصر.
  • dblclick -- ينطلق عندما ينقر المستخدم نقرا مزدوجا بزر الفارة الاساسي.
  • mousedown -- ينطلق عند الضغط على اي زر فارة على عنصر.
  • mouseup -- ينطلق عند تحرير زر الفارة فوق عنصر.
  • mousemove -- ينطلق باستمرار اثناء تحرك مؤشر الفارة فوق عنصر.
  • mouseenter -- ينطلق مرة واحدة عندما يدخل مؤشر الفارة حدود عنصر. لا يتصاعد في DOM.
  • mouseleave -- ينطلق مرة واحدة عندما يغادر مؤشر الفارة حدود عنصر. لا يتصاعد في DOM.
  • mouseover -- ينطلق عندما يدخل مؤشر الفارة عنصرا او اي من عناصره الفرعية. يتصاعد في DOM.
  • mouseout -- ينطلق عندما يغادر مؤشر الفارة عنصرا او ينتقل الى عنصر فرعي. يتصاعد في DOM.
  • contextmenu -- ينطلق عندما ينقر المستخدم بالزر الايمن على عنصر (قبل ظهور قائمة السياق).

مثال: الاستماع لاحداث الفارة الشائعة

const box = document.getElementById('interactive-box');

box.addEventListener('click', function(event) {
    console.log('تم النقر!');
});

box.addEventListener('dblclick', function(event) {
    console.log('تم النقر المزدوج!');
});

box.addEventListener('mousedown', function(event) {
    console.log('تم ضغط زر الفارة');
});

box.addEventListener('mouseup', function(event) {
    console.log('تم تحرير زر الفارة');
});

box.addEventListener('mousemove', function(event) {
    console.log('الفارة تتحرك عند:', event.clientX, event.clientY);
});

box.addEventListener('mouseenter', function(event) {
    console.log('دخلت الفارة الصندوق');
});

box.addEventListener('mouseleave', function(event) {
    console.log('غادرت الفارة الصندوق');
});

mouseenter/mouseleave مقابل mouseover/mouseout

هذا التمييز حاسم وغالبا ما يسبب ارتباكا. احداث mouseenter وmouseleave تنطلق فقط عندما يدخل المؤشر او يغادر العنصر المستهدف نفسه. لا تتصاعد ولا تنطلق عند التنقل بين العناصر الفرعية. في المقابل mouseover وmouseout تتصاعد وتنطلق عند التنقل بين العناصر الفرعية داخل العنصر المستهدف مما قد يسبب تاثيرات وميض اذا لم يتم التعامل معها بشكل صحيح.

مثال: سلوك mouseenter مقابل mouseover

// بنية HTML:
// <div id="parent">
//     <span id="child">عنصر فرعي</span>
// </div>

const parent = document.getElementById('parent');

// mouseenter ينطلق مرة واحدة عند دخول الاب
parent.addEventListener('mouseenter', function() {
    console.log('mouseenter على الاب');
});

// mouseover ينطلق عند دخول الاب وعند دخول الابن
parent.addEventListener('mouseover', function(event) {
    console.log('mouseover على:', event.target.id);
    // يسجل "parent" عند دخول الاب
    // يسجل "child" عند دخول الابن
});

// لتاثيرات التحويم استخدم mouseenter/mouseleave
// فهي ابسط واكثر قابلية للتنبؤ

خصائص MouseEvent

يوفر كائن MouseEvent معلومات مفصلة عن موضع الفارة والازرار التي تم ضغطها. فهم انظمة الاحداثيات المختلفة ضروري لبناء الميزات التفاعلية.

خصائص موضع الفارة

  • event.clientX / event.clientY -- موضع الفارة بالنسبة لنافذة العرض في المتصفح (المنطقة المرئية من الصفحة). هذه القيم لا تتغير عند تمرير الصفحة.
  • event.pageX / event.pageY -- موضع الفارة بالنسبة للمستند بالكامل بما في ذلك الجزء المُمرر. اذا تم تمرير الصفحة 200 بكسل للاسفل فان pageY ستكون اكثر بـ 200 من clientY لنفس موضع المؤشر الفعلي.
  • event.offsetX / event.offsetY -- موضع الفارة بالنسبة للزاوية العلوية اليسرى من العنصر المستهدف. هذا مفيد جدا لتطبيقات الرسم والالعاب حيث تحتاج احداثيات داخل عنصر محدد.
  • event.screenX / event.screenY -- موضع الفارة بالنسبة للشاشة الفعلية. نادرا ما يستخدم في تطوير الويب.

مثال: مقارنة خصائص موضع الفارة

const canvas = document.getElementById('drawing-area');

canvas.addEventListener('mousemove', function(event) {
    console.log('موضع نافذة العرض:', event.clientX, event.clientY);
    console.log('موضع المستند:', event.pageX, event.pageY);
    console.log('موضع العنصر:', event.offsetX, event.offsetY);
    console.log('موضع الشاشة:', event.screenX, event.screenY);
});

// استخدام عملي: عرض تلميح يتبع الفارة
const tooltip = document.getElementById('tooltip');
document.addEventListener('mousemove', function(event) {
    tooltip.style.left = event.pageX + 15 + 'px';
    tooltip.style.top = event.pageY + 15 + 'px';
});

خصائص ازرار الفارة

خاصية event.button تخبرك اي زر اطلق الحدث. القيم هي:

  • 0 -- الزر الاساسي (عادة النقر الايسر)
  • 1 -- الزر الاوسط (نقر عجلة التمرير)
  • 2 -- الزر الثانوي (عادة النقر الايمن)
  • 3 -- الزر الرابع (عادة رجوع المتصفح)
  • 4 -- الزر الخامس (عادة تقدم المتصفح)

خاصية event.buttons (لاحظ صيغة الجمع) هي قناع بتات يخبرك بالازرار المضغوطة حاليا اثناء حدث mousemove. هذا يختلف عن event.button الذي يخبرك فقط بالزر الذي اطلق الحدث.

مثال: اكتشاف زر الفارة الذي تم نقره

document.addEventListener('mousedown', function(event) {
    switch (event.button) {
        case 0:
            console.log('تم ضغط الزر الايسر');
            break;
        case 1:
            console.log('تم ضغط الزر الاوسط');
            break;
        case 2:
            console.log('تم ضغط الزر الايمن');
            break;
    }
});

// استخدام event.buttons اثناء mousemove
document.addEventListener('mousemove', function(event) {
    // event.buttons هو قناع بتات:
    // 1 = اساسي (ايسر)، 2 = ثانوي (ايمن)، 4 = اوسط
    if (event.buttons & 1) {
        console.log('الزر الايسر مضغوط اثناء التحريك');
    }
    if (event.buttons & 2) {
        console.log('الزر الايمن مضغوط اثناء التحريك');
    }
});

قائمة سياق مخصصة

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

مثال: بناء قائمة سياق مخصصة

const customMenu = document.getElementById('custom-menu');

document.addEventListener('contextmenu', function(event) {
    event.preventDefault();

    customMenu.style.display = 'block';
    customMenu.style.left = event.pageX + 'px';
    customMenu.style.top = event.pageY + 'px';
});

// اخفاء القائمة المخصصة عند النقر في مكان اخر
document.addEventListener('click', function() {
    customMenu.style.display = 'none';
});

// اخفاء عند ضغط Escape
document.addEventListener('keydown', function(event) {
    if (event.key === 'Escape') {
        customMenu.style.display = 'none';
    }
});

// التعامل مع نقرات عناصر القائمة
customMenu.addEventListener('click', function(event) {
    const action = event.target.dataset.action;
    if (action === 'copy') {
        console.log('تم تفعيل اجراء النسخ');
    } else if (action === 'paste') {
        console.log('تم تفعيل اجراء اللصق');
    } else if (action === 'delete') {
        console.log('تم تفعيل اجراء الحذف');
    }
    customMenu.style.display = 'none';
});

بناء لوحة رسم

دمج احداث mousedown وmousemove وmouseup يتيح لك بناء وظيفة الرسم. هذا نمط واقعي شائع يستخدم في لوحات التوقيع والسبورات البيضاء وادوات التصميم. المفتاح هو تتبع ما اذا كان المستخدم يرسم حاليا (زر الفارة مضغوط) والتقاط موضع الفارة لرسم الخطوط.

مثال: لوحة رسم بسيطة باحداث الفارة

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let isDrawing = false;
let lastX = 0;
let lastY = 0;

canvas.addEventListener('mousedown', function(event) {
    isDrawing = true;
    lastX = event.offsetX;
    lastY = event.offsetY;
});

canvas.addEventListener('mousemove', function(event) {
    if (!isDrawing) return;

    ctx.beginPath();
    ctx.moveTo(lastX, lastY);
    ctx.lineTo(event.offsetX, event.offsetY);
    ctx.strokeStyle = '#333';
    ctx.lineWidth = 3;
    ctx.lineCap = 'round';
    ctx.stroke();

    lastX = event.offsetX;
    lastY = event.offsetY;
});

canvas.addEventListener('mouseup', function() {
    isDrawing = false;
});

canvas.addEventListener('mouseleave', function() {
    isDrawing = false;
});

اكتشاف السحب

اكتشاف عمليات السحب يتطلب دمج احداث mousedown وmousemove وmouseup مع تتبع الموضع. النمط الشائع هو تعيين حد ادنى للمسافة قبل اعتبار الحركة سحبا للتمييز بين السحب والنقرات العادية.

مثال: تنفيذ اكتشاف السحب مع عتبة

const draggable = document.getElementById('draggable-element');
let isDragging = false;
let startX = 0;
let startY = 0;
const DRAG_THRESHOLD = 5; // الحد الادنى للبكسلات لاعتباره سحبا

draggable.addEventListener('mousedown', function(event) {
    startX = event.clientX;
    startY = event.clientY;
    isDragging = false;

    function onMouseMove(e) {
        const dx = e.clientX - startX;
        const dy = e.clientY - startY;
        const distance = Math.sqrt(dx * dx + dy * dy);

        if (distance > DRAG_THRESHOLD) {
            isDragging = true;
            draggable.style.transform =
                'translate(' + dx + 'px, ' + dy + 'px)';
        }
    }

    function onMouseUp() {
        document.removeEventListener('mousemove', onMouseMove);
        document.removeEventListener('mouseup', onMouseUp);

        if (isDragging) {
            console.log('اكتمل السحب');
        } else {
            console.log('كانت نقرة وليست سحبا');
        }
    }

    document.addEventListener('mousemove', onMouseMove);
    document.addEventListener('mouseup', onMouseUp);
});
نصيحة احترافية: اربط دائما مستمعي mousemove وmouseup بـ document بدلا من العنصر الذي يتم سحبه. اذا ربطتها بالعنصر تتوقف الاحداث عن الانطلاق عندما تتحرك الفارة اسرع مما يمكن للعنصر متابعته مما يتسبب في كسر السحب.

مقدمة في احداث المؤشر

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

  • pointerdown -- يحل محل mousedown وtouchstart
  • pointerup -- يحل محل mouseup وtouchend
  • pointermove -- يحل محل mousemove وtouchmove
  • pointerenter -- يحل محل mouseenter
  • pointerleave -- يحل محل mouseleave
  • pointerover -- يحل محل mouseover
  • pointerout -- يحل محل mouseout
  • pointercancel -- ينطلق عند الغاء تفاعل المؤشر

مثال: استخدام احداث المؤشر لدعم الاجهزة المتعددة

const element = document.getElementById('interactive');

element.addEventListener('pointerdown', function(event) {
    console.log('نوع المؤشر:', event.pointerType); // "mouse" او "touch" او "pen"
    console.log('معرف المؤشر:', event.pointerId);
    console.log('الضغط:', event.pressure); // 0 الى 1 للمس/القلم
    console.log('العرض:', event.width);   // عرض منطقة التلامس
    console.log('الارتفاع:', event.height); // ارتفاع منطقة التلامس
    element.setPointerCapture(event.pointerId);
});

element.addEventListener('pointermove', function(event) {
    console.log('يتحرك عند:', event.clientX, event.clientY);
});

element.addEventListener('pointerup', function(event) {
    element.releasePointerCapture(event.pointerId);
    console.log('تم تحرير المؤشر');
});
ملاحظة: تتضمن احداث المؤشر خصائص اضافية غير متوفرة في احداث الفارة مثل pointerType (الذي يخبرك ان كان الادخال من فارة او لمس او قلم) وpressure (مفيد لتطبيقات الرسم) وpointerId (الذي يتيح تتبع لمسات متعددة متزامنة). اذا كنت بحاجة لدعم اللمس المتعدد او ادخال الاستايلس فاحداث المؤشر هي النهج الموصى به.

اساسيات احداث اللمس

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

  • touchstart -- ينطلق عندما يلمس اصبع الشاشة.
  • touchmove -- ينطلق باستمرار اثناء تحرك الاصبع عبر الشاشة.
  • touchend -- ينطلق عند رفع الاصبع عن الشاشة.
  • touchcancel -- ينطلق عند مقاطعة تفاعل اللمس (مثلا بمكالمة هاتفية).

توفر احداث اللمس خاصية touches التي تحتوي على قائمة بجميع نقاط اللمس الحالية وخاصية targetTouches للمسات على العنصر المستهدف وخاصية changedTouches للمسات التي تغيرت في هذا الحدث.

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

const touchArea = document.getElementById('touch-area');

touchArea.addEventListener('touchstart', function(event) {
    event.preventDefault(); // منع التمرير
    const touch = event.touches[0];
    console.log('بدا اللمس عند:', touch.clientX, touch.clientY);
    console.log('عدد الاصابع:', event.touches.length);
});

touchArea.addEventListener('touchmove', function(event) {
    event.preventDefault();
    const touch = event.touches[0];
    console.log('اللمس يتحرك عند:', touch.clientX, touch.clientY);
});

touchArea.addEventListener('touchend', function(event) {
    const touch = event.changedTouches[0];
    console.log('انتهى اللمس عند:', touch.clientX, touch.clientY);
});

مثال واقعي: لوحة تبويب قابلة للتنقل بلوحة المفاتيح

لنبنِ لوحة تبويب كاملة الوصول تتبع ممارسات WAI-ARIA. يمكن للمستخدمين التنقل بين علامات التبويب بمفاتيح الاسهم وتفعيلها بـ Enter او Space.

مثال: لوحة تبويب سهلة الوصول مع تنقل بلوحة المفاتيح

// بنية HTML:
// <div role="tablist">
//     <button role="tab" id="tab-1" aria-selected="true">تبويب 1</button>
//     <button role="tab" id="tab-2" aria-selected="false">تبويب 2</button>
//     <button role="tab" id="tab-3" aria-selected="false">تبويب 3</button>
// </div>
// <div role="tabpanel" id="panel-1">المحتوى 1</div>
// <div role="tabpanel" id="panel-2" hidden>المحتوى 2</div>
// <div role="tabpanel" id="panel-3" hidden>المحتوى 3</div>

const tabs = document.querySelectorAll('[role="tab"]');
const panels = document.querySelectorAll('[role="tabpanel"]');

function activateTab(tab) {
    // الغاء تفعيل جميع التبويبات
    tabs.forEach(function(t) {
        t.setAttribute('aria-selected', 'false');
        t.setAttribute('tabindex', '-1');
    });
    panels.forEach(function(p) {
        p.hidden = true;
    });

    // تفعيل التبويب المحدد
    tab.setAttribute('aria-selected', 'true');
    tab.setAttribute('tabindex', '0');
    tab.focus();

    const panelId = tab.id.replace('tab', 'panel');
    document.getElementById(panelId).hidden = false;
}

tabs.forEach(function(tab, index) {
    tab.addEventListener('keydown', function(event) {
        let newIndex;
        if (event.key === 'ArrowRight') {
            newIndex = (index + 1) % tabs.length;
        } else if (event.key === 'ArrowLeft') {
            newIndex = (index - 1 + tabs.length) % tabs.length;
        } else if (event.key === 'Home') {
            newIndex = 0;
        } else if (event.key === 'End') {
            newIndex = tabs.length - 1;
        } else {
            return;
        }
        event.preventDefault();
        activateTab(tabs[newIndex]);
    });

    tab.addEventListener('click', function() {
        activateTab(tab);
    });
});

مثال واقعي: تلميح تفاعلي عند التحويم

اليك مثال عملي يجمع بين تتبع موضع الفارة واحداث mouseenter وmouseleave لانشاء تلميح ديناميكي يتبع المؤشر عند التحويم فوق عناصر لها سمة data-tooltip.

مثال: تلميح ديناميكي يتبع الفارة

// انشاء عنصر التلميح
const tooltip = document.createElement('div');
tooltip.className = 'custom-tooltip';
tooltip.style.position = 'absolute';
tooltip.style.display = 'none';
tooltip.style.pointerEvents = 'none';
document.body.appendChild(tooltip);

// اضافة مستمعين لجميع العناصر التي لها سمة data-tooltip
const targets = document.querySelectorAll('[data-tooltip]');

targets.forEach(function(target) {
    target.addEventListener('mouseenter', function() {
        tooltip.textContent = this.dataset.tooltip;
        tooltip.style.display = 'block';
    });

    target.addEventListener('mousemove', function(event) {
        tooltip.style.left = event.pageX + 12 + 'px';
        tooltip.style.top = event.pageY + 12 + 'px';
    });

    target.addEventListener('mouseleave', function() {
        tooltip.style.display = 'none';
    });
});

منع السلوك الافتراضي وانتشار الاحداث

عند العمل مع احداث لوحة المفاتيح والفارة غالبا ما تحتاج لمنع السلوك الافتراضي للمتصفح او ايقاف انتشار الحدث في شجرة DOM. استخدم event.preventDefault() لايقاف الاجراءات الافتراضية (مثل ارسال النموذج عند Enter او تمرير الصفحة عند Space) وevent.stopPropagation() لايقاف تصاعد الحدث الى العناصر الاب.

مثال: منع السلوك الافتراضي في التطبيق العملي

// منع ارسال النموذج عند Enter في حقل البحث
const searchField = document.getElementById('search');
searchField.addEventListener('keydown', function(event) {
    if (event.key === 'Enter') {
        event.preventDefault();
        performSearch(this.value);
    }
});

// منع تمرير الصفحة عند ضغط Space على زر مخصص
const customButton = document.getElementById('play-btn');
customButton.addEventListener('keydown', function(event) {
    if (event.key === ' ') {
        event.preventDefault();
        togglePlayback();
    }
});

// منع تحديد النص عند النقر المزدوج
const panel = document.getElementById('resize-panel');
panel.addEventListener('mousedown', function(event) {
    if (event.detail > 1) {
        event.preventDefault(); // يمنع تحديد النص بالنقر المزدوج
    }
});

function performSearch(query) { console.log('جاري البحث:', query); }
function togglePlayback() { console.log('تم تبديل التشغيل'); }
خطا شائع: الافراط في استخدام event.preventDefault() على احداث لوحة المفاتيح يمكن ان يكسر سلوك المتصفح الاصلي وامكانية الوصول. على سبيل المثال منع مفتاح Tab من العمل يزيل التنقل بلوحة المفاتيح تماما. امنع السلوك الافتراضي فقط عندما يكون لديك سبب محدد وتاكد دائما من وجود طريقة بديلة للمستخدمين لانجاز الاجراء.

اعتبارات الاداء لاحداث الفارة

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

مثال: خنق mousemove للاداء

// الاستراتيجية 1: استخدام requestAnimationFrame
let ticking = false;
let mouseX = 0;
let mouseY = 0;

document.addEventListener('mousemove', function(event) {
    mouseX = event.clientX;
    mouseY = event.clientY;

    if (!ticking) {
        requestAnimationFrame(function() {
            // تنفيذ التحديث المكلف هنا
            updateElementPosition(mouseX, mouseY);
            ticking = false;
        });
        ticking = true;
    }
});

// الاستراتيجية 2: الخنق بفاصل زمني
function throttle(func, limit) {
    let inThrottle = false;
    return function() {
        if (!inThrottle) {
            func.apply(this, arguments);
            inThrottle = true;
            setTimeout(function() {
                inThrottle = false;
            }, limit);
        }
    };
}

document.addEventListener('mousemove', throttle(function(event) {
    console.log('الموضع المخنوق:', event.clientX, event.clientY);
}, 50)); // ينفذ كل 50 مللي ثانية كحد اقصى

function updateElementPosition(x, y) {
    const el = document.getElementById('follower');
    el.style.left = x + 'px';
    el.style.top = y + 'px';
}

تمرين عملي

ابنِ تطبيق ويب تفاعلي يستخدم كلا من احداث لوحة المفاتيح والفارة. انشئ صفحة بعنصر canvas بحجم 400 في 400 بكسل. نفذ الميزات التالية: (1) يمكن للمستخدمين الرسم على اللوحة بالضغط المستمر على زر الفارة الايسر وتحريك الفارة. (2) ضغط مفتاح C يمسح اللوحة. (3) ضغط مفاتيح الارقام من 1 الى 5 يغير لون الفرشاة الى خمسة الوان مختلفة من اختيارك. (4) الضغط المستمر على Shift اثناء الرسم يجعل حجم الفرشاة اكبر. (5) النقر بالزر الايمن على اللوحة يعرض قائمة سياق مخصصة بخيارات المسح والحفظ كصورة وتغيير لون الخلفية. (6) اعرض احداثيات الفارة الحالية بالنسبة للوحة في شريط حالة اسفل اللوحة. (7) اضف اختصار لوحة المفاتيح Ctrl+Z للتراجع عن اخر خط. خزن كل خط كمصفوفة من النقاط واعد رسم جميع الخطوط ناقص الاخير عند التراجع. اختبر ان تطبيقك يعمل بسلاسة وان جميع اختصارات لوحة المفاتيح تعمل بشكل صحيح.