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

كيفية تفويض الأحداث في المتصفح ومعالجتها عبر جافاسكربت


محمد أمين بوقرة

يمكّننا انتشار اﻷحداث نحو اﻷسفل واﻷعلى من تطبيق أحد أقوى أنماط معالجة الأحداث، وهو ما يُسمى تفويض الأحداث (event delegation).

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

في داخل المعالج، يمكن أن نتعرّف على مكان وقوع الحدث من خلال event.target، ثم نعالجه.

لنرى مثالا على ذلك -- مخطط باكوا الذي يعكس الفلسفة الصينية القديمة، كما هو مبيّن من هنا. ويمكن تمثيله بواسطة HTML كالتالي:

<table>
  <tr>
    <th colspan="3"><em>Bagua</em> Chart: Direction, Element, Color, Meaning</th>
  </tr>
  <tr>
    <td class="nw"><strong>Northwest</strong><br>Metal<br>Silver<br>Elders</td>
    <td class="n">...</td>
    <td class="ne">...</td>
  </tr>
  <tr>...2 more lines of this kind...</tr>
  <tr>...2 more lines of this kind...</tr>
</table>

يحتوي الجدول على 9 خانات، لكنها قد تكون 99 أو 9999، لا يهمّ ذلك.

مهمّتنا هي إبراز الخانة عند النقر عليها.

بدل إسناد معالج onclick إلى كلّ <td> (قد يكون هناك الكثير منها)، سنسند المعالج "catch-all" إلى العنصر <table>.

يستخدم المعالجُ الخاصيّة event.target للحصول على العنصر الذي نُقر عليه ثم يبرزه.

إليك الشيفرة:

let selectedTd;

table.onclick = function(event) {
  let target = event.target; // أين كان النقر؟

  if (target.tagName != 'TD') return; // ؟ إذًا لا يهمّنا ذلك TD ليس في

  highlight(target); // أبرزه
};

function highlight(td) {
  if (selectedTd) { // أزل الإبراز الحالي إن وُجد
    selectedTd.classList.remove('highlight');
  }
  selectedTd = td;
  selectedTd.classList.add('highlight'); // الجديدة td أبرز الـ
}

لا تكترث هذه الشيفرة بعدد الخانات التي في الجدول. يمكننا إضافة وإزالة الخانات <td> ديناميكيا في أي وقت، وستظل وظيفة الإبراز تعمل.

لكن تبقى هناك نقيصة. قد لا يقع النقر على العنصر <td> بعينه ولكن على عنصر آخر بداخله. ففي حالتنا هذه، لو ألقينا نظرة داخل HTML، سنلاحظ أوسمة مدرجة داخل <td>، مثل <strong>:

<td>
  <strong>Northwest</strong>
  ...
</td>

فمن الطبيعي أنه لو وقع النقر على <strong>، فسيصير هو القيمة التي يحملها event.target.

bagua-bubble.png

داخل المعالج table.onclick، ينبغي علينا أن نأخذ event.target ونتحقق إن كان النقر قد وقع داخل <td> أو لا.

هذه هي الشيفرة المحسّنة:

table.onclick = function(event) {
  let td = event.target.closest('td'); // (1)

  if (!td) return; // (2)

  if (!table.contains(td)) return; // (3)

  highlight(td); // (4)
};

إليك بعض الإيضاحات:

  1. يعيد التابع elem.closest(selector)‎ أقرب سلف يطابق المُحدِّد selector. في حالتنا، نبحث عن أقرب <td> نصادفه صعودًا من العنصر المصدري.
  2. إذا لم يكن event.target بداخل أيّ <td>، فسيُعاد الاستدعاء مباشرة، إذ ليس هناك شيء لفعله.
  3. في حال تداخل الجداول، قد تكون event.target هي <td>، لكنّها موجودة خارج الجدول الحاليّ. فنتحقق إذًا من أنّ <td> خاصّة بجدولنا الحاليّ.
  4. وإذا كانت كذلك، نبرزها.

وبذلك، تكون لدينا شيفرة سريعة وفعّالة للإبراز، لا تكترث بعدد الخانات التي في الجدول.

مثال عن التفويض: الأفعال داخل الترميز (markup)

هناك استعمالات أخرى لتفويض الأحداث.

لنقُل أننا نود إنشاء قائمة من الأزرار: "حفظ" و "تحميل" و "بحث" وغير ذلك. ويوجد هناك كائن له التوابع save و load و search … فكيف يتم الربط بين هذه التوابع واﻷزرار؟

أوّل ما قد يتبادر إلى الذهن هو إسناد معالج إلى كلّ زر. ولكنّ هناك حلًّا أكثر أناقة. يمكننا إسناد معالج إلى القائمة بأكملها و إضافة سمات data-action للأزرار تحمل التابع الذي سيُستدعى:

<button data-action="save">Click to Save</button>

يقرأ المعالج السمة، وينفّذ التابع. ألقِ نظرة على المثال أدناه:

<div id="menu">
  <button data-action="save">Save</button>
  <button data-action="load">Load</button>
  <button data-action="search">Search</button>
</div>

<script>
  class Menu {
    constructor(elem) {
      this._elem = elem;
      elem.onclick = this.onClick.bind(this); // (*)
    }

    save() {
      alert('saving');
    }

    load() {
      alert('loading');
    }

    search() {
      alert('searching');
    }

    onClick(event) {
      let action = event.target.dataset.action;
      if (action) {
        this[action]();
      }
    };
  }

  new Menu(menu);
</script>

يُرجى ملاحظة أن this.onClick مرتبط بـ this في (*). هذا مهمّ، لأنّه لو لم يكن كذلك فإن this الذي بداخله سيشير إلى عنصر (elem) ، وليس الكائن Menu، ولا يكون بذلك this[action]‎ هو ما نحتاجه.

فما هي إذًا المزايا التي يقدّمها تفويض الأحداث هنا؟

  • لا نحتاج كتابة شيفرة لإسناد معالج لكل زر. بل ننشئ فقط تابعًا ونضعه في الترميز.
  • تصبح بنية HTML مرنة، فيمكننا إضافة وإزالة أزرار بسهولة.

يمكننا أيضا استخدام أصناف مثل ‎.action-save و ‎.action-load، لكن سمة مثل data-action أفضل من الناحية الدلالية، بالإضافة إلى إمكانية استخدامها في قواعد CSS.

نمط "السلوك"

يمكننا أيضا استخدام تفويض الأحداث لإضافة "سلوكيّات" للعناصر تصريحيًّا، بواسطة سمات وأصناف خاصة.

يتألّف هذا النمط من جزأين:

  1. نضيف سمة مخصّصة إلى العنصر تعبّر عن سلوكه.
  2. يتتبع الأحداثَ معالجٌ على نطاق المستند، فإذا وقع حدث على عنصر له سمة، فإنه يقوم بالفعل المناسب.

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

على سبيل المثال، تضيف السمة data-counter هنا سلوك "زيادة القيمة عند النقر" للأزرار:

Counter: <input type="button" value="1" data-counter>
One more counter: <input type="button" value="2" data-counter>

<script>
  document.addEventListener('click', function(event) {

    if (event.target.dataset.counter != undefined) { // إذا كانت السمة موجودة...
      event.target.value++;
    }

  });
</script>

إذا نقرنا على أحد الأزرار، فإن القيمة التي عليه ستزداد. بغضّ النظر عن هذه الأزرار، فإن المنهجية العامة المتبعة هنا مهمّة.

يمكن أن يكون هناك من السمات مع data-counter بقدر ما نرغب. يمكننا إضافة سمات جديدة إلى HTML في أي وقت. باستخدام تفويض الأحداث، نكون قد "وسّعنا" HTML من خلال إضافة سمة تعبّر عن سلوك جديد.


تنبيه: استخدم دائما addEventListener في المعالجات التي على مستوى المستند

عند إسناد معالج حدثٍ إلى الكائن document، يجب أن نستخدم دائما addEventListener، وليس document.on<event>‎، لأن هذا الأخير سيؤدي إلى تعارضات: تستبدل المعالجاتُ الجديدة المعالجات القديمة.

في المشاريع الواقعية، من الطبيعي أن تكون هناك عدة معالجات قد أُسندت بواسطة أجزاء مختلفة من الشيفرة.


مثال عن السلوك: القالِب (toggler)

لنرى مثالا آخر عن السلوك. يؤدي النقر على عنصرٍ له السمة data-toggle-id إلى إخفاء وإظهار العنصر الذي له ذاك الـ id.

<button data-toggle-id="subscribe-mail">
  Show the subscription form
</button>

<form id="subscribe-mail" hidden>
  Your mail: <input type="email">
</form>

<script>
  document.addEventListener('click', function(event) {
    let id = event.target.dataset.toggleId;
    if (!id) return;

    let elem = document.getElementById(id);

    elem.hidden = !elem.hidden;
  });
</script>

لنلاحظ مرة أخرى ما قمنا به. لإضافة وظيفة القلب إلى عنصرٍ ما من الآن فصاعدًا، لا حاجة لمعرفة جافاسكربت، يكفي استخدام السمة data-toggle-id.

قد يصير هذا ملائما بالفعل -- لا حاجة لكتابة جافاسكربت لكل واحد من هذه العناصر. يكفي استخدام السلوك فقط. يجعل المعالج الذي على مستوى المستند ذلك يعمل مع أي عنصر على الصفحة.

يمكننا أن نجمع بين عدّة معالجات في نفس العنصر أيضا.

قد يشكل نمط "السلوك" بديلا عن الأجزاء المصغرة (mini-fragments) في جافاسكربت.

الملخص

تفويض الأحداث رائع حقًّا! إذ يُعدّ واحدا من أنفع الأنماط المتعلّقة بأحداث DOM.

كثيرا ما يُستخدم تفويض اﻷحداث لإضافة نفس المعالج لعدّة عناصر متماثلة، لكن لا يقتصر اﻷمر على ذلك.

الخوارزمية:

  1. أسند معالجًا وحيدًا إلى العنصر الحاوي.
  2. في المعالج -- افحص العنصر المصدري event.target.
  3. إذا وقع الحدث داخل عنصر يهمّنا، عالج الحدث.

المزايا:

  • يبسّط التهيئة ويوفّر الذاكرة: لا حاجة لإضافة عدة معالجات.
  • أقلّ شيفرة: عند إضافة أو إزالة عناصر، لا داعي لإضافة أو إزالة المعالجات.
  • التعديلات على DOM: يمكننا إضافة أو إزالة العناصر جماعيا بواسطة innerHTML وما إلى ذلك.

للتفويض حدود أيضا بالطبع:

  • أولًا، يجب أن يكون الحدث منتشرًا نحو اﻷعلى. بعض الأحداث لا تنتشر نحو اﻷعلى. يجب كذلك أن لا تَستخدم المعالجاتُ التي في الأسفل event.stopPropagation()‎.
  • ثانيًا، قد يضيف التفويض عبئًا على وحدة المعالجة المركزية (CPU)، لأنّ المعالج الذي على مستوى الحاوي يستجيب للأحداث في أي مكان في الحاوي، بغضّ النظر عن كونها مهمّة لنا أو لا. لكن العبئ عادةً طفيف، فلا نأخذه بالحسبان.

التمارين

أخفي الرسائل باستخدام التفويض

الأهمية: 5

هناك قائمة من الرسائل لها أزرار لإزالتها [x]. اجعل الأزرار تعمل.

كما هو مبيّن هنا.

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

أنجز التمرين في البيئة التجريبية

الحل

افتح الحل في البيئة التجريبية

قائمة شجرية

الأهمية: 5

أنشئ شجرة يمكن فيها إظهار وإخفاء العقد الأبناء بواسطة النقر:

كما هو مبيّن هنا.

المتطلبات:

  • معالج واحد للأحداث فقط (استخدم التفويض).
  • يجب ألا يفعل النقر خارج عقدة العنوان (في مساحة فارغة) أي شيء.

أنجز التمرين في البيئة التجريبية

الحل

ينقسم الحل إلى جزئين:

  1. ضع كلّ عقدة عنوان في الشجرة داخل <span>. بهذا يمكننا إضافة تنسيقات CSS إلى :‎hover و معالجة النقرات على النص بالضبط، لأن عُرض <span> هو نفس عُرض النص بالضبط (بخلاف ما لو كان بدونه).
  2. عيّن معالجًا على العقدة الجذر tree، وعالج النقرات على العنوانين <span> تلك.

افتح الحل في البيئة التجريبية

جدول قابل للترتيب

الأهمية: 4

اجعل الجدول قابلًا للترتيب: يجب أن يؤدي النقر على العناصر <th> إلى ترتيبه حسب العمود الموافق.

لكلّ <th> نوع معين موجود بداخل السمة، هكذا:

<table id="grid">
 <thead>
  <tr>
   <th data-type="number">Age</th>
   <th data-type="string">Name</th>
  </tr>
 </thead>
 <tbody>
  <tr>
   <td>5</td>
   <td>John</td>
  </tr>
  <tr>
   <td>10</td>
   <td>Ann</td>
  </tr>
  ...
 </tbody>
</table>

في المثال أعلاه، يحتوي العمود الأول على الأرقام، و العمود الثاني على الحروف. يجب أن تقوم دالة الترتيب بمعالجة الترتيب حسب النوع.

يستلزم فقط أن يُدعم النوعان "string" و "number".

يمكن مشاهدة المثال يعمل من هنا.

ملاحظة: يمكن أن يكون الجدول كبيرا، بأي عدد من الأسطر والأعمدة.

أنجز التمرين في البيئة التجريبية

الحل

افتح الحل في البيئة التجريبية

سلوك التلميحات

الأهمية: 5

أنشئ شفرة جافاسكربت لأجل سلوك التلميحات (tooltips).

عندما يحوم مؤشر الفأرة فوق عنصر له السمة data-tooltip، فيجب أن تظهر التلميحة فوقه، وعندما يفارقه فإنها تختفي.

هذا مثال لشفرة HTML مع الشرح:

<button data-tooltip="the tooltip is longer than the element">Short button</button>
<button data-tooltip="HTML<br>tooltip">One more button</button>

يجب أن تعمل كما هنا.

سنفترض في هذا التمرين أن جميع العناصر التي لها data-tooltip تحتوي على نص فقط. لا وسوم متداخلة (بعد).

التفاصيل:

  • يجب أن تكون المسافة بين العنصر والتلميحة 5px.
  • يجب أن تكون التلميحة في منتصف العنصر، إن أمكن ذلك.
  • يجب ألا تقطع التلميحة حوافّ النافذة. من المفترض أن تكون التلميحة فوق العنصر، فإذا كان العنصر في أعلى الصفحة ولا مكان هناك للتلميحة، فإنها تكون تحته.
  • يُعطى محتوى التلميحة في السمة data-tooltip. يمكنها أن تحوي أي شفرة HTML.

ستحتاج إلى حدثين هنا:

  • mouseover يحصل عندما يحوم المؤشر فوق العنصر.

  • mouseout يحصل عندما يفارق المؤشر العنصر.

يُرجى استخدام تفويض الأحداث: أسند اثنين من المعالجات إلى document لتتبّع كلّ "الحومان" و "المفارقة" للعناصر التي لها data-tooltip وقم بإدارة التلميحات من هناك.

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

ملاحظة: يجب أن تظهر تلميحة واحدة فقط في نفس الوقت.

أنجز التمرين في البيئة التجريبية

الحل

افتح الحل في البيئة التجريبية

ترجمة -وبتصرف- للمقال Event delegation من سلسلة Browser: Document, Events, Interfaces لصاحبها Ilya Kantor


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

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

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



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

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

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

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   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.


×
×
  • أضف...