۲۵ ژانویه ۲۰۲۴

اکشن‌های پیشفرض مرورگر

بسیاری از رویدادها اقدامات خودبه‌خودی از سمت مرورگر را در پی دارند

برای نمونه:

  • یک کلیک بر روی لینک – شما را به آدرس موردنظر میرساند
  • یک کلیک روی دکمه ارسال فرم – تایید و ارسال فرم به سرور آغاز می‌شود.
  • فشردن دکمه ماوس برروی متن و حرکت دادن آن – متن را انتخاب می‌کند

اگر بخواهیم که یک رویداد را در جاوااسکریپت مدیریت کنیم ممکن است نخواهیم تا اکشن پیشفرض مرورگر اتفاق بیفتد و بخواهیم که رفتار متفاوتی به جای آن را پیاده‌سازی کنیم.

جلوگیری از اکشن‌های مرورگر

دو راه برای اینکه به مرورگر بگوییم نمیخواهیم تا رفتار پیشفرض را انجام دهد وجود دارد:

  • راه اصلی استفاده از آبجکت event است. متدی به نام ()event.preventDefault وجود دارد.
  • اگر هندلر با استفاده از <on<event مشخص شده باشد (نه با addEventListener) آنگاه بازگرداندن مقدار false به همین صورت عمل خواهد کرد.

در این فایل HTML, کلیک بر روی یک لینک منجر به تغییر آدرس مروگر نمی‌شود; مرورگر کاری نمی‌کند:

<a href="/" onclick="return false">اینجا را کلیک کنید</a>
یا
<a href="/" onclick="event.preventDefault()">اینجا</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 اینطور به نظر می‌رسد:

آیتم‌های منو به شکل به شکل لینک با تگ <a> پیاده‌سازی شده‌اند نه به شکل دکمه با تگ <button>. دلایلی مختلفی برای انجام اینکار وجود دارد. برای مثال:

  • بسیاری از افراد علاقه دارند تا از "کلیک راست " استفاده کنند و گزینه “open in a new window” برای باز کردن مقصد در یک صفحه جدید استفاده کنند. اگر از <button> یا <span> استفاده کنیم امکان اینکار وجود نخواهد داشت.
  • موتورهای جستجو تگ <a href="..."> را هنگام ایندکس کردن دنبال میکنند.

بنابراین ما از <a> در مارک‌آپ استفاده میکنیم. اما چون معمولا تمایل داریم تا کلیک‌ها را در جاوااسکریپت هندل کنیم بنابراین باید از رفتار پیشفرض مرورگر جلوگیری کنیم.

مثل اینجا:

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

  let href = event.target.getAttribute('href');
  alert( href ); // میتواند لود از سرور یا ساخت UI و مانند آن باشد...

  return false; // از رفتار پیشفرض مرورگر جلوگیری میکند (به آدرس نرو)
};

اگر return false را حذف کنیم آنگاه پس از اجرای کد ما مرورگر “رفتار پیشفرض” خود را انجام خواهد داد – مرورگر شما را به آدرس موجود در href هدایت خواهد کرد. و ما به آن اینجا نیاز نداریم چون میخواهیم که کلیک توسط خودمان مدیریت شود.

درضمن استفاده از event delegation در اینجا منوی ما را بسیار انعطاف پذیر می‌کند. چون می‌توانیم از لیست‌های تودرتو استفاده کرده و آنرا با استفاده از CSS استایل دهیم. مثل استایل “slide down”

Follow-up events

رویدادهای مشخص در یکدیگر جاری میشوند. اگر از اولی جلوگیری کنیم رویداد دومی وجود نخواهد داشت.

برای مثال رویداد mousedown برروی <input> منجر به فوکوس آن و رویداد فوکوس می‌شود. اگر از رویداد mousedown جلوگیری کنیم آنگاه فوکوسی نخواهیم داشت.

تلاش کنید تا برروی اولین <input> کلیک کنید – رویداد focus رخ خواهد داد. اما اگر برروی دومی کلیک کنید فوکوسی وجود نخواهد داشت.

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

این بخاطر آن است که اکشن مرورگر برروی mousedown لغو شده است. فوکوس کردن همچنان امکان‌پذیر است اگر ما راه دیگری برای وارد کردن اینپوت استفاده کنیم. برای مثال کلید Tab برای انتقال از اینپوت اول به دوم اما بدون استفاده از کلیک ماوس.

<<<<<<< HEAD

امکان هندلر “passive”

آپشن اختیاری passive: true از addEventListener این سیگنال را به مرورگر میدهد که مرورگر هندلر ()preventDefault را صدا نخواهد کرد.

چرا ممکن است که به این آپشن نیاز پیدا کنیم؟

ایونت هایی همچون touchmove در دیوایس‌های موبایلی وجود دارند (زمانی که یوزر انگشت خود را برروی صفحه‌نمایش حرکت می‌دهد) که به صورت پیشفرض باعث اسکرول می‌شوند اما این اسکرول خوردن میتواند با وجود ()preventDefault در هندلر جلوگیری شود.

بنابراین زمانی که مرورگر چنین ایونتی را شناسایی میکند اول از همه باید همه‌ی هندلرهارا بررسی کرده و اگر preventDefault جایی صدا زده نشده باشد میتواند با اسکرول خوردن ادامه یابد که این میتواند سبب تاخیرها و لرزش‌های غیرضروری شود.

گزینه passive: true به مرورگر میگوید که هندلر قصد لغو اسکرول را ندارد آنگاه مرورگر بلافاصله عمل اسکرول را انجام می‌دهد و تجربه خوب و روانی را برای کاربر به وجود می‌آورد و درضمن رویداد هم هندل می‌شود.

در بعضی از مرورگرها (فایرفاکس و کروم) مقدار passive به صورت پیشفرض برای رویدادهای touchstart و touchmove مقدار true دارد.

bae0ef44d0208506f6e9b7f3421ee640ab41af2b

event.defaultPrevented

ویژگی event.defaultPrevented اگر از اکشن پیشفرض جلوگیری شده باشد true بوده و درغیراینصورت false خواهد بود.

یک مورد استفاده جالب برای این وجود دارد.

به یاد دارید که در فصل بالارفتن و گرفتن راجع به ()event.propagation و اینکه چرا bubbling خوب نیست صحبت کردیم؟

گاهی اوقات برای اینکه به باقی ایونت هندلرها خبر بدهیم که ایونت هندل شده میتوانیم از event.defaultPrevented استفاده کنیم.

بیاید تا با هم یک مثال عملی را ببینیم.

به صورت پیشفرض مروگر در پاسخ به ایونت contextmenu (کلیک راست ماوس) یک منو با آپشن‌های استاندارد را نمایش می‌دهد. ما می‌توانیم از این موضوع جلوگیری کرده و منوی خودمان را نمایش دهیم. به اینصورت:

<button>کلیک راست context menu مرورگر را نمایش می‌دهد</button>

<button oncontextmenu="alert('Draw our menu'); return false">
  کلیک راست context menu ما را نمایش می‌دهد
</button>

حالا علاوه بر context menu میخواهیم تا یک منو تعریف شده در داکیومنت پیاده‌سازی کنیم.

با کلیک‌راست نزدیک‌ترین context menu ظاهر خواهد شد.

<p>برای context menu داکیومنت اینجا کلیک راست کنید</p>
<button id="elem">برای 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 کلیک کنیم دو منو خواهیم داشت: منوی تعریف شده برروی دکمه و (زمانی که رویداد bubble up می‌کند) منوی تعریف شده در داکیومنت.

چطور این مسئله را فیکس کنیم؟ یکی از راه‌حل ها این است: “زمانی که که میخواهیم کلیک‌راست را در هندلر دکمه هندل کنیم بیاید تا bubbling آن را متوقف کرده” و از ()event.stopPropagation استفاده کنیم.

<p>برای منوی داکیومنت اینجا کلیک راست کنید</p>
<button id="elem">برای منوی دکمه (با 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>کلیک راست برای منوی داکیومنت (حالتی که بررسی event.defaultPrevented اضافه شده است)</p>
<button id="elem">برای منوی دکمه کلیک راست کنید</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() and event.preventDefault()

واضح است که ()event.stopPropagation و ()event.preventDefault (که همچنین به عنوان return false شناخته می‌شود) دو چیز متفاوت از هم هستند که به یکدیگر ربطی ندارند.

معماری context menu های تودرتو

همچنین راه‌های جایگزینی برای پیاده‌‍سازی منوهای تودرتو وجود دارد. یکی از آنها این است که یک آبجکت گلوبال با یک هندلر برای document.oncontextmenu داشته باشیم و همچنین متدهایی که به ما اجازه می‌دهند تا دیگر هندلرهایی را در آن ذخیره کنیم.

آبجت هرگونه کلیک راستی را گرفته، نگاهی به هندلرهای آن می‌اندازد و هندلر مناسب را اجرا میکند.

اما در اینصورت هر تکه کدی که بخواهد context menu داشته باشد باید راجع به آن آبجکت بداند و به جای هندلر contextmenu خودش از کمک آن آبجکت گلوبال استفاده کند.

خلاصه

اکشن های پیشفرض مختلفی وجود دارند:

  • mousedown – انتخاب متن را آغاز می‌کند (برای انتخاب ماوس را حرکت دهید)
  • click بر روی <input type="checkbox">input را check/uncheck می‌کند.
  • submit – کلیک بر روی <input type="submit"> یا فشردن Enter درون یک فیلد فرم باعث رخ دادن این رویداد می‌شود و مرورگر پس از آن فرم را سابمیت می‌کند.
  • keydown – فشردن یک کلید ممکن است باعث افزودن یک کاراکتر به فیلد یا اکشن‌های دیگر شود.
  • contextmenu – رویدادی که با کلیک راست رخ می‌دهد و اکشن مرتبط با آن نمایش context menu مرورگر است.
  • –موارد بیشتری هم وجود دارند–

اگر بخواهیم تا ایونت را به طور خاص با جاوااسکریپت هندل کنیم می‌توانیم از همه‌ی اکشن‌های پیشفرض جلوگیری کنیم.

برای جلوگیری از یک اکشن پیشفرض میتوانیم از ()event.preventDefault یا return false استفاده کنیم. دومین متد تنها برای هندلرهای اختصاص یافته با <on<event کار می‌کند.

اگر از اکشن پیشفرض جلوگیری شده باشد مقدار event.defaultPrevented به true تغییر می‌کند در غیراینصورت false می‌شود.

سمنتیک باقی بمانید و سوءاستفاده نکنید

به شکل تکنیکال با جلوگیری از اکشن‌های پیشفرض مرورگر و افزودن جاوااسکریپت میتوانیم رفتار المنت‌ها را شخصی‌سازی کنیم. برای مثال میتوانیم کاری کنیم تا تگ <a> مانند یک دکمه کار کند و یک دکمه <button> مانند یک لینک رفتار کند (یه یک آدرس دیگر ریدایرکت کند و امثالهم).

اما ما باید به طور کلی معنای سمنتیک المنت‌ها را حفظ کنیم. برای مثال <a> باید هدایت مرورگر به آدرس‌ها را انجام دهد نه یک دکمه.

این موضوع علاوه بر اینکه “چیز خوبی است” کد HTML شما را از نظر دسترسی‌پذیری بهتر میکند.

علاوه بر این اگر مثال <a> را درنظر بگیریم باید به این نکته توجه کنید که: یک مرورگر به ما این اجازه را می‌دهد تا این لینک‌ها را در یک پنجره جدید باز کنیم (با کلیک راست برروی منوها) و کاربران این رفتار را دوست دارند. اما اگر کاری کنیم که یک دکمه مانند لینک رفتار کند و حتی با استفاده از css ظاهرش را مانند یک لینک کنیم آنگاه ویژگی‌های منحصر به فرد تگ <a> برای آن عمل نخواهد کرد.

تمارین

اهمیت: 3

Why in the code below return false doesn’t work at all?

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

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

The browser follows the URL on click, but we don’t want it.

How to fix?

When the browser reads the on* attribute like onclick, it creates the handler from its content.

For onclick="handler()" the function will be:

function(event) {
  handler() // the content of onclick
}

Now we can see that the value returned by handler() is not used and does not affect the result.

The fix is simple:

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

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

Also we can use event.preventDefault(), like this:

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

<a href="https://w3.org" onclick="handler(event)">w3.org</a>
اهمیت: 5

Make all links inside the element with id="contents" ask the user if they really want to leave. And if they don’t then don’t follow.

Like this:

Details:

  • HTML inside the element may be loaded or regenerated dynamically at any time, so we can’t find all links and put handlers on them. Use event delegation.
  • The content may have nested tags. Inside links too, like <a href=".."><i>...</i></a>.

باز کردن یک sandbox برای تمرین.

That’s a great use of the event delegation pattern.

In real life instead of asking we can send a “logging” request to the server that saves the information about where the visitor left. Or we can load the content and show it right in the page (if allowable).

All we need is to catch the contents.onclick and use confirm to ask the user. A good idea would be to use link.getAttribute('href') instead of link.href for the URL. See the solution for details.

باز کردن راه‌حل درون sandbox.

اهمیت: 5

Create an image gallery where the main image changes by the click on a thumbnail.

Like this:

P.S. Use event delegation.

باز کردن یک sandbox برای تمرین.

The solution is to assign the handler to the container and track clicks. If a click is on the <a> link, then change src of #largeImg to the href of the thumbnail.

باز کردن راه‌حل درون sandbox.

نقشه آموزش