اذهب إلى المحتوى

مدخل إلى اﻷحداث في جافاسكريبت


ابراهيم الخضور

تُعرَّف اﻷحداث أنها أفعال أو ظواهر تحدث في النظام الذي تبرمجه، ويخبرك بها هذا النظام كي تستجيب لها بطريقة مناسبة إن أردت. فلو نقر مثلًا مستخدم زرًا في صفحة ويب، قد ترغب في الاستجابة لهذا الحدث بعرض رسالة معينة. وما نناقشه في هذا المقال هي بعض المفاهيم المهمة المتعلقة باﻷحداث وكيفية عملها في المتصفح والتعامل معها في جافا سكريبت.

ننصحك قبل أن تبدأ العمل معنا في هذه السلسلة أن تطلع على:

  1. أساسيات علوم الحاسب.
  2. أساسيات HTML.
  3. أساسيات عمل CSS
  4. أساسيات جافاسكريبت كما شرحناها في سلسلة المقالات السابقة.

ما هي اﻷحداث؟

تُعرَّف اﻷحداث events أنها أفعال أو ظواهر تحدث في النظام الذي تبرمجه، ينتج عن النظام (أو يحرّض) إشارة من نوع ما عند وقوع هذا الفعل أو الظاهرة، ويزوّدك بآلية لتحديد اﻹجراءات التي تتُخذ تلقائيًا (تشغيل شيفرة معينة) عند وقوعها. تقع اﻷحداث ضمن نافذة المتصفح وُربط أغلب اﻷحيان بعنصر محدد ضمن النافذة، قد يكون العنصر مفردًا أو مجموعة من العناصر أو صفحة HTML حُمِّلت ضمن النافذة الحالية أو نافذة المتصفح بأكملها. وستجد أنواعًا مختلفة من اﻷحداث التي تقع مثل:

  • اختيار المستخدم عنصرًًا أو النقر عليه أو تحريك مؤشر الفأرة فوقه.
  • اختيار المستخدم مفتاحًا من لوحة المفاتيح.
  • تغيير المستخدم حجم نافذة المتصفح أو إغلاقها.
  • إنهاء تحميل صفحة ويب.
  • تشغيل فيديو أو إيقافه أو انتهائه.
  • وقوع خطأ.

إذًا هناك الكثير من اﻷحداث التي يمكن ترصدها.

ولكي تستجيب لحدث ما، نربطه بما يُدعى معالج حدث event handler، وهو كتلة من الشيفرة (دالة جافاسكريبت عادة) تُنفَّذ عندما يقع الحدث. وعندما تُعرف كتلة برمجية كهذه كي تعمل استجابة لوقوع حدث ما، نقول أننا سجلنا معالج حدث. وتجدر الملاحظة أن معالج الحدث يُسمى أحيانًا مترصد حدث event listener، وسنتستخدم المصطلحين معًا ويعملان معًا. فالمترصد هو من يكتشف وقوع الحدث والمعالج هو الشيفرة التي تُنفّذ استجابة له.

ملاحظة: لا تُعد أحداث الويب جزءًا من بنية جافاسكريبت، بل كجزء من الواجهة البرمجية المدمجة مع المتصفح.

مثال: التعامل مع حدث النقر على عنصر

لدينا في المثال التالي صفحة HTML تضم زرًا <button> واحدًا:

<button>Change color</button>

سنعود إليه لاحقًا بعد كتابة شيفرة جافا سكريبت في القسم التالي، لكن ما يهمنا حاليًا هو إضافة معالج حدث "النقر click" إلى الزر وسيتفاعل المتصفح مع هذا الحدث بانتقاء لون عشوائي للصفحة:

const btn = document.querySelector("button");

function random(number) {
  return Math.floor(Math.random() * (number + 1));
}

btn.addEventListener("click", () => {
  const rndCol = `rgb(${random(255)}, ${random(255)}, ${random(255)})`;
  document.body.style.backgroundColor = rndCol;
});

سيكون خرج المثال كالتالي:

استخدام التابع ()addEventListener

رأينا في المثال السابق أن العناصر التي يمكنها إطلاق حدث، تمتلك التابع ()addEventListener وهو اﻵلية التي ننصح بها ﻹضافة معالج حدث. لنلق نظرة أقرب إلى شيفرة المثال السابق:

const btn = document.querySelector("button");

function random(number) {
  return Math.floor(Math.random() * (number + 1));
}

btn.addEventListener("click", () => {
  const rndCol = `rgb(${random(255)}, ${random(255)}, ${random(255)})`;
  document.body.style.backgroundColor = rndCol;
});

سيحرّض المستخدم عندما ينقر على الزر <button> حدثًا، لذلك عرّفنا دالة ()addEventListener نستدعيها عند وقوع الحدث ونمرر لها معاملين هما:

  • القيمة النصية "click"ونحدد فيها أن الحدث الذي نترصده هو حدث النقر. ويمكن للأزرار أن تطلق العديد من اﻷحداث مثل mouseover عندما يحرّك المستخدم الفأرة فوق الزر أو keydown عندما يضغط على مفتاحًا ويكون تركيز الدخل على الزر.
  • دالة نستدعيها عند وقوع الحدث، وفي حالتنا تولّد هذه الدالة لون RGB عشوائي وتضبط قيمة الخاصية background-color للعنصر <body> على قيمة اللون تلك.

ولا بأس أن تفصل دالة الحدث باسم خاص بها كالتالي:

const btn = document.querySelector("button");

function random(number) {
  return Math.floor(Math.random() * (number + 1));
}

function changeBackground() {
  const rndCol = `rgb(${random(255)}, ${random(255)}, ${random(255)})`;
  document.body.style.backgroundColor = rndCol;
}

btn.addEventListener("click", changeBackground);

ترصّد اﻷحداث

ستجد العديد من اﻷحداث التي يمكن للزر أن يطلقها، ولكي نختبر ذلك، انسخ الملف إلى جهازك وافتحه ضمن المتصفح. يمثل الملف نسخة عن مثال اللون العشوائي السابق. حاول اﻵن تغيير الحدث click إلى القيم المختلفة التالية على التوالي وراقب النتيجة:

  • focus و blur: يتغير لون خلفية الصفحة عندما يتلقى الزر تركيز الدخل أو يفقد. حاول أن تنقل تركيز الدخل إلى الزر باستخدام المفتاح Tab ثم اضغط على المفتاح مجددًا ﻹبعاد تركيز الدخل. تُستخدم هذه اﻷحداث لعرض معلومات عن ملء نموذج بالبيانات عندما ينتقل التركيز إلى حقل نصي أو زر، أو لعرض رسالة خطأ عند ملئ أحد حقول النموذج بقيمة خاطئة.
  • dblclick: يتغير اللون عندما تنقر الزر نقرًا مزدوجًا.
  • mouseover أو mouseout: يتغير لون الخلفة عندما تمرر مؤشر الفأرة فوق الزر أو عندما يبتعد مؤشر الفأرة عن الزر.

وبعض اﻷحداث مثل متاح تقريبًا لجميع العناصر، بينما يكون بعضها مخصصًا لحالات محددة. فالحدث play متاح مثلًا لبعض العناصر مثل العنصر <video.

إزالة مترصد حدث

عندما تضيف مترصد حدث باستخدام الدالة ()addEventListener، بإمكانك إزالته باستخدام التابع removeEventHandler. ستزيل الشيفرة التالية معالج الحدث ()changeBackgroud:

btn.removeEventListener("click", changeBackground);

كما يزال المعالج بتمرير إشارة إيقاف AbortSignal إلى الدالة ()addEventListener ومن ثم استدعاء التابع ()abort العائد للمتحكم controller الذي يمتلك إشارة اﻹيقاف لاحقًا.

const controller = new AbortController();

btn.addEventListener("click",
  () => {
    const rndCol = `rgb(${random(255)}, ${random(255)}, ${random(255)})`;
    document.body.style.backgroundColor = rndCol;
  },
  { signal: controller.signal } // تمرير إشارة إيقاف إلى المعالج
);

عندها يمكن إزالة معالج الحدث الذي تنتجه الشيفرة السابقة كالتالي:

controller.abort(); // يزيل أي أو كل معالج حدث مرتبط بهذا المتحكم

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

إضافة أكثر من مترصد لنفس الحدث

إن أردت استدعاء الدالة ()addEventListener أكثر من مرة وتزويدها بأكثر من معالج حدث لنفس الحدث، يمكنك استخدام أكثر من معالج كالتالي:

myElement.addEventListener("click", functionA);
myElement.addEventListener("click", functionB);

وهكذا ستُنفَّذ كلا الدالتين في المعالجين السابقين عندما يُنقر الزر.

عُد إلى موسوعة حسوب إن أردت الاطلاع على ميزات وخيرات أخرى مفيدة للدالة ()addEventListener.

آليات اخرى لاستخدام مترصد الحدث

ننصح دومًا باستخدام ()addEventListener لتسجيل معالجات اﻷحداث، فهي الطريقة اﻷقوى وتتلائم جيدًا مع البرامج المعقدة. مع ذلك، هنالك طريقتان إضافيتين لتسجيل معالج الحدث قد تصادفهما وهما استعمال الخاصية الموافقة للحدث أو معالج الحدث السطري inline event handler.

خاصيات معالج الحدث

للكائنات التي تطلق أحداثًا مثل اﻷزرار خاصيات تبدأ أسماؤها بالسابقة on يليها اسم معالج الحدث، مثل الخاصية onclick. تُدعى هذه الخاصيات بخاصيات معالج الحدث. ولكي تترصد حدثًا، أسند دالة معالج الحدث إلى هذه الخاصية كما في المثال التالي:

const btn = document.querySelector("button");

function random(number) {
  return Math.floor(Math.random() * (number + 1));
}

btn.onclick = () => {
  const rndCol = `rgb(${random(255)}, ${random(255)}, ${random(255)})`;
  document.body.style.backgroundColor = rndCol;
};

وتستطيع أيضًا إسناد اسم الدالة إلى الخاصية كالتالي إن كانت دالة المعالج مسماة:

const btn = document.querySelector("button");

function random(number) {
  return Math.floor(Math.random() * (number + 1));
}

function bgChange() {
  const rndCol = `rgb(${random(255)}, ${random(255)}, ${random(255)})`;
  document.body.style.backgroundColor = rndCol;
}

btn.onclick = bgChange;

ولا يمكن بالطبع إسناد أكثر من معالج حدث إلى حدث ما باستخدام الخاصيات، إذ يمكنك مثلًا استدعاء الدالة (click',handler')addEventListener من قبل عنصر عدة مرات وبتمرير دوال معالجة مختلفة كوسيط ثانِ:

element.addEventListener("click", function1);
element.addEventListener("click", function2);

لكن ذلك مستحيل التنفيذ باستخدام الخاصيات لأن الإسناد الثاني سيلغي اﻷول:

element.onclick = function1;
element.onclick = function2;

معالجات الأحداث السطرية

لا تستخدم هذه الطريقة، لكنك قد تصادفها:

<button onclick="bgChange()">Press me</button>

إليك شيفرة جافاسكريبت

function bgChange() {
  const rndCol = `rgb(${random(255)}, ${random(255)}, ${random(255)})`;
  document.body.style.backgroundColor = rndCol;
}

ظهرت هذه الطريقة في مراحل مبكرة وتضمنت استخدام سمات HTML التي تحدد معالج الحدث (المعالجات السطرية inline event handler) كما تعرضه الشيفرة السابقة. وتحمل هذه السمة حرفيًا شيفرة جافاسكريبت التي تريد تنفيذها عند وقوع الحدث. نستدعي في المثال السابق دالة معرّفة ضمن العنصر <script> في نفس الصفحة، كما يمكنك إدراج شيفرة جافاسكريبت مباشرة ضمن السمة كالتالي:

<button onclick="alert('Hello, this is my old-fashioned event handler!');">
  Press me
</button>

قد تجد سمات HTML تكافئ العديد من خاصيات معالجات اﻷحداث، لكن لا ينبغي استخدامها لأنها ممارسة عملية سيئة. وقد يبدو استخدام معالج أحداث ضمن السمة سهلًا عند تنفيذ أمر سريع، لكنها ستغدو صعبة اﻹدارة وغير فعّالة.

والسبب في ذلك من ناحية أولى أن دمج توصيف الملف HTML مع جافاسكريبت أمر غير عملي لما يسببه صعوبة في قراءة وفهم الصفحات، لهذا ينبغي فصل شيفرة جافاسكريبت في ملف خاص بها وهذا عمليًا أمر جيد ﻹمكانية إدراج نفس الملف في صفحات مختلفة.

وحتى لو استخدمت ملفًا واحدًا، لا يُعد استخدام معالجات الأحداث السطرية فكرة جيدة، فقد ينفع الأمر إن كان هناك زر واحد، لكن ماذا لو ضمت الصفحة 100 زر؟ عليك حينها إضافة 100 سمة إلى الملف، وستكون إدارتها وصيانتها كابوسًا. أما عند استخدام جافاسكريبت، ستتمكن من إضافة دالة معالج الحدث إلى كل اﻷزرار في الصفحة مهما كان عددها وباستخدام شيفرة كهذه:

const buttons = document.querySelectorAll("button");

for (const button of buttons) {
  button.addEventListener("click", bgChange);
}

ومن ناحية أخرى لا تسمح الكثير من إعدادات الخوادم باستخدام شيفرة جافاسكريبت سطرية، لأسباب تتعلق باﻷمان. لهذا لا يجب استخدام سمات HTML المتعلقة بمعالجات اﻷحداث، فهي قديمة واستخدامها عادة سيئة.

كائنات الحدث

قد تجد ضمن دالة الحث معاملات خاصة تحمل أسماء مثل event أو evt أو e، تُدعى هذه المعاملات كائنات حدث event object، وتمرر تلقائيًا إلى المعالجات لتزويدها بميزات ومعلومات إضافية. لنُعِد على سبيل المثال كتابة مثال اﻷلون العشوائية السابق مع بعض الاختلاف:

const btn = document.querySelector("button");

function random(number) {
  return Math.floor(Math.random() * (number + 1));
}

function bgChange(e) {
  const rndCol = `rgb(${random(255)}, ${random(255)}, ${random(255)})`;
  e.target.style.backgroundColor = rndCol;
  console.log(e);
}

btn.addEventListener("click", bgChange);

ملاحظة: يمكنك إيجاد الشيفرة الكاملة لهذا المثال على جت-هب (جرّب الشيفرة مباشرة أيضًا).

لاحظ كيف أضفنا هنا كائن أحداث e في الدالة، ثم استخدمنا ضمن الدالة الخاصية target للكائن كالتالي e.target لضبط لون خلفية الصفحة، حيث تشير هذه الخاصية دائمًا إلى العنصر الذي يقع عليه الحدث. لذا نضبط في هذا المثال خلفية لونية عشوائية للزر وليس للصفحة لأن target تستهدف الزر فقط.

ملاحظة: بإمكانك استخدام أي اسم تريده لكائن الحدث، لكن عليك استخدام اسم تتذكره عند استخدامه ضمن دالة الحدث. يستخدم المطورون عادة أسماء مثل event أو evt أو e لأنها قصيرة وسهلة التذكر. والأهم أن تحافظ على تناسق التسميات سواء في مشاريعك الشخصية أو المشتركة.

خاصيات إضافية لكائن اﻷحداث

لبعض كائنات الأحداث خاصيات إضافية تتعلق بالطبيعة الخاصة لكل حدث. إذ يقع الحدث keydown مثلًا عندما يضغط المستخدم على مفتاح، لهذا يضم كائن اﻷحداث الموافق له keyboardEvent الخاصية key التي تعطيك الزر الذي ضغطه المستخدم:

const textBox = document.querySelector("#textBox");
const output = document.querySelector("#output");
textBox.addEventListener("keydown", (event) => {
  output.textContent = `You pressed "${event.key}".`;
});

حاول استخدام لوحة المفاتيح لكتابة شيء ما وراقب ما يحدث:

إيقاف السلوك الافتراضي

تضطر في بعض المواقف إلى منع الحدث من تنفيذ وظيفته الافتراضية. ومن اﻷمثلة الشائعة عن ذلك حالة نموذج ويب web form مثل نموذج تسجيل مستخدم جديد. فعندما تملأ الحقول وتنقر زر "تسليم submit"، سيكون السلوك الافتراضي هو رفع البيانات إلى صفحة محددة على الخادم لمعالجتها، ويُعاد توجيه المتصفح لعرض رسالة تشير إلى نجاح التسليم ضمن صفحة جديدة أو في نفس الصفحة.

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

لنلق نظرة على المثال التالي:

إليك أولًا ملف htmL بسيط تحتاجه لإدخال الاسم اﻷول واﻷخير للمستخدم:

<form>
  <div>
    <label for="fname">First name: </label>
    <input id="fname" type="text" />
  </div>
  <div>
    <label for="lname">Last name: </label>
    <input id="lname" type="text" />
  </div>
  <div>
    <input id="submit" type="submit" />
  </div>
</form>
<p></p>

إليك ثانيًا شيفرة جافاسكريبت التي نتحقق فيها من الحدث submit بشكل مبسط (يُطلق هذا الحدث ضمن النموذج <form> عندما يُرسل إلى الخادم) وذلك إن كانت المربعات النصية في النموذج فارغة. فإن كانت كذلك، نستدعي الدالة ()preventDefault العائدة لكائن الحدث ﻹيقاف عملية اﻹرسال ومن ثم نعرض رسالة خطأ في الفقرة النصية أسفل النموذج ﻹبلاغ المستخدم بالخطأ:

const form = document.querySelector("form");
const fname = document.getElementById("fname");
const lname = document.getElementById("lname");
const para = document.querySelector("p");

form.addEventListener("submit", (e) => {
  if (fname.value === "" || lname.value === "") {
    e.preventDefault();
    para.textContent = "You need to fill in both names!";
  }
});

من الواضح أنها طريقة ضعيفة في التحقق، فهي لن تمنع المستخدم من إدخال فراغات أو أرقام في حقول النموذج، لكنها مناسبة لمثالنا. سيكون الخرج كالتالي:

ملاحظة: يمكنك إيجاد الشيفرة الكاملة لهذا المثال على جت-هب (جرّب الشيفرة مباشرة أيضًا).

رفع اﻷحداث

يُقصد برفع اﻷحداث bubbling كيفية تعامل المتصفح مع اﻷحداث المستهدفة لعناصر متداخلة.

إعداد مترصد لعنصر أب

تأمل صفحة ويب لها الهيكلية التالية:

<div id="container">
  <button>Click me!</button>
</div>
<pre id="output"></pre>

إن الزر هنا داخل العنصر <div> الذي يمثل العنصر اﻷب له. ما الذي سيحدث إن أضفنا معالج حدث نقر إلى العنصر اﻷب ثم نقرنا الزر؟

const output = document.querySelector("#output");
function handleClick(e) {
  output.textContent += `You clicked on a ${e.currentTarget.tagName} element\n`;
}

const container = document.querySelector("#container");
container.addEventListener("click", handleClick);

لاحظ كيف يطلق العنصر اﻷب الحدث عندما ينقر المستخدم الزر:

You clicked on a DIV element

إن اﻷمر منطقي، فالزر ضمن العنصر <div> وعندما تنقر هذا العنصر فانت تنقر على الزر ضمنًا.

مثال عن رفع الحدث

ما الذي قد يحدث إن أضفنا مترصد الحدث إلى الزر وليس إلى العنصر اﻷب؟

<body>
  <div id="container">
    <button>Click me!</button>
  </div>
  <pre id="output"></pre>
</body>

لنجرّب إضافة حدث نقر إلى الزر (الموجود ضمن <div> وكلاهما ضمن العنصر <body>?

const output = document.querySelector("#output");
function handleClick(e) {
  output.textContent += `You clicked on a ${e.currentTarget.tagName} element\n`;
}

const container = document.querySelector("#container");
const button = document.querySelector("button");

document.body.addEventListener("click", handleClick);
container.addEventListener("click", handleClick);
button.addEventListener("click", handleClick);

سترى أن العناصر الثلاث جميعها ستطلق الحدث عندما ينقر المستخدم على الزر:

You clicked on a BUTTON element
You clicked on a DIV element
You clicked on a BODY element

في هذه الحالة:

  • يُطلق حدث النقر على الزر أولًا.
  • يليه حدث النقر على العنصر اﻷب (العنصر <div>).
  • يليه حدث النقر على العنصر اﻷب للعنصر <div> (العنصر <div>).

نصف ذلك برفع الحدث من العنصر الأعمق إلى الأب اﻷول. يمكن ان يكون هذا السلوك مفيدًا وقد يسبب مشاكل غير متوقعة، سنرى مثالًا عن هذه المشاكل في فقرات لاحقة وسنجد لها حلًا.

مثال عن مشغّل فيديو

تضم صفحتنا في هذا المثال فيديو أُخفي عمدًا وزر عنوانه "Display vedio". ونريد ان نظهر السلوك التفاعلي التالي:

  • عندما ينقر المستخدم على الزر، يُعرض الصندوق الذي يضم الفيديو، لكنه لا يُشغّل المقطع.
  • عندما ينقر المستخدم على الفيديو، يبدأ العرض.
  • عندما ينقر المستخدم في أي مكان في الصندوق خارج الفيديو يختفي الصندوق.

إليك شيفرة HTML:

<button>Display video</button>

<div class="hidden">
  <video>
    <source
      src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm"
      type="video/webm" />
    <p>
      Your browser doesn't support HTML video. Here is a
      <a href="rabbit320.mp4">link to the video</a> instead.
    </p>
  </video>
</div>

تتضمن الشيفرة:

  • زر <button>.
  • عنصر حاوية <div> له السمة "class="hidden.
  • عنصر <video> ضمن العنصر <div>.

أما شيفرة جافاسكريبت فهي كالتالي:

const btn = document.querySelector("button");
const box = document.querySelector("div");
const video = document.querySelector("video");

btn.addEventListener("click", () => box.classList.remove("hidden"));
video.addEventListener("click", () => video.play());
box.addEventListener("click", () => box.classList.add("hidden"));

تضيف الشيفرة ثلاثة مترصدين للحدث 'click':

  • اﻷول للزر <button> الذي يعرض الحاوية التي تضم الفيديو.
  • والثاني للفيديو <video> لكي يُشغّله.
  • والثالث للحاوية <div> لكي يخفي الفيديو.

راقب كيف يجري اﻷمر:

سترى عند النقر على الزر كيف يظهر الصندوق والفيديو، لكن عند النقر على الفيديو سيعمل ويختفي الصندوق مجددًا. ولأن الفيديو داخل الحاوية، سيسبب نقره تنفيذ كلا معالجي اﻷحداث وظهور السلوك السابق.

إصلاح المشكلة باستخدام الدالة ()stoppropogation

قد يسبب رفع اﻷحداث كما رأينا سابقًا بعض المشاكل، لكن بالطبع توجد طريقة لمنع اﻷمر. إذ يُقدّم كائن اﻷحداث Event دالة تُدعى ()stopPropogation والتي تمنع عند استدعائها من داخل معالج الحدث من رفع الحدث إلى عناصر أعلى. ولكي نصلح المشكلة التي ظهرت معنا في المثال السابق، عدّل شيفرة جافاسكريبت كالتالي:

const btn = document.querySelector("button");
const box = document.querySelector("div");
const video = document.querySelector("video");

btn.addEventListener("click", () => box.classList.remove("hidden"));

video.addEventListener("click", (event) => {
  event.stopPropagation();
  video.play();
});

box.addEventListener("click", () => box.classList.add("hidden"));

كل ما فعلناه هنا هو استدعاء الدالة ()stopPropogation العائدة لكائن اﻷحداث داخل معالج الحدث الذي يتعامل مع الحدث 'click' الخاص بالعنصر <video>. سيمنع ذلك الحدث من الارتفاع إلى الصندوق. جرّب أن تنقر على الزر ثم على الفيديو:

التقاط الحدث

إن الشكل اﻵخر لانتقال اﻷحداث هو التقاط الحدث event capture. واﻷمر مشابه لرفع الحدث لكن بترتيب معكوس. فبدلًا من اطلاق الحدث أولًا من قبل العنصر اﻷدنى في التسلسل الهرمي ثم ينتقل الحدث صعودًا إلى العنصر اﻷعلى مستوىً، يحدث العكس ويطلق الحدث العنصر اﻷعلى مستوى ويهبط وصولًا إلى العنصر اﻷدنى مستوىً.

هذا الخيار معطّل افتراضيًا وعليك أن تمرر الخيار capture إلى الدالة ()addEventListener.

يعرض المثال التالي نفس المثال السابق عن رفع اﻷحداث لكن سنستخدم فيه الخيار capture. إليك شيفرة HTML:

<body>
  <div id="container">
    <button>Click me!</button>
  </div>
  <pre id="output"></pre>
</body>

وشيفرة جافاسكريت:

const output = document.querySelector("#output");
function handleClick(e) {
  output.textContent += `You clicked on a ${e.currentTarget.tagName} element\n`;
}

const container = document.querySelector("#container");
const button = document.querySelector("button");

document.body.addEventListener("click", handleClick, { capture: true });
container.addEventListener("click", handleClick, { capture: true });
button.addEventListener("click", handleClick);

تُعكس في هذه الحالة الرسائل التي تُعرض عند تنفيذ الشيفرة، حيث يطلق العنصر <body> الحدث أولًا يليه العنصر <div> من ثم الزر <button>:

You clicked on a BODY element
You clicked on a DIV element
You clicked on a BUTTON element

لكن لماذا نربك أنفسنا برفع الحدث أو التقاطه؟ يعود اﻷمر إلى الزمن الذي كانت فيه المتصفحات أقل توافقية بكثير مع بعضها، فاستخدم المتصفح نيتسكيب التقاط اﻷحداث واستخدم إنترنت إكسبلورر رفع اﻷحداث. وعندما حاولت منظمة W3C وضع معيار لهذا السلوك لكنها انتهت بإضافتها معًا، وهذا ما تتبناه المتصفحات اﻷحدث. وتستخدم معظم المتصفحات حاليًا رفع الأحداث افتراضيًا، وهذا أمر منطقي في معظم اﻷوقات.

تفويض اﻷحداث

لقد رأينا في الفقرات السابقة المشكلة التي ولدها رفع الحدث وكيفية إصلاحه. ومع أن رفع اﻷحداث مزعج أحيانًا لكنها قد تكون شديدة الفائدة. فهي تمكّن من استخدام تفويض اﻷحداث event delegation تحديدًا. فلو أردنا تنفيذ شيفرة محددة عندما يتفاعل المستخدم مع أي عنصر ابن ضمن عدد كبير من اﻷبناء، نهيئ مترصد أحداث للعنصر اﻷب ثم نرفع اﻷحداث التي تقع إلى العنصر اﻷب بدلًا من ضبط مترصد حدث لكل عنصر ابن على حدى.

لنعد إلى مثالنا الأول التي نغيّر فيها لون خلفية الصفحة عند النقر على الزر، ولنفترض أن الصفحة مقسمة إلى 16 قسمًا ونريد إعطاء كل قسم لونًا عشوائيًا:

إليك شيفرة HTML:

<div id="container">
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
</div>

إليك شيفرة CSS بسيطة لتحديد موضع وأبعاد اﻷقسام:

.tile {
  height: 100px;
  width: 25%;
  float: left;
}

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

function random(number) {
  return Math.floor(Math.random() * number);
}

function bgChange() {
  const rndCol = `rgb(${random(255)}, ${random(255)}, ${random(255)})`;
  return rndCol;
}

const container = document.querySelector("#container");

container.addEventListener("click", (event) => {
  event.target.style.backgroundColor = bgChange();
});

سيكون الخرج كالتالي:

ملاحظة: نستخدم في هذا المثال الخاصية event.target للحصول على العنصر الذي كان هدفًا للحدث. لكن إذا أردنا الوصول إلى العنصر الذي يعالج الحدث (وهو في حالتنا العنصر اﻷدنى مستوى) يمكن استخدام الخاصية event.currentTarget.

ملاحظة: يمكنك إيجاد الشيفرة الكاملة لهذا المثال على جت-هب (جرّب الشيفرة مباشرة أيضًا).

اﻷحداث ليست فقط لصفحات الويب

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

ونجد مثلًا بيئة Node.js وهي بيئة تنفيذية شعبية لجافاسكريبت تساعد المطوّرين على بناء تطبيقات للشبكات وتطبيقات الخادم، أن نموذجها الخاص بالأحداث يعتمد على مترصدي اﻷحداث listeners لترقب وقوع الحدث ومصدري اﻷحداث emitters لبث اﻷحداث دوريًا. قد لا يبدو اﻷمر مختلفًا جدًا، لكن الشيفرة مختلفة تمامًا. إذ تُستخدم دوال مثل ()on لتسجيل مترصد حدث، و ()once لتسجيل مترصد أحداث مرة واحدة ثم إلغاء تسجيله بعد تنفيذه مباشرة. يمكنك الاطلاع على توثيق أحداث الاتصال وفق بروتوكول HTTP لأمثلة أوضح.

بإمكانك استخدام جافاسكريبت لبناء إضافات قابلة للعمل في بيئات مختلفة باستخدام تقنية تُدعى موّسعات الويب WebExtension. إن نموذج اﻷحداث مشابه لنموذج أحداث الويب مع اختلاف بسيط، هو أن خاصيات مترصد اﻷحداث تُكتب باستخدام أسلوب سنام الجمل (مثل onMessage بدلًا من onmessage) والحاجة إلى دمجه مع الدالة addListener.

لا حاجة حاليًا لاستيعاب أي شيء عن البيئات اﻷخرى، لكننا أردنا توضيح فكرة أن اﻷحداث قد تختلف من بيئة برمجة إلى أخرى.

الخلاصة

لقد تعلمّت اﻻن ما يجب عليك تعلّمه عن أحداث الويب في هذه المرحلة المبكرة من مسيرتك، وكما ذكرنا سابقًا، لا تُعد اﻷحداث جزءًا بنيويًا من جافاسكريبت بل تُعرّف ضمن الواجهة البرمجية للمتصفح.

كما ينبغي أن تدرك أن سياق استخدام جافا سكريبت يتطلب نموذجًا مختلفًا لمعالجة اﻷحداث ابتداءًا بالواجهات البرمجية للويب إلى تقنيات أخرى مثل موسّعات ويب الخاصة بالمتصفحات وجافاسكريبت المخصصة للعمل على الخوادم مثل Node.js.

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

ترجمة -وبتصرف- للمقال Introduction to events

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...