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

البحث في الموقع

المحتوى عن 'جافاسكربت'.

  • ابحث بالكلمات المفتاحية

    أضف وسومًا وافصل بينها بفواصل ","
  • ابحث باسم الكاتب

نوع المحتوى


التصنيفات

  • الإدارة والقيادة
  • التخطيط وسير العمل
  • التمويل
  • فريق العمل
  • دراسة حالات
  • التعامل مع العملاء
  • التعهيد الخارجي
  • السلوك التنظيمي في المؤسسات
  • عالم الأعمال
  • التجارة والتجارة الإلكترونية
  • نصائح وإرشادات
  • مقالات ريادة أعمال عامة

التصنيفات

  • مقالات برمجة عامة
  • مقالات برمجة متقدمة
  • PHP
    • Laravel
    • ووردبريس
  • جافاسكربت
    • لغة TypeScript
    • Node.js
    • React
    • Vue.js
    • Angular
    • jQuery
    • Cordova
  • HTML
  • CSS
    • Sass
    • إطار عمل Bootstrap
  • SQL
  • لغة C#‎
    • ‎.NET
    • منصة Xamarin
  • لغة C++‎
  • لغة C
  • بايثون
    • Flask
    • Django
  • لغة روبي
    • إطار العمل Ruby on Rails
  • لغة Go
  • لغة جافا
  • لغة Kotlin
  • لغة Rust
  • برمجة أندرويد
  • لغة R
  • الذكاء الاصطناعي
  • صناعة الألعاب
  • سير العمل
    • Git
  • الأنظمة والأنظمة المدمجة

التصنيفات

  • تصميم تجربة المستخدم UX
  • تصميم واجهة المستخدم UI
  • الرسوميات
    • إنكسكيب
    • أدوبي إليستريتور
  • التصميم الجرافيكي
    • أدوبي فوتوشوب
    • أدوبي إن ديزاين
    • جيمب GIMP
    • كريتا Krita
  • التصميم ثلاثي الأبعاد
    • 3Ds Max
    • Blender
  • نصائح وإرشادات
  • مقالات تصميم عامة

التصنيفات

  • مقالات DevOps عامة
  • خوادم
    • الويب HTTP
    • البريد الإلكتروني
    • قواعد البيانات
    • DNS
    • Samba
  • الحوسبة السحابية
    • Docker
  • إدارة الإعدادات والنشر
    • Chef
    • Puppet
    • Ansible
  • لينكس
    • ريدهات (Red Hat)
  • خواديم ويندوز
  • FreeBSD
  • حماية
    • الجدران النارية
    • VPN
    • SSH
  • شبكات
    • سيسكو (Cisco)

التصنيفات

  • التسويق بالأداء
    • أدوات تحليل الزوار
  • تهيئة محركات البحث SEO
  • الشبكات الاجتماعية
  • التسويق بالبريد الالكتروني
  • التسويق الضمني
  • استسراع النمو
  • المبيعات
  • تجارب ونصائح
  • مبادئ علم التسويق

التصنيفات

  • مقالات عمل حر عامة
  • إدارة مالية
  • الإنتاجية
  • تجارب
  • مشاريع جانبية
  • التعامل مع العملاء
  • الحفاظ على الصحة
  • التسويق الذاتي
  • العمل الحر المهني
    • العمل بالترجمة
    • العمل كمساعد افتراضي
    • العمل بكتابة المحتوى

التصنيفات

  • الإنتاجية وسير العمل
    • مايكروسوفت أوفيس
    • ليبر أوفيس
    • جوجل درايف
    • شيربوينت
    • Evernote
    • Trello
  • تطبيقات الويب
    • ووردبريس
    • ماجنتو
    • بريستاشوب
    • أوبن كارت
    • دروبال
  • الترجمة بمساعدة الحاسوب
    • omegaT
    • memoQ
    • Trados
    • Memsource
  • برامج تخطيط موارد المؤسسات ERP
    • تطبيقات أودو odoo
  • أنظمة تشغيل الحواسيب والهواتف
    • ويندوز
    • لينكس
  • مقالات عامة

التصنيفات

  • آخر التحديثات

أسئلة وأجوبة

  • الأقسام
    • أسئلة البرمجة
    • أسئلة ريادة الأعمال
    • أسئلة العمل الحر
    • أسئلة التسويق والمبيعات
    • أسئلة التصميم
    • أسئلة DevOps
    • أسئلة البرامج والتطبيقات

التصنيفات

  • كتب ريادة الأعمال
  • كتب العمل الحر
  • كتب تسويق ومبيعات
  • كتب برمجة
  • كتب تصميم
  • كتب DevOps

ابحث في

ابحث عن


تاريخ الإنشاء

  • بداية

    نهاية


آخر تحديث

  • بداية

    نهاية


رشح النتائج حسب

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

  • بداية

    نهاية


المجموعة


النبذة الشخصية

تم العثور على 15 نتائج

  1. تُعَد البيانات وجافاسكربت للواجهة الأمامية من الناحية العملية أحداثًا متنافية، إذ يرى سوق العمل أن وظيفة الواجهة الخلفية معالجة البيانات وتجميعها، أما وظيفة جافاسكربت فهي عرض البيانات المُجمّعة مسبقًا، كما يُنظر إلى عرض النطاق الترددي وزمن المعالجة على أنهما عقبتان رئيسيتان لدى التعامل مع البيانات في الواجهة الأمامية وهذا صحيح إلى حدٍ كبيرٍ، ولكن توجد حالاتٌ من المجدي فيها معالجة البيانات ضمن المتصفح. إذًا فكيف يمكن النجاح بمعالجة البيانات في هذه الحالات؟ التفكير في البيانات يتطلب العمل على البيانات باستخدام جافاسكربت امتلاكًا كاملًا للبيانات وفهمًا للأدوات المتوفرة دون اتصالٍ غير ضروريٍ بالخادم، حيث من المفيد التمييز بين البيانات التابعة لثلاثة أطرافٍ والبيانات المُلّخصة. تتألف البيانات ثلاثية الأطراف من بيانات معاملاتٍ أولية، وهي بيانات منخفضة المستوى يصعب تحليلها لوحدها؛ وتوجد من ناحيةٍ أخرى البيانات المُلخصة وهي البيانات الممكن عرضها بطريقةٍ مفيدةٍ ومنطقية، وتُدعى أيضًا بالبيانات المكوّنة. يهتم المطورون بهيكلية البيانات الكامنة بين تفاصيل المعاملات والبيانات المكوّنة، وهذه هي النقطة التي سنُركز عليها، فهذه البيانات مُجمّعة ولكنها تتضمن أكثر مما نحتاج للعرض النهائي، كما تمتلك بُعدين أو أكثر (متعددة الأبعاد)، وهذا يوفّر مرونةً في طريقة عرضها، فهي تسمح للمستخدمين النهائيين بتشكيل البيانات واستخراج المعلومات لمزيدٍ من التحليل، وبالرغم من أنها صغيرة وذات أداء جيدٍ، إلا أنها توفّر تفاصيلًا تسمح لنا وللمؤلف بالاطلاع على معلوماتٍ لم نكن نتوقعها. يجب أن لا يكون الهدف هو الوصول بالبيانات إلى هيئةٍ مثالية، وذلك لتجنب أي تلاعبٍ بها في الواجهة الأمامية، بل يجب تقليص البيانات إلى مجموعة بياناتٍ متعددة الأبعاد، كما يجب استخدام أبعادٍ متعددة يهتم بها العميل المُستهدف مثل الأشخاص والمنتجات والأماكن والوقت، وقياساتٍ متعددة مثل المجموع والتعداد والمتوسط وأصغر قيمة وأعظم قيمة، وبالتالي فعلينا في النهاية عرض هذه البيانات على صفحةٍ مع عناصر نموذج تسمح بتقسيم البيانات لتحليلها بصورةٍ أعمق. يُعَد إنشاء مجموعات البيانات عمليةً دقيقة جدًا، حيث لا بد من امتلاك بياناتٍ كافية ليكون تحليلنا ذو قيمة وبدون إجهاد جهاز العميل سواءً كان حاسوبًا أو هاتفًا ذكيًا أو غير ذلك، وهذا يعني الوصول لمتطلباتٍ واضحة وموجزة، وسوف نحتاج لاستخدام أبعاد متنوعة وقياسات مختلفة، وهذا يعتمد على مدى اتساع مجموعة البيانات التي نتعامل معها. سنذكر فيما يلي بعض الأمور الواجب أخذها بالحسبان: هل غالبية المحتوى هو محتوى مميز أو سوف يُستخدم مرارًا؟ حيث نعتمد على قاعدة 80/20 التي تنص على أن 80% من المستخدمين عامةً يحتاجون إلى 20% مما هو متوفرٌ. هل كل الأبعاد منتهية؟ حيث يجب أن تمتلك الأبعاد مجموعةً من القيم المُحددة مسبقًا، فمثلًا مخزون منتج ما يزداد باستمرار سوف يتضخم بسرعة، لذلك فإن العمل على تصنيفات المنتج ربما سيكون أفضل. العمل على تجميع البيانات ما أمكن وبالأخص التواريخ، وإن كنت تستطيع الاكتفاء بعملية التجميع باستخدام الأعوام فافعل ذلك، وإن كنت بحاجة للتجميع بالاعتماد على الشهر فلا بأس بذلك، ولكن تجنب المجالات الأصغر من ذلك. اعتماد مبدأ "القلة أفضل"، فكلما كان عدد القيم ضمن البعد أقل، كان هذا أفضل من ناحية الأداء. لنأخذ مجموعة بيانات مؤلفةً من 200 صفٍ على سبيل المثال، فإذا أضفنا بعدًا آخر بأربع قيمٍ ممكنة، فسوف تتضخم مجموعة البيانات هذه إلى 200x4 = 800 صفٍ كحدٍ أعظمي؛ أما إذا أضفت بعدًا بخمسين قيمة، فعندها سوف تتضخم مجموعة البيانات إلى 200x50 = 10000 صفًا، وهذا سوف يتضاعف مع كل بعد نُضيفه. تجنُب القياسات المُلخصة في مجموعات البيانات متعددة الأبعاد لأنها تحتاج إعادة الحساب في كل مرةٍ تتغير فيها مجموعة البيانات، إذ يجب عليك مثلًا تضمين المجموع الكلي والعدد الكلي وحساب المتوسطات ديناميكيًا إذا كنت تخطط لعرض المتوسط، ونتمكن بهذه الطريقة من إعادة حساب المتوسطات باستخدام القيم المُلخصة عند تلخيص البيانات. تأكد من فهمك الجيد للبيانات التي تعمل عليها قبل البدء بتنفيذ أيٍ من المذكور سابقًا، فمن المحتمل أن تفترض افتراضاتٍ خاطئةٍ تقود إلى قراراتٍ مبنيةٍ على معلوماتٍ خاطئة، لذلك فإن جودة البيانات هي أولويةٌ هامة وهذا ينطبق على البيانات التي تطلبها أو تُنشئها. لا تأخذ أي مجموعة بيانات وتبني افتراضاتٍ عن بعد أو قياسٍ فيها، ولا تتردد بالسؤال دائمًا عن فهرس البيانات أو أي توثيقٍ خاصٍ بها يُمكن أن يساعد على فهم ما تتعامل معه، حيث لا يعتمد تحليل البيانات على الحدس أو التنبؤ، فربما طُبقت قواعد خاصة بعملٍ تجاريٍ أو حُذفت بيانات في خطوةٍ سابقة، وبالتالي فإن كنت لا تملك هذه المعلومات فسينتهي بك الأمر بإنتاج مجموعات بياناتٍ وتصوراتٍ لا معنى لها أو مضللة بأسوء الأحوال. سوف يساعد المثال التالي على فهم ما سبق. حالة الاستخدام المدروسة سوف نستخدم مجموعة بيانات BuzzFeed المختصة بتحليل البيانات المتعلقة بالأماكن التي يأتي منها اللاجئون إلى الولايات المتحدة والأماكن التي يذهبون إليها، حيث سنبني تطبيقًا صغيرًا يعرض لنا عدد اللاجئين الواصلين لولايةٍ محددة في عامٍ محدد، وسنعرض على وجه الخصوص واحدةً مما يلي بناءً على طلب المستخدم: العدد الكلي للواصلين إلى ولايةٍ معينة في عامٍ معينٍ. العدد الكلي للواصلين خلال كل الأعوام لولايةٍ معينة. العدد الكلي للواصلين لكل الولايات في عام معيّن. سوف تكون واجهة المستخدم عبارة عن نموذجٍ بسيطٍ لاختيار الولاية والعام، وسوف تعمل الشيفرة على: إرسال طلبٍ بالبيانات. تحويل النتيجة إلى JSON. معالجة البيانات. تسجيل أي خطأ في الطرفية. عرض النتائج للمستخدم. لن نمرّر مجموعات بياناتٍ ضخمة الحجم للمتصفح لسببين هما عرض الحزمة، وقدرات وحدة المعالجة المركزية CPU، لذلك ستُجمع البيانات في الخادم باستخدام Node.js. بيانات المصدر [{"year":2005,"origin":"Afghanistan","dest_state":"Alabama","dest_city":"Mobile","arrivals":0}, {"year":2006,"origin":"Afghanistan","dest_state":"Alabama","dest_city":"Mobile","arrivals":0}, ... ] البيانات متعددة الأبعاد: [{"year": 2005, "state": "Alabama","total": 1386}, {"year": 2005, "state": "Alaska", "total": 989}, ... ] كيفية ضبط هيكلية البيانات في المكان الصحيح واجهة AJAX وFetch API توجد عدة طرق في جافاسكربت من أجل جلب البيانات من مصدرٍ خارجيٍ، وكان علينا قديمًا استخدام طلب XHR لذلك، لأنه مدعوم على نطاقٍ واسعٍ ولكنه معقد ويتطلب استخدام عدة طرقٍ مختلفة، كما توجد مكتبات تُساعد على تخفيض التعقيد، مثل Axios أو jQuery's Ajax API، كما أنها توفّر دعمًا عبر المتصفحات، ولهذا فهي خيارٌ متاحٌ إن كنت تستخدم أحدها، لكن لا بد من اختيار الحلول الأصيلة ما أمكن. توجد أيضًا طريقة Fetch API وهي الأحدث، ولذلك فهي مدعومةٌ على نطاقٍ أضيق من سابقاتها، ولكنها أسهل وقابلة للتسلسل، وستُحوّل الشيفرة البرمجية إلى مكافئٍ مدعومٍ على نطاقٍ أوسع إن كنت تستخدم ناقلًا مثل Babel. ستُستخدم واجهة Fetch API في الحالة المدروسة لجلب البيانات للتطبيق. window.fetchData = window.fetchData || {}; fetch('./data/aggregate.json') .then(response => { // when the fetch executes we will convert the response // to json format and pass it to .then() return response.json(); }).then(jsonData => { // take the resulting dataset and assign to a global object window.fetchData.jsonData = jsonData; }).catch(err => { console.log("Fetch process failed", err); }); الشيفرة البرمجية السابقة هي جزءٌ من ملف main.js الموجود في مستودع GitHub، حيث ترسل الطريقة fetch()‎ طلبًا بالبيانات ثم نحوّل النتائج إلى JSON، وستُستخدم الطريقة then()‎ من أجل ضمان عدم تنفيذ التعليمة التالية إلا بعد جلب كامل مجموعة البيانات، وستُنفذُ جميع عمليات معالجة البيانات ضمن هذه الكتلة، كما ستُسجل الأخطاء باستخدام console.log()‎. الهدف هنا هو تحديد الأبعاد الأساسية المطلوبة لعمل تقريرٍ بالعام والولاية قبل تجميع عدد الواصلين المرتبطين بهذه الأبعاد، وإزالة الدولة الأم والمدينة المتوجهين لها، ويمكنك الاطلاع على النص البرمجي Node.js في الملف preprocess/index.js/ في مستودع GitHub لفهم كيفية إنجاز العمليات السابقة بصورةٍ أكبر، حيث يُنشئ هذا النص البرمجي ملف aggregate.json الذي يجلبه التابع fetch()‎. البيانات متعددة الأبعاد الهدف من الصيغة متعددة الأبعاد هو تحقيق المرونة بحيث تكون البيانات مُفصلةً جيدًا، حتى لا يُضطر المستخدم لطلب استعلام للخادم في كل مرةٍ يحتاج فيها إجابةً عن سؤالٍ مختلف، وتكون مُلخصةً بنفس الوقت بحيث لا يُضطر التطبيق للبحث في كامل مجموعة البيانات مع كل جزءٍ جديدٍ من البيانات. يجب علينا توقع الأسئلة وتقديم بيانات تُجيب عن هذه الأسئلة لأن المستخدم يحتاج إلى أن يكون قادرًا على إجراء بعض التحليلات دون أن يشعر بأنه مُقيدٌ أو مغمورٌ بكمٍ هائلٍ من المعلومات. ستُستخدم بيانات JSON كما هو الحال مع معظم واجهات برمجة التطبيقات APIs، فهي معيارٌ مُستخدمٌ لإرسال البيانات للتطبيقات على أنها كائناتٌ تتألف من أزواج (اسم وقيمة). ألقِ نظرةً على العينة التالية من مجموعة بياناتٍ متعددة الأبعاد قبل العودة إلى الحالة المدروسة: const ds = [{ "year": 2005, "state": "Alabama", "total": 1386, "priorYear": 1201 }, { "year": 2005, "state": "Alaska", "total": 811, "priorYear": 1541 }, { "year": 2006, "state": "Alabama", "total": 989, "priorYear": 1386 }]; تستطيع استخدام جافاسكربت على نحوٍ أكبر لتحليل مجموعة البيانات عندما تكون مُجمّعةً بطريقة صحيحة. لنُلقِ نظرةً على بعض من توابع مصفوفات جافاسكربت المُستخدمة لتكوين البيانات. كيفية العمل بفعالية مع البيانات من خلال جافاسكربت ترشيح المصفوفة تأخذ طريقة filter()‎ الخاصة بالنموذج الأولي للمصفوفة Array.prototype.filter()‎، وظيفة اختبار كل عنصرٍ ضمن المصفوفة، وتُعيد مصفوفةً أخرى تحتوي على القيم التي تجاوزت الاختبار، وهذا يسمح بإنشاء مجموعة بياناتٍ فرعيةٍ ذات معنى عند استخدام القائمة المنسدلة أو مرشحات النص، كما سيتمكن المستخدم من الاطلاع على المعلومات من خلال عرض أقسامٍ من البيانات، وهذا صحيحٌ أيضُا عند استخدام أبعادٍ ذات معنى ومنفصلة لمجموعة البيانات متعددة الأبعاد. ds.filter(d => d.state === "Alabama"); // Result [{ state: "Alabama", total: 1386, year: 2005, priorYear: 1201 },{ state: "Alabama", total: 989, year: 2006, priorYear: 1386 }] ربط بيانات المصفوفة تأخذ طريقة map()‎ الخاصة بالنموذج الأولي للمصفوفة Array.prototype.map()‎ وظيفة تمرير كل عنصرٍ في المصفوفة وإعادة مصفوفةً جديدةً بنفس عدد العناصر القديم، حيث يسمح ربط البيانات بإنشاء مجموعات بياناتٍ مرتبطة، وأحد استخدامات هذه الطريقة هو ربط بياناتٍ غامضةٍ مع بياناتٍ ذات معنى وموصوفة، أو استخدامها لأخذ قياسات وإجراء حسابات على عناصر المصفوفة لتحقيق تحليلٍ أكثر عمقًا. 1. ربط البيانات ببيانات ذات معنى ds.map(d => (d.state.indexOf("Alaska")) ? "Contiguous US" : "Continental US"); // Result [ "Contiguous US", "Continental US", "Contiguous US" ] 2. ربط البيانات مع النتائج المحسوبة ds.map(d => Math.round(((d.priorYear - d.total) / d.total) * 100)); // Result [-13, 56, 40] تلخيص بيانات المصفوفة تأخذ طريقة reduce()‎الخاصة بالنموذج الأولي للمصفوفة Array.prototype.reduce()‎ وظيفة معالجة كل عنصرٍ ضمن المصفوفة وإعادة بياناتٍ مُجمعةٍ، وتُستخدم لإنجاز حساباتٍ رياضيةٍ مثل إضافة أو ضرب كل رقمٍ ضمن المصفوفة، ويُمكن استخدامها أيضًا لضم المحارف والعديد من الأمور الأخرى، ولا بد من تعلُّم هذه الدالة من خلال مثالٍ نظرًا لصعوبتها. عليك التأكد من تلخيص البيانات عند عرضها، بحيث تُقدم فكرةً مُلخصةً للمستخدمين، ولذلك يُمكن تجميع البيانات اعتمادًا على ما يحتاجه المُستخدم حتى عند تطبيق عمليات تلخيصٍ عامة على البيانات من جهة الخادم، وبالنسبة للتطبيق الذي ننشئه، فعليّنا إضافة المجموع الكلي لكل مُدخلٍ وعرض النتيجة المُجمّعة باستخدام طريقة reduce()‎ للمرور على كل سجلٍ وإضافة القيمة الحالية إلى المجمّع، وبهذا تكون النتيجة النهائية هي المجموع الكلي لقيم كل مصفوفة. ds.reduce((accumulator, currentValue) => accumulator + currentValue.total, 0); // Result 3364 تطبيق هذه الدوال على الحالة المدروسة حالما نحصل على البيانات سنعيّن حدثّا للزر "Get the Data" من أجل عرض المجموعة الفرعية من البيانات المناسبة، حيث توجد عدة مئاتٍ من العناصر ضمن بيانات JSON الخاصة بالتطبيق، وتتواجد الشيفرة المسؤولة عن دمج البيانات مع الزر في الملف main.js الخاص بنا: document.getElementById("submitBtn").onclick = function(e){ e.preventDefault(); let state = document.getElementById("stateInput").value || "All" let year = document.getElementById("yearInput").value || "All" let subset = window.fetchData.filterData(year, state); if (subset.length == 0 ) subset.push({'state': 'N/A', 'year': 'N/A', 'total': 'N/A'}) document.getElementById("output").innerHTML = `<table class="table"> <thead> <tr> <th scope="col">State</th> <th scope="col">Year</th> <th scope="col">Arrivals</th> </tr> </thead> <tbody> <tr> <td>${subset[0].state}</td> <td>${subset[0].year}</td> <td>${subset[0].total}</td> </tr> </tbody> </table>` } سوف يأخذ حقل الولاية أو العام القيمة الافتراضية "All" عندما يكون فارغًا. ويمكن الاطلاع على الشيفرة البرمجية التالية المُتاحة في الملف js/main.js/ وإلقاءِ نظرةٍ على الدالّة ()filterData التي نسجّل ضمنها حصة الأسد من عمليتي الترشيح والتجميع. // with our data returned from our fetch call, we are going to // filter the data on the values entered in the text boxes fetchData.filterData = function(yr, state) { // if "All" is entered for the year, we will filter on state // and reduce the years to get a total of all years if (yr === "All") { let total = this.jsonData.filter( // return all the data where state // is equal to the input box dState => (dState.state === state) .reduce((accumulator, currentValue) => { // aggregate the totals for every row that has // the matched value return accumulator + currentValue.total; }, 0); return [{'year': 'All', 'state': state, 'total': total}]; } ... // if a specific year and state are supplied, simply // return the filtered subset for year and state based // on the supplied values by chaining the two function // calls together let subset = this.jsonData.filter(dYr => dYr.year === yr) .filter(dSt => dSt.state === state); return subset; }; // code that displays the data in the HTML table follows this. See main.js. كما ذُكر سابقًا فإن القيمة الافتراضية لحقل الولاية أو العام هي "All" في حال بقي فارغًا، حيث تُرشح مجموعة البيانات وتُلخص قياسات جميع الأسطر حسب هذا البعد، كما ستُرشح قيمة كلٍ من العام والولاية عند إدخالهما. لدينا الآن مثال مجرب، حيث: بدأنا بمجموعة بياناتٍ أوليةٍ ذات معاملات. ثم أنشأنا مجموعة بياناتٍ متعددة الأبعاد وشبه مُجمعة. وبنينا نتيجةً كاملةً التكوين ديناميكيًا. يُمكن التلاعب بالبيانات حالما تصل للمستخدم بعدة طرقٍ دون الحاجة للاتصال المتكرر بالخادم، وهذا مفيد جدًا في حال فقد المستخدم الاتصال، لأنه بذلك لن يفقد القدرة على التفاعل مع البيانات، وهذه ميزةٌ رائعةٌ خصوصًا عند بناء تطبيق ويب تقدُّمي Progressive Web App أو اختصارًا PWA، يحتاج أن يعمل دون اتصالٍ بالانترنت. يمكن إنشاء أي تحليلٍ لأي مجموعة بيانات حالما تُحكم قبضتك على هذه الطرق الثلاث، لذلك اربط أحد الأبعاد ضمن مجموعة البيانات مع تصنيفٍ أكثر شمولية، ولخّص البيانات باستخدام reduce. تستطيع أيضًا استخدام مكتبة D3 من أجل ربط البيانات مع جداول ورسوم بيانية تسمح بتصوُّر مرئي مخصص بالكامل لهذه البيانات. الخلاصة يُطلعنا هذا المقال على الإمكانيات الكامنة في جافاسكربت عند التعامل مع البيانات، وكما ذُكر سابقًا فإن جافاسكربت للواجهة الأمامية ليست بديلًا لتفسير وتحويل البيانات على الخادم حيث تُنجز المهمات الصعبة، كما يجب بنفس الوقت عدم استبعادها عند معالجة مجموعات البيانات بالطريقة الصحيحة. ترجمة -وبتصرّف- للمقال Taming Data with JavaScript لصاحبه Brian Greig. اقرأ أيضًا هياكل البيانات: الكائنات والمصفوفات في جافاسكريبت ماهي جافاسكريبت؟ أمثلة على الاستخدامات برمجة الكائنات Objects في جافاسكريبت
  2. يرغب الجميع بأن يصبح خبيرًا، إذ يوجد على مر السنين نوعان من البشر يُدعون "الخبراء"؛ حيث خبراء النوع الأول هم ممن يعرفون جميع الأدوات المتوفرة في لغة البرمجة ويحرصون على استخدام كل جزئيةٍ منها سواءً كانت تُفيدهم أم لا؛ والنوع الثاني هم ممن يعرفون كل جزئيةٍ من لغة البرمجة ولكنهم انتقائيون بما يجب استخدامه لحل المشاكل البرمجية، حيث يأخذون بالحسبان مجموعةً من العوامل المرتبطة بالشيفرة البرمجية وأمورًا أخرى غير برمجية. يمكننا تخمين أي نوعٍ من هؤلاء الخبراء يرغب مدير الفريق العمل معهم، إذ يقع الاختيار غالبًا على النوع الثاني من الخبراء، فهم مطورون يركزون على تسليم سطورٍ برمجيةٍ مقروءة من جافاسكربت ومفهومةٍ أيضًا، كما يعملون على صيانتها، إضافةً إلى قدرتهم على جعل المعقد يبدو بسيطًا. هناك عدة عواملٍ يجب النظر إليها من قِبل الخبراء عند كتابة شيفرةٍ مقروءة، وتحديد الخيارات الواضحة الصحيحة منها والخاطئة. الخيار الواضح لقد أضاف TC39 الكثير من الميزات الجديدة إلى ECMAScript خلال السنوات الماضية بهدف تحسين تجربة المطوّر، حيث تتضمن هذه الميزات أنماطًا مُحسنةً ومستوحاةً من لغاتٍ أخرى، وأحد هذه الإضافات في ES2019 هي Array.prototype.flat()‎، حيث يُطلب مُدخلٌ واحدٌ وهو العمق أو Infinity لنشر مصفوفة، ويكون العمق الافتراضي عند عدم استخدام أي مدخلٍ هو 1، وبدون هذه الإضافة كان علينا استخدام الصيغة التالية لنشر المصفوفة في مستوٍ واحدٍ. let arr = [1, 2, [3, 4]]; [].concat.apply([], arr); // [1, 2, 3, 4] أما عند إضافة flat()‎ أصبح من المُمكن التعبير عن نفس الوظيفة باستخدام تعليمةٍ واحدةٍ وهي الدالّة التالية. arr.flat(); // [1, 2, 3, 4] يُعَد السطر الثاني من الشيفرة أكثر قابليةً للقراءة، وسوف يتفق على هذه النقطة كِلا النوعين من الخبراء. لا يعلم جميع المطورين بوجود الدالّة flat()‎ وهم ليسوا بحاجةٍ لذلك، وعند ورودها فإن flat سيكون فعلًا يصف نفسه ويتضمن محتوى ما يفعله وهي أسهل للفهم من concat.apply()‎. من النادر وجود جوابٍ واضحٍ وصريح بأفضلية الصيغ الجديدة عن القديمة، وسوف يختار الخبير -لأي مجموعةٍ كان ينتمي- الخيار الثاني إن كان على علمٍ بكلا الخيارين لكتابة التعليمات، لأن الخبير يميل لاختيار الشيفرة الأقصر والأوضح والتي تسهل صيانتها وتتبع الخطأ فيها. الفحص الداخلي تتميز لغة جافاسكربت بأنها متعددة الاستخدام وواسعة الانتشار، فهي مُستخدمة في جميع أجزاء شبكة الويب، ويمكن كتابة الشيفرة نفسها بعدة طرقٍ ولكن لن تتمكن من تحديد الخيار الصحيح ما لم تفهم جيدًا الخيارات المتوفرة والقيود التي تُرافق كل منها. ستُستخدم البرمجة الوظيفية مع map()‎ مثالًا لذلك وستُشرح جميع التكرارات التي تعطي نفس النتيجة، ويُعَد المثال التالي النسخة الأقصر من الأمثلة التي تستخدم map()‎ حيث يتسع في سطرٍ واحدٍ. const arr = [1, 2, 3]; let multipliedByTwo = arr.map(el => el * 2); // multipliedByTwo is [2, 4, 6] سيُضاف مِحرفان فقط إلى المثال التالي وهما قوسان "()"، حيث توجد دائمًا خسارةٌ وكسب بعد أي تغيير، كما يوجد اختلافُ عند استخدام دالةٍ متعددة المتغيرات الأقواس دائمًا، ولا يوجد ضررٌ في إضافة هذين القوسين هنا فهما يحسّنان التناسق عند كتابة دالةٍ متعددة المتغيرات، وحقيقةً يجبرنا Prettier على استخدام الأقواس، فهو لا يسمح بإنشاء دالةٍ سهميّةٍ دون استخدام الأقواس. let multipliedByTwo = arr.map((el) => el * 2); ستُرفع سوية المثال وتُضاف أقواس مجموعة وتعليمة return لتبدو هذه الدالّة أشبه بالدوال التقليدية. هذه الإضافات مطلوبة عندما تكون الدالّة أكثر من سطرٍ واحد، وبالتأكيد ستُكتب العديد من الدوال المتضمنة أكثر من سطر واحد ضمن الشيفرة البرمجية. let multipliedByTwo = arr.map((el) => { return el * 2; }); ستُزال الآن الدالّة السهمية وتُستخدم نفس صيغة المثال السابق لكن مع استخدام الكلمة المفتاحية function، وهذا مثير لأن هذه الصيغة سوف تعمل في جميع الحالات، ولن ينتج أي خطأ مهما كان عدد المتغيرات أو السطور، وذلك نظرًا لكونها مقروءةً ومألوفة أكثر من الصيغة المستخدمة في المثال الأول، ولكن هل هذا التغيير سيء؟ وكيف سيكون وقعه على مُبرمجٍ جديد أو شخصٍ خبير جدًا بلغةٍ مختلفةٍ عن جافاسكريبت؟ هل سوف تُشكل هذه الصيغة عبئًا على شخص يعرف جافاسكربت جيدًا بالموازنة مع المثال الأول؟ let multipliedByTwo = arr.map(function(el) { return el * 2; }); وصلنا الآن إلى الخيار الأخير وهو تمرير الدالّة فقط، حيث يمكن كتابة timesTwo بأي طريقةٍ تريدها ولا توجد مشكلة يُسببها تمرير اسم الدالّة في أية حالة، لكن لنتوقف للحظة ونفكر فيما إذا كانت هذه الصيغة تُسبب الحيرة أم لا، فإذا كنت جديدًا على طريقة كتابة هذه الشيفرة، فلن يكون واضحًا لك أن timesTwo هي دالّة وليست كائنًا، ولكن map()‎ موجودة لتُعطيك تلميحًا، لذلك هناك العديد من الأمور الهامة الواجب الانتباه إليها وهي امكانية إغفال هذه الدالة، وسهولة العثور عليها، وتأثيرها على النتيجة. const timesTwo = (el) => el * 2; let multipliedByTwo = arr.map(timesTwo); لا توجد إجابةٌ واضحةٌ هنا، حيث أن اتخاذ الخيار الصحيح لطريقة كتابة الشيفرة يعني فهم جميع الخيارات المتوفرة ومحدوديتها، كما يجب إدراك أن التناسق يتطلب استخدام أقواس المجموعة والأقواس العادية والكلمة المفتاحية return. يجب أن تسأل نفسك مجموعةً من الأسئلة عند كتابة شيفرة برمجية، والأسئلة المرتبطة بالأداء هي الأكثر رواجًا، ولكن عندما تنظر إلى شيفرة برمجية متطابقة من الناحية الوظيفية، فيجب أن يكون اختيارك مبنيًا على الإنسان وكيفية فهم غيرك لهذه الشيفرة. ليس بالضرورة أن يكون الأحدث هو الأفضل طُرح حتى الآن مثالٌ يوّضح أي صيغةٍ برمجيةٍ جديدةٍ، بحيث يستخدم كل نوعٍ من الخبراء حتى وإن كانت غير معروفةٍ عالميًا، كما طُرح مثالٌ يدعو للكثير من التساؤلات دون إجاباتٍ كافية، وأصبح الوقت الآن مُناسبًا للتعمق أكثر في الشيفرة البرمجية التي كُتبت سابقًا ثم أُزيلت، وهي الشيفرة البرمجية التي استخدمت صيغًا غير معروفة على نطاقٍ واسع لحل مشكلة، إضافةً إلى قابلية صيانتها بالاعتماد على القواعد المعروفة لكتابة الشيفرة. يسمح لك التعيين بالتفكيك Destructing assignment؛ إمكانية تفريغ القيم من الكائنات (أو المصفوفات) وهو مماثل للشكل الآتي. const {node} = exampleObject; وهو يُهيئ متغيرًا ويُسند له قيمةً في سطرٍ واحد ولكن هذا غير ضروري. let node ;({node} = exampleObject) يُسنِد السطر الأخير من الشيفرة البرمجية قيمةً للمتغير باستخدام التفكيك، لكن التصريح عن المتغير يشغل سطرًا قبله، وهذا أمرٌ يرغب الكثير باستخدامه، إلا أن الغالبية لا تُدرك إمكانية ذلك. نلاحظ عند النظر إلى الشيفرة بتمعُّن؛ وجود الفاصلة المنقوطة غير المُستخدمة لإنهاء السطر كما جرت العادة، إضافةً إلى استخدام الأقواس للإحاطة بالتعليمة واستخدام أقواس مجموعة داخلها أيضًا، وبالتأكيد فإن وظيفة هذه التعليمة غير واضحة، فمن الصعب قراءتها، و يجب ألا تكون موجودة في أي شيفرةٍ مكتوبة من قِبل خبير. let node node = exampleObject.node تحل هذه الشيفرة المشكلة فهي تعمل ووظيفتها واضحة، كما يسهل فهمها دون الحاجة للبحث عنها، وبالتالي فلا يجب استخدام الصيغة التفكيكية فقط لأننا نستطيع ذلك. الشيفرة ليست كل شيء كما هو ملاحظ فقد كان حل خبراء النوع الثاني واضحًا بالاعتماد على الشيفرة فقط، ومع ذلك لا يزال هناك فرقٌ واضحٌ بالنسبة للشيفرة التي يكتبها كل خبير، وذلك لأن الشيفرة موجهةٌ للآلة كي تقرأها وللإنسان كي يُفسرها، وبالتالي لا توجد عوامل مرتبطة بالشيفرة هنا لأخذها بالحسبان. تختلف صيغة لغة البرمجة التي تستخدمها من أجل فريقٍ من مطوّري جافاسكربت عن تلك التي تستخدمها من أجل فريقٍ متعدد اللغات لم يتعمق أفراده في تفاصيل اللغة، ونذكر على سبيل المثال الصيغة spread مقابل concat()‎، حيث أُضيفت spread إلى ECMAScript منذ عدة سنوات واعتُمدت على نحوٍ واسعٍ، فهي تُعَد ميزةً للصيغة تسمح لك بإنجاز العديد من الأمور وأحدها هو تعاقب تسلسل مجموعةٍ من المصفوفات. const arr1 = [1, 2, 3]; const arr2 = [9, 11, 13]; const nums = [...arr1, ...arr2]; بالرغم من فعالية وقوة spread إلا أنها ليست رمزًا بديهيًا تستطيع التنبؤ بعمله، وبالتالي إن كنت لا تعلم وظيفته فلن يكون مفيدًا. بينما يعتقد العديد من الخبراء أن هذه الصيغة مألوفةً بالنسبة لفريقٍ من مُختصي جافاسكربت، يتساءل خبراء النوع الثاني عما إذا كان هذا الكلام صحيحًا بالنسبة لفريقٍ من المبرمجين متعددي اللغات، لذلك فمن المُحتمل أن يختار هؤلاء الخبراء استخدام concat()‎ بدلًا من spread لأن اسمها يُعبّر عن عملها. ينتج عن الشيفرة البرمجية التالية نفس النتيجة التي تُقدمها spread في المثال السابق. const arr1 = [1, 2, 3]; const arr2 = [9, 11, 13]; const nums = arr1.concat(arr2); كان هذا أحد الأمثلة لتأثير العامل البشري على خيارات الشيفرة البرمجية، لذلك عند التعامل مع شيفرةٍ برمجيةٍ تعمل عليها فرقٌ مختلفة، فلا بد من الالتزام بمعاييرٍ صارمةٍ لا تراعي بالضرورة الصيغة الأحدث للغة البرمجة، وتنتقل بعد ذلك لعواملٍ أخرى مرتبطة بالأدوات المُستخدمة والتي تجعل العمل أكثر سلاسةً أو صعوبةً لأفراد الفريق. توجد شيفرات برمجية من الصعب اختبارها، وشيفرات من الصعب استخدامها على مجالٍ أوسع في المستقبل نظرًا لصعوبة إضافة وظائف إضافية لها، وشيفرات ذات أداءٍ ضعيف، وشيفرات لا تستطيع التعامل مع متصفحاتٍ مختلفة، وهذه جميعها عواملٌ يُفكر بها خبراء النوع الثاني، إضافةً إلى تأثير الأسماء المستخدمة. ولكن حتى هم لا يستطيعون اعتماد صيغةٍ صحيحةٍ معظم الوقت لهذا الأمر. الخلاصة لا يُثبت الخبير نفسه من خلال قدرته على استخدام كل جزئية من خواص لغة البرمجة، بل من خلال معرفة ما يكفي من هذه الخواص لاستخدامٍ حكيمٍ لصيغةٍ برمجية واتخاذ القرارات المناسبة، فهكذا يكون الخبراء، وبهذه الطريقة يساهمون في تخريج خبراء جُدد، وهذا يعني الكثير لمن يجد نفسه خبيرًا أو يطمح إلى أن يصبح خبيرًا، إذ عليه أن يسأل نفسه العديد من الأسئلة عند كتابة شيفرة برمجية والتفكير بمن يُتابعه من المطوّرين، حيث أن أفضل شيفرةٍ برمجية هي تلك التي تُنفِذ شيئًا معقدًا، ولكنها بنفس الوقت مفهومةٌ عند مراجعتها من قِبل فريق العمل، وهذا بالطبع ليس أمرًا سهل المنال ولا توجد طريقةٌ واضحةٌ للوصول له لكن لا بد من أخذه بالحسبان عند كتابة أي دالّة. ترجمة -وبتصرّف- للمقال Human-Readable JavaScript: A Tale of Two Experts لصاحبه Laurie Barth. اقرأ أيضًا تطويع البيانات في جافاسكربت التخاطب بين نوافذ المتصفح عبر جافاسكريبت مشروع تطبيقي لبناء رجل آلي (روبوت) عبر جافاسكريبت
  3. تُعرِّف HTML العنصر <canvas> على أنه "لوحة نقطية ذات أبعاد معينة يمكن استخدامها لعرض المخططات ورسومات الألعاب وغيرها من الصور المرئية برمجيًا". ويُمثِّل مستطيلًا في صفحتك حيث تستخدم JavaScript لرسم أي شيء تريده فيه. الدعم الأساسي للعنصر canvas IE Firefox Safari Chrome Opera iPhone Android 7.0+* 3.0+ 3.0+ 3.0+ 10.0+ 1.0+ 1.0+ * يتطلب IE ‏7 و 8 مكتبة خارجية هي explorercanvas، أما IE 9 فهو يدعم العنصر <canvas> داخليًا. كيف يبدو عنصر canvas؟ ليس له شكل، حقًا! ليس في عنصر <canvas> أي محتوى وليس له إطارٌ حتى. يُضاف عنصر canvas كالآتي: <canvas width="300" height="225"></canvas> لنضف إطارًا منقطًا لكي نستطيع أن نرى ما الذي نتعامل معه: الشكل 1: عنصر canvas مع إطار يمكن أن يكون لديك أكثر من عنصر <canvas> في نفس الصفحة، وسيظهر كل عنصر على حدة في شجرة DOM، ويحافظ كل عنصر canvas على خاصياته؛ فإن أعطيت كل عنصر canvas خاصية id، فيمكنك الوصول إليه كما تفعل مع أي عنصر آخر. لنوسِّع الشيفرة السابقة لكي تتضمن خاصية id: <canvas id="a" width="300" height="225"></canvas> أصبح بإمكاننا بسهولة العثور على عنصر <canvas> السابق في شجرة DOM. var a_canvas = document.getElementById("a"); الأشكال البسيطة IE Firefox Safari Chrome Opera iPhone Android 7.0+* 3.0+ 3.0+ 3.0+ 10.0+ 1.0+ 1.0+ * يتطلب IE ‏7 و 8 مكتبة خارجية هي explorercanvas، أما IE 9 فهو يدعم رسم الأشكال البسيطة في العنصر <canvas> داخليًا. يبدأ كل عنصر canvas فارغًاـ ثم علينا الرسم فيه. لنبدأ ببعض الأشكال. يمكن استخدام الحدث (action) ‏onclick لاستدعاء الدالة الآتية عندما يضغط المستخدم على زرٍ ما: function draw_b() { var b_canvas = document.getElementById("b"); var b_context = b_canvas.getContext("2d"); b_context.fillRect(50, 25, 150, 100); } لا يوجد شيءٌ مميزٌ في أول سطر من الدالة، إذ أنَّ مهمته هي العثور على عنصر <canvas> الموجود في شجرة DOM. function draw_b() { var b_canvas = document.getElementById("b"); var b_context = b_canvas.getContext("2d"); b_context.fillRect(50, 25, 150, 100); } لدى كل عنصر canvas ما نسميه "إطار الرسم" (drawing context)، الذي يحدث فيه كل الأمور المسلية. فبعد أن تعثر على عنصر <canvas> في شجرة DOM (باستخدام document.getElementById()‎ أو أيّة طريقة أخرى) ستستطيع أن تستدعي الدالة getContext()‎، يجب عليك تمرير السلسلة النصية "2d" دومًا إلى الدالة getContext()‎. س: هل يمكن رسم رسوميات 3D في canvas؟ ج: نعم، عبر تقنية WebGL التي يمكنها رسم الأشكال ثلاثية الأبعاد في المتصفحات دون إضافات. تدعم أغلبية المتصفحات الحديثة هذه التقنية (Firefox الإصدار الرابع وما بعده، و Chrome الإصدار التاسع وما بعده، و Opera 12 وما بعده، و Safari 5.1 وما بعده، و IE 11)؛ يتم العمل على تطوير هذه التقنية في مجموعة عمل WebGL. إذًا، أصبح لديك عنصر <canvas> ولديك إطار الرسم (drawing context) الخاص به. إطار الرسم هو المكان الذي ستُعرَّف فيه جميع دوال وخاصيات الرسم. هنالك مجموعةٌ كاملةٌ من الخاصيات والدوال المُكرَّسة لرسم المستطيلات: يمكن أن تكون الخاصية fillStyle لونًا من ألوان CSS، أو نقشًا (pattern)، أو تدرجًا لونيًا (gradient) (سنذكر مزيدًا من المعلومات عن التدرجات اللونية بعد قليل). القيمة الافتراضية لهذه الخاصية هي اللون الأسود، لكنك تستطيع أن تضبطها لما تشاء. سيتذكر كل إطار رسم (drawing context) خاصياته لطالما بقيت الصفحة مفتوحةً إلا إن فعلت شيئًا لإعادة ضبطه. الدالة (fillRect(x, y, width, height ترسم مستطيلًا مملوءًا باللون أو النقش الموجود في fillStyle. الخاصية strokeStyle شبيهة بخاصية fillStyle، فيمكن أن تكون لون CSS أو نقشًا أو تدرجًا لونيًا. الدالة (strokeRect(x, y, width, height ترسم مستطيلًا دون ملئه، إذ ترسم حوافه وإطاره الخارجي فحسب، وتَستعمل الخاصية strokeStyle لذلك. الدالة (clearRect(x, y, width, height تسمح كل البكسلات الموجودة في المستطيل المُحدَّد. س: هل يمكنني أن أعيد ضبط لوحة الرسم في عنصر canvas؟ ج: نعم، فسيؤدي تحديد عرض أو ارتفاع عنصر <canvas> إلى إعادة ضبط (reset) كل الخاصيات في إطار الرسم إلى قيمها الافتراضية. لاحظ أنَّه ليس من الضروري تغيير العرض، إذ يمكنك أن تضبطه إلى قيمته الحالية كما يلي: var b_canvas = document.getElementById("b"); b_canvas.width = b_canvas.width; بالعودة إلى الشيفرة في المثال السابق: var b_canvas = document.getElementById("b"); var b_context = b_canvas.getContext("2d"); b_context.fillRect(50, 25, 150, 100); سترسم الدالة fillRect()‎ عند استدعائها مستطيلًا وتملؤه بنمط الملء الحالي الذي هو اللون الأسود (إلا إذا غيرتَه). يُعرَّف المستطيل عبر زاويته العليا اليسرى (50، 25)، وعرضه (150)، وارتفاعه (100). لكي نفهم ذلك فهمًا جيدًا، فلنلقِ نظرةً إلى نظام الإحداثيات في canvas. الإحداثيات في عنصر canvas يمكننا تخيل لوحة الرسم في عنصر canvas على أنها شبكة ثنائية البعد، ويكون مبدأ الإحداثيات فيها (‎0, 0) في الزاوية العليا اليسرى من لوحة الرسم. تزداد القيم على المحور X عند الانتقال نحو الحافة اليمنى من لوحة الرسم، وتزداد القيم على المحور Y بالانتقال نحو الحافة السفلية من لوحة الرسم. الشكل 2: توضيح لنظام الإحداثيات في عنصر canvas رُسِمَ الشكل السابق عبر عنصر <canvas> الذي يحتوي على: مجموعة من الخطوط الأفقية مجموعة من الخطوط الشاقولية خطين أفقيين سوداوين خطين صغيرين سوداوين مائلين يشكلان سهمًا خطين شاقوليين سوداوين خطين صغيرين سوداوين مائلين يشكلان السهم الآخر الحرف "x" الحرف "y" النص "(‎(0, 0" قرب الزاوية العليا اليسرى النص "(‎500, 375)" قرب الزاوية السفلى اليمنى نقطة في الزاوية العليا اليسرى، وأخرى في الزاوية السفلى اليمنى علينا أولًا تعريف العنصر <canvas> نفسه، حيث يُحدِّد العنصر <canvas> خاصية العرض width والارتفاع height، والمُعرِّف id كي نستطيع العثور عليه بسهولة لاحقًا. <canvas id="c" width="500" height="375"></canvas> ثم علينا كتابة سكربت لكي نجد عنصر <canvas> في شجرة DOM ونحصل على إطار الرسم الخاص به. var c_canvas = document.getElementById("c"); var context = c_canvas.getContext("2d"); نستطيع الآن البدء في رسم الخطوط. المسارات IE Firefox Safari Chrome Opera iPhone Android 7.0+* 3.0+ 3.0+ 3.0+ 10.0+ 1.0+ 1.0+ * يتطلب IE ‏7 و 8 مكتبة خارجية هي explorercanvas، أما IE 9 فهو يدعم المسارات في العنصر <canvas> داخليًا. تخيل أنك تريد أن ترسم رسمةً بالحبر، من المؤكد أنَّك لن تبدأ الرسم بأقلام الحبر مباشرةً، لأنك قد تُخطئ؛ فعوضًا عن ذلك سترسم المستقيمات والمنحنيات بقلم الرصاص، ثم إن أعجبتك فيمكنك أن "تُحبِّرَها". هنالك ما نسميه "المسارات" (paths) في عناصر canvas، وتعريف المسار يُشبِه تمامًا الرسم بقلم الرصاص؛ يمكنك رسم ما تشاء لكنه لن يكون جزءًا من اللوحة النهائية إلا إن أمسكت أقلام التحبير ومررتها فوق المسار الذي رسمتَه. استعمل الدالتين الآتيتين لرسم المستقيمات "بقلم الرصاص": (moveTo(x, y تحريك قلم الرصاص إلى نقطة البداية المُحدَّدة. (lineTo(x, y رسم خط إلى نقطة النهاية المُحدَّدة. سيزداد حجم المسار كلما استدعيت الدالتين moveTo()‎ و lineTo()‎. ترسم الدالتان السابقتان "بقلم الرصاص" (يمكنك أن تسمي هذه العملية ما تشاء)، أي أنَّك لن ترَ شيئًا على لوحة الرسم إلى أن تستدعي إحدى دوال "التحبير". لنبدأ برسم الشبكة: //رسم المستقيمات الشاقولية for (var x = 0.5; x < 500; x += 10) { context.moveTo(x, 0); context.lineTo(x, 375); } //رسم المستقيمات الأفقية for (var y = 0.5; y < 375; y += 10) { context.moveTo(0, y); context.lineTo(500, y); } تلك الدوال "رصاصية"، أي لن يظهر شيءٌ على لوحة الرسم بعد؛ إذ سنحتاج إلى دالة "تحبير" لإظهار تلك الخطوط. context.strokeStyle = "#eee"; context.stroke(); الدالة storke()‎ هي إحدى دوال "التحبير"، وهي تُحبِّر المسار المعقد الذي عرَّفتَه بدوال moveTo()‎ و lineTo()‎ السابقة. خاصية strokeStyle تتحكم بلون تلك الخطوط. هذه هي النتيجة: الشكل 3: شبكة مرسومة داخل عنصر canvas س: لماذا بدأت x و y من 0.5 وليس من 0؟ ج: تخيل أنَّ كل بكسل هو مربع كبير، وأنَّ الإحداثيات ذات الرقم الكامل (0، 1، 2…) هي حواف تلك المربعات؛ فإذا أردت أن ترسم خطًا عرضُه واحدة الأطوال (one-unit-wide) بين الإحداثيات ذات الرقم الكامل، فسوف يمتد إلى أن يصل إلى طرفَي المربع (انظر الشكل الآتي للإيضاح)، وسيكون الخط الناتج مرسومًا بعرض بكسلين. أما لرسم خط عرضه بكسل واحد، فعليك أن تُزيح الإحداثيات بمقدار 0.5 عموديًا على منحى (أو اتجاه) الخط. على سبيل المثال، إذا كنت تحاول رسم خط من (‎1, 0) إلى (‎1, 3)، فسيرسم المتصفح خطًا يغطي 0.5 بكسل على جانبَيّ x=1، ولكن الشاشة غير قادرة على عرض نصف بكسل، لذلك سيمتد الخط لكي يُغطي بكسلين: أما لو أردت أن ترسم خطًا من (1.5, 0) إلى (1.5, 3)، فسيرسم المتصفح خطأ يغطي 0.5 بكسل على طرفَي x=1.5، الذي يؤدي إلى رسم خط بعرض 1 بكسل: لنرسم الآن السهم الأفقي. جميع الخطوط والمنحنيات الموجودة على نفس المسار ستُرسَم بنفس اللون (أو التدرج اللوني، وسنأتي لذكر ذلك لاحقًا). لكننا نريد أن نرسم السهم بلون مختلف (الأسود)، لذلك نحتاج إلى البدء بمسار (path) جديد. //مسار جديد context.beginPath(); context.moveTo(0, 40); context.lineTo(240, 40); context.moveTo(260, 40); context.lineTo(500, 40); context.moveTo(495, 35); context.lineTo(500, 40); context.lineTo(495, 45); وبنفس الطريقة نرسم السهم الشاقولي؛ ولمّا كان السهم الشاقولي بنفس لون السهم الأفقي، فلا حاجة إلى إنشاء مسار جديد، إذ سيُشكِّل السهمان مسارًا واحدًا. //لا حاجة إلى إنشاء مسار جديد context.moveTo(60, 0); context.lineTo(60, 153); context.moveTo(60, 173); context.lineTo(60, 375); context.moveTo(65, 370); context.lineTo(60, 375); context.lineTo(55, 370); لقد قلتُ أنَّ هذين السهمين يجب أن يُرسما باللون الأسود، لكن لون strokeStyle ما يزال "الفضي الفاتح" (لا تتم عملية إعادة تعيين قيم fillStyle و strokeStyle عندما تبدأ مسارًا جديدًا). لا بأس، لأننا رسمنا إلى الآن "بالرصاص" ولم "نُحبِّر" بعد، علينا الآن أن نضبط قيمة strokeStyle إلى اللون الأسود؛ وإلا سيُرسم هاذين السهمين بالفضي الفاتح وسيصعب علينا رؤيتهما. سيُغيّر السطران الآتيان اللون إلى الأسود ويرسمان الخطوط في لوحة الرسم: context.strokeStyle = "#000"; context.stroke(); هذه هي النتيجة: الشكل 4: المحوران مرسومان دون تسمية على لوحة الرسم النص IE Firefox Safari Chrome Opera iPhone Android 7.0+* 3.0+ 3.0+ 3.0+ 10.0+ 1.0+ 1.0+ * يتطلب IE ‏7 و 8 مكتبة خارجية هي explorercanvas، أما IE 9 فهو يدعم طباعة النصوص في العنصر <canvas> داخليًا. ** يتطلب Firefox 3.0 مكتبة للتوافقية. بالإضافة إلى القدرة على رسم خطوط في لوحة الرسم، يمكنك أيضًا "رسم" الجمل النصية. وعلى عكس النصوص في الصفحة المحيطة بلوحة الرسم، لا يوجد هنالك أيّة خاصيات CSS تتعلق بالتخطيط (layout) مثل floats و margins و padding و word wrapping. تستطيع ضبط بعض خاصيات الخط، ثم اختيار نقطة على لوحة الرسم و"رسم" نصك هناك. تتوفر خاصيات الخطوط الآتية في لوحة الرسم: الخاصية font التي يمكن أن تُضبَط إلى أي شيءٍ تستطيع ضبطه في خاصية font في CSS. وهذا يتضمن نمط الخط، ونوع الخط، وسماكة الخط، وحجم الخط، وارتفاع السطر، واسم "عائلة" الخط. الخاصية textAlign تتحكم بمحاذاة النص، وهي شبيهة (لكن ليس مماثلة تمامًا) بخاصية text-align في CSS. القيم المحتملة هي start و end و left و right و center. الخاصية textBaseline تتحكم في مكان "رسم" النص نسبةً إلى نقطة البداية. القيم المحتملة هي top أو hanging أو middle أو alphabetic أو ideographic أو bottom. الخاصية textBaseline معقدة ودقيقة، لأن طريقة كتابة النصوص معقدة (لا أقصد هنا الإنكليزية، لكنك تستطيع رسم أي محرف يونيكود في عنصر canvas، وكتابة نصوص يونيكود معقدة)، تشرح مواصفات HTML5 مختلف خطوط الأساس (baselines): أعلى مربع em هو تقريبًا على مستوى أعلى المحارف في الخط (font)، أما "hanging baseline" فهو مكان ارتكاز بعض المحارف مثل आ، أما خطmiddle فهو يقع في منتصف المسافة بين أعلى مربع em وأسفل مربع em. أما خط alphabetic فهو مكان ارتكاز الأحرف مثل Á و ÿ و f و Ω، وخط ideographic هو مكان ارتكاز المحارف مثل 私 و 達، وأسفل مربع em هو تقريبًا أسفل المحارف الموجودة في خطٍ (font) ما. قد يكون أعلى وأسفل الصندوق المحيط (bounding box) بعيدًا عن خطوط الأساس السابقة نتيجةً لامتداد بعض المحارف خارج مربع em. الشكل 5: خطوط الأساس تستطيع استخدام top أو middle أو bottom لخاصية textBaseline للأبجديات البسيطة مثل الإنكليزية دون أن تكترث للبقية. لنرسم بعض النصوص! النص المرسوم داخل عنصر canvas يرث حجم الخط ونمطه من عنصر <canvas> نفسه، لكنك تستطيع إعادة تعريف تلك القيم بضبط خاصية font في إطار الرسم. //تغيير نمط الخط context.font = "bold 12px sans-serif"; context.fillText("x", 248, 43); context.fillText("y", 58, 165); ترسم الخاصية fillText()‎ النص. context.font = "bold 12px sans-serif"; //رسم النص context.fillText("x", 248, 43); context.fillText("y", 58, 165); س: هل أستطيع استخدام مقاسات الخطوط النسبية لرسم النصوص في عنصر canvas؟ ج: نعم. مثل أي عنصر HTML آخر في صفحتك، يحسب العنصر <canvas> مقاس الخط بناءً على قواعد CSS في صفحتك. فإذا ضبطت خاصية context.font إلى مقاس خط نسبي مثل 1.5em أو ‎150%، فسيضرب متصفحك هذا الرقم بحجم الخط المحسوب لعنصر <canvas> نفسه. أريد أن يكون أعلى النص الموجود في الزاوية العليا اليسرى على بعد y=5 من الحافة العلوية، لكنني كسول ولا أريد قياس ارتفاع النص وحساب بعد خط الأساس (baseline)، وإنما سأضبط textBaseline إلى top وسأمرر إحداثيات الزاوية العليا اليسرى من مربع النص. context.textBaseline = "top"; context.fillText("( 0 , 0 )", 8, 5); أما للنص في الزاوية السفلى اليمنى، فسأرسم الزاوية السفلى اليمنى للنص في الإحداثيات (492,370)، أي بضعة بكسلات من الزاوية السفلى اليمنى من لوحة الرسم؛ لكنني لا أريد قياس عرض أو ارتفاع النص، لهذا أضبط الخاصية textAlign إلى right والخاصية textBaseline إلى botton، ثم استدعي الدالة fillText()‎ مُمرِّرًا إليها إحداثيات الزاوية السفلى اليمنى من مربع النص. context.textAlign = "right"; context.textBaseline = "bottom"; context.fillText("( 500 , 375 )", 492, 370); وهذه هي النتيجة: الشكل 6: تسمية المحاور لقد نسينا النقاط الموجودة على الزوايا! سنتعلم كيف نرسم الدوائر لاحقًا، أما الآن، فسأغش قليلًا وأرسمها كمستطيلات. context.fillRect(0, 0, 3, 3); context.fillRect(497, 372, 3, 3); هذا كل ما في الأمر! هذه هي النتيجة النهائية: الشكل 7: الرسم النهائي للإحداثيات عبر عنصر canvas التدرجات اللونية IE Firefox Safari Chrome Opera iPhone Android التدرجات اللونية الخطية 7.0+* 3.0+ 3.0+ 3.0+ 10.0+ 1.0+ 1.0+ التدرجات اللونية الشعاعية 9.0+ 3.0+ 3.0+ 3.0+ 10.0+ 1.0+ 1.0+ * يتطلب IE ‏7 و 8 مكتبة خارجية هي explorercanvas، أما IE 9 فهو يدعم التدرجات اللونية في العنصر <canvas> داخليًا. لقد تعلمنا سابقًا في هذا الفصل كيف نرسم مستطيلًا مملوءًا بلونٍ ما، ثم تعلمنا كيف نرسم مستطيلًا ذا إطارٍ ملوَّن بلونٍ معين. لكن الأمر ليس محدودًا للألوان فقط، إذ يمكنك فعل ما تشاء بالتدرجات اللونية، لننظر إلى أحد الأمثلة. الشكل 8: تدرج لوني من اليسار إلى اليمين سيبدو وسم canvas كغيره من الوسوم: <canvas id="d" width="300" height="225"></canvas> علينا أولًا أن نعثر على عنصر <canvas> ثم نحصل على إطار الرسم: var d_canvas = document.getElementById("d"); var context = d_canvas.getContext("2d"); يمكننا أن نبدأ بتعريف التدرج اللوني بعد حصولنا على إطار الرسم. التدرج اللوني هو انتقالٌ ناعمٌ بين لونين أو أكثر. يمكن تعريف نوعين من التدرجات اللونية في إطار الرسم: الدالة (createLinearGradient(x0, y0, x1, y1 ترسم تدرجًا لونيًا عبر قطعة مستقيمة من النقطة (x0, y0) إلى (x1, y1). الدالة (createRadialGradient(x0, y0, r0, x1, y1, r1 ترسم تدرجًا لونيًا عبر مخروط (cone) بين دائرتين. وتُمثِّل أول ثلاثة معاملات (parameters) دائرة البداية ذات المركز (x0, y0) ونصف القطر r0. أما آخر ثلاثة معاملات فهي تمثل دائرة النهاية ذات المركز (x1, y1) ونصف القطر r1. لنُنشِئ تدرجًا لونيًا خطيًّا (linear)؛ يمكن أن تكون التدرجات اللونية بأي قياس، لكننا سنجعل هذا التدرج بعرض 300 بكسل، مثل لوحة الرسم. var my_gradient = context.createLinearGradient(0, 0, 300, 0); لما كانت قيم y (الوسيط الثاني والرابع) تساوي 0، فسيكون اتجاه التدرج اللوني من اليسار إلى اليمين. يمكنك تعريف ألوان التدرج بعد أن تحصل على كائن التدرج اللوني. يمكن أن يكون للتدرج محطتَي توقف لوني أو أكثر. التوقف اللوني (color stop) يمكن أن يكون في أي مكان في التدرج. ولإضافة توقف لوني، عليك تحديد مكانه ضمن التدرج اللوني، يمكن أن تكون تلك القيم بين 0 و 1. لنُعرِّف تدرجًا لونيًا من الأسود إلى الأبيض. my_gradient.addColorStop(0, "black"); my_gradient.addColorStop(1, "white"); لا يُرسَم أيّ شيءٍ في لوحة الرسم عند تعريف التدرج اللوني، إذ سيُخزَّن ذاك الكائن في مكانٍ ما في الذاكرة. أما لرسم التدرج اللوني، فعليك أن تضبط خاصية fillStyle إلى ذاك التدرج ثم ترسم شكلًا ما مثل مستطيل أو مستقيم. context.fillStyle = my_gradient; context.fillRect(0, 0, 300, 225); وهذه هي النتيجة: الشكل 9: تدرج لوني من اليسار إلى اليمين لنفترض أنَّك تريد تدرجًا لونيًا من الأعلى إلى الأسفل؛ فسيكون عليك إنشاء كائن للتدرج اللوني تكون فيه قيمة x ثابتة (الوسيطين الأول والثالث)، وتجعل قيم y (الوسطين الثاني والرابع) تتراوح بين 0 إلى ارتفاع لوحة الرسم. var my_gradient = context.createLinearGradient(0, 0, 0, 225); my_gradient.addColorStop(0, "black"); my_gradient.addColorStop(1, "white"); context.fillStyle = my_gradient; context.fillRect(0, 0, 300, 225); وهذه هي النتيجة: الشكل 10: تدرج لوني من الأعلى إلى الأسفل وتستطيع أيضًا أن تجعل التدرجات اللونية قطرية. //قيم x و y متغيرة var my_gradient = context.createLinearGradient(0, 0, 300, 225); my_gradient.addColorStop(0, "black"); my_gradient.addColorStop(1, "white"); context.fillStyle = my_gradient; context.fillRect(0, 0, 300, 225); وهذه هي النتيجة: الشكل 11: تدرج لوني قطري الصور IE Firefox Safari Chrome Opera iPhone Android 7.0+* 3.0+ 3.0+ 3.0+ 10.0+ 1.0+ 1.0+ * يتطلب IE ‏7 و 8 مكتبة خارجية هي explorercanvas، أما IE 9 فهو يدعم تضمين الصور في العنصر <canvas> داخليًا. يُظهِر الشكل الآتي قطةً معروضةٌ عبر عنصر <img>: الشكل 12: قطة معروضة عبر عنصر img أما هذه فهي نفس القطة لكن مرسومة في عنصر canvas: الشكل 13: قطة معروضة ضمن عنصر canvas هنالك دالة اسمها drawImage()‎ في إطار الرسم تُستعمل لتضمين صورة في عنصر canvas. تأخذ هذه الدالة ثلاثة، أو خمسة، أو تسعة وسائط: (drawImage(image, dx, dy تأخذ صورةً وترسمها في لوحة الرسم، والإحداثيات المعطية (dx, dy) هي إحداثيات الزاوية العليا اليسرى من الصورة. فلو مررنا الإحداثيات (‎(0, 0 فستُرسَم الصورة في الزاوية العليا اليسرى من لوحة الرسم. (drawImage(image, dx, dy, dw, dh تأخذ صورةً وتغير عرضها إلى dw وارتفاعها إلى dh، ثم ترسمها على لوحة الرسم في الإحداثيات (dx, dy). (drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh تأخذ صورةً وتقتطعها لتصبح مساويةً إلى المستطيل (sx, sy, sw, sh) ثم تغير أبعادها إلى (dw, dh) ثم ترسمها على لوحة الرسم في الإحداثيات (dx, dy). توضِّح مواصفات HTML5 معاملات (parameters) الدالة drawImage()‎: المستطيل المصدر هو المستطيل (ضمن حدود الصورة المصدرية) الذي تكون رؤوس أضلاعه هي (sx, sy)، و (sx+sw, sy)، و (sx+sw, sy+sh)، و (sx, sy+sh). أما المستطيل الوجهة، فهو المستطيل (ضمن حدود لوحة الرسم) الذي تكون رؤوس أضلاعه هي النقاط (dx, dy)، و (dx+dw, dy)، و (dx+dw, dy+dh)، و (dx, dy+dh). الشكل 14: تمثيل رسومي لمعاملات الدالة drawImage لرسم صورة داخل لوحة الرسم، فستحتاج إلى صورة! يمكن أن تكون الصورة عنصر <img> موجود مسبقًا، أو بإمكانك إنشاء كائن Image()‎ باستخدام JavaScript. وفي كلا الحالتين عليك أن تتأكد أنَّ الصورة محمَّلة تحميلًا كاملًا قبل أن ترسمها في لوحة الرسم. إذا كنت تستخدم عنصر <img> موجود مسبقًا، فيمكنك رسم الصورة في لوحة الرسم أثناء الحدث window.onload. <img id="cat" src="images/cat.png" alt="sleeping cat" width="177" height="113"> <canvas id="e" width="177" height="113"></canvas> <script> window.onload = function() { var canvas = document.getElementById("e"); var context = canvas.getContext("2d"); var cat = document.getElementById("cat"); context.drawImage(cat, 0, 0); }; </script> أما لو كنتَ تُنشِئ كائن الصورة عبر JavaScript، فيمكنك رسم الصورة داخل لوحة الرسم أثناء الحدث Image.onload. <canvas id="e" width="177" height="113"></canvas> <script> var canvas = document.getElementById("e"); var context = canvas.getContext("2d"); var cat = new Image(); cat.src = "images/cat.png"; cat.onload = function() { context.drawImage(cat, 0, 0); }; </script> المعاملان الاختياريان الثالث والرابع في دالة drawImage()‎ يتحكمان في تغيير أبعاد الصورة. هذه هي نفس الصورة بعد تقليل أبعادها إلى النصف مرسومةً بشكل تكراري في إحداثياتٍ مختلفة ضمن نفس لوحة الرسم. الشكل 15: الكثير من القطط! هذا هو السكربت الذي يؤدي إلى رسم الشكل السابق: cat.onload = function() { for (var x = 0, y = 0; x < 500 && y < 375; x += 50, y += 37) { //تغيير أبعاد الصورة context.drawImage(cat, x, y, 88, 56); } }; ربما تتساءل سؤالًا منطقيًا: لماذا تريد رسم صورة في لوحة الرسم أصلًا؟ بماذا ستستفيد من التعقيد الناتج عن رسم صورة داخل لوحة الرسم عوضًا عن استخدام عنصر <img> وبعض خاصيات CSS؟ حتى التأثير السابق (تكرار صورة القطة) يمكن أن يتم باستخدام 10 عناصر <img> متداخلة. الجواب المبسط هو: لنفس سبب حاجتك إلى رسم النصوص في لوحة الرسم. لاحظ كيف تُضمَّن مخطط إحداثيات canvas نصًا ومستقيماتٍ وأشكالًا. فوضع النص ضمن لوحة الرسم هو جزءٌ من عملٍ أكبر. والمخططات الأكثر تعقيدًا ستستفيد كثيرًا من الدالة drawImage()‎ لتضمين الأيقونات والرسوميات وغيرها. ماذا عن متصفح IE؟ لم تكن الإصدارات الأقدم من الإصدار التاسع من متصفح Internet Explorer تدعم الواجهة البرمجية (API) لعنصر canvas (يدعم IE9 واجهة canvas البرمجية بالكامل). لكن الإصدارات القديمة من Internet Explorer تدعم تقنية مملوكة من مايكروسوفت اسمها VML، التي تفعل العديد من الأشياء التي يفعلها العنصر <canvas>، وبهذا وُلِدَت المكتبة excanvas.js. Explorercanvas ‏(excanvas.js) هي مكتبة JavaScript مفتوحة المصدر مُرخَّصة برخصة Apache التي تجعل من الممكن استعمال واجهة canvas البرمجية في متصفح Internet Explorer. عليك تضمين وسم <script> الآتي في أعلى صفحتك لكي تستخدمها. <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Dive Into HTML5</title> <!--[if lt IE 9]> <script src="excanvas.js"></script> <![endif]--> </head> <body> ... </body> </html> الأسطر ‎<!--[if lt IE 9]>‎ و ‎<![endif]-->‎ تسمى التعليقات الشرطية. حيث يعاملها Internet Explorer كأنها عبارة if الشرطية: "إن كان الإصدار الحالي من متصفح Internet Explorer أقل من الإصدار 9، فنفِّذ الشيفرة الآتية."، وستعامل بقية المتصفحات تلك الشيفرة على أنها تعليقات HTML. فالخلاصة هي أنَّ متصفح Internet Explorer (الإصدار الثامن والسابع) سيُنزِّل السكربت ثم سيُنفِّذه، لكن ستتجاهله بقية المتصفحات تمامًا، وهذا يجعل صفحتك تُحمَّل بشكلٍ أسرع في المتصفحات التي تدعم واجهة canvas البرمجية داخليًا (أي أنها لن تُنزِّل السكربت، ولن تنفذه، ولن تفعل أيّ شيءٍ معه). ليس عليك فعل أي شيءٍ إضافي بعد تضمين excanvas.js في ترويسة صفحتك. ضمِّن عناصر <canvas> في شيفرتك أو أنشِأها ديناميكيًا باستخدام JavaScript، واتبع التعليمات الواردة في هذا الفصل للحصول على لوحة رسم عنصر <canvas>، ثم تستطيع البدء برسم الأشكال والنصوص والنقوش. ليس تمامًا… هنالك بعض المحدوديات: يمكن أن تكون التدرجات اللونية خطيةً فقط. التدرجات اللونية الشعاعية غير مدعومة. يجب تكرار النقوش (patterns) في كلا الاتجاهين. ميزة Clipping regions غير مدعومة. تغيير الأبعاد غير المنتظم لا يستطيع تغيير أبعاد حدود الأشكال (strokes) بشكلٍ صحيح. إنها بطيئة؛ ولا أظن أنَّ ذلك صادمٌ لأي شخص، فمن المعلوم أنَّ مُفسِّر JavaScript في متصفح Internet Explorer أبطأ من غيره من المتصفحات؛ فما بالك برسم أشكال معقدة عبر مكتبة JavaScript ستحول الدوال والأوامر إلى تقنيةٍ مختلفة تمامًا؟ لن تلاحظ بطئًا في الأداء في الأمثلة البسيطة مثل رسم بعض الخطوط وإجراء بعض العمليات على صورة، لكنك سترى ذلك بوضوح بعد أن تحاول إنشاء تأثيرات حركية باستخدام تقنية canvas. هنالك تحذيرٌ آخر حول استخدام excanvas.js، وهذه مشكلة واجهتني أثناء إنشاء أمثلة هذا الفصل. يُهيّئ ExplorerCanvas واجهته البرمجية لعنصر canvas في مكان تضمينك لسكربت excanvas.js في صفحة HTML. لكن ذلك لا يعني أنَّ Internet Explorer جاهزٌ لاستخدامها مباشرةً، وفي بعض الحالات ستواجه حالة خاصة تكون فيها واجهة canvas جاهزة "تقريبًا" (لكن ليس تمامًا) للاستعمال. الأعراض الظاهرة لهذه المشكلة هي أنَّ Internet Explorer سيشتكي من أنَّ "object doesn’t support this property or method" في كل مرة تحاول فيها فعل أي شيء مع عنصر <canvas> مثل الحصول على لوحة الرسم الخاصة به. أسهل حل هذه المشكلة هي تأجيل كل العمليات التي ستجريها على canvas إلى أن يحدث الحدث onload، ولكن هذا قد يستغرق بعضًا من الوقت، خصيصًا إذا كانت في صفحتك الكثير من الصور أو مقاطع الفيديو، التي ستؤخر الحدث onload، لكن ذلك سيعطي المكتبة ExplorerCanvas وقتًا لفعل اللازم. مثال متكامل "الضامة" هي لعبةٌ قديمةٌ جدًا، وهنالك عدِّة نسخ منها؛ أنشأتُ في هذا المثال نسخة السوليتر (أي التي يلعبها شخصٌ واحد فقط) من الضامة ذات تسعِ قطعٍ في رقعةٍ مقاسها 9×9. تتواجد القطع في بداية اللعبة في مربع 3×3 في الركن الأسفل الأيسر من الرقعة. هدف اللعبة هو نقل جميع القطع كي تُشكِّل مربع 3×3 في الركن الأعلى الأيمن من الرقعة بأقل عدد من الحركات. هنالك نوعان من الحركات المسموحة في الضامة: خذ قطعةً وحركها إلى أي مربع فارغ مجاور. المربع "الفارغ" يعني أنَّه لا يحتوي على قطعةٍ فيه حاليًا. أما "مجاور" فهي تعني المربع الذي يقع مباشرةً في شمال أو جنوب أو شرق أو غرب أو الشمال الغربي أو الشمال الشرقي أو الجنوب الغربي أو الجنوب الشرقي من موقع القطعة الحالي (لا يمكن الالتفاف في الرقعة من طرفٍ آخر لآخر، فلو كانت قطعتك في العمود الأيسر من الرقعة، فلا يمكنك أن تحركها نحو الغرب أو الشمال الغربي أو الجنوب الغربي. وإذا كانت قطعتك في السطر الأدنى من الرقعة، فلا تستطيع تحريكها نحو الجنوب أو الجنوب الشرقي أو الجنوب الغربي). يمكنك أخذ قطعة وجعلها "تقفر" فوق قطعة مجاورة، ويمكنك تكرار ذلك. هذا يعني أنَّه إذا قفزت فوق قطعة مجاورة ثم قفزت فوق قطعة أخرى مجاورة لموقعك الجديد، فكل هذا يُحتَسب على أنَّه حركة واحدة. وفي الحقيقة، يتم احتساب أي عدد من القفزات على أنها خطوة واحدة (لأن الهدف من اللعبة هو تقليل العدد الكلي من الحركات؛ ويتطلب اللعب بشكلٍ جيد في الضامة بناءً واستخدامًا لسلاسل طويلة من القطع لكي تستطيع القطع الأخرى القفز قوفها في سلاسل طويل). هذه لقطة شاشة للعبة، يمكنك زيارة الصفحة الآتية لتجربة اللعبة تجربةً حيةً أو لإلقاء نظرة إلى مصدر الصفحة. الشكل 16: لقطة شاشة للعبة الضامة كيف تعمل؟ أنا ممتن لسؤالك. لن أريك كل الشيفرات هنا (يمكنك رؤيتها في halma.js)، وسأتخطى معظم الشيفرات التي تستعمل في تحديد آلية اللعب، لكنني سأركِّز على بعض الأجزاء التي مهتمها هي الرسم في عنصر canvas والاستجابة إلى نقرات الفأرة. سنُهيّئ اللعبة أثناء تحميل الصفحة بضبط أبعاد عنصر <canvas> ثم تخزين مرجع (reference) متعلق بلوحة الرسم. gCanvasElement.width = kPixelWidth; gCanvasElement.height = kPixelHeight; gDrawingContext = gCanvasElement.getContext("2d"); ثم سنفعل شيئًا لم نفعله من قبل قط: سنضيف متتبع أحداث إلى العنصر <canvas> لكي يتتبع حدث النقر على العنصر. gCanvasElement.addEventListener("click", halmaOnClick, false); ستُستدَعى الدالة halmaOnClick()‎ عندما ينقر المستخدم في أي مكان ضمن العنصر canvas، والوسيط الذي يُمرَّر إليها هو كائن MouseEvent الذي يحتوي على معلومات حول مكان نقر المستخدم. function halmaOnClick(e) { var cell = getCursorPosition(e); for (var i = 0; i < gNumPieces; i++) { if ((gPieces.row == cell.row) && (gPieces.column == cell.column)) { clickOnPiece(i); return; } } clickOnEmptyCell(cell); } الخطوة الآتية هي أخذ الكائن MouseEvent وحساب في أي مربع في رقعة الضامة قد تم النقر. تحتل رقعة الضامة كامل عنصر canvas، ولهذا تكون كل نقرة هي نقرة في مكانٍ ما على الرقعة، وكل ما علينا هو معرفة المكان. هذا أمرٌ معقدٌ بعض الشيء، لأن الأحداث المولدة من الفأرة تختلف بشكلٍ أو بآخر بين المتصفحات. function getCursorPosition(e) { var x; var y; if (e.pageX != undefined && e.pageY != undefined) { x = e.pageX; y = e.pageY; } else { x = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop; } } أصبح لدينا في هذه المرحلة إحداثيات x و y نسبةً إلى المستند (أي صفحة HTML كلها)، أي أنها ليس مفيدةً بعد؛ حيث نريد أن تكون الإحداثيات منسوبةً إلى لوحة الرسم. x -= gCanvasElement.offsetLeft; y -= gCanvasElement.offsetTop; أصبح لدينا إحداثيات x و y منسوبةً إلى لوحة الرسم، هذا يعني أنَّه لو كانت قيمة x مساويةً للصفر و y مساوية للصفر، فهذا يعني أنَّ المستخدم قد نقر على البكسل في أعلى وأيسر لوحة الرسم. من هنا سنستطيع معرفة أي مربع قد نقر المستخدم عليه، ثم نتصرف بناءً على ذلك. var cell = new Cell(Math.floor(y/kPieceHeight), Math.floor(x/kPieceWidth)); return cell; أوه! أمرُ الأحداث المرتبطة بالفأرة معقدٌ. لكنك يمكنك استخدام نفس التسلسل المنطقي (أو بالأحرى، يمكنك استخدام نفس الشيفرة) في جميع البرمجيات التي تعتمد على عنصر canvas. تذكر: نقرة بالفأرة ← إحداثيات منسوبة إلى المستند كله ← إحداثيات منسوبة إلى لوحة الرسم ← شيفرة خاصة بالتطبيق. حسنًا، لنلقِ نظرةً على آلية الرسم الرئيسية. لمّا كانت الرسوميات بسيطةً جدًا، فقررت مسح محتويات لوحة الرسم وإعادة رسم الرقعة في كل مرة يحصل فيها تغييرٌ في اللعبة ولكن هذا ليس ضروريًا. سيحتفظ إطار الرسم في عنصر canvas بمحتواه الذي رسمته عليه حتى لو تخطاه المستخدم بتمريره في الصفحة، أو إذا غيّر إلى لسانٍ (tab) آخر ثم عاد إليه لاحقًا. إذا كنتَ تُطوِّر تطبيقًا مبنيًا على عنصر canvas فيه رسوميات معقدة (مثل ألعاب الورق) فيمكنك تحسين الأداء بتتبع المناطق التي يجب مسحها من لوحة الرسم وإعادة الرسم داخل تلك المناطق فقط. لكن هذا خارجٌ عن نطاق هذا الكتاب. gDrawingContext.clearRect(0, 0, kPixelWidth, kPixelHeight); يجب أن تكون الشيفرات الآتية مألوفةً لديك، لأنها شبيهة بتلك التي استعملناها لرسم مخطط إحداثيات canvas سابقًا. gDrawingContext.beginPath(); // الخطوط الشاقولية for (var x = 0; x <= kPixelWidth; x += kPieceWidth) { gDrawingContext.moveTo(0.5 + x, 0); gDrawingContext.lineTo(0.5 + x, kPixelHeight); } // الخطوط الأفقية for (var y = 0; y <= kPixelHeight; y += kPieceHeight) { gDrawingContext.moveTo(0, 0.5 + y); gDrawingContext.lineTo(kPixelWidth, 0.5 + y); } // لنرسم الرسمة gDrawingContext.strokeStyle = "#ccc"; gDrawingContext.stroke(); المتعة الحقيقة تبدأ عندما نهم برسم القطع. القطعة هي دائرة، لكننا لم نرسم الدوائر من قبل، وعلاوةً على ذلك، لو اختار المستخدم قطعةً لكي يحركها، فسنحتاج إلى رسم تلك القطعة كدائرة مملوءة. يُمثِّل المعامل p هنا قطعة، التي لها خاصيات row (سطر) و column (عمود) التي تُحدِّد المكان الحالي للقطعة. سنستخدم آلية لتحويل (column, row) إلى إحداثيات منسوبة إلى لوحة الرسم (x, y)، ثم سنرسم دائرة، ثم -إن كانت القطعة مُحدَّدةً من المستخدم- سنملأ الدائرة بلونٍ معين. function drawPiece(p, selected) { var column = p.column; var row = p.row; var x = (column * kPieceWidth) + (kPieceWidth/2); var y = (row * kPieceHeight) + (kPieceHeight/2); var radius = (kPieceWidth/2) – (kPieceWidth/10); أصبح لدينا الآن إحداثيات (x, y) منسوبة إلى لوحة الرسم لمركز الدائرة التي نريد رسمها. للأسف لا توجد دالة باسم circle()‎ في واجهة canvas البرمجية، لكن هنالك الدالة arc()‎؛ لكن الدائرة ما هي إلا قوسٌ تتصل بدايته مع نهايته. هل تتذكر أساسيات الهندسة؟ تأخذ الدالة arc()‎ نقطة المركز (x, y)، ونصف قطر، وزاوية البداية والنهاية (بالردايان)، ورايةً (flag) لتحدد اتجاه الدوران (false لاتجاه دوران عقارب الساعة، و true لعكس جهة دوران عقارب الساعة). يمكنك استخدام وحدة Math المبنية في لغة JavaScript لحساب الزوايا بالردايان. gDrawingContext.beginPath(); gDrawingContext.arc(x, y, radius, 0, Math.PI * 2, false); gDrawingContext.closePath(); انتظر برهة! لم يُرسَم شيء. الدالة arc()‎ تشبه عمل moveTo()‎ و lineTo()‎. حيث ترسم "بقلم الرصاص". ولرسم الدائرة فعليًا، علينا ضبط خاصية strokeStyle ثم استدعاء الدالة stroke()‎ "لتحبيرها". gDrawingContext.strokeStyle = "#000"; gDrawingContext.stroke(); ماذا لو كانت القطعة مُحدَّدةً؟ يمكننا إعادة استخدام نفس المسار الذي أنشأناه لرسم حدود القطعة لملئها بلونٍ معيّن. if (selected) { gDrawingContext.fillStyle = "#000"; gDrawingContext.fill(); } هذا كل ما في الأمر. تهتم بقية السكربت بالبنية المنطقية للعبة، مثل التفريق بين الحركات الصحيحة وغير الصحيحة، وإحصاء عدد الحركات، ومعرفة إذا انتهت اللعبة. مرحى! لقد أنشأنا لعبةً عبر عنصر canvas برسم 9 دوائر وبعض المستقيمات ودالة وحيدة مرتبطة بالحدث onclick. لمزيد من المعلومات حول عنصر canvas يمكنك الإطلاع على سلسلة التعامل مع العنصر canvas باستخدام جافاسكربت. ترجمة -وبتصرّف- لفصل "Canvas" من كتاب Dive Into HTML5 لمؤلفه Mark Pilgrim. اقرأ أيضًا المقال التالي: صيغ ترميز الفيديو والصوت وحاوياتها وكيفية عملها في الويب المقال السابق: اكتشاف دعم المتصفحات لميزات HTML5 النسخة العربية الكاملة من كتاب نحو فهم أعمق لتقنيات HTML5
  4. توفّر jQuery أدوات قويّة لإيجاد العنصر أو العناصر التي تريدها في الصّفحة، ثمّ العمل بهذه العناصر للوصول إلى النّتيجة المرغوبة. تسهّل jQuery بأدواتها هذه عمليّات كانت لتكون أكثر تعقيدًا لو أردنا تنفيذها من خلال وظائف DOM الأصليّة. سنطّلع في هذا الجزء على بعض (لا كلّ) وظائف الانتقال عبر الصّفحة وتّعديل العناصر في jQuery. وقبل أن نبدأ، علينا فهم بعض المصطلحات الضّروريّة. لنفترض أنّ لدينا نصّ HTML التّالي: <ul> <li> <span> <i>Foo</i> </span> </li> <li>Bar</li> </ul> نقول عن عنصر القائمة الأوّل (<li>) أنّه ابن (child) القائمة غير المرتّبة (<ul>). نقول عن القائمة غير المرتّبة (<ul>) أنّها والد (parent) عنصري القائمة الاثنين. نقول عن العنصر <span> أنّه خَلَفُ (descendant) القائمة غير المرتّبة. نقول عن القائمة غير المرتّبة أنّها سَلَفٌ (ancestor) لكلّ ما داخلها. نقول عن عنصري القائمة أنّهما شقيقان (siblings). الانتقال عبر الصّفحة (Traversal) تسمح jQuery لنا بالانتقال عبر عناصر HTML الّتي تكوّن صفحتنا. إذ نُنشئ أوّلًا تحديدًا مبدئيًّا ثمّ ننتقل عبر DOM انطلاقًا منه. وخلال مسيرنا عبر DOM، فإنّنا نُغيّر من تحديدنا الأوّل فنضيف إليه أو نحذف منه بعض العناصر، أو نستبدل به تحديدًا آخر بالكامل في بعض الأحيان. تصفية التّحديدات بإمكانك تصفية تحديد موجودٍ بحيث يتضمّن فقط العناصر الّتي تطابق معاييرَ مُحدّدة. بإمكانك مثلًا إجراء التّصفية بإحدى الطّرق التّالية: var listItems = $( 'li' ); // صفِّ التّحديد ليحوي فقط العناصر ذات الصّنف 'special' var special = listItems.filter( '.special' ); // صفّ التّحديد ليحوي فقط العناصر من غير الصّنف 'special' var notSpecial = listItems.not( '.special' ); // صفّ التّحديد ليحوي فقط العناصر الّتي تتضمّن span var hasSpans = listItems.has( 'span' ); من المهمّ أن تعرف أن الوظيفة ‎.not()‎ ليست عكس ‎.is()‎، لأنّ ‎.is()‎ تُعيد قيمة منطقيّة (true أو false)، بينما تُعيد ‎.not()‎ كائن jQuery جديدًا. إيجاد العناصر انطلاقًا من تحديد يمكن الاستفادة من تحديد أوّليّ كأساس لإنشاء تحديدات إضافيّة؛ فإذا كان لديك تحديدٌ يحوي عنصر قائمة مُفردًا مثلًا، وأردت التّعامل مع "أشقّائه" أو مع القائمة غير المُرتّبة الّتي تحويه، فبإمكانك إنشاء تحديد جديد انطلاقًا من التّحديد الموجود بسهولة: // اختر أوّل عنصر قائمة في الصّفحة var listItem = $( 'li' ).first(); // أيضًا: .last() // اختر أشقّاء عنصر القائمة var siblings = listItem.siblings(); // اختر الشّقيق التّالي لعنصر القائمة var nextSibling = listItem.next(); // أيضًا: .prev() // اختر والد عنصر القائمة var list = listItem.parent(); // اختر عناصر القائمة الّتي تنحدر مباشرةً من القائمة var listItems = list.children(); // اختر كلّ عناصر القائمة ضمن القائمة، بما في ذلك العناصر الفرعيّة var allListItems = list.find( 'li' ); // اختر كل أسلاف عنصر القائمة ذوي الصّنف "module" var modules = listItem.parents( '.module' ); // اختر أقرب سلفٍ لعنصر القائمة له الصّنف "module" var module = listItem.closest( '.module' ); بإمكانك كذلك الإضافة على التّحديد الحاليّ باستخدام الوظيفة ‎.add()‎، الّتي تقبل مُحدِّدًا أو مصفوفة عناصر أو نص HTML أو كائن jQuery. var list = $( '#my-unordered-list' ); // افعل شيئًا ما بالقائمة ثم ... var listAndListItems = list.add( '#my-unordered-list li' ); العودة إلى التّحديد الأصليّ تحتفظ jQuery بإشارة إلى تحديد الأصليّ عندما تستخدمه للانتقال إلى تحديدات أخرى انطلاقًا منه، في حال أردت العودة إلى التّحديد الأصليّ. افترض مثلًا أنّك حدّدت قائمة غير مرتّبه، ثمّ أردت التّعديل على عناصر القائمة، ثمّ العودة مجدّدًا للعمل على القائمة غير المرتّبة، عندها بإمكانك استخدام الوظيفة ‎.end()‎ للرّجوع إلى التّحديد الأصليّ: $( '#my-unordered-list' ) .find('li') // نحن الآن نعمل على عناصر القائمة .addClass('special') .end() // عدنا الآن للعمل على القائمة ذاتها .addClass('super-special'); تُسهِّل الوظيفة ‎.end()‎ إجراء تعديلات كثيرة في جملة واحدة، إلّا أنّ هذا الأسلوب لا يُلقي بالًا لوضوح النّصّ البرمجيّ، فهو أشبه بأن تحكي قصّة دون أن تلتقط أنفاسك. لهذا السّبب لا أنصحك بالإكثار من استعماله، فهو يؤدّي في معظم الحالات إلى جعل قراءة النّصّ البرمجيّ وصيانته وتنقيحه أكثر صعوبة. فيما يلي حلّ أفضل للمشكلة ذاتها: var list = $( '#my-unordered-list' ); var listItems = list.find('li'); listItems.addClass( 'special' ); list.addClass( 'super-special' ); توفّر jQuery أيضًا الوظيفة ‎.addBack()‎ إن أردت إضافة تحديدك الأصليّ إلى التّحديد الحاليّ. مثال: $( 'li.special' ) .siblings() // نحن نعمل الآن على أشقّاء التّحديد السّابقة .removeClass( 'important' ) .addBack() // الآن نعمل على عناصر القائمة الأصليّة وأشقائها **معًا** .addClass( 'urgent' ); هل اختلط عليك الأمر؟ الوظيفة ‎.addBack()‎ تشبه الوظيفة ‎.end()‎ في عيوبها، فكلاهما (وإن كان لهما استخدامها) يزيدان تعقيد النّصّ البرمجيّة. الحلّ الأفضل هو استخدام الوظيفة ‎.add()‎ لدمج التّحديدين الأصليين معًا: var specialListItems = $( 'li.special' ); var otherListItems = specialListItems.siblings(); otherListItems.removeClass( 'important' ); specialListItems.add( otherListItems ).addClass( 'urgent' ); هناك وظائف عديدة لم نتطرّق إليها هنا، يمكنك الاطّلاع عليها في وثائق الانتقال عبر الصّفحة. التّعامل مع العناصر (Manipulation) تسمح وظائف التّعامل مع العناصر في jQuery بتغيير DOM الصّفحة بصياغة أكثر بساطة من تلك الّتي توفّرها وظائف DOM الخام. تُعيد وظائف التّعامل مع العناصر في jQuery كائن jQuery الّتي استدعيت للعمل عليه، وهذا يعني إمكانيّة ربطها في سلسلة أو دمجها مع وظائف jQuery أخرى كالّتي ناقشناها في الفقرات السّابقة. تعديل العناصر كثيرةٌ هي طرق تعديل العناصر في jQuery. سنطّلع فيما يلي على طرق إنجاز المهام الأكثر شيوعًا. إضافة أو حذف الأصناف (classes) يمكن الاستفادة من أصناف الكائنات في HTML بأن نستهدفها في CSS بغرض تنسيقها، كما يُستفاد منها في إنشاء تحديدات jQuery. فمثلًا يمكن لعنصر في الصّفحة أن يقع تحت الصّنف hidden، والّذي يُستخدم في CSS لجعل خاصّة display موافقة للقيمة none للعناصر من هذا الصّنف، ثمّ يمكن حذف هذا الصّنف أو إضافته لتغيير حالة ظهور العناصر الموافقة في jQuery: $( 'li' ).addClass( 'hidden' ); $( 'li' ).eq( 1 ).removeClass( 'hidden' ); جرّب المثال في ساحة التّجربة (تأكد من ضغط زر Run with JS في هذا المثال وكلّ الأمثلة التالية) إن تطلّبت حالتك إضافة صنفٍ أو حذفه مرارًا، فبإمكانك استخدام الوظيفة ‎.toggleClass()‎ الّتي تُبدّل حالة الصّنف على العنصر، فتضيفه إن لم يكن موجودًا أو تحذفه إن وُجد: $( 'li' ).eq( 1 ).toggleClass( 'hidden' ); جرّب المثال في ساحة التّجربة تغيير المظهر ملاحظة: يُفضّل دومًا استخدام الأصناف واستهدافها بقواعد CSS لتغيير طريقة عرض العناصر، والاقتصار على استخدام jQuery في إضافة هذه الأصناف أو حذفها كما ورد للتوّ. في هذه الفقرة سنتعرّف كيف نُغيّر مظاهر العناصر مُباشرةً في jQuery، ولكننا نُفضِّل دومًا الأسلوب الأوّل إن كان يُحقِّق النّتائج ذاتها. عندما تعجز عن تحقيق هدفك بإضافة الأصناف أو حذفها، فإنّ jQuery تقدّم الوظيفة .css() الّتي تسمح بتعيين مظهر العناصر مباشرةً، ولعلّ هذا يكون ضروريًّا عادةً عندما تحتاج إلى إسناد قيم عدديّة لا يمكن حسابها إلّا أثناء عمل التّطبيق، كمعلومات توضّع العناصر في الصّفحة. لا يُفضَّل استخدام الوظيفة ‎.css()‎ لإجراء تنسيقات بسيطة مثل ‎display: none‎، بل يُفضَّل في معظم الحالات إنجاز الغاية ذاتها باستخدام الأصناف وCSS. افترض مثلًا أنّنا نريد تعيين مظهر العنصر بالاعتماد على عرض والده، وربّما يصعب أو يستحيل معرفة عرض الوالد مُسبقًا عند اعتماد تخطيط مرنٍ للصّفحة. في هذه الحالة قد نلجأ إلى الوظيفة ‎.css()‎ لتنسيق العنصر: var list = $( '#my-unordered-list' ); var width = Math.floor( list.width() * 0.1 ); list.find('li').each(function( index, elem ) { var padding = width * index; $( elem ).css( 'padding-right', padding + 'px' ); }); جرّب المثال في ساحة التّجربة إن احتجت إلى تعيين أكثر من خاصّة في وقت واحدٍ، مرّر كائنًا إلى الوظيفة ‎.css()‎ بدلًا من اسم الخاصّة وقيمتها. لاحظ أنّ عليك إحاطة أيّة خاصّة تحوي الرّمز "-" بعلامتي اقتباس: $( 'li' ).eq( 1 ).css({ 'font-size': '20px', 'padding-right': '20px' }); جرّب المثال في ساحة التّجربة تغيير قيم النّماذج (forms) تقدّم jQuery الوظيفة ‎.val()‎ لتعديل قيمة العناصر في النّماج مثل input وselect. بإمكانك تمرير سلسلة نصّيّة لتعيين محتوى حقول input النّصّيّة: $( 'input[type="text"]' ).val( 'new value' ); جرّب المثال في ساحة التّجربة بالنّسبة للعناصر من نوع select، بإمكانك تعيين الخيار المُختار باستخدام ‎.val()‎ أيضًا: $( 'select' ).val( '2' ); جرّب المثال في ساحة التّجربة أمّا لحقول input من نوع checkbox، فعليك تعيين الخاصّة checked على العنصر بالوظيفة ‎.prop()‎. $( 'input[type="checkbox"]' ).prop( 'checked', 'checked' ); جرّب المثال في ساحة التّجربة ملاحظة: أُضيفت الوظيفة ‎.prop()‎ في الإصدارة 1.6 من jQuery؛ وقبل ذلك كانت تُستخدم الوظيفة ‎.attr()‎ للغرض ذاته، وهي ما تزال تعمل في الإصدارات الحديثة من jQuery، ولكنّها في حالة checked تكتفي باستدعاء ‎.prop()‎. إن كنت تستخدم إصدارةً أحدث من 1.6، فأنصحك باستخدام ‎.prop()‎ دومًا لتعيين الخاصّة checked وخصائص عناصر DOM الأخرى. اطّلع على الوثائق لتفاصيل أكثر. تغيير الصّفات (attributes) الأخرى بإمكانك استخدام وظيفة ‎.attr()‎ لتغيير صفات العناصر، فيمكنك مثلًا تغيير عنوان رابط (الخاصّة title لعنصر <a>): $( 'a' ).attr( 'href', 'new title'); عند تعيين قيمة لصفة، بإمكانك تمرير دالّة في موضع المُعامل الثّاني للوظيفة، ومثلها مثل كلّ دوالّ الكتابة السّابقة، تتلقّى هذه الدّالّة مُعاملين اثنين: دليل العنصر الّذي تعمل عليه، والقيمة الحاليّة للصّفة. يجب أن تُعيد هذه الدّالة القيمة الجديدة للصّفة: $( 'a' ).attr( 'href', function(index, value) { return value + '?special=true'; }); جرّب المثال في ساحة التّجربة بإمكانك حذف الصّفات أيضًا، وذلك باستخدام ‎.removeAttr()‎. الحصول على معلومات من العناصر ناقشنا في الجزء السّابق (أساسيّات jQuery) فكرة وظائف القراءة والكتابة. كلّ الوظائف الّتي يمكن استخدامها لتغيير العناصر، يمكن أيضًا استخدامها لقراءة معلومات من تلك العناصر. فيمكن مثلًا استخدام الوظيفة ‎.val()‎ الّتي وصفناها أعلاه كوظيفة قراءة وكتابة معًا: var input = $( 'input[type="text"]' ); input.val( 'new value' ); input.val(); // returns 'new value' وكذلك الأمر بالنّسبة للوظيفة ‎.css()‎، إذ يمكن استخدامها لقراءة قيمة خصائص CSS مُفردة بإمرار اسم الخاصّة فقط دون قيمة: var listItemColor = $( 'li' ).css( 'color' ); عندما تُستخدم وظائف التّعامل مع العناصر للقراءة، فإنّها تعمل فقط مع العنصر الأول في التّحديد، باستثناء الوظيفة ‎.text()‎ الّتي تقرأ المحتوى النّصيّ لكلّ العناصر المُحدّدة إن لم يُمرّر معامل إليها. إضافة العناصر إلى الصّفحة سواءٌ حدّدت عنصرًا أو أنشأت واحدًا جديدًا، فبإمكانك إضافة هذا العنصر إلى الصّفحة يمكن فعل ذلك بطريقتين: باستدعاء وظيفة تتبع للعنصر (أو العناصر) المطلوب إضافته، أو باستدعاء وظيفة تتبع لعنصر مرتبط بذلك الّذي تريد إضافته. افترض مثلًا أنّك تريد نقل عنصر في قائمة من رأسها إلى ذيلها، هناك عدّة طرق لفعل ذلك. بإمكانك مثلًا إضافة العنصر إلى القائمة باستدعاء الوظيفة ‎.appendTo()‎ على عنصر القائمة ذاته: var listItem = $( '#my-unordered-list li' ).first(); listItem.appendTo( '#my-unordered-list' ); جرّب المثال في ساحة التّجربة وبإمكانك أيضًا إضافة العنصر باستدعاء ‎.append()‎ على القائمة: var listItem = $( '#my-unordered-list li' ).first(); $( '#my-unordered-list' ).append( listItem ); أو إضافته باستدعاء ‎.insertAfter()‎ على العنصر المُراد نقله مُمرّرًا العنصر الأخير في القائمة إلى الوظيفة: var listItems = $( '#my-unordered-list li' ); listItems.first().insertAfter( listItems.last() ); جرّب المثال في ساحة التّجربة أو إضافته باستدعاء ‎.after()‎ على العنصر الأخير في القائمة مُمرًّرا العنصر الأولى في القائمة إلى الوظيفة: var listItems = $( '#my-unordered-list li' ); listItems.last().after( listItems.first() ); جرّب المثال في ساحة التّجربة هناك طرق آخرى كثير لإضافة العناصر، فبإمكانك إضافتها حول عناصر أخرى أو داخلها أو خارجها بحسب حاجتك. تعتمد أكثر الطّرق كفاءة في إضافة عنصر إلى الصّفحة على العناصر الّتي تتوفّر بين يديك بالفعل. فقد ترغب في إضافة العنصر إلى القائمة غير المُرتّبة في المثال السّابق إن كنت قد حدّدت القائمة غير المرتّبة من قبل لغرض آخر؛ أو إن كنت قد حدّدت عناصر القائجة جميعها، فقد يكون إضافة العنصر الأول بعد العنصر الأخير أمرًا أسهل. عندما تختار الطّريقة المناسبة لإضافة العنصر، فلا تكتفِ بالنّظر في سهولة الطّريقة، بل فكّر في إمكانيّة صيانتها لاحقًا. تجنّب الطّرق الّتي تعتمد على افتراض بنية مُحدِّدة بدقَّة لصفحتك، فقد تقرّر تغيير هذه البنية فيما بعد. نسخ العناصر بإمكانك إنشاء نسخة من عنصر أو مجموعة عناصر باستخدام الوظيفة ‎.clone()‎ في jQuery، وستُنشئ النُّسخة في الذّاكرة دون أن تُدرج في الصّفحة، فعليك فعل ذلك بنفسك إن أردته. بإمكانك تعديل العناصر المنسوخة قبل إضافتها: var clones = $( 'li' ).clone(); clones.html(function( index, oldHtml ) { return oldHtml + '!!!'; }); $( '#my-unordered-list' ).append( clones ); جرّب المثال في ساحة التّجربة ملاحظة*: لن تمنعك jQuery من نسخ عنصر ذي مُعرِّف (ID)، ولكن عليك التأكّد من حذف المُعرِّف أو تغييره في العنصر المنسوخ بتعديل الصّفة id قبل إدراجه في المستند، إذ لا ينبغي أ يوجد عنصران بمُعرِّف واحدٍ في الصّفحة. حذف العناصر هناك ثلاث طرق لحذف العناصر من الصّفحة: ‎.remove()‎‏ و‎.detach()‎‏ و‎.replaceWith()‎‏، ولكلّ منها غرضٌ مُختلف. يجب استخدام ‎.remove()‎‏ عند الحاجة لحذف العناصر بصورة دائمة، فهي ستحذف مع العنصر كلّ مُتوليّات الأحداث المُرتبطة به (event handlers). تُعيد الوظيفة ‎.remove()‎‏ إشارةً إلى العناصر المُحذوفة، ولكن عند إضافة هذه العناصر مرّة ثانيةً، فلن تكون أيّة أحداث مُرتبطةً بها. $( '#my-unordered-list li' ).click(function() { alert( $( this ).text() ); }); var removedListItem = $( '#my-unordered-list li' ).first().remove(); removedListItem.appendTo( '#my-unordered-list' ); removedListItem.trigger( 'click' ); // لا رسالة تنبيه! جرّب المثال في ساحة التّجربة تُفيد الوظيفة ‎.detach()‎‏ في حذف العناصر مؤقّتًا من الصّفحة، فمثلًا إن رغب في إجراء تعديلات كبيرة على بنية الصّفحة باستخدام jQuery، فقد يكون حذف العناصر مؤقّتًا من الصّفحة ثمّ إضافتها ثانيةً أفضل أداءً بمراحل. ستحتفظ العناصر المحذوفة بهذه الوظيفة بمُتولّيات الأحداث المُرتبطة بها، ويمكن بعد ذلك إضافتها إلى الصّفحة مُجدّدًا باستخدام ‎.appendTo()‎‏ أو غيرها من وظائف الإضافة. $( '#my-unordered-list li' ).click(function() { alert( $( this ).text() ); }); var detachedListItem = $( '#my-unordered-list li' ).first().detach(); // افعل شيئًا ما مُعقّدًا بعنصر القائمة detachedListItem.appendTo( '#my-unordered-list' ); detachedListItem.trigger( 'click' ); // alert! أخيرًا لدينا الوظيفة ‎.replaceWith()‎‏ الّتي تُحلِّ عنصرًا أو نصّ HTML محلّ عنصرٍ أو عناصر أخرى. تُعاد العناصر الّتي أُزيلت من الوظيفة، ولكنّ كلّ متولّيات الأحداث المرتبطة بها تُحذف، تمامًا كالوظيفة ‎.remove()‎‏. $( '#my-unordered-list li' ).click(function() { alert( $( this ).text() ); }); var replacedListItem = $( '#my-unordered-list li' ).first() .replaceWith( '<li>new!</li>' ); replacedListItem.appendTo( '#my-unordered-list' ); replacedListItem.trigger( 'click' ); // لا رسالة تنبيه! جرّب المثال في ساحة التّجربة خاتمة تعلّمنا في هذا الجزء الطّرق المختلفة للانتقال بين العناصر في الصّفحة، وكيفيّة نقلها وتغييرها وإضافة عناصر جديدة. سنتعلّم في الجزء القادم كيف نُنصِت لتفاعل المستخدم مع صفحتنا. ترجمة (بشيء من التصرف) للجزء الثالث من سلسلة jQuery Fundamentals لمؤلّفتها Rebecca Murphey.
  5. يُعتبر مربع البحث أحد المكونات التي لا غنى عنها في أي موقع، وفي هذا الدرس سوف تتعلم كيف يمكنك إنشاء مربع بحث يظهر ويختفي عند النقر عليه. قد تعتقد أنّ القيام بذلك سيكون سهلًا جدًا ولكنك سوف ترى أنّ الأمر ليس كذلك، فسوف نحتاج إلى تطبيق بعض الخدع حتى نجعل كل شيء يعمل بشكل صحيح. هذه هي المتطلبات التي نريدها مبدئيًا: ما نريده مبدئيًا هو ظهور زر يحتوي على أيقونة بحث. عند النقر على ذلك الزر نريد أن يظهر مربع البحث. نريده أن يكون متجاوبًا مع جميع الأجهزة. عند كتابة أي شيء في مربع البحث نريد أن يقوم المربع بالقيام بالبحث فور النقر على مفتاح الدخول (enter) من لوحة المفاتيح أو النقر على الأيقونة نفسها. إذا كان مربع البحث ظاهرًا ولكنه كان فارغًا فإننا نريده أن يختفي عند النقر على أيقونة البحث (أي أننا إذا قمنا بالنقر على أيقونة البحث وكان المربع فارغًا فإننا لا نريده أن يقوم بالبحث وإنما يقوم بإخفاء مربع البحث). نريد أيضًا أن يختفي مربع البحث عند النقر خارجه سواء كان فارغًا أم لا. لو كانت الجافاسكربت معطلة لدى المستخدم فإننا نريد لمربع البحث أن يكون ظاهرًا (أي دون الحاجة إلى النقر على أيقونة البحث). نريد أيضًا أن ندعم الأجهزة التي تعمل باللمس. يمكنك معاينة النتيجة النهائية لهذا الدرس. بما أننا بتنا الآن نعرف ما نريد فلنقم بذلك. بنية ملف HTML كل ما نحتاجه من وسوم HTML هو حاوٍ رئيسي (main container) وسوف يكون عبارة عن وسم <div> وسوف نحتاج إلى وسم <form> وإلى حقلي إدخال (inputs)؛ واحد من نوع "text" والآخر من نوع "submit" وأخيرًا سوف نحتاج إلى وسم <span> ليحتوي على أيقونة البحث: <div id="sb-search" class="sb-search"> <form> <input class="sb-search-input" placeholder="Enter your search term..." type="search" value="" name="search" id="search"> <input class="sb-search-submit" type="submit" value=""> <span class="sb-icon-search"></span> </form> </div> لنبدأ الآن بتنسيق العناصر باستخدام CSS. تنسيقات CSS بناءً على المتطلبات التي ذكرناها سابقًا فإنه يجب في البداية أن يكون لدينا زر يحتوي على أيقونة بحث وباقي العناصر يجب أن تكون مخفية. دعونا الآن نتخيل ما الذي سيحصل عند تمدد مربع البحث وجعله ظاهرًا (الذي هو نفسه سيكون الحاوي الرئيسي الذي ذكرناه سابقًا). كيف نقوم بذلك؟ سوف نستخدم الخاصية overflow: hidden وتكبير العرض الخاص بالعنصر الحاوي (sb-search) يجب أن يقوم بإظهار حقل البحث. دورة تطوير واجهات المستخدم ابدأ عملك الحر بتطوير واجهات المواقع والمتاجر الإلكترونية فور انتهائك من الدورة اشترك الآن إذًا أول شيء نقوم به هو تنسيق العنصر الحاوي (sb-search) بحيث سوف نجعله يطوف إلى اليمين باستخدام الخاصية float: right ونعطيه الخاصية overflow: hidden، والعرض يجب أن يكون 60px ولكن بما أننا نريد أن يزيد العرض إلى 100% فإننا سنواجه بعض المشاكل في متصفحات iOS فهي لا تقبل التغيير من عرض يعتمد على الـpixels إلى عرض يعتمد على النسب المئوية. لذلك سوف نقوم بتعريف خاصية min-width بالقيمة 60px وخاصية width بقيمة 0%. يمكنك قراءة المزيد عن هذا الحل العبقري من خلال هذا الرابط. سوف نستعمل أيضًا الخاصية transition والخاصية webkit-backface-visibility: hidden- لتلافي بعض الآثار للحقول في متصفحات الهواتف iOS: .sb-search { position: relative; margin-top: 10px; width: 0%; min-width: 60px; height: 60px; float: right; overflow: hidden; -webkit-transition: width 0.3s; -moz-transition: width 0.3s; transition: width 0.3s; -webkit-backface-visibility: hidden; } أي شيء يتجاوز/يفيض عن هذا المربع الصغير لن يكون ظاهرًا. لنقم الآن بموضعة حقل البحث. سوف نستعمل قيمة مئوية بالنسبة للعرض حتى نسمح للحقل بأن يتمدد مع تمدد العنصر الحاوي. ومع إضافة الارتفاع (height) وحجم الخط (font-size) والـpadding المناسبة يمكننا توسيط النص داخل العنصر (استعملنا padding بدل line-height لأن الخاصية line-height لا تعمل بشكل جيد في متصفح IE8). قد يبدو استعمالنا للخاصية position: absolute غير ضروري، ولكن استعمالها يقوم بحل مشكلة تظهر عند إغلاق مربع البحث بحيث يبدو الحقل وكأنه ظاهر في الجانب الأيمن لفترة قصيرة جدًا. .sb-search-input { position: absolute; top: 0; right: 0; border: none; outline: none; background: #fff; width: 100%; height: 60px; margin: 0; z-index: 10; padding: 20px 65px 20px 20px; font-family: inherit; font-size: 20px; color: #2c3e50; } input[type="search"].sb-search-input { -webkit-appearance: none; -webkit-border-radius: 0px; } قمنا أيضًا بإزالة التنسيقات الإفتراضية لحقل البحث في متصفحات WebKit. دعونا نقوم الآن بتعريف لون الخط بالنسبة للـplaceholder (الـplaceholder هو نص يظهر داخل حقل البحث قبل أن يقوم المستخدم بكتابة أي شيء بداخله حتى يُعطي المستخدم لمحة عما يجب عليه كتابته في ذلك الحقل): .sb-search-input::-webkit-input-placeholder { color: #efb480; } .sb-search-input:-moz-placeholder { color: #efb480; } .sb-search-input::-moz-placeholder { color: #efb480; } .sb-search-input:-ms-input-placeholder { color: #efb480; } دعونا الآن نقوم بتنسيق زر أيقونة البحث وحقل التأكيد/الإرسال (submit input)، فنحن نريدهما أن يظهرا في نفس المكان لذلك يجب أن نضعهما في الجانب الأيمن ونعطيهما نفس الأبعاد. وبما أنهما سيظهران فوق بعضهما فسوف نعطيهما الخاصية position: absolute: .sb-icon-search, .sb-search-submit { width: 60px; height: 60px; display: block; position: absolute; right: 0; top: 0; padding: 0; margin: 0; line-height: 60px; text-align: center; cursor: pointer; } نريد في بداية الأمر أن تكون الأيقونة قابلة للنقر، وعند فتح حقل البحث فإننا نريد لحقل التأكيد/الإرسال (submit input) أن يكون قابلًا للنقر. لذلك سوف نُعطي حقل التأكيد الخاصية z-index: -1 ونجعله شفافًا/مخفيًا حتى يمكننا رؤية أيقونة البحث: .sb-search-submit { background: #fff; /* IE needs this */ -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; /* IE 8 */ filter: alpha(opacity=0); /* IE 5-7 */ opacity: 0; color: transparent; border: none; outline: none; z-index: -1; } لاحظ أننا لم نقم بجعل الخلفية شفافة وذلك لأن الأمر لا يعمل بشكل جيد في متصفح IE فالعنصر لا يكون قابلًا للنقر عند ذلك، لذلك قمنا باستعمال لون معين (الأبيض) وجعلنا الشفافية (opacity) تساوي صفر. وبما أننا نريد لأيقونة البحث أن تظهر فوق كل شيء فإننا سوف نقوم بإعطائها الخاصية z-index بقيمة عالية، وسوف نقوم باستخدام الفئة الزائفة ::before لإضافة الأيقونة: .sb-icon-search { color: #fff; background: #e67e22; z-index: 90; font-size: 22px; font-family: 'icomoon'; speak: none; font-style: normal; font-weight: normal; font-variant: normal; text-transform: none; -webkit-font-smoothing: antialiased; } .sb-icon-search:before { content: "\e000"; } ودعونا لا ننسى إضافة خط الويب (web font) في بداية ملف الـCSS: /* Search icon by IcoMoon, made with http://icomoon.io/app/ */ @font-face { font-family: 'icomoon'; src:url('../fonts/icomoon/icomoon.eot'); src:url('../fonts/icomoon/icomoon.eot?#iefix') format('embedded-opentype'), url('../fonts/icomoon/icomoon.woff') format('woff'), url('../fonts/icomoon/icomoon.ttf') format('truetype'), url('../fonts/icomoon/icomoon.svg#icomoon') format('svg'); font-weight: normal; font-style: normal; } يمكننا الآن إعطاء العنصر الحاوي (sb-search) عرضًا بقيمة 100% وذلك في حالتين؛ الأولى عند النقر على أيقونة البحث وذلك عن طريق إضافة فئة (class) بالاسم "sb-search-open" باستخدام الجافاسكربت والثانية عندما لا يكون الجافاسكربت مفعّلًا لدى المستخدم: .sb-search.sb-search-open, .no-js .sb-search { width: 100%; } لنقم الآن بتغيير لون الخط الخاص بعنصر أيقونة البحث ونضعه أسفل حقل التأكيد/الإرسال وذلك عن طريق إعطائه قيمة z-index أقل من 90 (وهي القيمة التي أعطيناها للعنصر "sb-icon-search"): .sb-search.sb-search-open .sb-icon-search, .no-js .sb-search .sb-icon-search { background: #da6d0d; color: #fff; z-index: 11; } وأخيرًا لنقم بإضافة الخاصية z-index لحقل التأكيد/الإرسال ولكن بقيمة أكبر من 11 حتى يمكننا النقر عليها: .sb-search.sb-search-open .sb-search-submit, .no-js .sb-search .sb-search-submit { z-index: 90; } انتهينا الآن من تنسيقات CSS وبقي علينا الجافاسكربت. بعض الجافاسكربت لنبدأ بإضافة وإزالة الفئة "sb-search-open". بحيث سوف يتم إضافة الفئة عند النقر على الحاوي الرئيسي (sb-search) وإزالته عند النقر على حقل التأكيد/الإرسال ولكن فقط إذا كان حقل البحث فارغًا، أمّا إذا لم يكن فارغًا فإننا نريد تأكيد عملية البحث. وحتى لا نقوم بإزالة الفئة عند النقر على الحقل (لأن الحاوي بأكمله قابل للنقر) فإننا نحتاج إلى منع حدث النقر (click event) من الانتشار على ذلك العنصر. هذا يعني أنّ النقر على الحقل لن يؤدي إلى إثارة حدث النقر على العناصر الحاوية له. ;( function( window ) { function UISearch( el, options ) { this.el = el; this.inputEl = el.querySelector( 'form > input.sb-search-input' ); this._initEvents(); } UISearch.prototype = { _initEvents : function() { var self = this, initSearchFn = function( ev ) { if( !classie.has( self.el, 'sb-search-open' ) ) { ev.preventDefault(); self.open(); } else if( classie.has( self.el, 'sb-search-open' ) && /^\s*$/.test( self.inputEl.value ) ) { self.close(); } } this.el.addEventListener( 'click', initSearchFn ); this.inputEl.addEventListener( 'click', function( ev ) { ev.stopPropagation(); }); }, open : function() { classie.add( this.el, 'sb-search-open' ); }, close : function() { classie.remove( this.el, 'sb-search-open' ); } } window.UISearch = UISearch; } )( window ); سوف نحتاج أيضًا إلى إضافة الأحداث التي تقوم بإزالة الفئة "sb-search-open" عند النقر خارج مربع البحث، وحتى يعمل ذلك فإننا نريد أن نتعامل مع انتشار الأحداث (event bubbling) عند النقر على الحاوي الرئيسي. ;( function( window ) { function UISearch( el, options ) { this.el = el; this.inputEl = el.querySelector( 'form > input.sb-search-input' ); this._initEvents(); } UISearch.prototype = { _initEvents : function() { var self = this, initSearchFn = function( ev ) { ev.stopPropagation(); if( !classie.has( self.el, 'sb-search-open' ) ) { ev.preventDefault(); self.open(); } else if( classie.has( self.el, 'sb-search-open' ) && /^\s*$/.test( self.inputEl.value ) ) { self.close(); } } this.el.addEventListener( 'click', initSearchFn ); this.inputEl.addEventListener( 'click', function( ev ) { ev.stopPropagation(); }); }, open : function() { var self = this; classie.add( this.el, 'sb-search-open' ); // close the search input if body is clicked var bodyFn = function( ev ) { self.close(); this.removeEventListener( 'click', bodyFn ); }; document.addEventListener( 'click', bodyFn ); }, close : function() { classie.remove( this.el, 'sb-search-open' ); } } window.UISearch = UISearch; } )( window ); وشيء آخر علينا الاهتمام به وهو قصّ مصطلح البحث. عندما نقوم أيضًا بالنقر على أيقونة البحث فإننا نريد أن يكون الحقل مفعّلًا (focused)، ولأنّ هذا يسبب بعض المشاكل في متصفح iOS (لوحة المفاتيح تظهر في نفس الوقت) فإننا نريد تلافي ذلك في هذه الحالة، وعندما يتم إغلاق مربع البحث فإننا نريد أن يكون مربع البحث غير مفعل (blur). هذا سوف يحل بعض المشاكل في بعض الأجهزة التي تُظهر بأنّ المؤشر يومض حتى بعد أن يكون الحقل مغلقًا. ملاحظة جانبية: كلمة focus تدل على أنّ مربع البحث مفعّل (أي أنّ المؤشر بداخله) وكلمة blur تدل على عكس ذلك. ;( function( window ) { // http://stackoverflow.com/a/11381730/989439 function mobilecheck() { var check = false; (function(a){if(/(android|ipad|playbook|silk|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4)))check = true})(navigator.userAgent||navigator.vendor||window.opera); return check; } // http://www.jonathantneal.com/blog/polyfills-and-prototypes/ !String.prototype.trim && (String.prototype.trim = function() { return this.replace(/^\s+|\s+$/g, ''); }); function UISearch( el, options ) { this.el = el; this.inputEl = el.querySelector( 'form > input.sb-search-input' ); this._initEvents(); } UISearch.prototype = { _initEvents : function() { var self = this, initSearchFn = function( ev ) { ev.stopPropagation(); // trim its value self.inputEl.value = self.inputEl.value.trim(); if( !classie.has( self.el, 'sb-search-open' ) ) { ev.preventDefault(); self.open(); } else if( classie.has( self.el, 'sb-search-open' ) && /^\s*$/.test( self.inputEl.value ) ) { self.close(); } } this.el.addEventListener( 'click', initSearchFn ); this.inputEl.addEventListener( 'click', function( ev ) { ev.stopPropagation(); }); }, open : function() { var self = this; classie.add( this.el, 'sb-search-open' ); // focus the input if( !mobilecheck() ) { this.inputEl.focus(); } // close the search input if body is clicked var bodyFn = function( ev ) { self.close(); this.removeEventListener( 'click', bodyFn ); }; document.addEventListener( 'click', bodyFn ); }, close : function() { this.inputEl.blur(); classie.remove( this.el, 'sb-search-open' ); } } window.UISearch = UISearch; } )( window ); وحتى يعمل كل شيء بشكل سلس في أجهزة الهواتف فإننا سوف نحتاج إلى إضافة أحداث اللمس (touch events). كما أنّ إضافة preventDefault في دالّة initSearchFn سوف يمنع حدث النقر واللمس من أن يتفعّلا مع بعضهما في أجهزة اللمس. ;( function( window ) { // http://stackoverflow.com/a/11381730/989439 function mobilecheck() { var check = false; (function(a){if(/(android|ipad|playbook|silk|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4)))check = true})(navigator.userAgent||navigator.vendor||window.opera); return check; } // http://www.jonathantneal.com/blog/polyfills-and-prototypes/ !String.prototype.trim && (String.prototype.trim = function() { return this.replace(/^\s+|\s+$/g, ''); }); function UISearch( el, options ) { this.el = el; this.inputEl = el.querySelector( 'form > input.sb-search-input' ); this._initEvents(); } UISearch.prototype = { _initEvents : function() { var self = this, initSearchFn = function( ev ) { ev.stopPropagation(); // trim its value self.inputEl.value = self.inputEl.value.trim(); if( !classie.has( self.el, 'sb-search-open' ) ) { ev.preventDefault(); self.open(); } else if( classie.has( self.el, 'sb-search-open' ) && /^\s*$/.test( self.inputEl.value ) ) { ev.preventDefault(); self.close(); } } this.el.addEventListener( 'click', initSearchFn ); this.el.addEventListener( 'touchstart', initSearchFn ); this.inputEl.addEventListener( 'click', function( ev ) { ev.stopPropagation(); }); this.inputEl.addEventListener( 'touchstart', function( ev ) { ev.stopPropagation(); } ); }, open : function() { var self = this; classie.add( this.el, 'sb-search-open' ); // focus the input if( !mobilecheck() ) { this.inputEl.focus(); } // close the search input if body is clicked var bodyFn = function( ev ) { self.close(); this.removeEventListener( 'click', bodyFn ); this.removeEventListener( 'touchstart', bodyFn ); }; document.addEventListener( 'click', bodyFn ); document.addEventListener( 'touchstart', bodyFn ); }, close : function() { this.inputEl.blur(); classie.remove( this.el, 'sb-search-open' ); } } window.UISearch = UISearch; } )( window ); وأخيرًا، للأجهزة التي لا تدعم addEventListener وremoveEventListener فإننا سوف نستعمل polyfill. // EventListener | @jon_neal | //github.com/jonathantneal/EventListener !window.addEventListener && window.Element && (function () { function addToPrototype(name, method) { Window.prototype[name] = HTMLDocument.prototype[name] = Element.prototype[name] = method; } var registry = []; addToPrototype("addEventListener", function (type, listener) { var target = this; registry.unshift({ __listener: function (event) { event.currentTarget = target; event.pageX = event.clientX + document.documentElement.scrollLeft; event.pageY = event.clientY + document.documentElement.scrollTop; event.preventDefault = function () { event.returnValue = false }; event.relatedTarget = event.fromElement || null; event.stopPropagation = function () { event.cancelBubble = true }; event.relatedTarget = event.fromElement || null; event.target = event.srcElement || target; event.timeStamp = +new Date; listener.call(target, event); }, listener: listener, target: target, type: type }); this.attachEvent("on" + type, registry[0].__listener); }); addToPrototype("removeEventListener", function (type, listener) { for (var index = 0, length = registry.length; index < length; ++index) { if (registry[index].target == this && registry[index].type == type && registry[index].listener == listener) { return this.detachEvent("on" + type, registry.splice(index, 1)[0].__listener); } } }); addToPrototype("dispatchEvent", function (eventObject) { try { return this.fireEvent("on" + eventObject.type, eventObject); } catch (error) { for (var index = 0, length = registry.length; index < length; ++index) { if (registry[index].target == this && registry[index].type == eventObject.type) { registry[index].call(this, eventObject); } } } }); })(); خاتمة هذا كان كل شيء فيما يخص هذا الدرس، أتمنى أن تكون قد استفدت منه وتعلمت منه شيئًا جديدًا. ترجمة -وبتصرّف- للمقال Expanding Search Bar Deconstructed لصاحبته Mary Lou.
  6. تعتبر المخططات والجداول البيانية charts طريقة لعرض المعلومات information المُستخرجة من البيانات الخام data بشكل بصري يساهم في تقديم المعلومة بشكل سهل للمستخدم ويجعل تحليل الأجزاء الكبيرة من البيانات أكثر سهولة، بالإضافة إلى الجاذبية التي تضيفها هذه المخططات على الموقع بسبب الطريقة التفاعلية التي تُقدَّم بها المعلومات. تُؤمّن مكتبات الجافا سكربت لنا تضمين المخططات إلى موقعنا، والتي تمتاز بسهولة الفهم والتعامل، حيث سنتكلم في هذه المقالة عن أهم 3 مكتبات مفتوحة المصدر مكتوبة بلغة الجافا سكربت ومتخصصة في المخططات البيانية. في هذه المقالة سنتعلم عن أهم 3 مكتبات مفتوحة المصدر متخصصة في المخططات في الجافاسكربت مكتبة chart.js إن مكتبة chart.js هي مكتبة مفتوحة المصدر مكتوبة بالجافا سكربت تسمح لنا ببناء مخططات بيانية جميلة متحركة وتفاعلية نستخدمها في تطبيقات الويب، وهي متاحة تحت ترخيص MIT. يمكن يمكن من خلال هذه المكتبة بناء العديد من المخططات والرسوم البيانية المثيرة للإعجاب بمافيها مخططات الأعمدة bar charts، المخططات الخطية line charts، مخططات المساحة area charts، المقاييس الخطية linear scale، المخططات البيانية المبعثرة scatter charts. تمتاز المخططات المبنية بواسطة هذه المكتبة بأنها متجاوبة بشكل كامل مع غالبية الأجهزة، حيث يتم استدعاؤها وتضمينها في صفحة الويب باستخدام عناصر HTML5. سنطرح مثالًا توضيحيًا لطريقة رسم مخطط أعمدة باستخدام هذه المكتبة، حيث سنُضمِّمن مكتبة chart.js الموجودة في content delivery Network (اختصارًا CDN)، مع العلم أن البيانات المذكورة في المثال توضيحية فقط. <!DOCTYPE html> <html> <head> <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.5.0/Chart.min.js"></script> </head> <body> <canvas id="bar-chart" width=300" height="150"></canvas> <script> new Chart(document.getElementById("bar-chart"), { type: 'bar', data: { labels: ["North America", "Latin America", "Europe", "Asia", "Africa"], datasets: [ { label: "Number of developers (millions)", backgroundColor: ["red", "blue","yellow","green","pink"], data: [7,4,6,9,3] } ] }, options: { legend: { display: false }, title: { display: true, text: 'Number of Developers in Every Continent' }, scales: { yAxes: [{ ticks: { beginAtZero:true } }] } } }); </script> </body> </html> كما نرى في المثال، يتم إنشاء المخطط وتسمية نوعه عندما أسندنا القيمة bar إلى type (يمكن اختيار نوع المخطط هنا مثلًا يمكن تغيير نوع المخطط إلى horizontalBar) نسند قيم ألوان الأعمدة من خلال مصفوفة إلى backgroundColor ونلاحظ أن كل لون أسند إلى اسم العمود الذي يحمل نفس رقم الفهرسة في مصفوفة labels وسيكون الخرج بالشكل التالي: مكتبة Chartist.js مكتبة Chartist.js هي هي مكتبة تحريك بسيطة مكتوبة بلغة جافا سكربت تُستخدم لبناء مخططات وتصميمات جميلة متجاوبة وقابلة للتخصيص، وهي مكتبة مفتوحة المصدر تندرج تحت ترخيص MIT و WTFPL. صممت هذه المكتبة من قبل مجموعة من المطورين الذين كانوا غير راضيين عن الأدوات التي تُستخدم لتصميم المخططات، لذلك قاموا ببناء هذه المكتبة التي توفر ديناميكية رائعة للمصممين والمطورين. للبدء باستخدام هذه المكتبة ضمن تطبيقنا، نحتاج أولًا لتضمين ملف المكتبة Chartist.js والملف CSS، ويمكننا بعدها البدء ببناء العديد من أنواع المخططات والرسوم البيانية مثل مخططات الأعمدة، المخططات الخطية، بالإضافة إلى أنها إضافة بعض التحريك animation، حيث تقوم باستخدام SVG لاستدعاء وتحريك المخططات ديناميكيًا. سنطرح هنا مثالًا عن طريقة استخدام هذه المكتبة لبناء مخطط الفطيرة pie chart: <!DOCTYPE html> <html> <head> <link href="https//cdn.jsdelivr.net/chartist.js/latest/chartist.min.css" rel="stylesheet" type="text/css" /> <style> .ct-series-a .ct-slice-pie { fill: hsl(100, 20%, 50%); /* filling pie slices */ stroke: white; /*giving pie slices outline */ stroke-width: 5px; /* outline width */ } .ct-series-b .ct-slice-pie { fill: hsl(10, 40%, 60%); stroke: white; stroke-width: 5px; } .ct-series-c .ct-slice-pie { fill: hsl(120, 30%, 80%); stroke: white; stroke-width: 5px; } .ct-series-d .ct-slice-pie { fill: hsl(90, 70%, 30%); stroke: white; stroke-width: 5px; } .ct-series-e .ct-slice-pie { fill: hsl(60, 140%, 20%); stroke: white; stroke-width: 5px; } </style> </head> <body> <div class="ct-chart ct-golden-section"></div> <script src="https://cdn.jsdelivr.net/chartist.js/latest/chartist.min.js"></script> <script> var data = { series: [45, 35, 20] }; var sum = function(a, b) { return a + b }; new Chartist.Pie('.ct-chart', data, { labelInterpolationFnc: function(value) { return Math.round(value / data.series.reduce(sum) * 100) + '%'; } }); </script> </body> </html> تتيح لنا هذه المكتبة استخدام العديد من الأنماط Style المُعرَّفة مسبقًا في ملفات CSS التي قمنا بتضمينها في البداية بدلًا من كتابة الأنماط بشكل كامل يدويًا حيث يمكننا استخدام هذه الأنماط في التحكم بشكل المخطط الذي قمنا ببنائه. يُستخدم الصف .ct-chart المبني مُسبقًا في CSS لبناء حاوية container للمخطط، ويستخدم الصف .ct-golden-section للحصول على النسبة aspect ratios المستخدمة في تكبير وتصغيير التصميم ليتلائم مع التصميم المتجاوب responsive design وبالتالي يخفف عنا حساب الأبعاد. واستخدمنا الصف الافتراضي .ct-series-a لتحديد شكل الأقسام الموجودة في المخطط، حيث يمكن استبدال الحرف a للحصول على أشكال أخرى مبنية مُسبقًا. وقد استخدمنا التابع Chartist.Pie لبناء مخطط الفطيرة، وفي حال كنا نريد بناء شكل نوع آخر من المخططات مثل المخطط الخطي يمكن استخدام التابع Chartist.Line وسيكون الخرج لهذا الكود بالشكل: مكتبة D3.js تُعتبر المكتبة D3.js مكتبة أخرى مهمة مفتوحة المصدر مكتوبة بالجافا سكربت. وهي متاحة تحت رخصة BSD. وتقوم هذه المكتبة التي توفر أيضا تحريك ثلاثي الأبعاد 3D animation باستخدام HTML5، SVG، و CSS كإطار لعملها ضمن صفحة الويب. تقوم هذه المكتبة على على مبدأ أساسي في العمل وهو بناء مستند (Document Object Model) DOM ثم معالجته بالاعتماد على التوابع والبيانات المتوفرة داخله لنحصل منها على المخطط البياني الذي نريد بناؤه من خلال إحداث تغييرات في المستند. سنطرح هنا مثال عن مخطط الأعمدة باستخدام هذه المكتبة: <!DOCTYPE html> <html> <head> <style> .chart div { font: 15px sans-serif; background-color: lightblue; text-align: right; padding:5px; margin:5px; color: white; font-weight: bold; } </style> </head> <body> <div class="chart"></div> <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.5.0/d3.min.js"></script> <script> var data = [342,222,169,259,173]; d3.select(".chart") .selectAll("div") .data(data) .enter() .append("div") .style("width", function(d){ return d + "px"; }) .text(function(d) { return d; }); </script> </body> </html> حيث قمنا أولًا ببناء وسم style يحوي جميع اختياراتنا لشكل المخطط ويتم ربطها مع عقدة DOM لمعالجتها بنفس الطريقة التي يعمل بها jQuery. بعد حفط البيانات في المستند document يقوم التابع .enter() بالإذن ببدء التطبيق invoke لبناء العقدة الجديدة التي تحمل البيانات بالنسبة لجميع وكل التوابع التي تلي التابع .enter() فسيتم استدعاؤها من أجل كل عنصر من البيانات. وسيكون الخرج بالشكل التالي: الخلاصة توفر مكتبات المخططات في الجافا سكربت أدوات فعالة لإظهار البيانات بشكل بصري في الموقع الالكتروني الخاص بك، وهذا يزيد من جمالية وتفاعل الموقع. ترجمة وبتصرّف للمقال 3 top open source JavaScript chart libraries لصاحبه Dr. Michael J. Garbade
  7. عندما نسمع كلمة برمجة؛ تخطر ببالنا أسئلة كثيرة، مثل: من أين وكيف نبدأ؟ وما أفضل لغة برمجة؟ هل يمكنني تعلم البرمجة دون الالتحاق بمعهد أو جامعة؟ سنجيب في هذا الفيديو عن الأسئلة التي تخطر في بال أي مبتدئ. سنستخدم لغة JavaScript لشرح المفاهيم الأساسية كالمتغيرات والجمل الشرطية وحلقات التكرار loops وأخيرا الدوال functions، وسنكتب أول برنامج لنا باستخدام لغة جافاسكريبت.
  8. سنتطرّق في هذا الدّرس إلى طريقة إنشاء رسم بياني باستخدام Canvas قبل أن نتطرّق إلى بعض عمليات التّعديل على الصّور وبعض التّحولات، ثم سنختم بإنشاء معرض جميل للصّور. أنصحم بالاطّلاع على المقالات السّابقة حول Canvas هنا على أكاديمية حسوب قبل مواصلة القراءة. رسم الصور تُستخدم الدالة ()drawImage لتصيير الكائن image على العنصر canvas، تأخذ الدالة ()drawImage عدة أشكال overloaded وذلك بتغيير نوع وعدد المعاملات كما سنرى في الأمثلة المقبلة. الشكل الأول: drawImage(image, x, y) تقوم برسم CanvasImageSource محدّدة بالمعامل image في الإحداثيات (x,y). رسم خط بياني بسيط باستخدام الدالة ()drawImage سأعرض مثالًا يستخدم صورة خارجية كخلفية لرسم خط بياني بسيط. يتيح لك استخدام الخلفيات أو backdrops من أن تجعل السكربت الخاص بك أصغر بكثير وتجنب الحاجة لكتابة الشيفرات البرمجية لتوليد الخلفية. سأستخدم في هذا المثال صورة واحدة لذا يمكنني استخدام معالج حدث التحميل load event handler لتنفيذ جمل الرسم. تقوم الدالة ()drawImage بوضع صورة الخلفية عند الإحداثيات (0, 0) والمحدّدة بالزاوية اليسارية العليا للعنصر canvas. function draw() { var ctx = document.getElementById('canvas').getContext('2d'); var img = new Image(); img.onload = function(){ ctx.drawImage(img,,); ctx.beginPath(); ctx.moveTo(30,96); ctx.lineTo(70,66); ctx.lineTo(103,76); ctx.lineTo(170,15); ctx.stroke(); }; img.src = 'https://mdn.mozillademos.org/files/5395/backdrop.png'; } سيظهر الرسم البياني مع صورة الخلفية على الشكل التالي: التحجيم Scaling تتيح لك الدالة ()drawImage تحديد حجم الصور وفق ما ترغب وذلك بإضافة معاملين إضافيين هما الطول والعرض: drawImage(image, x, y, width, height) من خلال الخاصيتين width و height تستطيع إعطاء الحجم المناسب للصورة عند رسمها على الـ canvas. تبليط صورة Tiling an image في المثال التالي سأستخدم الصورة أدناه وأكررها عدّة مرات على مساحة الـ canvas بعد تحجيمها لتبدو كلوحة جدارية. سنحتاج لإنشاء حلقتي تكرار واستدعاء الدالة: drawImage(image, x, y, width, height) مع تغيير إحداثيات رسم الصورة كل مرة، ستكرر الحلقة الأولى الصورة عبر الصفوف والحلقة الثانية ستكرر الصورة عبر الأعمدة. سأقوم بتحجيم الصورة إلى ثلث حجمها الأصلي وهو 50x30 بكسل. ملاحظة: قد تبدو الصور ضبابية عند التحجيم وذلك بحسب نوع التحجيم (تكبير أو تصغير) كذلك إن كانت بعض الصور تحوي نصوصًا قد يؤدي تصغير حجمها إلى جعل النصوص غير مقروءة. function draw() { var ctx = document.getElementById('canvas').getContext('2d'); var img = new Image(); img.onload = function(){ for (var i=;i<4;i++){ for (var j=;j<3;j++){ ctx.drawImage(img,j*50,i*38,50,38); } } }; img.src = 'https://mdn.mozillademos.org/files/5397/rhino.jpg'; } ستظهر الصورة على الشكل التالي: استعرض المثال على jsfiddle. التشريح Slicing الاستخدام الثالث والأخير للدالة ()drawImage هو قص الصور حيث تأخذ الدالة ثمان معاملات: drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight) تحدد الدالة أعلاه مساحة الصورة التي يحددها المستطيل الذي زاويته اليسارية العليا محدّدة بـ (sx, sy) وطوله وعرضه محدّد بـ (sWidth ، sHeight) وتقوم برسمه على الـ canvas في الموضع (dx, dy) وتحدد قيمة التحجيم بالمعاملين (dWidth و dHeight). لتستطيع فهم عمل المعاملات بوضوح انظر للصورة التالية: كما تلاحظ أنه تم تحديد مساحة الصورة المراد قصها اعتمادًا على قيمة المعاملات في الدالة: drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight) تعتبر أداة التشريح او القص مفيدة عندما تريد عمل تراكيب حيث يمكن أن يكون لديك عدة عناصر في ملف صورة واحد وتستخدم الدالة ()drawImage لتجميعها برسم كامل على سبيل المثال يمكنك أن تقوم بإنشاء رسم بياني كصورة png وتكون كل النصوص اللازمة لوضعها على الرسم البياني موجودة في ملف واعتمادًا على البيانات يمكنك تغيير حجم الرسم البياني بكل سهولة. ميزة أخرى في عمل تركيبات الصور هي أنك لن تكون بحاجة لتحميل كل صورة على حدة والذي يمكن أن يحسن من أداء التحميل. تأطير الصورة Framing an image سأقوم بعرض مثال أستخدم فيه نفس الصورة السابقة لقصها باستخدام الدالة ()drawImage ووضعها بداخل إطار Frame: <html> <body onload="draw();"> <canvas id="canvas" width="150" height="150"></canvas> <div style="display:none;"> <img id="source" src="https://mdn.mozillademos.org/files/5397/rhino.jpg" width="300" height="227"> <img id="frame" src="https://mdn.mozillademos.org/files/242/Canvas_picture_frame.png" width="132" height="150"> </div> </body> </html> function draw() { var canvas = document.getElementById('canvas'); var ctx = canvas.getContext('2d'); // Draw slice ctx.drawImage(document.getElementById('source'), 33, 71, 104, 124, 21, 20, 87, 104); // Draw frame ctx.drawImage(document.getElementById('frame'),,); } ستظهر الصورة على الشكل التالي: استعرض المثال على jsfiddle. إنشاء معرض فني Art Gallery حان الوقت الآن لإنشاء معرض صور واستخدام الأدوات التي تعلمناها في التعامل مع الصور في canvas سيكون المعرض عبارة عن جدول يحوي مجموعة صور. عند تحميل الصفحة يقوم العنصر canvas بإدراج الصور ورسم إطار حول كل صورة. في حالتنا هذه سيكون لكل صورة طول وعرض ثابتين وكذلك الإطار. سنقوم باسترداد صورة الإطار من الرابط: https://mdn.mozillademos.org/files/242/Canvas_picture_frame.png ثم إنشاء حلقة تكرار للإنشاء رقعة canvas لكل صورة ومن ثم رسم الصورة ويتم ذلك باستخدام الدالة: parentNode.insertBefore تقوم هذه الدالة بإدراج الصورة قبل إدراج عنصر canvas في عناصر DOM: // Insert before the image document.images.parentNode.insertBefore(canvas,document.images); بعد إنشاء عنصر canvas وإدراج الصورة نقوم باستخدام سياق التصيير واستدعاء الدالة ()drawImage لرسم الصورة على الرقعة: ctx.drawImage(document.images,15,20); إضافة بعض التنسيقات CSS: body { background: -100px repeat-x url(https://mdn.mozillademos.org/files/5415/bg_gallery.png) #4F191A; margin: 10px; } img { display: none; } table { margin: auto; } td { padding: 15px; } السكربت أدناه يقوم برسم الصور مع الإطار: function draw() { // Loop through all images for (var i=;i<document.images.length;i++){ // Don't add a canvas for the frame image if (document.images[i].getAttribute('id')!='frame'){ // Create canvas element canvas = document.createElement('canvas'); canvas.setAttribute('width',132); canvas.setAttribute('height',150); // Insert before the image document.images[i].parentNode.insertBefore(canvas,document.images[i]); ctx = canvas.getContext('2d'); // Draw image to canvas ctx.drawImage(document.images[i],15,20); // Add frame ctx.drawImage(document.getElementById('frame'),,); } } } ستظهر الصورة على الشكل التالي: استعراض المثال على jsfiddle. التحكم بسلوك التحجيم Scaling Behavior كما ذكرت سابقًا أن عملية تحجيم الصورة يمكن أن تؤدي إلى تشويهها أو جعلها تبدو غير واضحة. يمكنك استخدام الخاصية imageSmoothingEnabled للتحكم باستخدام خوارزمية تنعيم أو تمهيد الصورة Smoothing وذلك بتحديد القيمة التي تأخذها إما true أو false ctx.mozImageSmoothingEnabled = false; ctx.webkitImageSmoothingEnabled = false; ctx.msImageSmoothingEnabled = false; ctx.imageSmoothingEnabled = false; التحولات Transformations يوفّر سياق التصيير Rendering Context في canvas مجموعة من الدوال لتطبيق التحولات على الأشكال في canvas كالتحريك، النقل، الدوران والتحجيم حيث يمكنك تطبيق هذه الخصائص بطرق مختلفة على الأشكال البسيطة في الرقعة canvas والحصول على أشكال معقدة أو متحركة بكل سهولة. قبل البدء بالتعرّف على دوال التحولات التي يوفرها سياق التصيير دعونا نلقي نظرة على حالتين لا غنى عنهما سنستخدمهما بمجرد البدء بتوليد أشكال معقدة باستخدام خصائص التحويل في canvas. حالتي الحفظ والاستعادة Saving and restoring state ()save: تقوم هذه الدالة بحفظ الحالة التي تكون عليها الرقعة في الوقت الحالي. ()restore: تعيد هذه الدالة أحدث حالة كانت عليها الرقعة canvas. يتم تخزين حالات الرقعة في الـ stack أو المجمع حيث أنه في كل مرة يتم فيها استدعاء الدالة ()save يتم وضع حالة الرسم الحالية في المجمع. مم تتكون حالة الرسم؟ تتكون حالة الرسم من العناصر التالية: التحولات التي يمكن تطبيقها في سياق التصيير كالنقل translate، الدوران rotate والتحجيم scale. القيم الحالية للخصائص التالية: strokeStyle, fillStyle globalAlpha lineWidth, lineCap, lineJoin miterLimit lineDashOffset shadowOffsetX, shadowOffsetY shadowBlur, shadowColor globalCompositeOperation font, textAlign, textBaseline direction imageSmoothingEnabled مسار clipping path الحالي. ملاحظة: يمكنك استدعاء الدالة ()save عدة مرات كما تريد. في كل مرة يتم فيها استدعاء الدالة ()restore يتم أخذ آخر حالة حفظ موجودة في المجمع واسترجاع جميع الإعدادات الأخيرة المحفوظة. سنكمل في الدرس المقبل شرح دوال التحولات وعرض مثال واضح عن استخدام حالتي الحفظ والاستعادة في سياق التصيير بالإضافة إلى شرح التحولات وطرق التعامل معها مع عرض مثال لكل خاصية تحول لترى كيف يمكنك عمل مجموعة من الأشكال المعقدة وتحريكها على عنصر الرقعة canvas. المصادر
  9. إن عالم الويب أصبح جزءًا لا يتجزأ من حياتنا اليومية هذا ما يجعلنا نبتكر طُرقًا وأساليبَ جديدة ليكون قريبًا منا أكثر. نحنُ كمطوري ويب بحاجة دائمًا إلى إضافةِ لمساتٍ ساحرة وتصاميمَ مبتكرة لإنعاش موقع الويب والتفرّد بعنصر الإبداع في إظهار الموقع بأبهى حلّة وأجمل تصميم. سأضع بين يديك مقدمة لأهم خيارين قد يختار منهما مطوّر الويب عند الشروع في بناء وتصميم محتوى وعناصر موقع الويب. الخيار أو الأسلوب الأوّل وهو الأكثر شهرة بحكم قدمه هو العمل مع عناصر DOM في HTML هذا النهج الذي تستخدمه أنت و 99% من المطورين في العالم الذي يعتمد على إنشاء HTML ،CSS، و JavaScript ليكون لديك عناصر ورسومات تُظهرها بإبداعك الخاص. النهج الثاني والذي ظهر بظهور HTML5 وهو استخدام عنصر <canvas>. والذي يتيح لك أن تجرب وتتخيل الرسم بطريقةٍ أخرى وأكثر متعة غير تلك التي ألِفناها. إنّ كُلًّا من هذين الخيارين له استخدامه الخاص الذي يعتمد بالدرجة الأولى على درجةِ تعقيدِ عنصر التصميم الذي تودُّ إنشاءه، سأقوم بعرض لمحة سريعة عن الفروقات بين المنهجيتين واستخدام كل منهما. إن عملية ترجمة محتوى الصفحة وإظهارها على المتصفّح تسير وفق نظامين هما نظام وضع الاحتفاظ (retained mode (Dom والنظام الحالي أو الفوري (Immediate Mode (canvas. نظام وضع الاحتفاظ (Dom) في هذا النظام تقوم بإرسال العناصر والمحتويات التي قمتَ بإنشائها إلى Graphics API والذي بدوره يقوم بإظهارها على المتصفّح. في الشكل التالي توضيح لكيفية حدوث نظام وضع الاحتفاظ retained mode: النظام الحالي أو الفوري (Canvas) النظام الفوري أو الحالي الذي يعتمد على استخدام <canvas> يتمتع بلياقة عالية تجعله أهلًا لرفع الأحمال الثقيلة heavy lifting فإنك لا تقوم بتحديد ما ستود رسمه فقط بل تقوم بإنشاء وصيانة التصميم أي أن Graphics API الذي قام بالكثير من أجلك في نظام وضع الاحتفاظ retained mode لا يقوم بالشيء الكثير هنا. في الشكل التالي توضيح لكيفية حدوث النظام الفوري أو الحالي Immediate mode: بمجمل الأحوال فإن استخدام عنصر <canvas> أسرع بكثير من استخدام نظام وضع الاحتفاظ retained mode كما أنه يوفّر مرونة كبيرة مقارنة بوضع الاحتفاظ وخاصة في التّعامل مع عناصر كثيرة. توجد سيئة واحدة في استخدام <canvas> وهي في حال العمل على مساحة واسعة سيكون هناك نوعًا من البطء في الأداء. لا يمكنك تخيل ما قد تستطيع عمله في هذه الأداة فبإمكانك القيام برسومات بيانية، إنشاء رسوم بتأثيرات حركية Animations وعمل تراكيب صور photo Compositions أو حتى القيام بمعالجة الفيديو. تدعم Canvas جميع المُتصّفحات الحديثة: فيرفكس (الإصدار 1.5 وما فوق). safari و OS X Dashboard (على نظام ماك). IE (الإصدار 9 وما فوق). Chrome و Opera. لنبدأ الآن بالتعرف على عنصر <canvas> عن قرب وتَعلُّم كيفيّة إنشاء سياق لوحة ثنائي الأبعاد وتقوم برسم أول مثال لك على متصفّحك. الرقعة Canvas يبدو لنا للوهلة الأولى أن عنصر <canvas> يشبه عنصر <img> مع فارق واضح وكبير هو أن عنصر <canvas> ليس لديه خاصيّة src و alt. عنصر <canvas> لديه خاصيتان وحيدتان فقط هما العرض width والطول height، القيم الافتراضية هي 300 بكسل للعرض و 150 بكسل للطول. ككل عناصر HTML فإن عنصر <canvas> يعرف كوسم: <canvas id="tutorial" width="150" height="150"> </canvas> لا تعتبر الخاصيّة id خاصيّة محددة بالرُّقعة <canvas> ولكنها واحدة من الخصائص العامة لـHTML global Html attribute. يمكن تصميم عنصر <canvas> كأي عنصر صورة image وتحديد الخصائص مثل (margin ،border ،background، ...) مثل هذه الخصائص لا تؤثر على عملية الرسم الفعلي هي فقط تحدد الشكل العام للوحة <canvas>. المحتوى الاحتياطي Fallback Content بالرّغم من أنه لا يتم إظهار أو تشغيل عنصر Canvas في المتصفّحات غير الداعمة للـ HTML5 إلا أنه يمكنك إنشاء محتوى احتياطي fallback content ليحل محل <canvas> في المتصفّحات ذات الإصدارات القديمة نوعًا ما والتي لا تدعم HTML5 مثل إصدارات Internet Explorer IE السابقة للإصدار 9. يجب أن توفر دائمًا محتوى احتياطي في حال استخدامك لعنصر الرُّقعة <canvas> ليتم إظهارها على جميع أنواع وإصدارات المتصفّحات التي لا تدعمها. لتوفير محتوى احتياطي ستحتاج إلى إضافة نص أو صورة داخل وسم <canvas> بحيث حين يتم تشغيل الصفحة في متصفّح لا يدعم Canvas سيقوم المتصفّح بتجاهل العنصر الحاوي على المحتوى الاحتياطي وهو عنصر canvas ومن ثم إظهار المحتوى الاحتياطي الذي بداخلها. أما في حال دعم المتصفّح لعنصر Canvas فسيقوم بتجاهل المحتوى الاحتياطي ويقوم بعمل rendering تصيير لعنصر <canvas> بشكل طبيعي. على سبيل المثال لنقم بإنشاء <canvas> وتوفير محتوى احتياطي لها، سأقوم بعمل محتوى احتياطي عبارة عن نص وكذلك مثال آخر يكون المحتوى الاحتياطي فيه عبارة عن صورة <img>: <canvas id="stockGraph" width="150" height="150"> current stock price: $3.15 +0.15 </canvas> <canvas id="clock" width="150" height="150"> <img src="images/clock.png" width="150" height="150" alt=""/> </canvas> وسم الإغلاق </canvas> لا بد أنك لاحظت أن الوسم <img> مثلًا لا يتطلب وسم إغلاق ولا تتأثر بذلك باقي العناصر، عنصر في حين أن عنصر canvas يحتاج لوضع وسم الإغلاق </canvas> هو وسم مطلوب required وذلك لأنه في حال عدم وضعه سيتم اعتبار كل شيء في الصفحة بعد الوسم <canvas> هو عبارة عن محتوى احتياطي ولن يتم إظهاره. سياق التصيير The rendering Context يقوم عنصر <canvas> بإنشاء سطح ذو قياس ثابت يتسع لأكثر من سياق تصيير rendering context التي يتم استخدامها لإنشاء والتعامل مع المحتوى المعروض. سنركز هنا على سياق التصيير ثنائي الأبعاد 2D rendering context. يوجد سياقات أخرى توفر أنواعًا مختلفة من التصيير rendering مثل السياق ثلاثي الأبعاد WebGL الذي يرتكز على OpenGL ES. بطبيعة الحال عند إنشاء <canvas> ستكون فارغة ولإظهار شيء فإن السكربت يحتاج للوصول إلى سياق التصيير Rendering Context والرسم عليه. عنصر <canvas> يحتوى على دالة method تسمى ()getContext تستَخدم لتطبيق سياق التصيير Rendering Context ودوال الرسم الخاصة به، تأخذ الدالة ()getContext معاملًا واحدًا one parameter وهو نوع السياق، للرسومات ثنائية الأبعاد 2D Graphics نقوم بتحديد “2d” لنحصل على سياق تصيير ثنائي الأبعاد Canvas RenderingContext2D. كما في المثال التالي: var canvas = document.getElementById('tutorial'); var ctx = canvas.getContext('2d'); يجلب السطر الأول من السكربت العنصر <canvas> من خلال استدعاء الدالة ()document.getElementById، ثم تحديد نوع السياق عن طريق الدالة ()getContext. التأكد من الدعم Checking for support شرحنا في البداية أن المحتوى الاحتياطي يظهر في المتصفّحات التي لا تدعم عنصر <canvas> يمكن أيضًا للسكربت أن يقوم بالتأكُّد من الدّعم برمجيًا من خلال اختبار بسيط لوجود الدالة ()getContext. لنقم بتعديل الشيفرة البرمجية أعلاه ونستخدم السكربت في التأكد من دعم المتصفّح لعنصر <canvas> ليصبح على النحو التالي: var canvas = document.getElementById('tutorial'); if (canvas.getContext){ var ctx = canvas.getContext('2d'); // drawing code here } else { // <canvas> unsupported code here } قالب الهيكل Skeleton Template سأقوم ببناء قالب أساسي لاستخدامه كنقطة انطلاق لأمثلتنا اللاحقة. ملاحظة: ليس من الأمور الجيدة تضمين السكربت في وسم HTML ولكن قمت بذلك للحفاظ على المثال موجز ومختصر. <!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <title>canvas tutorial</title> <script type="text/javascript"> function draw(){ var canvas = document.getElementById('tutorial'); if (canvas.getContext){ var ctx = canvas.getContext('2d'); } } </script> <style type="text/css"> canvas { border: 1px solid black; } </style> </head> <body onload="draw();"> <canvas id="tutorial" width="150" height="150"> </canvas> </body> </html> يحتوي السكربت على دالة تدعى ()draw التي تُنّفذ في حين الانتهاء من تحميل صفحة الويب، يتم ذلك عن طريق الاستماع للحدث load في الصفحة حيث يتم استدعاءه باستخدام ()window.setTimeout و ()window.setInterval أو أي معالج حدث آخر event handler طالما أنه يتم تحميل الصفحة أولًا. عند تنفيذ السكربت أعلاه سيظهر لنا الشكل التالي في صفحة الويب: تطبيق مثال بسيط دعونا نلقي نظرة على مثال بسيط يقوم برسم مستطيلين متقاطعين أحدهما يحوي على شفافية لونية alpha transparency. <!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <script type="application/javascript"> function draw() { var canvas = document.getElementById("canvas"); if (canvas.getContext) { var ctx = canvas.getContext("2d"); ctx.fillStyle = "rgb(200,0,0)"; ctx.fillRect (10, 10, 55, 50); ctx.fillStyle = "rgba(0, 0, 200, 0.5)"; ctx.fillRect (30, 30, 55, 50); } } </script> </head> <body onload="draw();"> <canvas id="canvas" width="150" height="150"> </canvas> </body> </html> اعتمادًا على القالب الأساسي الذي أنشأتُه وضعت السكربت الخاص باستخدام سياق التصيير ()Rendering Context getContext ضمن الشرط if وذلك ليتم تنفيذه في حال تحقق وجود دعم للعنصر <canvas> من قِبل المتصفّح. تقوم الدالة ()getContext بإرجاع متغير يمكن اعتباره غرض object يحوي على مجموعة دوال تقوم بعمليات الرسم وإنشاء الأشكال. الخاصيّة fillStyle تقوم بتحديد اللّون الذي سيتم به تلوين العنصر التالي، تأخذ الخاصيّة fillStyle اللّون بنظام (RGB (Red, Green, Blue والشفافية opacity. لاحظ أنه تم تلوين المستطيل الأول باللون الأحمر حيث أخذ قيمة 200 في حقل red، والمستطيل الثاني بلون أزرق مع تحديد الشفافية opacity=0.5 أي سيكون ذو شفافية لونية بنسبة 50% (تأخذ الخاصيّة opacity قيمها من 0.0 -1.0). الدالة ()FillRect ترسم مستطيل ذو مساحة لونية كاملة solid color وذلك بتحديد إحداثيات النقاط الأربع للمستطيل الذي نود رسمه ابتداءً من الزاوية اليسارية العليا التي تأخذ احداثيات المركز (0,0). كانت هذه أبرز النقاط التي تحيطك بالمفاهيم الأساسية لاستخدام عنصر canvas في الدروس المقبلة ستكون الشروحات غنية للغاية وسترى إمكانيات مذهلة لعنصر canvas تشحذ أفكارك الابداعية بامتياز. المصادر
  10. كما هو موضح في العنوان فإنّك في هذا الدرس سوف تتعلم كيفية إنشاء تأثير وكأنّك تتصفح كتابًا ما. وسوف نستخدم في هذا الدرس إضافة تدعى BookBlock، والفكرة من هذا الدرس هو أنّك سوف تقوم بإنشاء تأثير يُمكّن الزوار من تصفح موقع ما وكأنهم يتصفحون أحد الكتب. الفكرة هي أنّك سوف تتصفح صفحات الموقع باستخدام سهمين سوف يكونان موجودين في أعلى الصفحة أو أزرار لوحة المفاتيح أو حتى بالسحب باستخدام الفأرة وأيضًا سيكون هناك قائمة جانبية سوف تظهر عندما تقوم بالضغط على أيقونة ما. وسوف تحتوي القائمة الجانبية على روابط لصفحات الموقع وعندما تقوم بالضغط على أحد هذه الروابط فإنّها سوف تنقلك إلى الصفحة المطلوبة. سوف نستخدم أيضًا إضافة اسمها jScrollPane وذلك للحصول على شريط تمرير (scrollbar) يظهر عندما يكون المحتوى أطول من ارتفاع المتصفح. وهذه قائمة بإضافات jQuery التي سوف نستخدمها: BookBlockCustom jQuery++jScrollPanejQuery Mouse Wheel PluginCustom Mdernizerيمكنك معاينة المثال الموضح في هذا الدرس من هنا. كما يمكنك تحميل الملفات المصدرية. بنية ملف HTMLفي البداية يجب أن يكون لدينا حاوٍ رئيسي لاحتواء جميع العناصر، وداخل هذا الحاوي سوف يكون هناك عنصر <div> للقائمة الجانبية وسوف نعطيه فئة (class) بالاسم "menu-panel" وسوف يكون هناك عنصر <div> آخر يحتوي على المحتوى الرئيسي للموقع وسوف نعطيه فئة بالاسم "bb-custom-wrapper". وفي داخل كل قسم سوف يكون هناك حاوٍ للمحتوى وعنصر <div> سوف نحتاجه من أجل شريط التمرير الذي ذكرناه سابقًا. <div id="container" class="container"> <div class="menu-panel"> <h3>Table of Contents</h3> <ul id="menu-toc" class="menu-toc"> <li class="menu-toc-current"><a href="#item1">Self-destruction</a></li> <li><a href="#item2">Why we die</a></li> <li><a href="#item3">The honeymoon</a></li> <li><a href="#item4">A drawing joke</a></li> <li><a href="#item5">Commencing practice</a></li> </ul> </div> <div class="bb-custom-wrapper"> <div id="bb-bookblock" class="bb-bookblock"> <div class="bb-item" id="item1"> <div class="content"> <div class="scroller"> <h2>Self-destruction</h2> <p>...</p> </div> </div><!-- /content --> </div><!-- /bb-item --> <div class="bb-item" id="item2"><!-- ... --></div> <div class="bb-item" id="item3"><!-- ... --></div> <div class="bb-item" id="item4"><!-- ... --></div> <div class="bb-item" id="item5"><!-- ... --></div> </div><!-- /bb-bookblock --> <nav> <a id="bb-nav-prev" href="#">←</a> <a id="bb-nav-next" href="#">→</a> </nav> <span id="tblcontents" class="menu-button">Table of Contents</span> </div><!-- /bb-custom-wrapper --> </div><!-- /container -->سوف نقوم بربط عناصر القائمة الجانبية بصفحات الموقع (التي تحمل الفئة "bb-item)، وسوف نُضيف أيضًا سهمين في أعلى الصفحة من أجل التنقل بين الصفحات وزر يقوم بفتح وإغلاق القائمة الجانبية. لنقم الآن بإضافة تنسيقات CSS. تنسيقات CSSلن نتحدث هنا عن التنسيقات التي تأتي مع إضافة BookBlock لأنك سوف تجدها داخل ملف bookblock.css، وإنّما سوف نُركّز على التنسيقات الأخرى المهمة. لنبدأ التنسيقات بإضافة سطر يقوم بجلب الخط المسمى "Lato" من خدمة Google web fonts: @import url(http://fonts.googleapis.com/css?family=Lato:300,400,700);سوف نقوم بإعطاء الوسم <html> الخاصية height: 100% وذلك لأننا سوف نحتاج أن نجعل بعض العناصر تتمدد على ارتفاع المتصفح كاملًا: html { height: 100%; }سوف نستخدم أيضًا الخاصية box-sizing: border-box وذلك حتى نستخدم قيم مئوية لكل من العرض والإرتفاع أثناء استخدام padding دون القلق حول أبعاد العناصر والقيام بعمليات حسابية نحن بغنىً عنها: *, *:after, *:before { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; padding: 0; margin: 0; }لنقم الآن بتعريف الخط الذي سوف نستخدمه في الموقع (العنصر body) ونُعطيه ارتفاع بقيمة 100% (تذكّر أننا نريد ارتفاع بهذه القيمة لأنه سوف يكون هناك عناصر تتموضع بشكل مطلق (absolute positioning) وسوف تحتاج أن تتمدد على ارتفاع المتصفح كاملًا): body { font-family: 'Lato', Calibri, Arial, sans-serif; font-weight: 400; font-size: 100%; color: #333; height: 100%; }سوف نستعمل أيضًا إضافة Modernizr وسوف نُضيف الفئة "no-js" إلى وسم <html>، فإذا كانت الجافاسكربت مفعّلة فإنّ Modernizr سوف يستبدل تلك الفئة بالفئة "js". وهذا سوف يساعدنا على إعطاء خصائص CSS معينة لبعض العناصر التي لا نريدها إذا كانت الجافاسكربت معطّلة. لاحظ أيضًا أننا نحتاج أن يكون عرض الصفحة وارتفاعها بقيمة 100% فقط إذا كانت الجافاسكربت مفعّلة وعندها فقط نريد من العنصر body أن يكون له خاصية overflow: hidden: .js body { overflow: hidden; }وهذه بعض التنسيقات الخاصة بالروابط: a { color: #555; text-decoration: none; } a:hover { color: #000; }نريد من الحاوي الرئيسي أن يكون بعرض وارتفاع المتصفح كاملًا، وسوف نقوم بموضعة القائمة الجانبية خارج هذا الحاوي باستعمال الخاصية left وإعطائها قيمة سالبة تكون بنفس قيمة العرض الخاص بالقائمة الجانبية. والفكرة هي أنّه عند النقر على أيقونة القائمة الجانبية فإنّ الحاوي سوف يتحرك إلى اليمين مما يؤدي إلى ظهور القائمة الجانبية. دعونا إذًا نقوم بإعطاء الحاوي الرئيسي عرضًا وارتفاعًا بقيمة 100% وأن نُضيف الخاصية transition إلى الحاوي container: .container, .bb-custom-wrapper, .bb-bookblock { width: 100%; height: 100%; } .container { position: relative; left: 0px; transition: left 0.3s ease-in-out; }عند النقر على أيقونة القائمة الجانبية فإن فئة (class) أخرى سوف يتم إضافتها إلى الحاوي container والتي سوف تحتوي على الخاصية left: 240px (نفس العرض الخاص بالقائمة الجانبية) وبالتالي فإنّ الصفحة كاملة سوف تتحرك إلى اليمين بمقدار 240px وبالتالي ظهور القائمة الجانبية: .slideRight { left: 240px; }ولكن بدون الجافاسكربت لن نكون قادرين على القيام بما سبق لذلك سوف نقوم بإضافة الخاصية padding-left: 240px: .no-js .container { padding-left: 240px; }ونريد أن تكون القائمة الجانبية ثابتة في الجانب الأيسر بشكل افتراضي: .menu-panel { background: #f1103a; width: 240px; height: 100%; position: fixed; z-index: 1000; top: 0; left: 0; text-shadow: 0 1px 1px rgba(0,0,0,0.1); }وإذا كانت الجافاسكربت مفعلة فسوف نقوم بموضعة القائمة الجانبية بشكل مطلق وإلى اليسار بقيمة -240px: .js .menu-panel { position: absolute; left: -240px; }وهذه هي التنسيقات الخاصة بعناصر القائمة الجانبية: .menu-panel h3 { font-size: 1.8em; padding: 20px; font-weight: 300; color: #fff; box-shadow: inset 0 -1px 0 rgba(0,0,0,0.05); } .menu-toc { list-style: none; } .menu-toc li a { display: block; color: #fff; font-size: 1.1em; line-height: 3.5; padding: 0 20px; cursor: pointer; background: #f1103a; border-bottom: 1px solid #dd1338; } .menu-toc li a:hover, .menu-toc li.menu-toc-current a{ background: #dd1338; }وأمّا بالنسبة للقائمة الرئيسية التي سوف تحتوي على السهمين فإننا سوف نقوم بموضعتها بشكل مطلق وفوق جميع العناصر الأخرى: .bb-custom-wrapper nav { top: 20px; left: 60px; position: absolute; z-index: 1000; }كما أنّ روابط السهمين وزر القائمة الجانبية سوف يتموضعان بشكل مطلق (position: absolute) وسوف نعطيها الخاصية border-radius: 50% لنجعلها تظهر كالدائرة: .bb-custom-wrapper nav span, .menu-button { position: absolute; width: 32px; height: 32px; top: 0; left: 0; background: #f1103a; border-radius: 50%; color: #fff; line-height: 30px; text-align: center; speak: none; font-weight: bold; cursor: pointer; } .bb-custom-wrapper nav span:last-child { left: 40px; } .bb-custom-wrapper nav span:hover, .menu-button:hover { background: #000; }سوف يكون الزر الذي يفتح ويغلق القائمة الجانبية موجودًا في أعلى يسار الصفحة وسوف نقوم بإخفاء النص الموجود بداخله (نريد أن تظهر الأيقونة فقط): .menu-button { z-index: 1000; left: 20px; top: 20px; text-indent: -9000px; }لنقم الآن بإنشاء أيقونة بسيطة بدون استعمال أي صور وذلك باستعمال العنصر الزائف :after واستعمال الخاصية box-shadow والتي سوف تعمل على إنشاء الخطين العلوي والسفلي للأيقونة: .menu-button:after { position: absolute; content: ''; width: 50%; height: 2px; background: #fff; top: 50%; margin-top: -1px; left: 25%; box-shadow: 0 -4px #fff, 0 4px #fff; }وفي حالة كان الجافاسكربت معطلًا فإننا نريد أن نخفي هذه العناصر: .no-js .bb-custom-wrapper nav span, .no-js .menu-button { display: none; }لننتقل الآن إلى تنسيق الأجزاء الداخلية لكل قسم من أقسام الصفحة (bb-item). نريد أن يتم موضعة المحتوى (content) بشكل مطلق ونريد ان نستعمل الخاصية overflow: hidden، وهذا مهم لأننا نريد تطبيق شريط التمرير هنا ونريد أن نفعل ذلك فقط عند قلب/تغيير الصفحة. فإذا لم نستخدم الخاصية overflow: hidden فإنّك سوف ترى المحتوى يتداخل ببعضه. وأعيد وأكرر مرة أخرى بأنّ هذا سوف يحدث فقط إذا كان الجافاسكربت مفعلًا ولذلك سوف نستخدم الفئة "js": .js .content { position: absolute; top: 60px; left: 0; bottom: 50px; width: 100%; overflow: hidden; }العنصر <div class="scroller"> هو الذي سوف ينمو مع المحتوى لذلك سوف نعطيه الخاصية padding: .scroller { padding: 10px 5% 10px 5%; }لاحظ أننا استعملنا قيم مئوية للجوانب وذلك حتى نجعل الصفحة تتجاوب مع حجم الشاشة. دعونا نتخلص من الحواف الحادة عندما نقوم بالتمرير (scroll) وذلك باستخدام العناصر الزائفة إلى أعلى وأسفل عنصر المحتوى مع استخدام تدرج بين اللون الأبيض والشّفّاف: .js .content:before, .js .content:after { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 20px; z-index: 100; pointer-events: none; background: linear-gradient( to bottom, rgba(255,255,255,1) 0%, rgba(255,255,255,0) 100% ); } .js .content:after { top: auto; bottom: 0; background: linear-gradient( to top, rgba(255,255,255,1) 0%, rgba(255,255,255,0) 100% ); }هذا سوف يجعل النص يظهر بشكل باهت. لنقم الآن بتنسيق عناصر النصوص: .content h2 { font-weight: 300; font-size: 4em; padding: 0 0 10px; color: #333; margin: 0 1% 40px; text-align: left; box-shadow: 0 10px 0 rgba(0,0,0,0.02); text-shadow: 0 0 2px #fff; } .no-js .content h2 { padding: 40px 1% 20px; } .content p { font-size: 1.2em; line-height: 1.6; font-weight: 300; padding: 5px 8%; text-align: justify; }كل ما تبقى علينا الآن من تنسيقات CSS هو استخدام الـmedia queries. فإذا كانت الجافاسكربت معطلة فإننا لا نريد أن تظهر القائمة الجانبية إذا كان العرض أقل من 800px. كان هذا فقط مثالًا بسيطًا على كيفية التحكم بالعناصر تحت ظروف وشروط معينة. الـmedia query الأخيرة سوف تعمل على تكبير الخط قليلًا من أجل الأجهزة صغيرة الحجم كالهواتف. @media screen and (max-width: 800px){ .no-js .menu-panel { display: none; } .no-js .container { padding: 0; } } @media screen and (max-width: 400px){ .menu-panel, .content { font-size: 75%; } }كان هذا كل ما يتعلق بتنسيقات CSS ويتبقى علينا استخدام بعض الجافاسكربت. بعض الجافاسكربتسوف نبدأ اولًا بتخزين (caching) بعض العناصر حتى لا نضطر إلى استدعائها في كل مرة وسوف نقوم أيضًا بتهئية/مناداة إضافة BookBlock. نُريد أيضًا أن نقوم بضبط بعض الأمور بعد كل قلب/تغيير للصفحة وهذه الأمور هي رقم الصفحة الحالية والسلوك الخاص بإضافة jScrollPane. وهذا محدد في الاستدعاء الخلفي (callback) المسمى onEndFlip والممرر إلى إضافة BookBlock. var $container = $( '#container' ), // the element we will apply the BookBlock plugin to $bookBlock = $( '#bb-bookblock' ), // the BookBlock items (bb-item) $items = $bookBlock.children(), // index of the current item current = 0, // initialize the BookBlock bb = $( '#bb-bookblock' ).bookblock( { speed : 800, perspective : 2000, shadowSides : 0.8, shadowFlip : 0.4, // after each flip... onEndFlip : function(old, page, isLimit) { // update the current value current = page; // update the selected item of the table of contents (TOC) updateTOC(); // show and/or hide the navigation arrows updateNavigation( isLimit ); // initialize the jScrollPane on the content div for the new item setJSP( 'init' ); // destroy jScrollPane on the content div for the old item setJSP( 'destroy', old ); } } ), // the navigation arrows $navNext = $( '#bb-nav-next' ), $navPrev = $( '#bb-nav-prev' ).hide(), // the table of content items $menuItems = $container.find( 'ul.menu-toc > li' ), // button to open the TOC $tblcontents = $( '#tblcontents' ), transEndEventNames = { 'WebkitTransition': 'webkitTransitionEnd', 'MozTransition': 'transitionend', 'OTransition': 'oTransitionEnd', 'msTransition': 'MSTransitionEnd', 'transition': 'transitionend' }, // transition event name transEndEventName = transEndEventNames[Modernizr.prefixed('transition')], // check if transitions are supported supportTransitions = Modernizr.csstransitions;لنقم أولًا بربط الأحداث ببعض العناصر التي تم تهيئتها سابقًا، كما أننا نريد أن نقوم بتهيئة jScrollPane لأول عنصر (العنصر الحالي). function init() { // initialize jScrollPane on the content div of the first item setJSP( 'init' ); initEvents(); }بما أننا سوف نقوم بتهيئة وإعادة تهيئة وتدمير jScrollPane فلنقم بتعريف دالة لذلك: function setJSP( action, idx ) { var idx = idx === undefined ? current : idx, $content = $items.eq( idx ).children( 'div.content' ), apiJSP = $content.data( 'jsp' ); if( action === 'init' && apiJSP === undefined ) { $content.jScrollPane({verticalGutter : 0, hideFocus : true }); } else if( action === 'reinit' && apiJSP !== undefined ) { apiJSP.reinitialise(); } else if( action === 'destroy' && apiJSP !== undefined ) { apiJSP.destroy(); } }سوف نحتاج إلى ربط العديد من الأحداث كالتالي: سوف يتم استدعاء الدالتين ()next و()prev الخاصتين بإضافة BookBlock وذلك عند النقر على أزرار التنقل أو السحب باستخدام الفأرة.سوف يظهر جدول المحتويات أو يختفي عند النقر على زر القائمة (tblcontents$).سوف يتم استدعاء الدالة ()jump الخاصة بإضافة BookBlock وذلك عند النقر على أي عنصر من عناصر جدول المحتويات.سوف يتم تهيئة jScrollPane عند القيام بتغيير حجم النافذة (window resize).function initEvents() { // add navigation events $navNext.on( 'click', function() { bb.next(); return false; } ); $navPrev.on( 'click', function() { bb.prev(); return false; } ); // add swipe events $items.on( { 'swipeleft' : function( event ) { if( $container.data( 'opened' ) ) { return false; } bb.next(); return false; }, 'swiperight' : function( event ) { if( $container.data( 'opened' ) ) { return false; } bb.prev(); return false; } } ); // show TOC $tblcontents.on( 'click', toggleTOC ); // click a menu item $menuItems.on( 'click', function() { var $el = $( this ), idx = $el.index(), jump = function() { bb.jump( idx + 1 ); }; current !== idx ? closeTOC( jump ) : closeTOC(); return false; } ); // reinit jScrollPane on window resize $( window ).on( 'debouncedresize', function() { // reinitialise jScrollPane on the content div setJSP( 'reinit' ); } ); } ظهور أزرار التنقل من عدمه سوف يعتمد على الصفحة الحالية، فإذا كُنّا في الصفحة الأولى فإننا سوف نرى فقط زر "التالي" وإذا كُنّا في الصفحة الأخيرة فإننا سوف نرى فقط زر "السابق": function updateNavigation( isLastPage ) { if( current === 0 ) { $navNext.show(); $navPrev.hide(); } else if( isLastPage ) { $navNext.hide(); $navPrev.show(); } else { $navNext.show(); $navPrev.show(); } }عندما نفتح جدول المحتويات (القائمة الجانبية) فإننا نريد أن تختفي عناصر التنقل وأن تظهر مرة أخرى عندما نقوم بإغلاق القائمة الجانبية. سوف نقوم بتحريك القائمة الجانبية باستخدام خاصية transition، وإذا لم تكن هذه الخاصية مدعومة من المتصفح فإننا سوف نستخدم fallback بسيط: function toggleTOC() { var opened = $container.data( 'opened' ); opened ? closeTOC() : openTOC(); } function openTOC() { $navNext.hide(); $navPrev.hide(); $container.addClass( 'slideRight' ).data( 'opened', true ); } function closeTOC( callback ) { $navNext.show(); $navPrev.show(); $container.removeClass( 'slideRight' ).data( 'opened', false ); if( callback ) { if( supportTransitions ) { $container.on( transEndEventName, function() { $( this ).off( transEndEventName ); callback.call(); } ); } else { callback.call(); } } } خاتمةهذا كان كل شيء يخص هذا الدرس أتمنى أن يكون قد أعجبك وأن تكون قد وجدته مفيدًا. ترجمة -وبتصرّف- للدرس Fullscreen Pageflip Layout لصاحبته Mary Lou.
  11. ‏AJAX‏AJAX هي اختصار للعبارة "asynchronous JavaScript and XML"، وهي وسيلة لجلب البيانات من الخادوم دون الحاجة لإعادة تحميل الصّفحة، وهي تقوم على استخدام كائن مُتاح في المتصفّح اسمه XMLHttpRequest (أو XHR اختصارًا) لإرسال الطّلب إلى الخادوم ثمّ التّعامل مع البيانات الّتي يُجيب بها الخادوم. تُوفّر jQuery الوظيفة ‎$.ajax‎ (ووظائف أخرى مرافقة مُختصرة) لتسهيل العمل مع طلبات XHR في جميع المتصفّحات. ‏‎$.ajax‎بإمكاننا استخدام الوظيفة ‎$.ajax()‎ المُرفقة مع jQuery بعدّة أساليب: إحداها أن نُمرّر إليها كائنًا يحوي الإعدادات فقط، أو أن نُمرّر الرّابط مع أو بدون كائن الإعدادات. لنُلقِ نظرة على الأسلوب الأول: // أنشئ دالّة الاستدعاء الرّاجع الّتي ستُنفّذ عندما ينجح طلب AJAX var updatePage = function( resp ) { $( '#target').html( resp.people[0].name ); }; // وعندما يفشل var printError = function( req, status, err ) { console.log( 'something went wrong', status, err ); }; // أنشئ كائن الإعدادات الذي يصف الطّلب var ajaxOptions = { url: '/data/people.json', dataType: 'json', success: updatePage, error: printError }; // أرسل الطّلب $.ajax(ajaxOptions);بإمكانك طبعًا أن تُمرّر كائنًا حرفيًّا مباشرةً إلى الوظيفة‎$.ajax() ‎ وأن تستخدم دالّة مجهولة محلّ success وerror، هذا الأسلوب كتابته أسهل، وصيانته في المستقبل أسهل: $.ajax({ url: '/data/people.json', dataType: 'json', success: function( resp ) { $( '#target').html( resp.people[0].name ); }, error: function( req, status, err ) { console.log( 'something went wrong', status, err ); } });كما قلنا، بإمكانك استخدام الوظيفة‎$.ajax() ‎ بأسلوب ثانٍ، وذلك بتمرير الرّابط أوّلًا ثمّ كائن الإعدادات ثانيًا (ليس إلزاميًّا). يُفيدك هذا في حال رغبت في استخدام الإعدادات المبدئيّة للوظيفة أو في حال رغبت في استخدام كائن الإعدادات نفسه لأكثر من رابط: $.ajax( '/data/people.json', { type: 'GET', dataType: 'json', success: function( resp ) { console.log( resp.people ); }, error: function( req, status, err ) { console.log( 'something went wrong', status, err ); } });في المثال السّابق، لا تشترط الوظيفة سوى الرّابط، ولكنّ إضافة كائن الإعدادات تسمح لنا بإخبار jQuery بنوع البيانات الّتي نُرسلها، وأي فعل HTTP نستخدمه (POST، GET، إلخ...‏‏)، وما نوع البيانات الّتي نتوقّع استقبالها من الخادوم، وما الّذي يجب فعله إن نجح الطّلب أو فشل... اطّلع على وثائق الوظيفة‎$.ajax() ‎ لقراءة كامل الخيارات الّتي يمكن إضافتها إلى كائن الإعدادات. ‏A في AJAX تعني "لامتزامن"تجري طلبات AJAX بصورة لا متزامنة، وهذا يعني أنّ الوظيفة‎$.ajax ‎ تنتهي قبل انتهاء الطّلب، وقبل أن تُستدعى دّالة success، أيّ أنّ جملة return تُنفّذ قبل أن يصل جواب الطّلب. فالدّالة getSomeData في المثال التّالي ستُعيد قيمة data قبل أن تُعرّف، مما يؤدّي إلى وقوع خطأ: تحذير: نصّ برمجيّ غير سليم var getSomeData = function() { var data; $.ajax({ url: '/data/people.json', dataType: 'json', success: function(resp) { data = resp.people; } }); return data; } $( '#target' ).html( getSomeData().people[0].name );‏X في AJAX تعني JSON!وضع المصطلح AJAX عام 2005 ليصف طريقة لجلب البيانات من الخادوم دون الحاجة لإعادة تحميل كامل الصّفحة. في ذلك الوقت، كانت الصّيغة الأكثر شيوعًا للبيانات الّتي تُرسلها الخوادم هي XML، أمّا اليوم، فإنّ JSON هي الصّيغة الّتي تعتمدها أكثر التّطبيقات الحديثة. صيغة JSON في أساسها هي سلسلة نصّيّة (string) تُمثّل البيانات، وتبدو مُشابهة كثيرًا لكائن JavaScript عاديّ، ولكنّها لا تستطيع تمثيل كلّ أنواع البيانات الّتي يستطيع كائن JavaScript تمثيلها. فمثلًا: لا يمكن لـJSON تمثيل كائنات التّاريخ (Date) ولا الدّوال (functions). فيما يلي مثال عن نصّ JSON، لاحظ كيف تُحاط كلّ أسماء الخصائص بعلامتي اقتباس مُضاعفتين: { "people" : [ { "name" : "Ben", "url" : "http://benalman.com/", "bio" : "I create groovy websites, useful jQuery plugins, and play a mean funk bass. I'm also Director of Pluginization at @bocoup." }, { "name" : "Rebecca", "url" : "http://rmurphey.com", "bio" : "Senior JS dev at Bocoup" }, { "name" : "Jory", "url" : "http://joryburson.com", "bio" : "super-enthusiastic about open web education @bocoup. lover of media, art, and fake mustaches." } ] }تذكّر أنّ JSON هو تمثيل نصّيّ لكائن، ما يعني أنّه يجب تفسير السّلسلة النّصيّة لتحويلها إلى كائن JavaScript عاديّ قبل التّعامل معها. عندما تعمل مع جواب ورد من الخادوم بصيغة JSON، فإنّ jQuery تتولّى هذه المهمّة عنك. ولكن من المهمّ التمييز بين الكائنات الفعليّة، وطريقة تمثيلها في JSON. ملاحظة: إن أردت إنشاء سلسلة JSON نصّيّة من كائن JavaScript أو تفسير سلسلة JSON نصّيّة لتحويلها إلى كائن JavaScript دون الاستعانة بـjQuery، فإنّ المُتصفّحات الحديثة تُقدّم الوظيفتين ‎JSON.stringify()‎ و‎JSON.parse()‎، ويمكن إضافة هذه الخصائص إلى المُتصفّحات القديمة باستخدام المكتبة json2.js. توفّر jQuery أيضًا وظيفة ‎jQuery.parseJSON()‎، الّتي توافق الوظيفة ‎JSON.parse()‎ في المتصفّحات، إلّا أنّها لا توفّر وظيفة تُقابل ‎JSON.stringify()‎. وظائف مُختصرةإن كان كلّ ما نريده إرسال طلب بسيط، دون الاهتمام بالتّعامل مع الأخطاء الّتي قد تقع، فإنّ jQuery تُوفّر وظائف مُختصرة تسمح لنا بفعل ذلك. تستقبل كل وظيفة مُختصرة رابطًا وكائن إعدادات غير إلزاميّ، ودالّة تُستدعى عند نجاح الطّلب فقط: $.get( '/data/people.html', function( html ){ $( '#target' ).html( html ); }); $.post( '/data/save', { name: 'Rebecca' }, function( resp ) { console.log( resp ); });إرسال البيانات والعمل مع النّماذجبإمكاننا إرسال بيانات مع طلبنا بتعيين قيمة للخاصة data في كائن الإعدادات، أو تمرير كائن كمُعامل ثانٍ للوظائف المُختصرة. ستُضاف هذه البيانات إلى الرّابط في طلبات GET بصورة "جملة استعلام" (query string)، أمّا في طلبات POST فإنّها ستُرسل كبيانات نموذج. توفّر jQuery وظيفة مُفيدة ‎.serialize()‎ الّتي تستقبل مُدخلات نموذج وتُحوّلها إلى صيغة "جملة استعلام" (مثل field1name=field1value&field2name=field2value...): $( 'form' ).submit(function( event ) { event.preventDefault(); var form = $( this ); $.ajax({ type: 'POST', url: '/data/save', data: form.serialize(), dataType: 'json', success: function( resp ) { console.log( resp ); } }); });‏jqXHRتُعيد‎$.ajax() ‎ والوظائف المُختصرة المرافقة لها، كائن jqXHR (اختصارًا لـjQuery XML HTTP Request) والّذي يتضمّن وظائف مُفيدةً كثيرة. بإمكاننا إرسال طلب باستخدام ‎$.ajax()‎ ثمّ حفظ كائن jqXHR في مُتغيّر: var req = $.ajax({ url: '/data/people.json', dataType: 'json' });بإمكاننا استخدام هذا العنصر لربط الاستدعاءات الرّاجعة بالطّلب، حتّى بعد أن يكتمل الطّلب. بإمكاننا مثلًا استخدام الوظيفة ‎.then()‎ (ثُمَّ) لإرفاق استدعاءي نجاح الطّلب وفشله، إذ تقبل ‎.then()‎ دالّة أو اثنتين، تستدعى الأولى عند نجاح الطّلب، والثّانية إن فشل: var success = function( resp ) { $( '#target' ).append( '<p>people: ' + resp.people.length + '</p>' ); console.log( resp.people ); }; var err = function( req, status, err ) { $( '#target' ).append( '<p>something went wrong</p>' ); }; req.then( success, err ); req.then(function() { $( '#target' ).append( '<p>it worked</p>' ); });بإمكاننا استدعاء ‎.then()‎ على كائن الطّلب قدر ما نشاء، وستُنفّذ الاستدعاءات الرّاجعة بالتّرتيب ذاته الّتي أرفقت وفقه. إن لم نُرد إرفاق استدعاءي النّجاح والفشل معًا، فبإمكاننا استخدام الوظيفتين ‎.done()‎ و‎.fail()‎ على كائن الطّلب: req.done( success ); req.fail( err );لو أردنا إرفاق استدعاء راجعٍ يُنفَّذ دومًا، بغض النّظر عن نجاح الطّلب أو فشله، فيمكننا استخدام الوظيفة ‎.always()‎ على كائن الطّلب: req.always(function() { $( '#target' ) .append( '<p>one way or another, it is done now</p>' ); });‏JSONPيستغرب كثيرٌ من المبتدئين في JavaScript فشل طلبات XHR الّتي يرسلونها إلى نطاق آخر على الإنترنت، فمثلاً: يحاول بعض المُطوّرين جلب بيانات من واجهة برمجيّة من طرف ثالث (third-party API)، ليفاجؤوا بفشل الطّلب باستمرار. السّبب وراء ذلك أنّ المُتصفّحات لا تسمح بإرسال طلبات XHR إلى نطاقات إنترنت أخرى لأسباب أمنيّة، ولكنّ بعض الواجهات البرمجيّة تُعيد البيانات بصيغة JSONP (اختصارًا لـJSON with Padding)، الّتي تسمح للمُطوّرين بجلب البيانات متجاوزين حظر المُتصفّح. الحقيقة أن JSONP ليس طلب AJAX فعليًّا، فهو لا يستخدم طلب XHR الّذي يوفّره المُتصفّح، بل يعمل بإدراج وسم <script> في صفحة الويب، الّذي يحوي بدوره البيانات المطلوبة، مُحاطة بدالّة تُعيد هذه البيانات عند استدعائها. ليس هذه التّفاصيل مهمّة الآن، لأنّ jQuery تسمح لك بطلب JSONP كما لو كان XHR باستخدام الوظيفة ‎$.ajax()‎ بتعيين نوع البيانات dataType إلى 'jsonp' في كائن الإعدادات. $.ajax({ url: '/data/search.jsonp', data: { q: 'a' }, dataType: 'jsonp', success: function( resp ) { $( '#target' ).html( 'Results: ' + resp.results.length ); } });ملاحظة: عادةً ما توفّر الواجهات البرمجيّة خيارًا لتعيين اسم الدّالّة الّتي تُحيط بالبيانات والّتي ستُستدعى في عنوان الرّابط. عادةً ما يكون هذا اسم مُعامل الرّابط callback، وهذا ما تتوقّعه jQuery مبدئيًّا، إلّا أن بإمكانك تغييره بتعيين قيمة للخاصة jsonp في كائن الإعدادات الّذي تُمرّره لـ ‎$.ajax()‎. بإمكانك أيضًا استخدام الوظيفة المُختصرة ‎$.getJSON()‎ لإرسال طلب JSONP، حيث تستطيع jQuery تمييزه من خلال وجود ‎callback=?‎ أو ما يشبهها في الرّابط: $.getJSON( '/data/search.jsonp?q=a&callback=?', function( resp ) { $( '#target' ).html( 'Results: ' + resp.results.length ); } );مشاركة الموارد عبر الأصول (cross-origin resource sharing أو CORS اختصارًا) هي خيارٌ آخر للسّماح بالطّلبات العابرة للأصول. ولكنّها غير مدعومة في المتصفّحات القديمة، كما أنّها تحتاج تهيئة خاصّة على الخادوم وتعديل ترويسات الطّلبات في XHR لتعمل. الكائنات المُؤجّلة (Deferreds)ليست كائنات jqXHR الّتي تعرّفنا عليها إلا "نكهة" خاصّة ممّا يُعرف "بالكائنات المؤجّلة". تسمح jQuery لك بإنشاء كائنات مؤجّلة بنفسك، والّتي يمكن الاستفادة منها في تسهيل التّعامل مع الأوامر اللامتزامنة، فهي توفّر طريقة للاستجابة لعمليّة تجري بصورة غير متزامنة بعد نجاحها أو فشلها، وتجنّبك الحاجة لكتابة استدعاءات راجعة مُتداخلة فيما بينها. ‏‎$.Deferred‎بإمكانك إنشاء كائنٍ مؤجّل باستخدام ‏‎$.Deferred()‎. في المثال التّالي نُنفّذ دالة داخل setTimeout، ثمّ "نفي" (resolve) بوعدنا بإعادة القيمة الّتي تُرجعها الدّالة هذه. نُعيد الوعد (promise)، وهو كائن يمكن ربط الاستدعاءات الرّاجعة به، ولكنّه لا يؤثّر في نتيجة الكائن المؤجّل بحدّ ذاته. بإمكاننا "الإخلاف" (reject) بالوعد إذا وقع خطأ ما أثناء عمل الدّالّة: function doSomethingLater( fn, time ) { var dfd = $.Deferred(); setTimeout(function() { dfd.resolve( fn() ); }, time || 0); return dfd.promise(); } var promise = doSomethingLater(function() { console.log( 'This function will be called in 100ms' ); }, 100);‏‎.then()‎ و‎.done()‎ و‎.fail()‎ و‎.always()‎يمكننا ربط دوالّ تتولّى حالات الخطأ والنّجاح بالوعود، تمامًا كما في كائنات jqXHR: function doSomethingLater( fn, time ) { var dfd = $.Deferred(); setTimeout(function() { dfd.resolve( fn() ); }, time || 0); return dfd.promise(); } var success = function( resp ) { $( '#target' ).html( 'it worked' ); }; var err = function( req, status, err ) { $( '#target' ).html( 'it failed' ); }; var dfd = doSomethingLater(function() { /* ... */ }, 100); dfd.then( success, err );‏‎.pipe()‎بإمكاننا استخدام الوظيفة ‏‎.pipe()‎ للوعود للاستجابة إلى القيمة الّتي تُوفى وذلك بتعديلها ثمّ إعادة كائن مؤجّل جديد. تعمل الوظيفة ‏‎.then()‎ بدءًا من الإصدارة 1.8 من jQuery كما تعمل الوظيفة ‏‎.pipe()‎. function doSomethingLater( fn, time ) { var dfd = $.Deferred(); setTimeout(function() { dfd.resolve( fn() ); }, time || 0); return dfd.promise(); } var dfd = doSomethingLater(function() { return 1; }, 100); dfd .pipe(function(resp) { return resp + ' ' + resp; }) .done(function(upperCaseResp) { $( '#target' ).html( upperCaseResp ); });التّعامل مع العمليّات الّتي قد تكون لامتزامنةأحيانًا تكون لدينا وظيفة قد تعمل بصورة متزامنة أو لا متزامنة وفق ظروف مُعيّنة، فمثلًا: دالّة تقوم بعمليّة لا متزامنة أوّل مرّة تُستدعى فيها، ثمّ تُخزّن القيمة الّتي أنتجتها العمليّة لتُعيدها مباشرةً عند استدعاءها مُستقبلًا. في هذه الحالة يمكننا الاستفادة من ‎$.when()‎ للاستجابة لكلا الحالتين: function maybeAsync( num ) { var dfd = $.Deferred(); // أعِد وعدًا مؤجّلًا عندما num === 1 if ( num === 1 ) { setTimeout(function() { dfd.resolve( num ); }, 100); return dfd.promise(); } // أنهِ مباشرة فيما سوى ذلك، مُعيدًا num return num; } // هذا سيُجرى بصورة غير متزامنة ويعِد بإعادة 1 $.when( maybeAsync( 1 ) ).then(function( resp ) { $( '#target' ).append( '<p>' + resp + '</p>' ); }); // هذا سُيعيد 0 مُباشرةً $.when( maybeAsync( 0 ) ).then(function( resp ) { $( '#target' ).append( '<p>' + resp + '</p>' ); });بإمكانك أيضًا تمرير أكثر من معامل إلى ‎$.when()‎، الأمر الّذي يسمح لك بدمج عمليّات متزامنة ولا متزامنة معًا ثمّ الحصول على نتائج تنفيذها كلّها كُمعاملات للاستدعاء الرّاجع: function maybeAsync( num ) { var dfd = $.Deferred(); // أعد وعدًا مؤجّلًا عندما num === 1 if ( num === 1 ) { setTimeout(function() { dfd.resolve( num ); }, 100); return dfd.promise(); } // أنهِ مباشرةً فيما سوى ذلك، مُعيدًا num return num; } $.when( maybeAsync( 0 ), maybeAsync( 1 ) ) .then(function( resp1, resp2 ) { var target = $( '#target' ); target.append( '<p>' + resp1 + '</p>' ); target.append( '<p>' + resp2 + '</p>' ); });عندما يكون إحدى مُعاملات ‎$.when()‎ كائن jqXHR، فإنّنا نحصل على مصفوفة من المُعاملات تُمرّر إلى استدعائنا الرّاجع: function maybeAsync( num ) { var dfd = $.Deferred(); // أعد وعدًا مؤجّلًا عندما num === 1 if ( num === 1 ) { setTimeout(function() { dfd.resolve( num ); }, 100); return dfd.promise(); } // أنهِ مباشرةً فيما سوى ذلك، مُعيدًا num return num; } $.when( maybeAsync( 0 ), $.get( '/data/people.json' ) ) .then(function( resp1, resp2 ) { console.log( "Both operations are done", resp1, resp2 ); });مصادر إضافيةتوثيق AJAXكائن jqXHRالكائنات المؤجّلة في jQueryترجمة (بشيء من التصرف) للجزء السادس من سلسلة  jQuery Fundamentals لمؤلّفتها Rebecca Murphey.
  12. code { font-size: 1rem !important; }تجعل jQuery إضافة التأثيرات الحركيّة على الصّفحة أمرًا سهلًا للغاية، ويمكن لهذه التأثيرات أن تعتمد الإعدادات المبدئيّة أو إعدادات يُعيّنها المُطوّر. بإمكانك أيضًا إنشاء حركاتٍ مُخصّصة من خصائص CSS عشوائيّة. اطّلع على وثائق التأثيرات لتفاصيل أكثر عن تأثيرات jQuery. ملاحظة مهمّة عن الحركات: يكون إنجاز الحركات باستخدام CSS بدل JavaScript أكثر كفاءةً في المُتصفّحات الحديثة، وخصوصًا في الأجهزة المحمولة. تفاصيل إنجاز هذه الحركات خارجةٌ عن نطاق السّلسلة، ولكن إن كنت تستهدف المُتصفّحات والأجهزة المحمولة الّتي تدعم حركات CSS، فقد ترغب بتعيين الإعداد jQuery.fx.off إلى القيمة true على الأجهزة ذات المواصفات الضّعيفة؛ فهذا من شأنه إبطال الحركات والوصول بالعنصر المطلوب تحريكه إلى حالته النّهائية مباشرةً دون تطبيق الحركة. التأثيرات المُرفقة مع jQueryتُرفَق الحركات المُستخدم بكثرة مع jQuery كوظائف يمكنك استدعاؤها على أي كائن jQuery: ‏‎.show()‎: أظهر العناصر المُحدّدة.‏‎.hide()‎: أخفِ العناصر المُحدّدة.‏‎.fadeIn()‎: حرّك ظلاليّة العناصر (opacity) المُحدّدة إلى 100%.‏‎.fadeOut()‎: حرّك ظلاليّة العناصر المُحدّدة إلى 0%.‏‎.slideDown()‎: أظهر العناصر المُحدّدة بحركة سحب شاقوليّة.‏‎.slideUp()‎: أخفِ العناصر المُحدّدة بحركة سحب شاقوليّة.‏‎.slideToggle()‎: أخفِ العناصر المُحدّدة أو أظهرها بحركة سحبٍ شاقوليّة، اعتمادًا على كون العناصر المُحدّدة مخفيّة أو ظاهرة.يسهل تطبيق إحدى هذه التأثيرات على التّحديد بعد إنشائه: $( '.hidden' ).show();جرّب المثال في ساحة التّجربة (تأكد من ضغط الزرّ Run with JS في هذ المثال وكلّ الأمثلة التّالية) بإمكانك أيضًا تحديدُ مدّة للتأثيرات السّابقة، وهناك طريقتان لتحديدها، الأولى: تعيين الوقت بالميللي ثانيّة: $( '.hidden' ).show( 300 );جرّب المثال في ساحة التّجربة والثّانية استخدام إحدى السُرعات المُعرّفة مُسبقًا: $( '.hidden' ).show( 'slow' );جرّب المثال في ساحة التّجربة عُرِّفت هذه السُرعات في الكائن jQuert.fx.speeds؛ ممّا يعني أنّ بإمكانك تعديله لتغيير القيم المبدئيّة، أو إضافة سُرعات جديدة إليه: // أعد تعيين سرعةٍ مُعرّفة jQuery.fx.speeds.fast = 50; // عرّف سرعة جديدة jQuery.fx.speeds.turtle = 3000; // بما أنّنا غيّر قيمة السّرعة `fast`، فإنّ هذه الحركة ستستغرق 50 ميللي ثانية $( '.hidden' ).hide( 'fast' ); // بإمكاننا استخدام السّرعات الّتي عرفناها بأنفسنا تمامًا كتلك المُعرّفة مسبقًا $( '.other-hidden' ).show( 'turtle' );كثيرًا ما يرغب المُطوّر بفعل شيءٍ ما بعد انتهاء الحركة مباشرةً، فإن حاول فعله قبل انتهاء الحركة، فقد يسبّب تشوّه الحركة وتقطّعها، أو قد يحذف سهوًا عناصر تتحرّك في لحظة حركتها. بإمكانك تمرير استدعاء راجع (callback) إلى وظائف الحركة إن رغبت بتنفيذ أمرٍ ما بعد انتهاء التأثير، وتُشير this داخل هذا الاستدعاء إلى عنصر DOM الخام الّذي طُبقّت عليه الحركة، ومثلها ومثل دوالّ تولّي الأحداث، يمكن إحاطة this بالوظيفة ‎$()‎ لاستخدامها ككائن jQuery: $( 'p.old' ).fadeOut( 300, function() { $( this ).remove(); });جرّب المثال في ساحة التّجربة إن لم يحوِ التّحديد أيّة عناصر، فلن تُستدعى الدّالة. إن احتجت إلى استدعاء الدّالة بصرف النّظر عن وجود العناصر أو غيابها في التّحديد، بإمكانك إنشاء دالّة تتعامل مع الحالتين: var oldElements = $( 'p.old' ); var thingToAnimate = $( '#nonexistent' ); // هذه الدّالة ستكون الاستدعاء الرّاجع للوظيفة `show` في حال وجود عناصر نريد إظهارها، فإن لم توجد أيّة عناصر، فإنّنا نستدعيها مباشرةً بأنفسنا. var removeOldElements = function() { oldElements.remove(); }; if ( thingToAnimate.length ) { // ستُستدعى وظيفتنا بعد انتهاء الحركة thingToAnimate.show( 'slow', removeOldElements ); } else { removeOldElements(); }جرّب المثال في ساحة التّجربة تأثيرات مُخصّصة باستخدام ‎.animate()‎إن لم تُلبِّ الحركات المُرفقة مع jQuery حاجتك، فبإمكانك استخدام الوظيفة ‎.animate()‎ لإنشاء حركات مخصّصة قائمة على خصائص CSS مُتعدّدة (إحدى الاستثناءات: الخاصّ' color الّتي لا يمكن تحريكها، ولكن تتوفّر إضافة تسمح بذلك). تقبل الوظيفة ‎.animate()‎ ثلاثة مُعاملات على الأكثر: كائن يُحدّد الخصائص الّتي يُراد تحريكهامدّة الحركة، مُقدّرة بالميللي ثانيةدالّة تُستدعى عند انتهاء الحركةيمكن أن تُعيّن قيمة الحركة بكتابة القيمة النّهائيّة المُراد التّحريك إليها، أو كتابة المقدار الّذي يجب تحريكه (الفرق بين موضعي الحركة): $( '.funtimes' ).animate({ left: '+=50', // زد بمقدار 50 opacity: 0.25, fontSize: '12px' }, 300, function() { // تنفّذ عند انتهاء الحركة } );جرّب المثال في ساحة التّجربة ملاحظة: إن أردت تحريك خاصّة CSS يحوي اسمها على الإشارة "-"، فعليك تحويل الاسم إلى صيغة camelCase أوّلًا إن لم تشأ إحاطة اسم الخاصّة بعلامات اقتباس، فمثلًا الخاصّة font-size تُصبح fontSize. إدارة الحركاتتُوفّر jQuery وظيفتين مُهمّتين لإدارة الحركات: ‏‎.stop()‎: تُوقف الحركات الجارية على العناصر المُحدّدة.‏‎.delay()‎: تُؤخِّر بدء الحركة القادمة بالمقدار الذي يُمرّر إليها (بالميللي ثانية).تُوفّر jQuery أيضًا وظائف لإدارة تعاقب الحركات وتنظيمها في "طوابير"، وإنشاء طوابير مُخصّة، وإضافة دوالّ مُخصّصة إلى هذه الطّوابير. مناقشة هذه الوظائف موضوع أكبر من هذه السّلسلة، ولكن قد ترغب بالاطّلاع عليها في وثائق jQuery. ترجمة (بشيء من التصرف) للجزء الخامس من سلسلة  jQuery Fundamentals لمؤلّفتها Rebecca Murphey.
  13. تُسهّل jQuery الاستجابة لتفاعل المُستخدم مع صفحات الويب. معنى هذا أنّ بإمكانك تنفيذ أمرٍ ما عندما ينقر المُستخدم على جزءٍ مُعيّن من الصّفحة، أو عندما يُحرّك مؤشّر الفأرة فوق عنصر في نموذج مثلاً. في المثال التّالي، لدينا أمر يُنفَّذ عندما ينقر المُستخدم فوق أيّ عنصر قائمة في الصّفحة: $( 'li' ).click(function( event ) { console.log( 'clicked', $( this ).text() ); });جرّب المثال في ساحة التّجربة  (تأكد من ضغط الزرّ Run with JS في هذ المثال وكلّ الأمثلة التّالية) يُحدّد النّصّ السّابق كلّ عناصر القائمة ويُسند إليها دالّة تتولّى حدث النّقر على كلّ عنصر، باستخدام وظيفة ‎.click()‎ في jQuery. توفّر jQuery وظائف مُختصرةً عديدةً لربط الأحداث، وكلّ من هذه الوظائف يوافق حدث DOM أصليًّا: td {direction:ltr; }اسم الحدث الأصليّ                    الوظيفة المُختصرة‏click‎.click()‏keydown.keydown()‏keypress.keypress()‏keyup.keyup()‏mouseover.mouseover()‏mouseout.mouseout()‏mouseenter.mouseenter()‏mouseleave.mouseleave()‏scroll.scroll()‏focus.focus()‏blur.blur()‏resize.resize()تستخدم هذه الوظائف المُختصرة الوظيفة ‎.on()‎ ما وراء الكواليس، وهي وظيفة يمكنك استخدامها بنفسك لمرونةٍ أكبر. وعند استخدامك لها، فإنّك تُمرّر اسم الحدث الأصليّ كمُعامل أوّل للوظيفة، ثمّ دالّة تتولى الحدث كمعامل ثانٍ: $( 'li' ).on( 'click', function( event ) { console.log( 'clicked', $( this ).text() ); });جرّب المثال في ساحة التّجربة ما إن "تربط" مُتولّي الحدث بعنصرٍ من العناصر، فبإمكانك إثارة هذا الحدث بـjQuery أيضًا: $( 'li' ).trigger( 'click' );وإن كان للحدث الّذي تُريد إثارته وظيفةٌ مختصرة (كما ورد في الجدول السّابق)، فبإمكانك إثارة الحدث باستدعاء الوظيفة المختصرة ذاتها: $( 'li' ).click();ملاحظة: عندما تُثير حدثًا باستخدام ‎.trigger()‎، فإنّك تستدعي مُتولّيات الأحداث الّتي أُنشئت في JavaScript فقط ولا تستدعي السّلوك الافتراضيّ للحدث. فمثلًا إن أثرت حدث النّقر على رابط (عنصر <a>) فلن ينتقل المتصفّح إلى الرّابط المُسند إليه في صفة href (مع أنّ بإمكانك كتابة أوامر تُنفّذ هذه الغاية). بعد أن تربط حدثًا بُعنصر، بإمكانك فكّ هذا الارتباط باستخدام الوظيفة ‎.off()‎ الّتي تُزيل أية مُتولّيات ارتبطت بهذا الحدث: $( 'li' ).off( 'click' );حصر الأحداث ضمن فضاء أسماءمن المزايا الّتي تُتيحها ‎.on()‎ إمكانيّة حصر الأحداث ضمن "فضاء أسماء". قد تتساءل عن الحاجة لذلك. افترض مثلًا أنّك تريد ربط بعض الأحداث بعنصر ما، ثمّ إزالة بعض المُتولّيات، يمكنك أن تفعل ما فعلناه في الفقرة الماضية: تحذير: أسلوب برمجيّ غير مُفضّل $( 'li' ).on( 'click', function() { console.log( 'a list item was clicked' ); }); $( 'li' ).on( 'click', function() { registerClick(); doSomethingElse(); }); $( 'li' ).off( 'click' );إلّا أنّ هذا الأسلوب سيُزيل كلّ مُتولّيات النقر على كلّ عناصر القوائم، وليس هذا ما نريد. إذا ربطت متولّيًا للأحداث محصورًا ضمن فضاء أسماء، فبإمكانك استهدافه بدقّة: $( 'li' ).on( 'click.logging', function() { console.log( 'a list item was clicked' ); }); $( 'li' ).on( 'click.analytics', function() { registerClick(); doSomethingElse(); }); $( 'li' ).off( 'click.logging' );هذا الأسلوب لا يؤثّر على الأحداث المرتبطة بالنّقر والمُتعلّقة بأغراض إحصاءات الاستخدام في الصّفحة، بينما يزيل أحداث النّقر المُتعلّقة بالسّجلّات. بإمكاننا استخدام ميزة حصر الأحداث لإثارة أحداثٍ مُحدّدة: $( 'li' ).trigger( 'click.logging' );ربط أحداث مُتعدّدة في وقت واحدميزة أخرى تُوفّرها ‎.on()‎، وهي إمكانيّة ربط أحداث مُتعدّدة في وقتٍ واحد. افترض مثلًا أنّك تريد تنفيذ أمرٍ مُعيّن عندما يُمرّر المستخدم الصّفحة أو يغيّر قياس النّافذة. فهذه الوظيفة تُتيح لك تمرير الحدثين معًا مفصولين بمسافة في سلسلة نصيّة، يتبعهما الدّالّة الّتي تريد أن تتولّى الحدثين: $( 'input[type="text"]' ).on('focus blur', function() { console.log( 'The user focused or blurred the input' ); }); $( window ).on( 'resize.foo scroll.bar', function() { console.log( 'The window has been resized or scrolled!' ); });جرّب المثال في ساحة التّجربة تمرير دوالّ مُسمّاة كُمتولّيات الأحداثفي كلّ أمثلتنا السّابقة، كنّا نُمرّر دوالّ مجهولة كمُتولّيات للأحداث، ولكن يمكننا إنشاء دالّة قبل إمرارها وحفظها في مُتغيّر ثمّ تمرير هذا المُتغيّر كمُتولّي الحدث. هذا يُفيد في حال أردت استخدام الدّالة نفسها لتتولّى أحداثًا مُختلفة أو أحداثًا من عناصر مُختلفة: var handleClick = function() { console.log( 'something was clicked' ); }; $( 'li' ).on( 'click', handleClick ); $( 'h1' ).on( 'click', handleClick );كائن الحدثفي كلّ مرّة يُثار فيها حدثٌ ما، تستقبل الدّالّة المُتولّية للحدث مُعاملًا واحدًا، وهو كائن الحدث الّذي يتبع معايير مُتّفقًا عليها بين كلّ المُتصفّحات. ولهذا الكائن خصائص مُفيدة كثيرة، منها: $( document ).on( 'click', function( event ) { console.log( event.type ); // نوع الحدث، مثل: "click" console.log( event.which ); // الزرّ أو المفتاح الّذي ضغط console.log( event.target ); // العنصر الّذي انطلق منه الحدث console.log( event.pageX ); // موقع مؤشّر الفأرة على المحور X console.log( event.pageY ); // موقع مؤشّر الفأرة على المحور Y });جرّب المثال في ساحة التّجربة داخل مُتولّي الأحداثعندما تُحدِّد الدّالة المُتولّية لحدث ما، فإنّه يُتاح لهذه الدّالّة وصول إلى عنصر DOM الخام الّذي أطلق الحدث كسياق الدّالّة this، فإن أردت استخدام jQuery للتّعامل مع الحدث، فأحطه بـ‎$()‎: $( 'input' ).on( 'keydown', function( event ) { // this: العنصر الخام الّذي أطلق الحدث // event: كائن الحدث // غير لون الخلفية إلى أحمر إذا ضغط زر مسح الحرف، أو إلى أخضر فيما سوى ذلك $( this ).css( 'background', event.which === 8 ? 'red' : 'green' ); });جرّب المثال في ساحة التّجربة منع السّلوك المبدئيّقد ترغب أحيانًا في منع السّلوك المبدئيّ لحدثٍ ما، كأن ترغب في تولّي النّقر فوق رابط باستخدام AJAX، بدلًا من بدء إعادة تحميل كاملةٍ للصّفحة (وهو السّلوك المبدئيّ). يصل العديد من المُطوّرين إلى هذه الغاية بإعادة false من مُتولّي الحدث، ولكنّ لهذا تأثيرًا جانبيًّا آخر: فهو يمنع تفشّي الحدث (propagation) أيضًا (سنشرحه بعد قليل)، ممّا قد يعطي نتائج غير مرغوبة. الطّريقة السّليمة لمنع السّلوك المبدئيّ لحدث تكون باستدعاء ‎.preventDefault()‎ على كائن الحدث: $( 'input' ).on( 'keydown', function( event ) { // this: العنصر الخام الّذي أطلق الحدث // event: كائن الحدث // غير لون الخلفية إلى أحمر إذا ضغط زر مسح الحرف، أو إلى أخضر فيما سوى ذلك $( this ).css( 'background', event.which === 8 ? 'red' : 'green' ); });جرّب المثال في ساحة التّجربة صعود الأحداث (Event bubbling)تمعّن النّص البرمجيّ التّالي: $( 'a' ).on( 'click', function( event ) { // امنع الحدث المبدئيّ. event.preventDefault(); // سجّل ما حدث. console.log( 'I was just clicked!' ); });جرّب المثال في ساحة التّجربة يربط هذا النّص مُتولّيًا للنقر على كلّ العناصر في الصّفحة (وهو أمر يجب ألّا تفعله نهائيًّا في المواقع الحقيقيّة)، بالإضافة إلى عنصري النّافذة والمُستند. ولكن ما الّذي يحدث عندما تنقر على عنصر <a> مُدرج داخل عناصر أخرى؟ الحقيقة أنّ الحدث سيُثار على العنصر <a> وعلى كلّ العناصر الّتي تُحيط به صعودًا حتّى الوصول إلى العنصرين document وwindow. يُسمّى هذا السّلوك "صعود الأحداث"، فالحدث يُثار على العنصر الّذي نقر عليه المُستخدم، ثمّ ينتقل صاعدًا إلى كلّ العناصر الّتي تحويه وصولًا إلى أعلى DOM، إلّا إن استدعيت الوظيفة ‎.stopPropagation()‎ على كائن الحدث. بإمكانك فهم ذلك بسهولة أكبر في هذا المثال: <a href="#foo"><span>I am a Link!</span></a> <a href="#bar"><b><i>I am another Link!</i></b></a>$( 'a' ).on( 'click', function( event ) { event.preventDefault(); console.log( $( this ).attr( 'href' ) ); });عندما تنقر على "I am a Link!‎"، فإنك لا تنقر فعليًّا على العنصر <a>، بل على العنصر <span> داخله، ولكن الحدث "يصعد" نحو العنصر <a> ويُثير حدث النّقر المُرتبط به. تفويض الأحداث (Event delegation)يسمح لنا مفهوم "صعود الأحداث" بإنجاز فكرة "تفويض الأحداث"، أي ربط الأحداث بعناصر في مستوى أعلى، ثمّ اكتشاف أيّ عنصر فرعيّ ضمنها أثار الحدث. بإمكاننا مثلًا ربط حدث بقائمة غير مُرتّبة، ثمّ تحديد أيّ العناصر أثار الحدث: $( '#my-unordered-list' ).on( 'click', function( event ) { console.log( event.target ); // يُسجّل العنصر الّذي أثار الحدث });جرّب المثال في ساحة التّجربة بالطّبع ستتعقّد الأمور إذا احتوت عناصر القائمة على عناصر فرعيّة ضمنها، ولهذا تُقدّم jQuery وظيفةً مُساعدة تسمح لنا بتحديد أي العناصر نهتمّ بها، مع الاحتفاظ بالحدث مُرتبطًا بالعنصر ذي المُستوى الأعلى: $( '#my-unordered-list' ).on( 'click', 'li', function( event ) { console.log( this ); // يُسجّل عنصر القائمة الّذي أثار الحدث });جرّب المثال في ساحة التّجربة لتفويض الأحداث فائدتان اثنتان: أولاهما أنّه يسمح بربط عددٍ أقلّ من مُتولّيات الأحداث مقارنة بالعدد الّذي نحتاجه لو قرّرنا ربط الأحداث بالعناصر المُنفردة، وهذا يُحسّن الأداء في الصّفحة بصورة كبيرة. وثاني الفائدتين أنّه يسمح لنا بربط الأحداث بالآباء (كالقائمة غير المرتّبة في مثالنا)، مع اطمئنانا إلى أنّ الأحدث ستُثار حتّى وإن تغيّرت مُحتويات العنصر الأب. هذا النّصّ مثلًا، يُضيف عنصر قائمةٍ جديدًا بعد تفويض الحدث إلى العنصر الأب، والنّقر فوق هذا العنصر سيُثير الحدث كما ينبغي، دون الحاجة لربط أيّة أحداث جديدة: $( '#my-unordered-list' ).on( 'click', 'li', function( event ) { console.log( this ); // يُسجّل عنصر القائمة الّذي نُقر عليه }); $( '<li>a new list item!</li>' ).appendTo( '#my-unordered-list' );جرّب المثال في ساحة التّجربة خاتمةتعلّمنا في هذا الجزء الوسائل المُختلفة للإنصات إلى تفاعل المُستخدم مع صفحتنا، بما في ذلك كيفيّة الاستفادة من التفويض لتحسين كفاءة ربط الأحداث بالعناصر. سنتعرّف الجزء القادم كيف نُحرّك العناصر باستخدام وظائف التأثيرات الحركيّة في jQuery. ترجمة (بشيء من التصرف) للجزء الرابع من سلسلة  jQuery Fundamentals لمؤلّفتها Rebecca Murphey.
  14. تُسهِّل مكتبة jQuery التّعامل مع محتويات صفحة HTML بعد أن يعرضها المتصفّح، وتوفّر أدوات تُساعدك في متابعة تفاعل المستخدم مع الصّفحة، وتحريك العناصر فيها، والتّواصل مع الخواديم دون إعادة تحميل الصّفحة. سنشرح هذه الأدوات بعد قليل. لنبدأ أوّلًا بالاطّلاع على أساسيّات jQuery، وكيف يمكن استخدام وظيفتها الأساسيّة: الوصول إلى عناصر مُحدَّدة في الصفحة وفعل شيءٍ ما بها. ملاحظة: يفترض هذا الدّليل أنّك على علم بأساسيّات HTML ومُحدِّدات CSS. إن لم تكن تألف كيف يمكن استخدام مُحدّدات CSS للوصول إلى عناصر مُحدّدة في الصّفحة، فعليك أوّلًا تعلّم ذلك قبل الشروع في متابعة هذا الدّليل. ما هذا الرمز: $؟توفّر مكتبة jQuery الدّالّة jQuery، التي تتيح لك تحديد العناصر بمُحدّدات CSS. var listItems = jQuery( 'li' );إن قرأت من قبل برامج تستخدم jQuery، فلا بدّ أنّك اعتدت على هذا: var listItems = $( 'li' );كما ناقشنا في الجزء السّابق (أساسيّات JavaScript)، فكلّ الأسماء تكاد تكون سّليمة في JavaScript ما لم تبدأ برقم أو تحوي إشارة "-". ولذا فالاسم $ في المثال الأخير ليس إلّا اسمًا مُختصرًا للدّالّة jQuery، ولو اطّلعت على مصدر jQuery، لقرأت هذا قرب نهايته: // Expose jQuery to the global object window.jQuery = window.$ = jQuery;عندما تستدعي الدّالّة ‎$()‎ وتمرّر لها مُحدّدًا، فإنّك تحصل على كائن jQuery جديدٍ. الدّوالّ في JavaScript هي الأخرى كائنات، وهذا يعني أنّ للدّالّة $ (وjQuery بالطّبع) خصائص ووظائف أيضًا. مثلاً: يمكنك استخدام الخاصّة ‎$.support‎ لمعرفة ما الميزات الّتي يدعمها المتصفّح الحاليّ، كما يمكنك استخدام الوظيفة ‎$.ajax‎ لإرسال طلب AJAX. ملاحظة: من الآن فصاعدًا سنستخدم $ بدلًا من jQuery في هذه السّلسلة سعيًا للاختصار. لاحظ أنّه إن احتوت صفحتك أكثر من مكتبة واحدة، فقد يُستخدم الاسم $ من مكتبة أخرى، ممّا يمنع عمل jQuery، فإن واجهتك مشكلة كهذه، جرّب استخدام jQuery.noConflict قبل تحميل المكتبات الأخرى. ‏‎$(document).ready()‎قبل استخدام jQuery لفعل أيّ شيء في الصّفحة، علينا التأكّد من كون الصّفحة قد بلغت حالةً تسمح بتعديل محتوياتها. يمكن تنفيذ ذلك في jQuery بإحاطة برنامجنا ضمن دالّة ثمّ إمرار هذه الدّالة إلى ‎$(document).ready()‎. كما ترى في المثال التّالي، يمكن للدّالّة الّتي نمرّرها أن تكون مجهولة (بلا اسم): $(document).ready(function() { console.log( 'ready!' ); });هذا سيؤدّي إلى استدعاء الدّالّة الّتي مرّرناها إلى ‎.ready()‎ بعد أن يصبح المُستند (الصفحة) جاهزًا. ما الذي يحدث هنا؟ استخدمنا ‎$(document)‎ لإنشاء كائن jQuery من document في الصّفحة، ثمّ استدعينا الدّالّة ‎.ready()‎ على هذا الكائن، مُمرِّرين إليها الدّالّة الّتي نريد تنفيذها. بما أنّك ستجد نفسك تُعيد كتابة هذا النّصّ مرارًا، فإنّ jQuery تقدّم لك طريقةً مُختصرةً لإنجازه، إذ تقوم الدّالّة ‎$()‎ بمهمّة مُختلفة عند إمرار دالّة إليها بدلًا من مُحدِّد CSS، وعندها تتصرّف وكأنّها اسم بديلٌ للوظيفة ‎$(document).ready()‎: $(function() { console.log( 'ready!' ); });ملاحظة: من الآن فصاعدًا، سنفترض أنّ النّصوص الّتي ترد في هذه السّلسلة مُحاطة بالعبارة ‎$(document).ready(function() { ... });‎، وسنترك هذه العبارة بغرض الإيجاز. الوصول إلى العناصرأبسط ما يمكن إنجازه بـjQuery تحديد بعض العناصر ثمّ فعل شيء ما بها. إن كنت تفهم مُحدّدات CSS، فستجد أنّ الوصول إلى بعض العناصر سهل ومباشر: ليس عليك إلا إمرار المُحدِّد المناسب إلى ‎$()‎. $( '#header' ); // حدّد العنصر الّذي مُعرِّفه 'header' $( 'li' ); // حدّد كل عناصر القوائم في الصّفحة $( 'ul li' ); // حدّد كل عناصر القوائم الموجودة في قوائم غير مُرتّبة $( '.person' ); // حدّد كل العناصر ذات الصّنف 'person'من المهمّ أن تفهم أنّ أيّ تحديد تُجري لن يتضمّن إلّا العناصر الموافقة للمُحدّد والتي كانت موجودة في اللّحظة الّتي أجريت فيها التّحديد، بمعنى أنّك إذا كتبت ‎var anchors = $( 'a' );‎ ثمّ أضفت عنصر <a> إلى الصّفحة لاحقًا، فلن تحوي anchors العنصر الجديد. طرق أخرى لإنشاء كائن jQueryبالإضافة إلى إمرار مُحدّد بسيط إلى الدّالة ‎$()‎، يمكن إنشاء كائنات jQuery بطرق أخرى: // أنشئ كائن jQuery من عنصر DOM $( document.body.children[0] ); // أنشئ كائن jQuery من قائمة بعناصر DOM $( [ window, document ] ); // أجرِ التّحديد بسياق عنصر DOM var firstBodyChild = document.body.children[0]; $( 'li', firstBodyChild ); // أجرِ التّحديد ضمن تحديد سابق var paragraph = $( 'p' ); $( 'a', paragraph );هل يحتوي التّحديد الّذي أجريته على أيّة عناصر؟ترغب أحيانًا في تنفيذ بعض الأوامر عندما يُطابق تحديدك عنصرًا أو أكثر فقط، ولكنّ الدّالّة ‎$()‎ تُعيدُ دومًا كائن jQuery، وهذا الكائن دائمًا صائب (truthy)، ولذا عليك فحص محتوى التّحديد لمعرفة إن كان يحوي أيّة عناصر. تحذير: نصّ برمجيّ غير سليم if ( $( '#nonexistent' ) ) { // خطأ! الأوامر هنا ستُنفَّذ دومًا! } if ( $( '#nonexistent' ).length > 0 ) { // صحيح! لن تُنفّذ الأوامر هنا إلّا إن احتوت الصّفحة على كائن مُعرّفه 'nonexistent' }بإمكاننا اختصار هذا الفحص أكثر بالاعتماد على كون 0 قيمة خاطئة (falsy): if ( $( '#nonexistent' ).length ) { // لن تُنفّذ الأوامر هنا إلّا إن وجد عنصر مُطابق }الوصول إلى عناصر مُفردة من تحديدإن كنت تحتاج التّعامل مع عنصر DOM خام من تحديد، فعليك الوصول إلى هذا العنصر من كائن jQuery. لنفترض مثلًا أنّك تريد الوصول إلى الخاصّة value لكائن <input> مباشرةً، عليك إذن التّعامل مع عنصر DOM الخام: var listItems = $( 'li' ); var rawListItem = listItems[0]; // أو listItems.get( 0 ) var html = rawListItem.innerHTML;لاحظ أنّه ليس بإمكانك استدعاء وظائف jQuery على عناصر DOM الخام، فلن يعمل المثال التّالي: تحذير: نصّ برمجيّ غير سليم var listItems = $( 'li' ); var rawListItem = listItems[0]; var html = rawListItem.html(); // Object #<HTMLInputElement> has no method 'html'إن أردت العمل مع عنصر مُفرد في تحديد وأردت استخدام وظائف jQuery معه، فعليك إنشاء كائن jQuery جديد انطلاقًا منه باستخدام الدّالّة ‎.eq()‎، وإمرار دليل العنصر الّذي تريده: var listItems = $( 'li' ); var secondListItem = listItems.eq( 1 ); secondListItem.remove();جرب المثال في ساحة التّجربة (تأكد من ضغط زر Run with JS في هذا المثال وكل الأمثلة التالية) إنشاء كائنات جديدةللدّالّة $ دورٌ ثالث أخير: إنشاء عناصر جديدة. إن مرّرت قصاصة HTML إلى $()‎، فستُنشئ كائنًا جديدًا في الذّاكرة، بمعنى أنّ الكائن يُنشأ ولكن لا يُضاف إلى الصّفحة إلى أن تفعل ذلك بنفسك. $( '<p>' ); // يُنشئ عنصر <p> بلا محتوى $( '<p>Hello!</p>' ); // يُنشى عنصر <p> فيه نصّ $( '<p class="greet">Hello!</p>' ); // يُنشى عنصر <p> فيه نصّ وله صنفبإمكانك أيضًا إنشا عنصر جديد بإمرار كائنٍ يحوي معلومات تصف كيفيّة إنشاء العنصر: $( '<p>', { html: 'Hello!', 'class': 'greet' });لاحظ أنّه يجب أن نُحيط class بعلامتي اقتباس، لأنّ class كلمة محجوزة في JavaScript، وعدم إحاطتها بالعلامتين سيسبّب وقوع أخطاء في بعض المتصفّحات. راجع وثائق jQuery‏ لتفاصيل الخصائص المختلفة الّتي يمكنك تضمينها في هذا الكائن. سنتعرّف كيف نُضيف العناصر في الصّفحة في الجزء القادم من السّلسلة، الّذي يشرح الانتقال عبر الصّفحة وتعديل محتوياتها. التّعامل مع التحديداتبعد إنشائك كائن jQuery يحوي تحديدًا، فإنّك غالبًا ما تريد فعل شيء ما به، وقبل ذلك عليك أن تتعرّف على أسلوب jQuery في التّعامل مع الكائنات. فحص تحديدبإمكاننا معرفة إن كان تحديد ما يُطابق معايير مُعيّنة باستخدام الوظيفة ‎.is()‎. أكثر الطّرق شيوعًا لاستخدام هذه الوظيفة تزويدها بمُحدِّد كمُعاملٍ مفرد لها، وعندها تُعيد true أو false حسب مُطابقة التّحديد للمُحدِّد: $( 'li' ).eq( 0 ).is( '.special' ); // false $( 'li' ).eq( 1 ).is( '.special' ); // trueبإمكانك تمرير كائن jQuery أيضًا إلى الوظيفة ‎.is()‎، أو حتّى كائن DOM خام، أو حتّى دالّة لإجراء اختبار أكثر تعقيدًا إن لزم. راجع الوثائق لمزيد من التّفاصيل. وظائف القراءة والكتابة والسّرد الضّمنيّبعد عمل التّحديد، تتوفّر وظائف عديدة يمكنك استدعاؤها. تقع هذه الوظائف عمومًا في إحدى مجموعتين: وظائف القراءة (getters) ووظائف الكتابة (setters). فالأولى تعطينا معلومات عن التّحديد، والثّانية تُغيّر التّحديد بشكل من الأشكال. وفي معظم الحالات يقتصر عمل وظائف القراءة على العنصر الأول في التّحديد (‎.text()‎ إحدى استثناءات هذه القاعدة)؛ أمّا وظائف الكتابة فتشمل بعملها كلّ العناصر في التّحديد، مستخدمةً ما يُعرف بالسّرد الضّمنيّ (implicit iteration). معنى السّرد الضّمنيّ أنّ jQuery ستمرّ تلقائيًّا على كلّ العناصر في التّحديد عندما تستدعي وظيفة كتابة على هذا التّحديد، أيّ أنّه ليس عليك استدعاء وظيفة على كلّ عنصر في التّحديد بمفرده عندما تريد فعل شيء على كل العناصر في تحديد واحدٍ، بل اكتفِ باستدعاء هذه الوظيفة على التّحديد نفسه، وستفهم jQuery أنّ عليها تنفيذه على كلّ العناصر في التّحديد. لنفترض أنّنا نريد تغيير نصّ HTML في كل عناصر القوائم في الصّفحة، ولفعل ذلك علينا استخدام الوظيفة ‎.html()‎ الّتي تقوم بتغيير نصّ HTML في كلّ عناصر القوائم المُحدّدة. $( 'li' ).html( 'New HTML' );جرب المثال في ساحة التّجربة بإمكانك أيضًا إمرار دالّة إلى وظائف الكتابة في jQuery، وستُستخدم القيمة المُعادة منها باعتبارها القيمة الجديدة، وتستقبل هذه الدّالة مُعاملين اثنين: دليل العنصر (index) في التّحديد، والقيمة القديمة للشّيء الذي تحاول تغييره، وهذا مُفيد في حال احتجت معلومات عن حالة العنصر الحاليّة لتعيين حالته الجديدة بشكل صحيح. $( 'li' ).html(function( index, oldHtml ) { return oldHtml + '!!!' });جرب المثال في ساحة التّجربة السّرد الصّريح (Explicit Iteration)في بعض الأحيان، لن تلبّي وظائف jQuery الأصليّة المهمّة الّتي تريد إنجازها بدقّة، وسيكون عليك حينها المرور على العناصر في التّحديد بشكل صريح، وهذا ما تتيحه الوظيفة ‎.each()‎. في المثال التّالي نستخدمها لإضافة وسم <b> في بداية عنصر القائمة، يحوي دليل العنصر: $( 'li' ).each(function( index, elem ) { // this: عنصر DOM الخام الحالي // index: دليل العنصر الحالي في التّحديد // elem: عنصر DOM الخام الحالي (مثل this) $( elem ).prepend( '<b>' + index + ': </b>' ); });جرب المثال ساحة التّجربة ملاحظة: ستلاحظ أنّ عنصر DOM الخام مُتاح ضمن الدّالّة الّتي نُمرّرها إلى ‎.each()‎ بطريقتين: الأولى عبر this والثّانية عبر elem. وكما ناقشنا في الجزء السّابق (أساسيّات JavaScript)، فإنّ this كلمة خاصّة في JavaScript تُشير إلى الكائن الّذي يُمثّل سياق الدّالّة الحاليّ. وفي jQuery فإنّ this تُشير في معظم الحالات إلى عنصر DOM الخام الّذي تعمل عليه الدّالّة الحاليّة. لذا فإنّها تُشير في حالة ‎.each()‎ إلى العنصر الحاليّ في مجموعة العناصر الّتي نسردها. السَّلسَلة (Chaining)من أكثر الأمور فائدةً في jQuery إمكانيّة "سَلسَلة" الوظائف معًا. هذا يعني أنّ بإمكاننا استدعاء سلسِلة من الوظائف على تحديدٍ ما دون الحاجة لإعادة التّحديد أو حفظه في متغيّر. بإمكاننا حتّى إنشاء تحديدات جديدة بناء على التّحديد السّابق، دون كسر السّلسلة: $( 'li' ) .click(function() { $( this ).addClass( 'clicked' ); }) .find( 'span' ) .attr( 'title', 'Hover over me' );الأمر ممكن لأنّ كل دالّة كتابة (setter) في jQuery تُعيد التّحديد الذي اُستدعيت لتعمل عليه. وهذا أمر عظيم الفائدة، حتّى أنّ مكتبات كثيرة اعتمدته تأثّرًا بـjQuery. ولكن يجب الحذر عند استخدامه. فالسّلاسل الطّويلة تجعل النّصّ البرمجيّة صعب القراءة والتّعديل والتنقيح لا قاعدة واضحة تفرض طولًا مناسبًا للسّلسلة، ولكن حتّى السلاسل القصيرة قد تحتاج إلى إعادة الصّياغة تسهيلًا لقراءتها. var listItems = $( 'li' ); var spans = listItems.find( 'span' ); listItems .click(function() { $( this ).addClass( 'clicked' ); }); spans.attr( 'title', 'Hover over me' );خاتمةلدينا الآن معلومات ممتازة عن تفاصيل عمل jQuery؛ وسنستعرض في الجزء القادم كيف يمكننا تطبيق هذه المعلومات لإنجاز أشياء حقيقيّة بها! مصادر إضافيةوثائق الواجهة البرمجيّة لـjQueryوثائق المُحدّداتترجمة (بشيء من التصرف) للجزء الثاني من سلسلة  jQuery Fundamentals لمؤلّفتها Rebecca Murphey.
  15. بُنيت jQuery على لغة جافاسكريبت، وهي لغةٌ غنيّة وقويّة في حدّ ذاتها. يُغطّي هذا الدّرس أساسيّات لغة جافا سكريبت، وبعض الأخطاء الشّائعة الّتي يرتكبها المبتدئون بها. يُفيد هذا الدّرس القادمين الجدد إلى عالم البرمجة، ويفيد أيضًا المبرمجين بلغات أخرى الّذين لم يسبق لهم الاطّلاع على الجوانب المميّزة لـJavaScript. فيما يلي برنامج JavaScript بسيط يُضيف رسالةً إلى صفحة ويب: // أنشئ دالّة لإلقاء التّحية على شخص // وأسندها إلى المُتغيّر `greet` var greet = function( person, message ) { var greeting = 'Hello, ' + person + '!'; log( greeting + ' ' + message ); }; // استخدم الدالّة لتحيّة Jory، بإمرار اسمها ورسالة التّحيّة greet( 'Jory', 'Welcome to JavaScript' ); // استخدم الدالّة لتحيّة Rebecca، بإمرار اسمها ورسالة مختلفة greet( 'Rebecca', 'Thanks for joining us' );مُلاحظة: في المثال السابق، استخدمنا الدّالّة log. وهي دالّة مُساعِدة متوفّرة في الأمثلة في هذه السّلسلة فقط، وليست متوفّرة تلقائيًّا في JavaScript، يمكن استخدام log في محرّر النّصوص البرمجيّة في هذه السّلسلة، ولكن ستحتاج إلى استخدام console.log محلّها في النّصوص البرمجيّة خارج السّلسلة، وعندها ستُطبع نتائج النّصّ إلى طرفيّة المتصفّح الّذي تستعمله. // create a function that will greet a person, // and assign the function to the `greet` variable var greet = function( person, message ) { var greeting = 'Hello, ' + person + '!'; log( greeting + ' ' + message ); }; // use the function to greet Jory, passing in her // name and the message we want to use greet( 'Jory', 'Welcome to JavaScript' ); // use the function to greet Rebecca, passing in her // name and a different message greet( 'Rebecca', 'Thanks for joining us' );النّتيجة النّتيجة مطالعةلم نخض في أعماق لغة JavaScript بعدُ. شبكة مُطوّري موزيلّا (MDN) مصدر ممتاز (بالإنكليزيّة) لتعلّم JavaScript بتفاصيلها، وخصوصًا دليل JavaScript على الشّبكة. أكثر المواضيع أهمّيّة لك الآن: نظرة عامّة على JavaScriptالقيم والمتغيّرات والمكوّنات الحرفيّةالدّوالّعبارات الدّوالّ المُستدعاة فورًاالمصفوفاتمصادر إضافيةشبكة مُطوّري موزيلّا: JavaScriptChrome Developer Tools OverviewFixing these jQuery: A Guide to DebuggingChrome Developer Tools Cheat SheetChrome Dev Tools: 12 Tricks to Develop Quicker (فيديو)ترجمة (بشيء من التصرف) للجزء الأول من سلسلة  jQuery Fundamentals لمؤلّفتها Rebecca Murphey.
×
×
  • أضف...