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

لوحة المتصدرين

  1. محمد أمين بوقرة

    • نقاط

      2

    • المساهمات

      33


  2. سارة محمد2

    سارة محمد2

    الأعضاء


    • نقاط

      2

    • المساهمات

      86


  3. عائشة مرابط

    عائشة مرابط

    الأعضاء


    • نقاط

      1

    • المساهمات

      6


المحتوى الأكثر حصولًا على سمعة جيدة

المحتوى الأعلى تقييمًا في 03/16/21 in مقالات البرمجة

  1. لا يمكننا فقط إسناد معالجات للأحداث من خلال جافاسكربت، ولكن يمكننا أيضا توليد أحداث مخصّصة. يُمكن استعمال الأحداث المخصّصة لإنشاء "مكوّنات رسوميّة". على سبيل المثال، يمكن للعنصر الجذر في قائمةٍ تعمل بواسطة جافاسكربت افتعال أحداثٍ تُنبئ بما يحصل مع القائمة: open (عند فتح القائمة)، select (عند تحديد عنصر) وهكذا. يمكن أن تنصت شيفرة أخرى لهذه الأحداث وتراقب ماذا يحصل مع القائمة. لا يمكننا فقط توليد أحداث جديدة كليّا، نخترعها لأغراضنا الخاصّة، ولكن يمكن أيضا توليد اﻷحداث المضمّنة، مثل click و mousedown إلى غير ذلك. قد يساعد ذلك عند إجراء الاختبارات الآليّة. باني الأحداث تشكّل أصناف الأحداث المضمّنة سُلّميّة مشابهة لسلّميّة أصناف عناصر DOM. يكون الجذر فيها هو الصنف المُضمّن Event. ويمكننا إنشاء كائنات منها بهذه الطريقة: let event = new Event(type[, options]); الوسائط: type -- نوع الحدث، ويكون سلسلة نصيّة مثل "click" أو إذا كان خاصّا بنا مثل "my-event". options -- كائن بخاصيتين اختياريتين: bubbles: true/false -- إذا كانت true، فإنّ الحدث ينتشر نحو الأعلى. cancelable: true/false: إذا كانت true، فمن الممكن منع "الفعل الافتراضي". سنرى لاحقًا ما يعني ذلك للأحداث المخصّصة. تكون قيمة كلتيهما false افتراضيّا: {bubbles: false, cancelable: false}. dispatchEvent بعد إنشاء كائن الحدث، نستطيع أن "نجريه" على عنصرٍ ما بواسطة الاستدعاء elem.dispatchEvent(event)‎. تستجيب المعالجات له حينها كما لو كان حدث متصفّح عادي. وإذا كان الحدث قد أنشئ وله الراية bubbles، فإنّه ينتشر نحو الأعلى. ابتُدِئ الحدث click في المثال أدناه من خلال جافاسكربت. يعمل المعالج بنفس الطريقة كما لو أن الزرّ قد نُقر بالفعل: <button id="elem" onclick="alert('Click!');">Autoclick</button> <script> let event = new Event("click"); elem.dispatchEvent(event); </script> See the Pen JS-p2-dispatch-events -ex1 by Hsoub (@Hsoub) on CodePen. ملاحظة: event.isTrusted هناك طريقة لمعرفة إذا كان الحدث "حقيقيّا" من المستخدم أو مولّدًا بواسطة سكربت. تكون قيمة الخاصيّة event.isTrusted هي true للأحداث التي تأتي من أفعال حقيقيّة للمستخدم و تكون false للأحداث المولّدة بواسطة سكربت. مثال عن الانتشار نحو الأعلى يمكننا إنشاء حدث منتشر نحو الأعلى باسم "hello" والتقاطه في document. كلّ ما نحتاجه هو إعطاء bubbles القيمة true: <h1 id="elem">Hello from the script!</h1> <script> // ... document الالتقاط على مستوى document.addEventListener("hello", function(event) { // (1) alert("Hello from " + event.target.tagName); // Hello from H1 }); // elem الإرسال من ... let event = new Event("hello", {bubbles: true}); // (2) elem.dispatchEvent(event); // و يظهر الرسالة document سيشتغل المعالج المسند إلى </script> See the Pen JS-p2-dispatch-events -ex2 by Hsoub (@Hsoub) on CodePen. ملاحظات: يجب أن نستخدم addEventListener لأحداثنا المخصصة، لأن on<event>‎ توجد فقط للأحداث المضمّنة، فلا تعمل document.onhello مثلا. يجب وضع bubbles:true، وإلا فلن ينتشر الحدث نحو الأعلى. آلية الانتشار نحو الأعلى هي نفسها للأحداث المُضمّنة (click) والمخصصة (hello). هناك أيضا مرحلتا الانتشار نحو الأعلى والانتشار نحو الأسفل. MouseEvent و KeyboardEvent وغيرهما هذه قائمة قصيرة لأصناف أحداث واجهة المستخدم مأخوذة من مواصفة أحداث واجهة المستخدم: UIEvent FocusEvent MouseEvent WheelEvent KeyboardEvent … ينبغي أن نستخدمها عوضا عن new Event إذا أردنا إنشاء هذه الأحداث. على سبيل المثال، new MouseEvent("click")‎. يمكّن الباني المناسب من تحديد خاصيّات قياسية تتعلّق بنوع الحدث ذاك. مثل clientX/clientY لأحداث المؤشر: let event = new MouseEvent("click", { bubbles: true, cancelable: true, clientX: 100, clientY: 100 }); alert(event.clientX); // 100 يرجى التنبه: لا يتيح الباني العام Event ذلك. لنجرّب: let event = new Event("click", { bubbles: true, // cancelable و bubbles فقط cancelable: true, // Event تعملان في الباني clientX: 100, clientY: 100 }); alert(event.clientX); // تُهمل الخاصّية غير المعروفة ،undefined في الحقيقة، يمكننا الاحتيال على ذلك بإسناد event.clientX=100 مباشرة بعد إنشائه. فيؤول الأمر إلى المناسبة والالتزام بالقواعد. يكون نوع الأحداث التي يولّدها المتصفّح صحيحًا على الدوام. توجد القائمة الشاملة لمختلف أحداث واجهة المستخدم في المواصفة، على سبيل المثال، MouseEvent. الأحداث المخصصة بالنسبة لأنواع الأحداث الخاصّة بنا والجديدة كليّا مثل "hello" علينا أن نستخدم new CustomEvent. فنيّا، CustomEvent هي نفس Event لكن مع استثناء وحيد. في الوسيط الثاني (object) يمكننا إضافة خاصيّة أخرى detail من أجل أيّة معلومات مخصصّة نودّ تمريرها مع الحدث. على سبيل المثال: <h1 id="elem">Hello for John!</h1> <script> // تأتي المزيد من التفاصيل مع الحدث إلى المعالج elem.addEventListener("hello", function(event) { alert(event.detail.name); }); elem.dispatchEvent(new CustomEvent("hello", { detail: { name: "John" } })); </script> See the Pen JS-p2-dispatch-events -ex3 by Hsoub (@Hsoub) on CodePen. يمكن أن تحوي هذه الخاصيّة أي معطيات. في الحقيقة، من الممكن أن نعمل بدونها، لأننا نستطيع أن نسند أيّ خاصيّة إلى كائن new Event عاديّ بعد إنشائه. لكن CustomEvent تزوّده بحقل detail الخاص لتفادي التعارض مع خاصيّات الحدث الأخرى. إلى جانب ذلك، يبيّن صنف الحدث "أيّ نوع حدث" هو، وإذا كان الحدث مخصّصا، فينبغي استخدام CustomEvent لنكون فقط واضحين بخصوصه. ()event.preventDefault لدى العديد من أحداث المتصفّح "أفعال افتراضيّة"، كالانتقال إلى رابط، بداية تحديد، وما إلى ذلك. أمّا الأحداث المخصّصة الجديدة، فليس لديها بالطبع أيّة أفعال افتراضيًّة، لكن قد تكون للشيفرة التي ترسل مثل هذه الأحداث خُططًا خاصّة تقوم بها بعد افتعال الحدث. باستدعاء event.preventDefault()‎ ، يمكن لمعالج الحدث أن يرسل إشارة بإنّ تلك الأفعال المخطّط لها يجب أن تُلغى. في تلك الحالة يعيد الاستدعاء elem.dispatchEvent(event)‎ القيمة false. وتعلم الشيفرةُ التي أرسلت الحدث بأنّ عليها ألا تكمل. لنرى مثالًا تطبيقيّا لذلك -- أرنب متخفّي (قد يكون قائمة أيضا منغلقة أو أيّ شيئ آخر). يمكن أن ترى في الأسفل الأرنب rabbit# وعليه الدالة hide()‎ التي ترسل الحدث "hide"، لتعلم جميع الأطراف المعنيّة بأنّ الأرنب سيختفي. يمكن لأي معالج أن يستمع لذلك الحدث بواسطة rabbit.addEventListener('hide',...)‎ ، وإن تتطلّب الأمر، يمكنه إلغاء الفعل باستخدام event.preventDefault()‎. فعندها لن يختفي الأرنب: <pre id="rabbit"> |\ /| \|_|/ /. .\ =\_Y_/= {>o<} </pre> <button onclick="hide()">Hide()</button> <script> function hide() { let event = new CustomEvent("hide", { cancelable: true // preventDefault بدون تلك الراية لا يعمل }); if (!rabbit.dispatchEvent(event)) { alert('The action was prevented by a handler'); } else { rabbit.hidden = true; } } rabbit.addEventListener('hide', function(event) { if (confirm("Call preventDefault?")) { event.preventDefault(); } }); </script> See the Pen JS-p2-dispatch-events -ex4 by Hsoub (@Hsoub) on CodePen. يرجى التنبه هنا: يجب أن يكون للحدث الراية cancelable: true، وإلّا فسيُهمل event.preventDefault()‎ . تعمل الأحداث التي داخل أحداث أخرى بشكل متزامن تُعالَج الأحداث عادةً على شكل طابور. بعبارة أخرى، إذا كان المتصفّح يقوم بمعالجة onclick ووقع حدث جديد، كتحريك الفأرة مثلًا، فإن معالجته تُصفّ في طابور الانتظار، وتُستدعى المعالجات المتعلّقة بـ mousemove بعد الانتهاء من معالجة onclick. الاستثناء الجدير بالانتباه هنا هو عندما ينشأ حدث من داخل حدث آخر، بواسطة dispatchEvent مثلا. تُعالج تلك الأحداث فورًا: تُستدعى معالجات الحدث الجديد ثم تستمر معالجة الحدث الحالية. على سبيل المثال، في الشيفرة أدناه يُفتعل الحدث menu-open خلال الحدث onclick. ويُعالَج فورًا دون انتظار معالج onclick من الانتهاء: <button id="menu">Menu (click me)</button> <script> menu.onclick = function() { alert(1); menu.dispatchEvent(new CustomEvent("menu-open", { bubbles: true })); alert(2); }; // يفتعل بين 1 و 2 document.addEventListener('menu-open', () => alert('nested')); </script> See the Pen JS-p2-dispatch-events -ex5 by Hsoub (@Hsoub) on CodePen. يكون ترتيب المخرجات كما يلي: 1 -› الحدث الداخلي -› 2. يُرجى التنبّه إلى أنّ الحدث الداخليّ menu-open قد تم ألتقاطه على مستوى document. يتمّ انتشار الحدث الداخلي ومعالجته قبل عودة المعالجة إلى الشيفرة الخارجية (onclick). لا يخصّ هذا فقط dispatchEvent، بل هناك حالات أخرى. إذا استدعى معالجٌ توابعَ تفتعل أحداثًا أخرى، فإنّها تُعالج تزامنيًّا أيضًا، بشكلٍ متداخل. لكن لنفترض أنّنا لا نريد ذلك. نريد أن تتمّ معالجة onclick أوّلًا، باستقلالٍ عن menu-open أو غيره من اﻷحداث المتداخلة. يمكننا عندها إمّا أن نضع dispatchEvent (أو نداءَ افتعال أحداثٍ آخر) في آخر onclick، أو ربّما أفضل، أن نلفّه بـ setTimeout منعدمة التأخير: <button id="menu">Menu (click me)</button> <script> menu.onclick = function() { alert(1); setTimeout(() => menu.dispatchEvent(new CustomEvent("menu-open", { bubbles: true }))); alert(2); }; document.addEventListener('menu-open', () => alert('nested')); </script> See the Pen JS-p2-dispatch-events -ex6 by Hsoub (@Hsoub) on CodePen. تعمل الآن dispatchEvent لا تزامنيًّا بعد الانتهاء من تنفيذ الشيفرة الحاليّة، بما في ذلك menu.onclick، وتصير بذلك معالجات الأحداث منفصلة تمامًا. ويصير ترتيب المخرجات كالتالي: 1 -> 2 -> الحدث الداخلي. الملخص لتوليد حدثٍ من خلال الشيفرة، نحتاج أوّلًا أن ننشئ كائن حدث. يقبل الباني العام Event(name, options)‎ اسمًا للحدث والكائن options مع الخاصّيّتين: bubbles: true إذا كان يجب أن ينتشر الحدث نحو اﻷعلى. cancelable: true إذا كان يجب أن يعمل event.preventDefault()‎. يقبل بانو الأحداث اﻷصليّة الآخرون مثل MouseEvent و KeyboardEvent و ما إلى ذلك، خاصّيّات مختصّة بنوع الحدث ذلك. على سبيل المثال، clientX لأحداث المؤشر. ينبغي أن نستخدم الباني CustomEvent للأحداث المخصّصة. فلديه خيار إضافيّ اسمه detail، يمكن أن نسند إليه المعطيات المختصّة بالحدث. وبذلك يمكن لجميع المعالجات الوصول إليها بواسطة event.detail. بالرغم من الإمكانيّة التقنيّة لتوليد أحداث المتصفّح مثل click أو keydown، فينبغي استخدامها بحذرٍ شديد. لا ينبغي أن نعمد إلى توليد أحداث المتصفّح، إذ يعدّ ذلك طريقة مبتذلة لتشغيل المعالجات. وهي هندسة سيّئة في أغلب اﻷحيان. يمكن أن تُولّد اﻷحداث اﻷصليّة: كطريقة مبتذلة لجعل مكتبات الطرف الثالث تعمل كما يجب، إذا لم تكن توفّر وسائل أخرى للتفاعل معها. عند إجراء الاختبارات الآليّة، مثل "النقر على الزرّ" من خلال السكربت لمعرفة ما إذا كانت الواجهة تستجيب بشكل صحيح. تُولَّد الأحداث المخصّصة بأسمائنا الخاصّة لأغراض هندسيّة في الغالب، للإشارة إلى ما يحصل داخل القوائم و الدوّارات وما إلى ذلك. ترجمة -وبتصرف- للمقال Dispatching custom events من سلسلة Browser: Document, Events, Interfaces لصاحبها Ilya Kantor
    1 نقطة
  2. لنتعمّق في المزيد من التفاصيل حول اﻷحداث التي تقع عندما تتحرّك الفأرة بين العناصر. اﻷحداث mouseover/mouseout و relatedTarget يقع الحدث mouseover عندما يأتي مؤشّر الفأرة إلى عنصر ما، ويقع mouseout عندما يغادره. تتميّز هذه اﻷحداث بأنّ لديها الخاصيّة relatedTarget. تُكمِّل هذه الخاصيّةُ target. عندما تغادر الفأرة عنصرا ما نحو عنصر آخر، يصير إحدى هذه العناصر target، ويصير الآخر relatedTarget. بالنسبة للحدث mouseover: event.target هو العنصر الذي أتت إليه الفأرة. event.relatedTarget هو العنصر الذي أتت منه الفأرة (relatedTarget -> ‏target). والعكس بالنسبة للحدث mouseout: event.target هو العنصر الذي غادرته الفأرة. event.relatedTarget هو العنصر الذي غادرت نحوه الفأرة، وصار تحت المؤشّر (target ->‏ relatedTarget) في المثال التالي (يمكن مشاهدته من هنا)، يشكّل كلُّ وجهٍ وتقاسيمُه عناصر منفصلة. عند تحريك الفأرة، يمكنك في المساحة النصيّة أسفله مشاهدة الأحداث التي تقع. يرافق كلَّ حدث المعلوماتُ المتعلّقة بكلٍّ من target وrelatedTarget. See the Pen JS-p2-mousemove-mouseover-mouseout-mouseenter-mouseleave-ex01 by Hsoub (@Hsoub) on CodePen. تنبيه: يمكن أن يأخذ relatedTarget القيمة null يمكن للخاصيّة relatedTarget أن تكون null. هذا أمر عاديّ ويعني فقط أنّ الفأرة لم تأتِ من عنصر آخر، ولكن من خارج النافذة. أو أنّها قد غادرت النافذة. تخطي العناصر يقع الحدث mousemove عندما تتحرّك الفأرة. ولكنّ ذلك لا يعني أنّ كلَّ بكسل يؤدّي إلى حدث. يرصد المتصفّح الفأرة من وقت لآخر. وإذا لاحظ تغيّرات، فإنّه يعمل على وقوع اﻷحداث. هذا يعني لو حرّك المستخدم الفأرة بسرعة كبيرة فإنّ بعض عناصر DOM قد تُتخطّى: إذا تحرّكت الفأرة بسرعة كبيرة من العنصر ‎#FROM إلي العنصر ‎#TO كما هو مبيّن أعلاه، فإنّ عناصر <div> التي بين هذين العنصرين (أو بعضها) قد تُتخطّى. قد يقع الحدث mouseout على ‎#FROM ثم مباشرة على ‎#TO. يساعد هذا على تحسين اﻷداء، لأنّه قد تكون هناك الكثير من العناصر البينيّة، ولا نريد حقًّا معالجة الدخول والخروج في كلٍّ منها. في المقابل، يجب أن ندرك دائما أن مؤشّر الفأرة لا "يزور" جميع العناصر في طريقه، بل قد "يقفز". فمثلا، من الممكن أن يقفز المؤشّر إلى وسط الصفحة مباشرة قادما من خارج الصفحة. في هذه الحالة، تكون قيمة relatedTarget هي null، لأنّه قد أتى من "لا مكان": يمكنك رؤية ذلك "حيًّا" في المثال التالي أو من منصّة الاختبار التي من هنا: See the Pen JS-p2-mousemove-mouseover-mouseout-mouseenter-mouseleave-ex02 by Hsoub (@Hsoub) on CodePen. يوجد هناك في HTML عنصران متداخلان: العنصر <div id="child"‎> موجود داخل العنصر <div id="parent"‎>. إذا حرّكتَ الفأرة بسرعة فوقهما، فربّما يقع الحدث على العنصر div الابن فقط، أو ربّما على اﻷب، أو ربّما لن تكون هناك أحداث مطلقا. قم أيضا بتحريك المؤشّر إلى داخل العنصر div الابن، ثم حرّكه بسرعة نحو اﻷسفل عبر العنصر اﻷب. إذا كانت الحركة سريعة كفاية، فسيُتجاهل العنصر اﻷب. ستعبر الفأرة العنصر اﻷب دون الانتباه له. ملاحظة: إذا وقع mouseover، فلابدّ أن يكون هناك mouseout في حالة تحرّك الفأرة بسرعة، فقد تُتجاهل العناصر البينيّة، لكن هناك شيء مؤكّد: إذا دخل المؤشّر "رسميًّا" إلى عنصر ما (تولًّد الحدثُ mouseover)، فعند مغادرته إيّاه سنحصل دائما على mouseout. Mouseout عند المغادرة نحو عنصر ابني من الميزات المهمّة للحدث mouseout هي أنّه يقع عندما يتحّرك المؤشّر من عنصرٍ ما إلى عناصره السليلة، كأن يتحرّك من ‎#parent إلى ‎#child في شيفرة HTML هذه: <div id="parent"> <div id="child">...</div> </div> فلو كنّا في ‎#parent ثم حرّكنا المؤشّر إلى داخل ‎#child، فسنتحصّل على mouseout في ‎#parent! قد يبدو هذا غريبا، لكن يمكن تفسيره بسهولة. حسب منطق المتصفّح، يمكن للمؤشّر أن يكون فوق عنصر واحد فقط في نفس الوقت، وهو العنصر الذي يأتي في اﻷسفل وفق ترتيب القيمة z-index. فإذا ذهب المؤشّر نحو عنصر آخر (ولو كان سليلًا)، فإنّه يغادر العنصر السابق. يُرجى التنبّه إلى جرئيّة مهمّة أخرى في معالجة اﻷحداث. ينتشر الحدث mouseover الذي يقع في عنصر سليل نحو اﻷعلى. فإذا كان للعنصر ‎#parent معالج للحدث mouseover فإنّه سيشتغل: يمكنك رؤية ذلك جيّدا في المثال الذي من هنا. العنصر <div id="child"‎> هو داخل العنصر <div id="parent"‎>. هناك معالجات لـ mouseover/out مسندة إلى العنصر ‎#parent تعمل على إخراج تفاصيل الحدث. فإذا حرّكتَ الفأرةَ من ‎#parent نحو ‎#child، ستلاحظ وقوع حدثين في ‎#parent: mouseout [target: parent]‎ (مغادرة المؤشّر للعنصر اﻷب)، ثم mouseover [target: child]‎ (مجيء المؤشّر إلى العنصر الابن، انتشر هذا الحدث نحو اﻷعلى). See the Pen JS-p2-mousemove-mouseover-mouseout-mouseenter-mouseleave-ex03 by Hsoub (@Hsoub) on CodePen. كما هو ظاهر، عندما يتحرّك المؤشّر من العنصر ‎#parent نحو العنصر ‎#child، يشتغل معالجان في العنصر اﻷب: mouseout وmouseover: parent.onmouseout = function(event) { /* العنصر اﻷب :event.target */ }; parent.onmouseover = function(event) { /* العنصر الابن (انتشر نحو اﻷعلى)‏ :event.target */ }; إذا لم نفحص event.target داخل المعالجات، فقد يبدو اﻷمر وكأنّ مؤشّر الفأرة قد غادر العنصر `‎#parent ثم عاد إليه مباشرة. لكن ليس اﻷمر كذلك! لا يزال المؤشر فوق العنصر اﻷب، وقد تحرّك فقط إلى داخل العنصر الابن. إذا كانت هناك أفعال تحصل بمغادرة العنصر اﻷبويّ مثل إجراء حركات في parent.onmouseout، فلا نودّ حصولها عادة بمجرّد انتقال المؤشّر في داخل ‎#parent. لتجنّب ذلك، يمكننا تفحّص relatedTarget في المعالج، وإذا كان المؤشّر لا يزال داخل العنصر، فإنّنا نتجاهل الحدث. أو بدلًا عن ذلك، يمكننا استخدام اﻷحداث mouseenter وmouseleave التي سنتناولها الآن، إذ ليس لها مثل هذه المشاكل. اﻷحداث mouseenter وmouseleave الأحداث mouseenter/mouseleave مثل الأحداث mouseover/mouseout، فهي تقع عندما يدخل/يغادر مؤشّر الفأرة العنصر. لكن هناك فرقان مهمّان: لا تُحتسب الانتقالات التي تحدث داخل العنصر، من وإلى عناصره السليلة. لا تنتشر الأحداث mouseenter/mouseleave نحو الأعلى. هذه الأحداث بسيطة للغاية. عندما يدخل المؤشّر العنصر، يقع mouseenter، ولا يهمّ مكانه داخل العنصر أو داخل عناصره السليلة بالضبط. ثمّ عندما يغادر المؤشّر العنصر، يقع mouseleave. يشبه المثال الذي يمكن مشاهدته من هنا المثال أعلاه، لكن العنصر العلويّ الآن لديه mouseenter/mouseleave بدل mouseover/mouseout. مثلما يمكن أن ترى، الأحداث التي تولّدت هي الأحداث الناتجة عن تحريك المؤشّر إلى داخل وخارج العنصر العلويّ فقط. لا شيء يحدث عندما يذهب المؤشر إلى العنصر الابن ويرجع. تُتجاهل الانتقالات بين العناصر السليلة. تفويض الأحداث الأحداث mouseenter/leave بسيطة للغاية وسهلة الاستخدام. لكنّها لا تنتشر نحو الأعلى. بالتالي لا يمكننا استخدام تفويض الأحداث معها. تصوّر أنّنا نريد معالجة دخول/مغادرة الفأرة لخانات جدول ما. وهناك المئات من الخانات. بكون الحلّ الطبيعيّ عادةً بإسناد المعالج إلى <table> ومعالجة الأحداث هناك. لكنّ mouseenter/leave لا تنتشر نحو الأعلى. فإذا وقعت الأحداث في <td>، فلا يمكن التقاطها إلّا بواسطة معالج مُسنَد إلى ذلك العنصر <td>. تشتغل المعالجات المسندة إلى <table> فقط عندما يدخل/يغادر المؤشّر الجدول ككلّ. يستحيل الحصول على أيّة معلومات حول الانتقالات داخله. لذا، فلنستخدم mouseover/mouseout. لنبدأ بمعالجات بسيطة تعمل على إبراز العنصر الذي تحت المؤشّر. // لنبرز العنصر الذي تحت المؤشّر table.onmouseover = function(event) { let target = event.target; target.style.background = 'pink'; }; table.onmouseout = function(event) { let target = event.target; target.style.background = ''; }; يمكنك مشاهدتها تعمل من هنا. كلّما تنقّلت الفأرة عبر العناصر في هذا الجدول، سيكون العنصر الحالي بارزا على الدوام: See the Pen JS-p2-mousemove-mouseover-mouseout-mouseenter-mouseleave-ex04 by Hsoub (@Hsoub) on CodePen. في حالتنا هذه نودّ أن نعالج الانتقالات بين خانات الجدول <td>، أي عند دخول خانة أو مغادرتها. لا تهمّنا الانتقالات الأخرى كالتي تحصل داخل الخانة أو خارج كلّ الخانات. لنُنحّيها جانبا. هذا ما يمكننا فعله: حفظ العنصر <td> المُبرَز حاليّا في متغيّر، ولنسمّيه currentElem. تجاهل الحدث mouseover عند وقوعه في داخل العنصر <td> الحالي. هذا مثال لشيفرة تأخذ جميع الحالات الممكنة في الحسبان: // التي تحت الفأرة الآن (إن وُجدت)‏ <td> let currentElem = null; table.onmouseover = function(event) { // قبل دخول عنصر جديد، تغادر الفأرة دائما العنصر السابق // السابق <td> نفسه، فإنّنا لم نغادر currentElem إذا بقي العنصر // بداخله، تجاهل الحدث mouseover هذا مجرّد if (currentElem) return; let target = event.target.closest('td'); // تجاهل - <td> تحرّكنا إلى غير if (!target) return; // لكن خارج جدولنا (يمكن ذلك في حالة الجداول المتداخلة)‏ <td> تحرّكنا داخل // تجاهل if (!table.contains(target)) return; // جديد <td> مرحا! دخلنا currentElem = target; onEnter(currentElem); }; table.onmouseout = function(event) { // الآن، تجاهل الحدث <td> إذا كنّا خارج أيّ // <td> يُحتمل أنّ هذا تحرّك داخل الجدول، ولكن خارج // آخر <tr> إلى <tr> مثلا من if (!currentElem) return; // نحن بصدد مغادرة العنصر – إلى أين؟ ربمّا إلى عنصر سليل؟ let relatedTarget = event.relatedTarget; while (relatedTarget) { // currentElem اصعد مع سلسلة الآباء وتحقّق إذا كنّا لا نزال داخل // فهو إذًا انتقال داخليّ – تجاهله if (relatedTarget == currentElem) return; relatedTarget = relatedTarget.parentNode; } // .حقًّا .<td> لقد غادرنا العنصر onLeave(currentElem); currentElem = null; }; // أيّة دالّة لمعالجة دخول/مغادرة العنصر function onEnter(elem) { elem.style.background = 'pink'; // أظهر ذلك في المساحة النصيّة text.value += `over -> ${currentElem.tagName}.${currentElem.className}\n`; text.scrollTop = 1e6; } function onLeave(elem) { elem.style.background = ''; // أظهر ذلك في المساحة النصيّة text.value += `out <- ${elem.tagName}.${elem.className}\n`; text.scrollTop = 1e6; } مرّة أخرى، أهمّ ما يميّز هذه الطريقة هو التالي: تستخدم تفويض الأحداث لمعالجة دخول/مغادرة أيّ <td> داخل الجدول. فهي تعتمد على mouseover/out بدل mouseenter/leave التي لا تنتشر نحو الأعلى وبذلك لا تسمح بتفويض الأحداث. تُنحّى الأحداث الأخرى، مثل التنقّل بين العناصر السليلة لـ <td>، جانبًا لكيلا تشتغل المعالجات onEnter/Leave إلّا عندما يدخل أو يغادر المؤشّر <td> ككلّ. يمكنك مشاهدة المثال كاملا بجميع التفاصيل من هنا: See the Pen JS-p2-mousemove-mouseover-mouseout-mouseenter-mouseleave-ex05 by Hsoub (@Hsoub) on CodePen. جرّب تحريك المؤشّر إلى داخل وخارج خانات الجدول وكذلك بداخلها. بسرعة أو ببطء، لا يهمّ ذلك. لا تُبرَز إلّا <td> ككلّ، بخلاف المثال السابق. الملخص تناولنا الأحداث mouseover وmouseout وmousemove وmouseenter وmouseleave. من الجيّد التنبّه لهذه الأمور: قد تتخطّى الحركةُ السريعة للفأرة العناصر البينيّة. تملك الأحداث mouseover/out وmouseenter/leave الخاصيّة الإضافيّة relatedTarget، وهي تمثّل العنصر الذي أتينا منه/إليه، وتكمّل الخاصيّة target. تقع الأحداث mouseover/out حتى لو ذهبنا من عنصر أبويّ إلى عنصر ابنيّ. يَفترِض المتصفّحُ أنّه يمكن للفأرة أن تكون فوق عنصر واحد فقط في نفس الوقت، وهو العنصر الذي في الأسفل. تختلف الأحداث mouseenter/leave في هذا الجانب، فهي تقع فقط عندما تأتي الفأرة من/إلى العنصر ككلّ. إضافة إلى ذلك، هي لا تنتشر نحو الأعلى. التمارين سلوك التلميحة المحسَّن الأهميّة: 5 اكتب شيفرة جافاسكربت لإظهار تلميحة فوق العنصر الذي له السمة data-tooltip. يجب أن تصير قيمة هذه السمة نصّ التلميحة. يشبه هذا التمرين سلوك التلميحة، لكن يمكن هنا أن تكون العناصر الموسّمة متداخلة، ويحب حينها أن تُعرض التلميحة التي تكون في الأسفل. ويمكن أن تظهر تلميحة واحدة فقط في نفس الوقت. على سبيل المثال: <div data-tooltip="Here – is the house interior" id="house"> <div data-tooltip="Here – is the roof" id="roof"></div> ... <a href="https://en.wikipedia.org/wiki/The_Three_Little_Pigs" data-tooltip="Read on…">Hover over me</a> </div> يمكن مشاهدة النتيجة من هنا. أنجز التمرين في البيئة التجريبية الحل افتح الحل في البيئة التجريبية تلميحة "ذكيّة" الأهمية: 5 اكتب دالّة لإظهار تلميحة فوق عنصر ما فقط عندما يحرّك المستخدم الفأرة إلى داخل العنصر وليس عبره. بعبارة أخرى، إذا حرّك المستخدم الفأرة إلى عنصر ما وتوقّفت هناك، أظهر التلميحة. وإذا حرّكها مرورا به فقط، فلا داعي لذلك، إذ لا أحد يريد ومضات زائدة. فنيّا، يمكننا قياس سرعة الفأرة فوق العنصر، فإذا كانت بطيئة يمككنا افتراض أنّها أتت "فوق العنصر" وإظهار التلميحة. وإذا كانت سريعة تجاهلناها. أنشئ كائنا عموميّا new HoverIntent(options)‎ لهذا الغرض، له الخيارات options التالية: elem هو العنصر الذي نتتبّعه. over هي الدالّة التي نستدعيها إذا أتت الفأرة إلى العنصر: أي إذا تحرّكت ببطء أو توقّفت فوقه. out هي الدالّة التي نستدعيها إذا غادرت الفأرة العنصر (في حال استُدعيت over). هذا مثال لاستخدام هذا الكائن للتلميحة: // تلميحة تجريبيّة let tooltip = document.createElement('div'); tooltip.className = "tooltip"; tooltip.innerHTML = "Tooltip"; // over/out سيتتبّع الكائن الفأرة ويستدعي new HoverIntent({ elem, over() { tooltip.style.left = elem.getBoundingClientRect().left + 'px'; tooltip.style.top = elem.getBoundingClientRect().bottom + 5 + 'px'; document.body.append(tooltip); }, out() { tooltip.remove(); } }); يمكن مشاهدة النتيجة من هنا. إذا حرّكت الفأرة فوق "الساعة" بسرعة فلن يحصل شيء، وإذا حرّكتها ببطء أو توقّفت عندها، فستكون هناك تلميحة. يُرجى التنبّه: لا "تومض" التلميحة عندما يتحرّك المؤشّر بين العناصر الفرعيّة للساعة. أنجز التمرين في البيئة التجريبية. الحل تبدو الخوارزمية بسيطة: أسند معالجات onmouseover/out إلى العنصر. يمكن أيضا استخدام onmouseenter/leave هنا، لكنّها أقلّ عموما، ولن تعمل إذا أدخلنا التفويض. إذا دخل المؤشّر العنصر، ابدأ بقياس السرعة في mousemove. إذا كانت السرعة صغيرة، شغّل over. عند الذهاب إلى خارج العنصر وقد نُفّذت over، شغّل out. لكن كيف تُقاس السرعة؟ قد تكون أوّل فكرة: شغّل دالّة كلّ 100ms وقس المسافة بين الإحداثيات السابقة والجديدة. إذا كانت المسافة صغيرة، فإنّ السرعة صغيرة. للأسف، لا سبيل للحصول على"الإحداثيّات الحاليّة للفأرة" في جافاسكربت. لا وجود لدالّة مثل getCurrentMouseCoordinates()‎. السبيل الوحيد للحصول على الإحداثيّات هو الإنصات لأحداث الفأرة، مثل mousemove وأخذ الإحداثيّات من كائن الحدث. لنضع إذًا معالجًا للحدث mousemove، لتتبّع الإحداثيّات وتذكّرها. ثمّ لمقارنتها مرّة كلّ 100ms. ملاحظة: تَستخدم اختباراتُ الحلّ dispatchEvent لرؤية ما إذا كانت التلميحة تعمل جيّدا. شاهد الحل في البيئة التجريبية. ترجمة -وبتصرف- للمقال Moving the mouse: mouseover/out, mouseenter/leave من سلسلة Browser: Document, Events, Interfaces لصاحبها Ilya Kantor
    1 نقطة
  3. مقبس عميل TCP إنشاء مقبس يستخدم TCP (‏Transmission Control Protocol) $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); تأكّد من أنّ المقبس (socket) أُنشئ بنجاح، تُستخدم الدالة onSocketFailure لمعالجة أخطاء المقبس، مثال: if(!is_resource($socket)) onSocketFailure("Failed to create socket"); اتصال المقبس بعنوان محدد يفشل السطر الثاني بأمان إذا فشل الاتصال: socket_connect($socket, "chat.stackoverflow.com", 6667) or onSocketFailure("Failed to connect to chat.stackoverflow.com:6667", $socket); إرسال بيانات إلى الخادم ترسل الدالة socket_write البايتات عبر مقبس، تُمثَّل مصفوفة البايت في PHP بسلسلة نصية والتي هي غير حساسة للترميز بشكلٍ طبيعي. socket_write($socket, "NICK Alice\r\nUSER alice 0 * :Alice\r\n"); إرسال بيانات من الخادم تستقبل الشيفرة التالية بعض البيانات من الخادم باستخدام الدالة socket_read، إنّ تمرير قيمة المعامل الثالث على أنّها PHP_NORMAL_READ يقرأ حتى البايت ‎\r/\n ويُضمَّن في القيمة المُعادة، أما تمريرها على أنّها PHP_BINARY_READ يقرأ الكمية المطلوبة من بيانات المجرى. تُعيد الدالة socket_read القيمة false مباشرةً إذا اُستدعيت الدالة socket_set_nonblock قبلها واُستخدمَت القيمة PHP_BINARY_READ، وإلا يُعطَّل التابع حتى تُستقبَل بيانات كافية (الوصول إلى الطول المحدد في المعامل الثاني أو الوصول إلى نهاية السطر) أو يُغلَق المقبس. يقرأ المثال التالي بيانات من خادم IRC (‏Internet Relay Chat) بشكلٍ افتراضي: while(true) { // قراءة سطر من المقبس $line = socket_read($socket, 1024, PHP_NORMAL_READ); if(substr($line, -1) === "\r") { // (1) socket_read($socket, 1, PHP_BINARY_READ); } $message = parseLine($line); if($message->type === "QUIT") break; } يُقرأ في الموضع (1) بايت واحد من المقبس أو يتم تجاوزه، نفرض أنّ البايت التالي في المجرى يجب أن يكون ‎\n‎ ويعدّ هذا ممارسة سيئة ويجعل السكربت ضعيفًا ومعرّضًا لقيم غير متوقعة. إغلاق المقبس يؤدي إغلاق المقبس إلى تحريره وتحرير الموارد المرتبطة به. socket_close($socket); مقبس خادم TCP إنشاء المقبس إنشاء مقبس يستخدم TCP نفس طريقة إنشاء مقبس عميل. $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); ربط المقبس يكون اتصال الربط من شبكة ما (المعامل 2) إلى منفذ معين للمقبس (المعامل 3)، المعامل الثاني عادةً هو "0.0.0.0" والذي يقبل الاتصال من كل الشبكات. أحد أشهر الأسباب التي تؤدي إلى حدوث خطأ نتيجة التابع socket_bind هو أن يكون العنوان المحدد مقيد مسبقًا بعملية أخرى. تُنهى عادةً العمليات الأخرى يدويًا (لمنع إنهاء العمليات الحرجة دون قصد) لذا تُحرَّر المقابس. socket_bind($socket, "0.0.0.0", 6667) or onSocketFailure("Failed to bind to 0.0.0.0:6667"); ضبط المقبس للاستماع نستخدم التابع socket_listen لجعل المقبس يستمع إلى الاتصالات الواردة، يعبر المعامل الثاني عن عدد الاتصالات الأعظمي المسموح لها أن تكون في قائمة الانتظار قبل قبولها. socket_listen($socket, 5); معالجة الاتصال خادم TCP هو خادم يعالج الاتصالات الأبناء، يُنشئ التابع socket_accept اتصال ابن جديد. $conn = socket_accept($socket); نقل بيانات الاتصال من التابع socket_accept هو نفسه في مقبس عميل TCP. يمكنك استدعاء التابع socket_close($conn);‎ مباشرةً عندما تريد إغلاق الاتصال، ولا يؤثر هذا على مقبس خادم TCP الأصلي. إغلاق الخادم يجب استدعاء التابع socket_close($socket);‎ عند الانتهاء من استخدام الخادم، مما سيؤدي إلى تحرير عنوان TCP وبالتالي يُسمح لعمليات أخرى بالربط معه. مقبس خادم UDP خادم UDP (‏user datagram protocol) لا يعتمد على المجرى بل على الرزمة (packet-based) على عكس TCP، مثلًا عميل يرسل بيانات في وحدات تدعى رزم (packets) إلى الخادم ويعرف العميل العملاء من خلال عناوينهم، لا يوجد دالة مضمَّنة تربط الرزم المختلفة المُرسلة من نفس العميل (على عكس TCP حيث تُعالج البيانات المُرسلة من نفس العميل بمورد محدد ينشأه التابع socket_accept)، يمكننا التفكير أنّه عند كل وصول لرزمة UDP فإنّ اتصال TCP جديد يُقبَل ويُغلق. إنشاء مقبس خادم UDP $socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); ربط المقبس إلى عنوان نفس المعاملات الموجودة لخادم TCP. socket_bind($socket, "0.0.0.0", 9000) or onSocketFailure("Failed to bind to 0.0.0.0:9000",$socket); إرسال رزمة ترسل الشيفرة التالية المتغير ‎$data في رزمة UDP إلى ‎$address:$port. socket_sendto($socket, $data, strlen($data), 0, $address, $port); استقبال رزمة تحاول الشيفرة التالية إدارة رزم UDP بطريقة تعتمد على فهرسة العميل. $clients = []; while (true){ socket_recvfrom($socket, $buffer, 32768, 0, $ip, $port) === true or onSocketFailure("Failed to receive packet", $socket); $address = "$ip:$port"; if (!isset($clients[$address])) $clients[$address] = new Client(); $clients[$address]->handlePacket($buffer); } إغلاق الخادم يمكن استخدام التابع socket_close على مورد مقبس خادم UDP. سيحرر هذا عنوان UDP مما يسمح بربط العمليات الأخرى إلى هذا العنوان. معالجة أخطاء المقبس نستخدم التابع socket_last_error للحصول على رقم معرِّف الخطأ الأخير من إضافة المقابس، ونستخدم التابع socket_strerror لتحويل هذا الرقم إلى سلسلة نصيّة قابلة للقراءة من قِبل الإنسان. function onSocketFailure(string $message, $socket = null) { if(is_resource($socket)) { $message .= ": " . socket_strerror(socket_last_error($socket)); } die($message); } مقابس الويب (Webscockets) ينفّذ استخدام إضافة المقبس (socket) واجهة منخفضة المستوى لدوال اتصال المقبس بالاعتماد على مقابس BSD (‏Berkeley Software Distribution) الشائعة، مما يوفر إمكانية العمل كخادم مقبس وعميل. خادم TCP/IP بسيط يمكنك أن تجد هنا مثالًا بسيطًا يعتمد على توثيق PHP الرسمي. أنشئ سكربت مقبس ويب يستمع إلى المنفذ 5000 باستخدام putty والطرفية لتنفيذ الأمر telnet 127.0.0.1 5000 (المضيف المحلي)، يرد هذا السكربت بالرسالة التي أرسلتها (كتعقب عكسي): <?php // تعطيل المهلة set_time_limit(0); // تعطيل التخزين المؤقت للخرج ob_implicit_flush(); // الإعدادات $address = '127.0.0.1'; $port = 5000; // (1) if (($socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)) === false) { echo "Couldn't create socket".socket_strerror(socket_last_error())."\n"; } // (2) if (socket_bind($socket, $address, $port) === false) { echo "Bind Error ".socket_strerror(socket_last_error($sock)) ."\n"; } if (socket_listen($socket, 5) === false) { echo "Listen Failed ".socket_strerror(socket_last_error($socket)) . "\n"; } do { if (($msgsock = socket_accept($socket)) === false) { echo "Error: socket_accept: " . socket_strerror(socket_last_error($socket)) . "\n"; break; } /* إرسال رسالة ترحيب */ $msg = "\nPHP Websocket \n"; // الاستماع إلى دخل المستخدم do { if (false === ($buf = socket_read($msgsock, 2048, PHP_NORMAL_READ))) { echo "socket read error: ".socket_strerror(socket_last_error($msgsock)) . "\n"; break 2; } if (!$buf = trim($buf)) { continue; } // الرد على المستخدم برسالته $talkback = "PHP: You said '$buf'.\n"; socket_write($msgsock, $talkback, strlen($talkback)); // طباعة الرسالة على الطرفية echo "$buf\n"; } while (true); socket_close($msgsock); } while (true); socket_close($socket); ?> في الموضع (1) لدينا الدالة socket_create لها الشكل العام ( int $domain , int $type , int $protocol ): يمكن أن يكون المتغير ‎$domain هو AF_INET أو AF_INET6 من أجل IPV6 أو AF_UNIX من أجل بروتوكول الاتصال المحلي. يمكن أن يكون المتغير ‎$protocol إما SOL_TCP أو SOL_UDP ‏(TCP/UDP) تعيد هذه الدالة القيمة true في حالة النجاح. في الموضع (2) نستخدم الدالة socket_bind التي لها الشكل العام ‎socket_bind ( resource $socket , string $address [, int $port = 0 ] )‎، تربط هذه الدالة المقبس ليستمع إلى عنوان ومنفذ محددين. استيثاق HTTP سنكتب سكربت استيثاق ترويسة HTTP بسيط، ولاحظ أنّه يجب وضع هذه الشيفرة في ترويسة الصفحة وإلا لن يعمل: <?php if (!isset($_SERVER['PHP_AUTH_USER'])) { header('WWW-Authenticate: Basic realm="My Realm"'); header('HTTP/1.0 401 Unauthorized'); echo 'Text to send if user hits Cancel button'; exit; } echo "<p>Hello {$_SERVER['PHP_AUTH_USER']}.</p>"; // حفظ المعلومات $user = $_SERVER['PHP_AUTH_USER']; echo "<p>You entered {$_SERVER['PHP_AUTH_PW']} as your password.</p>"; // ‫حفظ كلمة المرور (يمكن إضافة التشفير اختياريًا) $pass = $_SERVER['PHP_AUTH_PW']; //Save the password(optionally add encryption)! ?> // ‫صفحة html ترجمة -وبتصرف- للفصول [ WebSockets - HTTP Authentication - Sockets] من كتاب PHP Notes for Professionals book اقرأ أيضًا المقال التالي: إضافة PHP MySQLi ونظام إدارة قواعد البيانات SQLite3 المقال السابق: معالجة بيانات طلبيات HTTP والتعامل مع أخطاء رفع الملفات في PHP
    1 نقطة
  4. واجهة mysqli هي تحسين (وتعني إضافة MySQL محسَّنة "MySQL Improvement extension") لواجهة MySQL التي أُهملت في الإصدار 5.5 وحُذِفت في الإصدار 7.0. طُوِّرت إضافة mysqli المعروفة أيضًا باسم إضافة MySQL المحسَّنة للاستفادة من إيجابيات الميزات الجديدة الموجودة في إصدارات أنظمة MySQL بدءًا من الإصدار 4.1.3 وما بعد. ضُمِّنت إضافة mysqli في الإصدار PHP 5 وما بعده. إغلاق الاتصال يُنصَح بإغلاق الاتصال بعد الانتهاء من الاستعلام من قاعدة البيانات لتحرير الموارد. النمط الكائني التوجه $conn->close(); النمط الإجرائي mysqli_close($conn); ملاحظة: سيُغلق الاتصال مع الخادم حالما ينتهي تنفيذ السكربت ما لم يُغلق مبكرًا باستدعاء دالة إغلاق الاتصال بشكلٍ صريح. حالة استخدام: يجب بكل تأكيد إغلاق الاتصال إذا احتوى السكربت على كمية معقولة من المعالجة بعد جلب النتيجة واستعاد مجموعة النتائج كاملةً، إذا لم نقم بذلك فهناك احتمال أن يصل خادم MySQL إلى حد الاتصال عندما يكون خادم الويب قيد الاستخدام المكثف. اتصال MySQLi النمط الكائني التوجه الاتصال بالخادم: $conn = new mysqli("localhost","my_user","my_password"); ضبط قاعدة البيانات الافتراضية: $conn->select_db("my_db"); الاتصال بقاعدة البيانات: $conn = new mysqli("localhost","my_user","my_password","my_db"); النمط الإجرائي الاتصال بالخادم: $conn = mysqli_connect("localhost","my_user","my_password"); ضبط قاعدة البيانات الافتراضية: mysqli_select_db($conn, "my_db"); الاتصال بقاعدة البيانات: $conn = mysqli_connect("localhost","my_user","my_password","my_db"); التحقق من الاتصال بقاعدة البيانات النمط الكائني التوجه if ($conn->connect_errno > 0) { trigger_error($db->connect_error); } // else: successfully connected النمط الإجرائي if (!$conn) { trigger_error(mysqli_connect_error()); } // else: successfully connected تمرير حلقة على نتائج MySQLi تجعل PHP من السهل الحصول على البيانات من النتائج وتكرارها باستخدام تعليمة while، وترجع false عندما تفشل في الحصول على السطر التالي وتنتهي الحلقة. تعمل هذه الأمثلة مع: mysqli_fetch_assoc - مصفوفة ترابطية مع أسماء الأعمدة كمفاتيح. mysqli_fetch_object - كائن stdClass مع أسماء الأعمدة كمتغيرات. mysqli_fetch_array - مصفوفة ترابطية وعددية (يمكنك استخدام وسيط لتحصل على إحداها) mysqli_fetch_row - مصفوفة عددية. النمط الكائني التوجه while($row = $result->fetch_assoc()) { var_dump($row); } النمط الإجرائي while($row = mysqli_fetch_assoc($result)) { var_dump($row); } يمكننا استخدام الشيفرة التالية للحصول على معلومات دقيقة من النتائج: while ($row = $result->fetch_assoc()) { echo 'Name and surname: '.$row['name'].' '.$row['surname'].'<br>'; // ‫طباعة معلومات من العمود 'age' echo 'Age: '.$row['age'].'<br>'; } تعليمات التحضير في MySQLi يمكنك قراءة المزيد حول منع حقن SQL مع الاستعلامات التي تحوي معاملات لمعرفة سبب مساعدة تعليمات التحضير لك في تأمين تعليمات SQL ضد هجمات حقن SQL. المتغير ‎$conn هو كائن MySQLi في الشيفرات التالية وفي المثالين نفرض أنّ ‎$sql: $sql = "SELECT column_1 FROM table WHERE column_2 = ? AND column_3 > ?"; تمثّل ? القيم التي سنوفرها لاحقًا، لاحظ أننا لا نحتاج علامات الاقتباس للنصوص البديلة بغض النظر عن النوع، يمكننا أيضًا أن نوفر النصوص البديلة فقط في جزء البيانات من الاستعلام أي SET وVALUES وWHERE، ولا يمكن استخدام النصوص البديلة في الأجزاء SELECT أو FROM. النمط الكائني التوجه if ($stmt = $conn->prepare($sql)) { $stmt->bind_param("si", $column_2_value, $column_3_value); $stmt->execute(); $stmt->bind_result($column_1); $stmt->fetch(); // ‫يمكننا الآن استخدام المتغير ‎$column_1 مثل أي متغير PHP آخر $stmt->close(); } النمط الإجرائي if ($stmt = mysqli_prepare($conn, $sql)) { mysqli_stmt_bind_param($stmt, "si", $column_2_value, $column_3_value); mysqli_stmt_execute($stmt); // جلب البيانات هنا mysqli_stmt_close($stmt); } يُحدَّد المعامل الأول للتابع ‎$stmt->bind_param أو المعامل الثاني للتابع mysqli_stmt_bind_param وفقًا لنمط البيانات للمعامل المقابل في استعلام SQL: 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; } المعامل نوع البيانات للمعامل المقيّد i integer عدد صحيح d double عدد عشري s string سلسلة نصية b blob كائن بيانات ثنائية يجب أن تكون قائمة المعاملات بنفس الترتيب الموجود في الاستعلام، تعني si في هذا المثال أنّ المعامل الأول ‎(column_2 = ?)‎ هو سلسلة نصية والمعامل الثاني ‎(column_3 > ?)‎ عدد صحيح. سلاسل الهروب النصية سلاسل الهروب النصية (Escaping Strings) طريقة قديمة (وأقل أمنًا) لتأمين البيانات لإدراجها في استعلام، تعمل باستخدام دالة MySQL ‏mysql_real_escape_string()‎ لمعالجة وتعقيم البيانات (أي أنّ PHP لا تقوم بعملية الهروب). توفر واجهة برمجة التطبيقات MySQLi الوصول المباشر لهذه الدالة. $escaped = $conn->real_escape_string($_GET['var']); //أو $escaped = mysqli_real_escape_string($conn, $_GET['var']); عند هذه النقطة يصبح لديك سلسلة نصية تعدها MySQL آمنة للاستخدام في استعلام مباشر. $sql = 'SELECT * FROM users WHERE username = "' . $escaped . '"'; $result = $conn->query($sql); إذًا لم لا تُعدّ هذه الطريقة آمنة مثل تعليمات التحضير؟ لأنه يوجد عدة طرق لخداع MySQL لإنتاج سلسلة نصية تُعد آمنة. إليك المثال التالي: $id = mysqli_real_escape_string("1 OR 1=1"); $sql = 'SELECT * FROM table WHERE id = ' . $id; لا يمثّل التعبير ‎1 OR 1=1‎ بيانات ستهرّبها MySQL، لكنه لا يزال يمثّل حقن SQL، يوجد أمثلة أخرى تمثّل حالات تُعاد فيها بيانات غير آمنة، المشكلة هي أنّ دالة الهروب في MySQL صُممت لجعل البيانات تتوافق مع صيغة SQL وليس للتأكد من أنّ MySQL لا تتمكن من خلط بيانات المستخدم من أجل تعليمات SQL. تنقيح أخطاء SQL في MySQLi الاستعلام في الشيفرة التالية سيفشل (استخدمنا المتغير ‎$conn للاتصال بالخادم كما وضحنا سابقًا): $result = $conn->query('SELECT * FROM non_existent_table'); نتيجة ‎$result هي false وهذا لا يساعدنا على معرفة الخطأ، لحسن الحظ يمكن لمتغير الاتصال ‎$conn أن يخبرنا عن الفشل: trigger_error($conn->error); أو بالنمط الإجرائي: trigger_error(mysqli_error($conn)); عندها ستحصل على خطأ مشابه لما يلي: Table 'my_db.non_existent_table' doesn't exist استعلام MySQLi تأخذ الدالة query سلسلة SQL صحيحة وتنفّذها مباشرةً على الاتصال بقاعدة البيانات ‎$conn. النمط الكائني التوجه $result = $conn->query("SELECT * FROM `people`"); النمط الإجرائي $result = mysqli_query($conn, "SELECT * FROM `people`"); سيظهر تحذير، المشكلة الشائعة هنا هي أنّك تنفّذ الشيفرة ببساطة وتتوقعها أن تعمل (تعيد مثلًا كائنًا من الصنف mysqli_stmt)، بما أنّ هذه الدالة تأخذ سلسلة نصية فقط فأنت تبني الاستعلام أولًا وإذا وُجدَت أي أخطاء في SQL سيفشل مصرِّف MySQL وعندها تعيد الدالة القيمة false. // سيفشل الاستعلام التالي $result = $conn->query('SELECT * FROM non_existent_table'); $row = $result->fetch_assoc(); ستنتج الشيفرة السابقة خطأ E_FATAL لأنّ نتيجة ‎$result هي false وليس كائنًا. PHP Fatal error: Call to a member function fetch_assoc() on a non-object الخطأ الإجرائي مشابه لكنه ليس خطأً فادحًا لأننا نخترق توقعات الدالة فقط. // نفس الاستعلام السابق $row = mysqli_fetch_assoc($result); ستحصل على الرسالة التالية من PHP: mysqli_fetch_array() expects parameter 1 to be mysqli_result, boolean given يمكنك تجنب هذا باستخدام شيفرة الاختبار التالية: if($result) $row = mysqli_fetch_assoc($result); كيفية الحصول على البيانات من تعليمات التحضير تعليمات التحضير اطّلع على تعليمات التحضير في mysqli لمعرفة كيفية تحضير وتنفيذ استعلام. ربط النتائج النمط الكائني التوجه $stmt->bind_result($forename); النمط الإجرائي mysqli_stmt_bind_result($stmt, $forename); مشكلة استخدام الدالة bind_result أنّها تتطلب تعليمة لتحديد الأعمدة التي ستُستخدم، أي أنّه كي تعمل الشيفرة السابقة يجب أن يبدو الاستعلام بالشكل SELECT forename FROM users، لتضمين المزيد من الأعمدة يمكنك إضافتها كمعاملات للدالة bind_result (وتأكد من إضافتهم إلى استعلام SQL). نُسند في كلتا الحالتين العمود forename للمتغير ‎$forename، تأخذ هذه الدالة عدد وسائط بعدد الأعمدة التي تريد إسنادها، يتم هذا الإسناد مرة واحدة بما أنّ الدالة تُربَط بالمرجع، لذا يمكننا تمرير حلقة بالشكل التالي: النمط الكائني التوجه while ($stmt->fetch()) echo "$forename<br />"; النمط الإجرائي while (mysqli_stmt_fetch($stmt)) echo "$forename<br />"; العيب في هذا أنّك تحتاج لإسناد الكثير من المتغيرات في وقت واحد وهذا يجعل تتبع الاستعلامات الكبيرة أمرًا صعبًا، إذا كان لديك محرك MySQL أساسي (mysqlnd) مثبّت فإنّ كل ما تحتاجه هو استخدام get_result. النمط الكائني التوجه $result = $stmt->get_result(); النمط الإجرائي $result = mysqli_stmt_get_result($stmt); يعدّ هذا سهلًا لأننا نحصل على كائن من الصنف mysqli_result? وهو نفس الكائن الذي تعيده mysqli_query أي أنّه يمكنك استخدام حلقة لعرض النتيجة. ماذا لو لم نستطع تثبيت mysqlnd؟ يمكنك الاطلاع على جواب مناسب من هنا. يمكن أن تؤدي هذه الدالة مهمة get_result بدون أن تكون مثبّتة على الخادم، فهي ببساطة تكرر النتائج وتبني مصفوفة ترابطية. function get_result(\mysqli_stmt $statement) { $result = array(); $statement->store_result(); for ($i = 0; $i < $statement->num_rows; $i++) { $metadata = $statement->result_metadata(); $params = array(); while ($field = $metadata->fetch_field()) { $params[] = &$result[$i][$field->name]; } call_user_func_array(array($statement, 'bind_result'), $params); $statement->fetch(); } return $result; } يمكننا بعدها استخدام الدالة للحصول على النتائج كما لو أننا نستخدم mysqli_fetch_assoc()‎: <?php $query = $mysqli->prepare("SELECT * FROM users WHERE forename LIKE ?"); $condition = "J%"; $query->bind_param("s", $condition); $query->execute(); $result = get_result($query); while ($row = array_shift($result)) { echo $row["id"] . ' - ' . $row["forename"] . ' ' . $row["surname"] . '<br>'; } سيظهر لنا نفس الخرج في حالة استخدام محرك mysqlnd باستثناء أنّه لا يحتاج للتثبيت، وهذا الحل مناسب جداً إذا كنت لا تستطيع تثبيت المحرك. إضافة معرف في MySQLi استعادة آخر معرّف مولَّد من استعلام INSERT على جدول فيه عمود AUTO_INCREMENT. النمط الكائني التوجه $id = $conn->insert_id; النمط الإجرائي $id = mysqli_insert_id($conn); ترجع الشيفرة السابقة القيمة صفر إذا لم يكن هناك استعلام سابق على الاتصال أو إذا لم يحدّث الاستعلام قيمة AUTO_INCREMENT. إضافة معرّف عند تحديث الأسطر لا تُرجع تعليمة UPDATE معرّف السطر المُضاف في الحالة العادية، إذ يُرجَع المعرّف AUTO_INCREMENT عند حفظ سطر جديد فقط (أو إضافته)، يمكن استخدام الصياغة INSERT ... ON DUPLICATE KEY UPDATE للتحديث مما يجعل التحديثات تطرأ على المعرِّف الجديد. مثال: CREATE TABLE iodku ( id INT AUTO_INCREMENT NOT NULL, name VARCHAR(99) NOT NULL, misc INT NOT NULL, PRIMARY KEY(id), UNIQUE(name) ) ENGINE=InnoDB; INSERT INTO iodku (name, misc) VALUES ('Leslie', 123), ('Sally', 456); Query OK, 2 rows affected (0.00 sec) Records: 2 Duplicates: 0 Warnings: 0 id name misc 1 Leslie 123 2 Sally 456 في حال حدّثَ الجدول IODKU واستعاد LAST_INSERT_ID()‎ المعرِّف المرتبط: $sql = "INSERT INTO iodku (name, misc) VALUES ('Sally', 3333) ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id), misc = VALUES(misc)"; $conn->query($sql); $id = $conn->insert_id; يجب أن تحدّث الشيفرة السابقة السطر الثاني من الجدول وتعيد القيمة الحالية للمعرِّف (2). الحالة التي يحدث فيها إضافة سطر وتعيد LAST_INSERT_ID()‎ المعرِّف الجديد: $sql = "INSERT INTO iodku (name, misc) VALUES ('Dana', 789) ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id), misc = VALUES(misc); $conn->query($sql); $id = $conn->insert_id; يجب أن تضيف الشيفرة السابقة سطرًا جديدًا إلى الجدول وتعيد القيمة الأخيرة للمعرِّف (3). ينتج عن الاستعلام: SELECT * FROM iodku; الجدول التالي: id name misc 1 Leslie 123 2 Sally 3333 3 Dana 789 درس سريع لمكتبة SQLite3 إليك مثال كامل عن جميع واجهات برمجة التطبيقات الشائعة الاستخدام المرتبطة بمكتبة SQLite3، بهدف جعلك تعمل بسرعة كبيرة ويمكنك أيضاً الحصول على ملف PHP قابل للتنفيذ لهذا الدرس. إنشاء/فتح قاعدة بيانات ننشئ قاعدة بيانات أولاً، ننشئها فقط إذا لم يكن الملف موجوداً ونفتحها للقراءة/الكتابة، امتداد الملف يعود لك لكن الشائع هو استخدام الامتداد ‎.sqlite الأكثر إيضاحًا. $db = new SQLite3('analytics.sqlite', SQLITE3_OPEN_CREATE | SQLITE3_OPEN_READWRITE); إنشاء جدول $db->query('CREATE TABLE IF NOT EXISTS "visits" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "user_id" INTEGER, "url" VARCHAR, "time" DATETIME )'); إضافة عينة بيانات يُنصح بإحاطة الاستعلامات المرتبطة ضمن عملية (transaction) (مع الكلمات المفتاحية BEGIN وCOMMIT) حتى لو لم تكن مهتمًا بالترابط، إذا لم تفعل ذلك فإنّ SQLite ستحيط كل استعلام مفرد بعملية بشكلٍ تلقائي مما يؤدي إلى البطء الشديد. قد تُفاجئ بسبب بطء تعليمات INSERT إذا كنت جديدًا على استخدام SQLite. $db->exec('BEGIN'); $db->query('INSERT INTO "visits" ("user_id", "url", "time") VALUES (42, "/test", "2017-01-14 10:11:23")'); $db->query('INSERT INTO "visits" ("user_id", "url", "time") VALUES (42, "/test2", "2017-01-14 10:11:44")'); $db->exec('COMMIT'); يمكنك القيام بإضافة بيانات قد لا تكون آمنة مع تعليمة تحضير، باستخدام المعاملات ذات الأسماء (named parameters): $statement = $db->prepare('INSERT INTO "visits" ("user_id", "url", "time") VALUES (:uid, :url, :time)'); $statement->bindValue(':uid', 1337); $statement->bindValue(':url', '/test'); $statement->bindValue(':time', date('Y-m-d H:i:s')); $statement->execute(); you can reuse the statement with different values جلب البيانات لنجلب زيارات اليوم للمستخدم رقم 42، سنستخدم تعليمة التحضير من جديد لكن هذه المرة مع معاملات ذات أسماء وهذا أكثر إيجازًا: $statement = $db->prepare('SELECT * FROM "visits" WHERE "user_id" = ? AND "time" >= ?'); $statement->bindValue(1, 42); $statement->bindValue(2, '2017-01-14'); $result = $statement->execute(); echo "Get the 1st row as an associative array:\n"; print_r($result->fetchArray(SQLITE3_ASSOC)); echo "\n"; echo "Get the next row as a numeric array:\n"; print_r($result->fetchArray(SQLITE3_NUM)); echo "\n"; ملاحظة: تُرجع الدالة fetchArray()‎ القيمة false إذا لم يكن هناك المزيد من الأسطر، يمكنك الاستفادة من هذا في حلقة while. حرّر الذاكرة - لا يحدث هذا تلقائيًا أثناء تشغيل السكربت. $result->finalize(); الاختزالات إليك اختزال مفيد لجلب سطر واحد كمصفوفة ترابطية، يعني المعامل الثاني أننا نريد كل الأعمدة المُختارة. انتبه إلى أنّ هذا الاختزال لا يدعم ربط المعاملات لكن يمكنك بدلًا من ذلك الهروب من السلاسل النصية، ضع القيم دائمًا بين علامات اقتباس مفردة إذ تستخدم علامات الاقتباس المزدوجة لأسماء الجداول والأعمدة (بشكل مشابه لعلامات الاقتباس المائلة في MySQL). $query = 'SELECT * FROM "visits" WHERE "url" = \'' . SQLite3::escapeString('/test') . '\' ORDER BY "id" DESC LIMIT 1'; $lastVisit = $db->querySingle($query, true); echo "Last visit of '/test':\n"; print_r($lastVisit); echo "\n"; اختزال آخر مفيد لاستعادة قيمة واحدة فقط. $userCount = $db->querySingle('SELECT COUNT(DISTINCT "user_id") FROM "visits"'); echo "User count: $userCount\n"; echo "\n"; التنظيف في النهاية، أغلق قاعدة البيانات على الرغم من أنّ هذا يتم تلقائيًا عندما ينتهي السكربت. $db->close(); الاستعلام من قاعدة بيانات <?php // ‫إنشاء كائن SQLite3 جديد من ملف قاعدة البيانات على الخادم $database = new SQLite3('mysqlitedb.db'); // ‫الاستعلام من قاعدة البيانات باستخدام SQL $results = $database->query('SELECT bar FROM foo'); // التكرار على كل النتائج وإظهارهم على الصفحة while ($row = $results->fetchArray()) { var_dump($row); } ?> استعادة نتيجة واحدة فقط بالإضافة إلى استخدام تعليمات ‏LIMIT في SQL، يمكنك استخدام الدالة querySingle في SQLite3 لاستعادة سطر واحد أو العمود الأول. <?php $database = new SQLite3('mysqlitedb.db'); //(1) $database->querySingle('SELECT column1Name FROM table WHERE column2Name=1'); // (2) $database->querySingle('SELECT column1Name, column2Name FROM user WHERE column3Name=1', true); في الموضع (1) بدون ضبط المعامل الثاني الاختياري للقيمة true سيُرجع الاستعلام العمود الأول من السطر الأول للنتائج فقط ويكون من نفس نوع columnName. في الموضع (2) مع المعامل الاختياري entire_row سيرجع هذا الاستعلام مصفوفة من كامل السطر الأول من نتائج الاستعلام. ترجمة -وبتصرف- للفصول من كتاب PHP Notes for Professionals book اقرأ أيضًا المقال التالي: استخدام MongoDB و Redis في PHP المقال السابق: المقابس (sockets) في PHP
    1 نقطة
  5. تجدر بنا الإشارة إلى قاعدة مهمة قبل الشروع في الحديث عن كيفية تعامل جافاسكربت مع الأنماط والأصناف. القاعدة بديهية جدًا ولكننا سنذكرها للإفادة. هناك طريقتان تُستخدمان لتنسيق عنصرٍ ما: إنشاء صنف في ملف 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
    1 نقطة
×
×
  • أضف...