Chapter 15رسیدگی به رخداد‌ها

آنچه در اختیار شماست، ذهن‌تان است نه رخداد‌های جهان بیرون. درک این موضوع به شما نیرو می‌بخشد.

مارکوس اورلیوس, تاملات
Picture a Rube Goldberg machine

بعضی برنامه‌ها با ورودی‌ کاربر سر و کار دارند؛ مانند کارهایی که توسط موس و صفحه‌کلید انجام می شود. این نوع ورودی‌ به صورت یک ساختار داده‌ی سازمان‌یافته و مرتب در دسترس قرار نمی گیرد – بلکه به صورت تدریجی و با اجرای برنامه دریافت می شود و برنامه می‌بایست همزمان با دریافت آن، واکنش نشان دهد.

گرداننده‌های رخداد

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

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

البته برنامه باید به خاطر داشته باشد که به سراغ صف برود و این کار را مدوام انجام دهد چرا که وجود فاصله‌ی زمانی بین فشردن کلید و باخبر شدن برنامه از رخداد، باعث می شود که نرم‌افزار روان کار نکند و مشکل‌دار به‌نظر برسد. این رویکرد را 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/squareworker.js ایجاد کنیم که به پیام‌ها با محاسبه‌ی یک مربع پاسخ داده و پیامی را به عنوان پاسخ برگرداند.

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.length) را می‌توان در ادامه برای دریافت یک خانه‌ی معتبر آرایه برای گرفتن عنصر مورد نظر و موقعیت دهی آن در آن رخداد استفاده نمود.

یک جلوه‌ی جالب دیگر را نیز می‌توان با مدل‌سازی یک سیستم فیزیکی ساده پیاده‌سازی کرد. از رخداد "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 برای این منظور استفاده کنید.

می‌توانید با ساختن یک آرایه‌ از برگه‌ها کار را شروع کنید که در این‌ صورت به آسانی در دسترس شما خواهند بود. برای پیاده‌سازی سبک‌های دکمه‌ها، می‌توانید شیءهایی را ذخیره کنید که حاوی هم پنل برگه و هم دکمه‌ی آن باشد.

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

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