فصل 14DOM یا مدل شیء سند
چه بد! یک داستان تکراری! وقتی کار ساخت خانه را تمام میکنی، متوجه میشوی که چیزی یاد گرفتی که میبایست پیش از شروع کار میدانستی.
وقتی یک صفحهی وب را در مرورگرتان باز میکنید، مرورگر متن HTML صفحه را گرفته و آن را تفسیر میکند، بسیار شبیه به آنچه تجزیهگر ما برای تجزیهی برنامهها در فصل 12 انجام می داد. مرورگر یک مدل از ساختار سند می سازد و از آن برای نمایش سند روی صفحهی نمایش استفاده میکند.
این نمایش از سند، یکی از ابزارهایی است که یک برنامهی جاوااسکریپت در جعبهی شنی (sandbox) خود به آن دسترسی دارد. یک ساختار داده که میتواند خوانده شود یا تغییر یابد؛ ساختار دادهای زنده: زمانی که تغییری در آن رخ میدهد، صفحهای که در مانیتور نمایش داده می شود نیز بهروز میشود تا تغییرات را منعکس کند.
ساختار سند
میتوانید یک سند HTML را به عنوان مجموعهای از مستطیلهای تودرتو در نظر بگیرید. برچسبهایی مثل <body>
و </body>
، دیگر برچسبها را در بر می گیرند، که خود نیز حاوی برچسبهای دیگر یا متن میباشند. به عنوان مثال، سندی از فصل قبل را مشاهده میکنید:
<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 آن یا برچسبها و متونی که در بر دارد را بدست بیاوریم. این طرز نمایش را مدل شیء سند یا به اختصار DOM می گویند.
متغیر سراسری document
امکان دسترسی به این اشیاء را فراهم می سازد. خاصیت documentElement
آن به شیئی ارجاع میدهد که نمایانگر برچسب <html>
است. چون هر سند HTML دارای یک سرصفحه و یک بدنه میباشد ، در نتیجه دارای خاصیتهای head
و body
نیز میباشد که به آن عناصر اشاره میکنند.
درختها
کمی به درختهای گرامر که در فصل 12 معرفی شدند فکر کنید. ساختار آنها بسیار شبیه به ساختار یک سند مرورگر است. هر گره ممکن است به دیگر گرهها ارجاع دهد، فرزندان، که خود میتوانند فرزندان خود را داشته باشند. این شکل یک ساختار تودرتوی معمول است که عناصر در آن میتوانند حاوی زیرعنصرهایی مشابه باشند.
یک ساختار داده را زمانی یک درخت مینامیم که دارای شاخههایی بدون دور باشد ( یک گره ممکن نیست به شکل مستقیم یا غیر مستقیم حاوی خودش باشد) و همچنین یک ریشه مشخص داشته باشد. در رابطه با DOM، ریشهی درخت، document.
میباشد.
درختها در علم کامپیوتر کاربرد زیادی دارند. علاوه بر نمایش ساختارهای درختی مانند اسناد HTML یا برنامهها، اغلب از آنها برای نگهداری مجموعههای مرتب شدهی داده استفاده میشود زیرا معمولا ورود یا جستجوی عناصر در یک درخت با کارایی بیشتری نسبت به یک آرایهی تخت انجام میشود.
یک درخت معمول دارای انواع مختلفی از گرهها میباشد. درخت گرامر زبان Egg دارای شناسهها، مقادیر، و گرههای کاربرد (Application) بود. گرههای کاربرد میتوانستند دارای فرزند باشند، درحالیکه شناسهها و مقادیر از جنس برگ بودند؛ منظور گرههایی است که فرزندی ندارند.
همین روال برای DOM هم برقرار است. گرهها به عنوان عناصر، که برچسبهای HTML را نمایش میدهند، ساختار سند را تعیین میکنند. این گرهها میتوانند گرههای فرزند داشته باشند. یک نمونه از این نوع گرهها document.body
است. بعضی از این فرزندان می توانند گرههایی از نوع برگ باشند، مثل متنها یا گرههای توضیحات.
هر شیء گرهی DOM دارای خاصیتی به نام nodeType
است که حاوی کدی (عددی) است که نوع آن گره را مشخص میکند. عناصر دارای کد 1 میباشند که همچنین به صورت یک خاصیت ثابت Node.
نیز در دسترس است. گرههای متنی ، که نمایانگر یک قسمت از متن در سند میباشند دارای کد 3 میباشند (Node.TEXT_NODE
). کد 8 نیز به توضیحات اختصاص دارد (Node.
).
یک روش دیگر برای به تصویر کشیدن درخت مربوط به سندمان، شکل زیر است.
برگها، گرههای متنی هستند و پیکانها نمایانگر روابط والد-فرزندی بین گرهها میباشند.
استاندارد
استفاده از کدهای عددی رمزگونه برای نمایش نوع گرهها چیزی نیست که در جاوااسکریپت معمول باشد. در ادامه فصل می بینیم که دیگر بخشهای رابط DOM نیز حسی نامانوس ایجاد میکنند. دلیل آن این است که DOM فقط برای جاوااسکریپت طراحی نشده است. بلکه تلاش شده که رابط آن نسبت به زبانها بی طرف باشد که بتوان از آن در دیگر سیستمها به خوبی استفاده شود – نه فقط در HTML بلکه همچنین برای XML که فرمتی عمومی برای دادهها است و گرامری شبیه به HTML دارد.
خوب این زیاد مطلوب نیست. استانداردها اغلب مفید میباشند. اما در این مورد خاص، مزیت آن (سازگاری فرا زبانی) آن قدرها قانع کننده نیست. در دست داشتن رابطی که به خوبی با زبانی که استفاده میکنید یکپارچه است، زمان زیادی برای شما صرفه جویی میکند تا اینکه یک رابط عمومی برای همهی زبانها داشته باشیم.
یک نمونه از این یکپارچگی ضعیف، خاصیت childNodes
است که در گرههای عنصر در DOM، وجود دارند. این خاصیت یک شیء آرایهطور را نگهداری میکند که خاصیتی به نام length
دارد و همچنین خاصیتهایی دارد که توسط اعداد برچسبگذاری شده اند تا بتوان به گرههای فرزند دسترسی داشت. اما این یک نمونه از نوع NodeList
است نه یک آرایهی واقعی بنابراین متدهای آرایه مثل slice
و map
را ندارد.
همچنین مشکلاتی وجود دارد که ناشی از طراحی ضعیف است. به عنوان مثال، راهی برای ایجاد یک گره جدید به همراه گرههای فرزند و خصوصیتها در یک گام وجود ندارد. بلکه باید ابتدا گره را ایجاد کنید، بعد فرزندان و خصوصیتها را یکی پس از دیگری به وسیله اثرات جانبی بسازید. کدهایی که با DOM تعامل زیادی برقرار میکنند، معمولا طولانی، تکراری و بدریخت هستند.
اما این ایرادات، مشکلات مهلکی محسوب نمیشوند. چون جاوااسکریپت این امکان را به ما میدهد که تجریدهای خودمان را بنویسیم، میتوان راههای بهتری را برای انجام عملیات برنامهتان طراحی کنید. خیلی از کتابخانههایی که برای برنامهنویسی مرتبط با مرورگر ایجاد شده اند، این ابزار را فراهم میکنند.
حرکت در درخت
گرههای DOM حاوی پیوندهای زیادی به دیگر گرههای نزدیک میباشند. نمودار زیر این موضوع را به تصویر می کشد:
اگرچه این نمودار فقط 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.
استفاده کنید.
<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.
ایجاد میشوند.
<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.
استفاده کنید. این متد یک نام برچسب گرفته و یک گرهی تهی از نوع داده شده را بر می گرداند.
در مثال پیش رو یک تابع کاربردی به نام 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) و شعاع یک استفاده میشوند. هر دوی این متدها ورودیهایشان را به عنوان موقعیتهایی روی این دایره تفسیر میکنند، که صفر به معنای نقطهای است که در راستترین قسمت دایره قرار گرفته است و حرکت در جهت گردش عقربههای ساعت تا 2π می باشد (حدود 6.28) که یک دور کامل دایره انجام میشود. Math.cos
مختصات x نقطهای که مربوط به موقعیت داده شده است را برمیگرداند در حالیکه Math.sin
مختصات y آن را می دهد. موقعیتهایی (یا زاویهها) که از 2π بزرگتر باشند یا از 0 کوچکتر باشند معتبر محسوب میشوند – یعنی چرخش تکرار میشود بنابراین a+2π معادل همان زاویهی a خواهد بود.
این واحد اندازهگیری برای زاویهها را رادیان مینامند- یک دایرهی کامل برابر 2π رادیان است، مشابه 360 در واحد درجه. ثابت π در جاوااسکریپت توسط Math.PI
در دسترس است.
کد پویانمایی گربه یک شمارنده نیز نگهداری میکند، 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.
برای ایجاد گرههای جدید استفاده کنید، همچنین از document.
برای ایجاد گرههای متنی و از appendChild
نیز برای قرار دادن گرهها در دیگر گرهها.
در ادامه لازم دارید تا نام کلیدها را پیمایش کنید؛ یک بار برای پر کردن ردیف بالایی و بعد دوباره برای هر همهی اشیاء موجود در آرایه تا بتوانید ردیفهای داده را بسازید. برای بدست آوردن یک آرایه از نام کلیدها از شیء اول، Object.keys
به دردتان خواهد خورد.
برای اضافه کردن جدول به گرهی والد فعلی، میتوانید از document.
یا document.
برای پیدا کردن گرهای با خاصیت id
مورد نظر استفاده کنید.
گرفتن عناصر به وسیلهی نام برچسبها
متد document.
تمامی عناصر فرزند را برای نام برچسب داده شده برمی گرداند. نسخهی خودتان از این متد را به عنوان یک تابع بنویسید که یک گره و یک رشته (نام برچسب) را به عنوان ورودیها بگیرد و آرایهای که حاوی تمامی گرههای فرزند متعلق به آن برچسب را برگرداند.
برای پیدا کردن نام برچسب یک عنصر، از خاصیت 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.
) را در نظر بگیریم. برای این نوع گرهها، ما باید فرزندانشان را پیمایش کنیم و برای هر فرزند، مشاهده کنیم که آیا با پرسوجوی ما مطابقت دارد یا خیر و همچنین یک فراخوانی بازگشتی روی آن نیز داشته باشیم تا فرزندان آن را نیز پوشش داده باشیم.
کلاه گربه
پویانمایی ایجاد شده در قبل را توسعه دهید تا گربه و کلاهش (<img src="img/
) هر کدام در جهت مخالف هم بچرخند.
یا کاری کنید که کلاه دور گربه بچرخد. یا پویانمایی را به صورتی که جالب باشد تغییر دهید.
برای ساده سازی روند موقعیت دهی چند شیء، احتمالا ایدهی خوبی است که به سراغ موقعیت دهی مطلق برویم. به این معنا که 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>