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

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

المحتوى عن 'javascript'.

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

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

نوع المحتوى


التصنيفات

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

التصنيفات

  • مقالات برمجة عامة
  • مقالات برمجة متقدمة
  • 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

ابحث في

ابحث عن


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

  • بداية

    نهاية


آخر تحديث

  • بداية

    نهاية


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

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

  • بداية

    نهاية


المجموعة


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

  1. إذا أردنا وصف البرنامج المثالي، فسيكون ذلك الذي له بنية واضحة مثل الكريستال، ويسهل وصف طريقة عمله، كما يملك كل جزء فيه دورًا محدَّدًا ومعروفًا. لكن هذا لا يكون في الواقع، بل تكون لدينا برامج تنمو نموًا عضويًا، بحيث تضاف إليها المزايا كلما دعت الحاجة إلى ذلك؛ أما الهيكلة البنائية لها والحفاظ على تلك الهيكلية فهي أمر آخر، إذ لا تُرى ثمرتها إلا في المستقبل حين يأتي شخص آخر ليعمل على البرنامج، وعلى ذلك فمن المغري للمبرمج إهمالها الآن لتصبح أجزاء البرنامج متشعبةً ومتداخلةً إلى حد فظيع. يتسبب هذا في مشكلتين حقيقيتَين، أولاهما أنّ فهم مثل هذا النظام صعب جدًا، إذ سيكون من العسير النظر لأيّ جزء فيه نظرة مستقِلة إذا كان كل شيء فيه يتصل بشيء آخر، حيث سنُجبَر على دراسة البرنامج كله من أجل فهم جزء واحد فقط؛ أما الثانية فهي إذا أردنا استخدام أيّ وظيفة من مثل هذا البرنامج في حالة أخرى، فستكون إعادة كتابتها من الصفر أسهل من استخراجها مستقِلةً من شيفرتها لتعمل في مكان آخر. يستخدم مصطلح "كرة الوحل" لمثل تلك البرامج التي لا هيكل لها، والتي يلتصق كل شيء فيها ببعضه، فإذا حاولت استخراج جزء، فستنهار الكرة كلها، ولا ينالك إلا تلطيخ يدك. الوحدات Modules تُعَدّ الوحدات محاولةً لتفادي مثل تلك المشاكل التي وصفناها، فالوحدة هي جزء من برنامج ما يحدِّد بوضوح ما هي الأجزاء الأخرى التي يعتمد عليها، وما هي الوظيفة -أي الواجهة الخاصة به- التي سيوفرها للوحدات الأخرى لتستخدمها. تشترك واجهات الوحدات مع واجهات الكائنات في أمور كثيرة كما رأينا في مقال الحياة السرية للكائنات في جافاسكريبت، إذ تجعل جزءًا من الوحدة متوفرًا للعالم الخارجي وتحافظ على خصوصية الباقي، كما يصبح النظام أشبه بقِطع الليجو LEGO في تقييد الطرق التي تتفاعل الوحدات بها مع بعضها البعض، فتتفاعل الأجزاء المختلفة من خلال وصلات connectors معرَّفة جيدًا، على عكس الوحل الذي يختلط فيه كل شيء. تسمى العلاقات التي بين الوحدات بالاعتماديات dependencies، فإذا احتاجت وحدة إلى جزء من وحدة أخرى، فسيقال أنها تعتمد على تلك الوحدة، وحين توصف تلك الحقيقة بوضوح داخل الوحدة نفسها، فيمكن استخدامها لمعرفة الوحدات الأخرى التي يجب أن تكون موجودةً من أجل استخدام وحدة ما، ولتحميل الاعتماديات تلقائيًا، كما ستحتاج كل واحدة منها إلى مجال خاص private scope إذا أردنا فصل الوحدات بهذه الطريقة. لا يكفي وضع شيفرة جافاسكربت في ملفات مختلفة لتلبية تلك المتطلبات، فلا زالت الملفات تتشارك فضاء الاسم العام global namespace نفسه، كما قد تتداخل عن قصد أو غير قصد مع رابطات بعضها bindings، وهكذا تظل هيكلية الاعتماديات غير واضحة، وسنرى بعد قليل كيف نعالج ذلك. قد يكون التفكير في تصميم وحدة ذات هيكل مناسب لبرنامج ما صعبًا بما أننا في مرحلة البحث عن المشكلة واستكشاف حلول لها، وذلك لنرى أيها يعمل، كما لا نريد شغل بالنا بتنظيم أحد الحلول إلا حين يثبت نجاحه. الحزم نستطيع استخدام نفس الجزء في برامج أخرى مختلفة، وهي إحدى المزايا التي نحصل عليها عند بناء برنامج من أجزاء منفصلة تستطيع العمل مستقِلة عن بعضها البعض. لكن كيف نُعِدّ هذا؟ لنقل أننا نريد استخدام الدالة parseINI من المقال السابق في برنامج آخر، فإذا كان ما تعتمد عليه الدالة واضحًا -لا تعتمد الدالة على شيء في هذه الحالة-، فلن نفعل أكثر من نسخ شيفرتها إلى المشروع الجديد واستخدامها؛ أما إذا رأينا خطأً في تلك الشيفرة، فسنصلحه في ذلك المشروع الجديد وننسى إصلاحه في البرنامج الأول الذي أخذنا منه الشيفرة، وبهذا نهدر الوقت والجهد في نسخ الشيفرات من هنا إلى هناك والحفاظ عليها محدَّثة. وهنا يأتي دور الحِزم packages، فهي قطعة من شيفرة يمكن نشرها وتوزيعها -أي نسخها وتثبيتها-، وقد تحتوي على وحدة واحدة أو أكثر، كما فيها معلومات عن الحِزم الأخرى التي تعتمد عليها، وتأتي مع توثيق يشرح وظيفتها كي يستطيع الناس استخدامها، فلا تكون مقتصرةً على من كتبها فقط. تُحدَّث حزمة ما إذا وُجدت مشكلة فيها أو أضيفت إليها ميزة جديدة، وعليه تكون البرامج التي تعتمد عليها -والتي قد تكون حِزمًا أيضًا- يمكنها الترقية إلى تلك النسخة الجديدة. يتطلب العمل بهذه الطريقة بنيةً تحتيةً، وذلك لحاجتنا إلى مكان لتخزين تلك الحِزم والبحث فيها، وإلى طريقة سهلة لتثبيتها وترقيتها كذلك، كما تُوفَّر هذه البنية التحتية في عالم جافاسكربت من قِبَل NPM وهي اختصار لـ Node Package Manager، التي تعني مدير الحِزم. يتكون مدير الحِزم NPM من شيئين، أولهما تقدِّم خدمة تنزيل ورفع الحزم عبر الإنترنت -أي أونلاين online-، وبرنامج -مضمَّن مع Node.js- يساعدك على تثبيت تلك الحزم وإدارتها، كما توجد أكثر من نصف مليون حزمة متاحة على NPM وقت كتابة هذه الكلمات (بنسختها الأجنبية)، وأكثرها لا فائدة منه، لكن الحزم المفيدة المتاحة للعامة موجودة هناك أيضًا. سنجد محلل ملفات INI مثلًا الذي يشبه ما بنيناه في المقال السابق في هيئة حزمة اسمها ini، كما سنرى في مقال لاحق كيف نُثبِّت مثل تلك الحزم محليًا باستخدام أمر npm في الطرفية. إتاحية مثل تلك الحزم عالية الجودة للتحميل مفيد جدًا، فهو يعني استطاعتنا تجنب إعادة اختراع برنامج كتبه مائة إنسان قبلنا، كما نتجاوز ذلك إلى استخدام حزمة مجرَّبة ومختبَرة جيدًا ببضع ضغطات على لوحة المفاتيح، ومن بداهة القول أنّ البرامج لا تكلف شيئًا في نَسخها، لكن كتابة البرامج أول مرة هي العمل الذي يكلف الجهد والوقت والمهارة، كما يماثلها الاستجابة لمن يجد مشاكل في الشيفرة، أو الاستجابة لمن يريد إدخال ميزات جديدة في البرنامج. مَن يكتب البرنامج يمتلك حقوقه افتراضيًا، ولا يستطيع أحد استخدامه إلا برخصة من المبرمج، لكن بما أنّ بعض البشر ذوو قلوب طيبة، ولأنّ نشر البرامج الجيدة سيبني لك سُمعة وسط المبرمجين، فستسمح صراحةً كثير من الحِزم تحت رخصة ما باستخدامها من أيّ كان. ترخَّص أغلب الشيفرات الموجودة في NPM بتلك الطريقة، في حين قد تشترط بعض الرخص نشر الشيفرة التي تبنيها على قمة الحزمة التي استخدمتها تحت رخصة الحزمة نفسها، وأخرى لا تريد أكثر من استخدام رخصة الحزمة نفسها حين تنشر برنامجك، وهي الرخصة التي يستخدمها أغلب مجتمع جافاسكربت، وبالتالي تأكد حين تستخدِم حِزمًا كتبها غيرك من قراءة الرخصة التي تأتي بها الحزمة. الوحدات المرتجلة Improvised modules لم يكن في جافاسكربت نظام وحدات مضمَّن حتى عام 2015، إذ لم يمنع ذلك الناس من بناء نظمًا كبيرة في جافاسكربت طيلة أكثر من عشر سنين، كما كانوا في أمسّ الحاجة إلى وحدات لهذا، وعليه فقد صمموا نظم وحداتهم الخاصة بهم فوق اللغة نفسها، حيث نستطيع استخدام دوال جافاسكربت لإنشاء نطاقات محلية local scopes وكائنات لتمثل واجهات الوحدات. لدينا فيما يلي وحدةً للانتقال بين أسماء الأيام وأرقامها -وذلك من إعادة التابع getDay الخاص بـ Date-، إذ تتكون واجهتها من weekDay.name وweekDay.number، كما تخفي رابطتها names المحلية داخل نطاق تعبير الدالة الذي يُستدعى فورًا. const weekDay = function() { const names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; return { name(number) { return names[number]; }, number(name) { return names.indexOf(name); } }; }(); console.log(weekDay.name(weekDay.number("Sunday"))); // → Sunday يوفِّر هذا النسق من الوحدات عزلًا إلى حد ما، لكنه لا يصرح عن الاعتماديات، كما يضع واجهته في النطاق العام global scope، ويَتوقع اعتمادياتها إذا وُجدت أن تحذو حذوه، في حين كان ذلك هو الأسلوب المتَّبع في برمجة الويب لمدة طويلة، لكنه صار مهجورًا الآن وقلّما يُستخدَم. إذا أردنا جعل علاقات الاعتماديات جزءًا من الشيفرة، فيجب التحكم في تحميل الاعتماديات، ويتطلب ذلك أن نستطيع تنفيذ السلاسل النصية على أساس شيفرات، إذ تستطيع جافاسكربت فعل ذلك لحسن الحظ. تقييم البيانات على أساس شيفرات هناك عدة طرق لأخذ البيانات -أي سلسلة نصية من شيفرات برمجية- وتشغيلها على أساس جزء من البرنامج الحالي، وأبسط طريقة هي العامل الخاص eval الذي سينفِّذ السلسلة النصية في النطاق الحالي current scope، لكن هذه فكرة سيئة لا يُنصح بها، إذ تعطِّل بعض الخصائص التي تكون عادةً للمجالات مثل توقعها للرابطة التي يشير إليها اسم ما بسهولة. const x = 1; function evalAndReturnX(code) { eval(code); return x; } console.log(evalAndReturnX("var x = 2")); // → 2 console.log(x); // → 1 يمكن تفسير البيانات على أساس شيفرة بطريقة أبسط باستخدام الباني Function الذي يأخذ وسيطين هما سلسلة نصية تحتوي قائمة من أسماء الوسائط مفصول بينها بفاصلة أجنبية، وسلسلة نصية تحتوي على الدالة نفسها. يغلف الباني الشيفرة داخل قيمة دالة كي تحصل على نطاقها الخاص، ولا تفعل أمورًا غريبة مع النطاقات الأخرى. let plusOne = Function("n", "return n + 1;"); console.log(plusOne(4)); // → 5 هذا هو المطلوب تحديدًا وما نحتاج إليه في نظام الوحدات، إذ نستطيع تغليف شيفرة الوحدة في دالة، ونستخدم نطاق تلك الدالة على أساس نطاق الوحدة. CommonJS لعل أكثر منظور لوحدات جافاسكربت المضافة إليها هو نظام وحدات جافاسكربت المشتركة common Javascript والذي يدعى بالاسم CommonJS، إذ تستخدِمه Node.js الذي هو النظام المستخدَم في أغلب الحِزم على NPM. تدور الفكرة العامة لوحدات CommonJS حول دالة اسمها require، فحين نستدعيها مع اسم وحدة الاعتمادية، فستضمن أن تحميل الوحدة وستعيد واجهتها. تحصل الوحدات على نطاقها المحلي الخاص بها تلقائيًا لأنّ المحمّل يغلف شيفرة الوحدة في دالة، وما عليها إلا استدعاء require كي تصل إلى اعتمادياتها، وتضع واجهاتها في الكائن المقيَّد بـ exports. توفِّر الوحدة المثال التالية دالةً لتنسيق التاريخ، إذ تستخدِم حزمتين من NPM هما ordinal لتحويل الأعداد إلى سلاسل نصية مثل "1st" و"2nd"، وdate-names للحصول على الأسماء الإنجليزية للشهور وأيام الأسبوع، ثم تصدِّر دالةً وحيدةً هي formatDate تأخذ كائن Date وسلسلة قالب template string. قد تحتوي سلسلة القالب على شيفرات توجه التنسيق مثل YYYY للسنة كاملة وDo لترتيب اليوم في الشهر، ومن الممكن إعطاؤها سلسلةً نصيةً مثل "MMMM Do YYYY" للحصول على خرج مثل "November 22nd 2017". const ordinal = require("ordinal"); const {days, months} = require("date-names"); exports.formatDate = function(date, format) { return format.replace(/YYYY|M(MMM)?|Do?|dddd/g, tag => { if (tag == "YYYY") return date.getFullYear(); if (tag == "M") return date.getMonth(); if (tag == "MMMM") return months[date.getMonth()]; if (tag == "D") return date.getDate(); if (tag == "Do") return ordinal(date.getDate()); if (tag == "dddd") return days[date.getDay()]; }); }; تتكون واجهة ordinal من دالة واحدة، بينما تصدِّر date-names كائنًا يحتوي على عدة أشياء، إذ تُعَدّ days وmonths مصفوفات من الأسماء، كما تُعَدّ عملية فك البنية الهيكلية سهلةً جدًا عند إنشاء رابطات للواجهات المستوردة. تضيف الوحدة كذلك دالة واجهتها إلى exports كي تصل إليها الوحدات التي تعتمد عليها، إذ نستطيع استخدام الوحدة كما يلي: const {formatDate} = require("./format-date"); console.log(formatDate(new Date(2017, 9, 13), "dddd the Do")); // → Friday the 13th نستطيع تعريف require في أبسط صورها كما يلي: require.cache = Object.create(null); function require(name) { if (!(name in require.cache)) { let code = readFile(name); let module = {exports: {}}; require.cache[name] = module; let wrapper = Function("require, exports, module", code); wrapper(require, module.exports, module); } return require.cache[name].exports; } تَقرأ الدالة المختلَقة readFile في المثال أعلاه ملفًا وتعيد محتوياته في سلسلة نصية؛ أما جافاسكربت فلا توفِّر مثل تلك الخاصية، لكن توفِّر بيئات جافاسكربت المختلفة مثل المتصفحات وNode.js طرقها الخاصة للوصول إلى الملفات، وقد كان المثال يفترض وجود دالة اسمها readFile. تحتفظ دالة require بذاكرة مؤقتة cache من الوحدات المحمَّلة بالفعل، وذلك لتجنب تحميل الوحدة نفسها عدة مرات، فإذا استُدعيت، فستنظر أولًا إن كانت الوحدة المطلوبة محمَّلة من قبل أم لا، ثم تُحمِّلها إن لم تكن محمَّلة، حيث يتطلب ذلك قراءة شيفرة الوحدة وتغليفها في دالة واستدعاءها. لم تكن واجهة حِزمة ordinal التي رأيناها من قبل كائنًا وإنما دالةً، ومن مزايا CommonJS أنه رغم إنشاء نظام الوحدات لكائن واجهة فارغ مقيَّد بـ exports، يمكننا استبدال أيّ قيمة بذلك الكائن عبر إعادة كتابة module.exports، حيث يتم ذلك بعدة وحدات لتصدير قيمة واحدة بدلًا من كائن واجهة؛ وإذا عرَّفنا require وexports وmodule على أساس معامِلات لدالة التغليف المولَّدة وتمرير القيم المناسبة عند استدعائها، فسيضمن المحمِّل إتاحية تلك الرابطات في نطاق الوحدة. تختلف الطريقة التي تُرجِمت بها السلسلة النصية التي أُعطيت إلى دالة require إلى اسم ملف حقيقي أو عنوان ويب باختلاف الأنظمة، فإذا بدأت بـ ‎"./"‎ أو ‎"../"‎، فستُفسَّر تفسيرًا مرتبطًا باسم ملف الوحدة، وبالتالي سيكون ‎"./format-date"‎ هو الملف المسمى format-date.js في المجلد نفسه؛ أما إذا لم يكن الاسم مرتبطًا بالوحدة، فستبحث Node.js عن حِزمة مثبَّتة تحمل الاسم نفسه، وسنفسر مثل تلك الأسماء التي في شيفرات أمثلة هذا المقال على أنها تشير إلى حِزم NPM، كما سننظر بالتفصيل في كيفية تثبيت واستخدام وحدات NPM لاحقًا في هذه السلسلة. نستطيع استخدام واحد من NPM الآن بدلًا من كتابة محلل ملف INI الخاص بنا. const {parse} = require("ini"); console.log(parse("x = 10\ny = 20")); // → {x: "10", y: "20"} وحدات ESCMAScript تظل وحدات CommonJS حلًا غير دائم وغير احترافي رغم عملها بكفاءة وسماحها لمجتمع جافاسكربت -مع NPM- بتشارك الشيفرات على نطاق واسع، وتُعَدّ صيغتها غريبةً قليلًا، إذ لا تكون الأشياء التي نضيفها إلى exports متاحةً في النطاق المحلي مثلًا، وبما أنّ require هي استدعاء دالة عادية تأخذ أيّ نوع من الوسطاء وليس القيم مصنَّفة النوع فقط، فمن الصعب تحديد اعتماديات الوحدة دون تشغيل شيفرتها. لهذا يُصدِر معيار جافاسكربت نظام وحداته الخاص منذ 2015، كما يطلق عليه غالبًا ES modules، حيث تشير ES إلى ECMAScript، ورغم أنّ المفاهيم الأساسية للاعتماديات والواجهات لا زالت كما هي، إلا أنه يختلف في التفاصيل، فصارت الصيغة الآن مدمجةً في اللغة، كما نستطيع استخدام الكلمة المفتاحية الخاصة import بدلًا من استدعاء دالة للوصول إلى اعتمادية ما. import ordinal from "ordinal"; import {days, months} from "date-names"; export function formatDate(date, format) { /* ... */ } تُستخدم بالمثل كلمة export المفتاحية لتصدير الأشياء، وقد تأتي قبل تعريف دالة أو صنف أو رابطة (let أو const أو var). لا تكون واجهة وحدة ES قيمةً واحدةً، بل مجموعة من الرابطات المسماة، كما تربط الوحدة السابقة formatDate بدالة، فإذا استوردنا من وحدة أخرى، فسنستورد الرابطة وليس القيمة، مما يعني أن الوحدة المصدِّرة قد تغير قيمة الرابطة في أي وقت، وسترى الوحدات المستوردة القيمة الجديدة. إذا وُجدت رابطة اسمها default فستُعامَل على أنها القيمة المصدَّرة الأساسية للوحدة، فإذا استوردنا وحدةً مثل ordinal التي في المثال دون الأقواس التي حول اسم الرابطة، فسنحصل على رابطة default، كما تستطيع مثل تلك الوحدات تصدير رابطات أخرى تحت أسماء مختلفة مع تصدير default الخاص بها. إذا أردنا إنشاء تصدير افتراضي، فسنكتب export default قبل التعبير أو تصريح الدالة أو تصريح الصنف. export default ["Winter", "Spring", "Summer", "Autumn"]; من الممكن إعادة تسمية الرابطات المستورَدة باستخدام الكلمة as. import {days as dayNames} from "date-names"; console.log(dayNames.length); // → 7 من الفروقات المهمة كذلك هو حدوث تصديرات وحدات ES قبل بدء تشغيل سكربت الوحدة، وبالتالي قد لا تظهر تصريحات import داخل الدوال أو الكتل، ويجب أن تكون أسماء الاعتماديات سلاسل نصية مقتَبسة وليست تعبيرات عشوائية. يعكف مجتمع جافاسكربت أثناء كتابة هذه الكلمات على اعتماد وتبني أسلوب الوحدات هذا، لكن يبدو أنها عملية طويلة، فقد استغرقنا بضع سنين بعد الاستقرار على الصياغة كي تدعمها المتصفحات وNode.js، ورغم أنها صارت مدعومةً إلى حد كبير إلا أنّ ذلك الدعم به مشاكل، ولا زال الجدل قائمًا حول الكيفية التي يجب توزيع تلك الوحدات بها في NPM. تُكتب مشاريع عديدة باستخدام وحدات ES، ثم تُحوَّل تلقائيًا إلى صيغة أخرى عند نشرها، فنحن في مرحلة انتقالية يُستخدم فيها نظامَي وحدات جنبًا إلى جنب، ومن المفيد أننا نستطيع قراءة وكتابة شيفرات بكليهما. البناء والتجميع إذا نظرنا إلى مشاريع جافاسكربت فسنجد العديد منها لا يُكتب بجافاسكربت من الأساس -تقنيًا-، فثَمة امتدادات مستَخدَمة على نطاق واسع مثل ذلك الذي تعرضنا له في مقال الزلات البرمجية والأخطاء في جافاسكريبت الذي يتحقق من اللهجة، وقد بدأت الناس باستخدام الامتدادات الجاهزة في اللغة قبل إضافتها إلى المنصات التي تشغِّل جافاسكربت بزمن طويل أصلًا، ولكي يستطيع المبرمج فعل ذلك فإنه يصرِّف compile شيفرته، ثم يترجمها من لهجة جافاسكربت التي استخدمها إلى جافاسكربت عادية، أو إلى نسخة أقدم من جافاسكربت كي تستطيع المتصفحات الأقدم تشغيلها. لكن إدخال برنامج يتكون من وحدات ومن 200 ملف مختلف إلى صفحة ويب له مشاكله، فإذا كان جلب الملف عبر الشبكة يستغرق 50 ميلي ثانية، فسيستغرق تحميل البرنامج كله 10 ثواني، أو نصف تلك المدة إن كان يحمِّل عدة ملفات في الوقت نفسه، وهذا وقت ضائع. بدأ مبرمجو الويب باستخدام أدوات تجمع برامجهم التي قضوا الساعات المضنية في تقسيمها إلى وحدات، ليكون البرنامج ملفًا واحدًا كبيرًا، ثم ينشرونه على الويب، ذلك أن جلب ملف واحد وإن كان كبيرًا عبر الويب سيكون أسرع من جلب الكثير من الملفات الصغيرة، ويطلق على مثل تلك الأدوات اسم المجمِّعات bundlers. يتحكم حجم الملفات في سرعة نقلها عبر الشبكة كذلك، فليس العدد وحده هو المعيار، وعليه فقد اخترع مجتمع جافاسكربت مصغِّرات minifiers، وهي أدوات تأخذ برنامج جافاسكربت وتجعله أصغر عبر حذف التعليقات والمسافات تلقائيًا، كما تعيد تسمية الرابطات bindings، وتستبدل شيفرات أصغر بالشيفرات التي تأخذ حجمًا كبيرًا؛ وهكذا فليس من الغريب إيجاد شيفرة في حزمة NPM أو شيفرة تعمل في صفحة ويب، وتكون قد مرت بعدة مراحل من التحويل من جافاسكربت الحديثة إلى نسخة أقدم، ومن صيغة وحدات ES إلى CommonJS، ثم جُمِّعت وصغِّرت كذلك، كما لن نخوض في تفاصيل تلك الأدوات في هذه السلسلة لأنها مملة وتتغير بسرعة، لكن من المهم معرفة أنّ شيفرة جافاسكربت التي تشغِّلها لا تكون بهيئتها التي كُتبت بها أول مرة. تصميم الوحدة تُعَدّ هيكلة البرامج واحدةً من أدق الأمور في البرمجة، إذ يمكن نمذجة أيّ وظيفة غريبة بعدة طرق، كما يُعَدّ القول بجودة تصميم برنامج ما أمرًا نسبيًا، فهناك حلول وسط دومًا، كما تتدخل التفضيلات الشخصية في الحكم على التصميم، وأفضل ما يمكن فعله لتعلُّم قيمة التصميم الجيد هو القراءة والعمل على برامج كثيرة، وملاحظة ما ينجح وما يفشل، وإذا رأيت شيفرات فوضوية فلا تركن إلى قولك أن هذا هو الواقع ولا مجال لتغييره، بل هناك مساحة لتطوير هيكل أيّ شيء تقريبًا بإعمال الفكر فيه. تُعَدّ سهولة الاستخدام أحد أركان تصميم الوحدات، فإذا كنت تصمم شيئًا ليستخدمه أشخاص عدة -أو حتى نفسك فقط-، فيُستحسن أن تكون واجهتك بسيطةً وسهلة الفهم والتوقع حين تنظر إليها بعد ثلاثة أشهر حين تنسى تفاصيل ما كتبته وما عملته، وقد يعني ذلك اتباع سلوكيات موجودة سلفًا، كما تُعَدّ حزمة ini مثالًا على ذلك، إذ تحاكي كائن JSON القياسي من خلال توفير دوال parse وstringify -لكتابة ملف INI-، كما تحوِّل بين السلاسل النصية والكائنات المجردة مثل JSON، وهكذا فإنّ الواجهة صغيرة ومألوفة، وستذكر كيف استخدمتها لمجرد عملك معها مرةً واحدةً. حتى لو لم تكن ثمة دالة قياسية أو حِزمة مستخدَمة على نطاق كبير لمحاكاتها، فستستطيع إبقاء وحداتك مألوفةً وسهلة التوقع باستخدام هياكل بيانات بسيطة تفعل أمرًا واحدًا فقط، كما توجد على NPM مثلًا وحدات عديدة لتحليل ملف-INI، إذ تحتوي على دالة تقرأ مثل هذا الملف من القرص الصلب مباشرة وتُحلله، ويجعل ذلك من المستحيل استخدام مثل تلك الوحدات في المتصفح، حيث لا يكون لنا وصولًا مباشرًا إلى نظام الملفات، كما يضيف تعقيدًا كان يمكن معالجته بأسلوب أفضل عبر تزويد composing الوحدة بدالة قارئة للملفات file-reading. يشير ذلك إلى منظور آخر مفيد في تصميم الوحدات، وهو سهولة تطعيم شيء ما بشيفرة خارجية، إذ تكون الوحدات المركزة التي تحسب قيمًا قابلةً للتطبيق في طيف واسع من البرامج، على عكس الوحدات الأكبر التي تنفِّذ إجراءات معقدة لها آثار جانبية، وقارئ ملف INI الذي يركز على قراءة الملف من القرص الصلب ليس له فائدة في سيناريو يكون فيه محتوى الملف من مصدر خارجي، وبالمثل فإنّ الكائنات الحالة stateful objects مفيدة أحيانًا، بل قد تكون ضرورية؛ لكن إذا كان يمكن تنفيذ أمر ما بدالة فمن الأفضل استخدام الدالة. توفِّر العديد من قارئات ملفات INI على NPM نمط واجهة interface style، إذ يحتاج منك إنشاء كائن أولً، ثم تحمِّل الملف إلى الكائن، وبعد ذلك تستخدِم توابع مخصصة للحصول على النتائج، وذلك الأمر شائع في البرمجة كائنية التوجه، كما هو أمر مرهق وخاطئ. علينا تنفيذ طقوس نقل الكائن خلال عدة مراحل بدلًا من تنفيذ استدعاء واحد لدالة، وبما أن البيانات مغلفة الآن في نوع كائن مخصص، فيجب على كل الشيفرات التي تتفاعل معها معرفة ذلك النوع، مما يجعل لدينا اعتماديات غير ضرورية. قد تكون لدينا حالات لا يمكن تجنب تعريف هياكل بيانات جديدة فيها، حيث لا توفر اللغة إلا بعض الهياكل البسيطة، كما ستكون العديد من أنواع البيانات معقدةً أكثر من مجرد مصفوفة أو خارطة map، لكن إذا كانت المصفوفة تكفي، فسنستخدِم المصفوفة. يمكن النظر إلى مثال المخطط الذي تعرضنا له في مقال مشروع تطبيقي لبناء رجل آلي (روبوت) عبر جافاسكريبت على أساس مثال على هيكل بيانات معقد، إذ لا توجد طريقة واضحة لتمثيل المخطط في جافاسكربت، وقد استخدمنا في ذلك المقال كائنًا تحمل خصائصه مصفوفات من السلاسل النصية، وهي العقد الأخرى التي يمكن الوصول إليها من تلك العقدة. توجد العديد من حِزم إيجاد المسار في NPM، لكن لا تستخدِم أيّ منها صيغة المخطط تلك، فهي تسمح عادةً لحدود المخطط أن يكون لها وزن يكون التكلفة أو المسافة المرتبطة به، ولم يكن ذلك ممكنًا في تمثيلنا. هناك حِزمة dijkstrajs مثلًا التي تستخدِم منظورًا شائعًا جدًا لإيجاد المسارات والذي يُسمى بخوارزمية ديكسترا Dijkstra's algorithm، إذ سُمي باسم إدزجر ديكسترا Edsger Dijkstra الذي كتبه، وهو مشابه لدالة findRoute الخاصة بنا، ومن الشائع أن تضاف اللاحقة js إلى اسم الحِزمة لتوضيح أنها مكتوبة بجافاسكربت. تستخدِم حزمة dijkstrajs صيغة مخطط تشبه صيغتنا، لكنها تستخدم كائنات تكون قيم خصائصها أعدادًا -أي أوزان الحدود- بدلًا من المصفوفات التي استخدمناها، فإذا أردنا استخدام تلك الحزمة، فسيكون علينا التأكد من تخزين المخطط بالصيغة التي تتوقعها الحزمة، كما يجب أن تحصل جميع الحدود على الوزن نفسه بما أنّ نموذجنا المبسط يعامِل كل طريق على أساس امتلاكه التكلفة نفسها -أي منعطف واحد-. const {find_path} = require("dijkstrajs"); let graph = {}; for (let node of Object.keys(roadGraph)) { let edges = graph[node] = {}; for (let dest of roadGraph[node]) { edges[dest] = 1; } } console.log(find_path(graph, "Post Office", "Cabin")); // → ["Post Office", "Salma’s House", "Cabin"] قد يكون ذلك حاجزًا معيقًا عن التركيب compositing، حيث تستخدِم حِزم عدة هياكل بيانات مختلفة لوصف أشياء متشابهة، فيكون جمعها معًا صعبًا، وبالتالي إذا أردنا تصميم شيء ليكون قابلًا للتركيب، فيجب علينا معرفة هياكل البيانات التي يستخدِمها الأشخاص أولًا، ثم نحذو حذوهم كلما تيسر. خاتمة توفر الوحدات هيكلًا لبرامج أكبر من خلال فصل الشيفرة إلى أجزاء صغيرة لها واجهات واضحة واعتماديات، كما تُعَدّ الواجهة الجزء المرئي من الوحدة للوحدات الأخرى، والاعتماديات هي الوحدات الأخرى التي تستخدِمها. ولأنّ جافاسكربت لم توفر نظامًا للوحدات في البداية، فقد بُني نظام CommonJS عليها، ثم حصلت جافاسكربت بعد مدة على نظام وحدات خاص بها، وهو الآن موجود إلى جانب CommonJS. تُعَدّ الحزمة مجموعة شيفرات يمكن توزيعها مستقِلة بذاتها، كما يُعَدّ NPM مستودعًا لحِزم جافاسكربت، ونستطيع تحميل شتى الحِزم منه سواء مفيدة أو غير مفيدة. تدريبات الروبوت التركيبي فيما يلي الرابطات التي ينشئها المشروع الذي في مقال مشروع تطبيقي لبناء رجل آلي (روبوت) عبر جافاسكريبت الذي أشرنا إليه بالأعلى. roads buildGraph roadGraph VillageState runRobot randomPick randomRobot mailRoute routeRobot findRoute goalOrientedRobot إذا أردت كتابة هذا المشروع على أساس برنامج تركيبي modular، فما الوحدات التي ستنشئها؟ وما الوحدات التي ستعتمد عليها كل وحدة؟ وكيف ستبدو واجهاتها؟ وأيّ الأجزاء ستكون متاحةً ومكتوبةً مسبقًا على NPM؟ وهل ستفضِّل استخدام حِزمة NPM أم تكتبها بنفسك؟ إرشادات للحل إليك ما كنا لنفعله بأنفسنا، لكن مرةً أخرى ليست هناك طريقة صحيحة وحيدة لتصميم وحدة معطاة: توجد الشيفرة المستخدَمة لبناء مخطط الطريق في وحدة graph، ولأننا نُفضِّل استخدام dijkstrajs من NPM بدلًا من شيفرة إيجاد المسار التي كتبناها، فسنجعل هذا المخطط قريبًا من الذي تتوقعه dijkstrajs. تصدِّر هذه الوحدة دالةً واحدةً هي buildGraph، كما سنجعل هذه الدالة تقبل مصفوفةً من مصفوفات ثنائية العناصر بدلًا من سلاسل نصية تحتوي على شخطات -، وذلك من أجل تقليل اعتماد الوحدة على صيغة الإدخال. تحتوي وحدة roads على بيانات الطريق الخام -أي مصفوفة roads- ورابطة roadGraph، كما تعتمد تلك الوحدة على ‎./graph وتصدر مخطط الطريق. يوجد صنف VillageState في وحدة state، حيث يعتمد على وحدة ‎./roads لأنه يحتاج إلى أن يكون قادرًا على التحقق من وجود طريق موجود فعليًا، كما يحتاج إلى randomPick، وبما أنّ هذه دالة من ثلاثة أسطر، فسنضعها في وحدة state على أساس دالة مساعدة داخلية، لكن randomRobot يحتاج إليها كذلك، لذا يجب تكرارها أو وضعها في وحدتها الخاصة، وبما أنّ تلك الدالة موجودة في NPM في حِزمة random-item، فمن الأفضل جعل كلا الوحدتين تعتمدان عليها، كما نستطيع إضافة دالة runRobot إلى تلك الوحدة أيضًا، بما أنها صغيرة ومرتبطة ارتباطًا وثيقًا بإدارة الحالة، في حين تصدِّر الوحدة كلا من الصنف VillageState ودالة runRobot. أخيرًا، يمكن دخول الروبوتات والقيم التي تعتمد عليها مثل mailRoute في وحدة example-robots التي تعتمد على ‎./roads وتصدِّر دوال الروبوت، ولكي يستطيع goalOrientedRobot البحث عن المسار، فستعتمد الوحدة على dijkstrajs أيضًا. صارت الشيفرة أصغر قليلًا من خلال نقل بعض المهام إلى وحدات NPM، حيث تنفِّذ كل وحدة مستقِلة شيئًا بسيطًا ويمكن قراءتها بنفسها، ويقترح تقسيم الشيفرات إلى وحدات على أساس تحسينات أكثر لتصميم البرنامج. يُعد اعتماد الروبوتات وVillageState على مخطط طريق بعينه غريبًا بعض الشيء، فربما يكون من الأفضل جعل المخطط وسيطًا لباني الحالة، ونجعل الروبوتات تقرؤه من كائن الحالة، فنقلل الاعتماديات -وهو أمر جيد-، ونجعل من الممكن إجراء عمليات محاكاة على خرائط مختلفة، وذلك خير وأفضل. هل من الجيد استخدام وحدات NPM لأمور كان يمكن كتابتها بأنفسنا؟ نظريًا، نعم، فمن المرجح أنك ستقع في أخطاء في الأمور المعقَّدة مثل دالة إيجاد المسار وتضيع وقتك في كتابتها بنفسك؛ أما بالنسبة للدوال الصغيرة مثل random-item فتكون كتابتها أمرًا يسيرًا، لكن إضافتها في أيّ مكان تحتاج إليها فيه سيعكر وحداتك. لكن بأي حال، لا تقلل من شأن الجهد المطلوب لإيجاد حِزمة NPM مناسبة، فحتى لو وجدتها فقد لا تعمل جيدًا أو تكون مفتقدةً إلى بعض المزايا التي تحتاجها، وفوق هذا يعني الاعتماد على حِزم NPM التأكد من أنها مثبَّتة، كما ستنشرها مع برنامجك، بل ربما يكون عليك ترقيتها دوريًا، فهذه قد تكون إحدى التبعات التي عليك تحملها، وعلى أيّ حال تستطيع اتخاذ القرار الذي يناسبك وفقًا لمقدار العون الذي تقدمه تلك الحزم لك. وحدة الطرق اكتب وحدة CommonJS بناءً على المثال الذي في مقال مشروع تطبيقي لبناء رجل آلي (روبوت) عبر جافاسكريبت، بحيث تحتوي على مصفوفة من الطرق وتصدِّر هيكل بيانات المخطط الذي يمثلها على أساس roadgraph، كما يجب أن تعتمد على وحدة ‎./graph التي تصدِّر الدالة buildGraph المستخدَمة لبناء المخطط، وتتوقع هذه الدالة مصفوفةً من مصفوفات ثنائية العناصر -أي نقاط البداية والنهاية للطرق-. تستطيع تعديل شيفرة التدريب لكتابة الحل وتشغيلها في طرفية المتصفح إن كنت تقرأ من متصفح، أو بنسخها إلى codepen. // أضف اعتماديات وتصديرات. const roads = [ "Salma's House-Omar's House", "Salma's House-Cabin", "Salma's House-Post Office", "Omar's House-Town Hall", "Sara's House-Mostafa's House", "Sara's House-Town Hall", "Mostafa's House-Sama's House", "Sama's House-Farm", "Sama's House-Shop", "Marketplace-Farm", "Marketplace-Post Office", "Marketplace-Shop", "Marketplace-Town Hall", "Shop-Town Hall" ]; إرشادات للحل بما أنّ هذه وحدة CommonJS، فعليك استخدام require لاستيراد وحدة المخطط، وقد وُصف ذلك على أساس تصدير دالة buildGraph، إذ تستطيع استخراجه من كائن واجهته مع تصريح فك الهيكلة const. أضف خاصيةً إلى كائن exports من أجل تصدير roadGraph، كما يجب أن يكون فصل سلاسل الطريق النصية داخل وحدتك لأن buildGraph تأخذ هيكل بيانات لا يطابق roads بالضبط. الاعتماد المتبادل بين الوحدات تُعَدّ حالة الاعتماد المتبادل بين وحدتين ويدعى circular dependency حالةً تعتمد فيها الوحدة A على الوحدة B -أي يكون الاعتماد على شكل حلقة تبادلية بين وحدتين ولذلك سمي باللغة الإنجليزية circular dependency-، والعكس صحيح مباشرةً أو غير مباشرة، كما تمنع العديد من أنظمة الوحدات ذلك بسبب استطاعتك التأكد من تحميل اعتماديات وحدة قبل تشغيلها، بعض النظر عن الترتيب الذي تختاره لتحميل مثل تلك الوحدات. تسمح وحدات CommonJS بصورة محدودة من الاعتماديات الدورية cyclic dependencies تلك، طالما أنّ الوحدات لا تستبدل كائن exports الافتراضي الخاص بها، ولا يكون لها وصول إلى واجهة غيرها حتى تنتهي من التحميل. تدعم دالة require التي تعرضنا لها سابقًا في هذا المقال مثل ذلك النوع من تبادلية الاعتماديات dependency cycle، فهل يمكنك رؤية كيف تعالج هذه الحالة؟ وما الذي قد يحدث إذا استبدلت وحدة في تعتمد وحدة أخرى عليها (أي داخلة في دورة الاعتمادية)دورة، كائن exports الافتراضي الخاص بها؟ إرشادات للحل تدور الفكرة هنا حول إضافة require وحدات إلى ذاكرتها المؤقتة قبل بدء تحميل الوحدة، وهكذا فإذا حاول أيّ استدعاء require تحميلها أثناء تشغيلها، فسيكون معروفًا وتُعاد الواجهة الحالية بدلًا من تحميل الوحدة مرةً أخرى، وذلك يتسبب في طفحان المكدس؛ أما إذا أعادت وحدة كتابة قيمة module.exports الخاصة بها، فستحصل أيّ وحدة أخرى كانت قد استقبلت قيمة واجهتها قبل أن تنهي التحميل، على كائن الواجهة الافتراضي -الذي سيكون فارغًا على الأرجح- بدلًا من قيمة الواجهة المقصودة. ترجمة -بتصرف- للفصل العاشر من كتاب Elequent Javascript لصاحبه Marijn Haverbeke. اقرأ أيضًا مقدمة إلى الوحدات Modules في جافاسكربت الوحدات Modules والحزم Packages في بايثون الوحدات Modules في AngularJS
  2. تُعرِّف 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
  3. هل أنت مهتم بتعلم PHP وبناء قوالب وملحقات خاصة بك على ووردبريس؟ أو ربما أنت مُهتمّ بتعلّم القليل الذي يُمكنك من إدخال التّعديلات التي تحتاجها على مواقعك ومدوّناتك التي تعتمد على ووردبريس. أيّا كان وضعك، هناك أمور يجب عليك أن تعرفها قبل أن تغوص في مجال تطوير ووردبريس. سنحاول في هذا المقال إعطاءك فكرة واضحة عليها. كيف يعمل ووردبريس؟ لنفهم ماذا وكيف يمكننا التطوير في ووردبريس، سنلقي أولا نظرة سريعة على آلية عمل ووردبريس. يتكون ووردبريس من ثلاثة عناصر رئيسية: النواة الأساسية والقوالب والمُلحقات. تحتوي النواة على جميع الوظائف الأساسية التي تشكل نظام إدارة المحتوى لووردبريس، وهذه الشيفرات البرمجية تتضمن كل شيء من البنية التحتية (backend) للإدارة إلى دوال جدولة المشاركات والتأكد من قوة كلمة المرور والسماح بإنشاء مستخدمين وغيرها. في حين أن النواة الأساسية (core) مسؤولة عن البنية التحتية للموقع وكيفية التعامل معها، فإن القوالب مسؤولة عن الواجهة الأمامية للموقع وكيفية ظهوره. نستخدم إدارة ووردبريس (WordPress admin) لإنشاء المشاركات والصفحات وبقية محتوى الموقع، وأما بالنسبة لكيفية ظهور هذه الأشياء للزائر فهو أمر متروك للقالب theme، لذلك فإن هذا سيعطيك قوة كبيرة للتحكم في موقعك، فقد ترغب بتثبيت وتفعيل القوالب الموجودة أو حتى تطوير قوالبك المخصصة. أبسط طريقة لوصف المُلحقات plugins هي أنها توفر وظائف إضافية للووردبريس، ولفعل ذلك، قد تعدّل المُلحق الشيفرة البرمجية للواجهة الأمامية أو البنية التحتية للموقع، وأبسط مثال لذلك هي المُلحق الذي يضيف زر لتغريدة تويتر، وقد تُنشئ أيضا صفحة إعدادات جديدة في البنية التحتية لقائمة الإدارة والتي ستمكنك من إعداد بعض الخيارات الافتراضية لتغريدة المستخدم ويمكنها إضافة نفسها إلى الواجهة الأمامية للموقع، والتي سيكون موقعها في الغالب تحت التدوينة (post). مجالات التطوير بقدر تقدمك في تطوير ووردبريس، ستتمكن من تطوير أي واحدة من المكونات الرئيسية المذكورة أعلاه بالإضافة إلى تطبيقات مستقلة بمساعدة REST API الجديدة وسنذكر المزيد عن هذا الموضوع بعد قليل. مهما كان ما تريد فعله مع ووردبريس، أنصحك باتباع منهج في دراستك، هذه القائمة ستساعدك على التعرف على الأنظمة التي تحتاج إلى معرفتها قبل بدء تعلمك المزيد حول المجال الذي اخترته: القوالب المُلحقات REST API النواة القوالب إن تعلم كيفية عمل القوالب وتطويرها سيسمح لك باحتراف إنشاء المواقع بدءا من المواقع الصفحة الواحدة إلى مواقع المحتوي الحيوي الثقيل، وستسمح لك أيضا بالحصول على المزيد من العملاء (إذا كان هذا ما يهمك) أو حتى بيع القوالب تجاريا. يوجد العديد من مطوري القوالب الناجحين في أسواق مثل Themeforest و Mojo Marketplace وغيرها. ففي وقت كتابة هذا المقال، أشهر قالب على موقع Themeforest تم بيع 1377 نسخة منه في أسبوع أي أكثر من $81,000 في أسبوع واحد فقط! يمكنك الحصول على الكثير من الأموال إذا كنت تستطيع تطوير قالب مميزة. على الرغم من أن هذا المبلغ لا يحققه جميع مطوري القوالب، وربما لن تصبح مليونيرًا بين عشيّة وضحاها من إنشاء القوالب، لكن يمكنك الحصول على مبلغ مالي جيد من القوالب إذا كنت تقوم بالأمر بالشكل الصحيح. إذا أردت رؤية بعض الأمثلة عن القوالب الشهيرة، فألق نظرة على الأسواق التي ذكرناها أعلاه، أو على قسم قوالب ووردبربس حيث جميع القوالب مجانية، فالكثير من مطوري القوالب يصدرون أعمالهم الأولى في هذا القسم. يمكنك تعلم كيفية إنشاء قوالب ووردبريس في أكاديمية حسوب من خلال سلسلة الدروس مدخل إلى تطوير قوالب ووردبريس. الملحقات plugins بطريقة ما، تعتبر المُلحقات شريان حياة ووردبريس، فهي تحوله إلى كل شيء من منتدى إلى شبكة اجتماعية أو منصة للتجارة الإلكترونية وغيرها باستخدام ضغطة زر واحدة، فالمُلحقات تعطيك تحكمًا كاملًا بجميع جوانب نظام ووردبريس، وتسمح لك بتعديل ما تشاء، وسيساعدك هذا كثيرا عند إنشاء مواقع ووردبريس للعملاء. كما هي الحال مع القوالب، فالمُلحقات تملك أيضا أسواقا وفي العادة يتم إدارتها من نفس الشركات التي تدير أسواق القوالب. يعتبر سوق CodeCanyon على سبيل المثال أكبر سوق للملحقات حيث يملك أكثر من 4000 ملحق متاحة للشراء. على الرغم من أن المبيعات ستكون أقل من القوالب إلا أنه يمكنك ربح الكثير إذا أنشأت ملحقًا جيدًا. يملك موقع ووردبريس قسمًا للملحقات المجانية ويحتوي على أكثر من 40000 ملحق، وهو بذلك مصدر كبير للأدوات والإلهام والأمثلة على الشيفرات البرمجية المميزة (والشيفرات البرمجية السيئة للأسف). يمكنك تعلم كيفية إنشاء ملحقات وإضافات ووردبريس في أكاديمية حسوب من خلال سلسلة الدروس مدخل إلى برمجة إضافات ووردبريس. ما بين WordPress.org و WordPress.com لقد أُطلق نظام ووردبريس بطريقتين مختلفتين، حيث كانت الأولى عن طريق WordPress.com؛ أمّا الثّانية، فعبر استخدام WordPress.org، وهذا نظرًا لإمكانيّة احتساب ووردبريس موقعًا للإنترنت، ومنصّة أعمال في آنٍ واحد، حيث يمكنك إنشاء حساب مجّاني في WordPress.com، والحصول على نسخة مُستضافة من نظام ووردبريس لخدمة موقعك، بحيث ستتحمل شركة الاستضافة مسؤوليّة صيانة الخادم وتثبيت ووردبريس عليه، ممّا يوفر عليك بعض العبء، ويتيح لك التّركيز على إنشاء المحتوى. تجني WordPress.com أرباحها عبر تقاضيها لأموال مقابل ترقيتها للمزايا، فعلى سبيل المثال إذا رغبت في استخدام اسم النّطاق الخاصّ بك في WordPress.com، فستحتاج إلى دفع القليل من الرسوم، وهناك الكثير من الشّركات التي تدفع لمنصّة WordPress.com لأجل استضافة مواقع ووردبريس ضخمة، مثل: بي بي سي أمريكا (BBC America)، ومجلة نيويوركر (The New Yorker Magazine)، ومدونة ستار وورز (Star Wars) الرّسمية. بسبب حاجة ووردبريس لدعم مختلف أنواع وأحجام المواقع، تمتلك منصّتها العديد من القيود المتعلّقة بتخصيص المواقع على نحو شخصيّ، فهناك عدد محدود من القوالب التي تتحكم بتنسيق ومظهر المواقع، كما يوجد عدد قليل من الإضافات التي تُعزّز من أداء وظائفه؛ أمّا WordPress.org فهو الموقع الرّسمي لنظام إدارة المحتوى ووردبريس الذي تستضيفه بنفسك. نقصد باستضافة ووردبريس عمليّة تنزيل نسخة من برنامج ووردبريس، وتثبيته على خادم إنترنت، والذي هو بالمقابل عبارة عن حاسب يستخدم برمجيّات خاصّة تسمح له بتوفير الوصول إلى مواقع الإنترنت المُخزّنة به لجميع المستخدمين على الإنترنت، حيث يمكن لهذا الخادم أن يكون مجرّد حاسب عتيق مُلقى في مكان ما بمنزلك، أو قد يكون مركز بيانات متطوّر، ولك حريّة اختيار الخادم المناسب لموقعك حسب تفضيلك، إذ تتيح استضافتك لنظام ووردبريس بنفسك إنشاء موقع إنترنت تمتلك فيه كامل الحريّة في التّحكم؛ بالمقابل، ستقع عليك مسؤوليّة إعداد وتثبيت نظام ووردبريس، وصيانته بنفسك. REST API ستستخدم REST API لاحقا عندما تحترف تطوير القوالب والمُلحقات، ونظريا ستكون قادرا على استخدامها إذا كانت لديك خبرة في البرمجة بلغة أخرى مثل جافا أو روبي. تعتبر REST API جديدة نسبيا وتسمح لك بإنشاء تطبيقات حقيقية بالاعتماد على ووردبريس، وهذا يتضمن تطبيقات أندرويد وأيفون وغيرها. أفضل طريقة لشرح هذه الميزة هي عن طريق مقارنتها بمواقع مثل تويتر أو انستغرام، لا شك أنك قد رأيت تغذية تويتر معروضة على موقع أحدهم، ولإضافة هذه الميزة إلى موقعك لن تحتاج إلى معرفة كيفية عمل نواة شيفرة البرمجية الخاصة بتويتر، كل ما تحتاجه هو القليل من الشيفرات البرمجية التي تقول: "من فضلك استرجع آخر خمسة من تغريداتي." وسيتعامل تويتر مع الباقي وسيرجع لك بيانات آخر خمسة تغريدات التي طلبتها، وبعد ذلك يمكنك استخدام HTML و CSS لعرضها. وهذا بالضبط ما يفعله REST API لووردبريس، يمكنك الاتصال بأي موقع وطلب منه آخر خمسة مشاركات فيها، ويمكنك أيضا القيام بأكثر من ذلك: يمكنك إنشاء/حذف المستخدمين وتعديل الفئات وغيرها (بالطبع، تحتاج إلى الاستيثاق قبل استخدامها)، هذا يعني أنه يمكنك استخدام ووردبريس كمستودع للمعلومات وبناء واجهة أمامية وبنية خلفية باستخدام نظام مختلف تماما. النواة ساهم في تطوير ووردبريس مجتمع من الآلاف المتطوعين حول العالم، حيث أنه يعمل 471 شخص على الإصدار 4.4 من ووردبريس، والعديد من الأشخاص الآخرين يعملون على جوانب مختلفة من ووردبريس، من الترجمة وتطبيقات الويب إلى الإتاحة (accessibility) والدعم. في الحقيقة، يمكنك الاشتراك معهم الآن، فبينما تحتاج إلى بعض الخبرة للمشاركة في برمجة نواة ووردبريس إلا أنه يمكنك المساهمة في تحديد المشاكل والأخطاء والتأكد منها وتقديم الأفكار في أي وقت باستخدام WordPress Trac، وهو المكان الذي يمكنك من متابعة تطوير شيفرة نواة ووردبريس. إذا وجدت علة (خطأ - bug) يمكنك إرفاقها بالشيفرة المناسبة للتخلص منها، وبعد عدة مراجعات وتدقيقات سيتم استخدام شيفرتك البرمجية إذا كانت مناسبة، وهذه هي أفضل طريقة للمشاركة في تطوير النواة. تطوير ووردبريس: ماذا تحتاج أن تتعلمه يجب أن تعتاد على العمل على مجموعة محددة من الأدوات حتى تتمكن من تطوير ووردبريس. إن عملية التعلم لا تنتهي بالنسبة إلى أي مطور جيد، سوف أعرض عليك هنا حزمة للبداية كما سأريك بعض الأدوات المتقدمة التي قد تحتاج إليها لاحقا عندما تريد أن تتعلم المزيد. الحد الأدنى من المتطلبات لتطوير الووردبريس هي HTML و CSS و PHP، بالإضافة إلى هذه اللغات ستحتاج إلى جافا سكربت و MySQL في مرحلة ما، عندما تتقن هذه اللغات يمكنك الانتقال إلى أشياء أخرى مثل LESS/SASS و Coffeescript و XML و JSON وغيرها، لكن هذه الأشياء لا تعتبر ضرورية لتطوير ووردبريس، لكن إذا عرفتها سيجعل حياتك أسهل كمطور وستوسع آفاقك. HTML و CSS أول لغتين يجب عليك تعلمهما هما HTML و CSS، مهما كانت الشيفرة البرمجية التي تكتبها ومهما كانت اللغة التي تستخدمها فستحتاج حتما إلى HTML عند إرسال صفحات الويب إلى المستخدم وإلى بعض CSS لتصميم وتزيين الصفحة. إن HTML مسؤولة عن إعطاء هيكل المواقع. على المستوى الأساسي، هذا يعني أنك ستقرر ما النص الذي يجب أن يكون عنوانًا وما يجب أن يكون قائمة وما الذي يجب أن يكون في رأس أو أسفل الصفحة. أما في المستوى الأعمق، ستقرر قرارات مهمة حول SEO والتحسين (optimization). يُستخدم CSS لتطبيق أنماط لهيكل HTML، أي أنك ستُعرّف لون النص والروابط والمسافة بين الفقرات ومحاذاة الصورة وألوان الحدود وسمكها وغيرها من العناصر عن طريق استخدام شيفرة CSS. إن CSS تتميز بسهولة تعلمها وبصعوبة إتقانها. PHP تحظى PHP بشعبية كبيرة كأشهر لغة سكربتات من جانب الخادوم server-side-، ووفقا للاستطلاع الذي قام به W3Techs، تستخدم PHP في 81.6% من المواقع وهي اللغة التي كُتب بها ووردبريس (وتقريبا أغلب أنظمة إدارة المحتوى الأخرى أيضا) ولذلك فإن معرفة PHP هو أمر ضروري لتطوير ووردبريس. إن معرفتك بـ PHP وحدها، سيجعلك تحصل على أكثر من معرفة بتطوير ووردبريس فقط. إن أهم فرق بين لغات جانب الخادوم عن لغات جانب العميل - مثل HTML - هو أنه يتم معالجتها في الخادوم أولا، ففي HTML، يمكنك كتابة شيفرة برمجية لـ Good Morning وسيرسلها HTML كما هي وسيعرضها على الزوار. أما في لغات جانب الخادوم، فستكتب شيء مشابه لهذا [good [time_of_day، وقبل أن يتم إرسال ذلك الأمر إلى المستخدم، سيعالج الخادوم هذه الأمر حتى يعرف ما يضعه في مكان [time_of_day] حسب الوقت الذي دخلت فيه إلى الموقع، فيمكنك أن ترى "Good day" أو "Good evening". لاحظ أن البيانات التي يستقبلها متصفحك تبقى HTML لكن تمت معالجتها في الخادوم مسبقا، ولاحظ أيضا أن الشيفرة السابقة لم تكن شيفرة PHP بل هي مثال فقط لأعطيك فكرة عن آلية عمل هذه اللغة. كما هو الحال مع جميع لغات البرمجة، فالممارسة والتمرين هي التي تجعلك تبرمج بشكل أفضل، فيمكنك تعلم PHP في غضون بضعة أيام لكنك ستجد نفسك أنك لا تملك أية فكرة عن ما الذي تقوم به، لذلك تحتاج إلى تجربة الأشياء وتخريبها وليس الوقوف دون معرفة ما يجري، وكما يقولون الممارسة تولد الإتقان. جافا سكربت ازداد دور جافا سكربت أهمية على الإنترنت، خاصة مع ظهور أدوات مبنية على جافا سكربت مثل Node و Angular. جافاسكربت تستخدم عادة لإضافة وظائف حيوية للمواقع وللتحميل غير المتزامن. سأعطيك مثال على كل واحد منها. فمثلا لو أردت أن يتم إخفاء نموذج بشكل كامل وأن يظهر ببطء داخل lightbox عندما يضغط المستخدم على زر معين، فيمكنك فعل ذلك باستخدام جافا سكربت بما أنه يمكنه التعامل مع تحريك الرسوم (animation) وربما الوظائف الأخرى، مثل التأكد من أنه تم ملئ النموذج بشكل صحيح قبل إرساله. أما التحميل غير المتزامن فيتم عمله عبر آلية تسمى بـ AJAX، فيمكنك استخدام AJAX للحصول على معلومات من الخادوم وعرضها دون إعادة تحميل الصفحة، وأبسط مثال لذلك هو التمرير اللانهائي (endless scrolling) فعندما يتم تحميل 10 مشاركات وتصل إلى نهاية الصفحة فسيتم عرض 10 مشاركات أخرى وتستمر هكذا. الكثير من المطورين تعلموا جافا سكربت من خلال إطار jQuery، فهو يُستخدم على نطاق واسع في ووردبريس وفي ملايين المشاريع على الإنترنت، وعلى الرغم من أن هذا الأمر جيد لكن ضع في اعتبارك أن جافا سكربت هو أكثر من مجرد jQuery وإن تعلم جافا سكربت كما هو هي فكرة جيدة. MySQL إن Mysql هي لغة تُستخدم للوصول والعمل مع البيانات المخزنة في قاعدة البيانات، يستخدم ووردبريس هذه اللغة كثيرا للتعامل مع البيانات، لكن كمطور، لا تحتاج إلى معرفة الكثير لأن ووردبريس يملك مجموعة من الدوال المساعدة. لكن في بعض الحالات الخاصة، قد تحتاج إلى كتابة استعلام قاعدة البيانات بنفسك أو ربما قد ترغب بتحسين شيء في موقعك، لذلك سيفيدك تعلم أساسيات MySQL. إن العمل مع قواعد البيانات واضح للغاية، فالجدول في قاعدة البيانات يشبه كثيرا جداول بيانات Microsoft Excel، فكل عمود يملك عنوانًا وبيانات مخزنة في الأسطر، ونستخدم MySQL لإضافة وحذف وتعديل واسترجاع البيانات، ربما ترغب بكتابة استعلام MySQL يرجع لك جميع المشاركات التي تحتوي على كلمة "awesome" ولديها أكثر من 8 تعليقات، أو قد ترغب أيضا بإيجاد جميع المستخدمين الذين يملكون اسم "Daniel". مرة أخرى، إن كتابة شيفرة MySQL خام في ووردبريس هو أمر نادر، لكنه يحدث في بعض الأحيان، لذلك فإن فهم كيفية عمل اللغة سيساعدك على كتابة شيفرات أفضل بشكل عام أدوات متقدمة بمجرد أن تتعود على الأقل على استخدام ثلاثة اللغات الأساسية - HTML و CSS و PHP - يمكنك البدء باكتشاف العديد من الأدوات المساعدة، واحدة من الأشياء الأولى التي يميل الناس إلى تعلمها هي LESS و/أو SASS، كلاهما "مجموعات موسّعة" (Superset) لـ CSS، أي أن أي شيفرة CSS صحيح تعتبر صحيحة في شيفرة LESS و SASS، وتكمن أهمية إضافة هاتين اللغتين هو إمكانية استخدام متغيرات ودوال و غيرها في CSS، والذي هو غير ممكن بشكل افتراضي. إن أدوات البناء (Build tools) هو شيء سترغب في اكتشافه عند مرحلة معينة، أفضلها Gulp و Grunt فهذان يمكنهما مراقبة تعديلات الملفات وتنفيذ مختلف المهام عند استيفاء المتطلبات، فعلى سبيل المثال، في أي وقت تعدل فيه ملف SASS يمكنك ترجمته تلقائيا إلى CSS وحفظه إلى ملف معين، أو يمكنك تحسين الصور أو دمج الملفات أو تحميل الحزم الخارجية فجميع المهام الأخرى يمكنك أن تفعلها باستخدام هذه الأدوات. سطر الأوامر أو الطرفية (terminal) هو شيء يجب أن تلقي نظرة عليه. نعم إن استخدام سطر الأوامر قد يبدو مخيفا لكنه ليس كذلك، فبمجرد كتابة بضعة أوامر يمكنك حفظ الكثير من الوقت، فسكربت/أداة مثل WP-CLI يمكنه تثبيت ووردبريس في بضعة ثواني، بما في ذلك تثبيت القوالب والمُلحقات والمحتويات للتجربة والتي لو قمت بها بشكل يدوي ستتطلب الكثير من الوقت والجهد. إن أنظمة االتّحكّم في الإصدارات (Version control) هي من الأدوات المفيدة للغاية فلن تعرف حاجتك إليها إلا لو استخدمتها، فعلى الرغم من أنها وُضعت أصلا للعمل على نفس الشيفرة البرمجية في مجموعات إلا أنه يمكنك استخدامها كحل لإدارة المشاريع والمشاكل والنسخ الاحتياطي في نفس الوقت. ويعتبر كل من SVN و Git الأشهر على الإطلاق، لكن بالنسبة لي، أعتبر أن Git أفضل نظرا لأنه يعمل بشكل أفضل وبسبب Github الذي يوفر خدمة استضافة مستودعات Git على الإنترنت. ووردبريس: بوابتك إلى عالم البرمجة إن أفضل شيء بالنسبة إلى ووردبريس أنه يمكن أن يكون مدخلا لتعلم مهارات تطوير ويب ولغات أخرى. ولقد خضت هذه التجربة شخصيا، فلقد تعلمت أولا البرمجة الموجهة في PHP ثم تعلمت إطار عمل Laravel وأدوات البناء ثم LESS/SASS وأدوات سطر الأوامر وغيرها الكثير. ولقد خضت أيضا في غمار native app باستخدام #C وغيرها من اللغات. بمجرد معرفتك بكيفية عمل كل شيء، ستتعلم المزيد عند إنشاء مشاريع جديدة وكبيرة وكل شيء سيصبح أسهل، فبعد البرمجة كائنية التّوجّه في PHP لن تبذل الكثير من الجهد في تعلم #C لأن دماغك سيتعود على منطقه بطريقة مشابه لتعلم قيادة الشاحنات بعد تعلمك قيادة السيارات، فعلى الرغم من وجود الكثير من الاختلافات لكن بمجرد أن يكون لديك بعض الأساسيات سيسهل عليك التّحكمّ فيه. خاتمة ها قد عرفت الآن ما الذي تحتاجه للبدء في تطوير موقع ووردبريس، وقد تشعر بالإرباك، لكن لا تقلق، فالجميع سيشعر بذلك في هذه المرحلة. تذكّر أمرًا: من المهم أن لا تثبط عزيمتك، فجميع المبرمجين بدؤوا من مكان ما، فالكثير منهم كان سيئا للغاية عندما بدأ، يصارع لفهم المفاهيم الجديدة وشعر بالإحباط، لكنهم لم يستسلموا وكانوا دائما يدفعون أنفسهم للتّعلّم وهذا ما يُميّز المبرمج الجيد من السيئ. ترجمة -وبتصرف- للمقال: WordPress Development for Beginners: Getting Started لصاحبه Daniel Pataki والمقال WordPress.org and WordPress.com من موقع wordpress.org. حقوق الصورة البارزة: Designed by Freepik.
  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. يمثّل كلٌّ من الضّغط والتّصغيرأمرًا تقوم بتنفيذه على الأصول الموجودة في موقعك (مثل ملفات css. وملفات js.). تستطيع من خلال كِلَيهما تقليص حجم الملف وبالتّالي جعلَه أكثر فعاليّةً في عبور الشبكة بين الخواديم والمتصفّحات. بكلمات أخرى، يصبح الأداء أفضل بتقليص حجم أصولك. تمثّل الشبكة نقطةَ اختناقٍ لسرعة الويب. لهذا السّبب، يساعد تخفيض حجم الملف وتقليصه في تفادي تحميل عبء زائد على الشّبكة. لكنّهما - أي الضغط والتصغير - يختلفان عن بعضهما بوضوح. إن كنت لا تعرف هذا بالفعل، فالأمر يستحق الاطّلاع. يقوم التّصغير بأمورٍ مثل حذف الفواصل، وحذف التّعليقات، وحذف الفواصل المنقوطة غير الضّروريّة وتقليل طول الرّموز السّت عشريّة وغيرها من الأمور المشابهة. يبقى الملف شيفرةً صالحةً تمامًا. لن ترغبَ في محاولة قراءتها أو العمل عليها، لكنها لا تخرق أيًّا من القواعد. يستطيع المتصفّح قراءتها واستخدامها كما هو الحال في الملف الأصليّ. يُنشئ التّصغير ملفًا جديدًا تقومُ أنتَ باستخدامه في نهاية المطاف. على سبيل المثال، تستطيع إنشاء ملف style.css لتعمل عليه، ومن ثمّ بإمكانك تصغيره إلى style.min.css. يتحرّى الضّغط جميع المقاطع المكرّرة ويستبدلها بمؤشّرات إلى موضع الورود الأول للمقطع المعنيّ. قدّمت جوليا إيفانز Julia Evans طريقةً رائعة لفهم ما سبق (راجع منشورها والفيديو). إليك الفقرة الأولى من القصيدة: .red { color: red; } Once upon a midnight dreary, while I {pon}dered weak an{d wea}{ry,} Over many{ a }quaint{ and }curious volume of forgotten lore, W{hile I }nodded, n{ear}ly napping, su{dde}n{ly }th{ere} ca{me }a t{apping,} As{ of }so{me o}ne gent{ly }r{apping, }{rapping} at my chamb{er }door. `'Tis{ some }visitor,'{ I }mu{tte}r{ed, }`t{apping at my chamber door} - O{nly th}is,{ and }no{thi}{ng }m{ore}. وَجد gzip أن النّصّ الوارد ضمن أقواس متعرّجة هو نصٌّ مكرّر. لهذا السبب، سيُستبدل بمؤشّر يستهلك مساحةً أقل مما يستهلكه النّصّ نفسه. تظهر فعاليّة هذا الأمر في إنقاص حجم الملف، خاصّةً في حالة الشّيفرة البرمجيّة على اعتبار أنّها تحتوي بطبيعتها على الكثير من المقاطع المكرّرة. تخيّل فقط عدد مرّات ورود ‎<div في ملف HTML أو عدد مرّات ورود { في ملف CSS. يمكنك إنشاء إصدارات مضغوطة من الملفات، على سبيل المثال style.css.zip لكنّك نادرًا ما ستضطّر لذلك ولن يعرف المتصفّح ماذا يفعل بهذه الإصدارات. تتّم عمليّة الضّغط على الويب من قبل الخادم مباشرةً ذلك إذا ما قمت بإعداده للقيام بها. حالما تُطبّق الإعدادات، يحدث الضّغط تلقائيًّا ولا حاجةَ بك للقيام بأي عمل آخر. يضغط الخادم الملف ويرسله عبر الشبكة مضغوطًا. يستقبل المتصفح الملف ويفكّ ضغطه قبل استخدامه. لم أسمع قطُّ أحدًا يذكر أعباء عمليّتَي الضّغط وفكّ الضّغط، لذا سأفترض أنّها مهملةٌ وأنّ فوائدها تفوقُ أعباءها بكثيرٍ. يجري عادةً أتمتة عملية الضغط عبر أدوات البناء مثل Gulp مما يسهل عليك العملية. إليك كيفيّة تفعيل عمليّة الضّغط على خادم Apache من خلال استخدام الوحدة mod_deflate . كما يّقدّم H5BP إعدادات الخواديم لجميع الخواديم الشّائعة التي تدعم الضّغط. مثال سنستخدم ملف CSS من Bootstrap على اعتباره مرجعًا شائعًا. ستُوفّر ما يقارب 17% عند تصغير ملف CSS، أو 85% عند ضغطه أو 86% عند قيامك بكليهما. الوضع المثاليّ عند التأكد من عمل كلِّ شيءٍ كما ينبغي من خلال أدوات المطور (DevTools) موضّح كما يلي: الضّغط أكثر فعاليّة، لكنّ القيام بكليهما هو الأفضل بالمطلق. تُخفّض عمليّة الضّغط حجم الملف أكثر بخمسة أضعاف من عمليّة التّصغير. لكنّك تحصل على دفعة صغيرة إضافيّة عند القيام بالتّصغير أيضًا. هناك أيضًا دليلٌ على أنّ المتصفّحات تقرأ وتحلّل الملف المُصغّر أسرع: شرَعَت مايكروسوفت هي الأخرى بتحسين مُحلّلاتها لهذا الأمر: يندرج التّخزين المؤقّت للأصول في هذا السّياق أيضًا حيث لا يوجد ما هو أسرع من مُتصفّح لا يحتاج إلى طلب الأصول على الإطلاق. يوجد الكثير من المعلومات حول هذا الموضوع على الوِيب (أو في الكتب)، وقد نقوم بنشر مقال حول هذا الموضوع قريبًا. ترجمة -وبتصرف- للمقال The Difference Between Minification and Gzipping لصاحبه Chris Coyier
  7. نشرح في هذا الفيديو طريقة تثبيت TypeScript وإعدادها لتستطيع الاستفادة منها في تطوير تطبيقاتك. يمكنك الاستعانة بتوثيق TypeScript في موسوعة حسوب المتاح باللغة العربية.
  8. مرّ معنا أثناء إضافة أرقام الصفحات كيف أضفنا ملف CSS جديد عن طريق وضع بضعة أسطر في ملف functions.php في ملفات القالب. سنتناول في هذا الدرس الشرح التفصيلي لهذه الآلية، وهي الطريقة الآمنة لإضافة ملفات JavaScript و CSS. فهرس السلسلة: مقدمة إلى تطوير قوالب ووردبريس: تحويل صفحة HTML إلى قالب ووردبريس التصفيح (Pagination) في قوالب ووردبريس إضافة قوائم التنقل (Navigation Menu) إلى قالب ووردبريس صف وتسجيل ملفات Javascript و CSS في قوالب ووردبريس (هذا الدرس) ما المقصود بالصف؟ هو وضع الملف في صفّ/دور/طابور (queue) لتقوم ووردبريس بمعالجته لاحقاً. تخيل أنك تضع الملف في دور/طابور شراء جهاز آي فون جديد مثلاً! وعندما يحين موعد عرض الملفات، تقوم ووردبريس بمعالجة الصفّ ومتطلبات كل ملفّ فيه، ثم إعادة ترتيب الصف حسب المتطلبات، وأخيراً عرض الملفات في مكانها المناسب مع متطلباتها. الخطوات العامة سنعرض الآن الخطوات بشكل عام، ثم تفصيلها وطريقة استخدامها في الفقرة اللاحقة. لصفّ ملفٍّ ما، سواء كان ملف JavaScript أو CSS نحتاج إلى: استخدام الحدث (action) المناسب. تسجيل الملف المراد استخدامه؛ حيث يجب استخدام معرّف (handle) للملف، مسار الملف، ويمكن تحديد متطلباته (dependencies) إن وُجدت. صفّ الملف (enqueue)؛ باستخدام المعرّف المُستخدم أثناء تسجيل الملف. تسجيل وصف ملفات CSS تسجيل ملف CSS لتسجيل ملفٍّ جديد نقوم باستخدام دالّة wp_register_style، يمكن للدالّة أن تقبل المحدّدات التالية: handle$: مطلوب، هو المعرّف الخاص بالملف، الذي سيتم استخدامه عند صفّ الملف (enqueue). src$: مطلوب، هو رابط (URL) ملف CSS المطلوب تسجيله، مثل:http://example.com/css/mystyle.css، لكن يجب ألا يتم استخدام الرابط بهذا الشكل، بل يجب أن يكون أكثر مرونة (التفصيل في الملاحظة بعد نهاية الفقرة). deps$: مصفوفة من المعرّفات، التي تمثّل متطلبات الملف الذي نقوم بتسجيله، كي يتم صفّها قبل صفّ الملف المُسجَّل. القيمة الافتراضية: مصفوفة فارغة ()array. ver$: إصدار الملف المُسجَّل، تقوم ووردبريس بوضعه كرقم بعد رابط الملف، على الشكل:custom.css?ver=123، إن لم يتم وضع قيمة لهذا المحدّد، فسيتم وضع إصدار ووردبريس الحالي بدلاً منه، لعدم وضع أي رقم نضع قيمة المحدّد null. القيمة الافتراضية:false. media$: قيمة حقل media الذي سيتم استخدامه مع وسم <link> أثناء صفّ الملف، القيمة الممكنة: all، screen، handheld، print. القيمة الافتراضية هي all. ملاحظة هامة: عند تسجيل أو صفّ الملفات، يجب أن تكون الروابط مرنة، أي أن يتم استبدال اسم الموقع/النطاق عن طريق دوالّ ووردبريس. مثال خاطئ: add_action( 'wp_enqueue_scripts', 'register_invalid_style' ); function register_invalid_style() { wp_register_style( 'my-invalid-style', 'http://localhost/wp-content/themes/my-theme/css/custom.css' ); } هل لاحظتم أنني وضعت المسار كاملاً؟ ترى هل سيعمل الرابط السابق إن قمنا باستخدام القالب على موقع على الإنترنت بدلاً من الموقع المحلّي؟ بالتأكيد لا! مثال صحيح: add_action( 'wp_enqueue_scripts', 'register_valid_style' ); function register_valid_style() { wp_register_style( 'my-valid-style', get_template_directory_uri() . '/css/custom.css' ); } تكون النتيجة في المتصفح مشابهة للتالي: <link rel='stylesheet' id='my-valid-style-css' href='http://localhost:8000/wp-content/themes/my-theme/css/custom.css?ver=4.2' type='text/css' media='all' /> تقوم دالّة ()get_template_directory_uri بإرجاع رابط القالب الفعّال (active)، مثلاً: http://example.com/wp-content/themes/my-theme، بحيث يكون اسم النطاق حسب الموقع الحالي، ثم يقوم المطوّر بإضافة مسار الملّف الذي يريده بعد رابط القالب الفعّال. إن أردنا تسجيل وصفّ الملفات ضمن الإضافات بدلاً من القوالب، نقوم باستخدام دالّة ()plugins_url بدلاً من الدالّة السابقة الخاصة بالقوالب. صف ملف CSS لصفّ ملف CSS نستخدم دالّة wp_enqueue_style، محدّدات الدالّة هي نفسها محدّدات دالّة wp_register_style، باستثناء: محدّد handle$ هو المحدد الوحيد المطلوب في حال استخدامنا لمعرّف ملف مُسجّل مسبقاً. محدد src$ غير مطلوب في حال نقوم باستخدام معرّف لملف مُسجّل مسبقاً، ومطلوب إن كنا نريد استخدام الدالّة لصفّ ملفّ غير مسجّل. فعوضاً عن تسجيل الملف بدالّة منفصلة ثم صفّه بدالّة أخرى، نقوم بصفّه مباشرة في هذه الدالّة. مثال عن صفّ ملف مسجّل مسبقاً: add_action( 'wp_enqueue_scripts', 'enqueue_style' ); function enqueue_style() { wp_enqueue_style( 'my-valid-style' ); } مثال عن صفّ ملف جديد دون تسجيل: add_action( 'wp_enqueue_scripts', 'register_enqueue_style' ); function register_enqueue_style() { wp_enqueue_style( 'my-valid-style', get_template_directory_uri() . 'my-theme/css/custom.css' ); } نلاحظ أننا في المثال الثاني استخدمنا دالّة wp_enqueue_style بشكل مماثل لدالّة wp_register_style. الفرق الرئيسي بين الطريقتين، أن الأولى تسمح لنا باستخدام الملف المُسجل في عدة أماكن، وتتيح مرونة أكبر بالتعامل مع الملفات. إلغاء صفّ أو إلغاء تسجيل ملف CSS قد نحتاج لإلغاء صفّ ملف، أو إلغاء تسجيله (كما سنرى في نهاية المقال)، تتيح ووردبريس دالّتين لهذين الغرضين هما: wp_dequeue_style لإلغاء صفّ ملف و wp_deregister_style لإلغاء تسجيل ملف. في كلا الدالّتين نقوم بتمرير محدّد واحد هو المعرّف الخاص بالملف الذي نريد إلغاء صفّه أو إلغاء تسجيله، لإلغاء صفّ إطار عمل Bootstrap مثلاً، نضع الأسطر التالية في ملف functions.php: add_action( 'wp_enqueue_scripts', 'dequeue_bootstrap' ); function dequeue_bootstrap() { wp_dequeue_style( 'bootstrap' ); } تسجيل وصفّ ملفات جافاسكريبت آلية تسجيل وصفّ ملفات جافاسكريبت هي مماثلة جداً للتعامل مع ملفات CSS، مع بعض الفروقات البسيطة التي سنستعرضها الآن. تسجيل ملف جافاسكريبت نقوم باستخدام دالّة wp_register_script، التي تقبل المحدّدات التالية: handle$: مطلوب، هو المعرّف الخاص بالملف، الذي سيتم استخدامه عند صفّ الملف (enqueue). src$: مطلوب، هو رابط (URL) ملف جافاسكريبت المطلوب تسجيله، مثل:http://example.com/js/myscript.js، لكن يجب ألا يتم استخدام الرابط بهذا الشكل، بل يجب أن يكون مرناً باستخدام ()get_template_directory_uri. deps$: مصفوفة من المعرّفات، التي تمثّل متطلبات الملف الذي نقوم بتسجيله، كي يتم صفّها قبل صفّ الملف المُسجَّل. القيمة الافتراضية: مصفوفة فارغة ()array. ver$: إصدار الملف المُسجَّل، تقوم ووردبريس بوضعه كرقم بعد رابط الملف، على الشكل:custom.js?ver=123، إن لم يتم وضع قيمة لهذا المحدّد، فسيتم وضع إصدار ووردبريس الحالي بدلاً منه، لعدم وضع أي رقم نضع قيمة المحدّد null. القيمة الافتراضية: false. in_footer$: بشكل افتراضي يتم صفّ ملفات جافاسكريبت وملفات CSS ضمن وسم <head>، لكن يمكن بوضع قيمة هذا المحدد true أن يتم صفّ ملفات جافاسكريبت في نهاية المستند، قبل إغلاق وسم <body/>، وهو الأفضل للأداء بالنسبة لزوار الموقع. القيمة الافتراضية: false. ملاحظة: صفّ ملفّات جافاسكريبت و CSS يتطلب وجود خطّاف ()wp_head ضمن القالب، وصفّ ملفات جافاسكريبت مع محدّد in_footer$ بقيمة true يتطلب وجود خطّاف ()wp_footer في القالب، قبل إغلاق وسم <body/>. صفّ ملف جافاسكريبت الاستخدام مشابه تماماً لصفّ ملف CSS، لكنه يتم عن طريق دالّة wp_enqueue_script، والتي تشابه بمحدداتها دالّة التسجيل wp_register_script. الفرق بين محددات دالة الصفّ ودالّة التسجيل الخاصة بملفات جافاسكريبت هي كالفرق بين محددات دالة الصف والتسجيل الخاصة بملفات CSS. محدّدات دالّة wp_enqueue_script هي نفسها محدّدات دالّة wp_register_script، باستثناء: محدّد handle$ هو المحدد الوحيد المطلوب في حال نقوم باستخدام معرّف لملف مُسجّل مسبقاً. محدد src$ غير مطلوب في حال نقوم باستخدام معرّف لملف مُسجّل مسبقاً، ومطلوب إن كنا نريد استخدام الدالّة صفّ ملفّ غير مسجّل. فعوضاً عن تسجيل الملف بدالّة منفصلة ثم صفّه بدالّة أخرى، نقوم بصفّه مباشرة في هذه الدالّة. إلغاء صفّ أو إلغاء تسجيل ملف جافاسكريبت طريقة إلغاء صفّ أو إلغاء تسجيل ملف جافاسكريبت هي مشابه للطريقة في ملفات CSS، لكن باستخدام دالّتي: wp_deregister_script و wp_dequeue_script. أمثلة وحالات استخدام بالمثال يتضح المقال، سنمرّ معاً على أربعة أمثلة وحالات استخدام لنرى من خلالها كيف يمكننا التعامل ثم الاستفادة من تسجيل وصفّ ملفات JavaScript و CSS: 1. عند استخدام إضافة رديئة الجودة لنفرض لسبب ما أنك تستخدم إضافة رديئة -لا تتبع المعايير ولا تستخدم أحد الإصدارات من المكتبات-، تتطلب هذه الإضافة وجود إصدارٍ قديم من مكتبة jQuery، بينما قالبك يستخدم الإصدار اﻷحدث منها. هل من المنطقي وجود نسختين من المكتبة في القالب؟ بالتأكيد لا. لحلّ هذه المشكلة نحن أمام ثلاثة خيارات: إن كانت الإضافة ليست رديئة الجودة كثيراً، وتقوم بصفّ مكتبة jQuery، فهذا شيء جيّد، يمكننا ببساطة إلغاء المكتبة من الصفّ وتنتهي المشكلة. إن كانت الإضافة رديئة كما وصفناها ولا تقوم بصفّ مكتبة jQuery، عندها يجب على المطوّر أن يقوم بالتعديل على ملفات الإضافة يدوياً لإلغاء تحميل مكتبة jQuery. وهناك احتمال كبير أن المطور سينسى التعديل الذي قام به، ومع مرور الأيام يقوم بتحديث الإضافة إلى إصدار جديد ويذهب التحديث اليدويّ الذي قام به! أو إن كان ذو ذاكرة قوية، سيقوم بالقيام بالتعديل اليدوي ذاته في كل مرة يظهر إصدار جديد من الإضافة. لكم أن تتخيلوا المعاناة التي ستصبح على كاهل المطوّر. الخيار الثالث والأسرع هو القيام بحذف هذه الإضافة رديئة الجودة والبحث عن واحدة أفضل منها تتبع المعايير والقواعد وتستخدم أحد الإصدارات من ملفات JavaScript و CSS. الخيار الثالث هو الأفضل لتقليل استخدام مسكنات ألم الرأس. من المهم اتباع المعايير والقواعد المتفق عليها حتى لا يقع المطوّر في الحُفر التي وُضعت تلك المعايير والقواعد من أجل تلافيها. 2. استخدام المكتبات الموجودة في ووردبريس ربّما حدّثتك نفسك في أحد الأيام أن تستعرض ملفات ووردبريس وترى محتواها، إنْ حدث ذلك فلا بدّ أنك رأيت الكثير من مكتبات جافاسكريبت مثل jQuery، jQuery UI، Backbone وغيرها. إن كانت هذه الملفات موجودة ضمن ووردبريس، فلمَ لا نقوم باستخدامها عند الحاجة إليها؟ لو كان القالب يحتاج إلى مكتبتيّ jQuery و jQuery UI فبدلاً من تحميل نسخة من كل مكتبة من الإنترنت ثم وضعها ضمن ملفات القالب واستخدامها، يمكننا بشكل مباشر استخدام نسخة jQuery و jQuery UI الموجودتان ضمن ووردبريس. بهذا نضمن الحصول على إصدار حديث من المكتبة يأتي مع كل تحديث لووردبريس بالإضافة لعدم التكرار (Don’t Repeat Yourself). من المكتبات الشهيرة المضمّنة في ووردبريس: jQuery jQuery UI Backbone jQuery Suggest Thickbox TinyMCE Underscore للاطلاع على كامل القائمة يمكن زيارة صفحة التوثيق. 3. استخدام jQuery بشكل مباشر من شبكة توصيل المحتوى (CDN) لا بدّ أنك سمعت بشبكة توصيل المحتوى (Content Delivery Network). تعريفها على ويكبيديا: هي مجموعة من الخوادم المتزامنة والموزعة والموجودة على الشبكة في أماكن جغرافية مختلفة، تحتوي على نسخ من البيانات. فالعميل يحصل على البيانات من الخادم الموجود في أقرب موقع جغرافي، بغرض تقليل التأخير الناتج في نقل البيانات. هناك موقع مخصص لاستخدام مكتبات JavaScript عن طريق شبكات توصيل المحتوى هو jsDelivr، سنقوم باستخدام رابط مكتبة jQuery منه (//cdn.jsdelivr.net/jquery/2.1.3/jquery.min.js) لنقوم بصفّها واستخدامها ضمن القالب، عوضاً عن استخدام النسخة المتضمنة في ملفات ووردبريس. للقيام بهذا نحتاج لوضع الأسطر القليلة التالي في ملف functions.php الخاص بقالبنا: add_action( 'wp_enqueue_scripts', 'register_jquery' ); function register_jquery() { wp_deregister_script( 'jquery' ); wp_register_script( 'jquery', ( '//cdn.jsdelivr.net/jquery/2.1.3/jquery.min.js' ), false, null, true ); wp_enqueue_script( 'jquery' ); } قمنا بإلغاء تسجيل jQuery (كانت مسجلة مع الملف المتضمَّن في ووردبريس)، ثم قمنا بتسجيلها مع رابط الملف من شبكة توصيل المحتوى (CDN)، وأخيراً قمنا بصفّها (enqueue) ليتم إدراجها في القالب. 4. صفّ ملف جافاسكريبت يعتمد على jQuery في معظم الحالات نحتاج في القوالب لإضافة جافاسكريبت، سواء لإضافة حركات معيّنة أو لتعديل شيءٍ ما، وبسبب شهرة مكتبة jQuery فمعظم المطورين يعتمدون عليها كقاعدة أساسية لبناء ملفات جافاسكريبت الخاصة بقوالبهم. على فرض أن الملف الذي نريد إضافته يعتمد على مكتبة jQuery وهو موجود مع ملفات القالب في المسار: js/custom.js، لصفّ هذا الملف نقوم بإضافة الأسطر التالية إلى ملفfunctions.php: add_action( 'wp_enqueue_scripts', 'enqueue_custom_js' ); function enqueue_custom_js() { wp_register_script( 'my-custom-js', get_template_directory_uri() . '/js/custom.js', ['jquery'] ); wp_enqueue_script( 'my-custom-js' ); } قمنا بتسجيل الملف الذي نريد صفّه، ولنلاحظ كيف حدّدنا متطلبات الملف ضمن مصفوفة، يعتمد الملف على مكتبة jQuery فقط. ثم قمنا بصفّه باستخدام المعرّف الذي استخدمناه أثناء تسجيل الملف. تكون النتيجة في المتصفح مشابهة للتالي: <script type="text/javascript" src="//cdn.jsdelivr.net/jquery/2.1.3/jquery.min.js?ver=4.2"></script> <script type="text/javascript" src="http://localhost:8000/wp-content/themes/my-theme/js/custom.js?ver=4.2"></script> ونلاحظ أن ووردبريس قامت بصفّ مكتبة jQuery قبل الملف الذي قمنا بتسجيله، وذلك كي يقوم المتصفح بقراءة ملف المكتبة في البداية وتكون متوفرة للاستخدام، وعند قراءة المتصفح للملف الخاص يمكن للملف استخدام مكتبة jQuery بعد أن أصبحت متوفرة. تمرير متغيّرات من PHP للجافاسكريبت ماذا لو أردنا استخدام متغيّرات ما ضمن جافاسكريبت؟ قد يتهيؤ للبعض أن يقوم بعمل طلب AJAX أو وضع ما يريد استخدامه في جافاسكريبت بداخل ملف خارجي. قد تعمل هذه الحلول، لكنها لن تجدي نفعاً إن أردنا تمرير متغيّرات تتبدّل قيمتها باستمرار كأن تكون من قاعدة البيانات مثلاً. توفّر ووردبريس حلّاً سهلاً ومناسباً لهذه المشكلة، وذلك باستخدام دالّة wp_localize_script، اسم الدالّة قد يوحي أنها مخصصة للترجمة، لكن يمكن استخدامها لتمرير جمل الترجمة وأي نوع آخر من المتغيّرات إلى جافاسكريبت. محددات الدالّة هي: - handle$: معرّف لملف جافاسكريبت الذي نريد تمرير المتغيّرات له، يجب أن يكون الملف مسجّلاً قبل استخدام الدالّة. - name$: اسم متغيّر جافاسكريبت الذي سيتم وضع البيانات بداخله. - data$: مصفوفة المتغيّرات التي نريد تمريرها إلى جافاسكريب. مثال: لنقم بتمرير متغيّرين هما سلسلة نصية ورقم إلى ملف جافاسكريبت ذو المحدد my-custom-js: add_action( 'wp_enqueue_scripts', 'enqueue_custom_js' ); function enqueue_custom_js() { wp_register_script( 'my-custom-js', get_template_directory_uri() . '/js/custom.js', ['jquery'] ); $translation_array = array( 'some_string' => 'A String to be using inside JS', 'a_value' => '10' ); wp_localize_script( 'my-custom-js', 'object_name', $translation_array ); wp_enqueue_script( 'my-custom-js' ); } كي نصل إلى المتغيّرات من داخل ملف custom.js، نستخدم شيئاً مشابهاً: alert( object_name.some_string); يجب أن تظهر رسالة تنبيه (Alert) بداخلها النصّ الذي استخدمناه. صفّ الملفات في لوحة التحكم كل ما مرّ معنا من تسجيل وصفّ الملفات هو خاص بواجهة الموقع (Front-end)، أي الذي يراه الزوار. إن أردنا تسجيل وصفّ الملفات في لوحة التحكم (Dashboard) يمكننا ذلك بنفس الطريقة، لكن باستبدال حدث wp_enqueue_scripts بحدث: admin_enqueue_scripts. مثلاً لصفّ مكتبة jQuery Suggest في لوحة التحكم (المكتبة معرّفة مسبقاً في ووردبريس)، نستخدم الأسطر التالية: add_action( 'admin_enqueue_scripts' , 'enqueue_jquery_suggest' ); function enqueue_jquery_suggest() { wp_enqueue_script( 'suggest' ); } ملاحظة: من المناسب وضع شروط معيّنة قبل صفّ الملفات وقصرها على صفحاتٍ معينة، كي لا يتم وضع الملف في كل صفحات لوحة التحكم. الخاتمة تعرّفنا على كيفية صفّ ملفات JavaScript و CSS، هذه الآلية تسهّل كثيراً تنظيم الملفات والتعامل معها، ويجب الحرص على استخدامها بشكل دائم، فهي من المعايير والأشياء المتعارف عليها في تطوير قوالب وإضافات ووردبريس. أرجو أن يكون الشرح واضحاً ومفيداً، إن كان لديكم سؤال أو فكرة فلتشاركونا إياها في التعليقات.
  9. تقدّمت منذ فترة بطلب إلى مطوّري إطار عمل Laravel لإضافة مكتبة JavaScript مشابهة لتلك الموجودة في إطار العمل Ruby on Rails. إذ تقدّم المكتبة المتوفرة في إطار Rails مجموعة من الخصائص المفيدة التي يمكن إضافتها إلى شيفرة HTML للحصول على المزيد من الوظائف التي تسهّل على المطوّر أداء الكثير من المهامّ الشائعة. أجري نقاش حول هذا الموضوع وتمخّض النقاش عن الاقتراح التالي: إما أن يصار إلى إنشاء حزمة Composer من طرف ثالث، أو أن المكتبة التابعة لإطار العمل Rails صالحة للاستخدام في Laravel. وفقًا للمقترح السابق قرّرت استخدام مكتبة UJS التابعة لـ Rails في أحد مشاريعي ورحت أتفحّص الطرق التي يمكن من خلالها استخدام نفس الخصائص في الإصدار الرابع من Laravel. ولسوء الحظ لا يمتلك Laravel نفس التوابع المساعدة الموجودة في Rails والمسؤولة عن أداء وظائف معيّنة، ولكن لا زال بالإمكان تفعيل هذه الوظائف في المكان الذي ترغب فيه باستخدام الخواص الملائمة، ومن المؤكّد أن هذه الوظائف ستعمل حسب المطلوب في أي إطار عمل آخر. البداية بعد أن تربط مكتبتي jQuery و Rails UJS مع ملف HTML ستحتاج إلى إجراء بعض التعديلات السريعة. سنحتاج أوّلًا إلى إعداد مكتبة UJS للعمل مع رمز CSRF على فرض أنّك تستخدم هذه الرموز في تطبيقاتك (أنصحك باستخدامها)، لذا سنضع وسمي <meta> في بداية المستند: <meta name="csrf-param" content="_token"> <meta name="csrf-token" content="{{ csrf_token() }}"> يخبر الوسم الأول مكتبة UJS بالاسم الذي يجب استخدامه لرمز CSRF عند إنشاء الطلب، أما الوسم الثاني فيخبر المكتبة بوسم CSRF الذي يجب استخدامه. يستخدم Laravel افتراضيًا token_ كاسم للمعامل (ألق نظرة على ملف filters.php للمزيد من المعلومات)، أما التابع ()csrf_token فيضيف رمز الحماية إلى الطلب. خاصية data-method هذه الخاصية مفيدة جدًّا عند استخدام للروابط، إذ تخطف مكتبة UJS النقرة على الرابط وتنشئ طلبًا باستخدام فعل Http الذي نقدّمه إلى الخاصية. مثال: <a href="posts/1" data-method="delete" rel="nofollow">Delete this post</a> عند النقر على الرابط، يتم إنشاء طلب من نوع DELETE بدلًا من الطلب الاعتيادي GET والذي ينشأ من مثل هذه الروابط، وبهذا يمكنك إجراء أحداث بنمط RESTful في تطبيقك دون الحاجة إلى الاستمارات. إن كنت ترغب في إنشاء روابط مشابهة لهذا الرابط في Laravel فإليك المثال التالي والذي نستخدم فيه دالة link المساعدة: {{ link_to_route('posts.destroy', 'Delete this post', $post->id, ['data-method' => 'delete', 'rel' => 'nofollow']) }} جدير بالذكر أنّه يجب إضافة الخاصية rel="nofollow" عند استخدام هذه الدالة المساعدة لضمان عدم فهرسة محركات البحث لهذا الرابط. خاصية data-confirm هذه الخاصية مفيدة للتحقّق من الموافقة على إجراء الحدث قبل الاستمرار، وتتم عملية التحقّق هذه بواسطة مربع التنبيه الذي يظهر بواسطة الدالة confirm() في JavaScript، ويمكن استبدال مربّع التنبيه هذا بآخر ذي شكل أجمل حسب الرغبة. تعمل هذه الخاصية بصورة جيّدة مع المثال السابق، فإن كان الرابط أو الزرّ يؤدّي إلى حذف شيء ما فيستحسن حينئذٍ التحقّق من الرغبة في القيام بذلك قبل إجراء عملية الحذف. <a href="posts/1" data-method="delete" data-confirm="Are you sure you want to delete this post?" rel="nofollow">Delete this post</a> إن ألغى المستخدم صندوق التأكيد هذا لن يتم تنفيذ الحدث، والعكس بالعكس. جدير بالذكر أنّك لست ملزمًا باستخدام هذه الطريقة مع خاصية data-method بل يمكن استخدامها مع أي حدث ترغب في التحقق منه قبل إجراءه. خاصية data-disable قد ترغب أحيانًا في تعطيل زرّ معين بعد النقر عليه مباشرة لإتاحة الفرصة لاكتمال الطلب وفي حال فشل الطلب تعيد تفعيل الزرّ مرة أخرى. تتيح لك مكتبة UJS القيام بهذا الأمر بواسطة خاصّية data-disable. <input type="submit" value="Save post" data-disable> هكذا سيتم تعطيل الزر بعد النقر عليه وستمنع المستخدم من النقر المستمر على الزر وإنشاء 10 سجلات جديدة في قاعدة البيانات. بالنسبة لمستخدمي Laravel يمكن الاستفادة من هذه الخاصّية باستخدام منشئ الاستمارات form builder. {{ Form::submit('Save post', ['data-disable']) }} خاصّية data-disable-with يمكن التوسع في المثال السابق وذلك بتقديم بعض المعلومات المفيدة للمستخدم، فعند تعطيل الزرّ يظهر نصّ يخبر المستخدم بأنّ الطلب في طور الإنشاء في الخلفية. <input type="submit" value="Save post" data-disable-with="Saving this post..."> والآن عندما يكون الزرّ معطلًا، يتم استبدال النص بعبارة تدلّ على أن النقر على الزرّ قد أدى إلى حدوث شيء ما في الخلفية. خاصّية data-remote عند إضافة الخاصّية data-remote إلى الاستمارة، تقوم مكتبة UJS بإرسال الاستمارة كطلب Ajax. وستأخذ المكتبة كل شيء بالحسبان (طريقة الإرسال، التحقق من الموافقة على الإجراء… الخ) قبل إرسال الاستمارة. <form action="posts" method="post" data-remote> <!-- Form goes here. --> </form> ويمكن التعامل مع الاستجابة الواردة بعد نجاح الطلب بكل سهولة، فإن استجاب المتصفّح بواسطة JavaScript فستنفّذ الاستجابة فور اكتمال الطلب. على سبيل المثال، قد تختار إعادة توجيه المستخدم بعد تنفيذ طلب للحذف: window.location.href = '{{ route('posts.index') }}'; أو لنقل مثلًا أنّك تستخدم رابطًا لحذف مقالة من صفحة تتضمن قائمة بالمقالات المتوفّرة في المدونة، وترغب في إخفاء المقالة بعد حذفها مباشرة. <a href="{{ route('posts.destroy', $post->id) }}" data-method="destroy" data-confirm="Delete this post?" rel="nofollow" data-remote>Delete this post</a> يمكن استخدام الشيفرة التالية لإخفاء المقالة: $('#post-{{ $post->id }}').fadeOut(); وهكذا، تُحذف المقالة من قاعدة البيانات والعرض في آن معًا. خاصّية data-type: إن استخدمت الخاصّية السابقة data-remote يمكنك كذلك استخدام خاصية data-type لتحديد هيئة البيانات التي يتم إرسالها إلى الخادوم. فعلى سبيل المثال يمكن إرسال الاستمارة على هيئة Json: <form data-type="json"> الأحداث البعيدة عند استخدام خاصّية data-remote يمكن الوصول إلى الأحداث المختلفة في طلب Ajax لتأدية بعض المهامّ الإضافية. إذ تطلق مكتبة UJS عددًا من الأحداث المساعدة خلال دورة حياة الطلب والتي من شأنها أن تساعد في بناء تطبيقات أكثر تعقيدًا. ajax:before ينطلق هذا الحدث قبل حدوث أي شيء، وإيقاف هذا الحدث يؤدي إلى إلغاء الطلب. ajax:beforeSend: ينطلق هذا الحدث قبل إرسال طلب Ajax مباشرة، وإيقاف الحدث يؤدي إلى إلغاء الطلب. ajax:send: ينطلق عند إرسال طلب Ajax. ajax:success: ينطلق بعد اكتمال الطلب بنجاح. ajax:error: ينطلق بعد فشل إرسال الطلب. ajax:aborted:required: ينطلق في حال وجود حقول إلزامية فارغة في الاستمارة، وفي حال إيقاف الحدث يتم إرسال الاستمارة. ajax:aborted:file: ينطلق في حال وجود حقول ملفات غير فارغة في الاستمارة، ويتم تجاهل الطلب في حال توقّف الحدث. آمل أن يكون هذا الموضوع مفيدًا لكل من يرغب في استخدام مكتبة UJS التابعة لإطار العمل Ruby on Rails مع إطارات العمل الأخرى مثل Laravel، إذ أرى أن هذه المكتبة مفيدة ولا يجوز أن تكون حكرًا على مستخدمي إطار Rails. ترجمة - وبتصرّف - للمقال Using Rails UJS in Laravel 4 (or any other framework) لصاحبه Dwight Conrad Watson.
  10. ما هي JSON؟ كلمة JSON و تلفظ جيسون هي اختصار ل JavaScript Object Notation. سميت بهذا الاسم لأن لغة جافا سكربت(javascript) كانت أول لغة تستفيد من هذه الصيغة. JSON هي طريقة لتخزين المصفوفات (arrays) و الكائنات (objects) بقيم نصية (string)، و تستخدم بشكل أساسي في نقل البيانات وأقل عرضة للخطر و أسهل من طرق أخرى كطريقة xml. تستخدم عادةً عندما يطلب جزء من تطبيق الويب بعض المعلومات من الخادم (server) من دون إعادة تحميل الصفحة. عادة تتم هذه العملية عن طريق جافا سكربت وطلبات AJAX. منذ النسخة 5.2.0، لغة PHP أصبحت قادرة على تحويل الكائنات والمصفوفات إلى JSON. وقد كانت إضافة رائعة لهذه اللغة. إذا كنت تعمل مع PHP لفترة من الزمن ففي الغالب أنك قمت باستعمال دالة ()serialize لعرض كائنات PHP كنصوص، و تستخدم لتحويل كائنات PHP إلى نصوص، و يمكنك فيما بعد استخدام دالة ()unserialize لتحويل هذا النص إلى الحالة السابقة بالقيم الأصلية. وليس فقط لغة PHP التي يمكن أن تتعامل مع JSON بل معظم لغات البرمجة يمكنها التعامل مع JSON وتحويل بياناتها إلى JSON. صيغة JSON {"name":"Lushui","species":"Panda","diet":"Green Things","age":7,"colours":["red","brown","white"]} افتراضيًا يتم تخزين JSON بلا أي فراغات (white space) مما يجعل قراءة JSON صعبة قليلًا، من أجل توفير الذاكرة عند نقل البيانات، لكن يمكنك أن تضع مهما كان من فراغات أو أسطر لجعلها مقروءة بالنسبة لك. لكن هل من المعقول أن نقوم بوضع فراغات في هذ الملف يدويًا إذا كان لدينا العديد من الأسطر، هناك العديد من الأدوات التي تتوفر على الإنترنت التي تقوم بتجميل ملف JSON ليصبح أكثر قابلية للقراءة. لنقم بإضافة بعض الفراغات: { "name": "Lushui", "species": "Panda", "diet": "Green Things", "age": 7, "colours": ["red", "brown", "white"] } كما ترى في هذا المثال أنه لدينا عدد من المفاتيح والقيم. كل مفتاح يجب أن تكون له قيمة مقابلة، إذا كنت قد تعاملت مع جافا سكربت من قبل فسوف ترى أن JSON لا تختلف كثيرًا عن جافا سكربت. var lushui = { name: 'Lushui', species: 'Panda', diet: 'Green Things', age: 7, colours: ['red', 'brown', 'white'] }; في جافا سكربت يتم تعيين هذا الكائن لمتغير كالتالي: var lushui = { .. }; لكن JSON هي صيغة لنقل البيانات، وليست لغة برمجة، لا تملك أي مفاهيم برمجية لذلك لا نحتاج إلى المتغيرات أو ما شابه في JSON. في جافا سكربت وفي JSON الكائنات يتم احتوائها داخل { } وكل مفتاح له قيمة، في جافا سكربت المفاتيح لا تتطلب وضعها بين إشارتي " " لأنها تمثل متغيرات. في جافا سكربت و JSON المفتاح و قيمته يفصل بينهما بـ ":" و كل مفتاح و قيمته مفصولان عن المفاتيح و القيم الآخرى بـ "," . JSON تدعم أنواع القيم التالية: Double Float String Boolean Array Object Null القيم العددية تمثل بلا إشارتي " " ، القيم المنطقية تمثل بـ true أو false و بلا إشارتي " " كما PHP ، و يجب التشديد مرة أخرى أن النصوص تمثل بإشارتي " " . القيمة null تعمل كعملها في php وتمثل في JSON بوضع الكلمة فقط غير محاطة بأي شيء. المصفوفات في JSON وجافا سكربت هي نفسها. // JavaScript ['red', 'brown', 'white'] ["red", "brown", "white"] لاحظ أنني لم أضع تعليق قبل المصفوفة الثانية، لأن JSON لا تدعم التعليق بسبب أنها تستخدم لنقل البيانات وليس هناك حاجة للتعليق لأنك لا تكتب أوامر برمجية لأنك لو عدت لملف JSON لاحقًا فكل ما فيه بيانات سوف تعرفها عند قراءتها مباشرةً. JSON تدعم المصفوفات و الكائنات المتداخلة. { "an_object": { "an_array_of_objects": [ { "The": "secret" }, { "is": "that" }, { "I": "still" }, { "love": "shoes!" } ] } } لدينا في المثال السابق كائن JSON يحتوي كائن آخر يحتوي مصفوفة من الكائنات. JSON و PHP كما ذكرت سابقًا، منذ النسخة 5.2.0 وفرت الدعم لتحويل البيانات إلى JSON والآن سوف نلقي نظرة على هذه الميزة في PHP عن قرب. لتحويل قيمة في PHP إلى JSON فقط نحتاج إلى دالة ()json_encode كما التالي: <?php $truth = array('panda' => 'Awesome!'); echo json_encode($truth); نتيجة تنفيذ التعليمات السابقة. {"panda":"Awesome!"} رائع هذا بالضبط ما كما نريده، لنقم الآن بتحويل هذه البيانات إلى مرة آخرى إلى صيغة مفهومة من قبل PHP. <?php $truth = json_decode('{"panda":"Awesome!"}'); echo $truth['panda']; لنلقي نظرة على النتيجة؟ Fatal error: Cannot use object of type stdClass as array in ... كما ترى، دالة ()json_decode لا تتحول إلى مصفوفة PHP. بدلًا من ذلك تستخدم كائن stdClass لتمثيل بياناتنا، لنصل إلى بياناتنا بشكل صحيح سندخل إلى خصائص هذا الكائن بهذه الطريقة. <?php $truth = json_decode('{"panda":"Awesome!"}'); echo $truth->panda; // Awesome! لكن إذا ما زلت ترغب في الحصول على هذه البيانات كمصفوفة فقط أضف وسيط آخر إلى هذه الدالة وهو true. <?php $truth = json_decode('{"panda":"Awesome!"}', true); echo $truth['panda']; // Awesome! ترجمة –وبتصرّف- للمقال Laravel 4 Primer: JSON لصاحبه Dayle Rees
  11. من المؤكد أنك قمت بتصفح الإنترنت من هاتف محمول، ستلاحظ في كثير من المواقع زر فتح قائمة التنقل الموجود في أعلى الموقع الذي بمجرد أن تضغط عليه يقوم بفتح قائمة تنقل الموقع. إضافة هذا الزر إلى موقع ووردبريس الخاص بك سوف تحسن تجربة المستخدم بشكل كبير للمستخدمين الذين يزورون الموقع من أجهزة الهاتف المحمول أو الأجهزة اللوحية. صحيح بأنه يمكنك استعمال إضافة ووردبريس جاهزة للقيام بهذه المهمة أو تنصيب قالب يحوي هذه الميزة ولكن ماذا لو كنت تستعمل قالب خاص بك وتريد إضافة هذه الميزة بنفسك؟ في هذه المقالة سوف أريك كيف تقوم بعمل هذا بمساعدة ووردبريس وبعض تعليمات css وjavascript وتحويل القائمة إلى الشكل المطلوب. ماذا ستحتاج لتتابع مع هذا المقال، ستحتاج إلى: موقع ووردبريس على حاسوبك الذي يحتوي قائمة. قالب ابن للقالب الذي تعمل عليه من أجل أن تبقى تعديلاتك في مكانها عندما يتم تحديث هذا القالب من الإنترنت. سأقوم بتنفيذ هذه التعليمات على موقعي الخاص، الذي يستهدف قائمة التنقل الرئيسية والتي في موقعي الخاص تملك اسم صنف css وهو menu.main ، إذا كان اسم هذا الصنف مختلفًا في موقعك فسوف تقوم بتطبيق التعليمات على اسم الصنف الخاص بقائمتك. القائمة الحالية قائمتي تبدو على أجهزة الحاسب بهذا الشكل: لكن على شاشة الهاتف ليست جميلة: يمكنني تحسين ذلك بجعل محاذاة القائمة إلى المنتصف لكن ذلك سيأخذ الكثير من مساحة الشاشة لذلك سنقوم بعمل زر القائمة الذي بمجرد الضغط عليه سوف تظهر القائمة التي سنقوم بإخفائها في أجهزة الهاتف المحمول. إضافة زر القائمة الخطوة الأولى هي إضافة أيقونة زر القائمة. سوف نقوم بذلك في ملف header.php في قالبك أضف رابط تحت قائمة التنقل الرئيسية ها هو الخاص بي: <?php wp_nav_menu( array( 'container_class' => 'main-nav', 'theme_location' => 'primary' ) ); ?> <a class="toggle-nav" href="#">☰</a> هذا الرابط سيقوم بعمل زر الذي يقوم بفتح وإغلاق زر القائمة وله اسم الصنف togglenav وبداخله الأيقونة وهي في هذه الحالة رمز html. هذا كل ما يتطلب إضافته إلى ملف header.php إذا قمت بإعادة تحميل صفحتك سترى بأن زر القائمة قد ظهر على الشاشة: إخفاء زر القائمة على الشاشات الكبيرة لنبدأ مع شاشات الحاسب ونضيف هذه التعليمات، لكن لا تنسى وضع هذه التعليمات ضمن media query المناسبة لعرض الشاشة: .toggle-nav { display: none !important; } هذا يجعل الرابط الذي يحوي الأيقونة بداخله يختفي. لقد أضفت كلمة important! حتى تبقى هذه التعليمة هي الأقوى و لا يتم الكتابة عليها من قبل التعليمات الآخرى. لقد اختفى. سوف نقوم بإخفائه أيضًا في الشاشات الأصغر من ذلك و لكن سنأتي لهذا لاحقًا. إضافة تنسيقات css لزر القائمة الأن نحن نحتاج لإضافة بعض التنسيقات إلى القائمة في شاشات الهاتف والتي تظهر عندما يضغط المستخدم زر القائمة. أولًا، في ملف css الخاص بك أضف media query المناسبة: @media screen and ( max-width: 480px) { } لقد استهدفت الشاشات التي يبلغ أكبر عرض لها 480px ولكن يمكنك اختيار الأبعاد المناسبة لقائمتك. الآن لنقم بإضافة بعض تعليمات css في media query أولًا سنقوم بإظهار الأيقونة مجددًا و نقوم بوضع تنسيقات لها لتبدو أفضل: .toggle-nav { padding: 15px; margin: 15px; display: inline-block !important; color: #8D7F68; color: #fff; transition: color linear 0.15s; } .toggle-nav:hover, .toggle-nav.active { text-decoration: none; color: #8D7F68; } الأن لنقم بتنسيق القائمة نفسها و إضافة هذه التعليمات في نفس media query : .menu.main { display: inline-block; position: relative; background: #fff; } .menu.main ul { display: none; position: absolute; top: 80%; left: 0px; padding-left: 15px; background: #fff; } .menu.main li { display: block; float: none; } والآن الخطوة الأخيرة هي إضافة javascript المسؤولة عن إظهار القائمة عندما يضغط المستخدم على زر الأيقونة. إضافة سكربت javascript هذه الخطوة تتألف من خطوتين استدعاء السكربت وإضافة التعليمات له. لنقم أولًا باستدعائه. في قالبك أضف مجلدًا جديدًا يسمى scirpts وفي داخله ملف فارغ باسم burger-menu-script.js. الآن افتح ملف functions.php في قالبك وأضف التالي إليه: function wpmu_burger_menu_scripts() { wp_enqueue_script( 'burger-menu-script', get_stylesheet_directory_uri() . '/scripts/burger-menu.js', array( 'jquery' ) ); } add_action( 'wp_enqueue_scripts', 'wpmu_burger_menu_scripts' ); /* الآن قمنا باستدعاء الملف بشكل صحيح وسنضيف التعليمات البرمجية له، افتح ملف وأضف التالي */ jQuery(document).ready(function() { jQuery('.toggle-nav').click(function(e) { jQuery('.menu.main ul').slideToggle(500); e.preventDefault(); }); }); هذه التعليمات استهدفت العنصر الذي له اسم الصنف toggle-nav. و هو في حالتنا الزر و الذي سوف يتم تنفيذ التعليمات عليه عند الضغط عليه من قبل المستخدم و ستعمل دالة slideToggle الخاصة بمكتبة jquery لإخفاء زر القائمة و إظهاره عند الضغط عليه من قبل المستخدم. والآن لنرى النتيجة: وعندما أضغط على الأيقونة تظهر القائمة: وهذا فيديو يوضح كيف تجري العملية: استخدام زر القائمة سيحسن تجربة المستخدم على جهاز الهاتف المحمول إذا اتبعت الخطوات السابقة، سيصبح لديك زر قائمة بسيط والذي سيحسن تجربة المستخدم بشكل كبير عندما يزور المستخدمون موقعك من شاشات صغيرة. ويمكنك فعل ما تشاء لتحسن مظهر هذا الزر وجعله مناسبًا لموقعك. ترجمة -وبتصرّف- للمقال How to Create a Custom Animated Burger Menu for WordPress لصاحبته Rachel McCollin حقوق الصورة البارزة محفوظة لـ Freepik
  12. هل تعاني من صعوبة في تطبيق مفاهيم البرمجة الكائنية مع لغة JavaScript؟ إذًا أنت في المكان الصحيح، ففي هذا المقال سنبدأ بمطلع لمفاهيم البرمجة الكائنية (أو الشيئية كما قد يُطلق عليها البعض)، ومن ثم مراجعة نموذج جافا سكريبت في الكائنات، وأخيرًا شرح مفاهيم البرمجة الكائنية في جافا سكريبت، لتحصل على إلمام واف والقصة الكاملة. مراجعة في جافاسكريبت JavaScript يُمكن العودة إلى المقال إعادة تقديم JavaScript لمن أساء فهمها هنا في أكاديمية حسوب للحصول على مراجعة لأساسيات لغة جافا سكريبت وأخذ فكرة عن المغيرات وأنواع الدوال وبقية الأساسيات. البرمجة الكائنية Object-oriented programming إن البرمجة الكائنية (OOP) ما هي إلا نمط برمجي يَستخدم التجريد في إنشاء نماذج/نسخ لتجسيد العالم الحقيقي، وتَستخدم البرمجة الكائنية في ذلك أساليب مُتعدّدة من هذا النمط، فهي تستخدم الوحدات module، وتعدديّة الأشكال polymorphism والتغليف encapsulation، وتجدر الإشارة إلى أن معظم لغات البرمجة تدعم مفهوم OOP أمثال اللغات البرمجية: جافا، بايثون، روبي، وطبعًا جافا سكريبت. يُعالج أو لنقل يَتصور مفهوم البرمجة الكائنية OOP البرنامج كتشكيلة من الأشياء/الكائنات المتعاونة/المترابطة بدلًا من يتصوّره كتشكيلة من الدوال (functions) أو كسرد من الأوامر. ففي مفهوم OOP، كل كائن/شيء له القدرة على استقبال الرسائل، ومعالجة البيانات، وإرسال الرسائل إلى باقي الكائنات، ويُمكن اعتبار أنه لكل كائن object كينونة خاصة به ودور/وظيفة مستقلة عن الكائن الآخر. تُعزز البرمجة الكائنية القدرة على صيانة الشيفرة البرمجية والمرونة في التطوير، وأثبتت جدارتها على نطاق واسع في هندسة البرمجيات الكبيرة، ولأن البرمجة الكائنية تُشدد على استخدام الوحدات module، فإن الشيفرة/الكود المكتوب بمفهوم البرمجة الكائنية هو أبسط في التطوير وأسهل في الفهم مستقبلًا (عند التنقيح والتعديل)، وكما يعزز مفهوم البرمجة الكائنية التحليل المباشر للشيفرة، وفهم الحالات الشائكة فهمًا أفضل من باقي الأساليب البرمجية الأخرى. مصطلحات البرمجة الكائنية المجال في البرمجة الكائنية Namespace ما هو إلا عبارة عن حاوي تسمح للمطوّر بتحزيم جميع الوظائف تحت اسم محدد وفريد. الصنف أو الفئة Class في البرمجة الكائنية يعتني الصنف بكل ما يتعلّق بميزات وخصائص الكائن، والصنف ما هو إلا قالب template تعريفي بخاصيات properties وبطُرق/وظائف methods الكائن object. الكائن Object في البرمجة الكائنية الكائن ما هو إلا حالة/أمثولة instance من صنف class. الخاصية property في البرمجة الكائنية ما هي إلا مميزات وخصائص الكائن، كاللون مثلًا. الطريقة أو الوظيفة Method في البرمجة الكائنية تعتني الطريقة أو الوظيفة كما يُسميها البعض بقدرات الكائن، مثل قدرة المشي مثلًا، وهي دور أو وظيفة مرتبطة مع صنف class. المشيد Constructor في البرمجة الكائنية ما هو إلا طريقة method تُستدعى في لحظة استهلال instantiate الكائن، وعادةً ما يكون له نفس اسم الصنف الذي يحتويه. الوراثة Inheritance في البرمجة الكائنية يُمكن للصنف أن يرث مميزات من صنف آخر. التغليف Encapsulation في البرمجة الكائنية طريقة في تحزيم البيانات data والطُرق methods التي تستخدم البيانات. التجريد Abstraction في البرمجة الكائنية يجب على الاقتران الحاصل من: الوراثة والطُرق methods والخاصيات properties لكائن معقد وشائك التمثيل برمجيًا أن يعكس الواقع المراد محاكاته في البرمجة الكائنية. تعددية الأشكال Polymorphism في البرمجة الكائنية تحمل كلمة Poly بحد ذاتها المعنى "متعدد" وتحمل الكلمة morphism المعنى "أشكال، ويُشير المفهوم ككل إلى أن أكثر من صنف قد يُعرّف نفس الطريقة method أو الخاصية property. البرمجة المعتمدة على النموذج الأولي Prototype البرمجة المعتمدة على النموذج الأوّلي (Prototype-based programming) ما هي إلا نموذج من البرمجة الكائنية OOP ولكنها لا تستخدم الأصناف classes، بل تقوم أولًا بإعداد سلوك أي صنف class ما ومن ثم تُعيد استخدامه، ويُطلق البعض على هذا النموذج: البرمجة بلا أصناف classless، أو البرمجة المَبْدَئِية المنحى prototype-oriented، أو البرمجة المعتمدة على الأمثولة instance-based). يعود أصل اللغة المعتمدة على النموذج الأولي إلى لغة Self، والتي طوّرها David Ungar وRandall Smith، ولكن أسلوب البرمجة بدون أصناف class-less توسّع ونال شهرة كبيرة في العقد الأخير، واُعتمد من قبل العديد من اللغات البرمجية أشهرهم جافا سكريبت. البرمجة الكائنية باستخدام جافا سكريبت المجال Namespace في جافا سكريبت المجال هو أشبه بمستوعب/بحاوية (container) تسمح للمطوّر في تحزيم وظائف تحت اسم فريد، أو اسم تطبيق محدد، ففي جافا سكريبت المجال هو مجرد كائن object كأي كائن آخر يحتوي على طُرق methods، وخاصيات properties، وحتى كائنات objects. ملاحظة هامة: من المهم جدًا الانتباه إلى أنه في جافا سكريبت، لا يوجد فرق بين الكائنات العادية والمجالات namespaces، وهذا يختلف عن اللغات الكائنية الأخرى، الأمر الذي قد يُربك المبرمجين المبتدئين في جافا سكريبت. إن إنشاء مجال namespace في جافا سكريبت بسيطٌ للغاية، فمن خلال إنشاء كائن عام/مشترك/شامل global، ستصبح جميع المُتغيّرات variables والطرق methods، والدوال functions خاصياتٍ لهذا الكائن، ويٌقلل استخدام المجالات namespaces أيضًا من احتمالية تضارب الأسماء في التطبيق، منذ أن كل كائن من كائنات التطبيق ما هي إلى خاصيات كائن شامل/عام معرّفة على مستوى التطبيق. سيُنشئ في الخطوة التالية كائنًا عامًا global وبالاسم MYAPP: // global namespace var MYAPP = MYAPP || {}; يُظهر المثال السابق، كيف تم التأكّد أولًا فيما إذا كان MYAPP معرفًا (سواء في نفس الملف أو في آخر)، ففي حال الإيجاب سيُستخدم الكائن العام MYAPP، وفي حال عدم تعريفه مُسبقًا سيُنشئ كائنًا خالٍ وبالاسم MYAPP والذي سيغلّف encapsulate الطرق methods والدوال functions والمتغيرات variables والكائنات objects. كما يُمكن أيضًا إنشاء مجال فرعي sub-namespaces: // sub namespace MYAPP.event = {}; يوضّح المثال التالي الصيغة المستخدمة في إنشاء مجال namespace وإضافة متغيرات ودوال: // Create container called MYAPP.commonMethod for common method and properties MYAPP.commonMethod = { regExForName: "", // define regex for name validation regExForPhone: "", // define regex for phone no validation validateName: function(name){ // Do something with name, you can access regExForName variable // using "this.regExForName" }, validatePhoneNo: function(phoneNo){ // do something with phone number } } // Object together with the method declarations MYAPP.event = { addListener: function(el, type, fn) { // code stuff }, removeListener: function(el, type, fn) { // code stuff }, getEvent: function(e) { // code stuff } // Can add another method and properties } // Syntax for Using addListener method: MYAPP.event.addListener("yourel", "type", callback); الكائنات الأساسية/القياسية المبنية داخل لغة جافا سكريبت Standard built-in objects تتضمن لغة جافا سكريبت العديد من الكائنات في تركيبتها، على سبيل المثال، يوجد كائنات مثل Math، Object، Array، String، ويُظهر المثال التالي كيفيّة استخدام الكائن Math للحصول على رقم عشوائي باستخدام أحد طُرق method هذا الكائن وهي الطريقة ()random. console.log(Math.random()); ملاحظة: يَفترض المثال السابق وجميع الأمثلة التالية في المقال وجود دالة function بالاسم ()console.log معرّفة تعريفًا عامًا (globally)، مع العلم أن هذه الدالة ليست جزء من اللغة نفسها، ولكنها دالة متوفّرة في العديد من متصفحات الإنترنت لأغراض تشخيص الشيفرة البرمجية debugging. يُمكن العودة إلى مرجع لغة جافا سكريبت: الكائنات الأصلية المعيارية للحصول على قائمة بالكائنات المبينة داخل لغة جافا سكريبت نفسها. كل كائن في جافا سكريبت هو حالة/أمثولة instance من الكائن Object ويَرث كافة خاصياته properties وطُرقه methods. الكائنات المخصصة Custom objects في جافا سكريبت الصنف/الفئة The class لغة جافا سكريبت لغة من النوع prototype-based ولا تحتوي على العبارة class كما هو حال باقي لغات البرمجة الكائنية، كما في روبي أو بايثون، ويُربك هذا الأمر المبرمجين المعتادين على اللغات التي تعتمد على هذه العبارة أو المفهوم، وتستخدم جافا سكريبت بدلًا من ذلك الدوال functions لمحاكات مفهوم الأصناف classes، وتعريف صنف هو بسهولة تعريف أي دالّة: var Person = function () {}; الكائن (أمثولة الصنف class instance) يتطلب إنشاء حالة/أمثولة instance جديدة من كائن obj استخدام العبارة new obj، وتعيين النتيجة إلى متغيّر بغرض الوصول إلى فيما بعد. عُرّف في الشيفرة السابقة صنف class بالاسم Person، وفي الشيفرة التالية، سيُنشئ حالتين/أمثولتين instances من هذا الصنف، الأولى بالاسم person1 والثانية بالاسم person2. var person1 = new Person(); var person2 = new Person(); المشيد The constructor يُستدعى المُشيّد constructor في لحظة الاستهلال instantiation (اللحظة التي يُنشئ فيها الكائن)، والمُشيّد ما هو إلا طريقة method من طُرق الصنف class، وفي جافا سكريبت تعمل الدالة على تشييد الكائن، ولذلك لا داعي إلى تعريف طريقة method من أجل عميلة التشييد، وكل إجراء مصرّح في الصنف class يُنفّذ في لحظة الاستهلال instantiation. يُستخدم المُشيّد في تعيين خاصيات properties الكائن، أو في استدعاء طُرق methods معينة لتحضير الكائن للاستخدام، وأما إضافة طُرق صنف وتعريفها يحدث باستخدام صيغة syntax مختلفة سنتطرّق إليها فيما بعد خلال المقال. تُظهر الشيفرة التالية كيف يُسجّل log (يُرسل رسالة نصية إلى طرفية المتصفح console) مُشيّد الصنف Person رسالة نصية حينما يُستهل instantiated. var Person = function () { console.log('instance created'); }; var person1 = new Person(); var person2 = new Person(); الخاصية The property (خاصية الكائن object attribute) الخاصيات properties ما هي إلا متغيرات محتوات في الصنف class، وكل حالة/أمثولة من الكائن تمتلك هذه الخاصيات، وتُعيّن الخاصيات في دالة مُشيّد الصنف بحيثُ تُنشئ مع كل حالة/أمثولة instance. إن الكلمة المفتاحية this، والتي تُشير إلى الكائن الحالي، تسمح للمطوّر بالعمل مع الخاصيات من ضمن الصنف، والوصول (قراءةً وكتابةً) إلى الخاصية property من خارج الصنف يكون من خلال الصيغة InstanceName.Property كما هو الأمر في لغة C++ (سي بلس بلس) وJava والعديد من اللغات الأخرى، ومن داخل الصنف تُستخدم الصيغة this.Property للحصول على قيمة الخاصية أو لتعيين قيمتها. في الشيفرة التالية، عُرّفت الخاصية firstName للصنف Person وفي لحظة الاستهلال instantiation: var Person = function (firstName) { this.firstName = firstName; console.log('Person instantiated'); }; var person1 = new Person('Alice'); var person2 = new Person('Bob'); // Show the firstName properties of the objects console.log('person1 is ' + person1.firstName); // logs "person1 is Alice" console.log('person2 is ' + person2.firstName); // logs "person2 is Bob" الطرق The methods الطرق methods ما هي إلا دوال (وتُعرّف كما تعرّف الدوال functions)، فيما عدا ذلك فهي تُشبه الخاصيات properties، واستدعاء طريقة method مشابه إلى الوصول إلى خاصيّة ما، ولكن مع إضافة () في نهاية اسم الطريقة، وربما مع مُعطيات arguments، ولتعريف طريقة، تُعيّن دالة إلى خاصيّة مُسمّات من خاصيّة الصنف prototype، ويُمكن فيما بعد استدعاء الطريقة على الكائن بنفس الاسم الذي عُيّن للدالة. في الشيفرة التالية، عُرّفت ومن ثم اُستخدِمت الطريقة ()sayHello للصنف Person. var Person = function (firstName) { this.firstName = firstName; }; Person.prototype.sayHello = function() { console.log("Hello, I'm " + this.firstName); }; var person1 = new Person("Alice"); var person2 = new Person("Bob"); // call the Person sayHello method. person1.sayHello(); // logs "Hello, I'm Alice" person2.sayHello(); // logs "Hello, I'm Bob" إن الطُرق methods في جافا سكريبت ما هي إلا دالة كائن عادية مرتبطة مع كائن كخاصية property، وهذا يعني أنه يُمكن استدعاء الطُرق خارج السياق، كما في المثال التالي: var Person = function (firstName) { this.firstName = firstName; }; Person.prototype.sayHello = function() { console.log("Hello, I'm " + this.firstName); }; var person1 = new Person("Alice"); var person2 = new Person("Bob"); var helloFunction = person1.sayHello; // logs "Hello, I'm Alice" person1.sayHello(); // logs "Hello, I'm Bob" person2.sayHello(); // logs "Hello, I'm undefined" (or fails // with a TypeError in strict mode) helloFunction(); // logs true console.log(helloFunction === person1.sayHello); // logs true console.log(helloFunction === Person.prototype.sayHello); // logs "Hello, I'm Alice" helloFunction.call(person1); كما يُظهر المثال السابق، جميع الإحالات المستخدمة في استدعاء الدالة sayHello تُشير إلى نفس الدالة سواءً الاستدعاء الحاصل مع person1 أو Person.prototype أو حتى في المتغيّر helloFunction وقيمة this خلال استدعاء الدالة يعتمد على الكيفية التي تُستدعى فيها، حيث تُشير الكلمة المفتاحية this إلى الكائن الحالي الذي تُستدعى عليه الطريقة method، بمعنى عندما تم استدعاء الطريقة ()sayHello على الكائن person1 فإن this تُشير إلى الكائن person1، وعند استدعاء sayHello على الكائن person2 فإن this تُشير إلى الكائن person2، ولكن إن تم الاستدعاء بطريقة مختلفة، فإن this ستُعيّن تعينًا مختلفًا، فاستدعاء this من المتغيّر (كما في ()helloFunction) سيُعيّن this إلى الكائن العام global (والذي سيكون window في متصفح الإنترنت)، ومنذ أن هذا الكائن (على الأغلب) لا يملك الخاصّيّة firstName، ستكون النتيجة كما هو الحال في المثال السابق “Hello, I’m undefined”، كما يمكن دائمًا تعيين this صراحةً باستخدام Function#call (أو Function#apply) وهو كما كان في نهاية المثال. الوراثة تُستخدم الوراثة في جافا سكريبت في إنشاء صنف class كمثيل مخصص لصنف أو أكثر (تدعم جافا سكريبت وراثة وحيدة فقط single inheritance)، ويُطلق على الصنف المخصص عادةً ابن (child)، ويطلق على الصنف الآخر عادةً الأب (parent)، وفي جافا سكريبت يتمّ ذلك من خلال إسناد حالة/أمثولة من الصنف الأب إلى الصنف الابن، ومن ثم تخصيصه، وفي متصفحات الإنترنت الحديثة يُمكن استخدام Object.create في تحقيق الوراثة inheritance أيضًا. ملاحظة: لا تتفقد جافا سكريبت مُشيّد صنف الابن prototype.constructor (راجع Object.prototype)، وعليه يجب التصريح عن ذلك يدويًا، لمزيد من التفصيل راجع السؤال التالي على Stackoverflow. عُرّف في الشيفرة التالية الصنف Student كصنف ابن للصنف Person، ومن ثم أُعيد تعريف الطريقة ()sayHello وأُضيفت الطريقة ()sayGoodBye علاوة على ذلك. // Define the Person constructor var Person = function(firstName) { this.firstName = firstName; }; // Add a couple of methods to Person.prototype Person.prototype.walk = function(){ console.log("I am walking!"); }; Person.prototype.sayHello = function(){ console.log("Hello, I'm " + this.firstName); }; // Define the Student constructor function Student(firstName, subject) { // Call the parent constructor, making sure (using Function#call) // that "this" is set correctly during the call Person.call(this, firstName); // Initialize our Student-specific properties this.subject = subject; }; // Create a Student.prototype object that inherits from Person.prototype. // Note: A common error here is to use "new Person()" to create the // Student.prototype. That's incorrect for several reasons, not least // that we don't have anything to give Person for the "firstName" // argument. The correct place to call Person is above, where we call // it from Student. Student.prototype = Object.create(Person.prototype); // See note below // Set the "constructor" property to refer to Student Student.prototype.constructor = Student; // Replace the "sayHello" method Student.prototype.sayHello = function(){ console.log("Hello, I'm " + this.firstName + ". I'm studying " + this.subject + "."); }; // Add a "sayGoodBye" method Student.prototype.sayGoodBye = function(){ console.log("Goodbye!"); }; // Example usage: var student1 = new Student("Janet", "Applied Physics"); student1.sayHello(); // "Hello, I'm Janet. I'm studying Applied Physics." student1.walk(); // "I am walking!" student1.sayGoodBye(); // "Goodbye!" // Check that instanceof works correctly console.log(student1 instanceof Person); // true console.log(student1 instanceof Student); // true فيما يخص السطر ;(Student.prototype = Object.create(Person.prototype في الإصدارات القديمة من جافا سكريبت والتي لا تدعم Object.create يمكن إما استخدام بعض الحيل في خداع المتصفحات –هذه الخدع معروفة إما بالاسم polyfill أو shim—أو استخدام دالة تحقق نفس النتيجة كما في المثال التالي: function createObject(proto) { function ctor() { } ctor.prototype = proto; return new ctor(); } // Usage: Student.prototype = createObject(Person.prototype); التأكّد من أن this تُشير إلى الكائن المطلوب بغض النظر عن كيف للكائن أن يُستهل يمكن أن يكون صعبًا، ومع ذلك يوجد صياغة أبسط من شأنها أن تسهّل الأمر. var Person = function(firstName) { if (this instanceof Person) { this.firstName = firstName; } else { return new Person(firstName); } } التغليف Encapsulation ليس بالضرورة أن يعلم الصنف Student كيف تمّ تنفيذ/تعريف الطريقة ()walk للصنف Person لكي يستطيع استخدام تلك الطريقة، ولا يحتاج الصنف Student إلى تعريف تلك الطريقة صراحةً إلا إذا كان المطلوب التعديل عليها، ويُطلق على هذا الإجراء مفهوم التغليف encapsulation، والذي فيه يَحزم كل صنف البيانات والطُرق methods داخل وحدة/كينونة وحيدة. إخفاء المعلومات سمة شائعة في باقي اللغات البرمجية وعادةً ما توجد كخاصيات/كطُرق إما بالاسم private أو protected، وعلى الرغم من أنه يُمكن مماثلة/محاكاة ذات الأمر في جافا سكريبت، إلا أن هذا الأمر ليس مطلبًا من متطلبات البرمجة الكائنية. التجريد Abstraction التجرير ما هو إلا ميكانيكية تسمح للمطوّر في تجسيد جانب من المشكلة التي يُعمل عليها، إما من خلال الوراثة inheritance (التخصيص specialization) أو التركيب composition، وتُحقق جافا سكريبت التخصيص من خلال الوراثة، والتركيب من خلال السماح لحالات/أمثولات الصنف لتكون قيمًا لخاصيات attributes الكائنات الأخرى. الصنف Function في جافا سكريبت يرث من الصنف Object (وهذا يوضّح التخصيص في هذا النموذج) والخاصية Function.prototype ما هي إلا حالة/أمثولة من الصنف Object (وهذا يوضّح جزئية التركيب composition). var foo = function () {}; // logs "foo is a Function: true" console.log('foo is a Function: ' + (foo instanceof Function)); // logs "foo.prototype is an Object: true" console.log('foo.prototype is an Object: ' + (foo.prototype instanceof Object)); تعددية الأشكال Polymorphism كما أن جميع الطُرق methods والخاصيات properties معرّفة ضمن الخاصية prototype، فيُمكن لبقية الأصناف أن تُعرِّف طُرقًا methods بنفس الاسم، وستكون الطُرق في نطاق الصنف الذي عُرفت به، إلا إذا كان الصنفان على علاقة من نوع أب وابن parent-child، بمعنى آخر أحد الصنفان يرث من الآخر خاتمة إن الأساليب التي تم التطرُّق إليها ليست الأساليب الوحيدة التي يمكن استخدامها في تطبيق مفاهيم البرمجة الكائنية في جافا سكريبت، والتي هي مرنة إلى حد كبير في هذا الصدد، ولم تلجئ إلى أي خدع في تطبيق هذه المفاهيم، ولم تقلّد أيضًا الأساليب والنظريات المستخدمة في اللغات الأخرى، وفي جعبة جافا سكريبت العديد من الأساليب الأخرى لتطبيق مفاهيم متقدمة في البرمجة الكائنية التوجّه، ولكن هذه الأساليب المتقدمة هي خارج إطار مقالة تمهيدية، ربما نتطرّق إليها لاحقًا في الأكاديمية. ترجمة وبتصرّف للمقال Introduction to Object-Oriented JavaScript.
  13. أعترف بأنّي لا أمتلك خبرة كبيرة في مكتبة Webpacker الجديدة في إطار العمل Ruby on Rails، ولكنّي قرّرت الاعتماد على هذه المكتبة تمامًا والاستغناء كلّيًا عن مكتبة Sprockets للتعامل مع الأصول assets. وباعتباري أحد متّبعي مبدأ Convention Over Configuration فقد حاولت جاهدًا إيجاد الطريقة التي يمكن الاصطلاح عليها في تشييد تطبيق Webpacker. هذه المكتبة في أيامها الأولى لذا أظنّ أنّ فريق مطوري Rails لم يقوموا بهذا الأمر أيضًا، وأعتقد بأنّ مجتمع المطوّرين سيجد حلًّا لهذه المسألة قريبًا. على أي حال، إليك الطريقة التي اتبعتها في استبدال asset pipeline بـ Webpacker. إن كنت ترغب في العمل على مشروع جديد، فاستخدم الأمر: rails new blank --skip-sprockets --webpack ليتم إنشاء تطبيق Rails جديد مع الاستغناء عن مكتبة Sprockets وإضافة المكتبة Webpacker. أما لو كنت ترغب في إضافة Webpacker إلى مشروع قائم فعليك بمراجعة التوثيقات. بعد ذلك احذف بعض الجواهر gems والتي لم نعد بحاجة إليها من ملف Gemfile، وهي sass-rails، uglifier و coffee-rails. كذلك يمكنك التخلص من المجلد app/assets لأنّنا لم نعد بحاجة إليه بعد الآن. لنلق نظرة في البداية على محتويات ملف application.js الذي يتم إنشاؤه افتراضيًّا بواسطة Webpacker. /* eslint no-console:0 */ // This file is automatically compiled by Webpack, along with any other files // present in this directory. You're encouraged to place your actual application logic in // a relevant structure within app/javascript and only use these pack files to reference // that code so it'll be compiled. // // To reference this file, add <%= javascript_pack_tag 'application' %> to the appropriate // layout file, like app/views/layouts/application.html.erb console.log('Hello World from Webpacker') تشير التعليقات الواردة في هذا الملف إلى أن المجلد app/javascript/packs هو نقطة الولوج entry point إلى الحزم المستخدمة في التطبيق، وأنّ عليك وضع التطبيق الحقيقي في المجلد app/javascript. ترتيب الملفّات والمجلّدات لقد نظّمت تطبيق Webpacker الخاصّ بي كما هو موضح أدناه، والتطبيق يحمل اسم blog. سترى أنّي قد أدرجت تطبيق JavaScript الحقيقي في المجلد app/javascript/blog بدلًا من app/javascript ولم أقم بذلك اعتباطًا. أولًا: يعني هذا أن بمقدوري إضافة العديد من التطبيقات إلى المشروع الواحد وحسب الحاجة، بدلًا من تكديس جميع الشيفرات جنبًا إلى جنب. ثانيًا: ستتيح لي هذه الطريقة امتلاك نقطة ولوج حقيقية للحزم وهذا ما سأوضّحه الآن. blog +-- app | +-- javascript | | +-- blog | | | +-- fonts | | | +-- images | | | +-- styles | | | +-- index.js | | +-- packs | | | +-- application.js لنلق نظرة الآن إلى ملف app/javascript/packs/application.js وهو نقطة الولوج إلى حزمتي، وهو ملفّ بسيط للغاية: import 'blog'; سيتم استيراد التطبيق وتشغيل الملف app/javascript/blog/index.js والذي سيصبح نقطة الولوج إلى تطبيق JavaScript الخاصّ بي. بهذه الطريقة أحافظ على نقطة الولوج بسيطة قدر الإمكان أما ما تبقى من الشيفرة فيكون ضمن التطبيق. جدير بالذكر كذلك أنّك لست ملزمًا بتسمية المجلد - والملفّ - باسم blog، بل يمكنك استخدام أي اسم تشاء، ولكنّني توخيت تبسيط الأمور بجعل اسم المجلد مطابقًا لاسم تطبيق Rails. والآن سنستخدم javascript_pack_tag للإشارة إلى تطبيقنا. javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' مرحلة التطوير عند العمل في بيئة التطوير Development استخدم الأمر bin/webpack-dev-server وستتمّ مراقبة التطبيق وإعادة بنائه عند الحاجة، وإرسال التعديلات إلى المتصفّح. وبعد أن يصبح التطبيق جاهزًا للتجميع Compile يمكن استدعاء الأمر bin/webpack أو rails assets:precompile، ولكن سيتولى الخادوم هذه المهمّة على الأرجح. مكتبتا Turbolinks وRails UJS إن كنت ستستخدم Turbolinks و Rails UJS في تطبيقك فعليك إعداد هاتين المكتبتين وتشغيلهما. من السهل استدعاء المكتبتين بواسطة الأمر //= require في حال كنت تستخدم asset pipline ولكن عند استخدام هاتين المكتبتين كوحدات فالأمر مختلف قليلًا. في البداية علينا تثبيت المكتبتين: yarn add rails-ujs turbolinks بعد ذلك علينا استيراد المكتبتين وتشغيلهما في ملف app/javascript/blog/index.js: import Rails from 'rails-ujs'; import Turbolinks from 'turbolinks'; Rails.start(); Turbolinks.start(); كما تلاحظ فقد اتبعنا نفس الأسلوب في استدعاء كلتا المكتبتين وتشغيلهما. متغيرات البيئة Environment Variables يمكن الوصول إلى متغيرات البيئة عبر الشيفرات الخاصة بنا بعد تجميعها. عادة ما أضيف اللاحقة .erb إلى اسم الملف ثم أنفّذ شيئًا مماثلًا لهذا: <%= ENV['X_ENV_VAR'] %> ولكن هناك طريقة أفضل، إذ يمكن تهجير المتغيرات إلى process.env وكما يلي: export const STRIPE_API_KEY = process.env.STRIPE_API_KEY; أوراق الأنماط Stylesheets يمكن وبكل بساطة استيراد ملفات CSS أو Sass التي ترغب باستخدامها في التطبيق. لقد حدّدت الملف app/javascript/blog/styles/app.scss كنقطة ولوج Sass وبهذا أبقي جميع الملفات في مجلد styles، وتصبح عملية استيرادها إلى التطبيق أمرًا سهلًا للغاية: import './styles/app.scss'; يمكن استخدام المحرّف ~ مع import وستبدأ عملية البحث عن الملف من المجلد node_modules، فلو أردت مثلًا استيراد مكتبة Bootstrap إلى التطبيق يمكنك استخدام الشيفرة التالية: @import '~bootstrap/scss/bootstrap'; الصور لا تختلف الصور عن أوراق الأنماط في شيء، إذ يجب استيرادها في البداية لتتمكن من استخدامها في التطبيق. عادة ما أضع الصور في مجلد app/javascript/blog/images ثم أنشئ ملفًّا باسم index.js في نفس المجلد وظيفته استيراد جميع الصور في المجلد. فعلى سبيل المثال: import './logo.svg'; import './menu-open.svg'; import './menu-close.svg'; عليك الانتباه إلى أنّ هذه الطريقة لن تُضمّن الصور في أوراق الأنماط، وإنما تدفعها إلى Webpacker لتكون متاحة للاستخدام في التطبيق. ويمكنك حينئذٍ استخدام الدالة المساعدة asset_pack_path في العرض للإشارة إلى هذه الملفات. فلو أردت مثلًا استخدام إحدى هذه الصور: = image_tag asset_pack_path('logo.svg') إضافة إلى ذلك يمكنك الإشارة إلى الصور في CSS أو Sass وسيتلقّفها Webpacker بصورة تلقائية. وبصورة عامة يكون مسار الجذر نقطة الولوج الخاصّة بأوراق الأنماط لذا لن تكون بحاجة إلى استخدام المسارات المطلقة. ختامًا كما شاهدت فإن الأمر يتطلب الكثير من العمل، وهذا هو الأسلوب الذي أتبعه في استخدام Webpacker الآن. حاولت البحث عن مقالات تعنى بتفصيل طريقة استخدام Webpacker ولكنّي لم أجد شيئًا يذكر في الوقت الحاضر. أنا متحمّس جدًّا لمعرفة طريقة الاستخدام القياسية لهذه المكتبة هذا في حال تمّ تحديدها في المستقبل. ترجمة - وبتصرّف - للمقال Replacing Rails Asset Pipeline with Webpacker لصاحبه Dwight Conrad Watson.
  14. مواقع الويب الحديثة أصبحت أكثر ديناميكية مؤخرا، عبر استخدام Javascript لتزويد المستخدم بتحديثات مباشرة ومتحكمات أكثر سلاسة. أحد أكثر المميزات شيوعا هي إظهار وإخفاء المحتوى، سواءا كان على شكل تبويبات، مخفيا على جانب الصفحة، أو على شكل قائمة متعددة المستويات. عادة، هذه المميزات يتم تطبيقها عبر Javascript فحسب، ولكنك تستطيع القيام بنفس التأثيرات عبر CSS3 بدون اللجوء إلى Javascript. في هذه المقالة سنرى كيف يمكننا تحقيق ذلك، مع تضمين بعض الأمثلة. المحتوى مثال بسيط حل حقيقي الإخفاء والإظهار عبر CSS تحسين القائمة دعم المتصفحات مثال بسيط هذا مثال بسيط يظهر كيف يمكن إخفاء وإظهار المحتوى عبر CSS: See the Pen &amp;amp;amp;amp;amp;amp;lt;a data-cke-saved-href='http://codepen.io/assassinateur/pen/YXKgRz/' href='http://codepen.io/assassinateur/pen/YXKgRz/'&amp;amp;amp;amp;amp;amp;gt;YXKgRz&amp;amp;amp;amp;amp;amp;lt;/a&amp;amp;amp;amp;amp;amp;gt; by Nadir (&amp;amp;amp;amp;amp;amp;lt;a data-cke-saved-href='http://codepen.io/assassinateur' href='http://codepen.io/assassinateur'&amp;amp;amp;amp;amp;amp;gt;@assassinateur&amp;amp;amp;amp;amp;amp;lt;/a&amp;amp;amp;amp;amp;amp;gt;) on &amp;amp;amp;amp;amp;amp;lt;a data-cke-saved-href='http://codepen.io' href='http://codepen.io'&amp;amp;amp;amp;amp;amp;gt;CodePen&amp;amp;amp;amp;amp;amp;lt;/a&amp;amp;amp;amp;amp;amp;gt;. عندما نضغط على زر إخفاء، سيختفي الزر إلى جانب القائمة في الأسفل، ويظهر زر "إظهار". وإذا شاهدت نسخة الطباعة من الصفحة، فستلاحظ أنّ الأزرار لن تظهر. الآن بعد مشاهدتنا للمثال، لنلقي نظرة على ما استخدمنا والتقنيات الكامنة خلف ذلك، ونبني قائمة بينما نحن نقوم بذلك. حل حقيقي نستطيع استخدام الحل الذي سنطوره في إحدى الحالات التالية: نريد أن تجعل محتوى معين يظهر ويختفي في مكان معين (مثل قوائم المحتوى في ويكيبيديا) لسبب ما، قد لا نريد استعمال جافاسكربت لنبدأ بمشاهدة حل يستخدم جافاسكربت؛ قسم المحتوى في مقالة ما في ويكيبديا See the Pen &amp;amp;amp;amp;amp;amp;lt;a data-cke-saved-href='http://codepen.io/assassinateur/pen/rVBbMR/' href='http://codepen.io/assassinateur/pen/rVBbMR/'&amp;amp;amp;amp;amp;amp;gt;rVBbMR&amp;amp;amp;amp;amp;amp;lt;/a&amp;amp;amp;amp;amp;amp;gt; by Nadir (&amp;amp;amp;amp;amp;amp;lt;a data-cke-saved-href='http://codepen.io/assassinateur' href='http://codepen.io/assassinateur'&amp;amp;amp;amp;amp;amp;gt;@assassinateur&amp;amp;amp;amp;amp;amp;lt;/a&amp;amp;amp;amp;amp;amp;gt;) on &amp;amp;amp;amp;amp;amp;lt;a data-cke-saved-href='http://codepen.io' href='http://codepen.io'&amp;amp;amp;amp;amp;amp;gt;CodePen&amp;amp;amp;amp;amp;amp;lt;/a&amp;amp;amp;amp;amp;amp;gt;. المثال السابق مأخوذ من صفحة ويكيبيديا (هو مجرد HTML رغم هذا)، يعمل الجدول عبر إخفاء المحتوى، وتغيير الكلمة من "أخفي" إلى "أظهر" ويتم هذا عبر جافاسكربت. يتم إضافة تسجيل مراقب للزر (Event Listener)، عندما يتم الضغط على الزر، يقوم jQuery بإضافة class جديد للصندوق، يتم أيضا إَضافة واحد لحاوي الروابط، كما يتم تغيير محتوى الزر من "أخفي" إلى "أظهر". الإخفاء والإظهار عبر CSS سنحاول إعادة صنع قائمة ويكيبيديا، ولكن عبر CSS فحسب، فسنفقد بعض المميزات، ولكننا سنكسب بعض الأمور في نفس الوقت. يستطيع رابط ما أن يأخذك لأيّ جزء من الصفحة عبر تحديد ID الخاص به كمكان اتجاه للرابط. في المثال سنستخدم شيئا مشابها، ولكن ليس للتصفح، بل لإظهار وإخفاء المحتوى. نستخدم الكود في الأسفل ثم نشرحه. See the Pen &amp;amp;amp;amp;amp;amp;lt;a data-cke-saved-href='http://codepen.io/assassinateur/pen/oXvOZb/' href='http://codepen.io/assassinateur/pen/oXvOZb/'&amp;amp;amp;amp;amp;amp;gt;oXvOZb&amp;amp;amp;amp;amp;amp;lt;/a&amp;amp;amp;amp;amp;amp;gt; by Nadir (&amp;amp;amp;amp;amp;amp;lt;a data-cke-saved-href='http://codepen.io/assassinateur' href='http://codepen.io/assassinateur'&amp;amp;amp;amp;amp;amp;gt;@assassinateur&amp;amp;amp;amp;amp;amp;lt;/a&amp;amp;amp;amp;amp;amp;gt;) on &amp;amp;amp;amp;amp;amp;lt;a data-cke-saved-href='http://codepen.io' href='http://codepen.io'&amp;amp;amp;amp;amp;amp;gt;CodePen&amp;amp;amp;amp;amp;amp;lt;/a&amp;amp;amp;amp;amp;amp;gt;. مثل قائمة ويكيبديا، نريد من قائمتنا أن تظهر مفتوحة بشكل افتراضي. نصنع class باسم .show و .hide. وبما أن القائمة ستكون مفتوحة، لا نريد من زر .show أن يظهر. .show { display: none; } والآن نريد أن نجعل زر الإظهار 'يظهر' ونخفي زر 'الإخفاء' عندما يتم الضغط عليه، يمكننا تحقيق هذا عبر شبه الصنف :focus والذي يعمل عندما يتم الضغط على الرابط بواسطة الفأرة أو التركيز عليه بواسطة لوحة المفاتيح. سنستخدم المحدد "+" والذي يعني العنصر الذي يأتي مباشرة بعد العنصر الأول. a.hide:focus + .show { display: inline; } a.hide:focus { display: none; } الآن، لنجعل الزر يعمل حقا! سوف نقوم بجعل القائمة #menu تختفي عندما يكون زر الإخفاء في حالة :focus بما أنّ القائمة لا تأتي مباشرة بعد الزر، سيكون علينا استخدام المحدد غير المباشر ~ من أجل تحديد القائمة. a.hide:focus ~ #menu { display:none; } والآن، لتعديل بسيط أخير. نريد أن نخفي أزرار الإخفاء والإظهار في حال طباعة الصفحة، أسرع طريقة للقيام بالأمر هي عبر استخدام media query والتي تتفقد هل يتم طباعة الصفحة، وإن كان كذلك تقوم بإخفاء الأزرار. @media print { .hide, .show { display: none; } } حتى الآن، ما قمنا به يعطينا التأثير المطلوب الذي رأيناه في أول المقال. القائمة التي لدينا تقوم بنفس عمل قائمة ويكيبيديا تماما، عدى بعض التأثيرات الخفيفة التي سنصل لها بعد قليل. يجب التنويه على أننا لم نستخدم جافاسكربت فحسب، بل استخدمنا مقدارا ضئيلا جدا من CSS من أجل تحقيق هذا، مقارنة ب3 دوال في جافاسكربت وحوالي 20 سطرا من الكود. تحسين القائمة الآن بعدما قمنا بصنع التأثير إخفاء المحتوى، نريد أن نضيف له بعض التأثيرات البصرية، وسنستخدم CSS أيضا. الاختفاء البطيء بدلا من الإظهار والإخفاء السريع، سيكون من اللطيف لو اختفت القائمة بشكل بطيء، وسنقوم بهذا عبر transition في CSS. سوف نحدد الوضع الافتراضي للقائمة (وهي أن تظهر بشكل كامل) ثم نخفيها ببطيء. #menu { opacity: 1; } a.hide:focus ~ #menu { opacity: 0; } بدل أن نضع التأثير خصيصا للعنصر، نستطيع إَضافته لأي عنصر عبر وضعه في class مستقل .tran1 { -o-transition: all 1s; -moz-transition: all 1s; -webkit-transition: all 1s; transition: all 1s; } الآن، تستطيع إضافة التأثير لأي عنصر تريد وذلك عبر إضافة class فحسب. النتيجة النهائية بعد إَضافة بعض التحسينات البسيطة، هذه هي النتيجة النهائية See the Pen &amp;amp;amp;amp;amp;amp;lt;a data-cke-saved-href='http://codepen.io/assassinateur/pen/pJzBWQ/' href='http://codepen.io/assassinateur/pen/pJzBWQ/'&amp;amp;amp;amp;amp;amp;gt;pJzBWQ&amp;amp;amp;amp;amp;amp;lt;/a&amp;amp;amp;amp;amp;amp;gt; by Nadir (&amp;amp;amp;amp;amp;amp;lt;a data-cke-saved-href='http://codepen.io/assassinateur' href='http://codepen.io/assassinateur'&amp;amp;amp;amp;amp;amp;gt;@assassinateur&amp;amp;amp;amp;amp;amp;lt;/a&amp;amp;amp;amp;amp;amp;gt;) on &amp;amp;amp;amp;amp;amp;lt;a data-cke-saved-href='http://codepen.io' href='http://codepen.io'&amp;amp;amp;amp;amp;amp;gt;CodePen&amp;amp;amp;amp;amp;amp;lt;/a&amp;amp;amp;amp;amp;amp;gt;. ملاحظة: حالما تضغط على شيء آخر، يعود كل شيء كما كان وهذا هو التصرف الطبيعي لشبه الصنف :focus ولكنه ليس الأمر الذي نريده نحن. هذه المشكلة ليست موجودة في مثال ويكيبيديا بكل تأكيد، كما أن ويكيبيديا تستخدم الـcookies لتتذكر آخر حالة للقائمة حتى بعد تحديث الصفحة، وهو شيء لا نستطيع تحقيقه للأسف. خاتمة استطعنا القيام ببعض التأثيرات البصرية باستخدام CSS لوحدها عبر تعلم كيفية إخفاء وإظهار العناصر، هناك محددات واعدة أكثر ستظهر مستقبلا، وستتيح لنا التحكم أكثر وأكثر في العناصر وتحديدها بسهولة، احدى الخدع التي يمكن استخدامها هي استخدام checkbox بدل الروابط من أجل تغيير حالة الصندوق، ولكن كان هذا ليجعل الأمر أكثر تعقيدا. حقوق الصورة البارزة محفوظة لـ Freepik
  15. لا يجب أن تبدأ دائمًا بالتطوير باستعمال إطار عمل JavaScript، لكن هنالك حالات يكون فيها استعمال إطار العمل أمرًا منطقيًا. منذ فترةٍ كتبتُ مقالةً بعنوان «لماذا يدفعنا التطوير بلغة JavaScript إلى حافة الجنون!» والتي زارها الكثيرون، وظهر التساؤلان الآتيان ردًا عليها: - متى يجب أن نستخدم إطار عمل؟ - إذا لم نستعمل إطار عمل، فكيف نبدأ؟ سأحاول في هذه المقالة الإجابة على التساؤل الأول، أما سؤال كيفية البدء إذا لم نستعمل إطار عمل فهو سؤالٌ أكبر، ويحتاج تفصيلًا أكثر. بدأتُ العمل منذ عدِّة سنوات في شركة (لن أسميها هنا)، والتي تستطيع أن تطلب منها إنشاء أي شيء غريب يمكن تخيله، وستصنعه لك خلال فترةٍ وجيزة. كان للشركة هامش ربحٍ كبير، وتجني أموالًا طائلة، وتعلمتُ الكثير عن إدارة الأعمال منها، لكنني تعلمتُ أيضًا بعض الأمور عن الأنظمة. كانت واجهة موقع الشركة الإلكتروني معقدةً! ولهذا السبب بدأ موقع الشركة في بدايات 2000 بالتحول إلى تطبيقٍ ذي صفحةٍ واحدة، حتى قبل انتشار فكرة التطبيقات ذات الصفحة الواحدة (single page application). كانت شيفرات JavaScript التي تشغل موقعهم معقدةً جدًا، وكانوا نادرًا ما يوظفون مبرمجين، واختاروا إطار عمل YUI بدلًا من jQuery لأنه كان شائعًا في تلك الفترة، ثم اشتقوا (fork) شيفرة YUI مما جعل التعامل معها وصيانتها أمرًا صعبًا جدًا. ولسببٍ أجهله، كانوا يكتبون شيفرات JavaScript بشكلٍ شبيهٍ بشيفرات Visual Basic. كانوا أيضًا يستعملون نظامًا لإدارة الإصدارات (version control system) من الثمانينات الذي كانت آلية حماية الملفات من التعديل فيه هي قفلها (file locks) والتي تستطيع بسهولة تجاوزها. سكربت البناء الذي كانوا يستعملونه هو شخصٌ اسمه Ed، فلتحويل الشيفرة إلى الخادوم الإنتاجي سيكون عليك أن تطبع قائمةً بالملفات التي عدَّلتَها وتسلمها لذاك الشخص، ثم سيبحث عن تلك الملفات وينسخها يدويًا إلى كل خادوم على حدة. كان السند الخلفي (backend) للموقع هو حاسوبٌ قديمٌ يُشغِّل برمجيات مكتوبة في سبعينيات القرن الماضي، وكانت تُحدَّث بيئة التطوير المشتركة يدويًا من قِبل المطورين، متى أرادوا ذلك. إذا كنتَ تضحك الآن على حالهم وتظن أن هؤلاء الأشخاص أغبياء، فأظن أنَّ الوقتَ مناسبٌ لأشير إلى أنهم لم يعانوا من انقطاعاتٍ في الخدمة، وكانوا يُصدرون ميزاتٍ جديدة دوريًّا، ويجنون أموالًا طائلة تزداد عامًا بعد عامًا. لذا أظن أنهم يضحكون الآن خلال طريقهم إلى البنك ليستلموا أرباحهم. هل تتلقى أجرًا لقاء كتابتك للشيفرات؟ إذًا أنت تعمل لدى شركة. وعندما تقرر الشركة كيف تُنفِق ميزانية المطورين، فلن يكون منطقيًا أن تنفق مالًا لكتابة البرمجيات «بالطريقة الصحيحة». الفكرة التي أحاول إيصالها هنا هي أنَّ حال الشركة السابقة قد يبدو بائسًا بالنسبة إلى مطوري البرمجيات، لكنه منطقيٌ جدًا وجيد بالنسبة إلى أصحاب تلك الشركة الذين يهتمون بالربح. استخدام إطار عمل JavaScript قد لا يكون خيارًا استراتيجيًا للشركات حتى لو كان حالها سيئًا كالشركة التي نتحدث عنها. إذًا، كيف يجب أن تتابع الشركة السابقة بتطوير برمجياتها؟ كيف ستقرر الشركة أنها بحاجة إلى استخدام إطار عمل؟ شرحتُ سابقًا في مقالة «لماذا يدفعنا التطوير بلغة JavaScript إلى حافة الجنون!» أنَّ من غير المنطقي البدء بتطوير البرمجيات انطلاقًا من إطار عمل. لنفترض وجود شيءٍ شبيهٍ بإطار Angular عند نشأة الشركة السابقة؛ فهل كان عليهم استخدام إطار عمل؟ هل يجب عليهم استخدام إطار عمل في هذه الفترة؟ عندما تختار الشركة إطار عمل لتستعمله لتطوير برمجياتها، فهنالك كلف ومخاطر: - ماذا لو اختفى دعم إطار العمل خلال خمس سنوات؟ هل يمكن أن تتحمل الشركة عبء صيانة إطار العمل بنفسها؟ هل يتوافر في فريق العمل أشخاصٌ لهم خبرات في هذا المجال؟ الجواب هو النفي لأغلبية الشركات، ولا نُغفِل أنَّ الكثير من أطر العمل Frameworks تختفي فجأةً. عليك أن تجري مع التيار وتخمِّن إن كانت الزيادة في الإنتاجية الآتية من اختيار إطار عمل تستحق الوقت والكلفة اللازمة للتحويل إلى إطارٍ آخر لاحقًا، أو أنَّها تستحق كلفة التعامل مع برمجيات غير مدعومة لسنوات في نفس الوقت الذي تحاول فيه توفير ميزانية كافية للتحويل إلى إطارٍ جديد. إذا كانت شيفرات برمجيتك قليلة نسبيًا وكنت تعمل في شركة وتحذف كميةً لا بأس بها من الشيفرات وتُعيد كتابتها (كثير منا يفعل ذلك لكنهم لا يعترفون)، فأرى أنَّ من المناسب استعمال إطار عمل. أما لو كانت شيفرات برمجيتك كثيرة جدًا، وكنت تنظر إلى خطتك التي وضعتها للسنوات الخمسة القادمة ورأيتها مليئةً بالميزات الجديدة التي ستكوِّن جزءًا أساسيًا من شركتك، فإن اختيار إطار عمل الذي سينتهي دعمه خلال خمسة سنوات هو رهانٌ خاسر، والطريقة الوحيدة التي ستجعلك تختار واحدًا هي التفكير بأجوبةٍ للأسئلة المتبقية … - كم ستخسر من إنتاجية مطوريك حتى يتعلموا إطار العمل الجديد؟ إذا كنت تنتقل إلى إطارٍ وليكن Angular (وما لم توظِّف مطورين يعرفون Angular من قبل ويلمّون بأحدث المعلومات) فستنفق مالًا لتدريب فريق التطوير وسيضيّع المطورون الأشهر القادمة يسألون «كيف أفعل ذلك في Angular؟» حتى لو كانوا يفعلون نفس الأمر تمامًا باستخدام jQuery وشيفرات JavaScript البسيطة دون أدنى تفكير. - هل يمكننا بناء هذه الميزة التي تجعلنا نجني مالًا أكثر بنفس مقدار الوقت اللازم لتعلم إطار العمل؟ الجواب لأغلبية الشركات الربحية هو «نعم». - هل سيسمح لنا إطار العمل بجني المزيد من المال بجعلنا نُنشِئ الميزات الجديدة بوتيرة أسرع في المستقبل؟ نظريًا: هذه هي الفائدة المرجوة من إطار العمل. إذ ستحصل على فائدة أكبر من إطار العمل عندما يكون فريقك كبيرًا، وشيفرات مشروعك كثيرة، وتريد تسريع وتيرة تطوير الميزات الجديدة. لكن لاحظ أنَّك تستطيع تطوير بعض البرمجيات بسهولة ولن تستفيد فيها من التحول إلى إطار عمل. حسنًا، ربما تظن أنَّ هذا قرارٌ صعبٌ، إلا أنه في الواقع عكس ذلك لأغلبية الشركات. - الشركات الصغيرة وسريعة التغيرات تقع في الزاوية العليا اليسرى من المخطط السابق، فإذا كانت شيفرات برنامجك تتغير بسرعة فلن يكون من الخطر التغيير إلى إطار عمل آخر. فلو كنت تضيف الميزات يمنةً ويسرةً لأنك لم تعلم أيها يجني مالًا بعد، فلن تهتم إن «مات» إطار العمل الذي تستخدمه، لأنك ستحذف تلك الميزات والأجزاء من الشيفرات على أيّة حال. إذا كانت إنتاجيتك أكبر عندما تستعمل إطار عمل، وكنت تعمل لدى شركةٍ ناشئةٍ فسأقول أنَّ من الصواب استعمال إطار عمل، مع الأخذ بالحسبان أنَّ أغلبية الأشخاص تقل إنتاجيتهم عندما يستعملون إطار عمل لكنهم لا يعترفون بذلك. - تتواجد الشركات الصغيرة وذات معدل التغيير البطيء في الطرف السفلي اليساري من المخطط، وتكون إما شركات صغيرة تقليدية التي لا تُمثِّل البرمجيات مكوِّنًا أساسيًا من طريقة عملها، أو شركات البرمجيات الصغيرة. إذا كنت مبرمجًا تعمل لدى شركة صغيرة، فلا تستعمل إطار Angular أو React أو غيرهما. وإنما استخدم المكتبات المستقرة والثابتة والتي تعمل على جميع المتصفحات. فلو كنت تعمل في شركةٍ صغيرةٍ، فأرى أنَّ مخاطر استعمال إطار عمل تفوق ميزاته. - الشركات الكبيرة وذات معدل التغيير السريع تتواجد في القسم العلوي الأيمن من المخطط، وهي الشركات التي تكون البرمجيات مكوِّنًا أساسيًا فيها، أي أنهم يضيفون الميزات كثيرًا ويحذفون أجزاءً كبيرةً من الشيفرات ويعيدون كتابتها (حتى لو لم يعترفوا بذلك)، ولديهم فرق برمجية كبيرة، والميزات الجديدة التي يُضيفونها ستدر عليهم مالًا. لذا من المنطقي أن يدرِّبوا فِرَق البرمجة عندهم لاستعمال إطار عمل، لكي يبدأ الجميع من نفس المكان ولتُكتَب الميزات بوتيرة أسرع. إذا كانت شركتك ذات دخلٍ كبير، فستقل مخاطر فقدان دعم إطار العمل مع مرور الزمن، لأنك عندما كنت تختار ما هو إطار العمل الذي ستعمل عليه فستأخذ بالحسبان الدعم الطويل له. أما لو كنت تعمل في شركةٍ كبيرةٍ وتجني أموالًا طائلة وكانت البرمجيات من أساس عملك، فمن المنطقي أن تكتب إطار عمل خاص بك مثل Facebook و Google … - الشركات الكبيرة وذات معدل التغيير البطيء تقع في الركن السفلي اليميني من المخطط. وتنتمي الشركة التي تحدثنا عنها في بداية المقالة إلى هذا النوع، ولن يكون الخيار الصائب واضحًا هنا. فليس مجال عمل الشركة هو البرمجيات، لكن البرمجيات هي جزءٌ أساسيٌ نظام عمل الشركة. لن تتغير الشركة بسرعة، لكن الميزات الجديدة ستساعد الزبائن بشراء المزيد من المنتجات. مثاليًا، يمكنك أن تبني على إطار عمل الذي سيبقى حوالي 50 سنة، وهذا أمرٌ معقولٌ بالنسبة إلى شركةٍ تستعمل برمجيات وشيفرات عمرها 40 سنة. لكن هذا ليس موجودًا، لذا قد تفكر ببناء إطار عمل خاص بك، لأنك ستتأكد أنك لن تقع في فخ أطر العمل الميتة بعد 10 سنوات. لذا يجب أن تنشئ كل شيء من الصفر، وسترى أنَّ نفقات إنشاء ذلك قليلةٌ مقارنةً بالربح خلال 50 سنة … إذًا، هل يجب أن تنتقل الشركة إلى إطار عمل؟ لا يوجد جواب سهل! فعندما كنتُ مطورًا أعمل عندهم وكنت أفكر عن البرمجيات فقط، فكان الجواب الجلي بالنسبة لي هو: بالطبع يجب أن ينتقلوا لاستخدام Angular! لكن عندما بدأتُ شركتي الخاصة، فأصبحتُ أرى البرمجيات من زاويةٍ أخرى، لذا لن يكون الخيار سهلًا. توقف برهةً وفكِّر بعمق قبل الالتزام بإطار عمل، وإذا كنتَ صاحب قرار التحول إلى إطار عمل في حال استعملتك الشركة كمستشار، فأسدِ خدمةٍ إلى الشركة، وانزع عنك نظارات المبرمج، وضع نظارات مدير الأعمال. ترجمة -وبتصرّف- للمقال Should You Ever Use a JS Framework?‎ لصاحبه Sean Fioritto
  16. أقدّم بين أيديكم هذه السلسلة التي استلهمت فكرتها من كتب برمجة صغيرة مجّانيّة ومحبّبة، مثل كتاب The Little Book on CoffeeScript لمؤلّفه Alex MacCaw. لقد ألّفت هذه السلسلة لتعليم Angular بالطّريقة التي تمنّيت أن أتعلمها بها، ولذلك فهذه السلسلة معينك للانطلاق السريع مع Angular واستخدام هذه المكتبة الرائعة في عملك كمطوّر ويب. أسلوب هذه السلسلة مستلهم من مبدأ باريتو Pareto فهي تعلّمك جزءًا كبيرًا من نقاط قوّة Angular دون أن تثقل كاهلك بالكثير من تعقيداتها، وستتمكّن بعد إنهائك لهذه السلسلة من كتابة تطبيقات واجهة front-end قويّة، وستدهشك سهولة ذلك، بالرّغم من أنّ السلسلة لا تقدّم إلا جزءًا محدودًا ممّا عليك تعلّمه لاحتراف Angular بشكل كامل، لكنها ستمنحك الثّقة الكافية للقيام بذلك لأنّ خبرتك ستكون مبنيّة على التّجربة العمليّة أثناء قراءتك لدروسها، فهي بحدّ ذاتها صفحات ويب ديناميكيّة تحوي آخر نسخة من Angular وتستخدمها لتنفيذ جميع الأمثلة بشكل مباشر، وجميع الشّيفرات البرمجيّة تتيح التّعديل المباشر عليها ممّا يغيّر المخرجات مباشرة، وأتمنّى لو تقوم بكلّ التّجارب والتّعديلات التي تخطر على بالك عند كل مثال. إذا كنت ترغب في معرفة سبب كتابتي لهذه السلسلة يمكنك قراءة هذه المقدّمة إلى نهايتها فهي تتضمّن بعضًا من ذكرياتي مع Angular، ثم سنناقش أفضليّة استخدام Angular في مشاريعك. هل هي صعبة أم سهلة؟لقد عملت من قبل على تطبيقات معتمدة على Web MVC لما يزيد عن عشر سنوات، واستعملت الكثير من الأدوات بدءًا من Struts وانتهاءً بـSpring MVC مرورًا بـRuby on Rails وBackbone، حتّى أنّني قمت بكتابة كتاب Backbone and CoffeeScript لذا كان من الطّبيعي بالنّسبة لي أن أفترض أنّ تعلّم ِAngular سيكون بسيطًا بالنسبة لي، إلّا أنّ تقدّمي في تعلّمها واجه سدًّا كبيرًا من المصطلحات غير المألوفة بعد غوصي في توثيق هذه المكتبة، مصطلحاتٌ مثل transclusion، توجيه directive والمجال المعزول isolate scope، وكلّما قرأت أكثر في التّوثيق الرّسمي للّغة ودروسها تأكّدت أكثر بأنّني كنت أتوهّم عندما ظننت بأنّ Angular أداةٌ سهلة، وأتذكّر بشكل خاصّ مروري على العبارة التّالية: لن تجد العبارة السابقة هذه الأيام داخل توثيق اللّغة، والشّكر يعود للجهود الجادّة التي بذلها فريق ِAngluar لتحسين التّوثيق، وعلى أيّ حال فالاقتباس السّابق جعلني أشعر بالجهل وبدأت الشّكوك تراودني والقلق يساورني: هل هذه المكتبة مجال جديد كلّيّا عليّ؟ كانت الإجابة تأتيني من كلّ الجهات تقريبًا: إنّ ِAngular تقنيّة معقدّة، ولا مجال للعبث معها، ففي ملتقيات المبرمجين على Stack Overflow ومجموعة AngularJS على Google وفي كلّ مكان آخر، كانت تفاصيل هذه المكتبة محلّ النّقاشات المطوّلة، مع توثيقات مرعبة للمشاكل والأفخاخ والحيل فيها، فشمّرت عن ساعد الجد كأي محترف يحترم نفسه وقبلت هذا التّحدّي، ومع مرور الوقت أصبحت أكثر ألفة مع هذه المفاهيم والمصطلحات، وتقبّلت أنّ تعلّمي هذه الأداة سيكون بطيئًا، إلى أن جاء ذلك اليوم الذي شاهدت فيه مقابلة مع Miško Hevery مخترع Angular، وبعد ذلك اكتشفت حقيقة بسيطةً إلّا أنّها كانت شديدة الأهمّيّة: الهدف الأصليّ من Angular هو سهولة الاستخدام. لقد بيّن Hevery بأنّه أراد أن تكون Angular أداة ليستخدمها غير المبرمجين، ليتمكّنوا من بناء صفحات ويب ديناميكيّة باستخدامهم لنصوص تصريحية بسيطة، لقد اكتشفت بأنّني قد تعمّقت في الكثير من تفاصيل Angular دون أن أجني منها فائدة مكافئة للوقت والجهد المبذول، ورغم أنّ مشروع Angular ومجتمع Angular أصبحا معتمدين على بعضهما بشكل وثيق حيث تقدّم المكتبة التّحديات والتّعقيدات التي تشبع شغف هؤلاء، إلّا أنّه كان هناك طريق أفضل لاستخدام المكتبة كما أراد مخترعها، بعيدا عن مبدأ عملها المصنوع بعناية، وقد كان الصّواب بالنسبة إليّ الابتعاد عن هذه الأمور الدّاخليّة الدّقيقة وتركها آمنة لتقود كلّ شيء دون المساس بها، وبعد أن اعتمدت هذا المبدأ في التعامل معها وبالرّغم من وجود العديد من الأدوات الأخرى إلّا أنّ شمس Angular صارت تشرق لي عند مواجهة العديد من الحالات. هل تناسبك؟تعتمد إجابة هذا السّؤال على معرفتك لمشروعك بدقّة، فعليك معرفة فيما إن كان يحتاج بالفعل لمعالجة البيانات وإخراج HTML في طرف الزّبون، أم أنّك تخدع نفسك ببعض المتطلّبات الإضافيّة لتعطيها دافعًا لتعلّم أداة جميلة وجذّابة، فقد لا تلزمك Angular إن كان بإمكانك الاعتماد على بعض المعالجة من طرف الخادوم مع "رشّةٍ" من التّفاعل المعتمد على jQuery. ضع في حسابك أنّ Angular واسعة وتدعو للتشبّث بها، فبالرّغم من أنّنا ندعوها "مكتبة" إلا أنّ دعمها الذّاتي للـوحدات modules ولـحقن التّبعيّة dependency injection سيفرض على المطوّر طريقة إدارة مشروعه، وقد يُفضّل حلًّا آخر على Angular، ربّما سيختار أداة أقدم منها ومتميزةً بكونها الأفضل في وقتها، لكنّه قد يجد أنّه من الصّعب أو المستحيل أن تحلّ محلّ Angular، أضف إلى ذلك أنّك لن تحتاج إلى الدّعم الفنّي من فريق تطوير Angular إن لم تكن تنوي أن يصبح مشروعك كبيرًا بالقدر الّذي يعتبره مهندسو Google مثاليًّا. هناك مشكلة أخرى تظهر عندما تحاول دمج شيفرات Angular مع شيفرات غير Angular وتريدها أن تعمل إلى جانب تغليف Angular للبيانات أو آلية إخراج الصفحة فيها، سيكون عليك عندها الغوص في تفاصيل غامضة في Angular غوصًا عميقا، قارن ذلك مع Backbone التي تعد طبقة رقيقة فوق jQuery فهي أصغر وأقرب للفهم، وإن كان تطبيقك معتمدًا بشكل كبير على ملحقات jQuery فسيكون عليك استخدام Backbone لتزيد من التّحسين في تصميم تطبيقك. أخيرًا، إنّ لطريقة Angular الأساسيّة للرّبط ثنائي الاتجاه بين عناصر واجهة المستخدم وكائنات النمذجة حدودًا تعتمد على مدى تعقيد التّطبيق، وقد تشارك المطوّرون في Facebook خيبة أملهم بربط البيانات ثنائيّ الاتجاه فقالوا: وبالرّغم من كون مكتبة React الخاصة بـFacebook بالكاد تهتمّ بالإخراج ومن ثمّ فهي تعدّ حلّا جزئيًّا بالمقارنة مع Angular، إلّا أنّ تطبيقات طرف الزّبون التي تعتمد عليها تستحقّ التقدير حقًّا. أسباب هامة تدفعك لاستخدام Angularلا شك أنّ Angular هي أشهر مكتبات JavaScript المختصّة بحلول النّمذجة والعرض عالميًّا في هذه الأيام، فقد حصلت على أكثر من 33000 نجمة على GitHub، وربّما ازدادت أكثر من ذلك بكثير منذ وقت كتابة هذه السلسلة إلى اليوم، فقد تربّعت على قمّة قائمة الحلول المطروحة التي تمّت دراستها ومقارنتها بواسطة مشروع TodoMVC وسنناقش الآن بعض أسباب نجاح Angular. الإنتاجية الآنيةإذا كان مشروعك بحاجة إلى واجهة مستخدم معقّدة إلى حدّ ما لتقوم بعمليات إدارة البيانات CRUD على البيانات من طرف الزّبون، فإنّ Angular ستفي بوعدها لك بتحقيق إنتاجيّة شبه آنيّة، فبإضافتك لرشّةٍ من بعض الخصائص المميّزة غلى نصّ HTML الأصلي، والقليل من شيفرات Javascript، ستتمكّن من جعل صفحتك تتفاعل مع المستخدم، في حين كنت ستحتاج إلى الكثير من المهارات وبذل الجهود لتقوم بذلك باستخدام مكتبة أدنى مستوى. الألفةتعتمد Angular على كتابة شيفرات JavaScript السّهلة ونصوص مطابقة تقريبًا لتعليمات HTML، لقد قلت "تقريبًا" لأنّ Angular تقدّم عناصر وخصائص جديدة وبعض الشيفرات المستغربة، إلّا أنها بالمقارنة مع أنظمة القوالب الأخرى تبقى قريبةً جدًّا إلى HTML النّقيّة، وهذا يجعلها سهلة الفهم لأغلبية مطوري الويب. المرونةتتبنّى Angular التّوجّه الحالي نحو التكيف مع واجهات المستخدم في أطر عمل JavaScript، دون الكثير من التّضحية بإنتاجيّتها، فإن كنت تحبّ العمل مع أحد أطر العمل الشهيرة المختصة بواجهة المستخدم مثل Bootstrap فستتمكّن من الاستفادة من الإضافة المقدّمة من مشاريع طرف ثالث مثل AngularUI لتقوم بتكامل سهل. لذا سواء كنت تريد زخرفة صفحة ويب تقليديّة ببعض التّطبيقات التفاعلية هنا وهناك، أو كنت تريد تطوير تطبيقٍ كاملٍ وحيد الصّفحة فإن Angular ستكون الأفضل للعمل ضمن شروطك ومحدداتك. المعايير المستقبليةلا أدري أيّهما أكثر صحّة، إن كانت Angular متبصّرة بالمستقبل أو أنّ Google والدةَ Angular ستصنع المستقبل، إلا أنّه من الواضح أنّ استخدامك لـAngular سيكون طريقة ناجحة لتألف المعايير المقترحة مثل Web Components وأضف إلى ذلك أنّني أتوقّع بقاء Angular واستمرارها لأنّها تواكب ميزات JavaScript المرتقبة مثل Object.observe. مجتمع Angularلابدّ أنّ أحد أقوى الأسباب لاختيار Angular بدلًا من منافساتها هو أنّها الأكثر شيوعًا، فهي تُستخدم الآن في عدد غير محدود من مواقع الويب كثيرة الزيارة، ولكن تذكّر بأنّ الجري وراء الأغلبية ليس الصّواب دومًا وليس لكلّ النّاس ولا في كلّ الأحيان، وعليك أن تدرس متطلّبات مشروعك الحقيقيّة بعناية. عن هذه السلسلةلقد جاءتني فكرة الكتب التفاعلية منذ عدّة سنوات أثناء قراءتي لكتاب إلكترونيّ عن البرمجة باستخدام JavaScript على حاسوبي المحمول، فقد توجّب عليّ لتجربة أحد أمثلة الكتاب أن أقوم بتحميله من موقع النّاشر، ثم البحث عن موضع الملفّ الذي تم تحميله وإيجاد مكان لفكّ ضغطه، ثم الإبحار عبر الكثير من الملفّات إلى أن أعثر على الشّيفرة المطلوبة، وأفتحها أخيرًا في أحد المحرّرات، ثمّ أكتشف بأنّ عليّ إنشاء ملف HTML ليُشغّل السكربت، وبعد كلّ هذا لا تعمل الشيفرة وأكون قد ضيّعت وقتي في مطاردة أماكن الملفّات والارتباطات. لم كلّ هذا؟ إن كنت أقرأ كتابًا عن تقنية front-end على الحاسوب فهل عليّ أن أعاني كلّ تلك المعاناة لأشغّل المثال؟ لقد اكتشفت بعد مدّة قصيرة من ذلك النّسخةَ الأولى لكتاب Marijn Haverbeke المسمّى Eloquent JavaScript واكتشفت بأنّه بإمكاني استخدام مشروعه CodeMirror لأنشر رؤيتي الخاصّة: كتاب إلكترونيّ رائع المظهر، أنيق الحروف وجميل التّصميم، ولكن مع أمثلة حية قابلة للتّعديل والتّشغيل مباشرة من الدّرس ذاته، ولهذا ستجد جميع أمثلة السلسلة داخل محرّرات حيّة تفاعليّة، وستجد مخرجات هذه الشيفرات مباشرة تحت المثال داخل صندوق iframe. <div ng-app=""> <strong>The lucky number {{11+12}}</strong> </div> See the Pen angular-intro by Hsoub Academy (@HsoubAcademy) on CodePen. هيّا حاول تغيير المثال أعلاه الآن، رغم أنك لم تتعلم شيئًا بعد عن Angular، قم ببعض التغييرات على المثال وراقب التّغييرات. ماذا بعد؟سيأخذك الفصل الأوّل من هذه السلسلة في رحلة لطيفة للتّعرف على مبادئ Angular في القولبة templating من طرف الزّبون وفي الرّبط ثنائيّ الاتّجاه، وقد أقرّ العديد من المطوّرين المحترفين في ِAngular بأنهم استفادوا من هذا الفصل رغم أنّ جميع أمثلته بسيطة ويمكن فهمها بلمحة سريعة. ترجمة وبتصرّف للجزء الأول من كتاب: Angular Basics لصاحبه: Chris Smith.
  17. سنتطرّق في هذا الدّرس إلى طريقة إنشاء رسم بياني باستخدام 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. المصادر
  18. إن عالم الويب أصبح جزءًا لا يتجزأ من حياتنا اليومية هذا ما يجعلنا نبتكر طُرقًا وأساليبَ جديدة ليكون قريبًا منا أكثر. نحنُ كمطوري ويب بحاجة دائمًا إلى إضافةِ لمساتٍ ساحرة وتصاميمَ مبتكرة لإنعاش موقع الويب والتفرّد بعنصر الإبداع في إظهار الموقع بأبهى حلّة وأجمل تصميم. سأضع بين يديك مقدمة لأهم خيارين قد يختار منهما مطوّر الويب عند الشروع في بناء وتصميم محتوى وعناصر موقع الويب. الخيار أو الأسلوب الأوّل وهو الأكثر شهرة بحكم قدمه هو العمل مع عناصر 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 تشحذ أفكارك الابداعية بامتياز. المصادر
  19. رغم أنّ تحويلات CSS3 ثلاثية الأبعاد صار لها فترةً لا بأس بها من الزمن، إلّا أنّني اكتشفت عدم امتلاك الخبرة الكافية للعمل معها بعد. أستخدم Windows 8 منذ فترة من الوقت، ومن أوّل الأمور التي لفتت نظري كانت الانتقالات transitions والتحريكات animations المبنية ضمن لوح البداية Start Dashboard، لذلك خطرت ببالي فكرة رائعة، وهي لماذا لا تكون خبرتي الأولى مع تحويلات CSS3 ثلاثية البعد ببناء تطبيق يماثل في سلوكه تلك التحريكات والتأثيرات في Windows 8؟ وهذا ما حدث في هذا الدرس. سأستخدم خصائص CSS بدون أي بادئة prefix وذلك بغرض الاختصار، لكنك ستجد الخصائص كاملة ضمن النص المصدري للمشروع على Github. ستعمل مقاطع الشيفرة التي ستجدها هنا على متصفحات تدعم خصائص CSS المستخدمة. الرماز The Markupبنية هذا التطبيق بسيطة: يتكون لوح البداية من قائمة من القطع tiles بثلاثة قياسات وهي الصغير، الكبير، والكبير جدًا. لكل قطعة من هذه القطع صفحة page مرتبطة معها. والصفحة عبارة عن تغطية overlay تظهر عند النقر على إحدى القطع في لوح البداية. سنعتبر الصفحة أنّها تُحاكي تطبيق سطح مكتب في Windows 8، فتكون القطعة كما هو واضح اختصارًا له (للصفحة). عند النقر على قطعة ستُفتح الصفحة الموافقة لها. يوجد نوعان من الانتقالات للصفحة بعد فتحها يدعمهما اللوح في Windows 8. يفتح أحدهما الصفحة بتأثير دوران ثلاثي الأبعاد اعتبارًًا من يمين الشاشة، أمّا الآخر فتنزلق فيه الصفحة من وإلى اليسار. سنعرّف صنف CSS لكل نوع من نوعيّ الصفحة، حيث سيكون الصنف s-page للصفحات التي تنزلق من وإلى اليسار، أمّا الصنف r-page فسيكون للصفحات التي تدور من اليمين. ينبغي علينا من أجل كل قطعة تعيين نوع الصفحة التي ستفتحها القطعة (بالاعتماد على التأثير الذي نريده من أجل هذه الصفحة). سنعرّف نوع الصفحة لكلّ قطعة باستخدام سمة attribute مخصّصة سنسميها data-type-page، ستهتم هذه الـسمة بتطبيق أسماء أصناف CSS الصحيحة التي ستفعّل التحريكات المناسبة فيما بعد. يجب أن يكون لكل صفحة اسم. سيختلف اسم الصفحة لتطبيق معيّن عن اسم الصفحة لتطبيق آخر، فمثلًا الـقطعة المسمّاة Skype ستفتح صفحة اسمها skype-app وهكذا دواليك. لقد استخدمت اسميّ صفحة فقط في هذا المثال، وقد كرّرتهما من أجل جميع الـقطع الباقية بغرض التبسيط، كما استخدمت الاسم custom-page لآخر قطعة وذلك على سبيل المثال. ربما تحتاج أن تضيف صفحة مختلفة لكل قطعة، وهذا يعني اسم صفحة مختلف لكلٍّ منها. إليك الرماز الخاص بكامل اللوح (الـقطع والصفحات): <div class="demo-wrapper"> <div class="s-page random-restored-page"> <div class="page-content"> <h2 class="page-title">Some minimized App</h2> <div class="close-button s-close-button">x</div> </div> </div> <div class="s-page custom-page"> <div class="page-content"> <h2 class="page-title">Thank You!</h2> <div class="close-button s-close-button">x</div> </div> </div> <div class="r-page random-r-page"> <div class="page-content"> <h2 class="page-title">App Screen</h2> <p>Chew iPad power cord chew iPad power cord attack feet chase mice leave dead animals as gifts and stick butt in face chew iPad power cord. Chase mice. Run in circles use lap as chair why must they do that. Intrigued by the shower destroy couch leave hair everywhere sleep on keyboard chew iPad power cord. Use lap as chair. Missing until dinner time stand in front of the computer screen, intently sniff hand. Find something else more interesting. Destroy couch play time so inspect anything brought into the house hate dog burrow under covers. Sleep on keyboard destroy couch so hate dog so hide when guests come over. Chase mice destroy couch lick butt throwup on your pillow use lap as chair yet intrigued by the shower but climb leg. Stare at ceiling make muffins or hunt anything that moves claw drapes. Intently sniff hand intrigued by the shower. Why must they do that. Cat snacks leave dead animals as gifts or inspect anything brought into the house sweet beast so stare at ceiling give attitude. Flop over claw drapes but sun bathe lick butt, and chase mice. Rub face on everything lick butt leave hair everywhere lick butt, missing until dinner time for use lap as chair lick butt. Make muffins leave dead animals as gifts play time. Chew foot intrigued by the shower stare at ceiling inspect anything brought into the house yet hopped up on goofballs. Hunt anything that moves intently sniff hand for hunt anything that moves play time. Chew foot climb leg throwup on your pillow so lick butt yet make muffins hate dog. Intrigued by the shower. Intently sniff hand shake treat bag. Cat snacks burrow under covers make muffins but all of a sudden go crazy find something else more interesting. Flop over chase mice. Give attitude. Inspect anything brought into the house. Stick butt in face sun bathe so find something else more interesting and intrigued by the shower. Rub face on everything use lap as chair. Under the bed claw drapes chase mice but leave hair everywhere yet make muffins yet claw drapes. Use lap as chair. Find something else more interesting stretch for under the bed. Nap all day intrigued by the shower, hate dog sweet beast intently sniff hand so hate dog nap all day. Swat at dog hide when guests come over and mark territory chase mice for cat snacks. Use lap as chair. Lick butt throwup on your pillow need to chase tail. Mark territory. Stick butt in face shake treat bag yet hunt anything that moves, yet hopped up on goofballs yet stare at ceiling under the bed. Give attitude chase imaginary bugs stretch so hunt anything that moves so hide when guests come over but intrigued by the shower find something else more interesting. Make muffins behind the couch for chew foot. Sweet beast flop over but throwup on your pillow. Intently sniff hand use lap as chair and missing until dinner time and chase imaginary bugs. </p> </div> <div class="close-button r-close-button">x</div> </div> <div class="dashboard clearfix"> <ul class="tiles"> <div class="col1 clearfix"> <li class="tile tile-big tile-1 slideTextUp" data-page-type="r-page" data-page-name="random-r-page"> <div><p>This tile's content slides up</p></div> <div><p>View all tasks</p></div> </li> <li class="tile tile-small tile tile-2 slideTextRight" data-page-type="s-page" data-page-name ="random-restored-page"> <div><p class="icon-arrow-right"></p></div> <div><p>Tile's content slides right. Page opens from left</p></div> </li> <li class="tile tile-small last tile-3" data-page-type="r-page" data-page-name="random-r-page"> <p class="icon-calendar-alt-fill"></p> </li> <li class="tile tile-big tile-4" data-page-type="r-page" data-page-name="random-r-page"> <figure> <img src="images/blue.jpg" /> <figcaption class="tile-caption caption-left">Slide-out Caption from left</figcaption> </figure> </li> </div> <div class="col2 clearfix"> <li class="tile tile-big tile-5" data-page-type="r-page" data-page-name="random-r-page"> <div><p><span class="icon-cloudy"></span>Weather</p></div> </li> <li class="tile tile-big tile-6 slideTextLeft" data-page-type="r-page" data-page-name="random-r-page"> <div><p><span class="icon-skype"></span>Skype</p></div> <div><p>Make a Call</p></div> </li> <li class="tile tile-small tile-7 rotate3d rotate3dX" data-page-type="r-page" data-page-name="random-r-page"> <div class="faces"> <div class="front"><span class="icon-picassa"></span></div> <div class="back"><p>Launch Picassa</p></div> </div> </li> <li class="tile tile-small last tile-8 rotate3d rotate3dY" data-page-type="r-page" data-page-name="random-r-page"> <div class="faces"> <div class="front"><span class="icon-instagram"></span></div> <div class="back"><p>Launch Instagram</p></div> </div> </li> </div> <div class="col3 clearfix"> <li class="tile tile-2xbig tile-9" data-page-type="custom-page" data-page-name="random-r-page"> <figure> <img src="images/summer.jpg" /> <figcaption class="tile-caption caption-bottom">Fixed Caption: Some Subtitle or Tile Description Goes Here with some kinda link or anything </figure> </li> <li class="tile tile-big tile-10" data-page-type="s-page" data-page-name="custom-page"> <div><p>Windows-8-like Animations with CSS3 & jQuery © Sara Soueidan. Licensed under MIT.</p></div> </li> </div> </ul> </div> </div>حصلت على خط الأيقونة الذي استخدمته من Icomoon. الذي سيحدث الآن: ستحصل JavaScript على اسم ونوع الصفحة المراد فتحها عند نقر القطعة، وبعد ذلك، ووفقًا لنوع الصفحة ستعمل JavaScript على تطبيق أسماء أصناف CSS المناسبة على الصفحة (والتي سنحصل على اسمها أيضًا من السمة data-page-name) لفتحها بنمط تحريك معيّن لكل صنف CSS يتم تطبيقه. تنسيقات CSSأرجو ملاحظة أنّني أستخدم أنماط تنسيق تراعي الأجهزة المحمولة أولًا mobile-first، والتي سنجعلها لاحقًا ذات استجابية responsive عالية ضمن قسم استعلامات الوسائط media queries في CSS. سنتناول في البداية التنسيقات الخاصة بالحاوية (عنصر div) والتي ستحوي كامل المثال التوضيحي. سنعرّف تنسيقات عامّة ونتأكّد من ضبط الخاصية perspective بحيث يؤدي ذلك إلى تفعيل الفضاء ثلاثي البعد 3D، وإلّا سيبدو كامل المثال بشكل منبسط أي ثنائي البعد 2D. .demo-wrapper { padding: 2em .5em; width: 100%; height:100%; perspective: 3300px; position: relative; }لنبدأ الآن بتنسيقات وتحريكات لوح البداية. ستُنفَّذ أوّل تحريكة سنطبّقها على لوح البداية عند تحميل الصفحة ضمن متصفّح الويب. سيكون لوح البداية مخفيًّا في يمين الشاشة بادئ الأمر، ثمّ يظهر تدريجيًّا مع الانتقال إلى موقعه الأساسي عند تحميل الصفحة. .dashboard { margin: 0 auto; width: 100%; padding: 1em; transform: translateX(200px); opacity:0; animation: start 1s ease-out forwards; } @keyframes start{ 0%{ transform: translateX(200px); opacity:0; } 50%{ opacity:1; } 100%{ transform: translateX(0); opacity:1; } }يتضاءل لوح البداية ويختفي عند نقر قطعة منه. حيث ينتقل اللوح على طول محور z، ويتضاءل حجمه، كما تنخفض قيمة الخاصية opacity له تدريجيًا حتى تصبح في النهاية تساوي الصفر مما يعطي شعورًا بأنّ اللوح يختفي بالتدريج. أمّا عندما يُغلق المستخدم الصفحة فإنّ لوح البداية يعود للظهور مرّة أخرى. بالنسبة للأعمدة الثلاثة في لوح البداية فإنّها تظهر تدريجيًّا واحدًا تلو الآخر مع تأخير زمني طفيف بينها. عندما تُغلق الصفحة، سيُضاف اسم صنف CSS لكل عمود (بواسطة JavaScript) وكل من هذه الأصناف ستُفعّل تحريكة بتأخير زمني مُحدّد. فيما يلي أصناف CSS والتحريكات المطبّقة على لوح البداية عند نقر القطع tiles وإغلاق الصفحات. .fadeOutback{ animation: fadeOutBack 0.3s ease-out 1 normal forwards; } .fadeInForward-1, .fadeInForward-2, .fadeInForward-3 { opacity:0; transform: translateZ(-5em) scale(0.75); animation: fadeInForward .5s cubic-bezier(.03,.93,.43,.77) .4s normal forwards; } .fadeInForward-2{ animation-delay: .55s; } .fadeInForward-3{ animation-delay: .7s; } @keyframes fadeOutBack{ 0% {transform: translateX(-2em) scale(1); opacity:1;} 70% {transform: translateZ(-5em) scale(0.6); opacity:0.5;} 95% {transform: translateZ(-5em) scale(0.6); opacity:0.5;} 100% {transform: translateZ(-5em) scale(0); opacity:0;} } @keyframes fadeInForward{ 0% {transform: translateZ(-5em) scale(0); opacity:0;} 100% {transform: translateZ(0) scale(1); opacity:1;} }وبالنسبة لتنسيقات الصفحات: .r-page { width: 100%; height: 100%; text-align: center; font-size: 2em; font-weight: 300; position: absolute; right: 0; top: 0; left:0; bottom:0; opacity: 0; color: white; z-index: 10; padding:10px; transform-origin: 100% 0%; transform: rotateY(-90deg) translateZ(5em) } .s-page { color: white; z-index: 10; text-align: center; font-size: 2em; font-weight: 300; } .page-content{ overflow-y:auto; max-height:100%; font-size:.6em; padding:.6em; text-align:left; } .s-page, .r-page{ background-color: white; color:black; } .page-title { margin: .25em 0; font-weight: 100; font-size: 3em; text-align:center; } .close-button { font-size: 1.5em; width: 1em; height: 1em; position: absolute; top: .75em; right: .75em; cursor: pointer; line-height: .8em; text-align: center }لقد ضبطت الموقع الأصلي لكل صفحة من النوع r-page في الفضاء ثلاثي البعد بتدويرها حول محور التراتيب (محور y) بعد ذلك نقل الصفحة بمقدار 5em إلى يسار الشاشة باستخدام الخاصية translateZ (النقل على محور z). ينبغي ألّا ننسى أنّه عند تحويل (نقل – تدوير) عنصر في الفضاء ثلاثي البعد فمن الضروري تحويل نظام الإحداثيات الخاص به بنفس الصورة. الذي نريده الآن هو نقل الصفحة بمقدار 5em إلى يسار الشاشة، ولكن لاحظ أنّنا بدلًا من استخدام translateX استخدمنا translateZ، ويعود سبب ذلك إلى أنّه بعد التحويل الأوّل (الدوران حول محور y) يدور نظام الإحداثيات أيضًا، وهكذا يُشير محور z في هذه الحالة إلى اليسار وليس إلى الأعلى، أمّا محور الفواصل (محور x) فسيشير باتجاه المستخدم. يكون لجميع الصفحات باستثناء النوع s-page نفس موقع البداية في الفضاء ثلاثي البعد. بالنسبة للصفحات من النوع s-page فإنّها تكون بموقع يبعد بما يُعادل -150% يسار الشاشة (واضح أنّها لن تكون مرئية بهذه الحالة)، بحيث أنّها تنزلق لتعود إلى الشاشة عند تفعيل التحريكة الخاصة بها. عند نقر قطعة مرتبطة بصفحة ما، فسيُضاف صنف CSS الموافق بواسطة JavaScript إلى الصفحة، بالنتيجة ستحصل الصفحة على اسم صنف CSS يُعرّف التأثير ثلاثي البعد الواجب تطبيقه عليها. فيما يلي أسماء أصناف CSS التي تُفعّل عملية فتح وإغلاق الصفحات، بالإضافة إلى التحريكات المعرّفة من أجل كل صنف. .openpage{ animation: rotatePageInFromRight 1s cubic-bezier(.66,.04,.36,1.03) 1 normal forwards; } .slidePageLeft{ transform: rotateY(0) translateZ(0); opacity: 1; animation:slidePageLeft .8s ease-out 1 normal forwards; } .slidePageInFromLeft{ animation: slidePageInFromLeft .8s cubic-bezier(.01,1,.22,.99) 1 0.25s normal forwards; } .slidePageBackLeft{ opacity: 1; left: 0; animation: slidePageBackLeft .8s ease-out 1 normal forwards; }لاحظ أنّني أستخدم الخاصية animation بالشكل المختصر. تعود القيمة الأخيرة forward ضمن الخاصية animation إلى الخاصيّة الفرعية animation-fill-mode، وهذه القيمة ضرورية لها، وإلّا ستعود الصفحة إلى حالتها الابتدائية (المغلقة) فور انتهاء التحريكة التي ستُظهر الصفحة. إذًا لكي نُبقي الصفحة مفتوحة، ولكي نكون قادرين على إنشاء تحريكات متلاحقة، يجب على العنصر أن يبقى مُحتفظًا بحالته النهائية المعرّفة ضمن تحركية ما، ومن هذه "الحالة النهائية" يبدأ عمل التحريكة التالية، وهكذا. فيما يلي التحريكات لأصناف CSS المطبّقة على الصفحات. @keyframes rotatePageInFromRight{ 0% {transform:rotateY(-90deg) translateZ(5em);opacity:0} 30% {opacity:1} 100% {transform: rotateY(0deg) translateZ(0) ; opacity:1} } @keyframes slidePageLeft{ 0% {left:0; transform: rotateY(0deg) translateZ(0) ; opacity:1} 70% {opacity:1;} 100% {opacity:0; left:-150%; transform: rotateY(0deg)} } @keyframes slidePageInFromLeft{ 0% {opacity:0; } 30% {opacity:1} 100% {opacity:1; left:0;} } @keyframes slidePageBackLeft{ 0% {opacity:1; left:0; transform: scale(0.95);} 10% {transform: scale(0.9);} 70% {opacity:1;} 100% {opacity:0; left:-150%;} }أخيرًا وليس آخرًا، سنُنسّق قطع لوح البداية ونُعرّف الانتقالات والتحريكات المطبّقة عليها عند تحريك مؤشّر الفأرة فوقها. لاحظ أنّ التنسيقات العامّة تُعرّف حجم القطع. .tile{ float: left; margin: 0 auto 1%; color: white; font-size: 1.3em; text-align: center; height: 8em; font-weight: 300; overflow: hidden; cursor: pointer; background-color: #fff; color: #333; position:relative; transition: background-color 0.2s ease-out } .tile-2xbig{ height:16.15em; width:100%; } .tile-big { width: 100% } .tile-small { width: 49%; margin-right: 2% } .tile-small.last { margin-right: 0 }سيحتوي زوج من القطع على صورة مع عنوان لها، هاتان القطعتان ستحصلان على الصنف fig-tile وذلك لتمييز نوعهما في شيفرة JavaScript التي سنراها لاحقًا. سنحصل على الألوان المستخدمة من أجل النص والخلفية لصفحة ما من الألوان المستخدمة في العنوان، لذلك لا تنسى أن تُعرّفهم. بالنسبة للعنوان إمّا أن يكون ثابتًا أو أن يُعطي شعورًا بالانزلاق عندما يتحرك فوقه مؤشّر الفأرة: .tile-caption{ position:absolute; z-index:1; background-color: #455962; color:#fff; font-size:1em; padding:1em; text-align: left; } .caption-bottom{ left:0; bottom:0; right:0; height:40%; } .caption-left{ left:-100%; top:0; bottom:0; width:40%; transition: left .3s linear; } .tile:hover .caption-left{ left:0; }بالنسبة للقطع النظامية regular tiles التي لا تملك أي نوع من التحريك، فستغيّر لون خلفيّتها ولون النص عند تحريك الفأرة فوقها. لكي نتأكّد من أنّ النص مُوسّط عموديًا vertically centered في كل قطعة، سنضع في كل قطعة عنصر div يحوي بدوره عنصر فقرة (العنصر <p>) يحوي النص. سنستخدم القيمة table-cell للخاصية display وذلك لتوسيط النص عموديًا ضمن الفقرة: .tile div{ position:absolute; top:0; left:0; right:0; bottom:0; width:100%; height:100%; text-align:center; display:table; padding:0 1em; transition: all .3s ease; } .tile div p{ display:table-cell; vertical-align:middle; }سنترك الحديث عن أنماط التنسيق العامّة للقطع بهدف الاختصار، ولكن ينبغي علينا التأكّد من أنّنا سنعيّن لون النص والخلفية لجميع القطع، حتى تلك التي ستُغطّى بصورة، لأنّ هذه الألوان التي سنحصل عليها باستخدام JavaScript ستُستخدَم لضبط الألوان في الصفحة المرتبطة بها كما أشرنا قبل قليل. دعونا الآن نتحدّث عن التحريكات والانتقالات التي تحدث على قطعة ما. ستحتوي القطع المزوّدة بنص منزلق على عنصري div، سيبدو كل عنصر div كوجه أو كتلة منفصلة داخل القطعة. سيكون تموضعًا عنصريًا div مطلقًا positioned absolutely وسيتحرّكان عند تحرّك الفأرة فوقهما وذلك وفقًا لاتجاه الانزلاق المطلوب. فلكي ينزلق نص القطعة إلى الأعلى عندما يحوم مؤشّر الفأرة فوقه سنطبّق الصنف slideTextUp: .slideTextUp div:nth-child(2){ top:100%; } .slideTextUp:hover div{ transform: translateY(-100%); } .tile-1 p{ font-size:1.3em; }وبشكل مماثل ولكي ينزلق نص القطعة إلى اليسار وإلى اليمين سنطبّق الصنفين slideTextLeft و slideTextRight على الترتيب. .slideTextRight div:first-child{ left:-100%; } .slideTextRight:hover div{ transform: translateX(100%); } .slideTextLeft div:nth-child(2){ left:100%; } .slideTextLeft:hover div{ transform: translateX(-100%); }أمّا بالنسبة لزوج القطع التي ستنقلب، فسيكون لهما تأثير مختلف عندما يحوم مؤشّر الفأرة فوقهما، فهما يدوران ليُظهران الوجه الخلفي. يُعتبر هذا التأثير نوعًا بسيطًا جدًا من تأثير "انقلاب البطاقة" card flip. لن نخوض في تفاصيل هذا التأثير. لإنجاز هذا التأثير، سنطبّق الصنف rotate3d إلى القطعة التي نريد أن تنقلب. بالنسبة للقطعة التي نريد أن تنقلب عموديًّا، سنطبّق الصنف rotate3dy، أمّا بالنسبة للانقلاب الأفقي سنطبّق الصنف rotate3dx (مع الانتباه إلى وجوب وجود الصنف rotate3d في كلتا الحالتين). انظر إلى تنسيقات CSS التالية: .rotate3d{ perspective: 800px; overflow: visible; } .faces{ transform-style: preserve-3d; transition: transform 1s; } .faces div { display: block; position: absolute; top:0; left:0; right:0; bottom:0; width: 100%; height: 100%; backface-visibility: hidden; }لاحظ بأنّه عند تحريك مؤشّر الفأرة فوقهما بالتتالي، فسيظهر وجه أحدهما في حين ستظهر خلفية الآخر بنفس الوقت. .rotate3dY .back{ transform: rotateY( 180deg ); } .rotate3dX .back{ transform: rotateX( 180deg ); }وعندما يحوم مؤشّر الفأرة فوق القطعة فإنّ أي عنصر div يخضع للصنف faces. سيُدوّر ليُظهر وجهه الخلفي: .rotate3dY:hover .faces:hover{ transform: rotateY( 180deg ); } .rotate3dX:hover .faces:hover{ transform: rotateX( 180deg ); } لتنسيق القطع في الدوران ثلاثي البعد، ينبغي الانتباه إلى ضبط لوني الخلفية والنص للوجه الأمامي (ذو الصنف front.) بحيث يمكن الحصول على هذين اللونين واستخدمهما لاحقًا في الصفحة المتربطة بالقطعة عند فتحها. لنعرّف الآن تنسيقات تفاعلية للوح Dashboard. ستكون أعمدة اللوح ذات عرض كامل على الشاشات الصغيرة في البداية (تذكّر أنّنا نراعي متطلبات تصميم الأجهزة المحمولة أولًا mobile-first)، في حين أنّ هذه الأعمدة ستكون بجانب بعضها البعض في الشاشات الكبيرة. .col1, .col2, .col3 { width: 99%; margin: 1em auto } @media screen and (min-width: 43.75em) { .col1, .col2, .col3 { float: left; margin-right: 1%; width: 49% } .page-title{ font-size:2.5em; } .page-content{ font-size:1em; } .close-button{ font-size:2em; } } @media screen and (min-width: 64em) { .col1, .col2, .col3 { float: left; margin-right: .5%; width: 31% } .col3 { margin-right: 0 } .col1 { margin-left: 2em } .page-title{ font-size:3.5em; } } جافا سكريبت JavaScriptستُعالَج جميع أحداث النقر عن طريق JavaScript. سنستخدم مكتبة jQuery لهذه الغاية، وسنضبط معالج حدث event handler النقر لكل قطعة على لوح البداية، فعندما ينقر المستخدم على قطعة ما، يعمل معالج حدث النقر الموافق على الحصول على اسم ونوع الصفحة المرتبطة بهذه القطعة وذلك من السمتين data-page-name و data-page-type على الترتيب، حيث سنستخدم هذه المعلومات لفتح الصفحة المطلوبة. أمّا عند إغلاق الصفحة عند النقر زر الإغلاق لها، فسيعمل معالج حدث النقر لهذا الزر على تطبيق أسماء الأصناف المناسبة لإغلاق الصفحة. بالإضافة لذلك، ولكي نُكسِب كل صفحة لون خلفية ولون نص مماثل لتلك التي للقطعة المرتبطة بها، فإنّنا سنطوف بدايةً على جميع القطع، ونحصل على ألوانها، ثمّ نُطبّق هذه الألوان على الصفحات المرتبطة معها. في حال كان لقطعة ما الصنف rotate3d، فإنّنا سنبحث عن لون الخلفية لوجه face القطعة، ومن ثمّ نطبّق هذا اللون على الصفحة المرتبطة معها. function(){ $('.tile').each(function(){ var $this= $(this), page = $this.data('page-name'), bgcolor = $this.css('background-color'), textColor = $this.css('color'); if($this.hasClass('rotate3d')) { frontface = $this.find('.front'); bgcolor = frontface.css('background-color'); textColor = frontface.css('color'); } if($this.hasClass('fig-tile')) { caption = $this.find('figcaption'); bgcolor = caption.css('background-color'); textColor = caption.css('color'); } $this.on('click',function(){ $('.'+page).css({'background-color': bgcolor, 'color': textColor}) .find('.close-button').css({'background-color': textColor, 'color': bgcolor}); }); }); function showDashBoard(){ for(var i = 1; i <= 3; i++) { $('.col'+i).each(function(){ $(this).addClass('fadeInForward-'+i).removeClass('fadeOutback'); }); } } function fadeDashBoard(){ for(var i = 1; i <= 3; i++) { $('.col'+i).addClass('fadeOutback').removeClass('fadeInForward-'+i); } } $('.tile').each(function(){ var $this= $(this), pageType = $this.data('page-type'), page = $this.data('page-name'); $this.on('click',function(){ if(pageType === "s-page"){ fadeDashBoard(); $('.'+page).addClass('slidePageInFromLeft').removeClass('slidePageBackLeft'); } else{ $('.'+page).addClass('openpage'); fadeDashBoard(); } }); }); $('.r-close-button').click(function(){ $(this).parent().addClass('slidePageLeft') .one('webkitAnimationEnd oanimationend msAnimationEnd animationend', function(e) { $(this).removeClass('slidePageLeft').removeClass('openpage'); }); showDashBoard(); }); $('.s-close-button').click(function(){ $(this).parent().removeClass('slidePageInFromLeft').addClass('slidePageBackLeft'); showDashBoard(); }); })();وبهذا نكون قد وصلنا إلى نهاية الدرس. أرجو أن يكون ممتعًا ومفيدًا. بإمكانك استعراض مثال حيّ لهذا الدّرس من هنا. أما الشيفرة المصدرية فهي مُتوفّرة في هذا المُستودع. ترجمة -وبتصرّف- للمقال How to Create Windows-8-like animations with CSS3 and JQuery لصاحبته Sara Soueidan.
  20. كما هو موضح في العنوان فإنّك في هذا الدرس سوف تتعلم كيفية إنشاء تأثير وكأنّك تتصفح كتابًا ما. وسوف نستخدم في هذا الدرس إضافة تدعى 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.
  21. في سلسلة من عدة أجزاء سنناقش موضوعًا نظريًّا يُعتبر من أساسيّات هندسة البرامج، وهو أنماط التصميم (Design Patterns)، وسنعتمد لغة JavaScript في نقاشنا لتصاعد شعبيّتها ومرونتها التي تسمح لنا ببناء مشاريعنا وفق أنماط متنوّعة مما سيُسهّل علينا شرح موضوع السّلسلة ما هي أنماط التصميم؟عندما تبدأ بتعلّم البرمجة، فغالبًا ما يكون اهتمامك مُنصبًّا على أن يكون البرنامج قادرًا على إنجاز المهمّة الّتي تريدها قبل كل شيء، أمّا بعد أن تتطوّر مهاراتك، فسينتقل اهتمامك إلى مواضيع أكثر عمقًا، وستبدأ بطرح بعض الأسئلة على نفسك، حتّى قبل أن تبدأ بكتابة البرنامج، من هذه الأسئلة: كيف أبني برنامجي بحيث يسهُل تحسينه فيما بعد؟كيف أتأكّد أن برنامجي سيبقى يؤدّي ما يُتوقّع منه حتّى وإن قمت بتعديل أجزاء منه بعد زيادة تعقيده؟كيف أبني برنامجي بحيث أستطيع إعادة استخدام أجزاء منه في برامج أخرى في المستقبل؟كيف أجعل برنامجي يستخدم أجزاء من مشاريع أخرى كتبها مطوّرون آخرون؟الإجابة على هذه الأسئلة هي واحدة دومًا: اختر نمط التصميم المناسب لمشروعك. لم نُعرِّف بعدُ مفهوم نمط التّصميم، لكنّنا بدأنا نُكوّن فكرة عنه. نمط التّصميم هو وصف لطريقة مُعيّنة في حلّ مشكلة برمجيّة ما، فالعديد من المُشكلات البرمجيّة يمكن حلّها بأكثر من طريقة، ولكلّ طريقة مساوئ ومحاسن، وبعضها قد يكون أكثر مُناسبةً للمشروع الحاليّ، واختيار نمط التصميم المُناسب سيضمن استمرار تطوّر المشروع بسهولة وربّما يُفيدنا في عزل أجزاء منه لإعادة استخدامها في مشاريع أخرى بحيث لا نُضطَّر لكتابتها مرارًا. أغلب الظنّ أنّك تستخدم واحدًا أو أكثر من أنماط التّصميم وإن لم تعرف ما هي أنماط التّصميم بمعناها النّظريّ، فإنشاء أصناف (classes) لمفاهيم مُجرّدة في اللّغات الكائنيّة التّوجّه (object-oriented) وإنشاء نُسخ عنها (instances) وتوزيع هذه الأصناف في ملفّات مستقلّة، هو في الواقع نمط من أنماط التّصميم. يُرجى الانتباه إلى أن التّطبيق العمليّ لأنماط التّصميم يفرض على المطوّر دمج أكثر من نمط معًا وشرحنا لأحدها لا يمكن أن يخلو من استخدام لأنماط أخرى كما سيتبيّن لك بعد انتهاء السّلسلة، إذ يمكن مثلًا إنشاء وحدة (module) تُصدِّر صنفًا (class) وهذا يعني أنّنا استخدمنا نمطين اثنين (نمط الوحدات، ونمط مُشيّد الكائنات constructor) في وقت واحد. نمط الوحدات (Module Pattern)بغرض تبسيط الأمور، سنبدأ بتوضيح أحد أنماط التّصميم الشّائعة في JavaScript، وهو ما يُعرف بنمط الوحدات (module pattern)، والتي ازدادت شعبيّة بعد ظهور Node.js وما أحدثته من تأثير انتقل حتّى إلى أساليب بناء وتصميم المكتبات البرمجيّة الّتي تستهدف المُتصفّحات. الوحدات هي أجزاء مُستقلّة ومعزولة من النّص البرمجيّ للمشروع توفّر مهمّة مُعيّنة، الأمر الّذي يجعل الهدف من كل وحدة واضحًا ومحدّدًا ويُجنّب المشروع الفوضى التي تنتج عن كتابة كامل النّصّ البرمجيّ في كتلة واحدة متداخلة يصعب معها تنقيحه وصيانته. فصل الأجزاء هذا ليس الفائدة الوحيدة الّتي يُقدّمها نمط الوحدات، إذ من خلاله يمكن مُحاكاة مفهوم المكوّنات السّرّيّة (private) والعلنيّة (public) وحماية بعض محتويات الوحدة من الوصول إليها من خارجها في JavaScript، وذلك بإخفائها ضمن الوحدة والامتناع عن تصديرها الأمر الذي يجعل الوصول إليها من خارج الوحدة مستحيلًا كما سنوضّح بعد قليل. تتوفّر في عالم JavaScript أشكال مختلفة لتصميم الوحدات، منها: الكائنات الحرفيّة (object literals)الدّوالّ المغلقة المجهولة (anonymous closures)وحدات CommonJSوحدات AMDوحدات ECMAScript 6الشّكل الأول ربّما هو أبسط الأشكال وأكثرها بدائيّة، وهو يعني ببساطة إنشاء كائن باستخدام صياغة القوسين المعكوفين {} يضمّ خصائص ووظائف متعلّقة بمهمّة واحدة لعزلها وتسهيل استخدامها: var userSettings = { preferences: { privacy: "strict", language: "ar", showEmail: false, available: true, }, updatePreferences: function(newPrefs) { this.preferences = newPrefs; } }بعض خبراء JavaScript لا يعتبرون هذا النّمط وحدةً حقيقيّة لبساطته الشّديدة، فمن الواضح أنّ هذا النّمط أبسط من حاجات التّطبيقات المعقّدة، فغالبًا ما يكون توزيع الوحدات على ملفّات منفصلة أمرًا مرغوبًا أثناء تطوير التّطبيقات ولهذا نحتاج إلى وسيلة لاستيراد هذه الملفّات وتصديرها بما يسمح باستخدام وحدة واقعة في ملفّ من ملفّ آخر، ولهذا الغرض طوّر مجتمع JavaScript خلال الأعوام الماضية أساليب قياسيّة اتّفق على استخدامها على الرّغم من أن اللّغة ذاتها لم تقدّم مفهوم الوحدات إلّا في الإصدار الأخير (ES6)، والذي استلهم من الأساليب السّابقة أصلًا؛ كما أنّنا قد نرغب بحماية كائن مثل preferences في مثالنا السّابق من تعديله بصورة مباشرة. الشّكل الثّاني هو أسلوب أكثر تطوّرًا لإنشاء الوحدات، ويحتاج فهمه إلى فهم معنى الدّوال المُغلقة (closures)، فإذا كانت لدينا دالّة تُعيد عند استدعائها دالّة أخرى، فإنّنا ندعو الدّالة الأخيرة دالّة مُغلقة، ويتاح لهذه الدّالة الوصول إلى المتّغيّرات الّتي كانت مفروضة في الدّالة الأولى حتّى عندما تُستدعى من خارجها: function addNumberToN(n) { return function(number) { return n + number; } } var addTo2 = addNumberToN(2); var five = addTo2(3); // 5هذه الخاصيّة في JavaScript تسمح لنا بعزل المتّغيّرات (encapsulation) ضمن الدّالة الخارجيّة مع الاحتفاظ بإمكانيّة الوصول إليها من الدّوال والكائنات الفرعيّة، الأمر الذي يحاكي مفهوم خصوصيّة المتغيّرات في لغات البرمجة الأخرى (access modifiers). لنفترض مثلًا أنّنا نريد أن نقوم بإنشاء عدّاد لعدد النّقرات على زرّ معيّن في تطبيقنا، ولا نريد الاحتفاظ بهذه القيمة في النّطاق العامّ لأنّ هذا قد يعرّضها للتّعارض من أسماء مُتغيّرات أخرى أو يجعلها قابلة للتّعديل من إضافات خارجيّة في المتصفّح، لهذا نقوم بإنشاء دالّة مُغلقة تُحيط بهذه القيمة: function() { var counter = 0; return { increaseCounter: function() { counter++; } } }وبهذا نكون قد قيّدنا إمكانيّة تعديل قيمة المتغيّر بزيادته فقط، وعبر الدّالة increaseCounter()‎ فقط. لا يمكن استخدام الدّالّة increaseCounter()‎ إلا بعد استدعاء الدّالة المجهولة (anonymous) الّتي تُحيط بها، ويتمّ هذا كما يلي: (function() { var counter = 0; return { increaseCounter: function() { counter++; } } })()لتُصبح الدّالة increaseCounter()‎ مُتاحة في النّطاق العامّ، وبهذا نكون حصلنا على شكل بدائي لفكرة "تصدير الوحدات" (module exports). لنستعرض الآن الأساليب الأخرى لإنشاء الوحدات، ولعلّ أكثر هذه الأساليب شيوعًا أسلوب CommonJS‏ المُعتمد في Node.js‏، والذي يُتيح استيراد الوحدات باستخدام الدّالّة require()‎: var UrlMaker = require("./url-maker"); var url_maker = new UrlMaker("Hello World!"); var url = url_maker.make(); console.log(url); // hello-world;وأمّا تصدير الوحدات لإتاحة استخدامها، فيتم بإسناد الخصائص إلى الكائن module.exports، كما في الملفّ url-maker.js الموجود في مسار العمل الحالي: module.exports = function UrlMaker(string) { return { make: function() { return string.toLowerCase().replace(/\s+/gi, "-").replace(/[!?*%$#@`]/gi, "") } } }لا يقتصر استخدام أسلوب CommonJS على بيئة Node.js، بل يمكن نقله إلى المتصفّحات باستخدام برامج مثل Browserify‏. من الأساليب الشائعة لإنشاء الوحدات كذلك نمط وحدات AMD (اختصارًا لـAsynchronous Module Definition) ولعلّه يُستخدم بكثرة مع مكتبة require.js‏ في المتصفّحات والّتي تسمح بتحميل الوحدات (الموزّعة كلّ منها على ملفّ منفصل) عند الحاجة إليها، إذ يتمّ التّصريح عن كلّ وحدة وما تعتمد عليه من وحدات أخرى وتقوم require.js بتلبية هذه المتطلبات بتحميل ملفّات الوحدات المنفصلة. في المثال التّالي، نُصرّح عن حاجة موقعنا لمكتبتي jQuery وUnderscore ووحدة أخرى قمنا بإنشائها بأنفسنا: require(["jquery", "underscore", "user_profile"], function($, _, UserProfile) { var user = new UserProfile(); user.firstName = $("#form input[name='first']").text(); user.lastName = $("#form input[name='last']").text(); user.username = $("#form input[name='username']").text(); // ... })ويتمّ التّصريح عن الوحدة user_profile في ملفّ منفصل باسم موافق: define(function() { function Profile() { /* ... */ } return Profile; })يوفّر الإصدار الجديد من JavaScript‏ (ES6) دعمًا أساسيًّا لتعريف الوحدات واستيرادها وتصديرها، وسنتطرّق له بالتّفصيل في الجزء القادم من سلسلة التّعريف بميزات ES6 الجديدة، لكنّ لا بأس من أن نتعرّف عليه سريعًا: import { jQuery as $ } from "/jquery.js"; import { Profile as UserProfile } from "/user.js"; var user = new UserProfile(); user.firstName = $("#form input[name='first']").text(); // ...وتُنشئ الوحدة وتُصدَّر في الملفّ user.js: class Profile { constructor() { // ... } validate() { // ... } } export { Profile };فوائد استخدام الوحداتلنُلخِّص إذن فوائد الوحدات: تنظيم النّصّ البرمجيّ للمشاريع الضّخمة بحيث يسهل فهم بنية البرنامج وتنقيحه ومتابعة صيانته في المستقبل.إدارة المُتطلّبات (dependencies): توضيح العلاقة بين مكوّنات المشروع، بحيث نستطيع إدارة ما تتطلّبه كلّ وحدة وتحميل هذه المتطلّبات آليًّا بالتّرتيب الصّحيح بدل الحاجة إلى التّصريح عن روابط المكتبات الخارجيّة في الصّفحة الرئيسيّة للموقع وإعادة ترتيبها كلّ ما تطلّب الأمر إضافة مكتبة جديدة تعتمد على أخرى.العزل (encapsulation): حماية أجزاء من المشروع من العبث بها سهوًا أو من نصوص برمجيّة خارجيّة، وذلك بعزلها ضمن نطاق فرعيّ خلافًا لتركها في النّطاق العامّ. ففي الحالة الطّبيعيّة تكون متغّيرات JavaScript المفروضة في النّطاق العامّ مُتاحة لأي دالّة، وأما عند فرض هذه المُتغيّرات ضمن الوحدات، فإنّ الوصول إليها يُصبح محدودًا بما هو داخل الوحدة ذاتها، ويتمّ ذلك باستغلال مفهوم النّطاقات (scopes) في JavaScript وإحاطة هذه المتغيّرات بدالّة تُغلّفها بنطاق فرعيّ ثمّ إعادة كائن يحوي فقط الخصائص الّتي نريد إتاحتها للعموم.متى أستخدم هذا النّمط؟يُنصح باستعمال هذا النّمط في المشاريع الضّخمة كتطبيقات الويب المُعقّدة الّتي تعمل في المتصفّح، إذ تكون الحاجة مُلحّة لتجزئة المشروع وتطوير كلّ جزء بصورة مستقلّة ثم ربط هذه الأجزاء مع بعضها إمّا بهدف تسهيل تطوير المشروع أو إدارة المتطلّبات بحيث تُجلَب عند الحاجة إليها نظرًا لكبر حجمها أو تأثيرها على أداء التّطبيق، كما يُنصح باستخدامه عند الحاجة لعزل تفاصيل الوحدة عن النّطاق العامّ. المصادر: كتاب JavaScript Design Patterns‏ لمؤلّفه Addy Osmani‏JavaScript Module Pattern: In-Depth‏وثائق require.js‏وثائق Node.js‏
  22. سوف نتطرق في هذا الدرس إلى كيفية تنسيق وتخصيص عناصر <"input type="file> بالطريقة الصحيحة والسليمة وباستعمال العنصر <label> وبعض الجافاسكربت. معاينة النتيجة النهائية. يمكنك تحميل الشيفرة المصدرية للأمثلة من هنا. هناك العديد من الطرق لتخصيص العنصر <"input type="file> وقد جربت العديد منها ولكنها لم تعجبني ولم تُلبّي متطلباتي. لذلك حاولت البحث في Google ولكني لم أجد مبتغاي. وبعد أن فقدت الأمل وظننت أنني لن أجد ما أبحث عنه وقعت عيني بالصدفة على أحد التعليقات الموجودة في موقع StackOverflow، وكان ذلك التعليق يحتوي على كلمة "<label>" وكان ذلك بداية الخيط وأعتقد أنّه ما كنت أبحث عنه. وكما تعلمون فالنقر على عنصر label يؤدي إلى تفعيل أحد عناصر <input> مرتبطة به، ومما يثير الاهتمام أنّه إذا كان ذلك العنصر عبارة عن <"input type="file> فإنّ النقر على الـlabel المرتبطة به يؤدي إلى فتح متصفح الملفات وهذا هو الحل المثالي الذي كنت أبحث عنه. <input type="file" name="file" id="file" class="inputfile" /> <label for="file">Choose a file</label>أي أنّ النقر على أي واحد من هذين العنصرين (<label> أو <"input type="file>) سوف يعطي نفس النتيجة وهي فتح متصفح الملفات، وهذا يعني أنّ أصعب جزء قد تم حلُّه. لن نحتاج إلى جافاسكربت أو حلول معقدة، كل ما نحتاجه هو السطرين البرمجيين الموجودين في الأعلى. أنظر إلى الصورة في الأسفل. دعونا الآن نقوم بتنسيق العناصر حتى تبدو وكأنّنا نملك زرا عاديا. إخفاء عنصر <input>في البداية يجب علينا إخفاء العنصر <input>، وسوف تتكفل الخاصيتين display: none أو visibility: hidden بذلك. لماذا نريد إخفاءه؟ لأنّ قيمة المُدخل (input) لن يتم ارسالها إلى الخادوم عندما نقوم بعمل تسليم (submit) للنموذج. والسبب الثاني هو أننا لا نريد أن يتم تحديد ذلك العنصر عندما يقوم الزائر بتصفح الموقع باستعمال الزر tab الموجود على لوحة المفاتيح (لأننا نريد لموقعنا أن يكون قابل للوصول accessible). وبناءً على ذلك قمت باستعمال تنسيقات CSS التي تراها في الأسفل التي سوف تعمل على إخفاء العنصر عن أنظارنا ولكنه سيبقى مرئي بالنسبة للمتصفح نفسه: .inputfile { width: 0.1px; height: 0.1px; opacity: 0; overflow: hidden; position: absolute; z-index: -1; }قد تتسائل لماذا وضعنا القيمة 0.1px لكل من العرض والارتفاع وليس 0px. يعود السبب في ذلك إلى أنّه إذا أعطينا عنصر ما عرض وارتفاع بقيمة 0px فإنّنا لن نتمكن من استخدام زر tab على تلك العناصر في بعض المتصفحات. وأمّا بالنسبة للخاصية position: absolute فقد استخدمناها حتى نمنع أن يتداخل العنصر مع العناصر الأخرى. تنسيق العنصر <label>بما أنّ العنصر <label> هو الزر افتراضيًا فإننا نستطيع تنسيق هذا العنصر كما نريد. سنقوم بشيء بسيط هنا ولن نجعل التنسيقات معقدة: .inputfile + label { font-size: 1.25em; font-weight: 700; color: white; background-color: black; display: inline-block; } .inputfile:focus + label, .inputfile + label:hover { background-color: red; }الوصولية (accessibility)كيف يمكنك أن تعرف بأنّ أحد عناصر الصفحة قابل للنقر عليه؟ هناك شيئان يدلان على ذلك، الأول هو أنّ العنصر يجب أن يظهر عليه ذلك، بحيث يعطيك شعورًا بأنّه يمكنك النقر عليه أو استعمال زر tab، والثاني هو أنّه يجب أن يتغير مؤشر الفأرة إلى شيء مناسب عندما تقوم بوضع مؤشر الفأرة عليه. وبما أننا قمنا بفعل الشيء الأول سابقًا (من خلال التنسيقات الموجودة في الأعلى) فسوف نهتم بالشيء الثاني (تغير مؤشر الفأرة عند وضعه عليه) باستعمال بعض الأكواد البسيطة: .inputfile + label { cursor: pointer; /* "hand" cursor */ }أنظر إلى الصورتين التاليتين ولاحظ أنّ في الصورة الأولى لا يتغير مؤشر الفأرة على عكس الصورة الثانية التي يتغير فيها مؤشر الفأرة عند وضعه على العنصر ليعطي انطباعًا بأنّ هذا العنصر قابل للنقر. التصفح/التنقل باستخدام لوحة المفاتيحإن كان زوار موقعك لا يستطيعون تصفح موقعك باستخدام لوحة مفاتيح فقط فتأكد حينها أنّك تقوم بشيء خاطئ ويجب عليك اصلاحه. وقد كان إخفاء عنصر <input> بطريقة صحيحة هو أحد الأشياء الجيدة لتحسين تجربة المستخدم، وأمّا الشيء الآخر هو أن تُعطي للمستخدم انطباعًا ما بأنّ العنصر قد أصبح في حالة focus (يُصبح العنصر في حالة focus عند التصفح باستخدام زر tab في لوحة المفاتيح، وبالتالي نستطيع استخدام الفئة الزائفة focus: على ذلك العنصر): .inputfile:focus + label { outline: 1px dotted #000; outline: -webkit-focus-ring-color auto 5px; }تُستخدم القيمة webkit-focus-ring-color auto 5px من أجل الحصول على المظهر الإفتراضي للخط الخارجي (outline) في متصفحات Chrome، Opera وSafari. وبالنسبة للقيمة 1px dotted #00 فهي موجودة فقط للمتصفحات التي لا تفهم -webkit-. مشاكل متعلقة باللمس (touch)إذا كنت تستخدم FastClick (وهي مكتبة للتخلص من الإيقاف المؤقت للنقر والذي مُدته 300ms في الأجهزة التي تعمل باللمس) وكنت تنوي إضافة عناصر إضافية داخل العنصر <label>، فإنّ الزر لن يعمل كما يجب إلا إذا استخدمت الخاصية pointer-events: none: <label for="file"><strong>Choose a file</strong></label>.inputfile + label * { pointer-events: none; } تحسين بعض الأمور باستخدام الجافاسكربتبقي علينا شيء واحد يجب فعله وهو إظهار إذا ما كان هناك ملفات تم اختيارها أم لا. ومع أنّ العنصر <"input type="file> يُظهر ذلك عادةً إلا أننا قمنا بإخفائه إن كنت تذكر، ولكن لحسن حظنا فهناك طريقة لفعل ذلك باستخدام الجافاسكربت بحيث نجعل نص الـlabel هو اسم الملف المُختار، وإذا كان هناك عدة ملفات فإنّ نص الـlabel يصبح عدد تلك الملفات: <input type="file" name="file" id="file" class="inputfile" data-multiple-caption="{count} files selected" multiple />var inputs = document.querySelectorAll( '.inputfile' ); Array.prototype.forEach.call( inputs, function( input ) { var label = input.nextElementSibling; labelVal = label.innerHTML; input.addEventListener( 'change', function( e ) { var fileName = ''; if( this.files && this.files.length > 1 ) fileName = ( this.getAttribute( 'data-multiple-caption' ) || '' ).replace( '{count}', this.files.length ); else fileName = e.target.value.split( '\\' ).pop(); if( fileName ) label.querySelector( 'span' ).innerHTML = fileName; else label.innerHTML = labelVal; }); });قمت أيضًا بكتابة أكواد jQuery تقوم بنفس العمل، لذلك تأكد من أن تتصفح الملف المصدري إن كنت تفضل استخدام jQuery. توضيح بسيط للأكواد الموجودة في الأعلى:وجود الصفة multiple في عنصر <input> يسمح للمستخدم بأن يختار أكثر من ملف مرة واحدة. أمّا الصفة data-multiple-caption فهي تستخدم للتعبير عن الرسالة التي تريد أن تظهر للمستخدم عندما يقوم باختيار عدة ملفات. وبالنسبة للعبارة { count } فهي اختيارية وسوف يتم استبدالها برقم يُعبّر عن عدد الملفات المُختارة.الصفة multiple غير مدعومة في متصفح Internet Explorer 9 أو أقل ولا حتى الخاصية files الخاصة بالجافاسكربت، ولذلك سوف نعتمد على value. وبما أنّها عادةً تحتوي على قيمة بالصيغة C:\fakepath\filename.jpg فإنّ ()split( '\\' ).pop تقوم باستخراج اسم الملف.من المثير للاهتمام أنّه يمكنك إلغاء قيمة من المدخلات عن طريق الضغط على زر ESC عندما تكون نافذة تصفح الملفات مفتوحة، وهذا متاح فقط في متصفحي Chrome وOpera. ولهاذ استخدمنا المتغير labelVal لتخزين القيمة الافتراضية للـlabel وإرجاعها عند الحاجة لذلك.سوف تكون النتيجة النهائية كما في الصورة: ولكن ماذا لو كانت الجافاسكربت غير مفعلة؟بما أنّه لا يوجد طريقة أخرى غير الجافاسكربت لمعرفة إذا ما قام المستخدم باختيار ملف أم لا، فإنّه من الأفضل الاعتماد على المظهر الافتراضي لمُدخِل الملفات من أجل سهولة الاستخدام. لذلك كل ما علينا فعله هو إضافة class باسم "no-js" للعنصر <html> ومن ثم نستخدم الجافاسكربت لاستبداله بالاسم "js" وبهذه الطريقة نعرف إذا كان الجافاسكربت مفعلًا أم لا. <html class="no-js"> <head> <!-- remove this if you use Modernizr --> <script>(function(e,t,n){var r=e.querySelectorAll("html")[0];r.className=r.className.replace(/(^|\s)no-js(\s|$)/,"$1js$2")})(document,window,0);</script> </head> </html>وهذه تنسيقات CSS: .js .inputfile { width: 0.1px; height: 0.1px; opacity: 0; overflow: hidden; position: absolute; z-index: -1; } .no-js .inputfile + label { display: none; } خطأ في متصفح Firefoxإنّه لمن المفاجئ معرفة أنّ متصفح Firefox يتجاهل input[type="file"]:focus بينما تعمل :hover و:active بشكل جيد. ولكن لحسن الحظ فإنّ هذا المتصفح يسمح لنا بالتعرف على حالة focus باستخدام الجافاسكربت، لذلك فإنّ الحل هو إضافة class للعنصر <input> ليسمح لنا بالتحكم بحالة الـfocus: input.addEventListener( 'focus', function(){ input.classList.add( 'has-focus' ); }); input.addEventListener( 'blur', function(){ input.classList.remove( 'has-focus' ); });.inputfile:focus + label, .inputfile.has-focus + label { outline: 1px dotted #000; outline: -webkit-focus-ring-color auto 5px; } خاتمةإلى هنا نكون قد وصلنا إلى نهاية هذا الدرس. لذلك تأكد بأن تطلع على الشفرة المصدرية وعلى المعاينات وأن تقوم بالتعديل عليها لتتناسب مع احتياجاتك وذوقك. كما أنّ لديك الحرية الكاملة في استخدام الشفرات الموجودة في هذا الدرس في مشاريعك القادمة. ترجمة -وبتصرّف- للمقال Styling & Customizing File Inputs the Smart Way لصاحبته Osvaldas Valutis.
  23. في سلسلة من عدة أجزاء سنناقش موضوعًا نظريًّا يُعتبر من أساسيّات هندسة البرامج، وهو أنماط التصميم (Design Patterns)، وسنعتمد لغة JavaScript في نقاشنا لتصاعد شعبيّتها ومرونتها التي تسمح لنا ببناء مشاريعنا وفق أنماط متنوّعة مما سيُسهّل علينا شرح موضوع السّلسلة نمط المُشيِّد (Constructor)يشيع استخدام المُشيّدات في اللغات الكائنيّة التّوجّه، حيث تُستخدم لإنشاء نُسخ (instances) من الأصناف (classes)، ومع أنّ JavaScript ليست لغةً كائنيّة التّوجّه بالمعنى التّقليديّ، إلّا أنّها تسمح بإنشاء نُسخ عن كائنات باستخدام بالمُشيّدات، ويمكن لأيّ دالّة أن تُستخدم كمُشيّد، وذلك بأن نُسبقها بالكلمة new، ولتوضيح هذا النّمط سنقوم بإنشاء مُنبّه (كالّذي تضبطه للاستيقاظ في هاتفك) يمكن ضبطه إلى تاريخ ووقت معيّنين ثمّ تفعيله أو تعطيله حسب الرّغبة: function Alarm(when) { this.setAt = when; this.enable = function() { var startAfter = new Date(this.setAt) - new Date; console.log("Alarm will wake you up after " + startAfter/1000 + " seconds"); this.timeout = setTimeout(function() { console.log("Wake up!"); }, startAfter); } this.disable = function() { if (this.timeout) { clearTimeout(this.timeout); delete this.timeout; console.log("Alarm diabled"); } } } var a = new Alarm("2015-03-19 5:58 PM"); a.enable() // Alarm will wake you up after 8.982 seconds // After a few seconds: // Wake up!في المثال السّابق نُسمّي الدّالة Alarm()‎ مُشيّدًا (constructor)، والكائن a نُسخة (instance). لاحظ أنّ Alarm في المثال السّابق ليست سوى دالّة (function)، فهي ليست صنفًا كما في لغات أخرى مثل Java وC++، إذ تُعتبر الدّوال في JavaScript مكوّنًا من الدّرجة الأولى وتُعامل كما يُعامل أيّ كائن، وهكذا يمكن استخدامها كمشيّد لكائن آخر ممّا يسمح بمحاكاة مفهوم الأصناف الّذي لم يُضَف إلّا مؤخّرًا في JavaScript. من عيوب المِثال السّابق إسناد الدّوال الّتي ستعمل عمل الوظائف (methods) إلى النُسخة ذاتها عند إنشائها، وهذا يعني تكرار محتوى الدّوال في الذّاكرة مع كلّ نسخة جديدة من الكائن Alarm، بينما يمكننا توفير هذا الاستهلاك غير المُبرّر للذّاكرة بإسناد الدّوال إلى النّموذج البدئيّ للكائن (أي إلى Alarm.prototype) مما يسمح بمشاركتها بين كل نسخ الكائن، لتقوم الآلة الافتراضيّة بتنفيذ النّصّ البرمجيّ للدّالّة ذاتها بسياق النّسخة (instance context) الّتي استدعت الدّالة، أي إنّ this تُشير ضمن الدّالة عند تنفيذها إلى النُسخةَ المنشأة وليس الصّنف؛ بالطّبع ليس من المرغوب تطبيق الفكرة ذاتها على المُتغيّرات الأخرى مثل setAt، لأنّه من البديهيّ أن تختلف قيمتها بين نسخة وأخرى. لنُعد كتابة المثال السّابق بصورة أفضل: function Alarm(when) { this.setAt = when; } Alarm.prototype.enable = function() { var startAfter = new Date(this.setAt) - new Date; console.log("Alarm will wake you up after " + startAfter/1000 + " seconds"); this.timeout = setTimeout(function() { console.log("Wake up!"); }, startAfter); } Alarm.prototype.disable = function() { if (this.timeout) { clearTimeout(this.timeout); delete this.timeout; console.log("Alarm diabled"); } } var a = new Alarm("2015-03-19 6:21 PM"); a.enable(); // Alarm will wake you up after 30.243 seconds // After 30 seconds... // Wake up!هذا الأسلوب في إنشاء الأصناف شائع جدًّا، وهو يتطلّب فهمًا دقيقًا لآليّة الوراثة في JavaScript؛ إذ يُبنى كلّ كائنٍ فيها على كائن آخر يُسمّى النّموذج البدئيّ (prototype)، ويقوم هذا الكائن الأخير على كائن ثالث أعلى منه في السّلسلة هو نموذجه البدئيّ، وهكذا حتّى نصل إلى null الّذي ليس له نموذج بدئيّ بحسب تعريف اللّغة. في مثالنا السّابق الكائن Alarm.prototype هو النّموذج البدئيّ للكائن a، وهذا يعني أنّ كل الخواصّ المُسندة إلى Alarm.prototype وما فوقه ستكون مُتاحة للكائن a، ولو كتابنا برنامجًا مُشابهًا بـJava لقُلنا إنّ Alarm صنفٌ وإنّ a نُسخة عن هذا الصّنف (instance). عندما نحاول الوصول إلى الخاصّة a.setAt، فإنّ مُفسِّر JavaScript يبدأ بالبحث عن هذه الخاصّة من أدنى سلسلة الوراثة، أي من الكائن a ذاته، فإنّ وجدها قرأها وأعاد قيمتها، وإلّا تابع البحث صعودًا إلى النّموذج البدئيّ وهكذا... وطبيعة JavaScript هذه هي ما سمح لنا بإسناد الوظيفتين enable وdisable إلى Alarm.prototype مطمئنّين إلى أنّها ستكون مُتاحة عند قراءة a.enable()‎ وa.disable()‎. يمكن التأكّد من النّموذج البدئيّ للكائن a كما يلي: Object.getPrototypeOf(a) == Alarm.prototype; // trueالأصناف في ECMAScript 6يُقدّم الإصدار الأحدث من JavaScript مفهوم الأصناف (classes) بصورته التّقليديّة المعروفة في اللّغات الأخرى، إلّا أنّه ليس سوى أسلوب آخر لصياغة النّماذج البدئيّة (أو ما يُسمّى syntactic sugar)، وهذا يعني أنّه نموذج الوراثة في JavaScript لم يتغيّر. يمكننا إعادة كتابة المثال السّابق بصياغة الأصناف في ES6 كما يلي: class Alarm { constructor(when) { this.startAt = when; } enable() { var startAfter = new Date(this.setAt) - new Date; console.log("Alarm will wake you up after " + startAfter/1000 + " seconds"); this.timeout = setTimeout(function() { console.log("Wake up!"); }, startAfter); } disable() { if (this.timeout) { clearTimeout(this.timeout); delete this.timeout; console.log("Alarm diabled"); } } }وستُسند الدّوال enable()‎ وdisable()‎ إلى Alarm.prototype تمامًا كما في المثال الذي سبقه. يُذكر أنّ استخدام new ليست الطّريقة الوحيدة لتشييد الكائنات، إذ يمكن استخدام الوظيفة Object.create()‎ لتُعطي النّتيجة ذاتها: var a = Object.create(Alarm.prototype); Object.getPrototypeOf(a) == Alarm.prototype; // trueإسناد الخواصّ إلى الكائنات‏JavaScript لغة ديناميكية، وهذا يعني أنّه يمكن إضافة وحذف الخواصّ من الكائنات وتعديل نماذجها البدئيّة أثناء التّنفيذ، وهذا ما يمنحها القسم الأكبر من مرونتها ويجعلها مناسبة للاستخدام في بيئة مُعقّدة مثل بيئة الويب، وليس من الغرابة أن توفّر اللّغة وسائل متعدّدة لإسناد الخصائص إلى الكائنات لتلبية الحاجات المتنوّعة لتطبيقات الويب. ماذا لو أردنا تغيير قيمة المنبّه في مثالنا السّابق بعد تفعيله؟ لربّما ترادونا للوهلة الأولى إمكانيّة تغيير قيمة الخاصّة setAt بالطّريقة التّقليدية: a.setAt = "2016-03-03 03:03 PM"; // or a["setAt"] = "2016-03-03 03:03 PM";لكنّ نتيجة هذا الفعل لن تكون كما يُتوقّع، فلو عدنا للمثال السابق وتمعّنا في خواصّه، للاحظنا عيبًا في كيفيّة عمل المُنبّه، إذ إنّ الخاصّة setAt مكشوفة ويمكن تغيير قيمتها في أيّ وقت، حتى بعد تفعيل المُنبّه، إلّا أنّ تغييرها بعدئذٍ لن يغيّر اللّحظة الحقيقيّة الّتي سينطلق فيها المنبّه كما يتضّح لنا عند قراءة النّصّ البرمجيّ، ولذا فنحن هنا أمام حلّين: إمّا منع تغيير قيمة الخاصّة setAt وجعلها للقراءة فقط بعد إنشاء المُنبّه، أو إيقاف المنبّه وإعادة ضبطه في كلّ مرّة تُغيّر فيها قيمة الخاصّة setAt، وكلا الحلّين متاحان إذا كنّا على علم بأساليب إسناد الخصائص في JavaScript. توفّر اللّغة الوظيفة Object.defineProperty()‎‏ الّتي تسمح بتعريف خواصّ لكائن ما مع إمكانيّة التّحكم بتفاصيل هذه الخاصّة، ومن هذه التّفاصيل: هل الخاصّة قابلة للكتابة؟ (writable)هل يجب المرور على هذه الخاصّة عند سرد خواصّ الكائن؟ (enumerable)ما الذي يحدث عند إسناد قيمة للخاصّة؟ (set)ما الذي يحدث عند قراءة قيمة الخاصّة؟ (get)وبهذا يمكننا بسهولة منع تغيير قيمة الخاصّة setAt بعد إسنادها: function Alarm(when) { Object.defineProperty(this, "setAt", { value: when, writable: false }) } Alarm.prototype.enable = function() { var startAfter = new Date(this.setAt) - new Date; console.log("Alarm will wake you up after " + startAfter/1000 + " seconds"); this.timeout = setTimeout(function() { console.log("Wake up!"); }, startAfter); } Alarm.prototype.disable = function() { if (this.timeout) { clearTimeout(this.timeout); delete this.timeout; console.log("Alarm diabled"); } } var a = new Alarm("2015-03-19 7:51 PM"); a.setAt = new Date("2016-03-19"); console.log(a.setAt); // "2015-03-19 7:51 PM"لاحظ أنّ قيمة setAt لم تتغيّر. هذا حلّ جيّد، لكن سيكون من الأفضل السّماح للمُستخدم بتعديل قيمة المنبّه، وعندها سنلجأ لإيقاف المنّبه وإعادة ضبطه كما يلي: function Alarm(when) { var _hidden_value = new Date(when); Object.defineProperty(this, "setAt", { set: function(newValue) { _hidden_value = new Date(newValue); if (this.timeout) { this.disable(); console.log("Alarm changed to " + newValue); console.log("You need to re-enable the alarm for changes to take effect"); } }, get: function() { return _hidden_value; } }) } Alarm.prototype.enable = function() { var startAfter = new Date(this.setAt) - new Date; console.log("Alarm will wake you up after " + startAfter/1000 + " seconds"); this.timeout = setTimeout(function() { console.log("Wake up!"); }, startAfter); } Alarm.prototype.disable = function() { if (this.timeout) { clearTimeout(this.timeout); delete this.timeout; console.log("Alarm diabled"); } } var a = new Alarm("2016-03-03 03:03 PM") a.enable(); // Alarm will wake you up after 30221933.66 seconds a.setAt = "2015-03-19 8:05 PM"; // Alarm changed to 2015-03-19 8:05 PM // You need to re-enable the alarm for changes to take effect a.enable() // Alarm will wake you up after 20.225 seconds // After 20 seconds... // Wake up!لاحظ أنّنا سنحتاج إلى مُتغيّر سرِّيِّ (‎_hidden_value) نُخزّن فيه القيمة الفعليّة لوقت التّنبيه. متى أستخدم هذا النّمط؟نمط المُشيّد لا يحتكر بنية مشروعك عند استخدامه؛ معنى هذا أنّه لا شيء يمنعك من استخدام نمط المُشيّد مع أي نمط آخر عند الحاجة لذلك، فيمكن (بل يشيع كثيرًا) استخدام المُشيّدات ضمن الوحدات (modules) ثمّ تصديرها لاستخدامها من موضع آخر في المشروع، ومثال ذلك أشياء مثل EventEmitter‏ وStreams‏ في Node.js. كما يمكن بناء أنماط أخرى سنتعرّف عليها لاحقًا على أساس المُشيِّدات مثل نمط الكائن المُتفرّد (Singleton) ونمط المُراقِب (Observer pattern). المصادر: شبكة مُطوّري موزيلّا: Inheritance and the prototype chain‏كتاب JavaScript Design Patterns‏ لمؤلّفه Addy Osmani
  24. ‏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.
  25. 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.
×
×
  • أضف...