عائشة ميرا

الأعضاء
  • المساهمات

    6
  • تاريخ الانضمام

  • تاريخ آخر زيارة

السُّمعة بالموقع

1 Neutral

1 متابع

  1. يتطلّب تحريك العناصر داخل الصفحة معرفة إحداثياتها. وتعمل معظم الدوالّ في لغة جافاسكربت وفق أحد نظامي الإحداثيات التاليين: إحداثيات بالنسبة للنافذة: تشبه الموضع الثابت (position:fixed)، حيث تُحسب الإحداثيات إنطلاقًا من الركن العلوي الأيسر للنافذة. نسمي هذه الإحداثيات clientX وclientY، وسنوضح السبب من وراء ذلك فيما بعد عند التطرق لخاصيات الأحداث (event properties). إحداثيات بالنسبة للصفحة: تشبه الموضع المطلق (position:absolute)، حيث تُحسب الإحداثيات إنطلاقًا من الركن العلوي الأيسر للصفحة. نسمي هذه الإحداثيات pageX وpageY. عندما يكون أعلى الصفحة ظاهرًا في النافذة، يكون الركن العلوي الأيسر للصفحة متطابقا مع الركن العلوي الأيسر للنافذة، وتكون الإحداثيات أيضًا متساوية. ولكن بعد تحريك الصفحة تتغيّر إحداثيات العناصر بالنسبة للنافذة كلما تحركت العناصر ضمن النافذة. أما الإحداثيات بالنسبة للصفحة فلا تتغيّر، بل تبقى دائمًا ثابتة. نأخذ في هذه الصورة نقطة من الصفحة لإظهار إحداثياتها قبل التمرير (الصورة على اليسار) وبعد التمرير (الصورة على اليمين). عند تمرير الصفحة: بقيت الترتيبة pageY بالنسبة للصفحة على حالها ولم تتغير، حيث تُحسب إنطلاقًا من أعلى الصفحة (الذي لم يعد ظاهرًا في النافذة بعد التحريك). تغيّرت الترتيبة clientY بالنسبة للنافذة (أصبح السهم أقصر) لأن النقطة نفسها أصبحت أقرب لأعلى النافذة. إحداثيات عنصر ما: getBoundingClientRect تعيد الدالّة elem.getBoundingClientRect() الإحداثيات (بالنسبة للنافذة) الخاصة بأصغر مستطيل يكون العنصر elem محصورًا فيه بصفته كائنا من الصنف المدمج DOMRect أهم خاصيات الصنف DOMRect: x وy: إحداثيات نقطة بداية المستطيل بالنسبة للنافذة (X وY). width وheight: عرض وطول المستطيل (يمكن أن تكون القيم سالبة). كما يملك الصنف DOMRect خاصيات مشتقة: top وbottom: ترتيبتي ركني المستطيل العلوي والسفلي. left وright: فاصلتي ركني المستطيل الأيسر والأيمن. فعلى سبيل المثال، عند استعمال الدالّةbutton.getBoundingClientRect()‎للحصول على إحداثيات زرٍ ما مع تمرير الصفحة نزولًا وصعودًا، ستلاحظ أنه كلما تغيّر موضع الزر بالنسبة للنافذة تغيّرت إحداثياته بالنسبة للنافذة (yوtopوbottom` إذا كان التمرير عموديًا). إليك صورة تمثيلية تعرض مخرجات (output) الدالّة elem.getBoundingClientRect()‎: لاحظ أن الخاصيات x وy وwidth وheight تصف المستطيل وصفًا وافيًا، حيث يمكن حساب بقية الخاصيات المشتقة إنطلاقًا منها: left = x top = y right = x + width bottom = y + height لاحظ هنا أنه: يمكن للإحداثيات أن تكون أعدادًا عشريةً مثل العدد 10.5. يُعد هذا الأمر عاديًا، حيث يستعمل المتصفح في عمليات الحساب عمليات القسمة، كما أنه ليس علينا تقريب هذه القيم (to round) عند إسنادها للخاصيتين style.left وstyle.top. يمكن أن تكون قيم الإحداثيات سالبة. في حالة تمرير الصفحة مثلا بحيث يكون العنصر elem تمامًا فوق النافذة، تكون قيمة الخاصية elem.getBoundingClientRect().top سالبة. ملاحظة: لماذا نحتاج إلى الخاصيات المشتقة؟ وما الهدف من وجود الخاصيتين left وtop في وجود الإحداثيات x وy؟ يُعرّف المستطيل في الرياضيات بإحداثيات نقطة البداية (x,y) وشعاع الاتجاه (width,height)، وبذلك تُستخدَم الخاصيات المشتقة لتسهيل الأمر فقط. من ناحية تقنية، يمكن أن تكون قيم الخاصيتينwidth وheight سالبة، فهي تسمح بوصف المستطيلات "الموجّهة" وصفًا دقيقًا بما فيها نقطتي البداية والنهاية، كالمستطيلات المتكوّنة عند النقر على الفأرة وسحبها مسافة معيّنة. وتعني القيم السالبة للخاصيتين width وheight أن المستطيل يبدأ من الركن الأيمن السفلي ليتشكّل عن طريق سحب الفأرة في الاتجاه الأيسر نحو الأعلى. وتمثل الصورة الموالية مستطيلًا بقيم سالبة لكل من الخاصيتين width وheight (مثال: width=-200, height=-100) وكما ترى، لا تتطابق قيم الخاصيتين left وtop مع قيم الخاصيتين x وy في هذه الحالة. ولكن عمليًا، تُعيد دائمًا الدالّة elem.getBoundingClientRect()‎ قيمًا موجبةً للعرض والطول. غير أن السبب الذي حملنا على ذكر القيم السالبة هنا، هو أن هذه القيم التي تبدو لك مكرّرةً، ليست مكرّرةً في الحقيقة. ملاحظة: لا يدعم المتصفح Internet Explorer الإحداثيات x وy لا يدعم المتصفح Internet Explorer خاصيتي الإحداثيات x وy لاسباب تاريخية. يمكنك إذًا إمّا استعمال مكتبة بديلة (polyfill) (إضافة جالبات الخاصيات (getters) للخاصية DomRect.prototype) أو استعمال الخاصيتين left وtop فقط بما أنهما دائمًا متطابقتين مع الإحداثيات x وy في الحالات التي تكون فيها قيم الخاصيتين width وheight موجبة، وخاصة في القيم التي تعيدها الدالّة elem.getBoundingClientRect()‎. ملاحظة: تختلف الإحداثيات right وbottom عن خاصيات التموضع المعتمدة في شيفرة CSS هناك أوجه تشابه جليّة بين الإحداثيات بالنسبة للنافذة، والموضع (position:fixed) المعتمد في شيفرة CSS. ولكن إذا تحدثنا عن التموضع في شيفرة CSS، تمثّل الخاصية right المسافة إنطلاقًا من الركن الأيمن، وتمثّل الخاصية bottom المسافة إنطلاقًا من الركن السفلي. أمّا إذا تمعّنا في الصورة أعلاه، نُدرك أن الأمر ليس كذلك في لغة جافاسكربت، حيث تُحسب كافة الإحداثيات بالنسبة للنافذة إنطلاقًا من الركن العلوي الأيسر، بما فيها إحداثيات الخاصيتين المذكورتين. elementFromPoint(x, y)‎ تعيد الدالّة document.elementFromPoint(x, y)‎ العنصر الأكثر تداخلًا مع إحداثيات النافذة (x, y)، وصيغتها كالآتي: let elem = document.elementFromPoint(x, y); فعلى سبيل المثال تُلقي هذه الشيفرة الضوء أو تُبرِز وسم العنصر المتموضع وسط النافذة أثناء معاينة الشيفرة: let centerX = document.documentElement.clientWidth / 2; let centerY = document.documentElement.clientHeight / 2; let elem = document.elementFromPoint(centerX, centerY); elem.style.background = "red"; alert(elem.tagName); See the Pen JS-p2-11-Coordinates-ex1 by Hsoub (@Hsoub) on CodePen. وبما أن الشيفرة تستعمل الإحداثيات بالنسبة للنافذة، يختلف العنصر الذي تُبرزه الشيفرة كلّما جرى تمرير للصفحة. ملاحظة: تعيد الدالّةelementFromPoint القيمة null إذا مُرِّرَت لها إحداثيات خارجة عن نطاق النافذة تعمل الدالّة document.elementFromPoint(x,y)‎ فقط إذا كانت الإحداثيات (x,y) المُمَرّرة لها داخل نطاق الجزء من الصفحة الظاهر في النافذة. وإذا كانت إحدى الإحداثيات سالبةً أو خارج نطاق طول أوعرض النافذة، تُعيد الدالّة القيمة null. وإليك نوع الخطأ الذي ينجر عن ذلك: let elem = document.elementFromPoint(x, y); //null القيمة elementFromPoint إذا كانت الإحداثيات خارج نطاق النافذة تعيد الدالّة (elem = null) elem.style.background = ''; // Error! See the Pen JS-p2-11-Coordinates-ex2 by Hsoub (@Hsoub) on CodePen. استعمال الإحداثيات للتموضع الثابت (fixed) غالبا ما نحتاج للإحداثيات من أجل إضافة عنصرٍ ما في موضعٍ ما. ولإظهار عنصرٍ بمحاذاة عنصرٍ آخر، يمكننا استعمال الدالّة getBoundingClientRect للحصول على إحداثياته، ثم الاستعانة بالخاصية position مع الخاصيتين left وtop (أو right وbottom). فعلى سبيل المثال، تُظهر الدالّة createMessageUnder(elem, html)‎ في الشيفرة الموالية رسالةً تحت العنصر elem: let elem = document.getElementById("coords-show-mark"); function createMessageUnder(elem, html) { // إنشاء العنصر الذي يضم الرسالة let message = document.createElement('div'); //يستحسن استعمال صنف CSS لتحديد النمط التنسيقي message.style.cssText = "position:fixed; color: red"; //إسناد الإحداثيات دون أن ننسى الحرفين 'px' let coords = elem.getBoundingClientRect(); message.style.left = coords.left + "px"; message.style.top = coords.bottom + "px"; message.innerHTML = html; return message; } // :الاستعمال // إضافته للصفحة لمدّة خمس ثوان let message = createMessageUnder(elem, 'Hello, world!'); document.body.append(message); setTimeout(() => message.remove(), 5000); See the Pen JS-p2-11-Coordinates-ex3 by Hsoub (@Hsoub) on CodePen. عند الضغط على الزر صاحب الخاصية id="coords-show-mark"‎ تظهر الرسالة تحته. يمكن التعديل على الشيفرة لإظهار الرسالة على اليسار، أو على اليمين، أو في الأسفل، أو تطبيق شيفرة حركات CSS (أي CSS animation) عليه لجعله يختفي تدريجيا، وهكذا دواليك. سيكون ذلك سهلًا طالما نملك كافة إحداثيات ومقاسات العنصر. لاحظ هذا التفصيل المهم: في حالة ما إذا قمنا بتمرير الصفحة تبتعد الرسالة عن الزر. والسبب واضح: موضع الرسالة ثابت (position:fixed)، لذا تبقى في نفس الموضع من النافذة عند تمرير الصفحة. ولتغيير ذلك، علينا استعمال الإحداثيات بالنسبة للصفحة، والموضع المطلق (position:absolute). إحداثيات الصفحة يبدأ احتساب الإحداثيات بالنسبة للصفحة من الركن العلوي الأيسر للصفحة، لا النافذة. ويقابِل الإحداثيات بالنسبة للنافذة في لغة CSS الموضع position:fixed، أما الإحداثيات بالنسبة للصفحة فيقابلها في لغة CSS الموضع position:absolute من الأعلى. يمكننا استعمال الموضع position:absolute والخاصيتين left وtop لوضع عنصرٍ ما في موضعٍ ما من الصفحة ، بحيث يبقى في نفس الموضع عند تمرير الصفحة. ولكن علينا أولا إيجاد الإحداثيات الصحيحة. لا يوجد أيّ دالّة في المعايير تسمح بالحصول على إحداثيات عنصرٍ ما بالنسبة للصفحة، ولكن يمكننا كتابتها. يعد نظامي الإحداثيات مرتبطين ببعضهما البعض من خلال المعادلة التالية: pageY=clientY+ طول الجزء العمودي من الصفحة غير الظاهر في النافذة. pageX=clientX+ عرض الجزء الأفقي من الصفحة غير الظاهر في النافذة. تأخذ الدالّة getCoords(elem)‎ الإحداثيات بالنسبة للنافذة باستخدام الدالّة elem.getBoundingClientRect()‎ وتضيف مسافة التمرير لها. // الحصول على إحداثيات العنصر بالنسبة للصفحة function getCoords(elem) { let box = elem.getBoundingClientRect(); return { top: box.top + window.pageYOffset, right: box.right + window.pageXOffset, bottom: box.bottom + window.pageYOffset, left: box.left + window.pageXOffset }; } في المثال أعلاه، استعملنا الموضع position:absolute، وبذلك تبقى الرسالة بمحاذاة العنصر عند تمرير الصفحة وتصبح الدالّة createMessageUnder بعد التعديل كالآتي: function createMessageUnder(elem, html) { let message = document.createElement('div'); message.style.cssText = "position:absolute; color: red"; let coords = getCoords(elem); message.style.left = coords.left + "px"; message.style.top = coords.bottom + "px"; message.innerHTML = html; return message; } الخلاصة لكلّ نقطة من الصفحة إحداثيات: بالنسبة للنافذة: تُساوي elem.getBoundingClientRect()‎ بالنسبة للصفحة: تُساوي elem.getBoundingClientRect()‎ + مقدار تمرير الصفحة. يُنصح باستخدام الإحداثيات بالنسبة للنافذة مع الموضع position:fixed، والإحداثيات بالنسبة للصفحة مع الموضع position:absolute. ولِكِلَا نظامي الإحداثيات إيجابيات وسلبيات، حيث يُستحسن استعمال أحدهما في حالات معيّنة، والآخر في حالات أخرى، كما هو الحال بالنسبة للمواضع absolute وfixed في لغة CSS. تمارين إيجاد إحداثيات المستطيل بالنسبة للنافذة درجة الأهمية:5 لدينا في الصورة الموالية صفحة ويب بها مستطيل أخضر “field”. استعمل لغة جافاسكربت للحصول على إحداثيات الأركان المؤشر عليها بالأسهم الحمراء بالنسبة للنافذة. أضيفت للصفحة إمكانية الحصول على إحداثيات أيّ نقطة منها بمجرد النقر عليها لتسهيل الأمر عليك. يمكنك الاطلاع على شيفرة التمرين عبر هذا الرابط عليك استعمال نموذج تمثيل الصفحة ككائن DOM للحصول على الإحداثيات بالنسبة للنافذة لكل من: الركن الخارجي العلوي الأيسر (وهو سهل). الركن الخارجي السفلي الأيمن (وهو سهل أيضا). الركن الداخلي العلوي الأيسر (صعب بعض الشيء). الركن الداخلي السفلي الأيمن ( هناك عدة طرائق للحصول عليه. اختر واحدة منها) ينبغي أن تكون الإحداثيات التي تحسبها الشيفرة متطابقةً مع الإحداثيات التي تظهر عند النقر على الركن بالفأرة. ينبغي أيضًا أن تعمل الشيفرة مهما كانت قيم المقاسات والأطر، ليس فقط من أجل القيم المعطاة في نص التمرين. الحل الركنان الخارجيان تكون إحداثيات الركنين الخارجيين عادةً هي الإحداثيات التي تعيدها الدالّة elem.getBoundingClientRect()‎. في الشيفرة التالية، تمثّل answer1 إحداثيات الركن العلوي الأيسر، وتمثّل answer2 إحداثيات الركن السفلي الأيمن. let coords = elem.getBoundingClientRect(); let answer1 = [coords.left, coords.top]; let answer2 = [coords.right, coords.bottom]; الركن الداخلي العلوي الأيسر الفرق بينه وبين الركن الخارجي هو عرض الإطار. والطريقة المثلى للحصول على هذه المسافة هي باستعمال الخاصيتين clientLeft وclientTop. let answer3 = [coords.left + field.clientLeft, coords.top + field.clientTop]; الركن الداخلي السفلي الأيمن علينا في هذه الحالة طرح عرض الإطار من الإحداثيات الخارجية. ويمكننا استعمال لغة CSS كالآتي: let answer4 = [ coords.right - parseInt(getComputedStyle(field).borderRightWidth), coords.bottom - parseInt(getComputedStyle(field).borderBottomWidth) ]; هناك أيضًا طريقةٌ بديلةٌ، وربما هي أحسن، تكون بإضافة قيم الخاصيتين clientWidth وclientHeight لإحداثيات الركن العلوي الأيسر. let answer4 = [ coords.left + elem.clientLeft + elem.clientWidth, coords.top + elem.clientTop + elem.clientHeight ]; يمكنك الاطلاع على شيفرة الحل عبر هذا الرابط إظهار ملاحظة بمحاذاة عنصرٍ ما درجة الأهمية: 5 أنشئ الدالّة positionAt(anchor, position, elem)‎ التي تضع العنصرelem في موضعٍ position قريبٍ من العنصرanchor. يكون الموضع position عبارة عن سلسلة نصية ويأخذ إحدى القيم التالية: "top": تضع العنصر elem فوق العنصرanchor مباشرة. "right": تضع العنصرelem على يمين العنصرanchor مباشرة. "bottom": تضع العنصرelem أسفل العنصر anchor مباشرة. تُستعمل هذه الدالّة بداخل الدالّة showNote(anchor, position, html)‎ (الشيفرة الخاصة بالدالّة معطاة في نص التمرين) التي تُنشئ عنصرًا يحتوي على ملاحظة من خلال الشيفرة html المُمَرّرة لها، وتظهر هذه الملاحظة في الموضع position المُمَرر لها بمحاذاة العنصر anchor، وهذه صورة تمثيلية للنتيجة التي نريد الوصول اليها: يمكنك الاطلاع على شيفرة التمرين عبر هذا الرابط. الحل في هذا التمرين نحتاج فقط حسابَ الإحداثيات بشكلٍ دقيقٍ. ولتفاصيل أكثر، اطلع على شيفرة الحل. لاحظ أن العناصر ينبغي أن تكون موجودةً في الصفحة حتى يتسنى لنا قراءة الخاصية offsetHeight وخاصيات أخرى. حيث أن العنصر المخفي (display:none) أو غير الموجود بالصفحة لا يملك مقاسات. يمكنك الاطلاع على الحل عبر هذا الرابط. إظهار ملاحظة بمحاذاة عنصرٍ ما (مطلق) درجة الأهمية: 5 عدِّل على حل التمرين السابق بحيث تستعمل الملاحظة موضعًا مطلقًا (position:absolute) بدلًا من الموضع الثابت (position:fixed). هذا ما يُبقِي الملاحظة بمحاذاة العنصر عند تمرير الصفحة. استعمل حل التمرين السابق كنقطة انطلاق، ولمعاينة التمرين أضف النمط التنسيقي التالي: <body style="height: 2000px"‎> الحل الحل بسيطٌ جدًا. غيّر الخاصية position الخاصة بالمحدِّد .note من position:fixed إلىposition:absolute في شيفرة CSS. استعمل الدالّة getCoords()‎ المعرّفة أعلاه للحصول على الإحداثيات بالنسبة للصفحة. يمكنك الإطلاع على الحل عبر هذا الرابط. وضع الملاحظة داخل العنصر (مطلق) درجة الأهمية: 5 استعمل التمرين السابق (إظهار ملاحظة بمحاذاة عنصرٍ ما [مطلق]) وعدّل على الدالّة positionAt(anchor, position, elem)‎ لتسمح بإضافة عنصر elem داخل العنصرanchor. والقيم الجديدة للموضع position هي: bottom-out ،right-out ،top-out: هي نفس المواضع المعرّفة في الحل الأول والتي تسمح بإضافة العنصرelem فوق أو على يمين أو تحت العنصر anchor. bottom-in، right-in، top-in: تسمح بإضافة العنصر elem داخل العنصر anchor بحيث يلتصق بالركن العلوي أو الأيمن أو السفلي. على سبيل المثال: // تظهر الملاحظة فوق إطار المقولة positionAt(blockquote, "top-out", note); // تظهر الملاحظة داخل إطار المقولة في الأعلى positionAt(blockquote, "top-in", note); والنتيجة تكون كالآتي: خذ شيفرة حل التمرين السابق (إظهار ملاحظة بمحاذاة عنصرٍ ما [مطلق]) واستعن بها كشيفرة نص لهذا التمرين. الحل يمكنك الاطلاع على الحل عبر هذا الرابط. ترجمة -وبتصرف- للفصل Coordinates من كتاب Browser: Document, Events, Interfaces
  2. هل تتساءل عن كيفية الحصول على طول وعرض نافذة المتصفح؟ وكيف لك أن تعرف الطول والعرض الكلي للصفحة بما فيه الجزء الذي لا يُؤشِر عليه شريط التمرير (الجزء من الصفحة غير الظاهر في النافذة)؟ وكيف يمكن تمرير محتوى الصفحة باستعمال لغة جافاسكربت؟ فيما يلي الأجوبة الشافية لكل هذه التساؤلات. يمكن، في أغلب هذه الحالات، استعمال العنصر الجذر للصفحة document.documentElement الذي يقابِل الوسم <html>، بالإضافة إلى بعض الدوالّ والخاصيات التي يُستحسن أخذها في الحسبان نظرًا لأهميتها. طول وعرض النافذة تُستخدم الخاصيتان clientHeight وclientWidth لمعرفة طول وعرض النافذة. حيث تُظهِرالتعليمة، في المثال التالي، طول نافذة المتصفح: alert( document.documentElement.clientHeight ); ملاحظة: لا تستعمل window.innerWidth وwindow.innerHeight تدعم المتصفحات الخاصيتين window.innerWidth وwindow.innerHeight، ويبدو أنهما تتوافقان مع ما نريده، فلماذا لا نستخدمهما بدلًا من الخاصيتين اللّتان ذكرناهما سابقا؟ في حالة وجود شريط تمرير في الصفحة، تعطي الخاصيتان clientHeight وclientWidth طول وعرض النافذة بعد طرح عرض شريط التمرير منه. أو بعبارة أخرى، تعطيان طول وعرض الجزء الظاهر من الصفحة والمخصص للمحتوى. وتعطي الخاصيتان window.innerWidth وwindow.innerHeight الطول والعرض الكليين للنافذة بما فيهما عرض شريط التمرير. في حالة ما إذا كان لدينا شريط تمرير يشغل مساحة ما من النافذة، تعطي التعليمتان التاليتان نتائج مختلفة: alert( window.innerWidth ); // العرض الكلي للنافذة alert( document.documentElement.clientWidth ); // عرض النافذة بعد طرح عرض شريط التمرير منه See the Pen JS-p2-10-Window sizes and scrolling-ex1 by Hsoub (@Hsoub) on CodePen. نحتاج، في أغلب الحالات، معرفة عرض النافذة المتاح (المخصص للمحتوى)، لرسم شيء ما أو وضعه، أي دون احتساب عرض شريط التمرير إن وُجد، لذا علينا استعمال الخاصيتين documentElement.clientHeight وdocumentElement.clientWidth. ملاحظة: التعليمة DOCTYPE مهمة لاحظ أن الخاصيات رفيعة المستوى المتعلقة بالهندسة يمكن أن لا تعمل بالشكل المطلوب إذا لم يحوي ملف HTML التعليمة <!DOCTYPE HTML> ، فمن الممكن أن تحدث أشياء غريبة بسبب ذلك. أضف دائمًا إذًا التعليمة DOCTYPE عند كتابة شيفرة HTML على الطريقة العصرية. طول وعرض الصفحة نظريا، وبما أن العنصر الجذر للصفحة هو document.documentElement، ويضُم كلّ المحتوى، يمكن قياس العرض والطول الكليين للصفحة باستعمال الخاصيتين document.documentElement.scrollWidth وdocument.documentElement.scrollHeight، ولكن هاتين الخاصيتين، عند استعمالهما على هذا العنصر، لا تعملان بالشكل المطلوب إن لم يكن هناك شريط تمرير في كلّ من المتصفحات Chrome/Safari/Opera، حيث يمكن أن تعطي الخاصية documentElement.scrollHeight قيمةً أصغر من القيمة التي تعطيها الخاصية documentElement.clientHeight. يبدو الأمر غريبا نوعا ما، أليس كذلك؟ للحصول إذًا على القيمة الصحيحة للطول الكلي للصفحة، علينا أخذ أكبر قيمة من بين القيم التالية: let scrollHeight = Math.max( document.body.scrollHeight, document.documentElement.scrollHeight, document.body.offsetHeight, document.documentElement.offsetHeight, document.body.clientHeight, document.documentElement.clientHeight ); alert('Full document height, with scrolled out part: ' + scrollHeight); See the Pen JS-p2-10-Window sizes and scrolling-ex2 by Hsoub (@Hsoub) on CodePen. لماذا يحدث ذلك؟ يُستحسن عدم طرح هذا السؤال لأن هذه التناقضات موجودة منذ زمن ولا تخضع لتفكير منطقي "ذكي". حساب إحداثيات الموضع الحالي لعنصر ما تملك عناصر تمثيل الصفحة ككائن DOM خاصيتين تحملان إحداثيات موضعها من الصفحة وهما: elem.scrollLeft وelem.scrollTop. فبالنسبة لموضِع النافذة، تعمل الخاصيتان document.documentElement.scrollLeft وdocument.documentElement.scrollTop في أغلب المتصفحات، عدا تلك التي تعتمد على محرك WebKit مثل المتصفح Safari (يوّلد استعمالها الخطأ البرمجي 5991)، حيث ينبغي في هذه الحالة استعمال document.body بدلا من document.documentElement. ومن حسن الحظ أنه ليس علينا على الإطلاق تذكُّر كلّ هذه الخاصيات، حيث توجد خاصيتان غيرهما تعطيان الموضع الحالي للنافدة، إنهما الخاصيتان window.pageXOffset وwindow.pageYOffset، وهما خاصيتان قابلتان فقط للقراءة. alert('Current scroll from the top: ' + window.pageYOffset); alert('Current scroll from the left: ' + window.pageXOffset); See the Pen JS-p2-10-Window sizes and scrolling-ex3 by Hsoub (@Hsoub) on CodePen. التمرير باستعمال الدوال scrollTo وscrollBy وscrollIntoView ملاحظة هامة لا يمكن تمرير الصفحة إلّا بعد البناء الكلي لنموذج تمثيل الصفحة ككائن (DOM)، فمن غير الممكن كتابة شيفرة لتمرير الصفحة ووضعها في الجزء <head> أي تنفيذها قبل أن تجهز الصفحة فلن يعمل ذلك حتما. ويمكن تمرير العناصر العادية بتغيير قيم الخاصيتين scrollTop وscrollLeft، كما يمكن عمل الشيء نفسه للصفحة باستعمال الخاصيتين document.documentElement.scrollLeft وdocument.documentElement.scrollTop، عدا في المتصفح Safari (حيث ينبغي استخدام الخاصيتين document.body.scrollLeft وdocument.body.scrollTop بدلا منها). وهناك أيضا طريقةٌ بديلةٌ، أبسط من الأولى، تعمل في كافة المتصفحات، وتكون باستعمال الدالّتين window.scrollBy(x,y)‎ وwindow.scrollTo(pageX,pageY)‎. تحرّك الدالّة scrollBy(x,y)‎ النافذة من مكانها الحالي بمقدار معين. حيث تُحرِك التعليمة window.scrollBy(0,10)‎ مثلا الصفحة إلى الأسفل بمقدار عشرة بكسل. وتحرِّك الدالّة scrollTo(pageX,pageY)‎ الصفحة إلى موضعٍ ذي إحداثياتٍ مطلقةٍ. بحيث تكون إحداثيات الركن العلوي الأيسر للجزء الظاهر من الصفحة هي (pageX, pageY) إنطلاقا من الركن العلوي الأيسر للصفحة. تعمل هذه الدالّة تماما كما لو أننا نُسنِد قِيَمًا للخاصيتين scrollLeft وscrollTop. مثلًا لتحريك النافذة إلى نقطة البداية (العودة إلى أعلى الصفحة)، يمكن استعمال التعليمة window.scrollTo(0,0)‎. وتعمل هذه الدوالّ بنفس الطريقة (تعطي النتائج نفسها) على كلّ المتصفحات. الدالة scrollIntoView دعنا نكمل ما بدأناه بشرح الدالّة elem.scrollIntoView(top)‎. ينتج عن مناداة هذه الدالّة تمرير الصفحة بحيث تجعل العنصر elem ظاهرًا، ولها وسيط واحد: إذا كانت قيمة الوسيط top تساوي true (وهي القيمة المبدئية -الافتراضية)، تُمرَّر الصفحة بحيث يظهر العنصر elem أعلى النافذة فتتطابق حافته العلوية مع أعلى النافذة. وإذا كانت قيمة الوسيط top تساوي false، تُمرَّر الصفحة بحيث يظهر العنصر elem أسفل النافذة فتتطابق حافته السفلى مع أسفل النافذة. مثال: إذا كان لدينا زر، فإن هذه التعليمة this.scrollIntoView()‎ تُحرِك الصفحة بحيث يظهر هذا الزر أعلى النافذة. أما هذه التعليمة this.scrollIntoView(false)‎، فتُحرِك الصفحة بحيث يظهر الزر أسفل النافذة. تثبيط قابلية التمرير نحتاج أحيانا إلى جعل الصفحة غير قابلة للتمرير، فنرغب مثلًا في إخفائها وراء رسالة طويلة للفت انتباه الزائر وحمله على التفاعل مع الرسالة وليس مع الصفحة. وهنا يكفي إسناد القيمة "hidden" للخاصية document.body.style.overflow حيث تثبِّت (تجمِّد) هذه التعليمة الصفحة في موضعها الحالي. فلتجرب ذلك باستعمال التعليمتين document.body.style.overflow="hidden" و document.body.style.overflow="". هنا، تثبِّط التعليمة الأولى قابلية التحريك، أما التعليمة الثانية فتعاود تفعيلها. ويمكننا استعمال هذه الطريقة لتجميد/تثبيت عناصر أخرى أيضا، ليس العنصر document.body فقط. غير أن لهذه الدالّة نقطة سلبية، حيث يختفي شريط التمرير تماما بعد مناداتها، وبما أنه كان يشغل مساحة ما من الصفحة، فعند اختفائه، يتحرك محتوى الصفحة بعض الشيء لملء الفراغ الذي نتج عن اختفاء شريط التمرير. يبدو هذا غريبا بعض الشئ، ولكن يمكننا تفادي حدوث ذلك بموازنة قيمة الخاصية clientWidth قبل وبعد التثبيط، فإذا ازداد العرض بعد اختفاء شريط التمرير، نضيف حاشية padding للعنصرdocument.body مكان شريط التمرير للإبقاء على عرض محتوى الصفحة كما هو. الخلاصة لمعرفة طول/عرض الجزء الظاهر من الصفحة (طول/عرض مساحة المحتوى) نستعمل الخاصيتين document.documentElement.clientWidth/Height. *لمعرفة الطول/العرض الكلي للصفحة بما فيها الجزء غير الظاهر في النافذة نبحث عن أكبر قيمة من بين القيم التالية: let scrollHeight = Math.max( document.body.scrollHeight, document.documentElement.scrollHeight, document.body.offsetHeight, document.documentElement.offsetHeight, document.body.clientHeight, document.documentElement.clientHeight ); لقراءة إحداثيات موضع الجزء الظاهر من الصفحة إنطلاقا من الركن العلوي الأيسر للصفحة نستعمل الخاصيتين window.pageYOffset/pageXOffset. ولتغيير الموضع الحالي للنافذة نستعمل الدوالّ التالية: window.scrollTo(pageX,pageY): التحريك نحو الموضع ذي الإحداثيات المطلقة (pageX,pageY). window.scrollBy(x,y) : التحريك بمقدار (x,y) انطلاقا من الموضع الحالي. elem.scrollIntoView(top): التحريك لجعل العنصر elem ظاهرًا، أي تحريكه بحيث يتطابق مع أعلى أو أسفل النافذة حسب قيمة الوسيط top. ترجمة -وبتصرف- للفصل Window sizes and scrolling من كتاب Browser: Document, Events, Interfaces
  3. تضم جافاسكربت العديد من الخاصيات التي تسمح بقراءة معلومات طول عنصر ما (height)، وعرضه (width) وجوانب أخرى متعلقة بهندسته (geometry). هي معلومات نحتاجها عادة متى ما أردنا تحريك أو وضع عناصر ما على صفحة ويب باستعمال لغة جافاسكربت. العنصر النموذجي نستعين فيما يلي بالعنصر النموذجي التالي من أجل عرض الخاصيات: <div id="example"> ...Text... </div> <style> #example { width: 300px; height: 200px; border: 25px solid #E8C48F; padding: 20px; overflow: auto; } </style> See the Pen JS-p2-09-Element size and scrolling-ex1 by Hsoub (@Hsoub) on CodePen. يملك هذا العنصر إطارًا (border)، وحاشية (padding) وإحداثيات تمرير (scrolling)؛ أي مجموعة كاملة من الخاصيات. في حين أنه لا يملك هوامش (margins) لأنها لا تُعدّ جزءًا منه وليست لها خاصيات. ويبدو العنصر على الشكل التالي: يمكنك الاطلاع على الشيفرة ومعاينتها على هذا الرابط. ملاحظة: شريط التمرير تُظهر الصورة السابقة أكثر الحالات تعقيدًا، أي حين يكون للعنصر شريط تمرير،حيث تُخصِص بعض المتصفحات ( وليس كلّها) مساحةً لشريط التمرير من المساحة المخصصة للمحتوى، والمؤشَّر عليها في الصورة بعبارة 'عرض المحتوى' (content width). وبذلك يكون عرض المحتوى هنا 300 بكسل في حالة عدم وجود شريط تمرير. ولكن شَغلَ شريط التمرير لمساحةٍ عرضها 16 بكسل (يتغيّر هذا العرض من متصفح لآخر ومن جهاز لآخر) يُصغِّر من عرض المحتوى ليصبح 300-16=284 بكسل. وعليه فإنه من الأهمية بمكان أخذ ذلك في الحسبان. يُسهِّل عدم وجود شريط التمرير العمليات الحسابية، ولكننا نفترض في الأمثلة الخاصة بهذا الفصل أنه موجود. ملاحظة: يمكن لمساحة الحاشية السفلى padding-bottom أن تحتوي نصا عادة ما تَظهر الحواشي فارغةً من أي محتوى. غير أن نص العنصر قد يكون طويلا جدًا بحيث لا تسعه مساحة المحتوى كاملا، فتُظهِره المتصفحات عادة في الحاشية السفلى للعنصر. الخاصيات الهندسية لعنصر ما فيما يلي صورة شاملة تُظهِر الخاصيات الهندسية لعنصر ما. تكون قيم هذه الخاصيات عبارة عن أرقام، ولكنها تُمثِّل في الحقيقة عدد البكسلات، فهي إذا مقاسات بالبكسل. فلنستعرض هذه الخاصيات إنطلاقًا من خارج العنصر نحو داخله. الخاصيات offsetParent وoffsetLeft وoffsetTop نادرًا ما نحتاج إلى هذه الخاصيات ولكنها تصف العنصر من الخارج ، لذا سنبدأ العرض بها: تمثل الخاصية offsetParent السلف الأقرب (ancestor) للعنصر ويستعملها المتصفح لحساب الإحداثيات أثناء ترجمة الشيفرات إلى صفحات ويب، ويكون السلف الأقرب إمّا: متموضع باستعمال لغة CSS: يكون الموضع (position) إمّا مطلقا (absolute)، أو نسبي (relative)، أو ثابت (fixed) أو لاصق (sticky) أو، أحد العناصر: <td> ،<th> ،<table>، أو، العنصر <body>. وتعطي، في المثال التالي، الخاصيتان offsetLeft وoffsetTop الإحداثيات x وy إنطلاقًا من الركن الأيسر العلوي للسلف الأقرب offsetParent. ويُعدّ العنصر <main> السلف الأقرب offsetParent للعنصر <div> المحصور داخله، وتحسب الخاصيتان offsetLeft وoffsetTop إنطلاقًا من الركن العلوي الأيسر للعنصر <main>: <main style="position: relative" id="main"> <article> <div id="example" style="position: absolute; left: 180px; top: 180px">...</div> </article> </main> <script> alert(example.offsetParent.id); // main alert(example.offsetLeft); // 180 (*) alert(example.offsetTop); // 180 </script> See the Pen JS-p2-09-Element size and scrolling-ex2 by Hsoub (@Hsoub) on CodePen. (*): لاحظ أن القيمة هنا عبارة عن رقم (180) وليس سلسلة نصية “180px”. هناك عدة حالات تحمل فيها الخاصية offsetParent القيمة null: إذا كان العنصر غير ظاهر (display:none أو غير ظاهرٍ في الصفحة) إذا كان العنصر هو <body> أو <html>، إذا كان موضع العنصر يحمل القيمة position:fixed. offsetWidth وoffsetHeight نباشر الآن في عرض خاصيات العنصر نفسه. تُعدّ الخاصيتان offsetHeight وoffsetWidth أبسط الخاصيات، حيث تعطيان الطول والعرض الخارجي الكلي للعنصر، بما في ذلك الأطر. وفيما يتعلّق بالعنصر النموذجي لدينا: offsetWidth = 390: العرض الكلي ويمكن حسابه بجمع العرض الداخلي المحدَّد في شيفرة CSS (أي 300 بكسل)، والحاشيتين (220 بكسل) و الإطارين (225 بكسل). offsetHeight = 290: الطول الخارجي. ملاحظة: تأخذ الخاصيات الهندسية للعناصر غير الظاهرة على الصفحة إحدى القيمتان 0 أو null لا تُحسب الخاصيات الهندسية إلا إذا تعلّق الأمر بعناصر ظاهرة على الصفحة. إذا كان العنصر أو أيًا من أسلافه يملك الخاصية display:none أو لم يكن ظاهرًا في الصفحة، تكون قيم كل الخاصيات الهندسية الخاصة به معدومة ( أو null، إذا تحدثنا عن الخاصية offsetParent). فعلى سبيل المثال، تحمل الخاصية offsetParent القيمة null، والخاصيتان offsetWidth وoffsetHeight القيمة 0 إذا أنشأنا عنصرًا ما ولم نُضفه إلى الصفحة أو إذا كانت الخاصية display الخاصة به أو بأسلافه محدّدة بقيمة none. ويمكن التحقّق ممّا إذا كان العنصر ظاهرًا أو مخفيًا كالآتي: function isHidden(elem) { return !elem.offsetWidth && !elem.offsetHeight; } لاحظ هنا أن الدالّة isHidden تعيد القيمة true أيضًا إذا كان العنصر ظاهرًا على الصفحة ولكن مقاساته منعدمة، كالعنصر <div> مثلا إذا كان فارغًا. clientTop وclientLeft داخل العنصر، توجد الأطر، ولقياسها لدينا الخاصيتان clientTop وclientLeft، حيث تكون قيم كلُّ من الخاصيتين في المثال السابق كالآتي: clientLeft = 25، عرض الإطار الأيسر. clientTop = 25، عرض الإطار العلوي. وحتى نكون دقيقين علينا أن ننبّه لأمر ما، وهو أن هاتين الخاصيتين لا تمثلان طول وعرض الإطار، بل الإحداثيات النسبية للجانب الداخلي إنطلاقًا من الجانب الخارجي. ما الفرق بينهما؟ يَظهر الفرق عندما يكون اتجاه الصفحة من اليمين إلى اليسار (إذا كانت لغة المتصفح هي العربية أو العبرية)، حيث يَظهر شريط التمرير في هذه الحالة على الجهة اليسرى بدل اليمنى، وبذلك تضم الخاصية clientLeft عرض شريط التمرير أيضا. وهنا لا تكون قيمة الخاصية clientLeft هي 25، بل 25 زائد عرض شريط التمرير (25 + 16 = 41) . وفيما يلي مثال باللغة العبرية: clientWidth وclientHeight تعطي هاتان الخاصيتان مقاسات المساحة المحصورة بين إطاري العنصر، وتضمّان عرض المحتوى بما فيه الحاشيتين، ولكن دون عرض شريط التمرير. فلنتحدث أولا عن الخاصية clientHeight المذكورة في الصورة أعلاه. لا وجود في الصورة لشريط تمرير أفقي، فقيمتها إذًا تساوي بالتدقيق مجموع ماهو محصور بين الإطارين، أي الطول المحدَّد في شيفرة CSS (أي 200 بكسل) زائد الحاشيتين السفلية والعلوية (2*20 بكسل) وهو ما يعطي القيمة 240 بكسل. أما بالنسبة للخاصية clientWidth، فلا يساوي عرض المحتوى 300 بكسل، بل 240 بكسل، ذلك لأن شريط التمرير يشغَل مساحةً عرضها 16 بكسل. فالنتيجة تكون 284 بكسل زائد عرض الحاشيتين اليمنى واليسرى. المجموع إذا 324 بكسل. وفي حالة عدم وجود الحاشيتين تساوي قيمة الخاصيتين clientWidth وclientHeight بالتدقيق عرض وطول مساحة المحتوى المحصورة بين الأطر وشريط التمرير إن وجد. يمكننا إذا استعمال الخاصيتين clientWidth وclientHeight للحصول على مقاسات مساحة المحتوى إن لم يكن للعنصر حواشي. scrollWidth وscrollHeight تشبه هاتان الخاصيتان الخاصيتين clientWidth وclientHeight، ولكنهما تضمان أيضًا الأجزاء غير الظاهرة. لدينا في الصورة أعلاه: scrollHeight = 723، وتمثل الطول الداخلي الكلي لمساحة المحتوى بما فيه الأجزاء غير الظاهرة. scrollWidth = 324، وتمثل العرض الداخلي الكلي، وبما أنه لا وجود لشريط تمرير أفقي، فهي تساوي clientWidth. ويمكننا استخدام هاتين الخاصيتين لتوسيع العنصر إلى طوله أو عرضه الكلي كالآتي: // توسيع محتوى العنصر إلى طوله الكلي element.style.height = `${element.scrollHeight}px`; حيث يختفي هنا شريط التمرير، ويظهر محتوى العنصر كاملا. scrollLeft وscrollTop تمثل الخاصيتان scrollLeft وscrollTop عرض وطول الجزء غير الظاهر من العنصر (الذي جرى المُرور عليه). لاحظ في الصورة الموالية الخاصيتين scrollHeight وscrollTop لمقطع يضم شريط تمرير عمودي. بعبارة أخرى، تمثل الخاصية scrollTop مقدار تمرير العنصر إنطلاقًا من أعلى الصفحة. ملاحظة: يمكن التعديل على الخاصيتين scrollLeft وscrollTop تُعدّ أغلب الخاصيات التي استعرضناها هنا قابلة للقراءة فقط (read-only)، ولكن الخاصيتين scrollLeft وscrollTop يمكن تغييرهما ليحرِّك المتصفح العنصر. إذا كان لدينا عنصرًا ما، وليكن elem، فإن التعليمة elem.scrollTop += 10 تُحرِّكه بمقدار 10 بكسل نحو الأسفل. ويؤدي إسناد القيمة 0 أو أيّ قيمةٍ كبيرةٍ جدًا (مثلا 1e9) للخاصية scrollTop إلى تحريك العنصر إلى أقصى أعلى أو أسفل الصفحة. لا تأخذ طول وعرض العنصر من شيفرة CSS تحدثنا أعلاه عن الخاصيات الهندسية لعناصر DOM التي يمكن استخدامها للحصول على الطول والعرض ولحساب المسافات. وسبق لك وأن تعرفت في فصل الأنماط التنسيقية والأصناف، أنه يمكن قراءة الطول والعرض المحدَّدّين في شيفرة CSS لعنصر ما باستعمال الدالّة getComputedStyle، فلماذا لا نقرأ عرض عنصرٍ ما باستعمال الدالّة getComputedStyle، كما في المثال التالي؟ let elem = document.body; alert( getComputedStyle(elem).width ); // إظهار عرض العنصر المحدَّدّ في شيفرة CSS See the Pen JS-p2-09-Element size and scrolling-ex3 by Hsoub (@Hsoub) on CodePen. لماذا عليك استعمال الخاصيات الهندسية بدلًا من هذه الدالّة؟ هناك سببان لذلك: الأول، هو أن الطول والعرض المحدَّدين في شيفرة CSS يتعلّقان بخاصية أخرى وهي box-sizing، التي تحدِّد الطول والعرض في شيفرة CSS. ويمكن لأيّ تغيير في الخاصية box-sizing أن يوقِف عمل شيفرة جافاسكربت. والثاني، هو أن الطول والعرض المحدَّدين في شيفرة CSS، قد يحملان القيمة "auto" كما هو الحال بالنسبة للعناصر السطرية (inline element). مثال: <span id="elem">Hello!</span> <script> alert( getComputedStyle(elem).width ); // auto </script> See the Pen JS-p2-09-Element size and scrolling-ex4 by Hsoub (@Hsoub) on CodePen. يُعدّ إسناد القيمة "auto" للخاصية width أمرًا عاديًا جدًا في لغة CSS، ولكن جافاسكربت يتطلّب مقاسا دقيقا بالبكسل حتى يستخدمه في العمليات الحسابية. لذلك لن يفيدك العمل بالعرض المحدَّد في شيفرة CSS في هذه الحالة. هناك أيضا سببٌ آخر يتعلّق بشريط التمرير. حيث يحدث أن يعمل سكربت معين بصفة عادية إن لم يكن هناك شريط تمرير، في حين يُظهِر العديدَ من الأخطاء البرمجية في حالة وجوده. هذا لأن شريط التمرير يَشغَل مساحةً من المحتوى في بعض المتصفحات، فيكون العرض الحقيقي المتاح للمحتوى أقل من العرض الذي تحدِّده شيفرة CSS، وهو ما تأخذه الخاصيتان clientWidth وclientHeight في الحسبان. ولكن الأمر يختلف عند استخدام التعليمة getComputedStyle(elem).width، حيث تُعيد بعض المتصفحات (مثل المتصفح Chrome) العرض الداخلي الحقيقي بعد طرح عرض شريط التمرير منه، وتُعيد بعض المتصفحات الأخرى (مثل المتصفح Firefox) العرض المحدَّد في شيفرة CSS (حيث يُهمِل هذا المتصفح شريط التمرير). ويُعدّ هذا الاختلاف من متصفح لآخر هو السبب الذي يحمِلنا على عدم استعمال الدالّة getComputedStyle بل الاعتماد على الخاصيات الهندسية بدلا منها. وإذا كان المتصفح المثبَّت على حاسوبك يُخصِص مساحةً لشريط التمرير (وهو ما تُوفره معظم متصفحات نظام التشغيل Windows) ، يمكنك معاينة السكربت من خلال هذا الرابط هنا، يساوي عرض العنصر المحدَّد في شيفرة CSS الذي يضم النص 300 بكسل. تُخصِص المتصفحات Chrome وFirefox وEdge على نظام التشغيل Windows المكتبي مساحةً لشريط التمرير. ولكن المتصفح Firefox يُظهر القيمة 300 بكسل، في حين يُظهر المتصفح Chrome والمتصفح Edge قيمةً أقل، لأن المتصفح Firefox يُظهر العرض المحدَّد في شيفرة CSS، أمّا بقية المتصفحات فتُعيد العرض الحقيقي للعنصر (بعد اقتطاع عرض شريط التمرير). لاحظ أن الفرق يظهر فقط عند قراءة القيمة باستعمال الدالّة getComputedStyle(...).width في شيفرة جافاسكربت، حيث يكون الشكل الذي يَظهر على الشاشة صحيحًا. الخلاصة تملك العناصر الخاصيات الهندسية التالية: offsetParent: تمثل السلف الأقرب للعنصر من حيث التموضع أو إحدى القيم التالية: td أو th أوtable أوbody. offsetLeft وoffsetTop: تمثل إحداثيات العنصر إنطلاقًا من الحافة العلوية اليسرى للعنصر السلف offsetParent الخاص به. offsetWidth وoffsetHeight: تمثل العرض والطول الخارجي لعنصر ما بما فيه الأطر. clientLeft وclientTop: تمثلان المسافتين بين الركن العلوي الأيسر الخارجي والركن العلوي الأيسر الداخلي (المحتوى + الحاشية). بالنسبة لأنظمة التشغيل التي تعمل باللّغات التي تُكتَب من اليسار إلى اليمين، تساوي هاتان الخاصيتان عرض الإطارين العلوي والأيسر، أما بالنسبة لأنظمة التشغيل التي تعمل باللّغات التي تُكتَب من اليمين إلى اليسار، يكون شريط التمرير من الجهة اليسرى، وبذلك تضم الخاصية clientLeft عرض شريط التمرير أيضًا. clientWidth وclientHeight: تمثلان عرض وطول المحتوى بما فيه الحواشي دون احتساب عرض شريط التمرير. scrollWidth وscrollHeight: تمثلان عرض وطول المحتوى، تماما مثل ما هو الحال بالنسبة للخاصيتين clientWidth وclientHeight، ولكن مع احتساب الأجزاء غير الظاهرة من العنصر. scrollLeft وscrollTop: تمثل عرض وطول الجزء العلوي للعنصر الذي جرى المُرور عليه إنطلاقًا من الركن العلوي الأيسر للعنصر. وتُعد كلّ هذه الخاصيات قابلة للقراءة فقط (read-only) عدا الخاصيتين scrollLeft وscrollTop اللّتين يؤدي تغيير قيمتهما إلى تحريك المتصفح للعنصر. تمارين مقاس الجزء الذي لم نمر عليه بعد درجة الأهمية: 5 تمثل الخاصية elem.scrollTop مقاس الجزء الذي جرى المُرور عليه إنطلاقًا من الأعلى. كيف يمكننا إذًا الحصول على مقاس الجزء الذي لم نمر عليه بعد (ولنسمه scrollBottom)؟ اكتب الشيفرة التي تسمح بالحصول على مقاس الجزء (من عنصر ما) الذي لم نمر عليه بعد، وليكن هذا العنصر elem. ملاحظة: عليك التحقّق من الشيفرة التي تكتبها، حيث ينبغي أن تعطي القيمة 0 إذا لم يكن هناك شريط تمرير، أو إذا كان العنصر متموضعًا أساسا في الأسفل. الحل الحل هو كالآتي: let scrollBottom = elem.scrollHeight - elem.scrollTop - elem.clientHeight; أو بعبارة أخرى، (الطول الكلي) ناقص (الجزء العلوي الذي جرى المُرور عليه) ناقص (الجزء الظاهر). هذا ما يسمح بالحصول على الجزء الذي لم نمر عليه بعد. ما هو عرض شريط التمرير؟ _درجة الأهمية: _3 اكتب الشيفرة التي تُعيد عرض شريط تمرير عادي. يكون هذا العرض بالنسبة لنظام التشغيل Windows بين 12 و20 بكسل. وفي حالة ما لم يُخصِص المتصفح مساحةَ لشريط التمرير (يحدث أن يكون الشريط نصف شفاف ويتموضع فوق النص)، يكون عرضه منعدما. ملاحظة: ينبغي أن تعمل الشيفرة على أيّ صفحة HTML كانت وأن لا تكون مرتبطة بمحتوى هذه الصفحة. الحل للحصول على عرض شريط التمرير، يمكن إنشاء عنصرٍ يملك شريط تمرير لكن دون أطر ولا حواشي. وبذلك يكون الفرق بين العرض الكلي للعنصر offsetWidth وعرض المساحة الداخلية للمحتوى clientWidth هو عرض شريط التمرير. // إنشاء حاوية تملك شريط تمرير let div = document.createElement('div'); div.style.overflowY = 'scroll'; div.style.width = '50px'; div.style.height = '50px'; // إضافتها للصفحة، وإلا فسوف نحصل على مقاسات منعدمة (0 بكسل) document.body.append(div); let scrollWidth = div.offsetWidth - div.clientWidth; div.remove(); alert(scrollWidth); وضع الكرة في مركز المساحة الخضراء درجة الأهمية: 5 تكون الصفحة مبدئيا كالآتي: ما هي إحداثيات مركز المساحة الخضراء؟ احسبها واتبِّع ما يلي لوضع الكرة في مركز المساحة الخضراء: حرِّك العنصر باستعمال لغة جافاسكربت، وليس لغة CSS. ينبغي أن تعمل الشيفرة مهما كان مقاس الكرة ومهما كان مقاس المساحة الخضراء وليس فقط مع المقاسات المعطاة. ملاحظة: يمكن وضع الكرة في المركز باستعمال لغة CSS، ولكننا نريد تجسيد ذلك باستعمال لغة جافاسكربت. سوف تُصادف مستقبلا مواضيع أخرى ووضعيات أكثر تعقيدًا، حيث يكون استعمال جافاسكربت ضرورة حتمية. يُعدّ هذا المثال مجرّد تمرين بسيط للتسخين. يمكنك الاطلاع على شيفرة التمرين من هنا الحل موضع الكرة هو position:absolute، وهو ما يعني أن إحداثياتها بالنسبة للأعلى ولليسار تُحسب بناءً على أقرب عنصر متموضع، أي العنصر ‎#field (لأنه يملك الموضع position:relative). يكون مبدأ الإحداثيات (0،0) متطابقا مع الركن العلوي الأيسر الداخلي للمساحة الخضراء. تُمثِل قيم الخاصيتين clientWidthوclientHeight العرض والطول الداخليين للمساحة الخضراء، وبالتالي تكون إحداثيات المركز كالآتي: (clientWidth/2, clientHeight/2). ولكننا حين نسند هذه القيم للخاصيتين ball.style.left وball.style.top نجعل الحافة العلوية اليسرى للكرة، وليس الكرة بأكملها، تتحرك نحو المركز، فتظهر على هذا الشكل: ball.style.left = Math.round(field.clientWidth / 2) + 'px'; ball.style.top = Math.round(field.clientHeight / 2) + 'px'; ومن أجل مطابقة مركز الكرة مع مركز المساحة الخضراء، ينبغي تحريك الكرة بمقدار نصف طولها للأعلى وبمقدار نصف عرضها نحو الجهة اليسرى. وبذلك تتموضع الكرة في مركز المساحة الخضراء. ball.style.left = Math.round(field.clientWidth / 2 - ball.offsetWidth / 2) + 'px'; ball.style.top = Math.round(field.clientHeight / 2 - ball.offsetHeight / 2) + 'px'; تحذير لا تعمل الشيفرة بالشكل المطلوب إذا لم يكن للكرة طول أو عرض. <img src="ball.png" id="ball"> إذا لم يجد المتصفح طول وعرض صورةٍ ما ضمن سمات الوسوم أو في شيفرة CSS، يعتدّها منعدمة إلى غاية التحميل الكلي للصورة ، وبالتالي تكون قيمة ball.offsetWidth منعدمة إلى غاية تحميل الصورة بشكل كامل. وهذا ما ينتج عنه إحداثيات خاطئة عند استعمال الشيفرة أعلاه. بعد عملية التحميل الأولى للصورة، يضع المتصفح الصورة في الذاكرة المؤقتة (cache)، وعند إعادة التحميل، يحصل المتصفح على المقاسات في الحين. لكن المشكل هو أن قيمة الخاصية ball.offsetWidth تكون منعدمة عند عملية التحميل الأولى. يمكن تفادي حصول ذلك بإسناد قيمة للسمتين width وheight الخاصتين بالصورة <img>: <img src="ball.png" width="40" height="40" id="ball"> أو إضافة المقاسات في شيفرة CSS كالآتي: #ball { width: 40px; height: 40px; } يمكنك الاطلاع على الحل من خلال هذا الرابط الفرق بين العرض المحدَّد في شيفرة CSS وclientWidth درجة الأهمية 5 ما هو الفرق بين getComputedStyle(elem).width و elem.clientWidth؟ أعط ثلاث فوارق على الأقل، ويستحسن أكثر. الحل الفوارق هي كالآتي: تُعدّ قيمة الخاصية clientWidth قيمة عددية، بينما تكون قيمة getComputedStyle(elem).width عبارة عن سلسلة نصية تحمل في نهايتها الحرفينpx. يمكن أن تُعيد الدالّة getComputedStyle قيمًا غير عددية كالقيمة "auto" مثلا بالنسبة للعناصر السطرية. تمثل الخاصية clientWidth مقاس المساحة الداخلية للمحتوى بالإضافة إلى الحواشي، بينما لا يضم العرض المحدَّد في شيفرة CSS (مع خاصية box-sizing عادية) الحواشي (يتمثل في مقاس المساحة الداخلية للمحتوى دون الحاشيتين). إذا كان هناك شريط تمرير وخَصص المتصفح مساحةً لهذا الشريط، قد تطرح بعض المتصفحات هذه المسافة من العرض المحدَّد في شيفرة CSS (لأنه لم يَعُد متاحا ليضم محتوى ما)، ولكن بعض المتصفحات الأخرى لا تسلك السلوك نفسه. أما الخاصية clientWidth فهي دائما تمثل نفس القيمة، حيث يُطرَح عرض شريط التمرير إذا خُصصت له مساحةً ما. ترجمة -وبتصرف- للفصل Element size and scrolling من كتاب Browser: Document, Events, Interfaces
  4. تجدر بنا الإشارة إلى قاعدة مهمة قبل الشروع في الحديث عن كيفية تعامل جافاسكربت مع الأنماط والأصناف. القاعدة بديهية جدًا ولكننا سنذكرها للإفادة. هناك طريقتان تُستخدمان لتنسيق عنصرٍ ما: إنشاء صنف في ملف CSS وإضافته للعنصر على الشكل التالي: <div class="..."‎> كتابة خاصيات السمة style مباشرة بين تسلسلات التهريب الخاصة بالعنصر على الشكل التالي: <div style="..."‎> تستطيع لغة جافاسكربت التعديل على خاصيات الأصناف وخاصيات الخاصية style. ويُستحسن استعمال أصناف CSS للتنسيق بدلا من السمة style، حيث نلجأ إلى الطريقة الثانية فقط إذا تعذّرعلينا إضافة التنسيق باستعمال الطريقة الأولى. فعلى سبيل المثال، يمكن استعمال الخاصية style إذا كان عليك حساب إحداثيات عنصر ما ديناميكيًا وتحديد قيمها باستعمال جافاسكربت كالآتي: let top = /* عمليات حسابية معقّدة */; let left = /* عمليات حسابية معقّدة */; elem.style.left = left; // e.g '123px', تُحسب أثناء التنفيذ elem.style.top = top; // e.g '456px' وفي حالات أخرى مثل تلوين النص بالأحمر، أو إضافة صورة للخلفية، يُستحسن وصف التنسيق باستعمال CSS ثم إضافة الصنف للعنصر (يمكن عمل ذلك باستعمال جافاسكربت)، حيث يمنحك ذلك أكثر مرونة وسهولة في البرمجة. اسم الصنف (className) وقائمة الأصناف (classList) يُعدّ تعديل الصنف أكثر عمليةٍ نُصادفها في السكربتات. وكانت جافاسكربت فيما مضى تتّسمُ بالمحدودية حين يتعلّق الأمر بالكلمة المحجوزة "class"، حيث لم تكن تسمح بأن تحمل خاصيةٌ من خواص الكائن (object) اسم "class" كالآتي : elem.class. حينها جاء التفكير في استحداث خاصية مشابهة تسمى "className" تُطبّق على الأصناف. حيث يمثِّل elem.className اسم السمة "class" كما هو مبين في المثال التالي: <body class="main page"> <script> alert(document.body.className); // main page </script> </body> See the Pen JS-p2-08-Styles and classes-ex1 by Hsoub (@Hsoub) on CodePen. في حالة ما إذا أسندنا قيمةً معينةً للخاصية elem.className، تعوِّض هذه القيمة مُجمل سلسلة الأصناف. هذا ما نحتاج إليه أحيانا، ولكننا نحتاج في غالبية الحالات إلى إضافة أو حذف صنفٍ واحدٍ فقط. ولهذا وُجدت خاصية أخرى؛ إنها خاصية elem.classList. تُعدّ هذه الخاصية كائنًا خاصًا بحد ذاته له دوالّه الخاصة (methods) لإضافة صنف ما (add) أو حذفه (remove) أو إما إضافته إن لم يكن موجودًا أو حذفه إن وُجد (toggle).كما هو مبين في المثال التالي: <body class="main page"> <script> // إضافة صنف document.body.classList.add('article'); alert(document.body.className); // main page article </script> </body> See the Pen JS-p2-08-Styles and classes-ex2 by Hsoub (@Hsoub) on CodePen. وبذلك يمكننا إجراء عمليات على مجمل سلسلة الأصناف دفعة واحدة باستعمال className أو على الأصناف، كلٌّ على حدى، باستعمال classList. اختيارنا لهذا أو ذاك مرتبط بما نحتاج القيام به. الدوالّ الخاصة بـالخاصية classList هي: *elem.classList.add/remove("class")‎: إضافة الصنف المذكور كوسيط للدلّة/حذف الصنف المذكور كوسيط للدلّة. *elem.classList.toggle("classe")‎: إضافة الصنف المذكور كوسيط للدلّة إن لم يكن موجودًا أو حذفه إن وُجد. *elem.classList.contains("class")‎: البحث عن الصنف المذكور كوسيط للدلّة، والنتيجة تكون صحيح أو خطأ (true/false). وتقبل الخاصية classList الإدماج داخل حلقة التكرار for....of لإظهار قائمة الأصناف كما في المثال التالي: <body class="main page"> <script> for (let name of document.body.classList) { alert(name); // main, and then page } </script> </body> See the Pen JS-p2-08-Styles and classes-ex3 by Hsoub (@Hsoub) on CodePen. تنسيق العنصر باستعمال الخاصية style تُعدّ الخاصية elem.style كائنًا يحمل محتوى السمة style. ويؤدي إسناد القيمة "100px" للخاصية elem.style.width على الشكل التالي: elem.style.width="100px" إلى النتيجة نفسها لو كانت السمة style تحمل السلسلة النصية "width:100px". إذا كان اسم السمة يتكون من عدة كلمات، يُشكَّل اسم الخاصية بجعل الحرف الأول من كل كلمة حرفا كبيرًا ماعدا الكلمة الأولى كما في المثال التالي: background-color => elem.style.backgroundColor z-index => elem.style.zIndex border-left-width => elem.style.borderLeftWidth ملاحظة: الخاصيات التي تبدأ ببادئة تتبع الخاصيات التي تبدأ ببادئة تُحدّد المتصفح نفس القاعدة، كالخاصيتين -moz-border-radius و-webkit-border-radius، حيث تُترجَم الشرطة إلى حرفٍ كبيرٍ كالآتي: button.style.MozBorderRadius = '5px'; button.style.WebkitBorderRadius = '5px'; تغيير قيمة خاصية التنسيق style يحدث أن تَرغب في إسناد قيمةٍ للخاصية style ثم حذفها لاحقا. يمكننا على سبيل المثال إسناد القيمة "none" للخاصية elem.style.display كالأتي elem.style.display = "none" ثم حذفها وكأننا لم نحدّد لها قيمةً من قبل. هنا، ينبغي إسناد سلسلة نصية فارغة للخاصية elem.style.display كالآتي elem.style.display = ""‎ بدلا من حذفها (delete). //عند تنفيذ هذا السكربت يختفي العنصر <body> ثمّ يُعاود الظهور document.body.style.display = "none"; // يختفي setTimeout(() => document.body.style.display = "", 1000); // يُعاود الظهور إذا أسندنا سلسلة نصية فارغة للخاصية style.display، يُطبِّق المتصفح أصناف CSS والأنماط التنسيقية المتضمَّنة بداخلها بطريقة عاديةٍ جدًا وكأن الخاصية style.display غير موجودة تماما. ملاحظة: التعديل على خاصيات الخاصية style جملةً واحدةً باستعمال الخاصية style.cssStyle تُستعمل عادة الخاصية *.style للتعديل على قيم خاصيات التنسيق، كلٌ على حدى، ولا يمكننا التعديل عليها دفعة واحدة كالآتي: div.style="color:red; width:100px"‎، لأن div.style هو كائنٌ لا يمكن التعديل عليه (ِread-only) بهذه الطريقة. يمكن تغيير التنسيق كاملا دفعة واحدة بإسناد سلسلة نصية (تحمل وصف التنسيق) للخاصية style.cssStyle كما في المثال التالي: <div id="div">Button</div> <script> // يمكننا استعمال رايات تنسيقية خاصة مثل الراية “important” div.style.cssText=`color: red !important; background-color: yellow; width: 100px; text-align: center; `; alert(div.style.cssText); </script> See the Pen JS-p2-08-Styles and classes-ex4 by Hsoub (@Hsoub) on CodePen. غير أنه من النادر استعمال هذه الخاصية كونها تحذف الأنماط التنسيقية السابقة وتستبدلها بالقيم الجديدة، أي أنها قد تحذف أشياء مازلنا بحاجتها. فيما يمكن أن تُستخدم لتنسيق العناصر الجديدة، فلن يؤدي إسناد القيم بهذه الطريقة إلى أيّ عملية حذف (بما أن العناصر الجديدة لا تملك تنسيقات بعد). ويمكننا عمل ذلك أيضا باستعمال الدالّة div.setAttribute('style', 'color: red...')‎. الوحدات لا تنس إضافة الوحدات للقيم في شيفرة CSS، فلا يصِحُّ إسناد القيمة "10" للخاصية elem.style.top بل القيمة 10px هي الأصح، وإلا فلن يعمل السكربت بالشكل المطلوب. <body> <script> // لا يعمل document.body.style.margin = 20; alert(document.body.style.margin); // '' (سلسلة نصية فارغة، إهمال عملية الإسناد) // يعمل بعد إضافة الوحدة document.body.style.margin = '20px'; alert(document.body.style.margin); // 20px alert(document.body.style.marginTop); // 20px alert(document.body.style.marginLeft); // 20px </script> </body> See the Pen JS-p2-08-Styles and classes-ex5 by Hsoub (@Hsoub) on CodePen. لاحظ في السطرين الأخيرين أن المتصفح يفكّك الخاصية style.margin إلى خاصيتين وهما: style.marginLeft وstyle.marginTop. الأنماط المحسوبة باستعمال الدالة getComputedStyle يُعدّ التعديل على الأنماط عمليةً سهلةً ولكن كيف تُقرأ الأنماط؟ نريد مثلا معرفة مقاس، هوامش ولون عنصرٍ ما، كيف نتحصل عليها؟ تعمل الخاصية style على تعديل قيمة السمة style فقط دون الوصول إلى الأنماط الموصوفة في الأوراق التنسيقية المتتالية CSS. وبالتالي لا يمكننا قراءة أيّ قيمٍ من أصناف CSS باستعمال الخاصية elem.style. فعلى سبيل المثال لا يمكن أن تصل الخاصية style في هذا المثال إلى الهامش. <head> <style> body { color: red; margin: 5px } </style> </head> <body> The red text <script> alert(document.body.style.color); // فارغة alert(document.body.style.marginTop); // فارغة </script> </body> See the Pen JS-p2-08-Styles and classes-ex6 by Hsoub (@Hsoub) on CodePen. ماذا لو أردنا على سبيل المثال إضافة 20px للهامش؟ سيكون علينا أولا الوصول إلى القيمة الحالية له حتى يتسنى لنا تعديلها. وهنا لدينا طريقة أخرى للحصول على ذلك وتكون باستعمال الدالة getComputedStyle وبنيتها كالآتي: getComputedStyle(element, [pseudo]) حيث يمثّل العنصر element العنصر الذي سنحسب قيمه ويمثل pseudo العنصر الزائف، مثلا before::. إذا كانت قيمة pseudo عبارة عن سلسلة نصية فارغة أو غير موجودة أصلا فهذا يعني أننا نقصد العنصر نفسه. وتكون مخرجات الدالّة (output) عبارة عن كائن يحوي أنماط تنسيقية مثله مثل elem.style ولكن يأخذ في الحسبان هذه المرة كلّ الأصناف الموجودة في ملف CSS. وفيما يلي مثال على ذلك: <head> <style> body { color: red; margin: 5px } </style> </head> <body> <script> let computedStyle = getComputedStyle(document.body); // يمكننا الآن قراءة اللون والهامش alert( computedStyle.marginTop ); // 5px alert( computedStyle.color ); // rgb(255, 0, 0) </script> </body> See the Pen JS-p2-08-Styles and classes-ex7 by Hsoub (@Hsoub) on CodePen. ملاحظة: القيم المحسوبة والقيم النهائية (المُحدَّدة) هناك مفهومان في لغة CSS هما: قيمة تنسيقية محسوبة وهي القيمة المتحصّل عليها بعد تطبيق مجمل القواعد التنسيقية وقواعد الوراثة المُتضمَّنة في ملف CSS. قد تكون على شكل height:1em أو font-size:125% . قيمة تنسيقية نهائية (مُحدَّدة) وهي القيمة التي يقع عليها الاختيار في آخر المطاف وتُطبَّق على العنصر. القيمتان 1em و125% هما قيمتان نسبيتان. يأخذ المتصفح كافة القيم المحسوبة ويجعل كافة الوحدات مطلقة كما في المثال التالي: height:20px ،font-size:16px. ويمكن للقيم النهائية الخاصة بالخاصيات الهندسية أن تكون عشرية مثل: width:50.5px. لقد اُستحدثت الدالّة getComputedStyle أساسا للحصول على قيم محسوبة ولكن تبيّن فيما بعد أن القيم النهائية (المُحدَّدة) أحسن، فتغيرت المعايير، وأصبحت الدالّة getComputedStyle تُخرِج القيم النهائية للخاصية والتي تكون غالبا بالبكسل px بالنسبة للخاصيات الهندسية. ملاحظة: تتطلب الدالة getComputedStyle ذكر الاسم الكامل للخاصية علينا البحث على الدوام عن الخاصية التي نحتاج إليها بدقة مثل: padingLeft، أو marginTop أو borderTopWidth وإلا فلن نتمكن من ضمان صحة النتيجة المتحصّل عليها. فمع وُجود، على سبيل المثال، الخاصيتين padingLeft/padingTop، على ماذا سوف نحصل عند تنفيذ الدالّة getComputedStyle(elem, pading)‎؟ لن نحصل على شيئ؟ أو ربما سنحصل على قيمة "مستوحاة" من قيم معرّفة مسبقا للحاشية (pading)؟ في الحقيقة لا يوجد أيّ معايير تتحدث عن هذا الموضوع. وهناك بعض التناقضات الأخرى، حيث تُظهِر بعض المتصفحات (Chrome مثلا) في مثال الموالي القيمة 10px، ولا تُظهِرها متصفحات أخرى (كالمتصفح Firefox). مثال: <style> body { margin: 10px; } </style> <script> let style = getComputedStyle(document.body); alert(style.margin); // نحصل على سلسلة فارغة عند استعمال المتصفح Firefox </script> See the Pen JS-p2-08-Styles and classes-ex8 by Hsoub (@Hsoub) on CodePen. ملاحظة: الأنماط التي تُطبَّق على الروابط ‎:visited تكون مخفية يمكن تلوين الروابط التي سبق وأن زيرت باستخدام الصنف الزائف ‎:visited في ملف CSS. لكن الدالّة getComputedStyle لا يمكنها الوصول إلى هذا اللون، لأن ذلك يمكِّن أيّ صفحة كانت من إنشاء الرابط على الصفحة والإطلاع على الأنماط وبالتالي معرفة ما إذا كان المستخدم قد زار الرابط من قبل. لا يمكن للغة جافاسكربت الإطلاع على الأنماط المعرّفة باستخدام الصنف الزائف ‎:visited، كما تمنع لغة CSS تطبيق تنسيقاتِ تغيير الشكل والأبعاد (geometry-changing styles) ضمن الصنف الزائف ‎:visited وذلك لغلق الطريق أمام أيّ صفحةٍ مشبوهةٍ تسعى لمعرفة ما إذا زار المستخدم الرابط أم لا، وبالتالي التعدي على خصوصيته. الخلاصة هناك خاصيتان تُستخدمان للعمل على الأصناف وهما: className: وهي سلسلة نصية تُستخدم للعمل على كافة الأصناف دفعةً واحدةً. classList: هي عبارة عن كائن له دوالّه الخاصة (add/delete/toggle/contains) وتستخدم للعمل على الأصناف، كلُ على حدى. ولتغيير التنسيق لدينا: الخاصية style؛ وهي عبارة عن كائن تُشكَّل خواصه بجعل الحرف الأول من كل كلمة حرفا كبيرًا ما عدا الكلمة الأولى. تُعدّ قراءته والتعديل عليه تماما كالتعديل على خاصيات السمة style، كلٌ على حدى. وللاطلاع على كيفية إضافة الراية important وغيرها، يمكنك زيارة موقع MDN حيث تجد قائمة من الدوالّ التي تُستخدم لذلك. الخاصية style.cssText: هي الخاصية التي تقابِل السمة "style" في مجملها، أي السلسلة النصية التي تحمل كافة الأنماط التنسيقية دفعةً واحدةً. ولقراءة الأنماط التنسيقية النهائية (التي تأخذ في الحسبان كافة الأصناف بعد تطبيق تنسيقات CSS وحساب القيم النهائية) وُجدَت الدالة getComputedStyle(elem, [pseudo])‎ والتي تخرِج/تعيد كائنا يحمل التنسيقات وهو قابلٌ للقراءة فقط. تمرين إنشاء إشعار درجة الأهمية: 5 اكتب شيفرة الدالّة showNotification(options)‎ التي تُنشِئ إشعارًا كالآتي: بالمحتوى الذي يُمرَّر لها كوسيط، حيث يختفي الإشعار بعد ثانية ونصف من إظهاره. وتُوفّر الخيارات التالية: // أظهِر عنصرًا يحمل النص "Hello" بالقرب من الركن العلوي الأيمن للنافذة showNotification({ top: 10, // عشرة بكسل بدءًا من أعلى النافذة والذي فاصلته 0 right: 10, // عشرة بكسل بدءًا من الحافة اليمنى للنافذة والتي ترتيبتها 0 html: "Hello!", // شيفرة HTML الخاصة بالإشعار className: "welcome" // صنف إضافي للحاوية ‘div’ (اختياري) }); استعمل تنسيقات CSS لتحديد موضع إظهار العنصر حسب الإحداثيات المعطاة بافتراض أن الصفحة تحتوي مسبقا على الأنماط التنسيقية الضرورية. الحل يمكنك الإطلاع على الحل من هنا ترجمة -وبتصرف- للفصل Styles and classes من كتاب Browser: Document, Events, Interfaces
  5. يُعدّ التعديل على نموذج تمثيل المستند ككائن (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> See the Pen JS-p2-07-Modifying the document-ex1 by Hsoub (@Hsoub) on CodePen. استعملنا في هذا المثال لغة 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> See the Pen JS-p2-07-Modifying the document-ex2 by Hsoub (@Hsoub) on CodePen. نادينا، في هذا المثال، الدالّة 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> See the Pen JS-p2-07-Modifying the document-ex3 by Hsoub (@Hsoub) on CodePen. وفيما يلي صورةٌ توضيحيةٌ لما تقوم به هذه الدوالّ: وبالتالي تصبح القائمة في شكلها النهائي كما يلي: 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> See the Pen JS-p2-07-Modifying the document-ex4 by Hsoub (@Hsoub) on CodePen. لاحظ هنا أن النص أُضيف بمثابة "نصٍ" وليس بمثابة شيفرة 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> See the Pen JS-p2-07-Modifying the document-ex5 by Hsoub (@Hsoub) on CodePen. الذي يولّد الشيفرة التالية: <p>Hello</p> <div id="div"></div> <p>Bye</p> كانت هذه هي الطريقة التي تُمكّننا من إضافة شيفرة HTML، أيًّا كانت، للصفحة. وفيما يلي صورة توضيحية لطرائق الإضافة في مختلف المواضع: ويمكننا بسهولة ملاحظة أوجه الشبه بين هذه الصورة والصورة التي قبلها، حيث أن مواضع الإضافة هي نفسها لكن هذه الدالّة تُضيف شيفرة 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> See the Pen JS-p2-07-Modifying the document-ex6 by Hsoub (@Hsoub) on CodePen. إزالة/حذف عقدة ما اُستحدثت الدالّة 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> See the Pen JS-p2-07-Modifying the document-ex7 by Hsoub (@Hsoub) on CodePen. لاحظ هنا أنه لا داعيَ لتحويل عنصرٍ ما من الموضع الذي كان فيه إذا أردت وضعه في مكان آخر، هذا لأنّ كافة دوالّ الإضافة تُحوِّل آليا العنصر من مكانه السابق. فلنُحوّل من خلال المثال الآتي موضعي العنصرين التاليين: <div id="first">First</div> <div id="second">Second</div> <script> // لا داعيَ لمناداة الدالّة remove second.after(first); //تُضيف العنصر second بعد العنصر first </script> See the Pen JS-p2-07-Modifying the document-ex8 by Hsoub (@Hsoub) on CodePen. نسخ العُقد باستعمال الدالة 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> See the Pen JS-p2-07-Modifying the document-ex9 by Hsoub (@Hsoub) on CodePen. العقدة الخاصة 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> See the Pen JS-p2-07-Modifying the document-ex10 by Hsoub (@Hsoub) on CodePen. لاحظ هنا في السطر الأخير الموسوم بالرمز (*) أنّنا أضفنا العقدة الخاصة 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> See the Pen JS-p2-07-Modifying the document-ex11 by Hsoub (@Hsoub) on CodePen. أتينا على ذكر العقدة الخاصة 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> See the Pen JS-p2-07-Modifying the document-ex12 by Hsoub (@Hsoub) on CodePen. الدالة 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> See the Pen JS-p2-07-Modifying the document-ex13 by Hsoub (@Hsoub) on CodePen. يمكننا إضافة العقدة 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> See the Pen JS-p2-07-Modifying the document-ex14 by Hsoub (@Hsoub) on CodePen. تكون مخرجات (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> See the Pen JS-p2-07-Modifying the document-ex15 by Hsoub (@Hsoub) on CodePen. تؤدي مناداة الدالّة 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> See the Pen JS-p2-07-Modifying the document-ex16 by Hsoub (@Hsoub) on CodePen. لا ينبغي إذًا استعمال هذه الدالّة بعد تحميل الصفحة، على عكس غيرها من دوالّ 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. حدِّد، من بين التعليمات الثلاث التالية، تلك التي لها نفس الوظيفة. elem.append(document.createTextNode(text))‎ elem.innerHTML = text 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’ على أن تكون النتيجة المتحصل عليها كالآتي: اِختر واحدةً من بين الطريقتين التاليتين لحل التمرين: أنشئ الشجرة في شكل شيفرة HTML ثم أسندها للخاصية Container.innerHTML أو أنشئ عُقد الشجرة وأضفها باستعمال دوالّ DOM. ويُستحسن أن تَحُل التمرين بالطريقين. ملاحظة: لا ينبغي أن تَضمّ الشجرة في أوراقها عناصر إضافية فارغة <ul></ul>. الحل ملاحظة: أسهل طريقة لتصفح الكائن (المرور بعناصره الواحد تلوى الآخر) تكون باستعمال التعاود (recursion). حل التمرين باستعمال الخاصية innerHTML حل التمرين باستعمال دوالّ DOM إظهار العناصر الوليدة لشجرة ما درجة الأهمية: 5 لدينا هذه الشجرة وهي مبنية على شكل عناصر ul/li متفرعة. اكتب شيفرة تُضيف من خلالها لكل عنصر <li> عدد عناصره الوليدة مع إهمال الأوراق (العُقد التي ليس لها عناصر وليدة)، والنتيجة تكون على الشكل التالي: الحل لإضافة نصٍ لكلّ عنصر من العناصر <li> يمكن التعديل على العُقدة النصية data. يمكنك الإطلاع على الحل من هنا إنشاء يومية الأهمية:4 اكتب الدالّة createCalendar(elem, year, month)‎ التي تُنشئ يومية الشهر month من السنة year، وتسندها للعنصر elem. تكون اليومية على شكل جدول تُمثِّل فيه العناصر <tr> الأسابيع والعناصر <td> الأيام، وتكون خانات السطر الأول عبارة عن عناصر <th> تحوي أسماء أيام الأسبوع مُرتّبةً من الإثنين إلى الأحد. فعلى سبيل المثال، تؤدي مناداة الدالّة createCalendar(cal, 2012, 9)‎ إلى إنشاء اليومية التالية وإسنادها للعنصر cal. ملاحظة: يكفي في هذه المرحلة إظهار اليومية، لا نحتاج أن تكون تفاعلية. يمكنك الإطلاع على شيفرة التمرين من هنا الحل نُنشئ الجدول على شكل سلسلة نصية "<table>....</table>" ثم نسندها إلى الخاصية innerHTML، باتباع خطوات الخوارزمية التالية: إنشاء خانات السطر الأول من الجدول (table header) باستعمال عناصر<th> تحوي أسماء أيام الأسبوع. إنشاء الكائنdate باستعمال الدالّة البانية d=newDate(year, month-1)‎ والذي يُمثِّل أول يوم في الشهر month (مع الأخذ في الحسبان أن ترتيب الأشهر في جافاسكربت يبدأ من 0 وليس من 1). ملء الخانات التي تسبق خانة اليوم الأول من الشهر (أي اليوم d.getDay()‎)، والتي تكون فارغة في اليومية، بإضافة عناصر <td></td> فارغة للجدول. الانتقال تصاعديا عبر الأيام باستعمال الدالّة d.setDate(d.getDate()+1)‎ وإضافة الخانات <td> لليومية مادامت قيمة d.getMonth()‎ تشير إلى الشهر نفسه (لم نصل بعد للشهر الموالي). وإذا كان اليوم يوم أحدٍ نضيف سطرًا جديدًا <tr></tr>. إذا وصلنا إلى نهاية الشهر، ومازالت في السطر خانات فارغة، نملؤها بعناصر <td></td> فارغة لنجعل الجدول مستطيلا. يمكنك الإطلاع على الحل من هنا إنشاء ساعة ملونة باستعمال الدالّة setInterval الأهمية: 4 أنشئ ساعة ملونة تشبه الساعة في الشكل التالي: استعمل 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. الحل الحل عبارة عن بضعة سطور فقط، ولكننا نحتاج إلى شرح الحيل التي اُستعمِلت فيه. ويكون باتباع خطوات الخوارزمية التالية: استخراج سطور الجدول من العنصر tbody. ترتيبها حسب محتوى العمود الأول name ترتيبًا أبجديًا. إضافة العُقد حسب الترتيب الجديد باستعمال append(sortedRows)‎. لستَ مضطرًا لحذف العناصر<tr>، عليك فقط إعادة إضافتها حيث تنتقل آليًا من مواضعها الأصلية إلى المواضع الجديدة بعد ترتيبها. ملاحظة: العنصر tbody من الجدول مذكورٌ صراحةً في شيفرة HTML ولكن حتى وإن لم يُذكر صراحةً، فهو دائمًا موجود في بنية DOM. يمكنك الإطلاع على الحل من هنا ترجمة -وبتصرف- للفصل Modifying the document من كتاب Browser: Document, Events, Interfaces
  6. عندما يُحمِّل المتصفّح الصّفحة، فهو يقرأ/يحلّل شيفرة HTML ويُنشئ من خلالها كائنات DOM. وحين يتعلّق الأمر بالعُقد العناصرية (element nodes)، فإن غالبية سمات HTML المعيارية تصبح خاصيات للكائنات DOM. فعلى سبيل المثال، إذا كانت السمة كالتالي: <"body id="page>، يكتسب كائن DOM الخاصية "body.id="page. لكن عملية الربط بين السمات والخاصيات لا تتم تقابليا (كلّ سمة لا يقابلها بالضرورة خاصية). فيما يلي ستتعرف على كيفية التمييز بين الْمفهومَيْن، لتتعلم طريقة استخدامهما حين يتطابقان وحين يختلفان. خاصيات DOM سبق وأن تعرفت على خاصيات نموذج تمثيل المستند ككائن (أي خاصيات DOM) المبنيّة مسبقًا. هناك العديد منها ولكن ذلك لا يمنع المبرمج من إضافة/إنشاء خاصيات أخرى إذا لم تكن الخاصيات المبنيّة مسبقًا كافية لتلبية حاجاته. تُعدّ عُقد DOM كائنات جافاسكربت عادية يمكنك التعديل عليها. فلننشئ على سبيل المثال خاصيّة جديدة للكائن document.body: document.body.myData = { name: 'Caesar', title: 'Imperator' }; alert(document.body.myData.title); // Imperator كما يمكن إضافة دالّة أيضًا: document.body.sayTagName = function() { alert(this.tagName); }; document.body.sayTagName(); // BODY (قيمة this في الدالة هي document.body) See the Pen JS-p2-06-Attributes and properties-ex2 by Hsoub (@Hsoub) on CodePen. يمكن أيضًا تعديل النماذج المبنيّة مسبقًا مثل Element.prototype وإضافة دوالّ جديدة تُطبّق على كلّ العناصر. Element.prototype.sayHi = function() { alert(`Hello, I'm ${this.tagName}`); }; document.documentElement.sayHi(); // Hello, I'm HTML document.body.sayHi(); // Hello, I'm BODY See the Pen JS-p2-06-Attributes and properties-ex3 by Hsoub (@Hsoub) on CodePen. تسلُك خاصيات ودوالّ DOM إذا سلوكًا مماثلًا لسلوك كائنات جافاسكربت العاديّة ويمكنها استقبال أيّ قيمةٍ كانت. فهي تخضع لشرط التمييز بين الحروف الكبيرة والحروف الصغيرة (فنكتب مثلا elem.nodeType، ولا نكتب elem.NodeType). سمات HTML يمكن أن تملك وسوم HTML سمات، ويتعرف المتصفّح على السمات المعيارية عندما يقرأ الوسوم ويحلّلها لإنشاء خاصيات لكائنات DOM المقابلة لها. وبذلك، عندما يكون للعنصر السمة id أوأيّ سمة معيارية أخرى، تُنشَأُ الخاصية الموافقة لها. ولكن ذلك لن يحدث إذا كانت الخاصية غير معيارية. انظر المثال التالي: <body id="test" something="non-standard"> <script> alert(document.body.id); // test // السمة غير المعيارية لا تولّد خاصية مقابلة لها alert(document.body.something); // undefined </script> </body> See the Pen JS-p2-06-Attributes and properties-ex4 by Hsoub (@Hsoub) on CodePen. لاحظ أن سمةً ما قد تكون معياريةً لعناصر دون غيرها وغير مُعرّفةٍ إذا تعلّق الأمر بعناصر أخرى. فمثلًا تُعدّ السمة "type" سمةً معياريةً للعنصر <input>‏ (HTMLInputElement) وغير معيارية للعنصر <body>‏ (HTMLBodyElement). وستجد وصفًا للسمات المعيارية في مواصفات صنف كل عنصر. والمثال التالي يوضّح ذلك: <body id="body" type="..."> <input id="input" type="text"> <script> alert(input.type); // text alert(body.type); // undefined لم تٌنشأ خاصية DOM لأنها غير معيارية // </script> </body> See the Pen JS-p2-06-Attributes and properties-ex5 by Hsoub (@Hsoub) on CodePen. وبالتالي إذا كانت السمة غير معيارية، لا تٌنشأ خاصية DOM الموافقة لها. فهل من طريقة تسمح بالوصول إلى هذه السمات؟ بالتأكيد، يمكن الوصول إلى جميع السمات باستعمال الدوالّ التالية: (elem.hasAttribute(name: للتحقّق من وجود السمة. (elem.getAttribute(name: للحصول على قيمة السمة. (elem.setAttribute(name, value: إسناد القيمة value للسمة name. (elem.removeAttribute(name: حذف السمة. وتُستخدم هذه الدوالّ مع السمات غير المعيارية كما هي مكتوبة بالضبط في شيفرة HTML. يمكن أيضًا قراءة كافة السمات باستعمال elem.attributes؛ وهو مجموعة من الكائنات التي تنتمي إلى صنف Attr المبني مسبقًا، تملك خاصيتي الاسم name والقيمة value. فيما يلي عرض لكيفية قراءة سمة غير معيارية: <body something="non-standard"> <script> alert(document.body.getAttribute('something')); // non-standard </script> </body> See the Pen JS-p2-06-Attributes and properties-ex6 by Hsoub (@Hsoub) on CodePen. تملك سمات HTML الميزات التالية: لا تخضع لشرط التمييز بين الحروف الصغيرة والحروف الكبيرة (id هو نفسه ID). تكون قيم هذه السمات دائما من نوع سلاسل نصّية (أي من النوع string). فيما يلي عرضٌ موسّعٌ يوضّح طريقة التعامل مع السمات: <body> <div id="elem" about="Elephant"></div> <script> alert( elem.getAttribute('About') ); // (1) 'Elephant', جلب elem.setAttribute('Test', 123); // (2), ضبط alert( elem.outerHTML ); // (3), التحقق من وجود السمة في شيفرة HTML (نعم) for (let attr of elem.attributes) { // (4) عرض القائمة كاملة alert( `${attr.name} = ${attr.value}` ); } </script> </body> See the Pen JS-p2-06-Attributes and properties-ex7 by Hsoub (@Hsoub) on CodePen. لاحظ ما يلي: (1).('getAttribute('About: الحرف الأول من كلمة About هو حرف كبير مع أنه صغير في شيفرة HTML. هذا لا يُسبّب أيّ مشكلةٍ طالما أن أسماء السمات لا تخضع لشرط التمييز بين الحروف الكبيرة والحروف الصغيرة. (2). يمكن إسناد قيمة من أيّ نوعٍ كان للسمة، ولكن هذه القيمة تصبح سلسلةً نصّيةً. قيمة السمة Test اذًا هنا هي "123". (3). تظهر كافة السمات بما فيها تلك التي أضفناها (أي غير المعيارية مثل تلك التي كانت موجودة في الشيفرة كالسمة 'About'، وتلك التي أضفناها كالسمة 'Test') في outerHTML. (4). تُعدّ مجموعة السمات attributes قابلة للتمرير ضمن حلقة تكرار، وتضمّ كافة سمات العنصر، المعيارية منها وغير المعيارية، في شكل كائنات تملك خاصيتي الاسم name والقيمة value. المزامنة (synchronization) بين الخاصية والسمة حين تتغيّر سمة معيارية ما، يجري تحديث الخاصية الموافقة لها آليًا والعكس كذلك صحيح (ما عدا في بعض الحالات الاستثنائية). عُدّلت في المثال الموالي السمة "id"، فلاحظ أن ذلك أدّى الى تعديل الخاصية أيضًا. وحدث الشّيء نفسه في المثال العكسي الذي جاء بعده، حيث عُدّلت الخاصية وهو ما أدّى إلى تعديل السمة آليًا. <input> <script> let input = document.querySelector('input'); // سمة => خاصّية input.setAttribute('id', 'id'); alert(input.id); // id (تحديث) // خاصية => سمة input.id = 'newId'; alert(input.getAttribute('id')); // newId (تحديث) </script> See the Pen JS-p2-06-Attributes and properties-ex8 by Hsoub (@Hsoub) on CodePen. غير أن هناك بعض الاستثناءات، فعلى سبيل المثال، لا يمكن مزامنة input.value سوى في اتجاه واحد، من السمة إلى الخاصية، وليس العكس. <input> <script> let input = document.querySelector('input'); // سمة ⇐ خاصية input.setAttribute('value', 'text'); alert(input.value); // text //خاصية ⇍ سمة input.value = 'newValue'; alert(input.getAttribute('value')); // text (لم يتم التحديث) </script> See the Pen JS-p2-06-Attributes and properties-ex9 by Hsoub (@Hsoub) on CodePen. في المثال أعلاه: يُؤدّي تعديل قيمة السمة value إلى تعديل الخاصية آليًا. ولكن التعديل على الخاصية مباشرةً لا يُؤدّي إلى أيّ أثر على السمة. هذه "الميزة" يمكن أن تكون فعلا مفيدة، حيث يمكن للمستخدم أن يغيّر القيمة value، وبعدها، إذا أردت استعادة القيمة الأولية من HTML، تجدها في السمة. خاصيات DOM لها أنواع لا تكون خاصيات DOM دائمًا عبارة عن سلاسل نصّية (أي من النوع "string")، فمثلا خاصية input.checked (في مربعات الاختيار (checkbox)) تكون عبارة عن قيمة بوليانية/منطقية. <input id="input" type="checkbox" checked> checkbox <script> alert(input.getAttribute('checked')); // قيمة السمة من نوع سلسلة نصّية فارغة alert(input.checked); // قيمة الخاصية هي “true” </script> See the Pen JS-p2-06-Attributes and properties-ex10 by Hsoub (@Hsoub) on CodePen. وهناك أمثلة أخرى؛ تكون السمة style عبارة عن سلسلة نصّية ولكن الخاصية style هي كائن. <div id="div" style="color:red;font-size:120%">Hello</div> <script> // سلسلة نصّية alert(div.getAttribute('style')); // color:red;font-size:120% // كائن alert(div.style); // [object CSSStyleDeclaration] alert(div.style.color); // red </script> See the Pen JS-p2-06-Attributes and properties-ex11 by Hsoub (@Hsoub) on CodePen. غير أن غالبية الخاصيات تكون من نوع سلاسل نصّية. وفي حالات نادرة جدًا، حتى وإن كانت خاصية DOM من نوع سلسلة نصّية، يمكن أن تختلف عن السمة. فمثلا، خاصية href في DOM تكون دائما عبارة عن عنوان URL مطلق كامل، حتى وإن تضمّنت السمة عنوان URL نسبي أو إشارةً إلى أحد العناصر في الصفحة عبر الصياغة hash#. وفيما يلي مثالٌ على ذلك. <a id="a" href="#hello">link</a> <script> // attribute alert(a.getAttribute('href')); // #hello // property alert(a.href ); // ‫ URL كامل على شكل http://site.com/page#hello </script> See the Pen JS-p2-06-Attributes and properties-ex12 by Hsoub (@Hsoub) on CodePen. إذا ما أردت الوصول إلى قيمة href، أو أيّ سمةٍ غيرها، كما هي مدّونة بالضبط في شيفرة HTML، استعمل الخاصية getAttribute. السمات غير المعيارية والخاصية dataset نستخدم الكثير من السمات المعيارية عند الكتابة بلغة جافاسكربت ولكن ماذا عن السمات غير المعيارية وهل هي مفيدة؟ فلنرى أولا إن كانت مفيدة أم لا وما هو دورها. تُستخدم أحيانا السمات غير المعيارية في تمرير البيانات من شيفرة HTML إلى جافاسكربت أو لـ : "وسم" عناصر HTML حتى تتعرّف عليها جافاسكربت. <!-- وسم العنصر div لإظهار الاسم هنا--> <div show-info="name"></div> <!-- والعمر هنا --> <div show-info="age"></div> <script> // تجد الشيفرة العنصر الموسوم وتقوم بإظهار المطلوب let user = { name: "Pete", age: 25 }; for(let div of document.querySelectorAll('[show-info]')) { // ملئ الحقل بالمعلومات الموافقة له let field = div.getAttribute('show-info'); div.innerHTML = user[field]; //(*) } </script> See the Pen JS-p2-06-Attributes and properties-ex13 by Hsoub (@Hsoub) on CodePen. (*): إظهارالقيمة "Pete" داخل العنصر div الموسوم بـ name وإظهار القيمة 25 داخل العنصر div الموسوم بـage. كما يمكن استعمالها لتنسيق عنصر ما. فمثلا، تُستخدم هنا السمة order-state للتعبير عن حالة الطلب (إن كان جديدًا، معلّقًا أو ملغى). <style> /* تعتمد الأنماط على السمة غير المعيارية 'order-state' */ .order[order-state="new"] { color: green; } .order[order-state="pending"] { color: blue; } .order[order-state="canceled"] { color: red; } </style> <div class="order" order-state="new"> A new order. </div> <div class="order" order-state="pending"> A pending order. </div> <div class="order" order-state="canceled"> A canceled order. </div> See the Pen JS-p2-06-Attributes and properties-ex14 by Hsoub (@Hsoub) on CodePen. لماذا يُستحسن استخدام السمة بدل استخدام الأصناف order-state-canceled. وorder-state-pending. و order-state-new.؟ ذلك لأنه يَسهُل استعمال الخاصيات موازنةً مع الأصناف حيث يمكن تعديل الحالة بمنتهى السهولة كما في المثال التالي: // أسهل من حذف/إضافة صنف جديد div.setAttribute('order-state', 'canceled'); ولكن السمات غير المعيارية قد تتسبّب في المشكل التالي: ماذا لو استُخدمت سمةٌ غير معياريةٍ لغرضٍ ما وبعدها اِستُحدِثت كسمةٍ معياريةٍ تقوم بوظيفةٍ ما؟ تُعد لغة HTML حيّةً، ومتطورةً، حيث تَستحدِث باستمرار سماتٍ جديدةٍ تلبّي حاجات المطوّرين. قد ينجرّ عن هذا الأمر انعكاساتٍ غير متوقعة. ولتفادي حصول مثل هذا التضارب وُجدت السمات *-data. كلّ السمات التي تبدأ بـ "-data" هي سمات محجوزة يستعملها المطوّرون فقط. ويمكن الوصول إليها عبر الخاصية dataset. فمثلا، إذا كانت السمة المسمّاة "data-about" تابعة للعنصر elem ، يمكن الوصول إليها باستعمال elem.dataset.about كالآتي: <body data-about="Elephants"> <script> alert(document.body.dataset.about); // Elephants </script> See the Pen JS-p2-06-Attributes and properties-ex15 by Hsoub (@Hsoub) on CodePen. تُصبح أسماء السمات التي تتكوّن من عدّة كلمات مثل data-order-state على الشكل: dataset.orderState، حيث تُكتب -عند استعمالها كخاصيات- بنمط سنام الجمل أي بجعل أول حرف من كلّ كلمة كبيرًا عدا الكلمة الأولى. نعيد فيما يلي كتابة المثال السابق (المثال الخاص بـ "حالة الطلب") بطريقة أخرى. <style> .order[data-order-state="new"] { color: green; } .order[data-order-state="pending"] { color: blue; } .order[data-order-state="canceled"] { color: red; } </style> <div id="order" class="order" data-order-state="new"> A new order. </div> <script> // قراءة alert(order.dataset.orderState); // new // تعديل order.dataset.orderState = "pending"; // (*) </script> See the Pen JS-p2-06-Attributes and properties-ex16 by Hsoub (@Hsoub) on CodePen. يُعدّ استعمال السمات التي تبدأ بـ *-data طريقةً فعالةً وآمنةً لتمرير البيانات غير المعيارية. لاحظ هنا أنه لا يمكنك قراءة السمات *-data فحسب، بل حتى تعديلها، ومن ثمّ تُطبَّق تنسيقات CSS بناءً عليها. في المثال أعلاه، غيّرت تعليمة السطر الأخير (الموسوم بالرمز (*)) اللّون ليصبح أزرقًا. الخلاصة السمة هي ما يدوّن على شيفرة HTML الخاصية هي ما يندرج ضمن الكائن DOM موازنة بسيطة الخاصيات السمات النوع أيّ قيمةٍ كانت، للخاصيات أنواعٌ موضّحةٌ في المواصفة التقنية سلسلة نصّية الاسم يخضع لشرط التمييز بين الحروف الكبيرة والحروف الصغيرة لا يخضع لشرط التمييز بين الحروف الكبيرة والحروف الصغيرة table { width: 100%; } thead { vertical-align: middle; text-align: center;} td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } دوالّ العمليات على السمات هي: (elem.hasAttribute(name: للتحقق من وجود السمة. (elem.getAttribute(name: للحصول على قيمة السمة. (elem.setAttribute(name, value: إسناد القيمة value للسمة name. (elem.removeAttribute(name: حذف السمة. elem.attributes: يمثّل مجموعة السمات الخاصة بالعنصر المحدد. ويُستحسن استعمال خاصيات DOM في أغلب الحالات، وعليك اللجوء إلى استعمال السمات فقط حين تحتاج إلى ذلك وإذا كان استعمال خاصيات DOM لا يفي بالغرض. مثلا: *تحتاج إلى سمةٍ غير معيارية، ولكن إن كانت تبدأ بـ -data عليك استعمالdataset. *تُريد قراءة القيمة كما هي مكتوبة بالضبط في شيفرة HTML. قد تكون قيمة الخاصية DOM مختلفةً عنها. فمثلا، تكون الخاصية href دائما عبارة عن عنوان URL مطلق كامل، وقد تحتاج أنت الوصول إلى القيمة الأصلية/الأولى. تمارين الوصول إلى سمةٍ ما درجة الأهمية: 5 اُكتب الشيفرة التي تسمح باختيار العنصر من المستند باستعمال السمة data-widget-name وقراءة قيمتها. <!DOCTYPE html> <html> <body> <div data-widget-name="menu">Choose the genre</div> <script> /*أكتب الشيفرة هنا */ </script> </body> </html> الحل <!DOCTYPE html> <html> <body> <div data-widget-name="menu">Choose the genre</div> <script> // الحصول عليه let elem = document.querySelector('[data-widget-name]'); // قراءة القيمة alert(elem.dataset.widgetName); // أو alert(elem.getAttribute('data-widget-name')); </script> </body> </html> تغيير لون الروابط الخارجية إلى البرتقالي درجة الأهمية: 3 غيّر لون جميع الروابط الخارجية إلى البرتقالي من خلال التعديل على خاصية style . يُعدّ الرابط خارجيًا إذا: كانت السمة href الخاصة به تضمّ السلسلة النصّية '//:' لم تكن تبدأ بـالسلسلة النصّية 'http://internal.com' مثال: <a name="list">the list</a> <ul> <li><a href="http://google.com">http://google.com</a></li> <li><a href="/tutorial">/tutorial.html</a></li> <li><a href="local/path">local/path</a></li> <li><a href="ftp://ftp.com/my.zip">ftp://ftp.com/my.zip</a></li> <li><a href="http://nodejs.org">http://nodejs.org</a></li> <li><a href="http://internal.com/test">http://internal.com/test</a></li> </ul> <script> // تنسيق رابط واحد let link = document.querySelector('a'); link.style.color = 'orange'; </script> ينبغي أن تكون النتيجة كالآتي: الحل عليك أولا إيجاد كلّ الروابط الخارجية. وهناك طريقتان: الأولى من خلال إيجاد كافة الروابط باستعمال ('document.querySelectorAll('a وبعدها أخذ ما تحتاج إليه فقط. let links = document.querySelectorAll('a'); for (let link of links) { let href = link.getAttribute('href'); if (!href) continue; // لا وجود للسمة if (!href.includes('://')) continue; // لا وجود للسلسلة النصية '://' if (href.startsWith('http://internal.com')) continue; // رابط داخلي link.style.color = 'orange'; } لاحظ هنا أنّنا نستعمل ('link.getAttribute('href وليس link.href لأنّنا نريد استخراج القيمة من شيفرة HTML . والطريقة الثانية، أبسط من الأولى، تكون بإضافة عمليات التحقّق لمحدّد CSS: البحث في href عن كلّ الروابط التي تضمّ السلسلة النصّية '//:' // // ولا تبدأ بـالسلسلة النصّية 'http://internal.com' let selector = 'a[href*="://"]:not([href^="http://internal.com"])'; let links = document.querySelectorAll(selector); links.forEach(link => link.style.color = 'orange'); ترجمة -وبتصرف- للفصل Attributes and properties من كتاب Browser: Document, Events, Interfaces