اذهب إلى المحتوى

نموذج كائن المستند في جافاسكريبت


أسامة دمراني
اقتباس

التخطيط قبل العمل يقلل مرات الفشل.

chapter_picture_14.jpg

حين تفتح صفحة ويب في متصفحك فإن المتصفح يجلب نص HTML الخاص بها ويحلله كما يفعل المحلل parser الذي أنشأناه في المقال الثاني عشر مع البرامج، كما يبني المتصفح نموذجًا لهيكل المستند ويستخدم هذا النموذج لإظهار الصفحة كما تراها على الشاشة.

يُعَدّ هذا التمثيل للمستند طريقةً في صناديق الاختبار sandboxes التي في برامج جافاسكربت، وهو هيكل بيانات يمكنك قراءته وتعديله، كما يتصرف على أساس هيكل بيانات حي تتغير الصفحة بتعديله لتعكس هذه التغييرات.

هيكل المستند

تخيَّل مستند 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-boxes.png

يتبع هيكل البيانات الذي يستخدمه المتصفح لتمثيل هذا المستند هذا الشكل، فهناك كائن لكل صندوق يمكننا التفاعل معه لمعرفة أشياء، مثل وسم HTML التي يمثلها والصناديق والنصوص التي يحتوي عليها، ويسمى هذا التمثيل بنموذج كائن المستند Document Object Model -أو DOM اختصارًا-.

وتعطينا رابطة document العامة وصولًا إلى هذه الكائنات، وتشير خاصية documentElement إلى الكائن الذي يمثل وسم <html>، وبما أيّ مستند HTML فيه ترويسة Head ومتن Body، فسيحتوي على خاصيتي head وbody اللتين تشيران إلى هذين العنصرين أيضًا.

الأشجار

لو أنك تذكر أشجار البُنى syntax trees التي تحدثنا عنها في المقال الثاني عشر والتي تشبه هياكلها هياكل المستندات التي في المتصفح شبهًا كبيرا؛ فكل عُقدة node تشير إلى عُقد أخرى وقد يكون للفروع children فروعًا أخرى، وهذا الشكل هو نموذج للهياكل المتشعبة حيث تحتوي العناصر على عناصر فرعية تشبهها.

نقول على هيكل البيانات أنه شجرة إذا احتوي على هيكل ذي بنية متفرعة branching وليس فيه دورات cycles -بحيث لا يمكن للعُقدة أن تحتوي نفسها مباشرةً أو بصورة غير مباشرة-، وله جذر واحد معرَّف جيدًا، وهذا الجذر في حالة DOM هو document.documentElement.

نتعرض للأشجار كثيرًا في علوم الحاسوب، فهي تُستخدَم للحفاظ على مجموعات مرتبة من البيانات، حيث يكون إدخال العناصر أو العثور عليها أسهل داخل شجرة من لو كان في مصفوفة مسطحة، وذلك إضافة إلى استخدامات أخرى مثل تمثيل الهياكل التعاودية recursive structures مثل مستندات HTML أو البرامج.

تمتلك الشجرة عدة أنواع مختلفة من العُقد، فشجرة البُنى للغة Egg التي أنشأناها في المقال الثاني عشر من هذه السلسلة كانت لها معرِّفات identifiers وقيم values وعُقد تطبيقات application nodes، كما يمكن أن يكون لعُقد التطبيقات تلك فروعًا، في حين يكون للمعرِّفات وللقيم أوراقًا leaves أو عُقدًا دون فروع.

ينطبق المنطق نفسه على DOM، فعُقد العناصر التي تمثِّل وسوم HTML تحدِّد هيكل المستند، ويمكن أن يكون لها عُقدًا فرعيةً child nodes، وأحد الأمثلة على تلك العُقد هو document.body. كذلك فإن تلك العُقد الفرعية قد تكون عُقدًا ورقيةً leaf nodes مثل النصوص أو عُقد التعليقات comment nodes.

يملك كل كائن عُقدة في DOM خاصية nodeType تحتوي على رمز -أو عدد- يعرِّف نوع العُقدة، فتحمل العناصر الرمز 1 الذي يُعرَّف أيضًا على أساس خاصية ثابتة لـ Node.ELEMENT_NODE؛ أما عُقد النصوص التي تمثِّل أجزاءً من النصوص داخل المستند فتحصل على الرمز 3 وهو Node.TEXT_NODE، في حين تحمل التعليقات الرمز 8 الذي هو Node.COMMENT_NODE.

يوضِّح الشكل التالي شجرة مستندنا بصورة أفضل:

html-tree.png

العُقد النصية هنا هي الأوراق، والأسهم توضح علاقة الأصل والفرع بين العُقد.

المعيار

لا يتلاءم استخدام رموز عددية مبهمة لتمثيل أنواع العُقد مع طبيعة جافاسكربت، وسنرى في هذا المقال أجزاءً أخرى من واجهة DOM ستبدو متعِبة ومستهجنة، وذلك لأن DOM لم يصمَّم من أجل جافاسكربت وحدها، بل يحاول أن يكون واجهة غير مرتبطة بلغة بعينها ليُستخدم في أنظمة أخرى، فلا يكون من أجل HTML وحدها بل لـ XML كذلك، وهي صيغة بيانات عامة لها بنية تشبه HTML.

لكن مزية المعيارية هنا ليست مقنعة ولا مبررة، فالواجهة التي تتكامل تكاملًا حسنًا مع اللغة التي تستخدمها ستوفر عليك وقتًا موازنة بالواجهة التي تكون موحدة على اختلاف اللغات، وانظر خاصية childNodes التي في عُقد العناصر في DOM لتكون مثالًا على هذا التكامل السيء، فتلك الخاصية تحمل كائنًا شبيهًا بالمصفوفة array-like object مع خاصية length وخصائص معنونة بأعداد للوصول إلى العُقد الفرعية، لكنه نسخة instance من النوع NodeList وليس مصفوفةً حقيقيةً، لذا فليس لديه توابع مثل slice وmap.

ثم هناك مشاكل ليس لها مراد إلا سوء التصميم، فليست هناك مثلًا طريقةً لإنشاء عقدة جديدة وإضافة فروع أو سمات إليها، بل يجب عليك إنشاء العُقدة ثم إضافة الفروع والسمات واحدة واحدة باستخدام الآثار الجانبية side effects، وعلى ذلك ستكون الشيفرة التي تتعامل مع DOM طويلةً ومتكررةً وقبيحةً أيضًا.

لكن هذه المشاكل والعيوب ليست حتميةً، فمن الممكن تصميم طرق مطوَّرة وأفضل للتعبير عن العمليات التي تنفذها أنت طالما تسمح لنا جافاسكربت بإنشاء تجريداتنا الخاصة، كما تأتي العديد من المكتبات الموجهة للبرمجة للمتصفحات بمثل تلك الأدوات.

التنقل داخل الشجرة

تحتوي عُقد DOM على روابط link كثيرة جدًا تشير إلى العُقد المجاورة لها، انظر المخطط التالي مثلًا:

html-links.png

رغم أن المخطط لا يظهر إلا رابطًا واحدًا من كل نوع إلا أنّ كل عُقدة لها خاصية parentNode التي تشير إلى العُقدة التي هي جزء منها إن وجدت، وبالمثل فكل عُقدة عنصر -التي تحمل النوع 1- لها خاصية childNodes التي تشير إلى كائن شبيه بالمصفوفة يحمل فروعه.

تستطيع نظريًا التحرك في أي مكان داخل الشجرة باستخدام روابط الأصول والفروع هذه، لكن جافاسكربت تعطيك أيضًا وصولًا إلى عدد من الروابط الإضافية الأخرى، فتشير الخاصيتان firstChild وlastChild إلى العنصرين الفرعيين الأول والأخير، أو تكون لهما القيمة null للعُقد التي ليس لها فروع، وبالمثل أيضًا تشير previousSibling وnextSibling إلى العُقد المتجاورة، وهي العُقد التي لها الأصل نفسه أو الأصل الذي يظهر قبل أو بعد العُقدة مباشرةً، وستحمل previousSibling القيمة null لأول فرع لعدم وجود شيء قبله، وكذلك ستحمل nextSibling القيمة null لآخر فرع.

لدينا أيضًا الخاصية children التي تشبه childNodes لكن لا تحتوي إلا عناصر فرعية -أي ذات النوع 1- ولا شيء آخر من بقية أنواع العُقد الفرعية، وذلك مفيد إذا لم تكن تريد العُقد النصية.

نفضِّل استخدام الدوال التعاودية recursive functions عند التعامل مع هيكل بيانات متشعب كما في المثال أدناه، حيث تقرأ الدالة التالية المستند بحثًا عن العُقد النصية التي تحتوي على نص معطى وتُعيد true إذا وجدته:

function talksAbout(node, string) {
  if (node.nodeType == Node.ELEMENT_NODE) {
    for (let child of node.childNodes) {
      if (talksAbout(child, 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

تحمل الخاصية nodeValue للعُقدة النصية السلسلة النصية التي تمثلها.

البحث عن العناصر

رغم أنّ التنقل بين الروابط سابقة الذكر يصلح بين الأصول parents والفروع children والأشقاء siblings، إلا أننا سنواجه مشاكل إذا أردنا البحث عن عُقدة بعينها في المستند.

فمن السيء اتباع الطريق المعتاد من document.body عبر مسار ثابت من الخصائص، إذ يسمح هذا بوضع فرضيات كثيرة في برنامجنا عن الهيكل الدقيق للمستند، وهو الهيكل الذي قد تريد تغييره فيما بعد.

تُنشأ كذلك العُقد النصية للمسافات الفارغة بين العُقد الأخرى، فوسم <body> يحمل أكثر من ثلاثة فروع والذين هم عنصر <h1> وعنصرين <p>، وإنما المسافات الفارغة بينها وقبلها وبعدها أيضًا، وبالتالي يكون سبعة فروع.

إذا أردنا الوصول إلى سمة href للرابط الذي في ذلك المستند فلن نكتب "اجلب الفرع الثاني للفرع السادس من متن المستند"، بل الأفضل هو قول "اجلب الرابط الأول في المستند"، ونحن نستطيع فعل ذلك، انظر كما يلي:

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 لإزالتها من عُقدة أباها، ولكي تضيف عُقدة فرعية إلى عُقدة عناصرية element node فيمكننا استخدام appendChild التي تضعها في نهاية قائمة الفروع، أو insertBefore التي تدخِل العُقدة المعطاة على أساس أول وسيط argument قبل العُقدة المعطاة على أساس وسيط ثاني.

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

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

لا يمكن للعُقدة أن توجد في المستند إلا في مكان واحد فقط، وعليه فإنّ إدخال فقرة Three في مقدمة الفقرة One سيزيلها أولًا من نهاية المستند ثم يدخلها في أوله، لنحصل على Three|One|Two، وبناءً على ذلك ستتسبب جميع العمليات التي تدخل عُقدة في مكان ما -على أساس أثر جانبي- في إزالتها من موقعها الحالي إن كان لها واحد.

يُستخدَم التابع 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 كذلك.

أما إذا أردت تجميعة ثابتة solid collection من العُقد -على النقيض من العُقد الحية- فستستطيع تحويل التجميعة إلى مصفوفة حقيقية باستدعاء 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 edition of ",
        elt("em", "The Open Society and Its Enemies"),
        ", 1950"));
</script>

السمات Attributes

يمكن الوصول إلى بعض سمات العناصر مثل href الخاصة بالروابط من خلال خاصية الاسم نفسه على كائن DOM الخاص بالعنصر وهذا شأن أغلب السمات القياسية المستخدَمة، لكن تسمح لك HTML بإسناد set أيّ عدد من السمات إلى العُقد، وذلك مفيد لأنه يسمح لك بتخزين معلومات إضافية في المستند، فإذا ألّفت أسماء سمات خاصة بك فلن تكون موجودة على أساس خصائص في عُقدة العنصر، بل يجب أن تستخدِم التابعَين 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>- يأخذ عرض المستند بأكمله وتُخرَج على أسطر مستقلة، وتسمى هذه العناصر بالعناصر الكتلية block elements؛ في حين بعضها الآخر مثل الروابط <a> والخط السميك <strong> تُخرَج على السطر نفسه مع النص المحيط بها، وتسمى هذه العناصر بـ: العناصر السطرية inline elements.

يستطيع المتصفح أن يضع مخططًا لأي مستند، بحيث يعطي كل عنصر فيه حجمًا وموضعًا وفقًا لنوعه ومحتواه، بعدها يُستخدَم هذا المخطط لرسم المستند في ما يعرضه المتصفح.

يمكن الوصول إلى حجم وموضع أي عنصر من خلال جافاسكربت، إذ تعطيك الخاصيتان offsetWidth وoffsetHeight المساحة التي تأخذها العناصر مقاسةً بالبكسلات pixels، وتُعَدّ البكسل أصغر وحدة قياس في المتصفح، وقد كانت تساوي أصغر نقطة تستطيع الشاشة عرضها، لكن الشاشات الحديثة التي تستطيع رسم نقاط صغيرة للغاية لا ينطبق عليها هذا المقياس، حيث يساوي البكسل الواحد عدة نقاط فيها، وبالمثل تعطي clientWidth وclientHeight حجم المساحة داخل العنصر متجاهلة عرض الإطار.

<p style="border: 3px solid red">
  أنا موجود داخل إطار
</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 بعرض 2000 بكسل، ويقيسان الوقت الذي يستغرقه كل واحد منهما.

<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>

التنسيق Styling

رأينا أنّ عناصر HTML المختلفة تُعرَض على الشاشة بطرق مختلفة، فبعضها يُعرَض في كتل مستقلة، وبعضها يكون داخل السطر نفسه، كما يضاف تخصيص مثل <strong> إلى بعض النصوص لجعلها سميكة، وكذلك يُضاف <a> إلى بعضها الآخر كي تظهر بلون أزرق وتحتها خط دلالةً على كونها رابطًا تشعبيًا.

ترتبط الطريقة التي يعرض بها وسم <img> صورة ما، أو يجعل وسم <a> رابطًا يذهب إلى صفحة أخرى عند النقر عليه ارتباطًا وثيقًا إلى نوع العنصر، لكن نستطيع تغيير التنسيق المرتبط بالعنصر مثل لون النص أو وضع خط أسفله.

انظر المثال التالي على استخدام خاصية style:

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

يمكن لسمة التنسيق style attribute أن تحتوي تصريحًا واحدًا أو أكثر، وهو خاصية -مثل color- متبوعة بنقطتين رأسيتين وقيمة -مثل green في المثال أعلاه-، وإذا كان لدينا أكثر من تصريح واحد فيجب فصل التصريحات بفواصل منقوطة كما في "color: red; border: none".

يتحكم التنسيق كما ترى في جوانب كثيرة من المستند. فمثلًا، تتحكم خاصية display في عرض العنصر على أنه كتلة مستقلة أو عنصر سطري، أي كما يلي:

يُعرض هذا النص <strong>في السطر كما ترى</strong>,
<strong style="display: block">مثل كتلة</strong>, و
<strong style="display: none">لا يُعرض على الشاشة</strong>.

سيُعرض وسم block في المثال السابق في سطر منفصل بما أن عناصر الكتل لا تُعرض داخل سطر مع نصوص حولها؛ أما الوسم الأخير فلن يُعرض مطلقًا بسبب none التي تمنع العنصر من الظهور على الشاشة، وتلك طريقة لإخفاء العناصر وهي مفضَّلة على الحذف النهائي من المستند لاحتمال الحاجة إليها في وقت لاحق.

يمكن لشيفرة جافاسكربت أن تعدّل مباشرةً على تنسيق عنصر ما من خلال خاصية style لذلك العنصر، وهذه الخاصية تحمل كائنًا له خصائص لكل خصائص التنسيق المحتملة، كما تكون قيم هذه الخصائص سلاسل نصية نكتبها كي نغيِّر جزءًا بعينه من تنسيق العنصر.

<p id="para" style="color: purple">
  هذا نص جميل
</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.

التنسيقات المورثة Cascading Styles

يسمى نظام تصميم وعرض العناصر في HTML باسم CSS، وهي اختصار لعبارة Cascading Style Sheets أو صفحات التنسيقات المُورَّثة، وتُعَدّ صفحة التنسيق style sheet مجموعةً من القوانين التي تحكم مظهر العناصر في مستند ما، ويمكن كتابتها داخل وسم <style>.

<style>
  strong {
    font-style: italic;
    color: gray;
  }
</style>
<p>الآن <strong>النص السميك </strong>صار مائلًا ورماديًا.</p>

وتشير المُورَّثة التي في هذه التسمية إلى إمكانية جمع عدة قواعد معًا لإنتاج التنسيق النهائي لعنصر ما.

تعطَّل أثر التنسيق الافتراضي لوسوم <strong> في المثال السابق التي تجعل الخط سميكًا بسبب القاعدة الموجودة في وسم <style> التي تضيف تنسيق الخط font-style ولونه color.

وإذا عرَّفت عدة قواعد قيمةً لنفس الخاصية، فإن أحدث قاعدة قُرِئت ستحصل على أسبقية أعلى وتفوز، لذا فإذا كان وسم <style> يحتوي على font-weight: normal وعارض قاعدة font-weight الافتراضية، فسيكون النص عاديًا وليس سميكًا، فالتنسيقات التي في سمة style والتي تُطبَّق مباشرةً على العُقدة لها أولوية أعلى وتكون هي الفائزة دائمًا.

من الممكن استهداف أشياء غير أسماء الوسوم في قواعد CSS، إذ سستُطبَّق قاعدة موجهة لـ ‎.abc على جميع العناصر التي فيها "abc" في سمة class الخاصة بها، وكذلك قاعدة لـ ‎#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 نفسها، ونوعية القاعدة مقياس لدقة وصف العناصر المتطابقة، وتُحدِّد بعدد جوانب العنصر التي يتطلبها ونوعها -أي الوسم أو الصنف أو المعرِّف ID. فمثلًا، تكون القاعدة التي تستهدف p.a أكثر تحديدًا من قاعدة تستهدف p أو ‎.a فقط، وعليه تكون لها الأولوية.

تطبّق الصيغة p > a {...}‎ التنسيقات المعطاة على جميع وسوم <a> التي تكون فروعًا مباشرةً من وسوم <p>، وبالمثل تطبّق p a {...}‎ على جميع وسوم <a> الموجودة داخل وسوم <p> سواءً كانت فروعًا مباشرةً أو غير مباشرة.

محددات الاستعلامات Query Selectors

لن نستخدم صفحات التنسيقات كثيرًا في هذه السلسلة، إذ يحتاج تعقيدها وتفصيلها إلى سلسلة خاصة بها، لكن فهمها ينفعك عند البرمجة في المتصفح، والسبب الذي جعلنا نذكر بُنية المحدِّد هنا -وهي الصيغة المستخدَمة في صفحات التنسيقات لتحديد العناصر التي تنطبق عليها مجموعة تنسيقات بعينها- هو أننا نستطيع استخدام التركيب اللغوي نفسه على أساس طريقة فعالة للعثور على عناصر 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>

لا يكون الكائن المعاد من querySelectorAll حيًا على عكس توابع مثل getElementsByTagName، كما لن يتغير إذا غيرت المستند، إذ لا يزال مصفوفةً غير حقيقية، لذا ستحتاج إلى استدعاء Array.from إذا أردت معاملته على أنه مصفوفة.

يعمل التابع querySelector -دون All- بأسلوب مشابه، وهو مفيد إذا أردت عنصرًا منفردًا بعينه، إذ سيعيد أول عنصر مطابق أو null إذا لم يكن ثمة مطابقة.

التموضع والتحريك

تؤثر خاصية التنسيق position على شكل التخطيط تأثيرًا كبيرًا، ولها قيمة static افتراضيًا، أي أن العنصر يظل في موضعه العادي في المستند، وحين تُضبط على relative فسيأخذ مساحةً في المستند أيضًا لكن مع اختلاف أنّ الخصائص التنسيقية top وleft يمكن استخدامها لتحريكه نسبة إلى ذلك الموضع العادي له.

أما حين تُضبط position على absolute فسيُحذَف العنصر من التدفق الاعتيادي للمستند normal flow، أي لا يأخذ مساحة، وإنما قد يتداخل مع عناصر أخرى، وتُستخدم top وleft هذه المرة لموضعة العنصر بصورة مطلقة هذه المرة نسبةً إلى الركن الأيسر العلوي لأقرب عنصر مغلِّف تكون خاصية position له غير static، أو نسبة إلى المستند ككل إن لم يوجد عنصر مغلِّف.

نستخدِم ما سبق عند إنشاء تحريك animation، كما يوضح المستند التالي الذي يعرض صورة قطة تتحرك في مسار قطع ناقص ellipse.

<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، كما سنشرح هذه الدوال إذا لم يكن لك بها خبرة سابقة بما أننا سنستخدمها بضعة مرات في هذه السلسلة.

تُستخدَم هاتان الدالتان لإيجاد نقاط تقع على دائرة حول نقطة الإحداثي الصفري (0,0) والتي لها نصف قطر يساوي 1، كما تفسِّران وسيطها على أساس موضع على هذه الدائرة مع صفر يشير إلى النقطة التي على أقصى يمين الدائرة، ويتحرك باتجاه عقارب الساعة حتى يقطع محيطها الذي يساوي 2 باي -أي 2π- والتي يكون مقدارها هنا 6.28 تقريبًا.

تخبرك Math.cos بإحداثية x للنقطة الموافقة للموضع الحالي، في حين تخبرك Math.sin بإحداثية y، وأيّ موضع أو زاوية أكبر من 2 باي 2π -أي محيط الدائرة- أو أقل من صفر يكون صالحًا ومقبولًا، ويتكرر الدوران إلى أن تشير a+2π إلى نفس الزاوية التي تشير إليها a.

تسمى هذه الوحدة التي تقاس بها الزوايا باسم الزاوية نصف القطرية أو راديان radian، والدائرة الكاملة تحتوي على ‎2 π راديان، ويمكن الحصول على الثابت الطبيعي باي π في جافاسكربت من خلال Math.PI.

cos_sin.png

يكون لشيفرة تحريك القطة مقياسًا يدعى angle للزاوية الحالية التي عليها التحريك، بحيث يتزايد في كل مرة تُستدعى فيها دالة animate، ثم يمكن استخدام هذه الزاوية لحساب الموضع الحالي لعنصر الصورة.

يُحسب التنسيق العلوي top باستخدام Math.sin ويُضرب في 20، وهو نصف القطر الرأسي للقطع الناقصة في مثالنا، وبالمثل يُبنى تنسيق left على Math.cos، ويُضرب في 200 لأن القطع الناقص عرضه أكبر من ارتفاعه.

لاحظ أن التنسيقات تحتاج إلى وحدات في الغالب، وفي تلك الحالة فإننا نحتاج أن نلحق "px" إلى العدد ليخبر المتصفح أن وحدة العدّ التي نستخدمها هي البكسل -وليس سنتيمترات أو ems أو أي شيء آخر-، وهذه النقطة مهمة لسهولة نسيانها، حيث ستتسبب كتابة أعداد دون وحدات في تجاهل التنسيق الخاص بك، إلا إن كان العدد صفرًا، وذلك لأن معناه لا يختلف مهما اختلفت الوحدات.

خاتمة

تستطيع البرامج المكتوبة بجافاسكربت فحص المستند الذي يعرضه المتصفح والتدخل فيه بالتعديل، من خلال هيكل بيانات يسمى 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> يمثل الصف الواحد، ونستطيع في كل صف وضع عناصر الخلايا سواءً كانت خلايا ترويسات <th> أو عادية <td>.

ولِّد هيكل DOM لجدول يَعُد الكائنات إذا أُعطيت مجموعة بيانات لجبال ومصفوفة من الكائنات لها الخصائص name وheight وplace، بحيث يجب أن تحتوي على عمود لكل مفتاح وصَفّ لكل كائن، إضافةً إلى صف ترويسة بعناصر <th> في الأعلى لتسرد أسماء الأعمدة.

اكتب ذلك بحيث تنحدر الأعمدة مباشرةً من الكائنات، من خلال أخذ أسماء الخصائص للكائن الأول في البيانات، وأضف الجدول الناتج إلى العنصر الذي يحمل سمة id لـ "mountains" كي يصبح ظاهرًا في المستند.

بمجرد أن يعمل هذا، اجعل محاذاة الخلايا التي تحتوي قيمًا عدديةً إلى اليمين من خلال ضبط خاصية style.textAlign لها لتكون "right".

تستطيع تعديل شيفرة التدريب لكتابة الحل وتشغيلها في طرفية المتصفح إن كنت تقرأ من متصفح، أو بنسخها إلى codepen.

<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"}
  ];

  // ضع شيفرتك هنا
</script>

إرشادات للحل

استخدِم document.createElement لإنشاء عُقد عناصر جديدة، وdocument.createTextNode لإنشاء عُقد نصية، والتابع appendChild لوضع العُقد داخل عُقد أخرى.

قد تريد التكرار على أسماء المفاتيح مرةً كي تملأ الصف العلوي، ثم مرةً أخرى لكل كائن في المصفوفة لتضع بيانات الصفوف، كما يمكنك استخدام Object.keys للحصول على مصفوفة أسماء المفاتيح من الكائن الأول.

استخدِم document.getElementById أو document.querySelector لإيجاد العُقدة التي لها سمة id الصحيحة، إذا أردت إضافة الجدول إلى العُقدة الأصل المناسبة.

جلب العناصر بأسماء وسومها

يُعيد التابع document.getElementsByTagName جميع العناصر الفرعية التي لها اسم وسم معيَّن. استخدِم نسختك الخاصة منه على أساس دالة تأخذ عُقدةً وسلسلةً نصيةً -هي اسم الوسم- على أساس وسائط، وتُعيد مصفوفةً تحتوي على عُقد العناصر المنحدرة منه، والتي لها اسم الوسم المعطى.

استخدِم خاصية nodeName لعنصر ما كي تحصل على اسم الوسم الخاص به، لكن لاحظ أن هذا سيعيد اسم الوسم بأحرف إنجليزية من الحالة الكبيرة capital، لذا يمكنك استخدام التابعين النصيين toLowerCase أو toUpperCase لتعديل حالة تلك الحروف كما تريد.

تستطيع تعديل شيفرة التدريب لكتابة الحل وتشغيلها في طرفية المتصفح إن كنت تقرأ من متصفح، أو بنسخها إلى codepen.

<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) {
    // ضع شيفرتك هنا.
  }

  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"> في القطع الناقص أو اجعل القبعة تدور حول القطة أو أي تعديل يعجبك في طريقة حركتيهما.

لتسهيل موضعة الكائنات المتعددة، من الأفضل استخدام التموضع المطلق absolute positioning، وهذا يعني أن top وleft تُحسبان نسبةً إلى أعلى يسار المستند.

أضف عددًا ثابتًا من البكسلات إلى قيم الموضع كي تتجنب استخدام الإحداثيات السالبة التي ستجعل الصورة تتحرك خارج الصفحة المرئية.

تستطيع تعديل شيفرة التدريب لكتابة الحل وتشغيلها في طرفية المتصفح إن كنت تقرأ من متصفح، أو بنسخها إلى codepen.

<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";

    // ضع شيفرتك هنا.

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

إرشادات للحل

تقيس الدالتان Math.cos وMath.sin الزوايا بصورة نصف دائرية أي بواحدة الراديان، فإذا كانت الدائرة تساوي 2 باي 2π كما تقدَّم، فستستطيع الحصول على الزاوية المقابلة بإضافة نصف هذه القيمة -والتي تساوي باي أو π- باستخدام Math.PI، وبالتالي سيسهل عليك وضع القبعة على الجهة المقابلة من القطة.

ترجمة -بتصرف- للفصل الرابع عشر من كتاب Elequent Javascript لصاحبه Marijn Haverbeke.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...