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

من اﻷمور التي ستحتاجها غالبًا عند كتابة شيفرة برمجية لصفحات وتطبيقات الويب هي التعامل مع مستندات الويب بطريقة أو بأخرى. وعادة ما يجري ذلك من خلال شجرة DOM وهي واجهة برمجية للتحكم بملف HTML وتنسيق المعلومات التي تستخدم الكائن Document بكثرة. سنفرد هذا المقال للحديث عن استخدام DOM بالتفصيل، إضافة إلى بعض الواجهات التي يمكنها تغيير بيئة العمل بطرائق مفيدة.

ننصحك قبل المتابعة في قراءة هذه المقالات أن:

اﻷجزاء المهمة في متصفح الويب

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

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

01 document window navigator

  • النافذة window: وهي الجزء الذي تُحمّل ضمنه صفحة الويب ويُمثّل في جافا سكريبت بالكائن Window. ونتمكن باستخدام التوابع التي يقدمها هذا الكائن من تنفيذ أشياء عديدة مثل الحصول على حجم النافذة (باستخدام خاصيات مثل Window.innerWidth و Window.innerHeight) أو التعامل مع المستند الذي يُحمّل ضمنها وتخزين بيانات متعلقة به في طرف العميل (مثل استخدام قاعدة بيانات محلية أو غيرها من آليات التخزين) أو ربط معالج أحداث بالنافذة الحالية وغيرها الكثير.
  • المستكشف navigator: ويمثّل حالة وهوية المتصفح (العميل الذي يستخدم المتصفح) عندما يتصفح الويب. يُمثَّل المستكشف في جافا سكريبت عن طريق الكائن Navigator الذي يُستخدم في الحصول على معلومات متعلقة بالمستخدم مثل اللغة المفضلة والتقاط بث كاميرا الويب الخاصة به وغيرها.
  • المستند document: تمثله شجرة DOM في المتصفح وهو في الواقع صفحة الويب التي تُحمّل ضمن النافذة. ويُمثَّل في جافا سكريبت من خلال الكائن Document. ويُستخدم هذا الكائن في التعامل مع عناصر HTML وأصناف CSS في المستند واستخلاص المعلومات منها أو تعديلها مثل الحصول على مرجع لأحد عناصر شجرة DOM وتغيير محتواه النصي وتنسيقه، كما يمكّنك من إنشاء عناصر جديدة وإضافتها إلى العنصر الحالي كعناصر أبناء، وكذلك حذفهم جميعًا.

ونركز في مقالنا بشكل أساسي على التعامل مع المستند، مع بعض اﻹضافات اﻷخرى.

شجرة DOM (نموذج كائن المستند)

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

نعرض تاليًا مثالًا بسيطًا لتوضيح اﻷمر (يمكنك تجربته مباشرة أيضًا). لهذا جرّب أن تفتح الملف في متصفحك وسترى صفحة بسيطة جدًا تضم العنصر <section> وضمنه صورة وفقرة نصية تضم رابطًا تشعبيًا.

<!doctype html>
<html lang="en-US">
 <head>
    <meta charset="utf-8" />
    <title>Simple DOM example</title>
 </head>
 <body>
    <section>
     <img
      src="dinosaur.png"
      alt="A red Tyrannosaurus Rex: A two legged dinosaur standing upright like a human,  with small arms, and a large head with lots of sharp teeth." />
     <p>
      Here we will add a link to the
      <a href="https://www.mozilla.org/">Mozilla homepage</a>
     </p>
    </section>
 </body>
</html>

تبدو شجرة DOM كالتالي:

02 dom screenshot

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

يُشار إلى العقد أيضًا بموقعها ضمن الشجرة نسبةً إلى عقد أخرى:

  • عقدة جذرية root node: وهي أعلى عقدة في الشجرة وهي دائمًا العقدة HTML في ملفات HTML (وتختلف من لغة تأشير إلى أخرى).
  • عقدة ابن child node: وهي عقدة تقع مباشرة ضمن عقدة أخرى، مثل العنصر IMG داخل العنصر SECTION في المثال السابق.
  • عقدة سليلة Descendant node: وهي عقد تقع في أي مكان داخل عقدة أخرى، مثل العنصر IMG داخل العنصر SECTION في المثال السابق فهي أيضًا عقدة سليلة. وهي ليست عقدة ابن للعنصر BODY لأنها تبعد مستويين عنه لكنها عقدة سليلة له.
  • عقدة أم Parent node: وهي عقدة تحتوي على عقد أخرى ضمنها، مثل العقدة BODY التي تمثل عقدة أم للعنصر SECTION.
  • عقد شقيقة Sibling nodes: وهي عقد تقع في نفس المستوى من شجرة DOM مثل العنصرين IMG و P.

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

تطبيق عملي: أساسيات العمل مع شجرة DOM

لنبدأ العمل مع شجرة DOM من خلال التطبيق العملي التالي:

  1. أنشئ نسخة محلية من صفحة التطبيق والصورة التي ترافقها.
  2. أضف العنصر <script></script> إلى ملف الشيفرة بعد الوسم <body/> مباشرة.
  3. ولكي تتعامل مع عنصر ضمن شجرة DOM عليك تخزين مرجع إليه ضمن متغير، لهذا أضف الشيفرة التالية ضمن العنصر <script>:
const link = document.querySelector("a");

وهكذا سيكون بإمكانك اﻵن التعامل مع العنصر الذي تخزن مرجعًا إليه باستخدام الخاصيات والتوابع المتاحة لهذا العنصر (تُعرَّف هذه الخاصيات والتوابع ضمن واجهات خاصة نذكر منها HTMLAnchorElement التي تتعامل مع الرابط التشعبي <a> وكذلك الواجهة اﻷم HTMLElement والواجهة Node التي تمثل جميع عقد الشجرة).

سنبدأ العمل بتغيير النص ضمن الرابط التشعبي بتحديث قيم الخاصية Node.textContent، لهذا أضف هذا السطر تحت السطر السابق:

link.textContent = "Mozilla Developer Network";

ولا بد أيضًا من تغيير عنوان URL الذي يشير إليه الرابط التشعبي كي لا ينقل المستخدم إلى وجهة خاطئة عند النقر عليه، لهذا نضيف السطر التالي تحت اﻷسطر السابقة:

link.href = "https://developer.mozilla.org";

وتجدر اﻹشارة إلى وجود عدة طرق لاختيار العنصر الذي نريد التعامل معه في جافا سكريبت، ويُعد التابع ()Document.querySelector مقاربة حديثة ننصح بها لأنها تسمح لك باختيار العنصر باستخدام محددات تنسيق CSS. إذ يبحث التابع السابق عند استدعائه في شيفرتنا عن أول عنصر <a> في المستند، وإن أردت البحث عن المزيد العناصر كي تتعامل معها دفعة واحدة، تستطيع استخدام التابع ()Document.querySelectorAll الذي يبحث عن كل عنصر في الصفحة له نفس مُحدد CSS الذي نستهدفه ومن ثم يخزّن مراجعًا إلى هذه العناصر ضمن كائن شبيه بالمصفوفة يُدعى قائمة عقد NodeList.

وتصادف أيضًا طرقًا أخرى للحصول على مراجع للعناصر ضمن الشجرة منها:

  • ()Document.getElementByID: يبحث هذا التابع عن عنصر ذو قيمة محددة للسمة id. فلو أردنا الوصول إلى العنصر <p id="myId">My paragraph</p>، نمرر قيمة هذه السمة إلى التابع كالتالي:
const elementRef = document.getElementById('myId');
  • ()Document.getElementsByTagName: تعيد كائنًا يشبه المصفوفة يضم كل العناصر من النوع نفسه مثل عناصر الفقرات النصية <p> أو الروابط التشعبية <a> وغيرها. يُمرر نوع العنصر إلى التابع كما في المثال التالي:
const elementRefArray = document.getElementsByTagName('p');

يعمل هذان التابعان بشكل أفضل مع المتصفحات القديمة، لكنهما أقل ملائمة مقارنة بالتابع ()Document.querySelector.

إنشاء عقد جديدة ووضعها ضمن شجرة DOM

رأيت مما سبق ما يمكن إنجازه، لكننا سنتقدم الآن خطوة للأمام لإلقاء نظرة على كيفية إنشاء عناصر جديدة.

  1. بالعودة إلى الشيفرة السابقة سنحاول إنشاء مرجع إلى العنصر <section>، لهذا ضع الشيفرة التالية في نهاية السكريبت (وهذا ما سنفعله مع اﻷسطر التي نضيفها تاليًا):
const sect = document.querySelector("section");
  1. لننشئ الآن فقرة نصية جديدة باستخدام ()DocumentCreateElement ونضع فيها بعض العبارات وفق نفس اﻷسلوب الذي اتبعناه سابقًا:
const para = document.createElement("p");
para.textContent = "We hope you enjoyed the ride.";
  1. نُلحق الفقرة النصية الجديدة بنهاية العنصر <section> من خلال التابع ()Node.appendChild:
sect.appendChild(para);
  1. لنُضف أخيرًا عقدة نصية text node إلى الفقرة النصية التي تضم الرابط التشعبي، لهذا ننشئ أولًا العقدة النصية باستخدام التابع ()Document.createTextNode:
const text = document.createTextNode(
 " — the premier source for web development knowledge.",
);

ثم ننشئ مرجعًا إلى الفقرة النصية ونلحق بها العقدة النصية:

const linkPara = document.querySelector("p");
linkPara.appendChild(text);

هذا كل ما تحتاجه غالبًا ﻹضافة عقد إلى شجرة DOM وستستخدم التوابع السابقة كثيرًا عند بناء واجهة ديناميكية.

نقل وإزالة عناصر

تحتاج أحيانًا إلى نقل عقدة أو حذفها وهذا أمر ممكن، فإن أردت نقل الفقرة النصية التي تضم الرابط التشعبي إلى نهاية العنصر <section>:

sect.appendChild(linkPara);

ولن تحدث بالطبع عملية نسخ للفقرة النصية إلى المكان الجديد لأن المتغير linkPara هو مرجع إلى النسخة الوحيدة فقط من هذه الفقرة. لكن إن أردت إنشاء نسخة عنها وإضافتها إلى المكان الذي تريد، فهذا أمر ممكن أيضًا من خلال التابع ()Node.cloneNode.

أما إزالة العقدة فهو أمر سهل وخاصة عندما يكون لديك مرجع إلى العقدة التي تريد إزالتها ومرجع إلى العقدة اﻷم لها، وعندها نستدعي التابع Node.removeChild:

sect.removeChild(linkPara);

باﻹمكان أيضًا إزالة عقدة بناء على مرجعها فقط، وهذا أمر شائع باستخدام التابع ()Element.remove:

linkPara.remove();

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

linkPara.parentNode.removeChild(linkPara);

أضف السطر السابق إلى شيفرتك.

العمل مع تنسيقات العناصر

بإمكاننا تعديل تنسيق CSS لعناصر الصفحة من خلال أكواد جافا سكريبت بطرق كثيرة. وكبداية بإمكانك الحصول على قائمة بكل ملفات التنسيق المرتبطة بالصفحة من خلال التابع Document.stylesheets الذي يعيد كائنًا مشابهًا للمصفوفات يضم عناصر من النوع CSSStyleSheet. وعندها ستتمكن من إضافة أو إزالة التنسيقات كما تشاء. ولن نتوسع في هذا اﻷسلوب لأنه قديم نوعًا ما ويصعب التعامل مع التنسيقات باستخدامه، وهنالك بالطبع طرق أسهل.

تقتضي الطريقة اﻷولى إضافة تنسيقات سطرية inline style مباشرة ضمن العناصر التي تريد تعديل تنسيقها ديناميكيًا. يُنفَّذ الأمر من خلال الخاصية HTMLElemet.style التي تضم معلومات عن التنسيق السطري لكل عنصر في الصفحة. وعندما تغيير قيمة هذه الخاصية سيتغير تنسيق العنصر مباشرة.

جرّب تغيير تنسيقات الفقرة النصية في شيفرتنا:

para.style.color = "white";
para.style.backgroundColor = "black";
para.style.padding = "10px";
para.style.width = "250px";
para.style.textAlign = "center";

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

<p style="color: white; background-color: black; padding: 10px; width: 250px; text-align: center;">
 We hope you enjoyed the ride.
</p>

ملاحظة: لاحظ كيف كُتبت نسخ جافا سكريبت من تنسيقات CSS بأسلوب سنام الجمل camelCase (مثلًا backgroundColor) مقارنة مع أسلوب CSS اﻷصلي وهو أسلوب الكباب lower-kebab-case (مثلًا background-color). احرص ألا تختلط عليك اﻷمور.

وهنالك أيضًا طريقة شائعة أخرى للتعامل مع تنسيق العناصر دينياميكيًا، سنلقي عليها نظرة اﻵن:

  1. احذف اﻷسطر الخمسة اﻷخيرة التي أضفتها.
  2. أضف ما يلي ضمن ترويسة الصفحة <head>:
<style>
 .highlight {
    color: white;
    background-color: black;
    padding: 10px;
    width: 250px;
    text-align: center;
 }
</style>

نستخدم التابع ()Element.setAttribute للتعامل مع عناصر HTML عمومًا، ويأخذ هذا التابع وسيطان أولهما الخاصية التي تريد ضبطها والثاني قيمة هذه الخاصية. وفي حالتنا ستكون الخاصية هي صنف الفقرة النصية class وقيمتها هي محدد CSS الذي نريد إسناده إلى الفقرة النصية لتنسيقها:

para.setAttribute("class", "highlight");

حدّث صفحتك ولن ترى أية تغيير في شيفرة HTML للفقرة النصية لأن التنسيق قد طّبق عليها بمنحها صنف تنسيق وليس بإضافة تنسيق سطري إليها.

إن اعتماد أسلوب معين هي حرية شخصية، ولكل اﻷساليب إيجابياتها وسلبياتها. فاﻷسلوب الأول بسيط ولا يتطلب إعدادات خاصة وهو جيد في بعض الحالات، بينما يعزز اﻷسلوب الثاني مبدأ فصل شيفرة التنسيق CSS عن شيفرة الصفحة HTML والذي يعُدّ ممارسة جيدة). وقد تميل مع تقدمك في مسيرة بناء تطبيقات أضخم وأعقد إلى استخدام اﻷسلوب الثاني أكثر، لكن في النهاية اﻷمر كما ذكرنا يعود إليك.

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

ملاحظة: يمكنك إلقاء نظرة على التمرين التطبيقي المنتهي) (أو تجربته مباشرة أيضًا).

تطبيق عملي: قائمة تسوّق ديناميكية

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

  • أن يُعرض العنصر الجديد ضمن القائمة.
  • أن يجاور كل عنصر زر لتتمكن من حذف هذا العنصر عند النقر على الزر.
  • أن يُفرّغ مربع اﻹدخال ويكتسب تركيز الدخل بعد إضافة عنصر إلى القائمة استعدادًا ﻹدخال عنصر آخر.

قد يبدو الشكل النهائي للتطبيق كالتالي:

03 shopping ist

وﻹكمال التمرين، اتبع الخطوات التالية وتأكد أن سلوك القائمة سيكون مطابقًا لما أشرنا إليه:

  1. نزّل نسخة عن ملف HTML الخاص بالتطبيق، وضعها في مكان مناسب. وسترى أن الملف يضم تنسيقًا بسيطًا وعنصر <div> وعنوان وعنصر إدخال <input> وزر وقائمة فارغة وعنصر <script> تضع ضمنه كل الشيفرة التي تحتاجها.
  2. أنشئ ثلاثة متغيرات لتحمل مراجع إلى القائمة <ul> والزرين <button>.
  3. أنشئ دالة تعمل عند النقر على الزر.
  4. خزّن ضمن الدالة القيمة الحالية لعنصر اﻹدخال ضمن متغير.
  5. فرّغ عنصر اﻹدخال من المحتوى بإسناد القيمة '' له.
  6. أنشئ ثلاثة عناصر جديدة هي عنصر قائمة <li> وعنصر <span> وعنصر <button>.
  7. ألحق الزر والعنصر <span> كابنين لعنصر القائمة.
  8. اجعل المحتوى النصي للعنصر <span> مطابقًا لقيمة عنصر اﻹدخال واجعل العبارة 'Delete' المحتوى النصي للزر.
  9. ألحق عنصر القائمة بالقائمة كعنصر ابن.
  10. اربط معالج حدث بزر الحذف كي يُحذف عنصر القائمة بأكمله (<li>...</li>) عند النقر على الزر.
  11. استخدم أخيرًا التابع ()focus لنقل تركيز الدخل إلى عنصر اﻹدخال كي يكون جاهزًا ﻹدخال العنصر التالي في القائمة.

ملاحظة: يمكنك إلقاء نظرة على التمرين التطبيقي المنتهي (أو تجربته مباشرة أيضًا).

الخلاصة

وصلنا اﻵن إلى نهاية مقالنا عن استخدام DOM وتعديلها من خلال جافا سكريبت، ومن المفترض في هذه المرحلة أن تكون قد أدركت اﻷجزاء المهمة من متصفح ويب فيما يتعلق بالتحكم بالمستندات وغيرها من النواحي التي تؤثر على تجربة مستخدم الويب. ومن المهم اﻷكثر أن تستوعب ماهية شجرة DOM وكيفية التعامل معها لتقديم ميزات مفيدة لتطبيقك.

ترجمة -وبتصرف- للمقال: Manipulating documents


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

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

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



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

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

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

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   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.


×
×
  • أضف...