فصل 15رسیدگی به رخدادها
آنچه در اختیار شما است، ذهنتان است نه رخدادهای جهان بیرون. درک این موضوع به شما نیرو میبخشد.
بعضی برنامهها با ورودی کاربر سر و کار دارند؛ مانند کارهایی که توسط موس و صفحهکلید انجام میشود. این نوع ورودی به صورت یک ساختار دادهی سازمانیافته و مرتب در دسترس قرار نمیگیرد – بلکه به صورت تدریجی و با اجرای برنامه دریافت میشود و برنامه میبایست همزمان با دریافت آن، واکنش نشان دهد.
گردانندههای رخداد (Event Handlers)
رابطی را تصور کنید که در آن تنها راه دانستن اینکه کلیدی در صفحه کلید فشرده میشود این است که حالت فعلی آن کلید را بخوانیم. برای این که بتوانیم به فشردن کلید واکنش نشان دهیم، باید به طور مداوم حالت کلید را بخوانیم تا قبل از اینکه دوباره کلید رها شود آن را بدست آوریم. در این حین اگر به انجام محاسبهی زمانگیر دیگری بپردازیم، این خطر وجود دارد که یک فشردن کلید را از دست بدهیم.
بعضی کامپیوترهای اولیه، ورودی ها را به همین شکل مدیریت میکردند. یک گام فراتر از این روش این است که سختافزار یا سیستمعامل متوجه این فشار کلید بشوند و آن را در یک صف قرار دهند. بعد یک برنامه میتواند به صورت دورهای این صف را برای رخدادهای جدید بررسی کند و به چیزی که میبیند واکنش نشان دهد.
البته برنامه باید به خاطر داشته باشد که به سراغ صف برود و این کار را مدوام انجام دهد چرا که وجود فاصلهی زمانی بین فشردن کلید و باخبر شدن برنامه از رخداد، باعث میشود که نرمافزار روان کار نکند و مشکلدار بهنظر برسد. این رویکرد را polling (سرکشیکردن) مینامند. بیشتر برنامهنویسان ترجیح میدهند که از آن اجتناب کنند.
یک مکانیزم بهتر برای سیستم این است که به طور پویا برنامهمان را از وجود یک رخداد باخبر کنیم. مرورگرها این کار را با فراهم کردن امکان ثبت توابعی به عنوان گرداننده برای رخدادهای خاص انجام میدهند.
<p>Click this document to activate the handler.</p> <script> window.addEventListener("click", () => { console.log("You knocked?"); }); </script>
متغیر window
به یک شیء درونی که توسط مرورگر فراهم شده اشاره میکند. این شیء نمایانگر پنجرهی مرورگر است که حاوی صفحه سند میباشد. فراخوانی متد addEventListener
متعلق به آن، آرگومان دوم را به عنوان تابعی ثبت میکند که در صورت بروز رخدادی که در آرگومان اول مشخص میشود، فراخوانی خواهد شد.
رخدادها و گرههای DOM
هر گردانندهی رخداد مرورگر در یک بستر (context) ثبت میشود. ما addEventListener
را روی شیء window
پیشتر برای ثبت یک گرداننده برای کل پنجره فراخوانی کردیم. این متد همچنین روی عناصر DOM و بعضی دیگر از انواع اشیاء موجود است. شنوندههای رخداد فقط زمانی فراخوانی میشوند که رخداد در بستر شیئی که در آن ثبت شده اند رخ داده باشد.
<button>Click me</button> <p>No handler here.</p> <script> let button = document.querySelector("button"); button.addEventListener("click", () => { console.log("Button clicked."); }); </script>
در مثال بالا یک گرداننده به گرهی دکمه منتسب میشود. کلیک روی آن دکمه باعث میشود که گردانندهی آن اجرا شود، اما کلیک روی دیگر قسمتهای سند باعث اتفاقی نمیشود.
اضافه کردن خصوصیت onclick
به یک گره نیز اثر مشابهی ایجاد میکند. این روش برای بیشتر رخدادها کار میکند - میتوانید یک گرداننده توسط این خصوصیت ثبت کنید که نام آن برای نام رخداد به همراه on
در جلوی آن خواهد بود.
اما یک گره، تنها میتواند یک خصوصیت onclick
داشته باشد، بنابراین در این روش برای هر گره، تنها میتوانید یک گرداننده ثبت نمایید. متد addEventListener
به شما این امکان را میدهد تا هر تعداد گرداننده که بخواهید اضافه کنید که به این معنا است که افزودن گردانندهها حتی زمانی که پیشتر گردانندهای روی عنصر اضافه شده است، معتبر است.
متد removeEventListener
اگر با آرگومانهایی شبیه به addEventListener
فراخوانی شود، باعث حذف یک گرداننده میشود.
<button>Act-once button</button> <script> let button = document.querySelector("button"); function once() { console.log("Done."); button.removeEventListener("click", once); } button.addEventListener("click", once); </script>
تابعی که به removeEventListener
داده میشود باید دقیقا همان مقدار تابعی باشد که به addEventListener
داده شده است. بنابراین برای حذف یک گرداننده، باید به تابع مورد نظر یک نام اختصاص داد (once
در مثال) تا بتوان همان تابع را در هردو متدها استفاده کرد.
اشیاء رخداد
با اینکه تاکنون به آن نپرداختهایم، توابع گردانندهی رخداد، آرگومانی دریافت میکنند که شیء رخداد نام دارد (event object). این شیء اطلاعات بیشتری دربارهی رخداد مورد نظر نگهداری میکند. به عنوان مثال، اگر بخواهیم بدانیم کدام دکمهی موس کلیک شده است، میتوانیم به خاصیت button
شیء رخداد مراجعه کنیم.
<button>Click me any way you want</button> <script> let button = document.querySelector("button"); button.addEventListener("mousedown", event => { if (event.button == 0) { console.log("Left button"); } else if (event.button == 1) { console.log("Middle button"); } else if (event.button == 2) { console.log("Right button"); } }); </script>
اطلاعاتی که در یک شیء رخداد ذخیره میشود با توجه به نوع رخداد متفاوت خواهد بود. در ادامه فصل، انواع مختلف آن را بحث خواهیم کرد. خاصیت type
این شیء همیشه رشتهای را نگهداری میکند که برای شناسایی رخداد استفاده میشود ( مثل "click"
یا "mousedown"
).
پخش (propagation)
در بیشتر انواع رخدادها، گردانندههایی که روی گرههای دارای فرزند، ثبت شده اند، رخدادهایی که روی فرزندان آنها رخ میدهد را نیز دریافت میکنند. اگر روی دکمهای که درون یک پاراگراف قرار گرفته است کلیک شود، گردانندههای رخداد روی پاراگراف نیز این رخداد کلیک را میبینند.
اما اگر هر دوی دکمه و پاراگراف دارای گرداننده باشند، گرداننده صریح تر – در اینجا دکمه – زودتر اجرا میشود. در این حالت گفته میشود که رخداد به سمت بیرون پخش یا propagate شده است، از گرهای که در آن رخ داده است تا گرهی والدش تا گرهی ریشه در سند. در نهایت بعد از اینکه همهی گردانندههای ثبت شده روی گرههای مشخص به نوبت فراخوانی شدند، گردانندههایی که روی کل پنجره ثبت شده اند فرصت این را خواهند داشت که به رخداد پاسخ دهند.
در هر نقطهای ، یک گردانندهی رخداد میتواند متد stopPropagation
را روی شیء رخداد فراخوانی کند تا مانع از دریافت رخداد توسط گرههای بالاتر شود. این میتواند به عنوان مثال در مواقعی که یک دکمه درون یک عنصر قابل کلیک دیگر دارید و نمی خواهید که کلیک روی آن دکمه باعث فعال شدن رفتار کلیک عنصر دیگر بشود کاربرد دارد.
در مثال پیش رو، گردانندهی "mousedown"
روی دکمه و پاراگراف پیرامونش ثبت میشود. با کلیک دکمهی راست موس ، گردانندهی مربوط به دکمه متد stopPropagation
را فراخوانی میکند که باعث میشود که گردانندهی روی پاراگراف متوقف شود. وقتی با دکمهی دیگر موس روی دکمه کلیک میشود، هر دوی گردانندهها اجرا میشوند.
<p>A paragraph with a <button>button</button>.</p> <script> let para = document.querySelector("p"); let button = document.querySelector("button"); para.addEventListener("mousedown", () => { console.log("Handler for paragraph."); }); button.addEventListener("mousedown", event => { console.log("Handler for button."); if (event.button == 2) event.stopPropagation(); }); </script>
بیشتر اشیاء رخداد، دارای خاصیتی به نام target
میباشند که به گرهای اشاره می کند که به آن تعلق دارند. برای اطمینان از اینکه به صورت تصادفی گرهی دیگری را رسیدگی نکنید، میتوانید از این خاصیت بهره ببرید.
همچنین این امکان وجود دارد که از target
برای پهن کردن یک تور گسترده برای یک نوع خاص از رخداد استفاده کنید. به عنوان مثال، اگر گرهای دارید که حاوی لیست بلندی از دکمههاست، ممکن است مناسب باشد که یک گردانندهی کلیک روی گرهی بیرونی ثبت شود و از خاصیت target
برای بررسی کلیک شدن یک دکمه استفاده شود تا اینکه برای تک تک دکمهها گردانندهای مجزا ثبت شود.
<button>A</button> <button>B</button> <button>C</button> <script> document.body.addEventListener("click", event => { if (event.target.nodeName == "BUTTON") { console.log("Clicked", event.target.textContent); } }); </script>
کارکردهای پیشفرض
به خیلی از رخدادها یک کارکرد پیشفرض اختصاص داده شده است. اگر روی یک پیوند کلیک کنید، به صفحهی هدف پیوند منتقل خواهید شد. اگر کلید پایین را روی صفحهی کلید فشار دهید، مرورگر صفحه را به سمت پایین اسکرول میکند. اگر با موس کلیک راست کنید، به شما یک منوی زمینه، نمایش داده خواهد شد و از این قبیل موارد.
گردانندههای رخداد در جاوااسکریپت، در بیشتر انواع رخدادها، پیش از اینکه رفتار پیشفرض اتفاق بیفتد فراخوانی میشوند. اگر گردانندهی مورد نظر مایل نباشد که رفتار پیشفرض رخ بدهد، معمولا به این دلیل که کنترل آن رخداد را خود به دست گرفته است، میتواند متد preventDefault
را روی شیء رخداد فراخوانی کند.
میتوان از این روش برای پیادهسازی کلیدهای میانبر خودتان یا منوی زمینه استفاده کرد. همچنین میتوان رفتاری که کاربر از یک رخداد انتظار دارد را کاملا خنثی نمود. به عنوان مثال، اینجا پیوندی وجود دارد که نمیشود آن را دنبال کرد.
<a href="https://developer.mozilla.org/">MDN</a> <script> let link = document.querySelector("a"); link.addEventListener("click", event => { console.log("Nope."); event.preventDefault(); }); </script>
سعی کنید به سراغ این گونه کارها نروید مگر اینکه دلیل محکمهپسندی برای آن داشته باشید. کاربرانی که از صفحهی وب شما استفاده میکنند، زمانی که رفتار مورد انتظارشان از کار افتاده باشد، احساس خوبی نخواهند داشت.
بسته به مرورگر مورد استفاده، بعضی از رخدادها را نمیتوان به هیچ وجه متوقف کرد. به عنوان مثال در گوگل کروم، کلید میانبری که برای بستن تب فعلی استفاده میشود (control-W یا command-W) را نمیتوان توسط جاوااسکریپت مدیریت کرد.
رخدادهای مربوط به کلیدها
زمانی که یک کلید در صفحهکلید فشرده میشود، مرورگر شما یک رخداد "keydown"
را ارسال میکند. وقتی کلید رها میشود، یک رخداد "keyup"
به وجود می آید.
<p>This page turns violet when you hold the V key.</p> <script> window.addEventListener("keydown", event => { if (event.key == "v") { document.body.style.background = "violet"; } }); window.addEventListener("keyup", event => { if (event.key == "v") { document.body.style.background = ""; } }); </script>
برخلاف نامش، "keydown"
فقط در زمانی که کلید به پایین فشرده میشود بهوجود نمیآید. زمانی که یک کلید فشرده میشود و در همان حالت نگهداشته میشود، این رخداد با هر بار تکرار آن کلید دوباره ارسال میشود. گاهی اوقات باید حواستان به این رفتار باشد. به عنوان مثال اگر بخواهید دکمهای را با فشردن یک کلید به DOM اضافه کنید و با رها کردن کلید حذف کنید، ممکن است تصادفی صدها دکمه اضافه شود زیرا ممکن است که کلید مورد نظر، زمان بیشتری در حالت فشرده نگه داشته شود.
در مثال، خاصیت key
از شیء رخداد بررسی شد تا مشخص شود که رخداد به کدام کلید مربوط است. این خاصیت یک مقدار رشته ای را نگه میدارد که برای بیشتر کلیدها معادل چیزی است که با فشردن آن کلید تایپ میشود. برای کلیدهای خاص مثل enter، نام آن به صورت رشته نگهداری میشود ("Enter"
در این مورد). اگر کلید shift راه هم در زمان فشردن کلیدی نگه دارید، این کار ممکن است که روی نام کلید تاثیر بگذارد – "v"
به "V"
تبدیل میشود، "1"
به "!"
تبدیل میشود، البته اگر این چیزی است که در صورت فشردن shift-1 در صفحهکلید شما تولید میشود.
کلیدهای اصلاحگر مثل shift، control، alt و meta (کلید command در Mac) شبیه به کلیدهای معمولی رخدادی را ایجاد میکنند. اما وقتی که ترکیب کلیدها را بررسی میکنید، میتوانید ببینید که این کلیدها هم پایین نگهداشته شده اند یا خیر. این کار با نگاه کردن به خاصیتهای shiftKey
، ctrlKey
، altKey
و metaKey
مربوط به رخدادهای موس و صفحه کلید قابل انجام است.
<p>Press Control-Space to continue.</p> <script> window.addEventListener("keydown", event => { if (event.key == " " && event.ctrlKey) { console.log("Continuing!"); } }); </script>
گرهای در DOM، که در آن یک رخداد کلید آغاز میشود، به عنصری که در زمان فشردن کلید، فعال (حالت focus) بوده است بستگی دارد. بیشتر گرهها نمیتوانند درحالت focus قرار گیرند مگر اینکه به آنها خصوصیت tabindex
را اختصاص دهید، اما چیزهایی مثل پیوندها، دکمهها، و فیلدهای فرم این امکان را به صورت پیشفرض دارند. در فصل 18 به بحث فیلدهای فرم خواهیم پرداخت. وقتی عنصر خاصی در صفحه، مورد تمرکز یا توجه نیست، document.body
به عنوان گرهی هدف رخدادهای کلید، در نظر گرفته میشود.
زمانی که کاربر در حال تایپ یک متن است، استفاده از رخدادهای کلید برای تشخیص چیزی که در حال تایپ شدن است با مشکلاتی روبرو میباشد. بعضی پلتفرمها، مخصوصا صفحهکلید مجازی موجود در گوشیهای اندروید، هیچ رخداد کلیدی را ارسال نمیکنند. حتی زمانی که از یک صفحهکلید قدیمی و از مد افتاده استفاده میکنید ، بعضی از انواع ورودی متن با کلیدی که فشرده میشود تطابق ندارند، مثل یک نرم افزار IME (“ویرایشگر روش ورود”) که توسط افرادی استفاده میشود که الفبای زبانشان در صفحهکلید معمولی قابل پیادهسازی نیست، جایی که ترکیب فشردن چند کلید برای ایجاد کاراکترها استفاده میشود.
برای اینکه متوجه شویم که چیزی تایپ شده است، عناصری که میتوان در آنها چیزی را تایپ کرد مثل <input>
و <textarea>
، با هر بار تغییر محتوایشان توسط کاربر، رخداد “"input"
را ایجاد میکنند. برای گرفتن محتوایی که تایپ شده است، بهترین کار این است که آن را مستقیما از فیلدی که مورد تمرکز است بخوانیم. فصل 18 چگونی آن را نشان میدهد.
رخدادهای مربوط به مکاننما
در حال حاضر، دو روش به طور گسترده برای اشاره به قسمتهای روی یک صفحه نمایش استفاده میشود : استفاده از موس ( شامل دیگر ابزار که شبیه به موس عمل میکنند مثل پدلمسی (touchpad) و گوی کنترلی (trackball) ) و صفحات لمسی. این دو روش رخدادهای متنوعی را ایجاد میکنند.
کلیکهای موس
فشردن یک دکمهی موس موجب ایجاد چندین رخداد می گردد. رخدادهای "mousedown"
و "mouseup"
مشابه "keydown"
و "keyup"
هستند و وقتی که دکمهی موس فشرده و رها میشود، ایجاد می گردند. این اتفاق روی گرههایی از DOM میافتد که در هنگام فشردن کلید زیر مکاننمای موس قرار دارند.
بعد از رخداد "mouseup"
، یک رخداد "click"
روی صریح ترین گرهای که هر دوی فشردن و رهاشدن کلید را در بر بگیرد، به وجود میآید. به عنوان مثال، اگر دکمهی موس را روی یک پاراگراف به پایین فشار دهیم و مکاننما را روی یک پاراگراف دیگر ببریم و دکمه را رها کنیم، رخداد "click"
روی عنصری رخ میدهد که هر دوی پاراگرافها را در بر داشته باشد.
اگر دو کلیک نزدیک هم اتفاق بیفتد، همچنین یک رخداد "dblclick"
(جفت کلیک) بعد از رخداد کلیک دوم، ایجاد میشود.
برای بدست آوردن اطلاعات دقیق جایی که رخداد موس در آنجا اتفاق افتاده است، میتوانید به خاصیتهای clientX
و clientY
رجوع کنید، که مختصات رخداد را (به پیکسل) نسبت به گوشهی بالا و چپ پنجره، نگهداری میکنند یا pageX
و pageY
که نسبت به گوشهی بالا و چپ کل سند این کار را انجام میدهند (که ممکن است در صورت اسکرول صفحه متفاوت باشد).
در مثال پیش رو یک برنامهی طراحی ابتدایی ایجاد میکنیم. هر بار که روی سند کلیک میکنید، برنامه یک نقطه زیر مکان کلیک شما اضافه میکند. برای دیدن مثالی کمتر ابتدایی از یک برنامهی طراحی، به فصل 19 مراجعه کنید.
<style> body { height: 200px; background: beige; } .dot { height: 8px; width: 8px; border-radius: 4px; /* rounds corners */ background: blue; position: absolute; } </style> <script> window.addEventListener("click", event => { let dot = document.createElement("div"); dot.className = "dot"; dot.style.left = (event.pageX - 4) + "px"; dot.style.top = (event.pageY - 4) + "px"; document.body.appendChild(dot); }); </script>
حرکت موس
هر بار که مکاننمای موس حرکت میکند، یک رخداد "mousemove"
ایجاد میشود. این رخداد را میتوان برای رصد موقعیت موس استفاده نمود. یک موقعیت رایج که این رخداد مفید خواهد بود زمانی است که شکلی از قابلیت کشیدن عناصر با موس را پیادهسازی میکنیم.
به عنوان یک مثال، برنامهی پیش رو یک میله را نمایش میدهد و گردانندههای رخدادی را تنظیم میکند که باعث میشوند که کشیدن به سمت چپ یا راست میله، موجب باریکتر یا ضخیم تر شدن آن بشود.
<p>Drag the bar to change its width:</p> <div style="background: orange; width: 60px; height: 20px"> </div> <script> let lastX; // Tracks the last observed mouse X position let bar = document.querySelector("div"); bar.addEventListener("mousedown", event => { if (event.button == 0) { lastX = event.clientX; window.addEventListener("mousemove", moved); event.preventDefault(); // Prevent selection } }); function moved(event) { if (event.buttons == 0) { window.removeEventListener("mousemove", moved); } else { let dist = event.clientX - lastX; let newWidth = Math.max(10, bar.offsetWidth + dist); bar.style.width = newWidth + "px"; lastX = event.clientX; } } </script>
توجه داشته باشید که گردانندهی "mousemove"
برای کل پنجره ثبت شده است. حتی اگر موس از محیط میله در حین تغییر اندازه، بیرون برود، تا زمانی که دکمهی موس پایین نگهداشته شود، هنوز قصد داریم تا اندازهی آن را تغییر دهیم.
با رها شدن دکمهی موس باید تغییر اندازه را متوقف کنیم. برای این کار، میتوانیم از خاصیت buttons
( به s انتهای آن توجه داشته باشید) استفاده کنیم، که اطلاعاتی دربارهی دکمههایی که در حال حاضر فشرده نگهداشته شدهاند را فراهم مینماید. زمانی که این خاصیت صفر است، هیچ دکمهای در حالت فشرده نمانده است. زمانی که دکمههایی فشرده مانده باشند، مقدار این خاصیت برابر با جمع کدهای هریک از دکمه ها خواهد بود – دکمهی چپ موس دارای کد 1، دکمهی راست 2 و دکمهی وسط دارای کد 4 است. با این کار میتوانید بررسی کنید که آیا دکمهی داده شده در حالت فشرده قرار دارد یا خیر. این کار با گرفتن باقیماندهی مقدار buttons
و کد آن بدست می آید.
توجه داشته باشید که ترتیب این کدها با ترتیبی که توسط button
استفاده میشد متفاوت است، دکمهی وسط قبل از دکمهی راست می آید. همانطور که ذکر شد، ثبات چیزی نیست که واقعا به عنوان یک نقطهی قوت برای رابط برنامهنویسی در مرورگرها بتوان در نظر گرفت.
رخدادهای لمسی
با توجه به زمانی که صفحات لمسی بسیار نادر بودند، سبک مرورگرهای گرافیکیای که ما استفاده میکنیم برای کار با موس طراحی شدهاند. برای اینکه وب روی گوشیهای اولیهی لمسی “کار” بکند، در مرورگرهایی که برای آن گوشیها ساخته میشد، تا حدی رخدادهای لمسی همان رخدادهای موس بودند. اگر روی صفحه ضربه بزنید، رخدادهای "mousedown"
، "mouseup"
و "click"
ایجاد خواهند شد.
اما این ترفند زیاد قدرتمند نیست. روش کار یک صفحهی لمسی کاملا با موس متفاوت است: یک صفحهی لمسی کلیدهای متعدد ندارد، نمیتوان انگشت را زمانی که روی صفحه قرار ندارد، رصد کرد ( تا "mousemove"
را شبیه سازی کنید) و میتوان در آن چندین انگشت را همزمان روی صفحه نمایش داشت.
رخدادهای موس فقط مواردی از تعاملات لمسی با صفحه را پوشش میدهند که ساده و سرراست هستند – اگر به یک دکمه یک گردانندهی "click"
اضافه کنید، کاربران صفحات لمسی نیز میتوانند از آن استفاده کنند. اما چیزی مثل یک میله با اندازهی قابل تغییر که در مثال قبل آمد روی صفحات لمسی کار نمیکند.
رخدادهای بخصوصی برای کارهای لمسی وجود دارند. زمانی که انگشت شروع به لمس صفحه میکند، شما یک رخداد "touchstart"
دریافت میکنید. زمانی که انگشت خود را مماس با صفحه حرکت میدهید، رخداد "touchmove"
ایجاد میشود. و در نهایت زمانی که لمس صفحه پایان می یابد شما یک رخداد "touchend"
دریافت میکنید.
به دلیل اینکه خیلی از صفحات لمسی میتوانند چند انگشت را همزمان شناسایی کنند، این رخدادها یک مجموعهی واحدی از مختصات مربوط به نقاط را ندارند. به جای آن، اشیاء این رخدادها دارای خاصیتی به نام touches
میباشند که شیءای آرایهگونه از نقاط را نگهداری میکند که هر کدامشان دارای خاصیتهای clientX
، clientY
، pageX
و pageY
مربوط به خود میباشند.
میتوانید با استفاده از کدی شبیه زیر دور قسمتهایی که با انگشت لمس شده اند دایرهای قرمز رنگ بکشید.
<style> dot { position: absolute; display: block; border: 2px solid red; border-radius: 50px; height: 100px; width: 100px; } </style> <p>Touch this page</p> <script> function update(event) { for (let dot; dot = document.querySelector("dot");) { dot.remove(); } for (let i = 0; i < event.touches.length; i++) { let {pageX, pageY} = event.touches[i]; let dot = document.createElement("dot"); dot.style.left = (pageX - 50) + "px"; dot.style.top = (pageY - 50) + "px"; document.body.appendChild(dot); } } window.addEventListener("touchstart", update); window.addEventListener("touchmove", update); window.addEventListener("touchend", update); </script>
گاهی لازم میشود که preventDefault
را در گردانندههای رخداد لمسی فراخوانی کنید تا رفتار پیشفرض مرورگر را تغییر دهید ( که ممکن است شامل اسکرولشدن صفحه در صورت کشیدن انگشت به اطراف باشد) و از بهوجود آمدن رخدادهای موس جلوگیری کنید، که ممکن است برای آن رخدادها، گردانندهی مجزایی در نظر گرفته باشید.
رخدادهای scroll
هر بار که عنصری اسکرول میشود، یک رخداد "scroll"
روی آن اجرا میشود. این موضوع کاربردهای متنوعی دارد؛ مثلا برای دانستن چیزی که کاربر در حال مشاهده است ( برای غیرفعالسازی جلوههای متحرکی که خارج از قسمت قابل مشاهده قرار می گیرند یا ارسال گزارشاتی شیطانی برای دفتر مرکزی شرکتتان) یا نمایش نمادهایی از میزان پیشرفت کاربر ( با برجسته سازی بخشهای فهرست محتوا یا شماره صفحات).
مثال پیش رو یک نوار پیشرفت را در قسمت بالای صفحه ایجاد میکند و با میزان اسکرول صفحه این نوار تکمیل میشود.
<style> #progress { border-bottom: 2px solid blue; width: 0; position: fixed; top: 0; left: 0; } </style> <div id="progress"></div> <script> // Create some content document.body.appendChild(document.createTextNode( "supercalifragilisticexpialidocious ".repeat(1000))); let bar = document.querySelector("#progress"); window.addEventListener("scroll", () => { let max = document.body.scrollHeight - innerHeight; bar.style.width = `${(pageYOffset / max) * 100}%`; }); </script>
اگر position
یک عنصر را fixed
قرار بدهیم نتیجه شبیه به استفاده از موقعیت دهی absolute
میشود اما در این حالت عنصر دیگر همراه با صفحه اسکرول نمیشود. این کار برای این است که نوار پیشرفت ما در بالای صفحه باقی بماند. تغییر عرض این نوار نمایانگر میزان پیشرفت خواهد بود. ما از %
به جای px
به عنوان واحد برای تنظیم عرض نوار استفاده میکنیم تا عنصر با توجه و نسبت به طول صفحه تغییر اندازه دهد.
متغیر سراسری innerHieght
به ما ارتفاع صفحه را میدهد که باید آن را از کل ارتفاع قابل اسکرول کم کنید – زمانی که به انتهای سند میرسید، نمیتوانید به اسکرول ادامه دهید. همچنین innerWidth
برای عرض صفحه وجود دارد. با تقسیم pageYOffset
، موقعیت اسکرول فعلی، بر موقعیت بیشینهی اسکرول و ضرب آن در 100، درصد پیشرفت را برای نوار پیشرفت بدست می آوریم.
فراخوانی preventDefault
روی یک رخداد scroll مانع از انجام اسکرول صفحه نمیشود. در واقع، گردانندهی رخداد فقط بعد از اینکه اسکرول صفحه اتفاق میافتد فراخوانی میشود.
رخدادهای Focus
زمانی که یک عنصر در صفحه فعال میشود، مرورگر یک رخداد "focus"
را روی آن ایجاد می کند. زمانی که عنصر، دیگر فعال نیست، یک رخداد "blur"
دریافت میکند.
برخلاف رخدادهایی که پیشتر بحث شد، این دو رخداد پخش نمیشوند (propagate). گردانندهای که در عنصر والد قرار دارد متوجه فعال شدن یا از دست دادن توجه کاربر از عنصر فرزندش نمیشود.
در مثال پیش رو یک متن راهنما برای فیلد متنی که در حال حاضر فعال است نشان داده میشود:
<p>Name: <input type="text" data-help="Your full name"></p> <p>Age: <input type="text" data-help="Your age in years"></p> <p id="help"></p> <script> let help = document.querySelector("#help"); let fields = document.querySelectorAll("input"); for (let field of Array.from(fields)) { field.addEventListener("focus", event => { let text = event.target.getAttribute("data-help"); help.textContent = text; }); field.addEventListener("blur", event => { help.textContent = ""; }); } </script>
شیء window دو رخداد "focus"
و "blur"
را زمانی دریافت میکند که کاربر از/یا به تب مرورگر یا پنجرهای که سند در آن نمایش داده میشود برود.
رخداد بارگیری - load
زمانی که بارگیری یک صفحه تمام میشود، رخداد "load"
روی شیء window و body سند ایجاد میشود. این رخداد معمولا برای زمانبندی کارهای آغازینی که برای کارکرد نیاز دارند که کل سند بارگیری شده باشد استفاده میشود. به خاطر داشته باشید که محتوای برچسب <script>
بلافاصله بعد از اینکه این برچسب مشاهده میشود اجرا میشود. ممکن است این اتفاق خیلی زودتر از موعد رخ بدهد، برای مثال، زمانی که اسکریپت لازم است با با بخشهایی از سند که بعد از برچسب <script>
می آیند کار کند.
عناصری مثل تصاویر و برچسبهای script که یک فایل بیرونی را بارگیری میکنند نیز یک رخداد "load"
دارند که نشان میدهد که فایلی که به آن ارجاع داده اند بارگیری شده است. شبیه رخدادهای مربوط به focus
، رخدادهای مربوط به بارگیری نیز “پخش” نمی شوند.
زمانی که یک صفحه بسته شود یا کاربر از آن خارج گردد (مثلا با رفتن به یک صفحهی دیگر)، یک رخداد "beforeunload"
اجرا میشود. کاربرد اصلی این رخداد برای جلوگیری از خروج تصادفی کاربر از صفحه و از دست دادن کارهایی است که در صورت خروج از صفحه، رخ میدهد. جلوگیری از خروج از صفحه همان طور که ممکن است حدس زده باشید، نمیتواند با متد preventDefault
انجام شود. در عوض، میتوان این کار را با برگرداندن یک مقدار غیر null از گرداننده میسر ساخت. زمانی که این کار را انجام میدهید، مرورگر یک پنجرهی تعاملی به کاربر نشان میدهد که از او بپرسد آیا مطمئن است که قصد خروج از صفحه را دارد. این مکانیزم اطمینان حاصل میکند که کاربر همیشه قادر باشد که صفحه را ترک کند حتی در صفحات مخربی که ترجیح میدهند کاربران را برای همیشه در صفحه حبس کرده و مجبورشان کنند که تبلیغات مسخره کاهش وزن را نگاه کنند.
رخدادها و حلقهی رخداد
در بستر حلقهی رخداد، که در فصل 11 بحث شد، گردانندههای رخداد مرورگر شبیه به دیگر اعلانهای ناهمگام عمل میکنند. این گردانندهها در زمان اتفاق رخدادها زمانبندی میشوند، اما قبل از اینکه شانس اجرا داشته باشند، باید برای دیگر اسکریپتها منتظر بمانند که اجرایشان پایان یابد .
این حقیقت که آن رخدادها فقط میتوانند زمانی پردازش شوند که چیز دیگری در حال اجرا نباشد به این معنا است که اگر حلقهی رخداد با دیگر کارها گره بخورد، هر تعامل با صفحه (که توسط رخدادها انجام میشود) تا زمانی که زمان برای پردازش آن وجود دارد به تاخیر خواهد افتاد. بنابراین اگر کار زیادی را زمانبندی کنید، چه با گردانندههای رخداد زمانگیر یا تعداد زیادی گردانندهی کوچک، صفحه کند میشود و استفاده از آن سخت خواهد شد.
برای مواردی که واقعا لازم است تا بعضی کارهای زمان-گیر را در پیشزمینه انجام دهید و صفحه از کار نیفتد ، مرورگرها چیزی به نام web workers (کارگزاران وب) را فراهم ساختهاند. یک کارگزار یک پردازش جاوااسکریپت است که در کنار اسکریپت اصلی اجرا میشود و خط زمانی خودش را دارد.
فرض کنید که محاسبهی مربع یک عدد کاری سنگین باشد، یک محاسبه زمانگیر که قصد داریم آن را در thread یا نخ دیگری انجام دهیم. میتوانیم فایلی به نام code/
ایجاد کنیم که به پیامها با محاسبهی یک مربع پاسخ داده و پیامی را به عنوان پاسخ برگرداند.
addEventListener("message", event => { postMessage(event.data * event.data); });
برای پیشگیری از بروز مشکلات داشتن چند thread که روی یک داده کار میکنند، کارگزاران، قلمروی سراسریشان یا هر دادهی دیگری را با محیط اصلی اسکریپت به اشتراک نمی گذارند. در عوض، ایجاد ارتباط با آنها باید از طریق ارسال و دریافت پیام انجام شود.
در کد زیر، کارگزاری تعریف میشود که یک اسکریپت را اجرا میکند؛ چندین پیام به آن ارسال کرده و پاسخها را به خروجی می فرستد.
let squareWorker = new Worker("code/squareworker.js"); squareWorker.addEventListener("message", event => { console.log("The worker responded:", event.data); }); squareWorker.postMessage(10); squareWorker.postMessage(24);
تابع postMessage
یک پیام ارسال میکند، که باعث میشود که یک رخداد "message"
در دریافتکننده ایجاد گردد. اسکریپتی که کارگزار (worker) را ایجاد کرده است پیام ها را از طریق شیء Worker
ارسال و دریافت میکند، جاییکه کارگزار با اسکریپتی که آن را ایجاد کرده است به وسیلهی ارسال و شنود مستقیم روی قلمروی سراسریاش، ارتباط برقرار میکند. فقط مقادیری که میتوان آنها را به صورت JSON نمایش داد میتوانند به عنوان پیامها ارسال شوند – سمت دیگر یک کپی از آنها را دریافت میکند نه خودشان را.
زمانسنج
در فصل 11 با تابع setTimeout
آشنا شدیم. این تابع، تابع دیگری را برای فراخوانی بعد از گذشت زمان داده شده به هزارم ثانیه زمانبندی میکند.
گاهی لازم است که اجرای تابعی را که زمانبندی کردهاید، لغو کنید. این کار با ذخیرهی مقداری که از تابع setTimeout
برگردانده میشود و فراخوانی clearTimeout
روی آن صورت میگیرد.
let bombTimer = setTimeout(() => { console.log("BOOM!"); }, 500); if (Math.random() < 0.5) { // 50% chance console.log("Defused."); clearTimeout(bombTimer); }
تابع cancelAnimationFrame
به همان صورت که تابع clearTimeout
عمل میکرد، کار میکند – فراخوانی آن روی مقدار برگشتی توسط requestAnimationFrame
باعث میشود که آن فریم لغو شود (با فرض این که پیش از آن فراخوانی نشده باشد).
یک مجموعهی مشابه از توابع، setInterval
و clearInterval
برای تنظیم زمانسنجهایی که باید هر X هزارم ثانیه تکرار شوند، استفاده میشود.
let ticks = 0; let clock = setInterval(() => { console.log("tick", ticks++); if (ticks == 10) { clearInterval(clock); console.log("stop."); } }, 200);
Debouncing (کاهش دفعات رسیدگی)
بعضی انواع رخدادها این قابلیت را دارند که به سرعت، و به دفعات در یک ردیف اجرا شوند ( برای مثال "mousemove"
و "scroll"
). در هنگام رسیدگی به این رخدادها، باید مواظب باشید که کاری که خیلی زمانگیر است را انجام ندهید که در این صورت گردانندهی شما زمان زیادی میگیرد و تعامل با صفحه با مشکل کندی روبرو میشود.
اگر لازم است که کاری جدی در این گونه گردانندهها انجام دهید، میتوانید با استفاده از setTimeout
اطمینان حاصل کنید که این کار را به دفعات کمتری انجام میدهید. این کار معمولا debounce رخداد نامیده میشود. برای این کار روشهای نسبتا متفاوتی وجود دارد.
در مثلا اول، قصد داریم با تایپ چیزی توسط کاربر واکنش نشان دهیم اما نمی خواهیم این کار را برای هر رخداد ورودی انجام دهیم. در لحظاتی که کاربر به سرعت تایپ می کند، کمی صبر میکنیم تا یک وقفه در تایپ به وجود بیاید. به جای اینکه بلافاصله کاری در گرداننده رخداد انجام دهیم، یک زمانسنج تنظیم میکنیم. همچنین timeout قبلی را هم متوقف میکنیم (در صورت وجود) در نتیجه هنگامیکه رخدادها نزدیک به هم رخ میدهند (نزدیک تر از وقفهی زمانسنج ما) timeout متعلق به رخداد قبلی لغو میشود.
<textarea>Type something here...</textarea> <script> let textarea = document.querySelector("textarea"); let timeout; textarea.addEventListener("input", () => { clearTimeout(timeout); timeout = setTimeout(() => console.log("Typed!"), 500); }); </script>
اگر به تابع clearTimeout
مقداری undefined بدهیم یا اینکه آن را روی یک timeout که پیشتر اجرا شده فراخوانی کنیم هیچ اثری تولید نخواهد کرد. بنابراین نیازی نیست که به زمان فراخوانی آن دقت کنیم و میتوانیم برای همهی رخدادها این کار را انجام دهیم.
میتوانیم از الگویی کمی متفاوت استفاده کنیم اگر بخواهیم بین پاسخ ها فاصله بیاندازیم و بین آنها یک حداقل زمان مشخص فاصله باشد اما باید آنها را در طول یک مجموعه از رخدادها اجرا کنیم نه بعد از آنها. به عنوان مثال، ممکن است بخواهیم به رخدادهای "mousemove"
با نشاندادن مختصات فعلی موس پاسخ بدهیم اما بعد از هر 250 هزارم ثانیه.
<script> let scheduled = null; window.addEventListener("mousemove", event => { if (!scheduled) { setTimeout(() => { document.body.textContent = `Mouse at ${scheduled.pageX}, ${scheduled.pageY}`; scheduled = null; }, 250); } scheduled = event; }); </script>
خلاصه
گردانندههای رخداد این امکان را فراهم میکنند که رخدادهایی که در صفحهی وب ما اتفاق می افتند را شناسایی و به آنها واکنش نشان دهیم. متد addEventListener
برای ثبت گردانندهها استفاده میشود.
هر رخداد دارای یک نوع است ("keydown"
، "focus"
و از این قبیل) که برای شناسایی آن استفاده میشود. بیشتر رخدادها روی عناصر بخصوصی از DOM فراخوانی میشوند و بعد از آن به سمت عناصر والد (اجداد) آن پخش (propagate) میشوند که به گردانندههای ثبت شده برای آن عنصرها نیز امکان واکنش به رخداد را فراهم میکنند.
زمانی که یک گردانندهی رخداد فراخوانی میشود، یک شیء رخداد که حاوی اطلاعات بیشتری دربارهی رخداد است به آن ارسال میشود. این شیء دارای متدهایی است که میتوان با آنها از پخش رخداد جلوگیری کرد (stopPropagation
) و مانع از اجرای واکنش پیشفرض مرورگر به رخداد شد (preventDefault
).
با فشردن یک کلید دو رخداد "keydown"
و "keyup"
اجرا میشوند. فشردن یک کلید موس نیز سه رخداد "mousedown"
، "mouseup"
و "click"
را اجرا میکند. حرکت دادن موس باعث ایجاد رخدادهای "mousemove"
میشود. تعامل با صفحهی لمسی باعث ایجاد رخدادهای "touchstart"
، "touchmove"
و "touchend"
میشود.
اسکرول صفحه را میتوان با رخداد "scroll"
شناسایی کرد و فعال شدن عناصر صفحه را میتوان با "focus"
و "blur"
تشخیص داد. زمانی که بارگیری یک سند پایان مییابد، یک رخداد "load"
روی window اجرا میشود.
تمرینها
بالون
صفحهای ایجاد کنید که یک بالون را نمایش دهد ( با استفاده از ایموجی بالون 🎈). زمانی که کلید بالا را در صفحهکلید فشار میدهید، بالون باید ده درصد باد شود (بزرگ شود) و زمانی که کلید پایین را فشار میدهید بالون باید ده درصد کوچک شود.
میتوانید اندازهی متن (ایموجیها متن محسوب میشوند) را با تنظیم خاصیت font-size
در CSS یا (style.fontSize
)) برای عنصر والدش کنترل کنید. به خاطر داشته باشید که یک واحد اندازه گیری در مقدار قرار دهید، برای مثال (10px
).
نام کلیدهای جهتدار در صفحهکلید "ArrowUp"
و "ArrowDown"
است. مطمئن شوید که این کلیدها اندازهی بالون را فقط تغییر میدهند و باعث اسکرول شدن صفحه نمیشوند.
وقتی تا اینجای کار به درستی کار کرد، امکانی اضافه کنید که در آن با بزرگتر شدن بالون از یک حد مشخص، بالون بترکد. در این جا ترکیدن را میتوان با جایگزینی ایموجی بالون با یک ایموجی 💥 انجام داد و گردانندهی رخداد نیز حذف شود ( تا دیگر انفجار را بزرگ یا کوچک نکند).
<p>🎈</p> <script> // Your code here </script>
لازم خواهید داشت تا یک گرداننده برای رخداد "keydown"
ثبت کنید و به سراغ event.key
بروید تا متوجه شوید کلید بالا فشرده شده است یا کلید پایین.
میتوان اندازهی کنونی را در یک متغیر نگهداری نمود که در این صورت میتوانید اندازهی جدید را بر مبنان آن تعریف کنید. اگر یک تابع برای تغییر اندازه - متغیر و سبک بالون در DOM - در نظر بگیرید، کار خوبی است و میتوانید از آن گردانندهی رخدادتان استفاده کنید، و احتمالا در شروع برای تنظیم اندازهی ابتدایی.
برای تغییر بالون به حالت منفجر شده میتوانید گرهی متن را با ایموجی جدید (با استفاده از replaceChild
) تغییر دهید یا خاصیت textContent
متعلق به والدش را با یک رشتهی جدید مقداردهی کنید.
دنبالهی موس
در روزهای اولیهی استفاده از جاوااسکریپت، که بورس صفحات وب جلف و پر زرق و برق و پر از عکسهای متحرک بود، بعضی افراد روشهای الهامبخشی برای استفاده از جاوااسکریپت در آن فضا پیدا کرده بودند.
یکی از آن روشها جلوهی دنبالهی موس بود – مجموعهای از عناصر که با حرکت موس در صفحه به دنبال مکاننمای آن حرکت میکردند.
در این تمرین، از شما می خواهم که یک دنبالهی موس درست کنید. از عناصر <div>
که به صورت مطلق (absolute) مقداردهی شده اند با اندازهی ثابت و رنگ پیشزمینه (برای مثال به کدی که در قسمت کلیکهای موس وجود دارد مراجعه کنید) استفاده کنید. به تعداد کافی از این عناصر ایجاد کنید و زمانی که موس حرکت میکند آنها را مثل ردپای مکاننمای موس به نمایش بگذارید.
روشهای متنوعی برای پیادهسازی این کار وجود دارد. میتوانید راه حلی پیچیده یا ساده را برگزینید. یک راهحل ساده برای شروع این است که تعداد ثابتی از عناصری دنبالهرو داشته باشیم و بین آنها بچرخیم و با هر بار ایجاد یک رخداد "mousemove"
عنصر بعدی را به موقعیت فعلی موس منتقل کنیم.
<style> .trail { /* className for the trail elements */ position: absolute; height: 6px; width: 6px; border-radius: 3px; background: teal; } body { height: 300px; } </style> <script> // Your code here. </script>
بهترین روش ایجاد عنصرها در اینجا استفاده از یک حلقه است. عناصر را به سند الحاق کنید تا نمایش داده شوند. برای اینکه بتوان در ادامه به آنها دسترسی داشت و موقعیتشان را تغییر داد، لازم است تا آنها را درون یک آرایه ذخیره کنید.
پیمایش آنها را میتوان با استفاده از یک متغیر شمارنده و افزودن 1 به آن با هر بار ارسال "mousemove"
صورت داد. عملگر باقیمانده (% elements.
) را میتوان در ادامه برای دریافت یک خانهی معتبر آرایه برای گرفتن عنصر مورد نظر و موقعیت دهی آن در آن رخداد استفاده نمود.
یک جلوهی جالب دیگر را نیز میتوان با مدلسازی یک سیستم فیزیکی ساده پیادهسازی کرد. از رخداد "mousemove"
فقط برای بهروزرسانی یک جفت متغیر که موقعیت موس را رصد میکنند استفاده کنید. سپس به سراغ requestAnimationFrame
برای شبیهسازی حالتی بروید که عناصر دنبالهرو جذب مکان موس میشوند. در هر گام انیمیشن، موقعیتهای آنها را بر اساس موقعیت آنها نسبت به مکاننما (و یک سرعت اختیاری که برای هر عنصر ذخیره شده است) بهروز کنید.
برگهها
پنلهای برگهدار (tabbed panels) به صورت گستردهای در رابطهای کاربر استفاده میشوند. این پنلها به شما امکان انتخاب پنل خاصی از بین تعدادی برگهی موجود فراهم میکنند که پنل منتخب به شکلی برجسته نمایش داده میشود.
در این تمرین شما باید یک پنل برگهدار ساده را پیاده سازی کنید. تابعی به نام asTabs
بنویسید که یک گرهی DOM را گرفته و یک رابط برگهدار را ایجاد میکند که عناصر فرزند آن گره را نشان میدهد. این تابع باید لیستی از عناصر <button>
را در بالای گره برای هر یک از عناصر فرزند نمایش دهد که هر دکمه عنوانش را از خصوصیت data-tabname
عنصر فرزند دریافت میکند. همهی عناصر فرزند به جز یک عنصر باید مخفی شوند ( با تنظیم خاصیت display
با مقدار none
). گرهای که در حالت فعلی قابل مشاهده است با کلیک کردن روی دکمهها مشخص میشود.
بعد از انجام آن، دکمهای که فعال است را سبکدهی متفاوتی کنید که مشخص باشد کدام برگه انتخاب شده است.
<tab-panel> <div data-tabname="one">Tab one</div> <div data-tabname="two">Tab two</div> <div data-tabname="three">Tab three</div> </tab-panel> <script> function asTabs(node) { // Your code here. } asTabs(document.querySelector("tab-panel")); </script>
یک دام که ممکن است در آن بیفتید این است که شما نمیتوانید مستقیما به سراغ خاصیت childNodes
به عنوان یک مجموعه از گرههای برگه بروید. اولا, وقتی دکمهها (buttons) را اضافه میکنید ، آنها نیز تبدیل به گرههای فرزند میشوند و در این شیء قرار میگیرند زیرا این شیء یک ساختار دادهی زنده است. دوما, گرههای متنی که برای فضاهای خالی بین گرهها ایجاد شده اند نیز در childNodes
موجود هستند اما نباید برای آنها برگه در نظر گرفته شود. میتوانید از children
بجای childNodes
برای این منظور استفاده کنید.
میتوانید با ساختن یک آرایه از برگهها کار را شروع کنید که در این صورت به آسانی در دسترس شما خواهند بود. برای پیادهسازی سبکهای دکمهها، میتوانید شیءهایی را ذخیره کنید که حاوی هم پنل برگه و هم دکمهی آن باشد.
توصیهی من این است که یک تابع مجزا برای تغییر برگهها بنویسید. میتوانید یا برگهی انتخاب شدهی قبلی را ذخیره کنید و سبکهایی که برای پنهانسازی آن و نمایش برگهی جدید لازم است تغییر دهید یا فقط سبک همهی برگهها را با هر بار انتخاب یک برگهی جدید تغییر دهید.
ممکن است بخواهید که این تابع را بلافاصله فراخوانی کنید تا رابط کاربری شما در ابتدا با یک برگهی مشخص نمایش داده شود.