فصل 14DOM یا مدل شیء سند

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

فردریش نیچه, فراسوی نیک و بد
Picture of a tree with letters and scripts hanging from its branches

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

این نمایش از سند، یکی از ابزارهایی است که یک برنامه‌ی جاوااسکریپت در جعبه‌ی شنی (sandbox) خود به آن دسترسی دارد. یک ساختار داده که می‌تواند خوانده شود یا تغییر یابد؛ ساختار داده‌ای زنده: زمانی که تغییری در آن رخ می‌دهد، صفحه‌ای که در مانیتور نمایش داده می شود نیز به‌روز می‌شود تا تغییرات را منعکس کند.

ساختار سند

می‌توانید یک سند HTML را به عنوان مجموعه‌ای از مستطیل‌های تودرتو در نظر بگیرید. برچسب‌هایی مثل <body> و </body>، دیگر برچسب‌ها را در بر می گیرند، که خود نیز حاوی برچسب‌های دیگر یا متن می‌باشند. به عنوان مثال، سندی از فصل قبل را مشاهده ‌می‌کنید:

<!doctype html>
<html>
  <head>
    <title>My home page</title>
  </head>
  <body>
    <h1>My home page</h1>
    <p>Hello, I am Marijn and this is my home page.</p>
    <p>I also wrote a book! Read it
      <a href="http://eloquentjavascript.net">here</a>.</p>
  </body>
</html>

ساختار این صفحه به شکل زیر است:

HTML document as nested boxes

ساختار داده‌ای که مرورگر برای نمایش این سند استفاده می‌کند از این شکل پیروی می کند. برای هر مستطیل، یک شیء وجود دارد، که می‌توانیم با آن ارتباط برقرار کرده تا چیزهایی مثل برچسب HTML آن یا برچسب‌ها و متونی که در بر دارد را بدست بیاوریم. این طرز نمایش را مدل شیء سند یا به اختصار DOM می گویند.

متغیر سراسری document امکان دسترسی به این اشیاء را فراهم می سازد. خاصیت documentElement آن به شیئی ارجاع می‌دهد که نمایانگر برچسب <html> است. چون هر سند HTML دارای یک سرصفحه و یک بدنه می‌باشد ، در نتیجه دارای خاصیت‌های head و body نیز می‌باشد که به آن عناصر اشاره می‌کنند.

درخت‌ها

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

یک ساختار داده را زمانی یک درخت می‌نامیم که دارای شاخه‌هایی بدون دور باشد ( یک گره ممکن نیست به شکل مستقیم یا غیر مستقیم حاوی خودش باشد) و همچنین یک ریشه مشخص داشته باشد. در رابطه با DOM، ریشه‌ی درخت، document.documentElement می‌باشد.

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

یک درخت معمول دارای انواع مختلفی از گره‌ها می‌باشد. درخت گرامر زبان Egg دارای شناسه‌ها، مقادیر، و گره‌های کاربرد (Application) بود. گره‌های کاربرد می‌توانستند دارای فرزند باشند، درحالیکه شناسه‌ها و مقادیر از جنس برگ بودند؛ منظور گره‌هایی است که فرزندی ندارند.

همین روال برای DOM هم برقرار است. گره‌ها به عنوان عناصر، که برچسب‌های HTML را نمایش می‌دهند، ساختار سند را تعیین می‌کنند. این گره‌ها می‌توانند گره‌های فرزند داشته باشند. یک نمونه از این نوع گره‌ها document.body است. بعضی از این فرزندان می توانند گره‌هایی از نوع برگ باشند، مثل متن‌ها یا گره‌های توضیحات.

هر شیء گره‌ی DOM دارای خاصیتی به نام nodeType است که حاوی کدی (عددی) است که نوع آن گره را مشخص می‌کند. عناصر دارای کد 1 می‌باشند که همچنین به صورت یک خاصیت ثابت Node.ELEMENT_NODE نیز در دسترس است. گره‌های متنی ، که نمایانگر یک قسمت از متن در سند می‌باشند دارای کد 3 می‌باشند (Node.TEXT_NODE). کد 8 نیز به توضیحات اختصاص دارد (Node.COMMENT_NODE).

یک روش دیگر برای به تصویر کشیدن درخت مربوط به سندمان، شکل زیر است.

HTML document as a tree

برگ‌ها، گره‌های متنی هستند و پیکان‌ها نمایانگر روابط والد-فرزندی بین گره‌ها می‌باشند.

استاندارد

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

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

یک نمونه از این یکپارچگی ضعیف، خاصیت childNodes است که در گره‌های عنصر در DOM، وجود دارند. این خاصیت یک شیء آرایه‌طور را نگهداری می‌کند که خاصیتی به نام length دارد و همچنین خاصیت‌هایی دارد که توسط اعداد برچسب‌گذاری شده اند تا بتوان به گره‌های فرزند دسترسی داشت. اما این یک نمونه از نوع NodeList است نه یک آرایه‌ی واقعی بنابراین متدهای آرایه مثل slice و map را ندارد.

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

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

حرکت در درخت

گره‌های DOM حاوی پیوندهای زیادی به دیگر گره‌های نزدیک می‌باشند. نمودار زیر این موضوع را به تصویر می کشد:

Links between DOM nodes

اگرچه این نمودار فقط 1 پیوند برای هر نوع نشان می‌دهد، اما هر گره خاصیتی به نام parentNode دارد که در صورت وجود به گره‌ای اشاره می‌کند که خودش بخشی از آن است. به همین صورت، هر گره‌ی عنصر (گره نوع ‍‍1) دارای یک خاصیت childNodes است که به یک شیء آرایه‌طور اشاره می‌کند که حاوی فرزندان آن گره می‌باشد.

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

همچنین خاصیتی به نام children وجود دارد که شبیه به childeNodes است با این تفاوت که فقط فرزندان نوع عنصر (نوع 1) را شامل می‌شود نه دیگر انواع گره فرزند. این خاصیت می‌تواند در زمانی که به گره های متنی نیازی ندارید استفاده شود.

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

function talksAbout(node, string) {
  if (node.nodeType == Node.ELEMENT_NODE) {
    for (let i = 0; i < node.childNodes.length; i++) {
      if (talksAbout(node.childNodes[i], string)) {
        return true;
      }
    }
    return false;
  } else if (node.nodeType == Node.TEXT_NODE) {
    return node.nodeValue.indexOf(string) > -1;
  }
}

console.log(talksAbout(document.body, "book"));
// → true

چون childNodes یک آرایه‌ی واقعی نیست نمی‌توان برای پیمایش آن از for/of استفاده کرد بلکه باید یا از یک حلقه‌ی معمولی for بهره برد یا از Array.from استفاده نمود.

خاصیت nodeValue یک گره‌ی متنی، حاوی رشته‌ی متنی است که گره نشان می‌دهد.

پیدا کردن عناصر

حرکت در طول پیوندها به گره‌های والد، فرزندان و گره‌های همجوار اغلب مفید است. اما اگر بخواهیم یک گره‌ی خاص را در یک سند پیدا کنیم، شروع از document.body و پیمایش یک مسیر ثابت از خاصیت‌ها برای یافتن گره، ایده‌ی خوبی نیست. برای این کار نیاز است تا فرض‌هایی درباره‌ی یک ساختار دقیق از سند داشته باشیم – ساختاری که احتمالا قرار است آن را تغییر دهید. یکی دیگر از فاکتورهایی که کار را پیچیده می‌کند این است که گره‌های متنی برای فضاهای خالی بین گره‌ها هم ایجاد می‌شوند. مثلا در برچسب <body> سند، فقط سه فرزند ندارد (<h1> و دو <p> ) بلکه در واقع دارای هفت فرزند می‌باشد. آن سه عنصر به همراه فضاهای خالی قبل، بعد و بین‌شان.

بنابراین اگر بخواهیم خصوصیت href پیوند موجود در سند را به دست بیاوریم، نمی خواهیم دستوری شبیه به ” فرزند دوم ششمین فرزند body سند را به دست بیاور” داشته باشیم. بهتر می بود اگر می‌توانستیم دستوری شبیه به “اولین لینکی که در سند آمده است را بگیر” داشته باشیم. و می‌توانیم این کار را بکنیم.

let link = document.body.getElementsByTagName("a")[0];
console.log(link.href);

تمامی گره‌های عنصر، دارای متدی به نام getElementsByTagName هستند که تمامی عناصری که برچسب داده شده را دارند و زیرمجموعه‌ی آن گره محسوب می‌شوند (فرزند مستقیم و غیر مستقیم) را جمع آوری می‌کند و به صورت یک شیء آرایه‌طور برمی گرداند.

برای یافتن یک گره‌ی خاص واحد، می‌توانید به آن یک خصوصیت id اختصاص دهید و از document.getElementById استفاده کنید.

<p>My ostrich Gertrude:</p>
<p><img id="gertrude" src="img/ostrich.png"></p>

<script>
  let ostrich = document.getElementById("gertrude");
  console.log(ostrich.src);
</script>

سومین متد که کاری مشابه انجام می‌دهد getElementsByClassName است که شبیه به getElementsByTagName عمل می‌کند و در محتوای یک گره‌ی عنصر به جستجو می پردازد و تمامی عناصری که رشته‌ی داده شده را در خصوصیت class شان دارند برمی گرداند.

ایجاد تغییر در سند

تقریبا تمامی قسمت‌های ساختار داده‌ی DOM را می‌توان تغییر داد. می‌توان شکل درخت سند را با ایجاد تغییر در روابط والد-فرزندی دستکاری کرد. گره‌ها دارای متدی به نام remove می‌باشند که می‌توان از آن برای حذف گره از والد کنونی‌اش استفاده نمود. برای افزودن یک گره‌ی فرزند به یک گره‌ی عنصر می‌توانیم از appendChild استفاده کنیم که باعث می‌شود آن گره به انتهای لیست فرزندان اضافه شود. یا از insertBefore استفاده کنیم که گره‌ای که به عنوان آرگومان اول آمده را قبل از گره‌ای که به عنوان آرگومان دوم آمده است وارد می‌کند.

<p>One</p>
<p>Two</p>
<p>Three</p>

<script>
  let paragraphs = document.body.getElementsByTagName("p");
  document.body.insertBefore(paragraphs[2], paragraphs[0]);
</script>

یک گره فقط در یک موقعیت از سند می‌تواند موجودیت داشته باشد. بنابراین، قراردادن پاراگراف 3 جلوی پاراگراف 1 آن را از انتهای سند حذف می‌کند و بعد جلو پاراگراف 1 قرار می‌دهد که نتیجه به این صورت می‌شود: 3/2/1. تمامی عملیاتی که گره‌ای را در جایی قرار می‌دهد به عنوان یک اثر جانبی موجب می‌شوند که ابتدا آن گره از جایگاه فعلی‌اش حذف شود (اگر جایی بوده باشد).

متد replaceChild برای جایگزین کردن یک گره‌ی فرزند با گره‌ی فرزندی دیگر استفاده می‌شود. این متد دو گره به عنوان آرگومان دریافت می‌کند: گره‌ی جدید و گره‌ای که باید جایگزین شود. گره‌ی جایگزین شده باید فرزند عنصری باشد که متد روی آن فراخوانی شده است. توجه داشته باشید که هر دوی replaceChild و insertBefore به عنوان آرگومان اول گره‌ای جدید دریافت می‌کنند.

ایجاد گره‌ها

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

این کار هم شامل حذف عکس‌ها می‌شود و هم ایجاد یک گره‌ی متنی جهت جایگزینی عکس. گره‌های متنی توسط document.createTextNode ایجاد می‌شوند.

<p>The <img src="img/cat.png" alt="Cat"> in the
  <img src="img/hat.png" alt="Hat">.</p>

<p><button onclick="replaceImages()">Replace</button></p>

<script>
  function replaceImages() {
    let images = document.body.getElementsByTagName("img");
    for (let i = images.length - 1; i >= 0; i--) {
      let image = images[i];
      if (image.alt) {
        let text = document.createTextNode(image.alt);
        image.parentNode.replaceChild(text, image);
      }
    }
  }
</script>

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

حلقه‌ای که عکس‌ها را پیمایش می‌کند از پایان لیست کارش را شروع می‌کند. این کار لازم است چرا که لیست گره‌ها که توسط متدی مثل getElementsByTagName برگردانده می‌شود ( یا خاصیتی مثل childNodes)، لیستی زنده است. به این معنا که با تغییر سند، لیست هم به‌روز می شود. اگر از ابتدا شروع می کردیم، حذف اولین عکس ممکن بود باعث شود لیست اولین عنصرش را از دست بدهد که در این صورت با تکرار حلقه در بار دوم ، زمانی که i برابر 1 می‌شود، از کار می ایستاد زیرا طول مجموعه اکنون برابر 1 است.

اگر یک مجموعه‌ی ثابت از گره‌ها را لازم دارید، به‌جای یک مجموعه‌ی زنده از آن ها، می‌توانید مجموعه را با فراخوانی Array.from به یک آرایه‌ی واقعی تبدیل کنید.

let arrayish = {0: "one", 1: "two", length: 2};
let array = Array.from(arrayish);
console.log(array.map(s => s.toUpperCase()));
// → ["ONE", "TWO"]

برای ایجاد گره‌های عنصر، می‌توانید از متد document.createElement استفاده کنید. این متد یک نام برچسب گرفته و یک گره‌ی تهی از نوع داده شده را بر می گرداند.

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

<blockquote id="quote">
  No book can ever be finished. While working on it we learn
  just enough to find it immature the moment we turn away
  from it.
</blockquote>

<script>
  function elt(type, ...children) {
    let node = document.createElement(type);
    for (let child of children) {
      if (typeof child != "string") node.appendChild(child);
      else node.appendChild(document.createTextNode(child));
    }
    return node;
  }

  document.getElementById("quote").appendChild(
    elt("footer", "—",
        elt("strong", "Karl Popper"),
        ", preface to the second editon of ",
        elt("em", "The Open Society and Its Enemies"),
        ", 1950"));
</script>

خصوصیت‌ها

بعضی از خصوصیت‌های عناصر مثل href برای پیوندها را می‌توان به عنوان خاصیتی با همین نام روی شیء DOM عنصر، مورد دستیابی قرار داد. این برای بیشتر خصوصیت‌های استاندارد رایج، برقرار است.

اما HTML این امکان را فراهم کرده است که هر خصوصیتی که بخواهید را به گره‌ها اضافه کنید. این امکان می‌تواند مفید باشد زیرا به شما اجازه می‌دهد اطلاعات بیشتری را در یک سند ذخیره کنید. با این وجود اگر نام خصوصیت‌های خودتان را بسازید، این خصوصیت‌ها به عنوان خاصیت شیء گره در دسترس نخواهند بود. برای کار با آن ها باید از متدهای getAttribute و setAttribute استفاده کنید.

<p data-classified="secret">The launch code is 00000000.</p>
<p data-classified="unclassified">I have two feet.</p>

<script>
  let paras = document.body.getElementsByTagName("p");
  for (let para of Array.from(paras)) {
    if (para.getAttribute("data-classified") == "secret") {
      para.remove();
    }
  }
</script>

توصیه شده که نام این گونه‌ خصوصیت‌های ساختگی را با پیشوند data- آغاز کنید تا مطمئن شوید که تداخلی با دیگر خصوصیت‌ها پیش نخواهد آمد.

خصوصیت class که یکی از خصوصیت‌های رایج است، یک کلیدواژه در جاوااسکریپت محسوب می شود. به دلایل تاریخی – بعضی از پیاده‌سازی‌های قدیمی جاوااسکریپت نمی‌توانستند نام‌های خاصیتی که با کلیدواژه‌ها مطابقت داشتند را مدیریت کنند – خاصیتی که برای دسترسی به این خصوصیت در نظر گرفته شده است className است. همچنین می‌توانید تحت نام واقعی خودش "class" نیز به وسیله‌ی متدهای getAttribute و setAttribute به آن دسترسی داشته باشید.

طرح‌بندی (layout)

ممکن است متوجه شده باشید که انواع مختلف عنصرها به صورت متفاوتی طرح بندی می‌شوند. بعضی مانند پاراگراف ها (<p>) یا سرعنوان‌ها (<h1>)، تمامی عرض سند را اشغال کرده و هر کدام در خط جدیدی به نمایش در می‌آیند. این عناصر، عناصر بلاک نامیده می‌شوند. دیگر عناصر، مثل پیوندها (<a>) یا عنصر <strong> در همان خط کنار متن پیرامونشان نمایش داده می‌شوند. این عنصرها را عناصر درون خطی (inline) می‌نامند.

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

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

به طور مشابه، clientWidth و clientHeight به شما اندازه‌ی فضای درون یک عنصر را نشان می‌دهد و عرض خط مرزی (border) در نظر گرفته نمی‌شود.

<p style="border: 3px solid red">
  I'm boxed in
</p>

<script>
  let para = document.body.getElementsByTagName("p")[0];
  console.log("clientHeight:", para.clientHeight);
  console.log("offsetHeight:", para.offsetHeight);
</script>

موثرترین روش برای بدست آوردن موقعیت دقیق یک عنصر روی صفحه‌ی نمایش، استفاده از متد getBoundingClientRect است. این متد یک شیء بر می گرداند که شامل خاصیت‌های top، bottom، left و right می‌باشد که مشخص می‌کنند موقعیت هر ضلع یک عنصر به نسبت بالا و چپ صفحه‌نمایش چند پیکسل است. اگر این اعداد را نسبت به کل سند لازم دارید، باید موقعیت اسکرول فعلی را به آن اضافه کنید. این موقعیت را می‌توان در متغیرهای pageXOffset و pageYOffset بدست آورد.

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

برنامه‌ای که زیاد و به تکرار به خواندن اطلاعات طرح DOM و تغییر DOM می پردازد باعث می‌شود که محاسبات طرح‌بندی زیاد اتفاق بیفتد که در نتیجه این برنامه به کندی اجرا خواهد شد. کد پیش رو مثالی از این گونه برنامه است. این مثال از دو برنامه تشکیل یافته است که خطی از کاراکترهای X با پهنای 2,000 پیکسل می سازند و زمانی که هرکدام طول می کشد را محاسبه می‌کنند.

<p><span id="one"></span></p>
<p><span id="two"></span></p>

<script>
  function time(name, action) {
    let start = Date.now(); // Current time in milliseconds
    action();
    console.log(name, "took", Date.now() - start, "ms");
  }

  time("naive", () => {
    let target = document.getElementById("one");
    while (target.offsetWidth < 2000) {
      target.appendChild(document.createTextNode("X"));
    }
  });
  // → naive took 32 ms

  time("clever", function() {
    let target = document.getElementById("two");
    target.appendChild(document.createTextNode("XXXXX"));
    let total = Math.ceil(2000 / (target.offsetWidth / 5));
    target.firstChild.nodeValue = "X".repeat(total);
  });
  // → clever took 1 ms
</script>

سبک دهی

تاکنون دیده‌ایم که عناصر مختلف HTML به شکل متفاوتی به تصویر کشیده می‌شوند. بعضی به عنوان بلاک و بعضی درون‌خطی (inline) نشان داده می‌شوند. بعضی سبک بصری اضافه می‌کنند – <strong> محتوایش را توپر می‌کند و <a> محتوایش را آبی رنگ و زیر آن خط می اندازد.

شیوه‌ای که یک برچسب <img> تصویر را نمایش می‌دهد یا یک برچسب <a> متنی را به یک لینک تبدیل می‌کند، به طور کامل به نوع آن عنصر وابسته است. اما سبک بصری‌ای که به صورت پیش‌فرض به یک عنصر اضافه می‌شود، مثل رنگ متن یا داشتن زیرخط، را می‌توانیم تغییر دهیم. در اینجا مثالی که از خصوصیت style استفاده می‌کند را می بینیم.

<p><a href=".">Normal link</a></p>
<p><a href="." style="color: green">Green link</a></p>

یک خصوصیت style می‌تواند حاوی یک یا چند اعلان باشد که شامل یک خاصیت (مثل color) که به همراه دونقطه و مقدارش می‌آید. زمانی که بیش از یک اعلان وجود دارد، باید هر اعلان با یک نقطه‌ویرگول جدا شود مثل "color: red; border: none".

خیلی از جنبه‌های مربوط به سند وجود دارند که می‌توانند توسط سبک‌دهی تاثیر بپذیرند. به عنوان مثال خاصیت display برای کنترل نحوه‌ی نمایش یک عنصر به صورت درون‌خطی یا بلاک استفاده می‌شود.

This text is displayed <strong>inline</strong>,
<strong style="display: block">as a block</strong>, and
<strong style="display: none">not at all</strong>.

برچسب block در خط خودش به پایان می‌رسد به دلیل اینکه عناصر بلاکی به صورت درون خطی کنار متن پیرامونشان نشان داده نمی‌شوند. برچسب آخر اصلا نمایش داده نمی‌شود – display: none مانع از نمایش یک عنصر در صفحه‌ی نمایش می‌شود. این روشی برای مخفی کردن عناصر است. معمولا ترجیح داده می‌شود بجای حذف کامل یک عنصر از سند، از این روش استفاده شود به دلیل این که بازگرداندن آن در آینده در این روش آسان تر است.

کدهای جاوااسکریپت می‌توانند مستقیما سبک‌دهی یک عنصر را با استفاده از خاصیت style دستکاری کنند. این خاصیت شیئی را نگهداری می‌کند که خاصیت‌هایی برای همه‌ی ویژگی‌های سبک‌دهی دارد. مقدار این خاصیت‌ها از نوع رشته است که می‌توانیم برای تغییر یک جنبه‌ی خاص از سبک بصری عنصر مورد نظر آن را بنویسیم.

<p id="para" style="color: purple">
  Nice text
</p>

<script>
  let para = document.getElementById("para");
  console.log(para.style.color);
  para.style.color = "magenta";
</script>

نام بعضی از خاصیت‌های سبک‌دهی حاوی کاراکتر خط پیوند (-) است مانند font-family. به دلیل این که کار با این نوع نام ها در جاوااسکریپت کمی دشوار است (برای دسترسی باید چیزی مثل style["font-family"] داشته باشید)، نام خاصیت‌های این گونه نام‌ها در شیء style آن خط پیوند را حذف کرده و حرف بعد از آن را با حروف بزرگ می نویسند (style.fontFamily).

سبک‌های آبشاری

به سیستم سبک‌دهی بصری در اچ‌تی‌ام‌ال، CSS گفته می‌شود که مخفف برگه‌های سبک آبشاری یا سلسله‌مراتبی (cascading style sheets) است. یک برگه‌ی سبک به مجموعه‌ای از دستورات گفته می‌شود که برای سبک‌دهی ظاهری به عناصر یک سند استفاده می‌شوند. می‌توان آن را درون یک جفت برچسب <style> قرار داد.

<style>
  strong {
    font-style: italic;
    color: gray;
  }
</style>
<p>Now <strong>strong text</strong> is italic and gray.</p>

معنای آبشاری این است که قوانینی با سلسله‌مراتب با هم ترکیب می‌شوند تا سبک نهایی را برای یک عنصر تولید کنند. در مثال بالا، سبک پیش‌فرض برای برچسب‌های <strong>، که در آن font-weight: bold تعریف شده بود، توسط دستوری دیگر که در برچسب <style> آمده است تغییر یافته و font-style و color به آن اضافه شده است.

وقتی چندین دستور یک مقدار را برای یک خاصیت واحد تعریف می‌کنند، آخرین دستوری که خوانده شود دارای حق تقدم بالاتری خواهد بود و اعمال می‌شود. بنابراین اگر دستوری که در برچسب <style> آمده است font-weight: normal را داشته باشد، دستور پیش‌فرض font-weight در نظر گرفته نمی‌شود و متن به شکل نرمال نمایش داده می‌شود نه به صورت توپر. دستوراتی که در خصوصیت style به صورت مستقیم به یک گره اعمال می‌شوند دارای بالاترین حق تقدم هستند و همیشه در سلسله‌مراتب برنده می‌شوند.

می‌توان چیزهایی بجز نام برچسب‌ها را در دستورات CSS مورد هدف قرار داد. دستوری به شکل .abc، به همه‌ی عناصری که خصوصیت classشان دارای مقدار "abc" است اعمال می‌شود. دستوری به شکل #xyz به عنصری اعمال می‌شود که خصوصیت id آن برابر "xyz" باشد (که باید در سند منحصر به فرد باشد).

.subtle {
  color: gray;
  font-size: 80%;
}
#header {
  background: blue;
  color: white;
}
/* p elements with id main and with classes a and b */
p#main.a.b {
  margin-bottom: 20px;
}

قاعده‌ی حق تقدمی که موجب می‌شد دستوری که آخر تعریف شده بود اعمال شود، زمانی موثر است که دستورات دارای specificity (درجه‌ی صراحت) یکسانی باشند. درجه‌ی صراحت یک دستور، معیاری از میزان دقتی است که آن دستور، عناصر هدفش را توصیف می‌کند که توسط عدد و نوع (برچسب، class و ID) عناصر مورد هدف تعیین می‌شود. به عنوان مثال، دستوری که p.a را هدف قرار می‌دهد دارای صراحت بیشتری از دستوراتی است که p یا فقط .a را هدف قرار می‌دهند و بنابراین حق تقدم بیشتری خواهد داشت.

دستوری به شکل p > a {…} سبک‌های تعریف شده را به همه‌ی برچسب‌های <a> که فرزند مستقیم برچسب‌های <p> محسوب می‌شوند اعمال می‌کند. به طور مشابه، p a {…} به همه‌ی برچسب‌های <a> که درون برچسب‌های <p> باشند اعمال می‌شود فارغ از اینکه فرزند مستقیم یا غیر مستقیم باشند.

گزینشگرهای پرس و جو

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

علت اینکه قواعد گزینشگر (selector) – منظور شیوه‌ی نشان‌گذاری استفاده شده در برگه‌های سبک‌دهی برای تعیین عناصر هدف برای اعمال سبک‌ها می‌باشد – را معرفی کردم این است که می‌توانیم از این زبان نصف و نیمه به عنوان روشی موثر برای پیدا کردن عناصر DOM استفاده کنیم.

متد querySelectorAll که هم در شیء document موجود است و هم روی گره‌های عنصر، یک رشته‌ی گزینشگر دریافت می‌کند و یک NodeList‍ که حاوی تمامی عناصر تطبیق خورده است را برمی گرداند.

<p>And if you go chasing
  <span class="animal">rabbits</span></p>
<p>And you know you're going to fall</p>
<p>Tell 'em a <span class="character">hookah smoking
  <span class="animal">caterpillar</span></span></p>
<p>Has given you the call</p>

<script>
  function count(selector) {
    return document.querySelectorAll(selector).length;
  }
  console.log(count("p"));           // All <p> elements
  // → 4
  console.log(count(".animal"));     // Class animal
  // → 2
  console.log(count("p .animal"));   // Animal inside of <p>
  // → 2
  console.log(count("p > .animal")); // Direct child of <p>
  // → 1
</script>

برخلاف متدهایی مثل getElementsByTagName، شیءای که توسط querySelectorAll برگردانده می‌شود زنده یا پویا نیست. با تغییر سند، این شیء به روز نمی‌شود و هنوز یک آرایه‌ی واقعی نیست و اگر لازم دارید تا از ویژگی‌های آرایه‌ها بهره ببرید باید Array.from را روی آن‌ها فراخوانی کنید.

متد querySelector (بدون All) به شکل مشابهی عمل می‌کند. این متد برای زمانی که قصد دارید یک عنصر مشخص را هدف قرار دهید مناسب است. این متد فقط اولین عنصری که مطابق گزینشگر بود را برمی گرداند و در صورت پیدا نکردن هیچ عنصری مقدار null را تولید می کند.

موقعیت دهی و متحرک‌سازی

خاصیت position در سبک‌دهی، تاثیر مهمی در در طرح‌بندی صفحه‌ می‌گذارد. به صورت پیش فرض مقدار آن برابر static است که یعنی عنصر مورد نظر در موقعیت نرمال خودش در سند قرار می‌گیرد. زمانی که این مقدار به relative تغییر می یابد، عنصر همچنان فضایی در سند اشغال می‌کند اما اکنون می‌توان از خاصیت‌های top و left برای تغییر مکان آن نسبت به جایگاه نرمالش استفاده کرد. زمانی که position برابر absolute قرار گیرد، عنصر مورد نظر از جریان چیدمان صفحه خارج می‌شود – به این معنا که دیگر فضایی را اشغال نمی‌کند و ممکن است روی دیگر عناصر بیفتد. همچنین در این حالت خاصیت‌های top و left را می‌توان برای موقعیت دهی مطلق عنصر نسبت به گوشه‌ی بالا و چپ نزدیک ترین عنصر والدش (که دارای مقدار position غیر از static است) استفاده کرد یا در صورت نبود آن، نسبت به کل سند موقعیت دهی می‌شود.

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

<p style="text-align: center">
  <img src="img/cat.png" style="position: relative">
</p>
<script>
  let cat = document.querySelector("img");
  let angle = Math.PI / 2;
  function animate(time, lastTime) {
    if (lastTime != null) {
      angle += (time - lastTime) * 0.001;
    }
    cat.style.top = (Math.sin(angle) * 20) + "px";
    cat.style.left = (Math.cos(angle) * 200) + "px";
    requestAnimationFrame(newTime => animate(newTime, time));
  }
  requestAnimationFrame(animate);
</script>

تصویر ما در مرکز صفحه قرار گرفته است و مقدار position آن برابر relative است. ما پیوسته مقدار top و left تصویر را برای ایجاد حرکت به‌روز رسانی می‌کنیم.

این اسکریپت از requestAnimationFrame برای زمانبندی اجرای تابع animate هنگامی که مرورگر آماده است تا تصویر صفحه را دوباره بکشد، استفاده می‌کنیم. تابع animate خود دوباره requestAnimationFrame را برای زمانبندی به‌روزرسانی بعدی فراخوانی می‌کند. زمانی که پنجره‌ی مرورگر (یا تب مرورگر) فعال است، این باعث می‌شود که به‌روز‌رسانی‌ها با نرخ 60 بار در ثانیه انجام شود که پویانمایی روانی را تولید می‌کند.

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

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

برای حرکت دایره‌ای از توابع مثلثات مثل Math.cos و Math.sin استفاده می‌شود. برای افرادی که با این مفاهیم آشنا نیستند، به طور خلاصه آن ها را معرفی خواهم کرد چرا که کم و بیش از آن ها در کتاب استفاده خواهیم کرد.

متدهای Math.cos و Math.sin برای پیدا کردن نقاطی که روی محیط دایره‌ای با مرکز (0,0) و شعاع یک استفاده می‌شوند. هر دوی این متدها ورودی‌هایشان را به عنوان موقعیت‌هایی روی این دایره تفسیر می‌کنند، که صفر به معنای نقطه‌ای است که در راست‌ترین قسمت دایره قرار گرفته است و حرکت در جهت گردش عقربه‌های ساعت تا می باشد (حدود 6.28) که یک دور کامل دایره انجام می‌شود. Math.cos مختصات x نقطه‌ای که مربوط به موقعیت داده شده است را برمی‌گرداند در حالیکه Math.sin مختصات y آن را می دهد. موقعیت‌هایی (یا زاویه‌ها) که از بزرگتر باشند یا از 0 کوچکتر باشند معتبر محسوب می‌شوند – یعنی چرخش تکرار می‌شود بنابراین a+2π معادل همان زاویه‌ی a خواهد بود.

این واحد اندازه‌گیری برای زاویه‌ها را رادیان می‌نامند- یک دایره‌ی کامل برابر رادیان است، مشابه 360 در واحد درجه. ثابت π در جاوااسکریپت توسط Math.PI در دسترس است.

Using cosine and sine to compute coordinates

کد پویانمایی گربه یک شمارنده نیز نگهداری می‌کند، angle، تا بتواند زاویه‌ی فعلی حرکت را داشته باشد و با هر بار فراخوانی تابع animate آن را افزایش می‌دهد. بعدا می‌تواند از این زاویه برای محاسبه موقعیت فعلی عنصر تصویر استفاده کند. مقدار خاصیت سبکی top هم به وسیله‌ی Math.sin و ضرب آن در 20 محاسبه می‌شود که نمایانگر شعاع عمودی در بیضی ما است. مقدار left نیز بر اساس Math.cos و ضرب آن در 200 است بنابراین بیضی ما دارای عرض بسیار بیشتری نسبت به طولش می‌باشد.

توجه داشته باشید که مقادیر مربوط به سبک‌ها معمولا به واحد نیاز دارند. در این مثال، ما باید "px" را به عدد اضافه کنیم تا به مرورگر اعلام کنیم که شمارش در واحد پیکسل می‌باشد (نه سانتیمتر، “em”، یا دیگر واحدها). ممکن است به آسانی از این نکته غفلت شود. استفاده از اعداد بدون واحدها باعث می‌شود که سبک‌دهی شما اعمال نگردد- مگر اینکه آن عدد 0 باشد که همیشه معنای یکسانی دارد.

خلاصه

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

DOM به شکل یک درخت سازماندهی شده است که در آن عناصر به صورت سلسله‌مراتبی براساس ساختار سند مرتب می‌شوند. اشیائی که نماینده‌ی عناصر هستند دارای خاصیت‌هایی مانند parentNode و childNodes هستند که می‌توان از آن ها برای حرکت در این درخت استفاده کرد.

نحوه‌ی نمایش یک سند را می‌توان با سبک‌دهی تغییر داد و این کار به دو روش چسباندن سبک‌ها به عناصر به صورت مستقیم و یا با تعریف دستوراتی که عناصر خاصی را هدف قرار می‌دهند صورت می‌پذیرد. خاصیت‌های سبک‌دهی زیاد و متنوعی وجود دارد مثل color یا display. کدهای جاوااسکریپت می‌توانند سبک یک عنصر را مستقیما از طریق خصوصیت style دستکاری کنند.

تمرین‌ها

ساخت یک جدول

یک جدول HTML به وسیله‌ی ساختار برچسب‌های زیر ساخته می‌شود:

<table>
  <tr>
    <th>name</th>
    <th>height</th>
    <th>place</th>
  </tr>
  <tr>
    <td>Kilimanjaro</td>
    <td>5895</td>
    <td>Tanzania</td>
  </tr>
</table>

برای هر ردیف، برچسب <table> یک برچسب <tr> خواهد داشت. درون این برچسب‌های <tr> می‌توانیم سلول‌های جدول را قرار دهیم: سلول‌های معمولی (<td>) یا سلول‌های عنوان (<th>).

با در دست داشتن مجموعه اطلاعاتی درباره‌ی کوه‌ها، آرایه‌ای از اشیاء شامل name، height و place ، یک ساختار DOM برای جدولی که این اشیاء را می‌شمارد ایجاد کنید. این جدول باید یک ستون برای هر کلید و یک ردیف برای هر شیء داشته باشد، مازاد بر آن یک ردیف عنوان به وسیله‌ی عنصرهای <th> در قسمت بالا که نام ستون‌ها را لیست کند.

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

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

جدول نهایی را به عنصری که دارای خصوصیت id برابر با "mountains" می‌باشد اضافه کنید تا در صفحه‌ی نمایش داده شود.

بعد از این که این قسمت را تکمیل کردید سلول‌هایی که حاوی مقادیر عددی هستند را راست چین کنید و این کار را با تنظیم style.textAlign برابر با "right" انجام دهید.

<h1>Mountains</h1>

<div id="mountains"></div>

<script>
  const MOUNTAINS = [
    {name: "Kilimanjaro", height: 5895, place: "Tanzania"},
    {name: "Everest", height: 8848, place: "Nepal"},
    {name: "Mount Fuji", height: 3776, place: "Japan"},
    {name: "Vaalserberg", height: 323, place: "Netherlands"},
    {name: "Denali", height: 6168, place: "United States"},
    {name: "Popocatepetl", height: 5465, place: "Mexico"},
    {name: "Mont Blanc", height: 4808, place: "Italy/France"}
  ];

  // Your code here
</script>

می‌توانید از document.createElement برای ایجاد گره‌های جدید استفاده کنید، همچنین از document.createTextNode برای ایجاد گره‌های متنی و از appendChild نیز برای قرار دادن گره‌ها در دیگر گره‌ها.

در ادامه لازم دارید تا نام کلید‌ها را پیمایش کنید؛ یک بار برای پر کردن ردیف بالایی و بعد دوباره برای هر همه‌ی اشیاء موجود در آرایه تا بتوانید ردیف‌های داده را بسازید. برای بدست آوردن یک آرایه از نام کلید‌ها از شیء اول، Object.keys به دردتان خواهد خورد.

برای اضافه کردن جدول به گره‌ی والد فعلی، می‌توانید از document.getElementById یا document.querySelector برای پیدا کردن گره‌ای با خاصیت id مورد نظر استفاده کنید.

گرفتن عناصر به وسیله‌ی نام برچسب‌ها

متد document.getElementsByTagName تمامی عناصر فرزند را برای نام برچسب داده شده برمی گرداند. نسخه‌ی خودتان از این متد را به عنوان یک تابع بنویسید که یک گره و یک رشته (نام برچسب) را به عنوان ورودی‌ها بگیرد و آرایه‌ای که حاوی تمامی گره‌های فرزند متعلق به آن برچسب را برگرداند.

برای پیدا کردن نام برچسب یک عنصر، از خاصیت nodeName استفاده کنید. اما توجه داشته باشید که این خاصیت نام برچسب را با حروف بزرگ برمی گرداند. از متدهای رشته، toLowerCase یا toUpperCase برای درست کردن آن استفاده کنید.

<h1>Heading with a <span>span</span> element.</h1>
<p>A paragraph with <span>one</span>, <span>two</span>
  spans.</p>

<script>
  function byTagName(node, tagName) {
    // Your code here.
  }

  console.log(byTagName(document.body, "h1").length);
  // → 1
  console.log(byTagName(document.body, "span").length);
  // → 3
  let para = document.querySelector("p");
  console.log(byTagName(para, "span").length);
  // → 2
</script>

ساده‌ترین روش پیاده‌سازی این راه‌حل استفاده از یک تابع بازگشتی است، شبیه به talksAbout تابع که پیش‌تر در این فصل تعریف گردید.

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

تابع بازگشتی باید نوع گره را بررسی کیند. در اینجا فقط مایلیم تا گره‌ نوع 1 (Node.ELEMENT_NODE) را در نظر بگیریم. برای این نوع گره‌ها، ما باید فرزندانشان را پیمایش کنیم و برای هر فرزند، مشاهده کنیم که آیا با پرس‌وجوی‌ ما مطابقت دارد یا خیر و همچنین یک فراخوانی بازگشتی روی آن نیز داشته باشیم تا فرزندان آن را نیز پوشش داده باشیم.

کلاه گربه

پویانمایی ایجاد شده در قبل را توسعه دهید تا گربه و کلاهش (<img src="img/hat.png">) هر کدام در جهت مخالف هم بچرخند.

یا کاری کنید که کلاه دور گربه بچرخد. یا پویانمایی را به صورتی که جالب باشد تغییر دهید.

برای ساده سازی روند موقعیت دهی چند شیء، احتمالا ایده‌ی خوبی است که به سراغ موقعیت دهی مطلق برویم. به این معنا که top و left بر اساس گوشه‌ی چپ و بالای سند محاسبه بشوند. برای جلوگیری از مختصات منفی، که باعث می‌شود که تصاویر به بیرون از فضای قابل مشاهده صفحه منتقل شوند، می‌توانید یک عدد مشخص و ثابت را به مقادیر موقعیت ها اضافه کنید.

<style>body { min-height: 200px }</style>
<img src="img/cat.png" id="cat" style="position: absolute">
<img src="img/hat.png" id="hat" style="position: absolute">

<script>
  let cat = document.querySelector("#cat");
  let hat = document.querySelector("#hat");

  let angle = 0;
  let lastTime = null;
  function animate(time) {
    if (lastTime != null) angle += (time - lastTime) * 0.001;
    lastTime = time;
    cat.style.top = (Math.sin(angle) * 40 + 40) + "px";
    cat.style.left = (Math.cos(angle) * 200 + 230) + "px";

    // Your extensions here.

    requestAnimationFrame(animate);
  }
  requestAnimationFrame(animate);
</script>

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