جافاسكربت في المتصفح التعديل على صفحة HTML عبر جافاسكربت


عائشة ميرا

يُعدّ التعديل على نموذج تمثيل المستند ككائن (DOM) مفتاح الحصول على صفحات "حيّة" (ديناميكية). سوف تتعرف فيما يلي على كيفية إنشاء عناصر جديدة على الصفحات في لمح البصر (بطريقة آنية دون الحاجة إلى إعادة تحميل الصفحات).

مثال: إظهار رسالة

فلنعرض ذلك على شكل مثالٍ تطبيقيٍ، حيث نُضيف رسالةً للصفحة تكون أكثر تنسيقًا من رسالة alert كالآتي:

<style>
.alert {
  padding: 15px;
  border: 1px solid #d6e9c6;
  border-radius: 4px;
  color: #3c763d;
  background-color: #dff0d8;
}
</style>
<div class="alert">
  <strong>Hi there!</strong> You've read an important message.
</div>

استعملنا في هذا المثال لغة HTML. فلنُنشئ نفس الحاوية div باستخدام جافاسكربت (على افتراض أن شيفرة HTML/CSS تتضمّن الأنماط التنسيقية).

إنشاء عنصر ما

توجد طريقتان لإنشاء عناصر (DOM):

  • الدالّة (document.createElement(tag: تُستخدم لإنشاء عقدة عناصرية جديدة باستعمال الوسم الذي يُمرَّر لها كالآتي:
let div = document.createElement('div');
  • الدالّة (document.createTextNode(text: تُستخدم لإنشاء عقدة نصية جديدة باستعمال النص الذي يُمرَّر لها كالآتي:
let textNode = document.createTextNode('Here I am');

غالبا ما نحتاج إلى إنشاء عقدٍ عناصريةٍ، مثل الحاوية div، من أجل إظهار الرسالة.

إنشاء الرسالة

يتطلّب إنشاء حاوية الرسالة div ثلاث مراحل:

// 1. إنشاء العنصر <div>
let div = document.createElement('div');

// 2. إسناد القيمة "alert"  لصنف الحاوية 
div.className = "alert";

// 3. وضع المحتوى داخل الحاوية 
div.innerHTML = "<strong>Hi there!</strong> You've read an important message.";

صحيحٌ أنّنا أنشأنا العنصر ولكنه لحد الآن غير ظاهرٍ في الصفحة لأنّنا أنشأناه داخل المتغيّر div ولم نُضفه بعد للصفحة.

دوالّ إضافة العناصر للصفحة

من أجل إظهار الحاوية div، نحتاج إلى إضافتها في موضع ما من الصفحة، داخل العنصر <body> مثلا والمشار إليه بالكائن document.body. توجد دالّة خاصة تسمّى append تساعد على ذلك كما هو موضّح في المثال الآتي (انظر السطر الموسوم بـ (*)):

<style>
.alert {
  padding: 15px;
  border: 1px solid #d6e9c6;
  border-radius: 4px;
  color: #3c763d;
  background-color: #dff0d8;
}
</style>

<script>
  let div = document.createElement('div');
  div.className = "alert";
  div.innerHTML = "<strong>Hi there!</strong> You've read an important message.";

  document.body.append(div);//(*)
</script>

نادينا، في هذا المثال، الدالّة append وطبقناها على الكائن document.body. كان يمكن أن نُطبقها على أيّ عنصرٍ كان من أجل إضافة عنصرٍ آخر له، حيث يمكننا على سبيل المثال إضافة عنصر ما للعنصر <div> باستعمال الدالّة div.append(anotherElement)‎. وهذه قائمة دوالّ غيرها تُستعمل لإضافة عنصرٍ ما لعنصرٍ آخرٍ في موضعٍ معيّنٍ من الصفحة:

  • node.append(...nodes or strings)‎: إضافة عقد أو سلاسل نصية في نهاية العقدة node.
  • node.prepend(...nodes or strings)‎: إضافة عقد أو سلاسل نصية في بداية العقدة node.
  • node.before(...nodes or strings)‎: إضافة عقد أو سلاسل نصية قبل العقدة node.
  • node.after(...nodes or strings)‎: إضافة عقد أو سلاسل نصية بعد العقدة node.
  • node.replaceWith(...nodes or strings)‎: وضع عقد أو سلاسل نصية مكان العقدة node.

وتكون وسائط هذه الدوالّ عبارة عن قائمة عشوائية من عقد DOM أو سلاسل نصية (التي تصبح آليا عقدًا نصية). دعنا نرى ذلك عبر المثال الحيّ التالي، حيث تُستخدم فيه هذه الدوالّ لإضافة عناصر li لقائمة ما ونصين، أحدهما قبلها والآخر بعدها.

<ol id="ol">
  <li>0</li>
  <li>1</li>
  <li>2</li>
</ol>

<script>
  ol.before('before'); // إضافة السلسلة "before" قبل العنصر <ol>
  ol.after('after'); // إضافة السلسلة "after" بعد العنصر <ol>

  let liFirst = document.createElement('li');
  liFirst.innerHTML = 'prepend';
  ol.prepend(liFirst); // إضافة  العنصر liFirst في بداية القائمة  <ol>

  let liLast = document.createElement('li');
  liLast.innerHTML = 'append';
  ol.append(liLast); // إضافة  العنصر liLast في نهاية القائمة  <ol>
</script>

001insertionMethodsExample.png

وفيما يلي صورةٌ توضيحيةٌ لما تقوم به هذه الدوالّ:

002insertionMethods.png

وبالتالي تصبح القائمة في شكلها النهائي كما يلي:

before
<ol id="ol">
  <li>prepend</li>
  <li>0</li>
  <li>1</li>
  <li>2</li>
  <li>append</li>
</ol>
after

وكما سبق أن ذكرنا، يُمكننا إضافة العديد من العقد والسلاسل النصية بمناداة الدالّة مرّةً واحدةً فقط. في المثال التالي، أُضيفت كلٌّ من السلسلة النصية والعنصر دفعةً واحدةً:

<div id="div"></div>
<script>
  div.before('<p>Hello</p>', document.createElement('hr'));
</script>

لاحظ هنا أن النص أُضيف بمثابة "نصٍ" وليس بمثابة شيفرة HTML، حيث تظهر التسلسلات التهريبية مثل < و>. وبالتالي تصبح شيفرة HTML النهائية كالآتي:

<p>Hello</p>
<hr>
<div id="div"></div>

وبعبارة أخرى، تُضاف السلاسل النصية بطريقة آمنة كأنها أُضيفت باستعمال الخاصية textContent.

تُستخدم هذه الدوالّ إذا فقط لإضافة عقد DOM أو مقاطع نصية. ولكن ماذا لو أردنا إضافة سلسلةٍ نصيةٍ تحوي شيفرة HTML بما فيها الوسوم وغيرها، مثل ما هو معمول به مع الخاصية innerHTML؟

الدوالّ insertAdjacentHTML/Text/Element

والجوابٍ على السؤال السابق، هو استخدام دالّةٍ أخرى متعدّدة الاستعمالات، إنها الدالّة elem.insertAdjacentHTML(where, html)‎. الوسيط الأوّل where للدالّة عبارة عن كلمة رمز (code word) تُحدِّد في أيّ موضع بالضبط من العنصر سيُضاف الوسيط الثاني، وتأخذ هذه الكلمة الرمزواحدةً من القيم التالية:

  • "beforebegin": إضافة شيفرة HTML مباشرةً قبل العنصرelem.
  • "afterbegin": إضافة شيفرة HTML في بداية العنصرelem.
  • "beforeend": إضافة شيفرة HTML في نهاية العنصرelem.
  • "afterend": إضافة شيفرة HTML مباشرةً بعد العنصرelem.

ويكون الوسيط الثاني html للدالّة عبارة عن سلسلة نصية تُضاف على شكل شيفرة HTML. لاحظ المثال التالي:

<div id="div"></div>
<script>
  div.insertAdjacentHTML('beforebegin', '<p>Hello</p>');
  div.insertAdjacentHTML('afterend', '<p>Bye</p>');
</script>

الذي يولّد الشيفرة التالية:

<p>Hello</p>
<div id="div"></div>
<p>Bye</p>

كانت هذه هي الطريقة التي تُمكّننا من إضافة شيفرة HTML، أيًّا كانت، للصفحة. وفيما يلي صورة توضيحية لطرائق الإضافة في مختلف المواضع:

003insertAdjacentHTML.png

ويمكننا بسهولة ملاحظة أوجه الشبه بين هذه الصورة والصورة التي قبلها، حيث أن مواضع الإضافة هي نفسها لكن هذه الدالّة تُضيف شيفرة HTML في حين تُضيف السابقة عقدًا أو سلاسل نصيةً. ولهذه الدالّة دالّتين "شقيقتين":

  • elem.insertAdjacentText(where, text)‎: لها نفس البنية ولكن السلسلة النصية تُضاف هنا كنص وليس كشيفرة HTML.
  • elem.insertAdjacentElement(where, elem)‎: لها أيضًا نفس البنية ولكنها تُستعمل لإضافة عنصرٍ ما.

وُجدت هاتين الدالّتين فقط لضرورة توحيد البنية، ولكن تُستخدم فقط الدالّة elem.insertAdjacentHTML على أرض الواقع، طالما وُجدت الدوالّ append/prepend/before/after التي تُستخدم لإضافة كلٍّ من العقد والنصوص بالإضافة إلى أنها أقصر من حيث عدد الأحرف.
وفيما يلي طريقةٌ بديلةٌ لإظهار رسالةٍ ما:

<style>
.alert {
  padding: 15px;
  border: 1px solid #d6e9c6;
  border-radius: 4px;
  color: #3c763d;
  background-color: #dff0d8;
}
</style>
<script>
  document.body.insertAdjacentHTML("afterbegin", `<div class="alert">
    <strong>Hi there!</strong> You've read an important message.
  </div>`);
</script>

إزالة/حذف عقدة ما

اُستحدثت الدالّة node.remove()‎ لإزالة/حذف عقدةٍ ما. فلنجعل الرسالة التي أظهرناها في المثال السابق تختفي بعد ثانية واحدة من إظهارها:

<style>
.alert {
  padding: 15px;
  border: 1px solid #d6e9c6;
  border-radius: 4px;
  color: #3c763d;
  background-color: #dff0d8;
}
</style>

<script>
  let div = document.createElement('div');
  div.className = "alert";
  div.innerHTML = "<strong>Hi there!</strong> You've read an important message.";

  document.body.append(div);
  setTimeout(() => div.remove(), 1000);
</script>

لاحظ هنا أنه لا داعيَ لتحويل عنصرٍ ما من الموضع الذي كان فيه إذا أردت وضعه في مكان آخر، هذا لأنّ كافة دوالّ الإضافة تُحوِّل آليا العنصر من مكانه السابق. فلنُحوّل من خلال المثال الآتي موضعي العنصرين التاليين:

<div id="first">First</div>
<div id="second">Second</div>
<script>
  // لا داعيَ لمناداة الدالّة remove
  second.after(first); //تُضيف العنصر second بعد العنصر first
</script>

نسخ العُقد باستعمال الدالة cloneNode

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

تُنشِئ الدالّة elem.cloneNode(true)‎ عند مناداتها نسخةً كاملةً للعنصر، بما فيه من سمات وتوابع (العناصر الوليدة). أما إذا نادينا الدالّة elem.cloneNode(false)‎- مع تغيير الوسيط من true إلى false- تُنشِئ الدالّة العنصر وحده دون العناصر التابعة له (العناصر الوليدة).

وهذا مثال عن نسخ الرسالة:

<style>
.alert {
  padding: 15px;
  border: 1px solid #d6e9c6;
  border-radius: 4px;
  color: #3c763d;
  background-color: #dff0d8;
}
</style>

<div class="alert" id="div">
  <strong>Hi there!</strong> You've read an important message.
</div>

<script>
  let div2 = div.cloneNode(true); // نسخ الرسالة 
  div2.querySelector('strong').innerHTML = 'Bye there!'; // تغيير محتوى الرسالة المنسوخة 

  div.after(div2); //  إظهار النسخة بعد الحاوية الأولى
</script>

العقدة الخاصة DocumentFragment

تُعدّ DocumentFragment عقدة DOM خاصة تُستعمل كمُغلِّف (wrapper) لتمرير قوائم تحوي عددًا من العقد. ويمكننا إضافة عقدٍ لها باستعمال الدالّة append، ولكن عند إضافتها هي (أي العقدة الخاصة DocumentFragment) في موضعٍ ما، يكون محتواها هو ما يُضاف في هذا الموضع بدلا عنها.

تُنشِئ الدالّة getListContent في هذا المثال مقطعا يتضمّن عناصر <li> تُضاف لاحقا إلى العنصر <ul>:

<ul id="ul"></ul>
<script>
function getListContent() {
  let fragment = new DocumentFragment();
  for(let i=1; i<=3; i++) {
    let li = document.createElement('li');
    li.append(i);
    fragment.append(li);
  }
  return fragment;
}
ul.append(getListContent()); // (*)
</script>

لاحظ هنا في السطر الأخير الموسوم بالرمز (*) أنّنا أضفنا العقدة الخاصة DocumentFragment باستعمال الدالّة append (التي تضيف عنصرًا في نهاية عنصرٍ آخر) لكنها تخلّلت الشيفرة فحصلنا على البنية التالية:

<ul>
  <li>1</li>
  <li>2</li>
  <li>3</li>
</ul>

ونادرًا ما تُستخدم عقدة DOM الخاصية DocumentFragment، ذلك لأنه لا يوجد سببٌ يحملنا على إضافة عنصرٍ أو عناصرَ إلى عقدة من نوعٍ خاصٍ في حين يمكننا وضعه/وضعها في مصفوفة من العقد كالآتي:

<ul id="ul"></ul>

<script>
function getListContent() {
  let result = [];

  for(let i=1; i<=3; i++) {
    let li = document.createElement('li');
    li.append(i);
    result.push(li);
  }

  return result;
}

ul.append(...getListContent()); 
</script>

أتينا على ذكر العقدة الخاصة DocumentFragment لأنها تندرج ضمن بعض المفاهيم، كالعنصر template (أي القالب) مثلا، التي سنتطرّق إليها لاحقا.

دوال الإضافة والحذف على الطريقة القديمة

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

  • append
  • prepend
  • before
  • after
  • remove
  • replaceWith

السبب الوحيد الذي جعلنا نذكر لك هنا هذه الدوالّ هو أنك يمكن أن تصادفها في الكثير من السكربتات القديمة.

الدالة appendChild‎

تضيف الدالّة parentElem.appendChild(node)‎ العقدة node كآخر عنصرٍ وليدٍ للعنصر parentElem. سنضيف في المثال التالي عنصرًا جديدًا <li> في نهاية العنصر الوالد <ol>:

<ol id="list">
  <li>0</li>
  <li>1</li>
  <li>2</li>
</ol>

<script>
  let newLi = document.createElement('li');
  newLi.innerHTML = 'Hello, world!';

  list.appendChild(newLi);
</script>

الدالة insertBefore

وتضيف الدالّة parentElem.insertBefore(node, nextSibling)‎ العقدة node قبل العقدة nextSibling ضمن العقدة parentElem.

تُضيف الشيفرة التالية عنصرًا جديدًا للقائمة <ol> مباشرة قبل العنصر<li> الثاني في القائمة:

<ol id="list">
  <li>0</li>
  <li>1</li>
  <li>2</li>
</ol>
<script>
  let newLi = document.createElement('li');
  newLi.innerHTML = 'Hello, world!';

  list.insertBefore(newLi, list.children[1]);
</script>

يمكننا إضافة العقدة newLi في المرتبة الأولى كالآتي:

list.insertBefore(newLi, list.firstChild);

الدالة replaceWith

تَستبدلُ هذه الدالّة العقدة node بالعقدة oldChild التي تُعدّ وليدة العقدة parentElem.

الدالة removeChild

تحذِف هذه الدالّة العقدة node من العقدة parentElem (حيث نفترض هنا أن العقدة node وليدة العقدة parentElem).

يُحذَف العنصر الأول <li> من القائمة <ol> في المثال الموالي:

<ol id="list">
  <li>0</li>
  <li>1</li>
  <li>2</li>
</ol>

<script>
  let li = list.firstElementChild;
  list.removeChild(li);
</script>

تكون مخرجات (output) هذه الدوالّ عبارة عن عُقد (العقد التي حُذفت/التي أُضيفت)، حيث تُوّلِد الدالّة parentElement.appendChild(node)‎ عقدةً ولكن غالبًا ما لا تُستعمل القيمة التي توّلدت من مناداة هذه الدالّة، نحتاج فقط إلى تطبيق الدالّة/تنفيذها.

نبذة عن الدالة "document.write"

هناك أيضا الدالّة document.write، وهي دالّة قديمة جدًا تُستخدم لإضافة جزءٍ ما لصفحة الويب، وبنيتها كالآتي:

<p>Somewhere in the page...</p>
<script>
  document.write('<b>Hello from JS</b>');
</script>
<p>The end</p>

تؤدي مناداة الدالّة document.write(html)‎ إلى كتابة الشيفرة html في الصفحة 'آنذاك وفي هذا الموضع بالضبط'. وبذلك تُولَّد السلسلة النصية html بطريقة ديناميكية (آنية) مرنة. يمكن إذًا استعمال لغة جافاسكربت لإنشاء وكتابة صفحة ويب كاملة.

اِستُحدثت هذه الدالّة في وقت لم تكن فيه كائنات DOM موجودة ولم تكن هناك معايير أيضًا….إلخ، أي منذ زمنٍ بعيدٍ جدًا، ومازالت 'حيّة' ما دام البعض يستعملها لحد الآن في سكربتاته، ولكن نادرًا ما نصادفها في السكربتات العصرية الدارجة حاليا، نظرًا لمحدوديتها في الجوانب التالية:

تُنفَّذ مناداة الدالّة document.write فقط أثناء تحميل الصفحة وفي حال نُوديَت بعد ذلك، يُحذف محتوى الصفحة كما في المثال التالي:

<p>After one second the contents of this page will be replaced...</p>
<script>
  // مناداة الدالّة document.write بعد مرور ثانية واحدة 
  // أي بعد تحميل الصفحة، فتَحذِف هذه الدالّة المحتوى الأصلي للصفحة 
  setTimeout(() => document.write('<b>...By this.</b>'), 1000);
</script>

لا ينبغي إذًا استعمال هذه الدالّة بعد تحميل الصفحة، على عكس غيرها من دوالّ DOM التي تحدثنا عنها سابقا.

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

فإذا أردنا مثلا إضافة سلسلةٍ كبيرةٍ تحوي شيفرة HTML بطريقة ديناميكية في مرحلة تحميل الصفحة، وكان عامل السرعة يصنع الفارق بالنسبة لنا، يمكن أن تكون هذه الطريقة فعالة جدًا. ولكننا نادرًا ما نصادف هذه السيناريوهات على أرض الواقع. بل غالبا ما نصادف هذه الدالّة في بعض السكربتات القديمة (سكربتات كُتبت في وقت لم تكن تُستعمل هذه الدالّة لأغراضٍ كهذه).

الخلاصة

  • الدوال التي تُستخدم في إنشاء عقدٍ جديدةٍ هي:
    • document.createElement(tag)‎: تُنشِئ عنصرًا بناءً على الوسم tag.
    • document.createTextNode(value)‎: تُنشِئ عقدةً نصيةً (نادرًا ما تُستخدم).
    • elem.cloneNode(deep)‎: تنسخ العنصر، مع كل توابعه (أي العناصر الوليدة) إذا كانت قيمة المعامل deep تُساوي true.
  • الدوالّ التي تستخدم للإضافة والحذف هي:
    • node.append(...nodes or strings)‎: تُضيف عقدة أو سلسلة نصية في نهاية العقدة node. -node.prepend(...nodes or strings)‎: تُضيف عقدة أو سلسلة نصية في بداية العقدة node. -node.before(...nodes or strings)‎: تُضيف عقدة أو سلسلة نصية مباشرة قبل العقدة node. -node.after(...nodes or strings)‎: تُضيف عقدة أو سلسلة نصية مباشرة بعد العقدة node. -node.replaceWith(...nodes or strings)‎: تستبدل بالعقدة node ما مُرِّر إليها من عقدة/عقد أو محتوى نصي. -node.remove()‎: تحذف العقدة node.

وهي تُضيف السلاسل النصية كنص وليس كشيفرة HTML.

  • الدوالّ القديمة هي:
    • rent.appendChild(node)‎
    • parent.insertBefore(node, nextSibling)‎
    • parent.removeChild(node)‎
    • parent.replaceChild(newElem, node)‎

وتكون مُخرجات (output) هذه الدوالّ عبارة عن عُقد.

  • تضيف الدالّة elem.insertAdjacentHTML(where, html)‎ شيفرة HTML، التي تُمرّر لها عبر الوسيط html، في المواضع التالية بناءً على قيمة الوسيط where:
  • "beforebegin": تُضاف الشيفرة html مباشرة قبل العنصر elem.
  • "afterbegin": تُضاف الشيفرة html في بداية العنصر elem.
  • "beforeend": تُضاف الشيفرة html في نهاية العنصر elem.
  • "afterend": تُضاف الشيفرة html مباشرة بعد العنصر elem.
  • هناك أيضًا دوالٌّ مماثلةٌ لها وهي elem.insertAdjacentText وelem.insertAdjacentElement ولكن نادرًا ما تٌستخدم.
  • إذا أردت إضافة شيفرة HTML إلى الصفحة قبل نهاية عملية تحميلها اِستعمل الدالّة document.write(html)‎.
    تؤدي مناداة هذه الدالّة بعد انتهاء عملية تحميل الصفحة إلى مسح محتوى الصفحة، ونُصادفها خاصة في السكربتات القديمة.

تمارين

createTextNode/innerHTML/textContent

درجة الأهمية: 5 لدينا عنصر DOM فارغ يسمّى elem وسلسلة نصية text. حدِّد، من بين التعليمات الثلاث التالية، تلك التي لها نفس الوظيفة.

  1. elem.append(document.createTextNode(text))‎
  2. elem.innerHTML = text
  3. elem.textContent = text

الحل

التعليمتان 1 و3 لهما نفس الوظيفة، حيث تضيف كلتا التعليمتين السلسلة النصية text للعنصر elem كنص وليس كشيفرة HTML. وفيما يلي مثال على ذلك:

<div id="elem1"></div>
<div id="elem2"></div>
<div id="elem3"></div>
<script>
  let text = '<b>text</b>';
  elem1.append(document.createTextNode(text));
  elem2.innerHTML = text;
  elem3.textContent = text;
</script>

مسح عنصر ما

درجة الأهمية: 5

أنشئ الدالّة clear(elem)‎ التي تحذف محتوى العنصر elem بالكامل.

<ol id="elem">
  <li>Hello</li>
  <li>World</li>
</ol>

<script>
  function clear(elem) { /*اكتب الشيفرة هنا */ }

  clear(elem); // clears the list
</script>

الحل

فلنبدأ بالحل الذي لا ينبغي أن تقترحه، لأنه ببساطة حلٌ خاطئ.

function clear(elem) {
  for (let i=0; i < elem.childNodes.length; i++) {
      elem.childNodes[i].remove();
  }
}

طريقة الحل هذه لا تنفع، لأن مناداة الدالّة remove()‎ تؤدي إلى تغيير مواضع العُقد ضِمن المجموعة elem.childNodes، فبعد حذف العقدة الأولى مثلا (التي كانت في الموضع 0 من المجموعة) تُسحب المجموعة كاملةً للخلف لتصبح العقدة ذات الموضع 1 مكان العقدة ذات الموضع 0، لكن المؤشر i في الدورة الثانية من الحلقة يأخذ القيمة 1 وبالتالي تُحذف العقدة ذات الموضع 1 وتبقى العقدة ذات الموضع 0 موجودة، وهكذا دواليك. لن تُحذف إذا بعض العناصر من المجموعة. ويحدث نفس الشيء عند استعمال الحلقة for...of.

الحل الصحيح يكون مثلا كالآتي:

function clear(elem) {
  while (elem.firstChild) {
    elem.firstChild.remove();
  }
}

أو بطريقة أبسط:

function clear(elem) {
  elem.innerHTML = '';
}

لماذا لا تختفي السلسلة 'aaa'؟

درجة الأهمية: 1

عند مناداة الدالّة table.remove()‎، في المثال التالي، يُحذف الجدول من الصفحة ولكن السلسلة 'aaa' تبقى ظاهرة. فلماذا يحدث ذلك؟

<table id="table">
  aaa
  <tr>
    <td>Test</td>
  </tr>
</table>

<script>
  alert(table); // الجدول موجود

  table.remove();
  // لماذا مازالت السلسلة 'aaa' ؟ ظاهرة 
</script>

الحل

يعود السبب في حدوث ذلك إلى خطأ في شيفرة HTML لهذا التمرين.

اِعلم أن من مهام المتصفّح تصحيح الأخطاء آليًا. وبما أن المواصفة التقنية تنصّ على أن الجداول لا تضمّ أيّ سلاسل نصية، بل فقط الوسوم الخاصة بالجداول، يُضيف المتصفح السلسلة "aaa" قبل الجدول table مباشرة. وعندما يُحذف الجدول، تبقى السلسلة "aaa" ظاهرة.

للإجابة على سؤال هذا التمرين، يمكنك الإستعانة بعرض نموذج تمثيل المستند ككائن DOM باستعمال المتصفح، وستظهر لك السلسلة "aaa" قبل الجدول table.

تُحدِّد معايير HTML، وبالتفصيل، كيف يُعالِج المتصفح الشيفرة التي تحتوي على أخطاء، ويُعدّ تعامل المتصفح بهذه الطريقة مع الخطأ المتضمَّن في شيفرة HTML لهذا التمرين صحيحا.


إنشاء قائمة

درجة الأهمية: 4 اكتب شيفرة تُنشِئ واجهةً تسمح للمستخدم بإدخال محتوى ما على مراحل وتجمع هذا المحتوى في شكل قائمة. ولكل عنصر من القائمة عليك:

  • مطالبة المستخدِم بإدخال المحتوى باستعمال الدالّة prompt.
  • إنشاء العنصر<li> بناءً على المحتوى الذي أدخله المستخدِم.
  • مواصلة العملية إلى غاية فراغ المستخدِم من إدخال المعطيات (بالضغط على المفتاح esc على لوحة المفاتيح، أو الضغط على زر الالغاء الظاهر على النافذة).

يجب أن تُنشَئ العناصر ديناميكيًا وإذا أدخل المستخدِم وسوم HTML، تُعالَج على أنها نصوص، لا شيفرة HTML.

الحل

ملاحظة: لاحظ استعمال الخاصية textContent لإسناد محتوى للعنصر <li>.

يمكنك الإطلاع على الحل من هنا


إنشاء شجرة اعتمادًا على كائن

درجة الأهمية: 5

اكتب الدالّة createTree()‎ التي تُنشئ قائمةً متفرعةً من العناصر ul/li بناءً على كائن متفرع:

let data = {
  "Fish": {
    "trout": {},
    "salmon": {}
  },

  "Tree": {
    "Huge": {
      "sequoia": {},
      "oak": {}
    },
    "Flowering": {
      "apple tree": {},
      "magnolia": {}
    }
  }
};

وتكون بنيتها كالآتي:

let container = document.getElementById('container');
createTree(container, data); // تُنشِئ الشجرة بداخل الحاوية ‘container’ 

على أن تكون النتيجة المتحصل عليها كالآتي:

004nestedTree.png

اِختر واحدةً من بين الطريقتين التاليتين لحل التمرين:

  • أنشئ الشجرة في شكل شيفرة HTML ثم أسندها للخاصية Container.innerHTML أو
  • أنشئ عُقد الشجرة وأضفها باستعمال دوالّ DOM.

ويُستحسن أن تَحُل التمرين بالطريقين.

ملاحظة: لا ينبغي أن تَضمّ الشجرة في أوراقها عناصر إضافية فارغة <ul></ul>.

الحل

ملاحظة: أسهل طريقة لتصفح الكائن (المرور بعناصره الواحد تلوى الآخر) تكون باستعمال التعاود (recursion).

  1. حل التمرين باستعمال الخاصية innerHTML
  2. حل التمرين باستعمال دوالّ DOM

إظهار العناصر الوليدة لشجرة ما

درجة الأهمية: 5

لدينا هذه الشجرة وهي مبنية على شكل عناصر ul/li متفرعة. اكتب شيفرة تُضيف من خلالها لكل عنصر <li> عدد عناصره الوليدة مع إهمال الأوراق (العُقد التي ليس لها عناصر وليدة)، والنتيجة تكون على الشكل التالي:

005showDescendants.png

الحل

لإضافة نصٍ لكلّ عنصر من العناصر <li> يمكن التعديل على العُقدة النصية data.

يمكنك الإطلاع على الحل من هنا


إنشاء يومية

الأهمية:4

اكتب الدالّة createCalendar(elem, year, month)‎ التي تُنشئ يومية الشهر month من السنة year، وتسندها للعنصر elem. تكون اليومية على شكل جدول تُمثِّل فيه العناصر <tr> الأسابيع والعناصر <td> الأيام، وتكون خانات السطر الأول عبارة عن عناصر <th> تحوي أسماء أيام الأسبوع مُرتّبةً من الإثنين إلى الأحد. فعلى سبيل المثال، تؤدي مناداة الدالّة createCalendar(cal, 2012, 9)‎ إلى إنشاء اليومية التالية وإسنادها للعنصر cal.

006createCalendar.png

ملاحظة: يكفي في هذه المرحلة إظهار اليومية، لا نحتاج أن تكون تفاعلية.

يمكنك الإطلاع على شيفرة التمرين من هنا

الحل

نُنشئ الجدول على شكل سلسلة نصية "<table>....</table>" ثم نسندها إلى الخاصية innerHTML، باتباع خطوات الخوارزمية التالية:

  1. إنشاء خانات السطر الأول من الجدول (table header) باستعمال عناصر<th> تحوي أسماء أيام الأسبوع.
  2. إنشاء الكائنdate باستعمال الدالّة البانية d=newDate(year, month-1)‎ والذي يُمثِّل أول يوم في الشهر month (مع الأخذ في الحسبان أن ترتيب الأشهر في جافاسكربت يبدأ من 0 وليس من 1).
  3. ملء الخانات التي تسبق خانة اليوم الأول من الشهر (أي اليوم d.getDay()‎)، والتي تكون فارغة في اليومية، بإضافة عناصر <td></td> فارغة للجدول.
  4. الانتقال تصاعديا عبر الأيام باستعمال الدالّة d.setDate(d.getDate()+1)‎ وإضافة الخانات <td> لليومية مادامت قيمة d.getMonth()‎ تشير إلى الشهر نفسه (لم نصل بعد للشهر الموالي). وإذا كان اليوم يوم أحدٍ نضيف سطرًا جديدًا <tr></tr>.
  5. إذا وصلنا إلى نهاية الشهر، ومازالت في السطر خانات فارغة، نملؤها بعناصر <td></td> فارغة لنجعل الجدول مستطيلا.

يمكنك الإطلاع على الحل من هنا


إنشاء ساعة ملونة باستعمال الدالّة setInterval

الأهمية: 4

أنشئ ساعة ملونة تشبه الساعة في الشكل التالي:

007coloredClock.png

استعمل HTML/CSS للتنسيق، وجافاسكربت فقط لتحديث القيم الزمنية للعناصر.

الحل

فلنكتب أولا شيفرة التنسيق باستعمال HTML/CSS. نضع كلَّ مكون زمني في حاوية خاصة به (الساعات في حاوية، الدقائق في حاوية أخرى والثواني أيضا في حاوية) ونلونها باستعمال CSS.

<div id="clock">
  <span class="hour">hh</span>:<span class="min">mm</span>:<span class="sec">ss</span>
</div>

تحدِّث الدالّة update()‎ الوقت، وتنادي الدالّة setInterval الدالّة update()‎ كل ثانية لتحيين الوقت.

function update() {
  let clock = document.getElementById('clock');
  let date = new Date(); // (*)
  let hours = date.getHours();
  if (hours < 10) hours = '0' + hours;
  clock.children[0].innerHTML = hours;

  let minutes = date.getMinutes();
  if (minutes < 10) minutes = '0' + minutes;
  clock.children[1].innerHTML = minutes;

  let seconds = date.getSeconds();
  if (seconds < 10) seconds = '0' + seconds;
  clock.children[2].innerHTML = seconds;
}

نتحقّق في السطر الموسوم بالرمز (*) من الوقت آنيا لأن القيم التي تَنتُج من مناداة الدالّة setInterval غير موثوقة، حيث يمكن أن يكون فيها بعض التأخير. فيما يلي الدوالّ التي تُستعمل لإدارة الساعة:

let timerId;

function clockStart() { // run the clock
  timerId = setInterval(update, 1000);
  update(); // (*)
}

function clockStop() {
  clearInterval(timerId);
  timerId = null;
}

لاحظ أن الدالّة update()‎، بالإضافة لكونها مُبرمَجةً لتُنفَّذ دوريًا كلّ ثانية داخل الدالّة clockStart()‎، فهي تُنفَّذُ في السطر الموسوم بالرمز (*) في الحين ولا تنتظر حتى تنادي الدالّة setInterval الدالّة update()‎، وذلك لتفادي بقاء الساعة فارغةً طول مدّة تنفيذ الدالّةsetInterval (أي مدّة ثانية واحدة) عند مناداتها أول مرة.

يمكنك الإطلاع على الحل من هنا


إضافة شيفرة HTML إلى قائمة

_الأهمية: 5 _

اكتب الشيفرة التي تُضيف المقطع <li>2</li><li>3</li> بين العنصرين <li> فيما يلي:

<ul id="ul">
  <li id="one">1</li>
  <li id="two">4</li>
</ul>

الحل

تُعدّ الدالّة insertAdjacentHTML أحسن اختيار حين يتعلق الأمر بإضافة شيفرة HTML في موضع ما من الصفحة.

one.insertAdjacentHTML('afterend', '<li>2</li><li>3</li>');

فرز جدول ما

الأهمية: 5

لدينا الجدول التالي (قد يحوي عددًا أكبر من السطور):

<table>
<thead>
  <tr>
    <th>Name</th><th>Surname</th><th>Age</th>
  </tr>
</thead>
<tbody>
  <tr>
    <td>John</td><td>Smith</td><td>10</td>
  </tr>
  <tr>
    <td>Pete</td><td>Brown</td><td>15</td>
  </tr>
  <tr>
    <td>Ann</td><td>Lee</td><td>5</td>
  </tr>
  <tr>
    <td>...</td><td>...</td><td>...</td>
  </tr>
</tbody>
</table>

اكتب الشيفرة التي تعيدُ ترتيبه ترتيبًا أبجديًا حسب محتوى العمود name.

الحل

الحل عبارة عن بضعة سطور فقط، ولكننا نحتاج إلى شرح الحيل التي اُستعمِلت فيه. ويكون باتباع خطوات الخوارزمية التالية:

  1. استخراج سطور الجدول من العنصر tbody.
  2. ترتيبها حسب محتوى العمود الأول name ترتيبًا أبجديًا.
  3. إضافة العُقد حسب الترتيب الجديد باستعمال append(sortedRows)‎.

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

ملاحظة: العنصر tbody من الجدول مذكورٌ صراحةً في شيفرة HTML ولكن حتى وإن لم يُذكر صراحةً، فهو دائمًا موجود في بنية DOM.

يمكنك الإطلاع على الحل من هنا

ترجمة -وبتصرف- للفصل Modifying the document من كتاب Browser: Document, Events, Interfaces





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


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



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن