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

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


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

تتسبّب الكثير من الأحداث في قيام المتصفّح تلقائيّا بأفعالٍ معيّنة. على سبيل المثال:

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

في حال معالجة الحدث في جافاسكربت، قد لا نريد حصول فعل المتصفّح المقابل له، ونريد تطبيق سلوكٍ آخر في مكانه.

منع أفعال المتصفح

هناك طريقتان لإخبار المتصفّح بأنّنا لا نريده أن يعمل:

  • الطريقة الرئيسيّة هي استخدام الكائن event، الذي لديه التابع event.preventDefault()‎.
  • إذا أٌسند المعالج باستخدام on<event>‎ (لا باستخدام addEventListener) ، فإنّ إعادة القيمة false تؤدي نفس الغرض أيضا.

في شيفرة HTML أسفله، لا يتسبّب النقر على الرابط في الانتقال إلى عنوانه، لا يفعل المتصفّح أيّ شيء.

<a href="/" onclick="return false">Click here</a>
or
<a href="/" onclick="event.preventDefault()">here</a>

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


تنبيه: إعادة false من المعالج هو استثناء

تُهمل عادةً القيمة المُعادة من طرف المعالج. الاستثناء الوحيد هو return false من المعالج المسنَد باستخدام on<event>‎. في جميع الحالات الأخرى، تُهمل قيمة return. على وجه الخصوص، لا معنى لإعادة true.


مثال: القائمة

لنفترض قائمة موقع كهذه:

<ul id="menu" class="menu">
  <li><a href="/html">HTML</a></li>
  <li><a href="/javascript">JavaScript</a></li>
  <li><a href="/css">CSS</a></li>
</ul>

بإضافة بعض تنسيقات CSS ستبدو كما هنا.

تُنشأ عناصر القائمة على شكل روابط HTML أي <a>، لا على شكل أزرار <button>. هناك عدّة أسباب لفعل ذلك، فمثلًا:

  • يحبّذ كثيرٌ من الناس استخدام "النقر باليمين" ثم "افتح في نافذة جديدة". لو استخدمنا <button> أو <span> فلن يعمل ذلك.
  • تتتبّع محرّكات البحث الروابط <a href="..."‎> عند الفهرسة.

بالتالي سنستخدم <a> عند إضافة الروابط في HTML. لكنّّنا نودّ عادةً معالجة النقرات على الروابط بواسطة جافاسكربت. فيجب إذًا أن نمنع فعل المتصفّح الافتراضي.كما هو مبيّن هنا:

menu.onclick = function(event) {
  if (event.target.nodeName != 'A') return;

  let href = event.target.getAttribute('href');
  alert( href ); // قد يُحمّل من الخادم أو تُولّد واجهة المستخدم إلى غير ذلك ...

  return false; //  (لا تذهب إلى العنوان) ‎ امنع فعل المتصفّح
};

إذا حذفنا return false، فسيقوم المتصفّح بعد تنفيذ الشيفرة "بفعله الافتراضي" -- الانتقال إلى العنوان الذي في href. ولا نحتاج ذلك هنا، إذ نعالج النقرات بأنفسنا.

بالمناسبة، يساعد استخدام تفويض الحدث هنا في جعل القائمة مرنة. يمكننا إضافة قوائم متداخلة وتنسيقها باستخدام CSS حتى "تنزلق إلى الأسفل".


ملاحظة: الأحداث المتتابعة

تجري بعض الأحداث الواحد تلو الآخر. فإذا منعنا الحدث الأوّل، لن يكون الثاني.

على سبيل المثال، يؤدّي وقوع الحدث mousedown على حقل <input> إلى التركيز عليه، ووقوع حدث focus. فإذا منعنا الحدث mousedown، لن يكون هناك تركيز.

جرّب النقر على <input> الأوّل من هنا -- يقع الحدث focus. لكن لو نقرت على الثاني، لن يكون هناك تركيز.

<input value="Focus works" onfocus="this.value=''">
<input onmousedown="return false" onfocus="this.value=''" value="Click me">

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


خيار المعالج "السلبي (passive)"

يشير الخيار الاختياري passive: true لـ addEventListener إلى المتصفّح بأنّ المعالج لن يستدعي preventDefault()‎. لكن لماذا قد يُحتاج لذلك؟

تتسبّب بعض الأحداث مثل touchmove على الأجهزة اللمسية (عندما يحرّك المستخدم اصبعه على الشاشة) في تمرير (scroll) الصفحة افتراضيًّا، لكن قد يُمنع ذلك التمرير باستخدام preventDefault()‎ في المعالج.

فإذا تحسّس المتصفّح مثل هذه الأحداث، عليه أوّلًا دراسة جميع المعالجات للتحقّق من عدم استدعاء preventDefault في أيٍّ منها، ليتمكّن عندها من الشروع في التمرير. قد يُسبّب هذا تأخيرات و "ارتجاجات" غير ضروريّة في واجهة المستخدم.

يُعلِم الخيارُ passive: true المتصفّحَ بأنّ المعالج لن يلغي التمرير. فيقوم المتصفّح بالتمرير مباشرة مقدّمًا بذلك تجربة سلسلة للغاية.

تكون قيمة passive في بعض المتصفّحات (Firefox, Chrome) هي true افتراضيًّا للأحداث touchstart و touchmove.


vent.defaultPrevented

تكون قيمة الخاصيّة event.defaultPrevented هي true إذا مُنع الفعل الافتراضي، وإلّا فهي false.

هناك حالة استخدامٍ لذلك مثيرة للاهتمام. تذكر أنّنا تحدّثنا في مقال انتشار الأحداث عن event.stopPropagation()‎، وكيف أنّ إيقاف انتشار الأحداث نحو الأعلى أمرٌ سيّء. يمكن أحيانًا استخدام event.defaultPrevented بدل ذلك، لإعلام المعالجات الأخرى بأنّ الحدث قد تمّت معالجته. لنرى مثالًا عمليّا لذلك.

يُظهر المتصفّح افتراضيًّا عند الحدث contextmenu (النقر بزرّ الفأرة الأيمن) قائمة منبثقة تضمّ خيارات اعتياديّة. يمكننا منع ذلك، وإظهار قائمتنا نحن بدلها، هكذا:

<button>Right-click shows browser context menu</button>

<button oncontextmenu="alert('Draw our menu'); return false">
  Right-click shows our context menu
</button>

بالإضافة إلى تلك القائمة المنبثقة، نودّ الآن إنشاء قائمة منبثقة تعمل على مستوى المستند.

عند النقر باليمين، يجب أن تظهر أقرب قائمة.

<p>Right-click here for the document context menu</p>
<button id="elem">Right-click here for the button context menu</button>

<script>
  elem.oncontextmenu = function(event) {
    event.preventDefault();
    alert("Button context menu");
  };

  document.oncontextmenu = function(event) {
    event.preventDefault();
    alert("Document context menu");
  };
</script>

المشكلة أنّه عندما ننقر على elem، فإننا نحصل على قائمتين: التي على مستوى الزرّ و (بانتشار الحدث نحو الأعلى) التي على مستوى المستند.

كيف يمكن إصلاح ذلك؟ يعتمد أحد الحلول على هذه الفكرة: "عند معالجة حدث النقر باليمين من طرف المعالج المسند إلى الزرّ، لنقم بإيقاف انتشاره نحو الأعلى باستخدام event.stopPropagation()‎:

<p>Right-click for the document menu</p>
<button id="elem">Right-click for the button menu (fixed with event.stopPropagation)</button>

<script>
  elem.oncontextmenu = function(event) {
    event.preventDefault();
    event.stopPropagation();
    alert("Button context menu");
  };

  document.oncontextmenu = function(event) {
    event.preventDefault();
    alert("Document context menu");
  };
</script>

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

الحلّ البديل هو التحقّق في معالج document ممّا إذا كان الفعل الافتراضي قد مُنع. وإذا كان كذلك، فإنّ الحدث قد عولج، ولا حاجة لكي نستجيب له.

<p>Right-click for the document menu (added a check for event.defaultPrevented)</p>
<button id="elem">Right-click for the button menu</button>

<script>
  elem.oncontextmenu = function(event) {
    event.preventDefault();
    alert("Button context menu");
  };

  document.oncontextmenu = function(event) {
    if (event.defaultPrevented) return;

    event.preventDefault();
    alert("Document context menu");
  };
</script>

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


ملاحظة: ()event.stopPropagation و ()event.preventDefault

كما يمكننا رؤية ذلك بوضوح، event.stopPropagation()‎ و event.preventDefault()‎ (أو ما يُعرف أيضا بـ return false) هما شيئان مختلفان. لا علاقة لهما ببعضهما البعض.



ملاحظة: هندسة القوائم المنبثقة المتداخلة

هناك أيضا طرق بديلة لتطبيق قوائم منبثقة متداخلة. إحداها هو أخذ كائن عامّ وحيد له المعالج document.oncontextmenu، وله كذلك توابع تمكّننا من تخزين معالجات أخرى داخله.

سيعمل الكائن على التقاط كلّ النقرات اليمنى، ثم ينظر في المعالجات المخزَّنة ويُشغّل المعالج المناسب.

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


الملخص

هناك عدة أفعال افتراضيًّة للمتصفّح:

  • mousedown -- يبدأ عمليّة التحديد (تُحرٌك الفأرة للتحديد).
  • click على <input type="checkbox"‎> -- إضافة أو إزالة التأشير على الخانة input.
  • submit -- يتسبّب النقر على <input type="submit"‎> أو الضغط على مفتاح Enter داخل حقل في نموذج بوقوع هذا الحدث، ويرسل المتصفّحُ النموذجَ بعد ذلك.
  • keydown -- قد يتسبّب الضغط على مفتاحٍ في إضافة حرف داخل حقل، أو إلى فعل آخر.
  • contextmenu -- يقع الحدث عند النقر باليمين، ويكون الفعل هو إظهار قائمة المتصفّح المنبثقة.
  • … و يوجد هناك المزيد …

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

لمنع الفعل الافتراضيّ، استخدم إمّا event.preventDefault()‎ أو return false. لا تعمل الطريقة الثانية إلّا مع المعالجات المسندة بواسطة on<event>‎.

يُعلِم الخيارُ passive: true المتصفّحَ بأنّ الفعل الافتراضيّ لن يُمنع. هذا مفيد لبعض أحداث الأجهزة المحمولة، مثل touchstart و touchmove، لإعلام المتصفّح بأنّ ليس عليه انتظار الانتهاء من تشغيل جميع المعالجات قبل التمرير.

إذا تمّ منع الفعل الافتراضيّ، فإنّ قيمة event.defaultPrevented تصير true، وإلّا فهي false.


تنبيه: حافظ على الدلاليّة، لا تفرط

فنيّا، بمنع الأفعال الافتراضية واستخدام جافاسكربت يمكننا تخصيص سلوك أيّ عنصر. على سبيل المثال، يمكننا جعل رابط <a> يعمل مثل زرّ، وجعل زرّ <button> يتصرّف مثل رابط (ينقل إلى عنوان آخر ونحوه).

لكن يجب عمومًا المحافظة على على المعاني الدلاليّة لعناصر HTML. على سبيل المثال، يجب أن يعمل على الانتقال الرابط، وليس الزرّ.

إلى جانب كون ذلك "مجرّد أمر جيّد"، يجعل هذا شيفرة HTML أفضل من ناحية إمكانية الوصول (accessibility).

بالإضافة إلى ذلك، يُرجى التنبّه إلى أن المتصفّح يتيح لنا إمكانيّة فتح الروابط <a> في نافذة مستقلّة (عن طريق النقر عليها باليمين وغير ذلك). ويودّ الناس ذلك. لكن لو جعلنا مكانها زرّا يعمل كرابط بواسطة جافاسكربت بل وجعلناه أيضا يبدو كرابط بواسطة CSS، فإنّ المزايا المختصّة بـ <a> التي يتيحها المتصفّح لن تعمل.


التمارين

لماذا لا تعمل "return false"؟

الأهمية: 3

لماذا لا تعمل return false إطلاقًا في هذه الشيفرة:

<script>
  function handler() {
    alert( "..." );
    return false;
  }
</script>

<a href="https://w3.org" onclick="handler()">the browser will go to w3.org</a>

يتّبع المتصفّح العنوان عند النقر، لكنّنا لا نريد ذلك.

كيف يمكن إصلاحه؟

الحل

عندما يقرأ المتصفّح الخاصيّة on* مثل onclick، فإنّه ينشئ معالجًا من محتواها.

فمن أجل onclick="handler()"‎ تصير الدالّة:

function(event) {
  handler() // onclick محتوى
}

نستطيع الآن أن نرى أن القيمة المعادة من طرف handler()‎ غير مستعملة ولا تؤثر في النتيجة.

طريقة إصلاح ذلك بسيطة:

<script>
  function handler() {
    alert("...");
    return false;
  }
</script>

<a href="https://w3.org" onclick="return handler()">w3.org</a>

يمكننا أيضا استخدام event.preventDefault()‎، هكذا:

<script>
  function handler(event) {
    alert("...");
    event.preventDefault();
  }
</script>

<a href="https://w3.org" onclick="handler(event)">w3.org</a>

التقط الروابط داخل العنصر

الأهمية: 5

اجعل جميع الروابط داخل العنصر الذي له id="contents"‎ تسأل المستخدم أوّلا إن كان يودّ المغادرة حقّا. فإذا كان لا يرغب، لا تذهب إلى العنوان.

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

التفاصيل:

  • يمكن أن تُحمّل شيفرة HTML داخل العنصر يدويّا، أو يمكن أن تُولّد ديناميكيّا في أيّ وقت، فلا سبيل لإيجاد جميع الروابط وإسناد معالجات إليها. استخدم تفويض الأحداث.
  • قد يكون داخل المحتوى وسوم متداخلة. وداخل الروابط أيضا، مثل <a href=".."><i>...</i></a>.

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

الحل

هذا استعمال جيّد لنمط تفويض الأحداث.

في الحالات الواقعية، بدل سؤال المستخدم، يمكننا إرسال طلب "التسجيل" (logging" request") إلى الخادم الذي يحفظ مكان مغادرة المستخدم. أو يمكننا تحميل المحتوى وعرضه في نفس الصفحة (إذا كان ذلك مسموحًا).

كلّ ما نحتاجه هو التقاط الأحداث contents.onclick واستخدام confirm لسؤال المستخدم. قد يمثّل استخدام link.getAttribute('href')‎ بدل link.href فكرة جيدة. انظر في الحل للتفاصيل.

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

معرض الصور

الأهمية: 5

أنشئ معرضًا للصور بحيث تتغير الصورة الرئيسيّة بالنقر على الصور المصغّرة.

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

ملاحظة: استخدم تفويض الأحداث.

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

الحل

يكون الحل بإسناد معالج إلى الحاوي وتتبّع النقرات. فإذا كان النقر على الرابط <a>، فنغير src الخاصّة بـ ‎#largeImg إلى href الخاصة بتلك الصورة المصغرة.

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

ترجمة -وبتصرف- للمقال Browser default actions من سلسلة 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.


×
×
  • أضف...