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

ابراهيم الخضور

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

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

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

آخر الزوار

6135 زيارة للملف الشخصي

إنجازات ابراهيم الخضور

عضو نشيط

عضو نشيط (3/3)

83

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

  1. أشرنا في المقال السابق إلى إمكانية استخدام قاعدة البيانات المدمجة في المتصفح IndexedDB في تخزين ما هو أعقد من النصوص واﻷرقام، بل يتعداها إلى إمكانية تخزين أي شيئ تريده بما في ذلك الكائنات ذات البنى المعقدة مثل بيانات الفيديوهات والصور الخام. مع ذلك، ليس من الصعب تخزين واسترجاع هذه البيانات مقارنة بغيرها من البيانات التي تعاملنا معها. ولتوضيح هذا اﻷمر، سنطور مثالًا تطبيقيًا باسم IndexedDB video store بإمكانك الاطلاع على عمله مباشرة. ينزّل هذا التطبيق عند تشغيله جميع مقاطع الفيديو من الشبكة ويخزنها في قاعدة البيانات IndexedDB، ويعرض بعدها هذه المقاطع في واجهة المستخدم ضمن العنصر <video>، وعندما تشغّل التطبيق في المرات القادمة، سيجد التطبيق المقاطع ضمن قاعدة البيانات ويعرضها بدلًا من تنزيلها مجددًا مما يجعل العملية أسرع، ويوفّر استهلاك حزمة البيانات المتاحة للاتصال باﻹنترنت. تطبيق عملي: تخزين فيديو في قاعدة البيانات IndexedDB سنعرض تاليًا اﻷجزاء اﻷكثر أهمية في تطبيقنا، ولن نستعرض كل التفاصيل طبعًا، فالكثير منها مشابه تمامًا لما غطيناه في المقال السابق، إضافة إلى وجود تعليقات كافية ضمن الشيفرة توضح خطوات العمل. نخزن بداية أسماء مقاطع الفيديو التي نريد إحضارها ضمن مصفوفة كائنات كما يلي: const videos = [ { name: "crystal" }, { name: "elf" }, { name: "frog" }, { name: "monster" }, { name: "pig" }, { name: "rabbit" }, ]; ننفذ الدالة ()init عند نجاح الاتصال بقاعدة البيانات. ووظيفة هذه الدالة التنقل بين أسماء مقاطع الفيديو السابقة ومحاولة إيجاد سجل يوافق اسم المقطع ضمن قاعدة بيانات الفيديو. فإن وجد مقطع الفيديو ستكون نتيجة request.result هي true وإلا ستكون undefined. ستمرر الدالة بعد ذلك اسم المقطع إن وجد إلى الدالة ()displayVideo لتضعه ضمن واجهة المستخدم وإلا تستدعي الدالة ()fetchVideoFromNetwork ﻹحضار المقطع من اﻹنترنت. function init() { //تنقل بين أسماء مقاطع الفيديو واحدًا تلو اﻵخر for (const video of videos) { // افتح الاتصال مع قاعدة البيانات واحصل على مخزن الكائنات وكل فيديو فيه const objectStore = db.transaction("videos_os").objectStore("videos_os"); const request = objectStore.get(video.name); request.addEventListener("success", () => { // إن وجد المقطع ضمن قاعدة البيانات if (request.result) { //displayVideo احضر المقطع واعرضه على الواجهة باستخدام الدالة console.log("taking videos from IDB"); displayVideo( request.result.mp4, request.result.webm, request.result.name, ); } else { // احضر مقطع الفيديو من الشبكة fetchVideoFromNetwork(video); } }); } } تحضر الدالة ()fetchVideoFromNetwork مقاطع فيديو من النوعين MP4 و WebM باستخدام الطلب ()fetch، بعدها سنستخدم التابع ()response.blob لاستخلاص جسم كل طلب على شكل كائن بيانات ثنائية blob والذي يعطي كائنًا يمثل مقطع الفيديو، ويمكن تخزينه وعرضه لاحقًا. أما المشكلة التي تواجهنا هنا، أن هذين الطلبين غير متزامنين، لكن ما نريده فعلًا هو عرض أو تخزين المقطع فقط عندما يكتمل الوعد promise. لهذا نستخدم التابع ()promise.all الذي يقبل معاملًا واحدًا وهو مصفوفة مراجع إلى كل الوعود التي تريد التحقق من إكتمالها، ويعيد وعدًا يتحقق عندما تتحقق كل الوعود في المصفوفة. نستدعي ضمن التابع ()then المتعلق بهذا الوعد الدالة ()displayVideo كما فعلنا سابقًا لعرض مقطع الفيديو، ثم نستدعي أيضًا الدالة ()storeVideo لتخزين المقطع في قاعدة البيانات: // fetch() إحضار مقاطع الفيديو باستخدام الدالة // blob تحويل أجسام الاستجابات إلى كائن const mp4Blob = fetch(`videos/${video.name}.mp4`).then((response) => response.blob(), ); const webmBlob = fetch(`videos/${video.name}.webm`).then((response) => response.blob(), ); // نفّذ الشيفرة التالية إن تحقق كلا الوعدان Promise.all([mp4Blob, webmBlob]).then((values) => { //displayVideo() اعرض الفيديو الذي أحضرته من الإنترنت باستخدام الدالة displayVideo(values[0], values[1], video.name); //storeVideo() خزن مقطع الفيديو في قاعدة البيانات باستخدام storeVideo(values[0], values[1], video.name); }); يشبه عمل الدالة ()storeVideo ما رأيناه في المقال السابق عندما أضفنا بيانات إلى قاعدة البيانات، إذ نفتح قناة العمليات readwrite مع القاعدة ونتخذ مرجعًا إلى مخزن الكائن video_os ثم ننشئ كائنًا يمثل السجل الذي نريد إضافته إلى القاعدة ونستخدم بعدها التابع ()IDBObjectStore.add: // storeVideo() تعريف الدالة function storeVideo(mp4, webm, name) { // فتح قناة اتصال قراءة وكتابة مع قاعدة البيانات const objectStore = db .transaction(["videos_os"], "readwrite") .objectStore("videos_os"); //Add() إضافة السجل إلى قاعدة البيانات باستخدام const request = objectStore.add({ mp4, webm, name }); request.addEventListener("success", () => console.log("Record addition attempt finished"), ); request.addEventListener("error", () => console.error(request.error)); } تُنشئ الدالة ()displayVideo عناصر شجرة DOM اللازمة لإدراج مقطع الفيديو في واجهة المستخدم ومن ثم تلحق هذه العناصر بالصفحة. أما النقاط اﻷكثر أهمية، فهي التي نستعرضها تاليًا. لعرض كائن البيانات الثنائية الذي يضم الفيديو داخل العنصر <video>، لا بد من إنشاء كائن عنوان URL أي عناوين داخلية تشير إلى كائن البيانات الثنائية المخزن في الذاكرة باستخدام التابع ()URL.creatObjectURL. بعدها يمكننا أن نجعل تلك العناوين قيمًا للسمات src العائدة للعناصر <source> ويعمل عندها كل شيء كما هو متوقع: //displayVideo() تعريف الدالة function displayVideo(mp4Blob, webmBlob, title) { //blob يشير إلى الكائن URL إنشاء كائن const mp4URL = URL.createObjectURL(mp4Blob); const webmURL = URL.createObjectURL(webmBlob); //لإدراج الفيديو في الصفحة DOM إنشاء عنصر في شجرة const article = document.createElement("article"); const h2 = document.createElement("h2"); h2.textContent = title; const video = document.createElement("video"); video.controls = true; const source1 = document.createElement("source"); source1.src = mp4URL; source1.type = "video/mp4"; const source2 = document.createElement("source"); source2.src = webmURL; source2.type = "video/webm"; //في الشجرة DOM إدراج عنصر section.appendChild(article); article.appendChild(h2); article.appendChild(video); video.appendChild(source1); video.appendChild(source2); } تخزين اﻷصول للعمل دون اتصال بالشبكة عرضنا في المثال السابق طريقة إنشاء تطبيق يُخزّن أصولًا assets في قاعدة البيانات IndexedDB حتى لا نضطر إلى تحميلها مجددًا. ويحسن هذا اﻷمر تجربة المستخدم بشكل ملحوظ. لكن لا تزال بعض الأصول المهمة مفقودة كي يعمل التطبيق وهي ملف HTML الرئيسي وملفات CSS وجافا سكريبت، ولا بد من تنزيلها في كل مرة ندخل فيها إلى الموقع، وبالتالي لن يعمل التطبيق بدون الاتصال باﻹنترنت. وهنا يأتي دور عمّال الخدمة service workers والواجهة البرمجية cache API يُعرف عامل الخدمة service worker في جافا سكريبت على أنه ملف يُسجّل تحت مصدر محدد مثل موقع ويب أو جزء من موقع ويب في نطاق معين عندما يلج إليه متصفح ويب. ويتمكن هذا الملف لكونه مسجلًا على نطاق ما أن يتحكم بالصفحات التي تنتمي إلى نفس اﻷصل أو النطاق. ويتوضع هذا الملف في مكان وسط بين الصفحة التي اكتمل تحميلها وشبكة اﻹنترنت ويعترض طلبات الشبكة التي تحوّل من وإلى ذلك المصدر أو اﻷصل. وعندما يعترض العامل الطلب سيكون بمقدوره تنفيذ أي شيئ تريده على هذا الطلب، لكن استخدامه النمطي هو تخزين الاستجابات على طلبات الشبكة والاستجابة لهذا الطلبات في المرات القادمة بدلًا من الاتصال بالشبكة والحصول على الاستجابة. وكنتيجة ستتمكن من بناء صفحة ويب تعمل كليًا دون اتصال بشبكة اﻹنترنت. أما الواجهة البرمجية Cache API فهي آلية أخرى لتخزين البيانات في طرف العميل، مع اختلاف بسيط هو أنها مخصصة لتخزين الاستجابات على طلبات HTTP، لهذا ستعمل جيدًا مع عمال الخدمة service workers. مثال عن عمال الخدمة لنطرح مثالًا يوضح قليلًا الفكرة السابقة. إذ أنشانا نسخة أخرى من مثال تخزين ملفات الفيديو الذي فصلناه في الفقرة السابقة. وتعمل هذه النسخة بنفس اﻷسلوب ما عدا أنها تخزّن أيضًا ملفات HTML و CSS وجافا سكريبت ضمن Cache API من خلال عامل خدمة وبالتالي سيعمل المثال دون اتصال باﻹنترنت. بإمكانك تجريب هذه النسخة مباشرة على جيت-هاب والاطلاع على الشيفرة المصدرية أيضًا. تسجيل عامل الخدمة أول ما تلاحظه هو وجود شيفرة إضافية في ملف جافا سكريبت. تختبر هذه الشيفرة بداية وجود العضو serviceWorker ضمن الكائن Navigator. فإن كان موجودًا (أعادت الشيفرة القيمة true)، نعلم حينها وجود دعم أساسي لعمال الخدمة في المتصفح. نستخدم التابع ()ServiceWorkerContainer.register لتسجيل عامل الخدمة الموجود في الملف sw.js على المصدر الذي يتواجد فيه، وبالتالي سيكون قادرًا على التحكم بالصفحات الموجودة في نفس المجلد أو المجلدات الفرعية. وعندما يتحقق الوعد سيكون عامل الخدمة قد سُجِّل: // تسجيل عامل الخدمة لتتمكن من تشغيل المثال دون اتصال بالشبكة if ("serviceWorker" in navigator) { navigator.serviceWorker .register( "/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/sw.js", ) .then(() => console.log("Service Worker Registered")); } ملاحظة: يُعطى مسار الملف sw.js بالنسبة إلى أصل الموقع، وليس بالنسبة إلى ملف جافا سكريبت الذي يضم الشيفرة. فالعامل موجود على العنوان: https://mdn.github.io/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/sw.js بينما عنوان اﻷصل هو https://mdn.github.io لذا لابد أن يكون العنوان المعطى عند التسجيل هو: /learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/sw.js وإذا أردت استضافة هذا المثال على حاسوبك محليًا، لا بد من تبديل هذه القيم بما يناسب. وعلى الرغم من أنه أمر مربك، لكنه ضروري لأسباب أمنية. تثبيت عامل الخدمة في كل مرة ندخل فيها إلى أحد الصفحات التي تقع تحت سيطرة عامل الخدمة، يُثبَّت عامل الخدمة على هذه الصفحة أي يبدأ العامل بالتحكم فيها. وعندما يحدث ذلك، يقع الحدث install على عامل الخدمة، وستتمكن من كتابة الشيفرة ضمن عامل الخدمة نفسه لتستجيب إلى حدث التثبيت. وإذا ألقينا نظرة على الملف sw.js سنجد أن مترصد حدث التثبيت مسجّل وفق القيمة self أي على العامل نفسه. والتعليمة self طريقة لتشير إلى الطبيعة العامة global scope لعامل الخدمة من داخل ملف عامل الخدمة. نستخدم ضمن دالة المعالج install التابع ()ExtendableEvent.waitUntil العائد إلى كائن الحدث لكي يبلغ المتصفح بعدم تثبيت العامل قبل أن يُنجز الوعد بنجاح. وهنا نجد طريقة عمل الواجهة Cache API الخاصة بعملية التخزين، إذ نستخدم التابع ()CacheStorage.open لفتح كائن تخزين مؤقت جديد cache object لنخزّن ضمنه الاستجابات. وعندما يتحقق الوعد، سيعيد كائن cache يمثل المخزن المؤقت للفيديو video-store. نستخدم بعد ذلك التابع ()Cache.addAll ﻹحضار سلسلة اﻷصول واستجاباتها إلى المخزن المؤقت: self.addEventListener("install", (e) => { e.waitUntil( caches .open("video-store") .then((cache) => cache.addAll([ "/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/", "/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/index.html", "/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/index.js", "/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/style.css", ]), ), ); }); وهكذا تنتهي عملية التثبيت. الاستجابة إلى طلبات أخرى مع تسجيل عامل الخدمة ليتحكم بصفحة HTML وإضافة كل اﻷصول إلى المخزن المؤقت، سيكون التطبيق جاهزًا للعمل. لكن بقي هناك شيء واحد وهو كتابة شيفرة تستجيب إلى طلبات HTML اﻷخرى التي ترد، وهذا ما يفعله القسم الثاني من الشيفرة في الملف sw.js. نضيف مترصدًا آخر عامًا إلى عامل الخدمة، يعمل على تنفيذ معالج الحدث عندما يقع حدث اﻹحضار fetch. ويقع هذا الحدث في كل مرة يحاول فيها المتصفح إجراء طلب لأحد اﻷصول الموجودة في نفس المجلد الذي يضم عامل الخدمة. نسجّل بداية ضمن دالة المعالج عنوان URL للأصل المطلوب، ثم نهيئ استجابة مخصصة لهذا الطلب باستخدام التابع ()FetchEvent.respondWith. وضمن كتلة التابع السابق نستخدم التابع ()CacheStorage.match للتحقق من وجود طلب موافق لهذا اﻷصل ضمن المخزن المؤقت. ويتحقق الوعد الموافق لهذا الطلب في حال وجود عنوان في المخزن يطابق عنوان URL للطلب وإلا سيعيد الوعد القيمة undefined. نعيد بعد ذلك الطلب على شكل استجابة مخصصة في حال كان العنوان موجودًا وإلا نستخدم الواجهة ()fetch ﻹحضاره من الشبكة كونه غير موجود في المخزن المؤقت: self.addEventListener("fetch", (e) => { console.log(e.request.url); e.respondWith( caches.match(e.request).then((response) => response || fetch(e.request)), ); }); وهكذا نستخدم عامل الخدمة، علمًا أن استخداماته أوسع من ذلك ولا يسعنا تغطيها في هذا المقال. اختبار المثال دون اتصال بالشبكة لاختبار عمل تطبيقنا، لا بد من تحميله عدة مرات للتأكد من تثبيت عامل الخدمة، وبعدها يمكنك: قطع الاتصال باﻹنترنت. اختيار العمل دون اتصال (اختر ملف ثم العمل دون اتصال إن كنت تستخدم فايرفوكس). الانتقال إلى أدوات مطوري ويب واختر تطبيقات ثم عمال الخدمة Service workers، ولا بد من تفعيل الخيار offline إن كنت تستخدم متصفح كروم. لو حاولت اﻵن تحديث الصفحة سترى أنها ستعيد التحميل دون أية مشكلات لأن كل أصول الصفحة قد خُزّنت في المخزن المؤقت cache، كما أن كل مقاطع الفيديو مخزنة ضمن قاعدة البيانات IndexedDB. الخلاصة تعرفنا في هذا المقال على طريقة لتشغيل صفحة ويب دون اتصال باﻹنترنت عن طريقة استخدام عامل خدمة service worker مع الواجهة البرمجية Cache التي تخزّن الأصول ضمن مخازن مؤقتة في حاسوبك. ترجمة -وبتصرف- للجزء الثالث من مقال: Client-side storage اقرأ أيضًا المقال السابق: تخزين البيانات في طرف العميل باستخدام قاعدة البيانات المفهرسة IndexedDB تخزين البيانات في طرف العميل: مخازن ويب Web Storage تخزين البيانات محليا في متصفح الويب عبر جافاسكربت التخزين المحلي (Local Storage) في HTML5 تعرّف على IndexedDB
  2. تُعد الواجه البرمجية IndexedDB أو IDB اختصارًا منظومة قواعد بيانات كاملة مضمنة في المتصفح تساعدك على تخزين بيانات مترابطة معقدة لا تقتصر فيها أنواع البيانات على قيم بسيطة مثل النصوص واﻷعداد. إذ تستطيع تخزين مقاطع الفيديو والصور وتقريبًا أي شيء في نسخ منفصلة من القاعدة IndexedDB. وتتيح لك هذه الواجهة البرمجية إنشاء قاعدة بيانات ومن ثم إنشاء مخازن كائنات object stores ضمن القاعدة. وتُعد مخازن الكائنات بمثابة بنى شبيهة بالجداول الموجودة في قواعد البيانات العلاقية relational databases، ويمكن لأي مخزن أن يضم مخازن كائنات أخرى. وبالطبع تأتي هذه الميزات مع آثار لا بد منها، فالواجهة IndexedDB أكثر تعقيدًا من واجهة مخازن ويب Web Stores من ناحية الاستخدام. لهذا سنحاول في هذا المقال تقديم مثال يعرض جزءًا ضئيًلا جدًا من إمكانات هذه الواجهة، لكنه سيزوّدك باﻷساسيات التي تساعدك على الانطلاق. تطبيق عملي: تخزين ملاحظات سنبني تطبيق بسيط يسمح لك بتخزين ملاحظات في المتصفح ومراجعة هذه الملاحظات وحذفها متى شئت. وسيكون البناء خطوة خطوة نشرح فيها اﻷجزاء اﻷكثر أهمية من قاعدة البيانات IndexedDB. سيبدو شكل التطبيق عند انتهائه كالتالي: تتكون كل ملاحظة من عنوان ومحتوى نصي يمكن تحرير أي منهما بشكل مستقل عن اﻵخر. وستجد ضمن شيفرة جافا سكريبت الخاصة بالتطبيق تعليقات كافية لشرح كل خطوة بالتفصيل. نقطة الانطلاق انسخ بداية الملفات index.html و style.css و index-start.js إلى مجلد جديد تُنشئه على حاسوبك. الق نظرة في البداية على تلك الملفات، وسترى أن ملف HTML يُعرّف موقع ويب له ترويسة وتذييل ومنطقة محتوى رئيسي تُعرض فيه الملاحظات، إضافة إلى نموذج ﻹدخالها في قاعدة البيانات. يقدّم ملف CSS مجموعة من قواعد التنسيق لتوضيح ما يجري، بينما يضم ملف جافا سكريبت تصريحًا عن خمسة ثوابت تضم مراجع إلى العنصر <ul> وستُعرض الملاحظات على شكل عنوان ونص ضمن عنصري إدخال <input> كما ننشئ مرجعًا إلى النموذج <form> بحد ذاته، ومرجعًا إلى زر <button>. غير اسم ملف جافا سكريبت إلى index.js. تهيئة قاعدة البيانات لنلق نظرة اﻵن على ما يتوجب علبنا فعله بداية لإعداد قاعدة البيانات: أضف السطر التالي تحت التصريحات عن الثوابت: // إنشاء نسخة عن كائن قاعدة البيانات let db; نصرّح في هذا السطر عن متغير يُدعى db لنستخدمه لاحقًا في تخزين الكائن الذي يمثل قاعدة البيانات. وطالما أننا نستخدمه في عدة أماكن لذلك صرحنا عنه كمتغير عام global لتسهيل اﻷمر. أضف تاليًا الشيفرة التالية: // فتح قاعدة البيانات مما يؤدي إلى إنشائها إن لم تكن موجودة const openRequest = window.indexedDB.open("notes_db", 1); يُنشئ هذا السطر طلبًا لفتح النسخة 1 من قاعدة بيانات تُدعى notes_db، فإن لم تكن موجودة سوف ينشئها السطر الذي يليه. سترى هذا الشكل من الطلبات كثيرًا عند استخدام IndexedDB. وطالما أن العمليات على قواعد البيانات تحتاج وقتًا، فلا ينبغي ايقاف المتصفح ريثما ننتظر نتيجة الطلب، لهذا عمليات قواعد البيانات هي عمليات غير متزامنة لن تُنفّذ مباشرة، بل في فترة ما مستقبلًا وستُبلَّغ بتنفيذها. ولمعالجة اﻷمر في IndexedDB، ننشئ كائن طلب request object ندعوه مثلًا openRequest، ويمكنك حينها استخدام معالج حدث لتنفيذ شيفرة مخصصة عند اكتمال هذا الطلب أو فشله، وهذا ما ستراه بعد قليل. ملاحظة: إن رقم النسخة أمر مهم. فلو أردت تحديث قاعدة بياناتك (مثل تغيير هيكلية الجدول)، عليك تنفيذ شيفرتك مجددًا بعد زيادة رقم النسخة، وتحديد تخطيط مختلف ضمن معالج الحدث upgradeneeded. لكننا لن نغطي تحديث قاعدة البيانات في هذا المقال. أضف اﻵن معالج الحدث التالي تحت الشيفرة السابقة: // معالج خطأ يحدد حالة فشل الاتصال بقاعدة البيانات openRequest.addEventListener("error", () => console.error("Database failed to open"), ); // معالج نجاح يحدد نجاح فتح قاعدة البيانات openRequest.addEventListener("success", () => { console.log("Database opened successfully"); //db خزن قاعدة البيانات المفتوحة ضمن المتغير db = openRequest.result; //التي تعرض الملاحظات الموجودة في قاعدة البيانات displayData() نفّذ الدالة displayData(); }); يُنفَّذ معالج الحدث error عندما يُبلغك النظام أن طلبك قد أخفق، مما يتيح لك التعامل مع المشكلة. وما فعلناه في مثالنا هو عرض رسالة خطأ في طرفية جافا سكريبت. ويُنفَّذ معالج الحدث success عندما ينجح الطلب، بمعنى أن الاتصال إلى قاعدة البيانات تحقق، وأصبح الكائن الذي يمثّل قاعدة البيانات متاحًا ضمن الخاصية openRequest.result وبالتالي إمكانية التعامل من خلاله مع قاعدة البيانات. نخزّن النتيجة ضمن المتغيّر db وننفذ دالة تُدعى ()displayData وظيفتها عرض البيانات الموجودة في قاعدة البيانات داخل العنصر <ul> بمجرد انتهاء تحميل الصفحة، وسترى تعريف هذه الدالة لاحقًا. في نهاية هذا القسم سنضيف معالج الحدث upgradeneeded الذي يُنفَّذ إن لم تكن قاعدة البيانات قد هُيئت مسبقًا أو عندما تكون قاعدة البيانات مفتوحة. لهذا أضف اﻷسطر التالية في نهاية شيفرتك منتبهًا إلى ضرورة استخدام رقم نسخة أعلى من رقم النسخة المخزنة في قاعدة البيانات عندما تريد تحديث القاعدة: // هيئ جداول قاعدة البيانات إن لم تكن مهيأة مسبقًا openRequest.addEventListener("upgradeneeded", (e) => { // احصل على مرجع إلى قاعدة البيانات المفتوحة db = e.target.result; // أنشئ مخزن كائنات في قاعدة البيانات لتخزين الملاحظة مع مفتاح يزداد تلقائيًا ومخزن الكائنات مشابه للجدول في قاعدة البيانات العلاقية const objectStore = db.createObjectStore("notes_os", { keyPath: "id", autoIncrement: true, }); // حدد ما ستضمه عناصر البيانات ومخازن الكائنات objectStore.createIndex("title", "title", { unique: false }); objectStore.createIndex("body", "body", { unique: false }); console.log("Database setup complete"); }); نعرّف هنا التخطيط الذي نعتمده لقاعدة البيانات، أي نحدد مجموعة الأعمدة أو الحقول التي تضمها. وما فعلناه أننا حددنا مرجعًا في البداية إلى قاعدة البيانات الموجودة عبر الخاصية result لمعالج الحدث e.target.result الذي يمثّل كائن الطلب request. وهذا الأمر مكافئ للسطر: db = openRequest.result; داخل معالج الحدث succes، لكن لا بد هنا من تنفيذه بشكل مستقل لأن معالج الحدث upgradeneeded سيُنفذ إن احتجنا إليه قبل المعالج success، ولن يكون المتغير db متاحًا ما لم نفعل ذلك. نستخدم بعد ذلك التابع IDBDatabase.createObjectStore ﻹنشاء مخزن كائن جديد ضمن قاعدة البيانات المفتوحة يُدعى notes_os ويكافئ هذا المخزن جدولًا مفردًا في قواعد البيانات النمطية. كما خصصنا أيضًا حقلًا مفتاحيًا autoIncrement في هذا الكائن وسميناه id تزداد قيمة هذا الحقل كلما أضفنا ملاحظة جديدة ولا حاجة أن يفعل المطوّر هذا صراحة بل هي عملية آلية. يعمل هذا الحقل كمعرّف فريد لكل سجل وذلك عندما نريد تعديل أو حذف هذا السجل. وأنشأنا أيضًا حقلين مفهرسين آخرين باستخدام التابع ()IDBObjectStore.createIndex وهما title و body كي يضما عنوان الملاحظة ونصها. وسيمثَّل كل منهما على شكل كائن له التخطيط التالي، وذلك عندما نبدأ بإضافة الملاحظات إلى قاعدة البيانات وفق التخطيط الذي وضعناه: { "title": "Buy milk", "body": "Need both cows milk and soy.", "id": 8 } إضافة بيانات إلى قاعدة البيانات لنرى اﻵن كيف يمكننا إضافة سجلات إلى قاعدة البيانات باستخدام النموذج الذي صممناه في صفحتنا. لهذا أضف الأسطر التالية تحت معالج الحدث السابق. تضبط هذه اﻷسطر معالج الحدث submit الذي يستدعي الدالة ()addData عند تسليم النموذج (النقر على زر اﻹرسال): //addData() إنشاء معالج حدث لعملية تسليم النموذج يعمل عند تنفيذ الدالة form.addEventListener("submit", addData); لنعرّف اﻵن الدالة ()addData كالتالي: // التصريح عن الدالة function addData(e) { //نمنع السلوك الافتراضي، فلا نريد تسليم النموذج بالطريقة النمطية e.preventDefault(); // الحصول على القيم التي نريد تخزينها ووضعها ضمن عنصر تخزين const newItem = { title: titleInput.value, body: bodyInput.value }; // فتح قناة كتابة وقراءة إلى قاعدة البيانات const transaction = db.transaction(["notes_os"], "readwrite"); // استدعاء مخزن كائنات موجود بالفعل في قاعدة البيانات const objectStore = transaction.objectStore("notes_os"); //إلى مخزن الكائنات newItem إنشاء طلب ﻹضافة الكائن const addRequest = objectStore.add(newItem); addRequest.addEventListener("success", () => { // مسح النموذج استعدادًا ﻹضافة سجل آخر titleInput.value = ""; bodyInput.value = ""; }); // اﻹبلاغ عن نجاح العملية على قاعدة البيانات عند اكتمالها transaction.addEventListener("complete", () => { console.log("Transaction completed: database modification finished."); //مجددًا displayData تحديث عرض البيانات بعد إضافة السجل الجديد باستدعاء الدالة displayData(); }); transaction.addEventListener("error", () => console.log("Transaction not opened due to error"), ); } لنحاول تفسير هذه الشيفرة كونها معقدة نوعًا ما: تنفيذ الدالة ()Event.preventDefault على كائن الحدث لإيقاف إرسال بيانات النموذج بالطريقة النمطية (لأنها تدفع إلى إعادة تحديث الصفحة وإفساد التأثير المطلوب). إنشاء كائن يمثل السجل الذي نريد إدخاله إلى قاعدة البيانات ونشره بالقيم التي نحصل عليها من عناصر اﻹدخال. ولاحظ أنه لا حاجة لتزويد السجل بقيمة للمعرّف id كما ذكرنا سابقًا بل سيضاف تلقائيًا. فتح عملية القراءة والكتابة readwrite إلى مخزن الكائنات notes_os باستخدام التابع ()IDBDatabase.transaction. يساعد كائن عمليات قاعدة البيانات في الوصول إلى مخزن الكائن لتنفيذ العملية المطلوبة عليه مثل إضافة سجل جديد. الوصول إلى مخزن الكائن باستخدام التابع ()IDBTransaction.objectStore وتخزين النتيجة في المتغير objectStore. إضافة السجل الجديد إلى قاعدة البيانات باستخدام التابع ()IDBObjectStore.add الذي يُنشئ كائن طلب بنفس اﻹسلوب الذي رأيناه سابقًا. إضافة مجموعة من معالجات اﻷحداث إلى الكائن request والكائن transaction لتنفيذ الشيفرة المطلوبة عند النقاط المطلوبة خلال دورة حياة التطبيق. وبمجرد نجاح الطلب، نمسح حقول اﻹدخال في النموذج لتحضيرها لعملية إدخال أخرى. وعند اكتمال العملية، ننفذ الدالة ()displayData مجددًا لتحديث ما يُعرض من ملاحظات في الصفحة. عرض البيانات أشرنا إلى الدالة ()displayData مرتين في تطبيقنا، لهذا من اﻷفضل اﻵن تعريف هذه الدالة. أضف اﻷسطر البرمجية التالية بعد تعريف الدالة السابقة: // displayData() تعريف الدالة function displayData() { // نمحي محتوى عناصر القائمة في كل مرة نحدّث فيها ما يُعرض // وإلا ستتكرر العناصر في كل مرة نجري فيها تحديثًا while (list.firstChild) { list.removeChild(list.firstChild); } // افتح مخزن الكائنات واحصل على مؤشر يتنقل بين عناصره المختلفة const objectStore = db.transaction("notes_os").objectStore("notes_os"); objectStore.openCursor().addEventListener("success", (e) => { // احصل على مرجع إلى هذا المؤشر const cursor = e.target.result; // استمر في تنفيذ الشيفرة طالما هناك عناصر يمكن التنقل بينها if (cursor) { //p وفقرة نصية h3 وعنوان itemlist أنشئ عنصر قائمة //كي تضع ضمنها كل عنصر بيانات سيُعرض // ألحق الفقرة والعنوان بعنصر القائمة ثم ألحقه بالقائمة const listItem = document.createElement("li"); const h3 = document.createElement("h3"); const para = document.createElement("p"); listItem.appendChild(h3); listItem.appendChild(para); list.appendChild(listItem); // ضع البيانات الموجودة في المؤشر ضمن الفقرة النصية والعنوان h3.textContent = cursor.value.title; para.textContent = cursor.value.body; // خزن معرّف عنصر البيانات داخل سمة ضمن عنصر القائمة //كي نعرف إلى أي عنصر تنتمي. وسيفيدنا ذلك عند حذف العناصر listItem.setAttribute("data-note-id", cursor.value.id); // أنشئ زرًا وضعه ضمن كل عنصر قائمة const deleteBtn = document.createElement("button"); listItem.appendChild(deleteBtn); deleteBtn.textContent = "Delete"; // اضبط معالج حدث يحذف عنصر القائمة عند النقر على هذا الزر deleteBtn.addEventListener("click", deleteItem); // انقل المؤشر إلى العنصر التالي cursor.continue(); } else { //إن لم تكن هنالك أية عناصر قائمة `No notes stored` اعرض رسالة if (!list.firstChild) { const listItem = document.createElement("li"); listItem.textContent = "No notes stored."; list.appendChild(listItem); } // إن لم تكن هنالك أية عناصر اخرى للتنقل بينها، ابلغ عن ذلك برسالة console.log("Notes all displayed"); } }); } لنشرح الآن الشيفرة السابقة بشيء من التفصيل: نمحي بداية محتوى العنصر <ul> قبل أن نملأه مجددًا بالمحتوى المُحدَّث، وإلا سينتهي بك اﻷمر إلى قائمة مليئة بالعناصر المكررة التي تُضاف عند كل تحديث. نتخذ مرجعًا إلى مخزن الكائن notes_os باستخدام التابعين ()IDBDatabase.transaction و ()IDBTransaction.objectStore كما فعلنا مع الدالة ()addData، ماعدا أننا نربطهما معًا في سطر واحد هنا. نستخدم تاليًا التابع ()IDBObjectStore.openCursor لطلب للحصول على مؤشر Cursor، وهي بنية تُستخدم للتنقل بين السجلات في مخزن الكائنات. كما نربط معالج حدث النجاح success في نهاية السطر لنجعل الشيفرة أكثر ترابطًا. وعندما يُعاد المؤشر بنجاح يُنفَّذ المعالج. نتخذ مرجعًا إلى المؤشر ذاته (على شكل كائن IDBCursor) باستخدام السطر const cursor= e.target.result نتحقق تاليًا من وجود سجل من مخزن الكائنات ضمن المؤشر ({}if (cursor))، فإن كان اﻷمر كذلك، ننشئ فرعًا في شجرة DOM وننشر ضمنه بيانات السجل ومن ثم نعرضها على الصفحة ضمن العنصر <ul>. كما نضمّن الفرع زرًا يحذف عنصر القائمة الذي يضم بيانات السجل عند النقر على هذا الزر، وذلك بتنفيذ الدالة ()deleteItem التي نتعرف عليها في الفقرة القادمة. في نهاية الكتلة if نستخدم التابع ()IDBCursor.continue لنقل المؤشر إلى السجل التالي في المخزن وتنفيذ محتوى الكتلة if مجددًا إن كان هنالك سجل آخر سيعرض في الصفحة، ومن ثم يتابع المؤشر تفقد وجود سجلات أخرى. عند انتهاء السجلات، يعيد المؤشر القيمة undefined وبالتالي ستعمل الكتلة else هذه المرة. وتتحقق هذه الكتلة من إضافة أية ملاحظات إلى القائمة <ul>، فإن لم تُضاف أية ملاحظات، تعرض رسالة مفادها عدم وجود أية ملاحظات محفوظة. حذف ملاحظة أشرنا سابقا إلى أن النقر على زر الحذف الموجود إلى جوار الملاحظة المعروضة يسبب حذفها. وننفذ ذلك باستخدام الدالة ()deleteitem: // deleteItem() تعريف الدالة function deleteItem(e) { // الحصول على الملاحظة التي ينبغي حذفها على شكل عدد قبل //IDB: IDB key محاولة استخدامها عن طريق الزوج // والانتباه إلى أن القيم حساسة لحالة الأحرف const noteId = Number(e.target.parentNode.getAttribute("data-note-id")); // فتح قناة العمليات مع قاعدة البيانات وحذف الملاحظة التي حصلنا //على رقمها سابقًا عن طريق السمة التي خزنا فيها معرف الملاحظة const transaction = db.transaction(["notes_os"], "readwrite"); const objectStore = transaction.objectStore("notes_os"); const deleteRequest = objectStore.delete(noteId); // اﻹبلاغ عن حذف عنصر القائمة transaction.addEventListener("complete", () => { // حذف العنصر اﻷب للزر وهو في حالتنا عنصر القائمة ولن يُعرض بعدها e.target.parentNode.parentNode.removeChild(e.target.parentNode); console.log(`Note ${noteId} deleted.`); //إن لم تكن هنالك عناصر قائمة `No Notes Stored` عرض رسالة if (!list.firstChild) { const listItem = document.createElement("li"); listItem.textContent = "No notes stored."; list.appendChild(listItem); } }); } نحصل على معّرف السجل الذي نحذفه باستخدام التابع Number(e.target.parentNode.getAttribute('data-note-id'))، وتذكر أن معرّف السجل قد خُزِّن سابقًا ضمن السمة data-note-id لعنصر القائمة <li> عندما عُرض أول مرة. لكن لا بد من تمرير الدالة التي تعطينا قيمة السمة إلى التابع العام ()Number لأنها من النوع النصي وما نريده هو المكافئ العددي للقيمة وإلا لن تميزها قاعدة البيانات التي تتوقع عددًا. نتخذ تاليًا مرجعًا إلى مخزن الكائن مستخدمين اﻷسلوب الذي خبرناه سابقًا ومن ثم التابع ()IDBObjectStore.delete لحذف السجل من قاعدة البيانات بعد أن نمرر له معرف الملاحظة. عند اكتمال العملية على قاعدة البيانات، نحذف العنصر <li> من شجرة DOM ونتحقق مجددًا من خلو القائمة <ul> من العناصر. إن واجهت صعوبة في تطبيق المثال، قارن بين نسختك والنسخة المكتملة كما يمكنك أيضًا الاطلاع على الشيفرة المصدرية. الخلاصة تعرفنا في هذا المقال على قاعدة البيانات المدمجة في المتصفح IndexedDB وكيفية التعامل معها من خلال مثال تطبيقي يعرض أساسيات إضافة وحذف البيانات واسترجاعها. ترجمة -وبتصرف- للجزء الثاني من مقال: Client-side storage اقرأ أيضًا المقال السابق: تخزين البيانات في طرف العميل: مخازن ويب Web Storage التحقق من صحة بيانات استمارة ويب في طرف العميل نظرة على تفاعلات الخادم مع العميل في موقع ويب ديناميكي الواجهات البرمجية للتعامل مع الصوت والفيديو في جافا سكريبت
  3. تقدم المتصفحات الحديثة مجموعة من التقنيات المختلفة التي تسمح بتخزين بيانات تتعلق بمواقع الويب ثم استرجاعها عند الضرورة، مما يسمح لنا بالحفاظ على البيانات لفترة أطول أو تخزينها للعمل دون اتصال باﻹنترنت وغير ذلك. لهذا سنناقش في مقالنا أبسط اﻷساسيات المتعلقة بهذا اﻷمر وكيفية عملها. ننصحك قبل المتابعة في قراءة هذه المقالات أن: تكون ملمًا بلغتي HTML و CSS. تكون ملمًا بلغة جافا سكريبت. تطلع على سلاسل المقالات السابقة التي ناقشت أساسيات جافا سكريبت والكائنات في جافا سكريبت. تطلع على أساسيات الواجهات البرمجية في طرف العميل. ماذا يعني تخزين البيانات في طرف العميل؟ تحدثنا في مقالات مختلفة عن الفرق بين مواقع الويب الساكنة والدينياميكية، لكن لا بد من اﻹشارة إلى أن معظم مواقع الويب الحديثة ديناميكية، فهي تخزن البيانات في الخادم مستخدمة نوعًا من قواعد البيانات (تخزين في طرف الخادم)، ومن ثم تنفّذ شيفرة في طرف الخادم لاستعادتها ووضعها ضمن قوالب صفحات ساكنة، لتقدّم النتيجة بعدها إلى العميل على شكل صفحاتHTML يعرضها المتصفح. ويعمل التخزين في طرف العميل وفق اﻷسلوب ذاته، لكن له استخدامات خاصة. ولتنفيذ هذه العمليات نحتاج إلى واجهات جافا سكريبت التي تسمح لنا بتخزين البيانات على جهاز العميل واستعادتها عند الحاجة. وتُخّزن البيانات في طرف العميل لاستخدامات محددة مثل: إضفاء خصوصية للمستخدم في الموقع (مثل عرض خيارات يفضلها المستخدم أو اختيار اللون أو حجم الخط). تخزين النشاطات السابقة للمستخدم مثل تخزين محتوى قائمة مشتريات من جلسة سابقة أو تذكر تسجيل الدخول السابق). تخزين البيانات واﻷصول محليًا لتسريع تحميل الموقع (مع احتمال انخفاض كلفة التصفح) وإمكانية التصفح دون الاتصال بالانترنت. تخزين صفحات الويب التي تولدها تطبيقات الويب ديناميكيًا محليًا لاستخدامها دون اتصال بالانترنت. تُستخدم الطريقتان السابقتان في التخزين معًا عادةً، فقد تحمّل مثلًا ملف موسيقى (يُستخدم مع لعبة ويب أو مشغّل موسيقى) ومن ثم تخزّنه في قاعدة بيانات طرف العميل ومن ثم تشغيله عند الحاجة. وهكذا يمكن للمستخدم تحميل الملف لمرة واحدة، وعند الزيارات اللاحقة للموقع يستخرج الملف من قاعدة بياناته المحلية مما يسرع العملية ويقلل تكلفة التصفح. ملاحظة: هناك حد لكمية البيانات التي يمكن تخزينها في طرف العميل عبر الواجهات البرمجية (منفصلة أو مجتمعة) ويختلف هذا الحد وفقًا للمتصفح، وقد يعتمد على اﻹعدادات التي يضبطها المستخدم. استخدام الطريقة التقليدية: ملفات تعريف الارتباط Cookies تُعد تقنية تخزين البيانات في طرف العميل تقنية قديمة، فقد استخدمت المواقع ملفات تعريف الارتباط cookies منذ البدايات الأولى للويب، وذلك لتخزين البيانات وإعطاء طابع شخصي للموقع. وقد كانت أولى أشكال تخزين البيانات في طرف العميل. أما حاليًا، فقد ظهرت تقنيات أفضل وأحدث لتخزين البيانات في طرف العميل، لذلك لن نتحدث عن استخدام ملفات تعريف اﻹرتباط في مقالنا الحالي. ولا يعني ذلك بالطبع أن ملفات تعريف الارتباط عديمة الفائدة في عالم الويب المعاصر، إذ لا تزال شائعة الاستخدام في تخزين البيانات المتعلقة بمعلومات المستخدم الشخصية وحالته مثل معرفات الجلسة session IDs ومفاتيح الوصول المشفرة access token. التقنية الجديدة: مخازن ويب وقاعدة البيانات IndexedDB من الميزات السهلة للتقنيتين اللتين يشير إليهما العنوان نجد: اﻵلية التي تقدمها واجهة مخازن ويب البرمجية Web Storage API في تخزين واسترجاع عناصر البيانات صغيرة الحجم والمكونة من اسم وقيمة موافقة. ولهذا اﻷمر أهميته عندما تحتاج إلى تخزين بيانات بسيطة مثل اسم المستخدم وتاريخ تسجيل الدخول إلى موقع الويب واللون الذي يفضله للخلفية وهكذا. قاعدة البيانات المتكاملة التي تقدمها الواجهة البرمجية IndexedDB API للمتصفح لتخزين البيانات الأكثر تعقيدًا. يمكن استخدام هذه القاعدة مثلًا في تخزين بيانات مجموعة كاملة من سجلات المستخدمين وحتى أنواع معقدة من البيانات مثل ملفات الصوت والفيديو. الواجهة البرمجية Cache صُممت هذه الواجهة لتحزين الاستجابات الناتجة عن طلبات HTTP محددة، وهي مفيدة خصوصًا في أمور مثل تخزين أصول موقع ويب محليًا ليتمكن الموقع من استخدامها باستمرار دون اتصال مع شبكة اﻹنترنت. تُستخدم واجهة التخزين المؤقت cache عادةً بمرافقة واجهة عمال الخدمة Service Worker API على الرغم من عدم الحاجة إلى ذلك فعليًا. ويُعد استخدام واجهة التخزين المؤقت مع واجهة عمال الخدمة موضوعًا متقدمًا لن نغطيه في سلسلة مقالاتنا بالتفصيل، مع ذلك، سنعرض في آخر مقال من هذه السلسلة مثالًا عنها. الواجهة Web Storage وتخزين بيانات بسيطة من السهل جدًا استخدام هذه الواجهة، إذ تخزن البيانات البسيطة على شكل أزواج مكونة من اسم name وقيمة value (محدودة بأنواع مخصصة مثل النصوص واﻷعداد وغيرها)، ومن ثم استرجاع تلك القيم عند الحاجة. الصياغة القواعدية الأساسية لنلق نظرة على ذلك: انتقل بداية إلى القالب الموجود على جيت-هاب وافتحه في نافذة جديدة. افتح طرفية جافا سكريبت في المتصفح. توضع مخازن ويب ضمن بُنى تشبه الكائنات في المتصفح هي sessionStorge و localStorage. تبقى البيانات المخزنة في البنية اﻷولى طالما أن المتصفح يعمل (تُحذف هذه البيانات عند إغلاق المتصفح)، بينما تبقى البيانات في البنية الثانية مقيمة في الذاكرة حتى بعد إغلاق المتصفح. سنستخدم في مقالنا البنية الثانية لأنها أكثر فائدة عمومًا. إذ يسمح التابع ()Strorage.setItem بتخزين البيانات في البنية Storge، وله معاملان: اﻷول هو اسم العنصر، والثاني هو القيمة. جرّب كتابة ما يلي في طرفية جافا سكريبت: localStorage.setItem("name", "Chris"); يأخذ التابع ()Storage.getItem معاملًا واحدًا يمثل عنصر البيانات الذي تريد استرجاع قيمته. جرّب اﻵن الشيفرة التالية: let myName = localStorage.getItem("name"); myName; سترى عند كتابتك الشيفرة السابقة كيف سيضم المتغير myName قيمة عنصر البيانات name. يأخذ التابع ()removeItem معاملًا واحدًا هو اسم عنصر البيانات التي تريد إزالته ومن ثم يزيله من مخزن ويب. جرّب الشيفرة التالية في طرفية جافا سكريبت: localStorage.removeItem("name"); myName = localStorage.getItem("name"); myName; من المفترض أن يعيد تنفيذ السطر الثالث القيمة null للعنصر name لأنه لم يعد موجودًا في مخزن ويب. البيانات المقيمة في الذاكرة من الميزات المهمة لمخازن ويب أن البيانات تبقى موجودة في الفترة التي تُحمّل فيها الصفحات وحتى بعد إغلاق المتصفح عند استخدام local Storage، لنلق نظرة على هذا اﻷمر: افتح مجددًا قالب مخازن ويب السابق لكن في متصفح يختلف عن المتصفح الذي تقرأ فيه هذا المقال. اكتب الشيفرة التالية في طرفية جافا سكريبت لهذا المتصفح: localStorage.setItem("name", "Chris"); let myName = localStorage.getItem("name"); myName; من المفترض أن ترى قيمة عنصر البيانات name. اغلق اﻵن المتصفح ثم افتحه مجددًا. اكتب الشيفرة التالية في طرفية جافا سكريبت: let myName = localStorage.getItem("name"); myName; سترى أن قيمة عنصر البيانات لا تزال متوفرة على الرغم من إغلاق المتصفح وفتحه مجددًا. مخزن منفصل لكل نطاق يُوجد مخزن بيانات منفصل لكل نطاق (لكل عنوان ويب حمّله المتصفح)، وسترى ذلك إن حمّلت موقعين وحاولت تخزين عنصر بيانات في أحدهما، فلن يكون هذا العنصر متاحًا للموقع اﻵخر. وهذا اﻷمر منطقي، فرؤية بيانات موقع من موقع آخر مصدر للكثير من الثغرات اﻷمنية. مثال على مخزن ويب بتفاصيل أكثر سنبني في هذه الفقرة مثالًا نطبق فيه ما تعلمناه ويعطيك فكرة عن كيفية استخدام مخزن ويب. ندخل في هذا المثال اسمًا ثم نُحدّث الصفحة بعد ذلك لترحب بصاحب الاسم شخصيًا. وستبقى هذه الحالة خلال إعادة تحميل الصفحات أو المتصفح لأننا سنخزن الاسم في مخزن ويب. بإمكانك إيجاد نسخة عن ملف HTML المستخدم على جيت-هاب، ويتضمن موقع ويب يتكون من ترويسة ومحتوى وتذييل ونموذج ﻹدخال الاسم. سنبني المثال اﻵن حتى نفهم آلية عمله: انسخ ملف المثال إلى مجلد على حاسوبك. لاحظ كيف يشير ملف HTML إلى ملف جافا سكريبت يُدعى index.js من خلال سطر يشبه السطر <script src="index.js" defer></script>. علينا إذًا إنشاء الملف index.js ضمن نفس المجلد الذي يضم ملف HTML وكتابة شيفرة جافا سكريبت ضمنه. نبدأ الشيفرة ببناء مراجع إلى جميع عناصر HTML التي نريد التعامل معها في مثالنا، وستكون هذه المراجع على شكل ثوابت لأننا لن نغيرها خلال دورة حياة التطبيق. أضف اﻵن الشيفرة التالية: // إنشاء الثوابت المطلوبة const rememberDiv = document.querySelector(".remember"); const forgetDiv = document.querySelector(".forget"); const form = document.querySelector("form"); const nameInput = document.querySelector("#entername"); const submitBtn = document.querySelector("#submitname"); const forgetBtn = document.querySelector("#forgetname"); const h1 = document.querySelector("h1"); const personalGreeting = document.querySelector(".personal-greeting"); علينا اﻵن كتابة مترصد أحداث بسيط لمنع النموذج من تسليم محتوياته عند النقر على زر اﻹرسال، فهذا ليس السلوك الذي نريده. أضف الشيفرة التالية تحت الشيفرة السابقة: // Stop the form from submitting when a button is pressed form.addEventListener("submit", (e) => e.preventDefault()); يجب إضافة الدالة التي تتعامل مع حدث النقر على الزر "Say hello"، وستجد شرحًا وافيًا ضمن تعليقات الشيفرة لكل خطوة، لكن ما تفعله الشيفرة عمومًا هو الحصول على الاسم الذي ندخله ضمن صندوق اﻹدخال النصي وتخزينه في مخزن ويب باستخدام الدالة ()setItem ثم تنفيذ الدالة ()nameDisplayCheck التي تعالج عملية تحديث النص المطلوب من الصفحة. أضف الشيفرة التالية أسفل الشيفرة السابقة: //`Say hello` نفذ الدالة عند النقر على الزر submitBtn.addEventListener("click", () => { // احفظ الاسم في مخزن ويب localStorage.setItem("name", nameInput.value); //لعرض التحية المخصصة nameDisplayCheck استخدم الدالة nameDisplayCheck(); }); نحتاج إلى معالج حدث للتعامل مع النقر على الزر "Forget" الذي يظهر فقط بعد النقر على الزر "Say hello". كما نزيل في دالة معالج الحدث العنصر name من مخزن ويب باستخدام التابع ()removeItem ثم ننفذ مجددًا الدالة ()nameDisplayCheck لتحديث ما يُعرض. أضف الآن الشيفرة التالية: //`Forget` نفّذ الدالة عند النقر على الزر forgetBtn.addEventListener("click", () => { //إزالة الاسم المخزن في مخزن ويب localStorage.removeItem("name"); //لعرض التحية الأصلية وتحديث ما يُعرض nameDisplayCheck استخدم الدالة nameDisplayCheck(); }); نعرّف اﻵن الدالة ()nameDisplayCheck التي نتحقق فيها فيما لو خُزِّن العنصر name في مخزن ويب باستخدام التابع ('name('localStorage.getItem من خلال عبارة شرطية. فإن وُجد في المخزن، ستكون نتيجة الشرط true وإلا ستكون false. نعرض في الحالة اﻷولى رسالة الترحيب الخاصة ونعرض الجزء "forget" من النموذج ونخفي الجزء "Say hello"، أما في الحالة الثانية، سنعرض الرسالة الأصلية ونجري عكس ما فعلناه في الحالة الأولى: // nameDisplayCheck() نعرّف الدالة function nameDisplayCheck() { // نتحقق من تخزين عنصر الاسم في مخزن ويب if (localStorage.getItem("name")) { // نعرض رسالة الترحيب المخصصة إن كان الأمر كذلك const name = localStorage.getItem("name"); h1.textContent = `Welcome, ${name}`; personalGreeting.textContent = `Welcome to our website, ${name}! We hope you have fun while you are here.`; //`forget` من الاستمارة ونعرض الجزء `remember` نخفي الجزء forgetDiv.style.display = "block"; rememberDiv.style.display = "none"; } else { // إن لم يكن الايم مخزنًانعرض الرسالة اﻷصلية h1.textContent = "Welcome to our website "; personalGreeting.textContent = "Welcome to our website. We hope you have fun while you are here."; //`remember` من الاستمارة ونعرض الجزء `forget` نخفي الجزء forgetDiv.style.display = "none"; rememberDiv.style.display = "block"; } } ننفذ الدالة ()nameDisplayCheck عند اكتمال تحميل الصفحة. لأن الرسالة المخصصة لن تظهر إن لم نفعل ذلك خلال تحميل الصفحة بشكل متكرر. أضف ما يلي إلى آخر الشيفرة: nameDisplayCheck(); وهكذا يكون مثالنا قد انتهي، وبإمكان الاطلاع في أي وقت على النسخة المكتملة منه على جيت-هاب. ملاحظة: تمنع السمة defer في السطر التالي من تنفيذ شيفرة جافا سكريبت حتى اكتمال تحميل الصفحة <script src="index.js" defer></script>. الخلاصة تعرفنا في هذا المقال على أساسيات تخزين البيانات في طرف العميل من خلال واجهات برمجية مخصصة مثل Web Storage API و IndexedDB. كما شرحنا مخازن ويب Web Storage وطريقة العمل معها من خلال مثال تطبيقي بسيط يعرض أساسيات العمل مع هذه الواجهة البرمجية. ترجمة -وبتصرف- للجزء الأول من مقال: Client-side storage اقرأ أيضًا المقال السابق: الواجهات البرمجية للتعامل مع الصوت والفيديو في جافا سكريبت معالجة المشاكل الشائعة للتوافق مع المتصفحات في شيفرة جافاسكربت الواجهة البرمجية fetch في JavaScript تخزين البيانات محليًا في المتصفح عبر قاعدة البيانات IndexedDB
  4. تضم لغة HTML عناصر مخصصة لتضمين الوسائط المتعددة إلى صفحاتك مثل <audio> و <video> والتي تأتي مزوّدة بواجهة برمجية مخصصة للتحكم بتشغيلها وتقديمها وتأخيرها. لهذا سنتعرف في هذا المقال على طرق تنفيذ بعض المهام الشائعة كإنشاء أدوات تحكم متخصصة باستخدام الواجهة البرمجية HTMLMediaElement التي توفر عدة ميزات للتحكم في تشغيل الصوت و الفيديو برمجيًا . ننصحك قبل المتابعة في قراءة هذه المقالات أن: تكون ملمًا بلغتي HTML و CSS. تكون ملمًا بلغة جافا سكريبت. تطلع على سلاسل المقالات السابقة التي ناقشت أساسيات جافا سكريبت والكائنات Prototype في جافا سكريبت. تطلع على أساسيات الواجهات البرمجية في طرف العميل. عناصر HTML الخاصة بالصوت والفيديو يسمح العنصران <video> و <audio> بإدراج مقاطع الصوت و الفيديو في صفحات الويب، وستبدو الطريقة النمطية ﻹنجاز اﻷمر كالتالي: <video controls> <source src="rabbit320.mp4" type="video/mp4" /> <source src="rabbit320.webm" type="video/webm" /> <p> Your browser doesn't support HTML video. Here is a <a href="rabbit320.mp4">link to the video</a> instead. </p> </video> تعرض الشيفرة السابقة مشغل فيديو في صفحتك كالتالي: وأكثر ما يثير الاهتمام في الشيفرة السابقة هي السمة controls التي تعرض أدوات التحكم الافتراضية مع مشغل الفيديو، وإن لم تستخدم هذه السمة فلن ترى عناصر التحكم على المشغل: لهذه اﻷدوات إيجابيات، لكن من أبرز مشكلاتها هي اختلافها من متصفح إلى آخر وهذا أمر مربك إن حاولت دعم عدة متصفحات في شيفرتك. ومن المشكلات الكبيرة أيضًا أن أدوات التحكم اﻷصلية في معظم المتصفحات لا تدعم التحكم من خلال لوحة المفاتيح. ويمكن حل كلتا المشكلتين السابقتين بإخفاء أدوات التحكم الأصلية (عن طريق إزالة السمة controls) وبرمجة أدوات تحكم خاصة بك باستخدام HTML و CSS وجافا سكريبت. وسنلقي نظرة في اﻷقسام التالية على اﻷدوات البسيطة المتاحة لهذا الغرض. الواجهة البرمجية HTMLMediaElement تزّودك بعض مواصفات الواجهة البرمجية HTMLMediaElement بميزات تسمح لك بالتحكم في مشغلات الصوت و الفيديو برمجيًا مثل التوابع ()HTMLMediaElement.play و ()HTMLMediaElement.pause وغيرها. وهاتان الواجهتان متاحتان للاستخدام مع العنصرين <audio> و<video> فهما متطابقان من ناحية العمل. لهذا سنوضح طريقة استخدام هذه الواجهات من خلال المثال التالي: يبدو مثالنا عندما يكتمل مشابهًا للتالي: نقطة الانطلاق حتى نبدأ العمل، عليك تنزيل الملف المضغوط الخاص بالمثال ثم تستخرج محتوياته في مجلد جديد على حاسوبك. أما إن حمّلت مستودع اﻷمثلة بأكمله، ستجد المثال في المسار javascript/apis/video-audio/start/. إن حمّلت المثال على متصفحك سترى مشغل فيديو HTML نمطي مع أدوات التحكم الافتراضية. التعرف على ملف HTML عندما تفتح الملف index.html سترى عددًا من العناصر، وستلاحظ أن معظم الشيفرة تدور حول مشغل الفيديو وأدوات التحكم الخاصة به: <div class="player"> <video controls> <source src="video/sintel-short.mp4" type="video/mp4" /> <source src="video/sintel-short.webm" type="video/webm" /> <!-- fallback content here --> </video> <div class="controls"> <button class="play" data-icon="P" aria-label="play pause toggle"></button> <button class="stop" data-icon="S" aria-label="stop"></button> <div class="timer"> <div></div> <span aria-label="timer">00:00</span> </div> <button class="rwd" data-icon="B" aria-label="rewind"></button> <button class="fwd" data-icon="F" aria-label="fast forward"></button> </div> </div> وضعنا المشغل بأكمله داخل العنصر <div> حتى يُنسّق بالكامل عند الحاجة. يحتوي العنصر <video> على عنصرين من النوع <source> كي نتمكن من تحميل تنسيقات مختلفة لمقطع الفيديو وفقًا للمتصفح الذي نستخدمه. وربما تكون أدوات تحكم HTML هي اﻷكثر أهمية هنا: لدينا أربعة أزرار <button> لتشغيل وإيقاف العرض مؤقتًا واﻹطفاء والتقديم للأمام والعودة للخلف. خصصنا لكل زر سمات هي اسم صنف التنسيق class و data-icon لتحديد اﻷيقونة التي تُعرض على الزر (سنرى كيف ننفذ ذلك لاحقًا) و aria-label لتقديم وصف مفهوم عن عمل كل زر، وذلك لأننا لا نقدم عنوانًا مقروءًا ضمن وسم العنصر. ويُقرأ محتوى السمة aria-label من قبل قارئات الشاشة عندما ينتقل التركيز إلى العناصر التي تمتلك هذه السمة. تضم الصفحة أيضًا مؤقتًا ضمن عنصر <div> يعرض الوقت المنقضي من مقطع الفيديو، ولتحسين تجربة المستخدم، زودنا المثال بآليتين لتحديد الوقت المنقضي: الأولى ضمن عنصر <span> يعطي الوقت المنقضي بالدقائق والثواني، والثانية ضمن عنصر <div> يضم شريط أفقي يزداد طوله عندما يتقدم عرض الفيديو. ولكي تأخذ فكرة عما ستكونه الصفحة بشكلها الكامل ألق نظرة هنا.. التعرف على ملف CSS افتح اﻵن ملف CSS وألقِ نظرة عليه. لا يبدو هذا الملف معقدًا، لكننا سنشير إلى النقاط المهمة فيه. لاحظ بداية القاعدة controls.: .controls { visibility: hidden; opacity: 0.5; width: 400px; border-radius: 10px; position: absolute; bottom: 20px; left: 50%; margin-left: -200px; background-color: black; box-shadow: 3px 3px 5px black; transition: 1s all; display: flex; } .player:hover .controls, .player:focus-within .controls { opacity: 1; } بدأنا بالخاصية visibility لمجموعة أدوات التحكم المخصصة وضبطناها على hidden، لكننا سنضبطها لاحقًا عبر جافا سكريبت لتكون visible ونزيل السمة controls من العنصر <video>. وذلك كي يبقى المستخدم قادرًا على تشغيل الفيديو باستخدام أدوات التحكم الافتراضية في حال فشل تحميل شيفرة جافا سكريبت لسبب ما. منحنا أدوات التحكم قتامة افتراضية opacity قيمتها 0.5، كي لا تشتت الانتباه عند عرض الفيديو، لكن عندما تمرر الفأرة فوق المشغّل أو تمنحه تركيز الدخل ستكون اﻷدوات كاملة القتامة. نرتب اﻷزرار ضمن شريط التحكم باستخدام تخطيط الصندوق المرن display: flex لتسهيل ضبط مواقعها. لنلق نظرة تاليًا على أيقونات اﻷزرار: @font-face { font-family: "HeydingsControlsRegular"; src: url("fonts/heydings_controls-webfont.eot"); src: url("fonts/heydings_controls-webfont.eot?#iefix") format("embedded-opentype"), url("fonts/heydings_controls-webfont.woff") format("woff"), url("fonts/heydings_controls-webfont.ttf") format("truetype"); font-weight: normal; font-style: normal; } button:before { font-family: HeydingsControlsRegular; font-size: 20px; position: relative; content: attr(data-icon); color: #aaa; text-shadow: 1px 1px 0px black; } استخدمنا بداية في أعلى ملف CSS الكتلة font-face@ لاستيراد خط ويب مخصص، وهذا الخط هو عبارة عن أيقونات بدلًا من الحرف اﻷبجدية وتستخدم لعرض أيقونات مختلفة يشيع استخدامها في التطبيقات. نولد بعد ذلك محتوىً خاصًا لعرض اﻷيقونات على كل زر: نستخدم المحدد before:: لعرض المحتوى قبل كل زر <button>. نستخدم الخاصية content لضبط المحتوى الذي يعرض في كل حالة ليكون نفسه محتوى السمة data-icon. ففي حالة زر التشغيل مثلًا، سيكون محتوى السمة data-icon هو المحرف P (بشكله الكبير). نطبق خط ويب السابق على اﻷزرار باستخدام الخاصية font-family، وسيكون الحرف P في هذا الخط عمليًا أيقونة التشغيل، وهكذا ستظهر على زر التشغيل أيقونة التشغيل. إن الخطوط التي تعرض أيقونات جميلة ومفيدة ﻷسباب عديدة منها تقليل عدد طلبات HTTP لانك لن تحتاج إلى تحميل تلك اﻷيقونات على شكل ملفات صور، إضافة إلى إمكانية تكبير وتصغير اﻷيقونات بدقة وكذلك إمكانية استخدام خاصيات نصية لتنسيق تلك اﻷيقونات مثل color و text-shadow. لنلق نظرة أيضًا على تنسيق المؤقت الزمني: .timer { line-height: 38px; font-size: 10px; font-family: monospace; text-shadow: 1px 1px 0px black; color: white; flex: 5; position: relative; } .timer div { position: absolute; background-color: rgb(255 255 255 / 20%); left: 0; top: 0; width: 0; height: 38px; z-index: 2; } .timer span { position: absolute; z-index: 3; left: 19px; } ضبطنا قيمة الخاصية flex للعنصر timer. الخارجي على القيمة 5 لتشغل أكبر مساحة من شريط التحكم. كما ضبطنا خاصية الموقع بالشكل position:relative كي نتمكن من ضبط العناصر ضمن العنصر الخارجي كما نشاء وبالنسبة إلى حدوده وليس حدود العنصر <body>. ضبطنا موقع العنصر <div> الداخلي ليكون مطلقًا position:absolute لكي يظهر مباشرة في أعلى العنصر <div> الخارجي. كما ضبطنا قيمة اتساع العنصر على الشكل width:0 كي لا يُرى العنصر إطلاقًا. وعندما يبدأ العرض نستخدم جافا سكريبت لزيادة اتساع العنصر. ضطنا موقع العنصر <span> ليكون مطلقًا وبالتالي سيكون بالقرب من الطرف اﻷيسر لشريط المؤقت. ضبطنا خاصية العلو z-index للكائن <div> الداخلي والكائن <span> كي يُعرض الشريط الزمني في الأعلى وتحته العنصر <div> الداخلي، ونضمن بذلك أنك سترى كل المعلومات ولن يَحجِب صندوق آخر. إنجاز شيفرة جافا سكريبت بعد أن حضرنا واجهتي HTML و CSS، لا بد من كتاب شيفرة اﻷزرار المخصصة للتحكم بمشغل الفيديو. أنشئ ملف جافا سكريبت جديد في نفس المجلد الذي يضم الملف index.html وسمِّه custom-player.js. ضع الشيفرة التالية أعلى الملف: const media = document.querySelector("video"); const controls = document.querySelector(".controls"); const play = document.querySelector(".play"); const stop = document.querySelector(".stop"); const rwd = document.querySelector(".rwd"); const fwd = document.querySelector(".fwd"); const timerWrapper = document.querySelector(".timer"); const timer = document.querySelector(".timer span"); const timerBar = document.querySelector(".timer div"); أنشأنا في الشيفرة السابقة ثوابت لتكون مراجع إلى الكائنات التي نريد التعامل معها، ولدينا ثلاثة مجموعات: العنصر <video> وشريط التحكم. أزرار التحكم "تشغيل/إيقاف مؤقت play/pause" و "للأمام rewind" و "للخلف fast forward". غلاف المؤقت الخارجي <div> والعنصر <span> الذي يعرض المؤقت والعنصر <div> الخارجي الذي يزداد اتساعه عندما يتقدم الفيديو. ضع اﻵن الشيفرة التالية تحت سابقتها: media.removeAttribute("controls"); controls.style.visibility = "visible"; تزيل الشيفرة السابقة مشغل الفيديو الافتراضي الخاص بالمتصفح ويُظهر أدوات التحكم المخصصة: تشغيل وإيقاف الفيديو مؤقتًا سننجز اﻵن شيفرة التحكم بزر التشغيل و اﻹيقاف المؤقت: أضف بداية الشيفرة التالية في أسفل الشيفرة كي تُستدعى الدالة ()playPauseMedia عند النقر على زر التشغيل: play.addEventListener("click", playPauseMedia); ولتعريف الدالة ()playPauseMedia، أضف الشيفرة التالية إلى أسفل الشيفرة السابقة: function playPauseMedia() { if (media.paused) { play.setAttribute("data-icon", "u"); media.play(); } else { play.setAttribute("data-icon", "P"); media.pause(); } } نستخدم هنا عبارة if للتحقق من توقف تشغيل الفيديو، وتعيد الخاصية HTMLMediaElement.paused القيمة true عند توقف التشغيل مؤقتًا بما في ذلك عند ضبطته على القيمة 0 بعد تحميله أول مرة. عند ذلك نضبط قيمة السمة data-icon لزر التشغيل على u التي تعرض بدورها أيقونة التشغيل المؤقت عليه، وتستدعي التابع ()HTMLMediaElement.play لتشغيل الفيديو. وعند النقر على الزر مرة ثانية سيعود الزر كما كان، إذ تظهر أيقونة التشغيل وسيتوقف الفيديو بتنفيذ التابع ()HTMLMediaElement.paused. إيقاف عرض الفيديو نضيف بداية الشيفرة التي تتعامل مع إيقاف تشغيل الفيديو تحت الشيفرة السابقة: stop.addEventListener("click", stopMedia); media.addEventListener("ended", stopMedia); يضيف سطري الشيفرة مترصدي أحداث addEventListener للتعامل مع الحدث click الذي يوقف تشغيل الفيديو بتنفيذ الدالة ()stopMedia عند النقر على زر اﻹيقاف. ولا بد من إيقاف التشغيل أيضًا عند إنتهاء المقطع، لهذا نترصد أيضًا الحدث ended من خلال مترصد الحدث الثاني والذي ينفذ أيضًا الدالة ()stopMedia عند انتهاء مقطع الفيديو. نعرّف تاليًا الدالة ()stopMedia، بإضاف اﻷسطر التالية بعد الدالة ()playpauseMedia: function stopMedia() { media.pause(); media.currentTime = 0; play.setAttribute("data-icon", "P"); } وبما أن الواجهة البرمجية HTMLMediaElement لا تقدم تابعًا مخصصًا ﻹيقاف عرض الفيديو، سنستخدم التابع ()pause ﻹيقاف التشغيل مؤقتًا ثم نضبط قيمة الخاصية currentTime على القيمة 0 ليعود الفيديو إلى البداية. فضبط قيمة هذه الخاصية (بالثواني) سينقل الموقع الحالي للفيديو إلى النقطة الزمنية المحددة. يبقى علينا فقط إظهار أيقونة التشغيل على زر التشغيل. وبصرف النظر عن وضع الفيديو سواءً كان قيد التشغيل أو أوقف مؤقتًا عند النقر على زر إيقاف التشغيل "Stop"، لابد أن تُظهر أن المشغل جاهز للعمل مجددًا. التنقل بالفيديو إلى اﻷمام والخلف ستجد العديد من الطرق لتقديم أو إعادة المشغل إلى نقطة زمنية محددة، وما سنعرضه حاليًا طريقة معقدة نوعًا ما في تنفيذ الأمر لتفادي اﻷخطاء التي قد تحدث عند النقر على أزرار مختلفة بترتيب غير متوقع. أضف مترصدي الحدث التاليين تحت تعريف المترصدين السابقين: rwd.addEventListener("click", mediaBackward); fwd.addEventListener("click", mediaForward); أضف الدالتين ()mediaBackWard و ()mediaForWard التاليتين تحت الدوال السابقة، وستصبح الشيفرة كالتالي: let intervalFwd; let intervalRwd; function mediaBackward() { clearInterval(intervalFwd); fwd.classList.remove("active"); if (rwd.classList.contains("active")) { rwd.classList.remove("active"); clearInterval(intervalRwd); media.play(); } else { rwd.classList.add("active"); media.pause(); intervalRwd = setInterval(windBackward, 200); } } function mediaForward() { clearInterval(intervalRwd); rwd.classList.remove("active"); if (fwd.classList.contains("active")) { fwd.classList.remove("active"); clearInterval(intervalFwd); media.play(); } else { fwd.classList.add("active"); media.pause(); intervalFwd = setInterval(windForward, 200); } } هيأنا أولًا متغيرين intervalFwd و intervlRwd وسترى عملهما لاحقًا، كما ستلاحظ أن عمل الدالتين ()mediaBackWard و ()mediaForWard متطابق لكن بترتيب معكوس: يجب تصفير اﻷصناف والمجالات التي ضبطناها عند تنفيذ وظيفة التقديم السريع للأمام، لأننا لو نقرنا على زر rwd بعد النقر على الرز fwd من المفترض أن نلغي أي إعدادات خاصة بالتقديم السريع للمشغل fwd واستبدالها بإعدادت التراجع rwd، لأن المشغل سيخفق لو حاولنا النقر على كلا الزرين في نفس الوقت. استخدمنا عبارة if للتحقق من ضبط صنف الزر rwd ليكون active في إشارة إلى أن الزر قد نُقر للتو. ويتمتع كل عنصر بالخاصية classlist، وهي خاصية مفيدة تضم كل الأصناف التي يمتلكها العنصر وتقدم توابع ﻹزالة وإضافة اﻷصناف. وقد استخدمنا التابع ()classList.contains للتحقق من جود الصنف active ضمن أصناف الزر، وتعيد قيمة منطقية true/false. في حال كان active أحد أصناف العنصر rwd نزيله باستخدام التابع ()classList.remove ثم نلغي قيمة الفاصل الزمني الذي ضُبط مسبقًا عندما نقرنا على الزر ومن ثم نستخدم التابع ()HTMLMediaElement.play ﻹلغاء العودة للخلف وتشغيل الفيديو بشكل طبيعي. إن لم يمتلك الزر تلك الخاصية نضيفها إليه باستخدام التابع ()clasList.add ومن ثم نوقف الفيديو مؤقتًا باستخدام التابع ()HTMLMediaElement.pause. نضبط بعدها قيمة المتغير intervalRwd ليعادل القيمة المعادة من استدعاء الدالة ()setInterval. تُحدد هذه الدالة فترة زمنية معينة تنفذ بعد انقضائها الدالة التي تُمرر إليها كمعامل أول أما الفترة الزمنية فيحددها المعامل الثاني بالميلي ثانية. وهنا ننفذ الدالة كل 200 ميلي ثانية كي نعيد مشغل الفيديو إلى الخلف بوتيرة ثابتة. ولكي نوقف تنفيذ الدالة ()setInterval نستدعي الدالة ()clearIterval ممرين لها المتغير intervalRwd (الذي أسندت إليه الدالة ()setInterval). عرفنا أخيرًا الدالة ()windBackwrd والدالة ()windForward اللتان تمررا إلى ()setInterval، لهذا أضف الشيفرة التالية تحت الدوال السابقة: function windBackward() { if (media.currentTime <= 3) { rwd.classList.remove("active"); clearInterval(intervalRwd); stopMedia(); } else { media.currentTime -= 3; } } function windForward() { if (media.currentTime >= media.duration - 3) { fwd.classList.remove("active"); clearInterval(intervalFwd); stopMedia(); } else { media.currentTime += 3; } } سنشرح تاليًا الدالة الأولى فقط لكون الدالتين متطابقتان من ناحية الشيفرة ومتعاكستان عملًا. وما فعلناه في الدالة ()windBackward هو التالي (تذكر أنه بمجرد تفعيل الفاصل الزمني الذي سيتراجع فيه المشغل إلى الخلف ستُستدعى هذه الدالة كل 200 ميلي ثانية): نبدأ الشيفرة بالعبارة if التي تتحقق أن المدة المنقضية من المقطع أقل من 3 ثانية، أي سيعود المشغل عند تراجعه إلى ما قبل نقطة البداية، وهذا ما يسبب سلوكًا غريبًا للمشغل. فلو كانت الحالة كذلك، نوقف تشغيل المقطع باستدعاء الدالة ()stopMedia ومن ثم نزيل الصنف active من قائمة أصناف الزر rwd ونمحي قيمة المتغير intervalRwd ﻹيقاف عملية التراجع. وفي حال أهملنا هذه الخطوة اﻷخيرة سيستمر المشغل بالتراجع إلى ما لا نهاية. إن كان الوقت المنقضي أكبر من 3 ثانية، نزيل ثلاث ثوانٍ من الوقت الحالي باستخدام التعليمة media.currentTime -=3، أي نعيد مشغل الفيديو إلى ما قبل ثلاث ثوان وذلك كل 200 ميلي ثانية. تحديث الوقت المنقضي آخر ما سننفذه ﻹنجاز أدوات التحكم المخصصة بمشغل الفيديو هو تحديد الوقت المنقضي من زمن المقطع. لذا نشغّل دالة تحدّث الوقت الذي نعرضه في كل مرة يقع فيها الحدث timeupdate المرتبط بالعنصر <video>. أما تواتر عملية وقوع هذا الحدث، فتعتمد على المتصفح وقوة معالج جهازك. أضف اﻵن السطر التالي الذي يعرّف مترصد تحديث زمن التشغيل: media.addEventListener("timeupdate", setTime); ولتعريف الدالة ()setTime، أضف مايلي في أسفل ملف جافا سكريبت: function setTime() { const minutes = Math.floor(media.currentTime / 60); const seconds = Math.floor(media.currentTime - minutes * 60); const minuteValue = minutes.toString().padStart(2, "0"); const secondValue = seconds.toString().padStart(2, "0"); const mediaTime = `${minuteValue}:${secondValue}`; timer.textContent = mediaTime; const barLength = timerWrapper.clientWidth * (media.currentTime / media.duration); timerBar.style.width = `${barLength}px`; } هذه الدالة طويلة، لهذا سنناقشها خطوة خطوة: نعمل بداية على تحديد الدقائق والثواني المنقضية من خلال قيمة HTMLMediaElement.currentTime. نهيئ بعد ذلك متغيرين إضافيين هما minuteValue و secondValue، ثم نستخدم التابع ()padStart لكي نمثّل قيمة الدقائق والثواني على شكل محرفين فقط حتى لو كانت القيمة رقمًا وحيدًا. أما الوقت الفعلي الذي سيُعرض فهو قيمة المتغير minuteValue تليه نقطتان متعامدتان ثم قيمة المتغير secondValue. نضبط قيمة المؤقت Node.textContent لتعادل قيمة الوقت الحالي وبالتالي ستُعرض هذه القيمة على واجهة المشغّل. نحدد طول عنصر <div> الداخلي (الذي سيعرض شريط تقدم مقطع الفيديو) من خلال تحديد اتساع عنصر <div> الخارجي (نأخذها من الخاصية clientWidth) ومن ثم ضرب هذه القيمة بالوقت الحالي HTMLMediaElement.currentTime ونقسم على المدة الكلية لمقطع الفيديو HTMLMediaElement.duration. نضبط قيمة اتساع العنصر <div> الداخلي ليعادل طول شريط تتبع تقدم الفيديو بعد إضافة القيمة "px" كي تشير إلى الاتساع مقدرًا بالبكسل. إصلاح مشكلات التشغيل واﻹيقاف المؤقت للفيديو هنالك مشكلة واحدة ولا بد من حلها. فعند النقر على زر التشغيل أو إيقاف الفيديو وزر التقديم أو التراجع، فلن يعمل هذا الزر! وما علينا إصلاحه هنا هو إلغاء وظائف التقدم أو التراجع عند النقر على زر التشغيل لمتابع العمل كما هو متوقع، وهذا أمر سهل. أضف بداية الشيفرة التالية ضمن الدالة ()stopMedia: rwd.classList.remove("active"); fwd.classList.remove("active"); clearInterval(intervalRwd); clearInterval(intervalFwd); أضف اﻵن نفس اﻷسطر في بداية الدالة ()playpauseMedia (وقبل عبارة if). يمكنك اﻵن ازالة نفس الأسطر من الدالتين ()windBackwrd و ()windForward لأننا وضعنا هذه الوظيفة المشتركة بينهما في الدالة ()stopMedia. ملاحظة: يمكنك تحسين فعالية الشيفرة أكثر من خلال إنشاء دالة منفصلة تضم اﻷسطر السابقة ومن ثم استدعاء هذه الدالة عند الحاجة بدلًا من تكرار اﻷسطر عدة مرات في الشيفرة. الخلاصة تعلمنا في هذا المقال ما يكفي عن الواجهة HTMLMediaElement التي تقدم كما كبيرًا من الوظائف ﻹنشاء مشغل وسائط متعددة، وما رأيناه هو مجرد جزء ضئيل من إمكانياتها. إليك أخيرًا بعض الاقتراحات التي تساعد في تحسين مثالنا: يختل عمل المؤقت إن كانت مدة المقطع أكثر من ساعة (فلن يعرض الساعات بل الدقائق والثواني فقط). هل يمكنك تعديل الشيفرة لتعرض الساعات أيضًا؟ يمتلك العنصر <audio> نفس وظائف HTMLMediaElement وبالتالي يمكنك تشغيل المقاطع الصوتية بسهولة، جرب لك. هل يمكنك إيجاد طريقة الانتقال إلى مكان ما من المقطع بالنقر على شريط تقدم الفيديو (العنصر <div> الداخلي). وكتلميح يمكنك إيجاد x و y لزوايا الشريط من خلال التابع ()getBoundingClientRect وإيجاد إحداثيي موقع مؤشر الفأرة من خلال كائن الحدث الذي ينتج عن حدث النقر على المستند. إليك مثالًا: document.onclick = function (e) { console.log(e.x, e.y); }; ترجمة -وبتصرف- لمقال: Video and audio APIs اقرأ أيضًا المقال السابق: العمل مع واجهات الرسوميات البرمجية في جافا سكريبت: الحلقات والرسوم المتحركة إضافة مقاطع الفيديو عبر العنصر <video> في HTML5 إضافة محتوى سمعي ومرئي في صفحة HTML تأثيرات التمرير في صفحات الويب باستخدام Javascript وCSS
  5. غطينا في مقالنا السابق بعض أساسيات الرسوم ثنائية البعد ضمن العنصر <canvas>، لكنك لن تلمس عمليًا فعالية هذا العنصر ما لم ترى قدرته على تحريك الرسوم. إذ يقدم هذا العنصر إمكانية إنشاء صور ورسومات باستخدام سكربتات مخصصة، لكن إن لم يكن هدفك تحريك أي شيء، عليك استخدام صور ثابتة لتوفر على نفسك عناء العمل. إنشاء الحلقات في Canvas لن يكون صعبًا التعامل مع الحلقات في <canvas>، وما عليك سوى استخدام تعليمات هذا العنصر (التوابع والخاصيات) داخل حلقة for أو غيرها من الحلقات كغيرها من شيفرات جافا سكريبت. لهذا سنعطي مثالًا تطبيقيًا عن الموضوع: أنشئ نسخة جديدة عن القالب الرسومي الذي أنشأناه في المقال السابق وافتحه ضمن محرر الشيفرة الذي تستخدمه. أضف السطر التالي إلى أسفل ملف جافا سكريبت، ويتضمن هذا السطر تابعًا جديدًا هو ()translate الذي يحرّك نقطة المبدأ في لوحة الرسم: ctx.translate(width / 2, height / 2); يسبب ذلك تحريك نقطة المبدأ (0,0) إلى مركز اللوحة بدلًا من كونها في الزاوية العليا اليسارية. ولهذا اﻷمر فائدته في الكثير من الحالات كما في مثالنا، إذ نريد أن يكون التصميم منسوبًا إلى مركز اللوحة. أضف اﻵن الشيفرة التالية: function degToRad(degrees) { return (degrees * Math.PI) / 180; } function rand(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } let length = 250; let moveOffset = 20; for (let i = 0; i < length; i++) {} سننجز هنا نفس الدالة ()degToRad التي رأيناها في مثال المثلث في مقالنا السابق، ونستخدم الدالة التي تعيد رقمًا عشوائيًا بين حدين علوي وسفلي معينين. إضافة إلى ذلك ننشئ المتغيرين length و moveOffset (سنرحهما لاحقًا)، كما نستخدم حلقة for فارغة. ما سنفعله هنا هو رسم شيء ما ضمن اللوحة لكن ضمن الحلقة for ثم نكرر ما نفعله عدة مرات. أضف اﻵن الشيفرة التالية داخل الحلقة for: ctx.fillStyle = `rgb(${255 - length} 0 ${255 - length} / 90%)`; ctx.beginPath(); ctx.moveTo(moveOffset, moveOffset); ctx.lineTo(moveOffset + length, moveOffset); const triHeight = (length / 2) * Math.tan(degToRad(60)); ctx.lineTo(moveOffset + length / 2, moveOffset + triHeight); ctx.lineTo(moveOffset, moveOffset); ctx.fill(); length--; moveOffset += 0.7; ctx.rotate(degToRad(5)); في كل تكرار: نضبط fillStyle ليكون ظلًا بنفسجيًا شفافًا قليلًا ويتغير كل مرة وفقًا لقيمة المتغير length. وكما سترى سيقل الطول في كل تكرار وبالتالي سيكون أثر ذلك على اللون الذي يصبح أكثر لمعانًا مع كل مثلث يُرسم على التتابع. نبدأ مسار الرسم. ننقل قلم الرسم إلى اﻹحداثي (moveOffset, moveOffset) ويُعرّف هذا المتغير المسافة التي يجب أن نحركها في كل مرة نرسم فيها مثلًا جديدًا. نرسم خطًا إلى اﻹحداثي (moveOffset+length, moveOffset) وهذا الخط طوله قيمة المتغير length ويوازي المحور x. نحسب ارتفاع المثلث كما فعلنا في المقال السابق. نرسم خطًا نحو رأس المثلث المتجه نحو الأسفل ومن ثم خطا إلى نقطة بداية المثلث. نستدعى التابع ()fill لملء المثلث. نحدّث قيمة المتغيرات التي تصف سلسلة المثلثات التي نرسمها كي نتمكن من رسم المثلث التالي. نخفض قيمة المتغير length بمقدار 1 وبالتالي سيصغر المثلث كل مرة. كما نزيد قيمة moveOffset بمقدار صغير كي يكون كل مثلث أبعد قليلًا عن سابقه. ونستخدم التابع ()rotate الذي يسمح لنا تدوير اللوحة بأكملها، حيث ندورها بمقدار خمس درجات قبل أن نرسم المثلث التالي. هذا كل ما في اﻷمر، وستبدو نتيجة مثالنا كالتالي: نشجعك اﻵن على إجراء تغييرات في هذا المثال وتجرّب ما تعلمته. إذ يمكنك مثلًا: أن ترسم مربعًا أو قوسًا بدلًا من المثلث. أن تغير قيمة المتغير length أو moveOffset. تغيير اﻷرقام العشوائية التي نولدها باستخدام الدالة ()rand التي وضعناها في الشيفرة السابقة ولم نستخدمها. ملاحظة: بإمكانك الاطلاع على الشيفرة كاملة لهذا المثال على جت-هب. الرسوم المتحركة ما فعلناه في مثالنا السابق أمر جميل، لكن ما تحتاجه حقيقة حلقة ثابتة تستمر وتستمر في أي تطبيق فعلي يعتمد على عنصر <canvs> (مثل اﻷلعاب الإلكترونية، والعرض البصري المباشر). فلو فكرت بلوحة الرسم على أنها فيلم سينمائي، ستعرف أنه عليك تحديث اﻹطارات المعروضة بشكل مستمر وبمعدل 60 إطار في الثانية (القيمة المثالية) كي يبدو المشهد المتحرك ناعمًا ومريحًا للعين البشرية. تقدم لك جافا سكريبت مجموعة من الدوال التي تسمح لك بتنفيذ دوال أخرى بشكل متكرر عدة مرات في الثانية الواحدة. ونجد أنسب هذه الدوال لمثالنا الدالة ()window.requestAnimationFrame التي تأخذ معاملًا واحدًا وهو اسم الدالة التي نريد استدعاءها. فإن رسمت هذه الدالة تحديثًا جديدًا من سلسلة الرسوم المتحركة التي سنعرضها، عليك حينها استدعاء الدالة ()window.requestAnimationFrame مجددًا قبل نهاية الدالة المنفذة للرسم كي تستمر حلقة الرسم. تنتهي الحلقة عندما تتوقف عن استدعاء ()window.requestAnimationFrame أو عند استدعاء الدالة ()window.caancelAnimationFrame بعد استدعاء ()window.requestAnimationFrame وقبل البدء برسم اﻹطار (الذي سيكون اﻷخير حينها). ملاحظة:من الممارسات الجيدة استدعاء الدالة ()window.caancelAnimationFrame من شيفرتك الرئيسية عند الانتهاء من الرسم، كي تضمن عدم وجود أية تحديثات أخرى يمكن أن تُعرض على اللوحة. يُنفّذ المتصفح التفاصيل المعقدة للعملية مثل تحريك الرسوم بمعدل ثابت، والتأكد من عدم تنفيذ رسوميات لا تُرى. ولكي تتعرف على عمل المتصفح، سنلقي نظرة على مثالنا السابق "الكرات القافزة المرتدة" (يمكنك تجربتها مباشرة أو الاطلاع على الشيفرة المصدرية😞 function loop() { ctx.fillStyle = "rgb(0 0 0 / 25%)"; ctx.fillRect(0, 0, width, height); for (const ball of balls) { ball.draw(); ball.update(); ball.collisionDetect(); } requestAnimationFrame(loop); } loop(); نشغل الدالة()loop مرة واحدة في آخر الشيفرة لنبدأ دورة الرسوميات برسم اﻹطار المتحرك الأول ومن ثم تتولى الدالة ()loop مسؤولية استدعاء الدالة (loop(requestAnimationframe التي ستحضر وترسم اﻹطار الثاني من الرسوم المتحركة وتكرر اﻷمر حتى النهاية. وتجدر ملاحظة أننا نمسح اللوحة تمامًا عند رسم كل إطار ومن ثم نعيد رسم كل شيء. إذ نرسم كل كرة ونحدّث موقعها ونتحقق فيما لو اصطدمت بكرة أخرى. وبمجرد أن ترسم شيئًا الى اللوحة، لن تتمكن من تعديله بشكل مستقل كما هو الحال مع عناصر شجرة DOM. ولن تستطيع أيضًا تحريك كل كرة بمفردها ضمن اللوحة، لأنك بمجرد رسم الكرة ستصبح جزءًا من اللوحة وليست كائنًا مستقلًا يمكنك التعامل معه. لهذا عليك مسح وإعادة رسم العناصر، إما بمسح اﻹطار بأكمله وإعادة رسم كل شيء أو كتابة شيفرة تحد تمامًا الجزء الذي يجب مسحه وبالتالي إعادة الرسم في المنطقة المحددة من اللوحة. لهذا يُعد تحسين الرسوم المتحركة اختصاصًا برمجيًا بحد ذاته، ويتطلب استعمال العديد من التقنيات الذكية المتاحة. لكن هذا اﻷمر خارج إطار مقالنا والمثال الذي نعمل عليه. وعمومًا، تتطلب عملية تنفيذ رسوم متحركة ضمن اللوحة الخطوات التالية: مسح محتوى اللوحة باستخدام ()fillRect أو ()clearRect. تخزين الحالة عند الضرورة باستخدام ()save، وذلك عندما تحتاج إلى حفظ اﻹعدادات التي حدّثتها في اللوحة قبل الاستمرار، وللأمر فائدته في التطبيقات المتقدمة. رسم الأشياء التي تريد تحريكها. استعادة الإعدادات التي خزنتها في الخطوة الثانية باستخدام ()restore. استدعاء الدالة ()requestAnimationFrame لجدولة رسم اﻹطار التالي من الرسم المتحرك. ملاحظة: لن نغطي الدالتين ()save و ()restore في مقالنا. تحريك شخصية بسيطة سننشئ اﻵن رسمًا متحركًا خاصًا بنا، تمشي الشخصية المقتبسة من أحد ألعاب الحاسوب القديمة خلال عرض الرسم المتحرك عبر الشاشة. أنشئ نسخة جديدة من القالب الذي نستخدمه في أمثلتنا وافتحه في محرر اﻷلعاب. حدّث شيفرة HTML حتى تعكس الصورة: <canvas class="myCanvas"> <p>A man walking.</p> </canvas> أضف اﻷسطر التالية إلى نهاية ملف جافا سكريبت كي تكون نقطة المبدأ منتصف لوحة الرسم.: ctx.translate(width / 2, height / 2); ننشئ تاليًا كائن HTMLImgeElement ونضبط قيمة الخاصية src له كي تكون عنوان الصورة التي نريد تحميلها ثم نضيف معالجًا للحدث onload الذي يستدعي الدالة ()draw عند اكتمال تحميل الصورة: const image = new Image(); image.src = "walk-right.png"; image.onload = draw; سنضيف اﻵن بعض المتغيرات التي تتعقب موقع الشخصية في اللوحة وعدد الشخصيات التي نرسمها على اللوحة: let sprite = 0; let posX = 0; سنشرح تاليًا صورة الشخصية المأخوذة من التطبيق Walking cycle using CSS animation تتضمن الصورة ست شخصيات تمثل تسلسل حركة الشخصية. عرض صورة كل شخصية 102 بكسل وارتفاعها 148 بكسل. ولرسم كل شخصية على حدة، علينا استخدام التابع ()drawImage لاقتصاص صورة واحدة للشخصية وعرض هذا الجزء فقط، كما فعلنا مع شعار فايرفوكس في مثال سابق. وينبغي ضرب اﻹحداثي X للشريحة بالعدد 102 ويبقى اﻹحداثي Y مساويًا للصفر، وستبقى أبعاد الشريحة دائمًا 102x148 بكسل. سنضع اﻵن شيفرة الدالة ()draw في اﻷسفل لكي نزودها بالشيفرة اللازمة: function draw() {} أما بقية الشيفرة في هذا القسم فستكون ضمن الدالة ()draw. لهذا أضف اﻷسطر التالية التي تمسح اللوحة وتعدها لرسم كل إطار. وانتبه إلى ضرورة تخصيص الزاوية العليا اليسارية من المربع لتكون (width/2, height/2) لأننا اتخذنا مركز اللوحة نقطة البداية. ctx.fillRect(-(width / 2), -(height / 2), width, height); نرسم تاليًا الصورة باستخدام الدالة drawImage التي تقبل تسع معاملات: ctx.drawImage(image, sprite * 102, 0, 102, 148, 0 + posX, -74, 102, 148); وكما ترى: خصصنا image لتكون الصورة التي نرسمها. يحدد المعاملان 2 و 3 إحداثيا الزاوية العليا اليسارية من الشريحة التي نريد اقتصاصها من الصورة المصدرية، ويكون X هو قيمة المتغير sprite مضروبًا بالعدد 102 (حيث يمثل المتغير عدد الشخصيات الموجودة في الصورة من 0 إلى 5) بينما تبقى قيمة Y هي 0. يحدد المعاملان 4 و 5 أبعاد الشريحة التي نقتصها وهي 102x148 بكسل. يحدد المعاملان 6 و 7 الزاوية العليا اليسارية من الصندوق الذي نرسم ضمنه الشخصية، وتكون قيمة اﻹحداثي X هي 0 + posX وبالتالي نستطيع تغيير مكان رسم الخصية بتغيير قيمة posX. يحدد المعاملان 8 و 9 أبعاد الصورة على اللوحة، وعلينا هنا المحافظة على اﻷبعاد اﻷصلية لهذا كانت قمة المعاملين 102 و 148 على التتالي: علينا تعديل قيمة المتغير sprite عند كل رسم if (posX % 13 === 0) { if (sprite === 5) { sprite = 0; } else { sprite++; } } لاحظ كيف وضعنا الشيفرة السابقة ضمن الكتلة ({}if (posX % 13 === 0 واستخدمنا العامل % (عامل باقي القسمة) للتحقق من إمكانية قابلية قسمة قيمة المتغير posX على 13. فإن كان الوضع كذلك ننتقل إلى الشخصية التالية بزيادة قيمة المتغير sprite بمقدار 1 (ثم نعود إلى 0 عندما تصبح قيمته 5). ويعني ذلك فعليًا أننا نغير الشخصية عند اﻹطار 13 وتقريبًا حوالي 5 إطارات في الثانية (تكرر الدالة ()requestAnimationFrame العملية بمعدل 60 إطار في الثانية إن أمكن). وعندما نعرض أخر شخصية نعود بعدها إلى الشخصية 0 وإلا سنزيد المتغير sprite بمقدار 1. سنعمل اﻵن على آلية تغيير قيمة posX مع كل إطار، لهذا عليك إضافة الشيفرة التالية تحت الشيفرة السابقة: if (posX > width / 2) { let newStartPos = -(width / 2 + 102); posX = Math.ceil(newStartPos); console.log(posX); } else { posX += 2; } نستخدم عبارة if...else للتحقق من تجاوز قيمة المتغير posX القيمة width/2 والذي يعني خروج الشخصية من يمين لوحة الرسم، وعندها نحسب موقعًا جديدًا للشخصية يضعها على يسار الحافة اليسرى للوحة. بينما إن لم تتجاوز قيمة المتغير posX تلك القيمة نزيد قيمته بمقدار 2. وبالتالي ستتحرك الشخصية إلى اليمين قليلًا في الإطار التالي. ولا بد أخيرًا من تنفيذ الحركة السابقة باستمرار عن طريق استدعاء ()requestAnimationFrame في نهاية الدالة ()draw: window.requestAnimationFrame(draw); ستبدو نتيجة الشيفرة اﻵن كالتالي: ملاحظة: بإمكانك الاطلاع على الشيفرة كاملة لهذا المثال على جت-هب. تطبيق رسومي بسيط كمثال أخير عن الرسوميات، سنعرض تطبيقًا بسيطًا جدًا للرسم نجمع فيه بين الاستجابة لمدخلات المستخدم (حركة الفأرة في هذا المثال) والحلقة التي تبني الرسم المتحرك. لن نشرح بالتفصيل خطوات بناء التطبيق بل سنلقي نظرة على الشيفرة اﻷكثر أهمية. بإمكانك الاطلاع على شيفرة التطبيق من خلال المستودع المخص له على جت-هب. لنلق نظرة على بعض اﻷجزاء المهمة: أولًا: نتتبع موقع الفأرة من خلال إحداثيات x و y، كما نترصد حدث نقر الفأرة وذلك من خلال المتغيرات curX و curY و pressed. وعندما تتحرك الفأرة يقع الحدث onmousemove وننفذ معالجه الذي يلتقط الإحداثيات الحالية لموقع الفأرة. كما نستخدم أيضًا معالجي الحدثين onmousedown و onmouseup لتغيير قيمة المتغير pressed إلى true عندما نضغط زر الفأرة وإلى false عندما نحرر الزر. let curX; let curY; let pressed = false; // حدّث إحداثيات موقع الفأرة document.addEventListener("mousemove", (e) => { curX = e.pageX; curY = e.pageY; }); canvas.addEventListener("mousedown", () => (pressed = true)); canvas.addEventListener("mouseup", () => (pressed = false)); عند النقر على الزر "مسح اللوحة Clean canvas" ننفذ دالة بسيطة تمحي اللوحة بأكملها وتعيدها إلى اللون اﻷسود: clearBtn.addEventListener("click", () => { ctx.fillStyle = "rgb(0 0 0)"; ctx.fillRect(0, 0, width, height); }); وحلقة الرسم بسيطة هنا، فعندما تكون قيمة المتغير pressed هي true، نرسم دائرة لها اللون الذي يحدده منتقي اﻷلوان color picker ونصف قطر يحدده عنصر تحديد المجال range input. وعلينا رسم الدائرة فوق النقطة المحددة بمقدار 85 بكسل، ذلك أن القياس مأخوذ بالنسبة إلى أعلى شاشة العرض (أعلى نافذة المتصفح) لكننا نرسم الدائرة بالنسبة ﻷعلى اللوحة التي تبدأ تحت شريط التحكم (الذي يضم منتقي اﻷلوان ومحدد نصف القطر) ذو الارتفاع 85 بكسل. ولو رسمنا الدائرة اعتمادًا على قيمة curY ستبدو الدائرة تحت النقطة المحددة للرسم بحدود 85 بكسل. function draw() { if (pressed) { ctx.fillStyle = colorPicker.value; ctx.beginPath(); ctx.arc( curX, curY - 85, sizePicker.value, degToRad(0), degToRad(360), false, ); ctx.fill(); } requestAnimationFrame(draw); } draw(); جميع أنواع عنصر الدخل <input> مدعومة جيدًا من قبل المتصفحات، وإن لم يدعمها متصفح سيعرض حقل نصي نمطي بدلًا عنه. الواجهة WebGL لنترك اﻵن البيئة الرسومية ثنائية البعد ونلقي نظرة سريعة على لوحات الرسم ثلاثية اﻷبعاد. تُستخدم الواجهة البرمجية WebGL API للعمل مع الرسومات ثلاثية البعد، وهي واجهة منفصلة تمامًا عن واجهة البيئة الرسومية ثنائية البعد مع أن شيفرتهما تُصيّر ضمن العنصر نفسه <canvas>. بنيت WebGL على أساس OpenGL (مكتبة الرسوميات المفتوحة Open Graphics Library) وتسمح لك بالتواصل مباشرة مع المعالج الرسومي للحاسوب GPU. لهذا فكتابة شيفرة WebGL خام أشبه بكتابة شيفرات لغات منخفضة المستوى مثل ++C مقارنة بشيفرة جافا سكريبت، فهي معقدة لكنها قوية جدًا. استخدام مكتبة جافا سكريبت خارجبة يستخدم معظم المطورون مكتبات يقدمها طرف آخر عند العمل مع الرسوميات ثلاثية البعد نظرًا لتعقيد WebGL مثل Three.js أو PlayCanvas أو Babylon.js. تعمل هذه المكتبات عومًا على نحو متشابه، فهي تقدم دوال أولية وأشكال مخصصة وكاميرات لعرض الموقع وطرق لتطبيق اﻹضاءة والظل ولتغطية السطوح بخامات مختلفة وغيرها. فهذه المكتبات تتعامل مباشرة مع WebGL بدلًا منك متيحة لك المجال للعمل وفق سوية برمجية أعلى. ويعني هذا بالطبع تعلم واجهات برمجية أخرى (واجهات يقدمها طرف آخر في حالتنا) لكنها أبسط بكثير من التعامل مع شيفرة WebGL الخام. إعادة إنشاء مكعب لنلق نظرة على مثال بسيط يشرح استخدام المكتبة WebGL، وسنختار فيه المكتبة Three.js كونها من أكثر المكتبات استخدامًا. وسنبني في مثالنا مكعب ثلاثي اﻷبعاد يدور حول نفسه. أنشئ نسخة عن ملف المثال) ضمن مجلد جديد ثم احفظ نسخة من الملف metal003.png في المجلد نفسه. ويمثل الملف اﻷخير الصورة التي نستخدمها لتغطية سطح المكعب لاحقًا. أنشئ ملفًا جديدًا باسم script.js في نفس المجلد السابق. نزّل المكتبة Three.min.js وخزنها في نفس المجلد السابق. لدينا اﻵن الملف three.js الذي يرتبط بصفحتنا، ويمكننا كتابة الشيفرة الذي تستخدمه ضمن الملف script.js. لنبدأ بإنشاء مشهد جديد عن طريق إضافة الشيفرة التالية: const scene = new THREE.Scene(); تُنشئ الدالة البانية()scene مشهدًا جديدًا يمثل بيئة عمل ثلاثية اﻷبعاد التي نريد عرضها. ونضيف بعد ذلك كاميرا لرؤية المشهد. ووفق مصطلحات التصميم ثلاثي اﻷبعاد، تمثل الكاميرا موقع المراقب، وﻹنشائها أضف اﻷسطر التالية: const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000, ); camera.position.z = 5; تأخذ الدالة البانية PrespectiveCamera أربع وسطاء: حقل الرؤية: ويدل على اتساع المساحة أمام الكاميرا التي يجب عرضها على الشاشة مقدرة بالدرجات. نسبة العرض aspect ratio: وهي عادة نسبة اتساع الشاشة مقسومًا على ارتفاعها، واستخدام نسب أخرى ستشوه المشهد. مستوي البعد: وتمثل البعد عن الكاميرا الذي لن تصير بعده اﻷشياء. نضبط أيضًا موقع الكاميرا ليكون على بعد خمس وحدات قياس بعيدًا عن المحور z، وهذا مشابه للخاصية z-index في CSS التي تمثل موقع العنصر بعيدًا عن الشاشة باتجاهك. أما المكون الحيوي الثالث فهو المصيّر renderer، وهو كائن يصير المشهد كما يُرى من الكاميرا. وسننشئ اﻵن مصيّرًا باستخدام الدالة البانية ()WebGLRenderer لكننا لن نستخدمه حاليًا: const renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); يُنشئ السطر الأول مصيرًا جديدًا والثاني يضبط الأبعاد التي سيرسم المصير ضمنها ما تعرضه الكاميرا، بينما يربط السطر الثالث العنصر <canvas> الذي يُنشئه المصيّر بجسم مستند HTML وسيُعرض كل ما يرسمه المصيّر في نافذة المتصفح. وﻹنشاء المكعب الذي نريد رسمه في اللوحة، عليك إضافة اﻷسطر التالية إلى نهاية ملف جافا سكريبت: let cube; const loader = new THREE.TextureLoader(); loader.load("metal003.png", (texture) => { texture.wrapS = THREE.RepeatWrapping; texture.wrapT = THREE.RepeatWrapping; texture.repeat.set(2, 2); const geometry = new THREE.BoxGeometry(2.4, 2.4, 2.4); const material = new THREE.MeshLambertMaterial({ map: texture }); cube = new THREE.Mesh(geometry, material); scene.add(cube); draw(); }); هناك نقاط عديدة يجدر شرحها في الشيفرة السابقة: ننشئ أولًا المتغير العام cube لكي نصل إلى المكعب في أي مكان من الشيفرة. ننشئ تاليًا كائن TextureLoader جديد ونستدعي التابع ()load العائد له. ويأخذ هذا التابع معاملين في حالتنا (علمًا أنه يأخذ أكثر): الخامة التي نريد أن نحمّلها (صورة PNG) ودالة ننفذها عند اكتمال تحميل الخامة. نستخدم داخل الدالة السابقة خاصيات الكائن لتكرار الصورة التي تغلف جميع أوجه المكعب بمقدار 2x2. ومن ثم ننشئ كائن BoxGeometry وكائن MeshLambertMaterial جديدان ونربطهما معًا ضمن شبكة Mesh ﻹنشاء المكعب. ويحتاج أي كائن نمطيًا إلى بنية هندسية (الشكل الذي سيكون عليه) ومظهر مادي (كيف سيبدو السطح الخارجي). وفي النهاية، نضيف المكعب إلى المشهد ومن ثم نستدعي الدالة ()draw لتبدأ عملية تحريك الرسم. وقبل أن نعرّف الدالة ()draw، نضيف زوجًا من الأضواء إلى المشهد، ليبدو المشهد أكثر حيوية: const light = new THREE.AmbientLight("rgb(255 255 255)"); // soft white light scene.add(light); const spotLight = new THREE.SpotLight("rgb(255 255 255)"); spotLight.position.set(100, 1000, 1000); spotLight.castShadow = true; scene.add(spotLight); يُعد الكائن AmbientLight نوعًا من اﻷضواء البرمجية التي تضيئ المشهد بأكمله بما يشبه الشمس التي تضيء عليك وأنت في الخارج. بينما يمثل الكائن spotLight شعاع ضوئي وفق اتجاه محدد مثل مشعل أو بقعة ضوء. لنضف اﻵن الدالة ()draw إلى أسفل شيفرة جافا سكريبت: function draw() { cube.rotation.x += 0.01; cube.rotation.y += 0.01; renderer.render(scene, camera); requestAnimationFrame(draw); } الشيفرة السابقة واضحة عمومًا، إذ ندوّر المكعّب قليلًا في كل إطار حول محوريه اﻷفقي والشاقولي ونصيّر المشهد كما يُرى من الكاميرا ونستدعي أخيرًا الدالة ()requestAnimationFrame لتحضير رسم اﻹطار التالي: إليك المشهد بشكله النهائي: ملاحظة: بإمكانك الاطلاع على الشيفرة كاملة لهذا المثال على جت-هب. الخلاصة لا بد وأن تكون في نهاية هذا المقال قد امتلكت فكرة لا بأس بها عن أساسيات برمجة الرسوميات باستخدام الواجهة البرمجية Canvas و المكتبة WebGL وما يمكن فعله باستخدام هاتين الواجهتين، وامتلكت فكرة جيدًا عن الأماكن التي تقصدها لتحصل على معلومات أكثر. ترجمة -وبتصرف- للقسم الثاني من مقال Drawing graphics اقرأ أيضًا المقال السابق: العمل مع الرسوميات في جافا سكريبت: الرسومات ثنائية البعد ضمن العنصر Canvas مقدمة إلى WebGL - إضافة التفاصيل إلى سطح مجسَّم مدخل إلى صناعة ألعاب المتصفح الرسم على لوحة في جافاسكربت
  6. يتضمن المتصفح مجموعة أدوات برمجية فعّالة للتعامل الرسوميات ابتداءً من لغة إنشاء الرسوميات الشعاعية SVG، إلى الواجهات التي تسمح لك بالرسم ضمن العنصر <canvas>. لهذا سنقدم في هذا المقال مدخلًا إلى الوجهة البرمجية Canvas، إضافة إلى بعض الموارد اﻷخرى لتزيد من معارفك في هذا المجال. ننصحك قبل المتابعة في قراءة هذه المقالات أن: تكون ملمًا بلغتي HTML و CSS. تكون ملمًا بلغة جافا سكريبت. تطلع على سلاسل المقالات السابقة التي ناقشت أساسيات جافا سكريبت والكائنات في جافا سكريبت. تطلع على أساسيات الواجهات البرمجية في طرف العميل. الرسوميات في الويب ذكرنا في مقالات سابقة أن الويب كان بداية نصيًا -أي يعرض محتوى نصي فقط- بشكل كامل مما جعله ضعيف الجاذبية، لذلك ظهرت الصور بداية من خلال العنصر <img> ولاحقًا من خلال خاصيات لغة التنسيق CSS مثل الخاصية background-image و من ثم بدأ استخدام الصور الشعاعية أو المتجهة SVG. مع ذلك لم يكن كل هذا كافيًا. وعلى الرغم من إمكانية استخدام CSS وجافا سكريبت لتحريك الصور الشعاعية SVG vector images والتعامل معها كونها تُكتب باستخدام تعليمات ترميز markup ولم تكن هناك طريقة لعمل المثل على الصور النقطية bitmap images وكانت الأدوات المتوفرة محدودة. ولم توجد طريقة أصيلة في الويب ﻹنشاء الرسوميات المتحركة أو اﻷلعاب أو المشاهد ثلاثية الأبعاد والتي تحتاج متطلبات خاصة تتعامل معها لغات برمجة منخفضة المستوى مثل ++C وجافا. بدأ الوضع بالتحسن عندما دعمت المتصفحات العنصر والواجهة البرمجية المتعلقة به في عام 2004. وكما سنرى تاليًا، تقدم عناصر canvas بعض اﻷدوات المفيدة التي تساعد في إنشاء رسوميات متحركة ثنائية البعد وألعاب، وعرض البيانات وغيرها من اﻹمكانات وخاصة عندما تتكامل مع واجهات برمجية أخرى تقدمها منصة الويب. لكن كان من الصعب إعدادها للوصول السهل accessibility. سترى في المثال التالي الكرات القافزة المرتدة التي عملنا عليها في مقال سابق وهي مشهد ثنائي البعد مبني على أساس العنصر canvas. وفي الفترة الممتدة بين 2006 إلى 2007 عملت موزيللا على إنجاز عناصر لوحات رسومية canvas ثلاثية اﻷبعاد، وتحولت فيما بعد إلى WebGL التي حظيت باهتمام مطوري المتصفحات وقد جرى توصيفها لتكون معيارًا بين عامي 2009-2010. وتتيح لك الواجهة WebGL إنشاء رسوميات ثلاثية اﻷبعاد ضمن المتصفح. بقدم المثال التالي مكعبًا يدور باستخدام هذه الواجهة: نركز في مقالنا على لوحات الرسم ثنائية البعد، وبما أن شيفرة WebGL الخام شديدة التعقيد، سنعرض طريقة استخدام المكتبة WebGL ﻹنشاء مشهد ثلاثي اﻷيعاد بسهولة أكبر. تطبيق عملي: ابدأ العمل مع لوحة الرسم canvas إن أردت إنشاء رسوميات ثنائية وثلاثية البعد على صفحة ويب عليك أن تنطلق من عنصر HTML الذي يُمثّل لوحة الرسم <canvas>. ويُستخدم هذا العنصر في تحديد منطقة من الصفحة للرسم فيها. واﻷمر بسيط ويتم بإضافة عنصر <canvas> إلى الصفحة كما يلي: <canvas width="320" height="240"></canvas> تنشئ الشيفرة السابقة لوحة رسم أبعادها 230 و 240 بكسل. ولا بد أن تضع شيئًا ما ضمن وسمي البداية والنهاية للعنصر كي يصف محتوى اللوحة لمستخدمي المتصفحات التي لا تدعم العنصر أو لمستخدمي قارئات الشاشة: <canvas width="320" height="240"> <p>اكتب هنا وصف اللوحة للمستخدمين الذين لا يمكنهم رؤيتها </p> </canvas> و لابد أن يعتبر ما تضعه ضمن وسمي العنصر بديلًا مفيدًا عن محتوى اللوحة، فإن كنت تصيّر أو تعرض رسمًا يتغير بشكل مستمر ليعبر عن أسعار البورصة مثلًا ، ينبغي أن يكون المحتوى البديل صورة تتضمن آخر تحديث للرسم مع نص بديل عنها alt يتحدث عن اﻷسعار أو قائمة من الروابط المستقلة لكل صفحة من صفحات هذه البورصة. ملاحظة: لا يمكن الوصول إلى محتوى لوحة الرسم من خلال قارئات الشاشات، لهذا عليك وضع نص يصف محتواها على شكل قيمة للسمة arial-label ضمن العنصر <canvas> نفسه أو استخدام محتوى مستقل ضمن وسمي البداية والنهاية للعنصر. وتذكر أن محتوى <canvas> ليس جزءًا من شجرة DOM لكن العنصر الذي تضعه ضمنه كذلك. إنشاء لوحة رسم وتحديد أبعادها لنبدأ بإنشاء لوحة رسم خاصة بتطبيقنا، لهذا اتبع الخطوات التالية: انسخ مجلد المشروع الذي يتضمن الملفات التالية: index.html script.js style.css افتح الملف index.html ثم أضف الشيفرة التالية ضمنه تحت الوسم <body>: <canvas class="myCanvas"> <p>Add suitable fallback here.</p> </canvas> أضفنا في الكود أعلاه صنفًا إلى العنصر <canvas> حتى يسهل الوصول إليه عن طريق جافا سكريبت في حال كان هناك أكثر من لوحة نريد العمل معها، لكننا أزلنا السمتين width و height حاليًا (بإمكانك إعادتهما إن أردت، لكننا سنضبطهما لاحقًا باستخدام جافا سكريبت). وستأخذ اللوحات افتراضيًا ارتفاعًا مقداره 150 بكسل واتساعًا مقداره 300 بكسل. افتح الملف scripts.js ثم أضف شيفرة جافا سكريبت التالية: const canvas = document.querySelector(".myCanvas"); const width = (canvas.width = window.innerWidth); const height = (canvas.height = window.innerHeight); خزّنا هنا مرجعًا إلى لوحة الرسم ضمن الثابت canvas ومن ثم أنشأنا ثابتًا آخر width وضبطنا قيمته وقيمة اتساع اللوحة لتكون مساوية لاتساع نافذة المتصفح Window.innerWidth، وكررنا ما فعلناه في السطر الثالث لكن مع ارتفاع اللوحة. وهكذا ستملأ لوحة الرسم نافذة المتصفح (تذكر أن الارتفاع هنا هو ارتفاع نافذة العرض viewport). لاحظ أيضًا كيف نفذنا سلسلة من اﻹسنادات باستخدام عدة عوامل مساواة =، وهذا أمر مسموح في جافا سكريبت ويُعد مفيدًا إن أردت إسناد القيمة ذاتها إلى عدة متغيرات. كما حرصنا على تأمين طريقة للوصول إلى أبعاد اللوحة بإسنادها إلى متغيرات، وتأتي فائدة هذه الفكرة إن احتجنا لاحقًا على سبيل المثال إلى رسم شيء ما في وسط اللوحة تمامًا. ملاحظة: علينا غالبًا ضبط أبعاد الصور باستخدام سمات HTML أو خاصيات شجرة DOM كما شرحنا في اﻷعلى. كما يمكنك استخدام CSS لكن تطبيق اﻷبعاد الجديدة سيكون بعد تصيير لوحة الرسم وهكذا قد تتعرض لوحة الرسم كغيرها من الصورة إلى التشوه. ضبط مسار العمل على اللوحة وإنهاء اﻹعداد نحتاج إلى مرجع خاص إلى منطقة العمل حتى نستطيع الرسم على اللوحة يُعرف بمسار العمل context. وننفذ هذا اﻷمر باستخدام التابع ()HTMLCanvasElement.getContext الذي يأخذ معاملًا واحدًا بأبسط حالات استخدامه تمثل نوع مسار العمل الذي نريده. وما نحتاجه في تطبيقنا لوحة ثنائية البعد، لهذا سنضيف شيفرة جافا سكريبت التالية في آخر الشيفرة الموجودة في الملف script.js: const ctx = canvas.getContext("2d"); ملاحظة: بإمكانك اختيار مسارات عمل أخرى مثل webgl من أجل WebGL و webgl2 من أجل 2 WebGL لكننا لن تحتاج هذه المسارات في مقالنا. وهكذا تصبح لوحة الرسم جاهزة في تطبيقنا، وسيحمل المتغير ctx الكائن CanvasRenderingContext2D وسيكون الرسم على اللوحة من خلال التعامل مع هذا الكائن. دعونا قبل إكمال العمل ننفذ شيئًا أخيرًا وهو تلوين خلفية الصفحة لتأخذ فكرة بسيطة عن الواجهة البرمجية Canvas. أضف الشيفرة التالية إلى شيفرة الملف script.js: ctx.fillStyle = "rgb(0 0 0)"; ctx.fillRect(0, 0, width, height); نضبط في هذه الشيفرة لون الخلفية باستخدام الخاصية fillStyle التي تأخذ قيمًا لونية كما هو حال خاصيات CSS المشابهة، ثم نرسم مربعًا يغطي كامل لوحة الرسم باستخدام التابع fillRect، ويمثل أول معاملين له الزاوية العليا اليسارية والمعاملين الباقين اتساع وارتفاع المربع الذي نريد رسمه (أخبرناك أن للمتغيرين width و height فوائد لاحقة). أساسيات الرسوميات ثنائية البعد ضمن العنصر <canvas> ذكرنا سابقًا أن جميع عمليات الرسم تجري من خلال التعامل مع الكائن CanvasRenderingContext2D (وهو ctx في تطبيقنا). وتحتاج الكثير من العمليات إلى إحداثيات لتحديد المكان الذي نرسم فيه بدقة، وتكون الزاوية العليا اليسارية بمثابة مبدأ الجملة اﻹحداثية وتمثل النقطة (0,0)، بينما يتجه المحور الأفقي (x) من اليسار نحو اليمين والعمودي (y) من اﻷعلى إلى اﻷسفل. تميل معظم الرسوميات إلى استخدام المربع البدائي primitive rectangle (الذي يمثل شكل أساسي يستخدم لبناء رسوميات أكثر تعقيدًا) أو تتبع خط عبر مسار محدد ومن ثم ملء الشكل الناتج. وسنشرح تاليًا كيف يجري اﻷمر. مربعات بسيطة سنبدأ برسم بعض المربعات البسيطة، لهذا: انسخ شيفرة قالب لوحة الرسم الذي حضرناه سابقًا (كما يمكنك إنشاء نسخة عن مجلد التطبيق إن لم تتابع معنا الخطوات السابقة). أضف الأسطر التالية من الشيفرة في أسفل شيفرة جافا سكريبت الموجودة: ctx.fillStyle = "rgb(255 0 0)"; ctx.fillRect(50, 50, 100, 150); إن حفظت التغيرات وأعدت تحميل الصفحة سترى مربعًا أحمر اللون ضمن اللوحة، تبعد زاويته العليا اليسارية مقدار 50 بكسل عن الحافتين العليا واليسارية للوحة (كما حددهما أول معاملين) وله اتساع مقداره 100 بكسل وارتفاع 150 بكسل (كما حددهما المعاملان اﻷخيران). لنضف اﻵن مربعًا آخر أخضر هذه المرة: ctx.fillStyle = "rgb(0 255 0)"; ctx.fillRect(75, 75, 100, 100); احفظ التغييرات وأعد تحميل الصفحة لترى النتيجة. تطرح الشيفرة السابقة نقطة هامة وهي أن جميع عمليات الرسم مثل رسم مربع أو خط وغيرها تُنفّذ وفق تسلسل ورودها في الشيفرة. فكّر بالأمر وكأنك ترسم على جدار، حيث تغطي كل طبقة ما تحتها. وبالطبع لا يمكن أن نغيّر هذا اﻷمر، لهذا عليك أن تفكّر مليًا بترتيب ما ترسمه على اللوحة. كما يمكنك إنشاء رسومات شبه شفافة عند اختيارك لونًا شبه شفاف باستخدام التابع ()rgb مثلًا. إذ تُعرّف القناة ألفا alpha channel مقدار الشفافية في اللون، وكلما كانت قيمتها أكبر كلما زادت قتامة اللون وغطّى ما تحته. أضف السطرين التاليين إلى الشيفرة: ctx.fillStyle = "rgb(255 0 255 / 75%)"; ctx.fillRect(25, 100, 175, 50); جرب أن ترسم مربعات لتختبر قدرتك! الإطارات وسماكة الخطوط رسمنا حتى اللحظة مربعات ممتلئة، لكنك تستطيع أيضًا رسم إطارات مربعة strokes. ولضبط لون اﻹطار نستخدم الخاصية strokeStyle ونرسمه باستخدام التابع strokeRect. أضف السطرين التاليين إلى الشيفرة: ctx.strokeStyle = "rgb(255 255 255)"; ctx.strokeRect(25, 25, 175, 200); للإطارات سماكة افتراضية قيمتها 1 بكسل، لكنك تستطيع تعديل السماكة باستخدام الخاصية lineWidth التي تأخذ قيمة تمثل سماكة اﻹطار مقدرة بالبكسل. أضف اﻵن السطر التالي إلى الشيفرة: ctx.lineWidth = 5; لاحظ كيف سيبدو اﻹطار أكثر سماكة. وسيبدو مثالنا حتى اللحظة كالتالي: ملاحظة: ستجد الشيفرة الكاملة لهذا المثال على جت-هب. رسم المسارات لو أردت رسم ما هو أعقد من مربع، لا بد حينها من رسم مسار. ويقتضي اﻷمر بأبسط أشكاله كتابة شيفرة تحدد تمامًا المسار الذي تريد أن يتحرك قلم الرسم عليه ضمن اللوحة حتى يرسم الشكل المطلوب. وتضم الواجهة Canvas دوال لرسم خطوط مستقيمة ودوائر ومنحنيات بيزيه وغيرها الكثير. لنبدأ اﻵن هذا القسم بنسخة جديدة من قالب المثال الذي أعددناه سابقًا، وسنستخدم بعض التوابع والخاصيات الشائعة خلال الأقسام التالية: ()beginPath: يبدأ رسم مسار من النقطة التي يكون عندها القلم حاليًا في اللوحة وستكون هذه النقطة مبدأ اﻹحداثيات إن كانت اللوحة جديدة. ()moveTo: ينقل القم إلى نقطة أخرى من اللوحة دون رسم أو تسجيل المسار بل يقفز القلم إلى النقطة المختارة. ()fill: يرسم شكلًا يملأ المسار الذي رسمه القلم. ()stroke: يرسم إطارًا مبنيًا على المسار الذي يرسمه القلم. باﻹمكان استخدام الخاصيات lineWidth و fillStyle أو strokeStyle مع المسارات أيضًا. تبدو شيفرة رسم مسار نمطي قريبة من التالي: ctx.fillStyle = "rgb(255 0 0)"; ctx.beginPath(); ctx.moveTo(50, 50); // ارسم مسارك ctx.fill(); رسم الخطوط لنرسم اﻵن مثلث متساوي الأضلاع ضمن اللوحة: أضف بداية الدالة المساعدة التالية في أسفل الشيفرة، مهمة هذه الدالة تحويل قيم الزوايا من درجات إلى راديان. تكمن فائدة هذه الدالة في أن جافا سكريبت تفهم قيم الزوايا بالراديان لكننا كبشر نفكر طبيعيًا بالدرجات. function degToRad(degrees) { return (degrees * Math.PI) / 180; } ابدأ المسار بإضافة الشيفرة التالية تحت الشيفرة السابقة، وفيها نضبط لون المثلث ونبدأ رسم المسار ثم ننتقل مباشرة إلى النقطة (0,0) دون رسم أي شيء ومن هذه النقطة نبدأ رسم المثلث: ctx.fillStyle = "rgb(255 0 0)"; ctx.beginPath(); ctx.moveTo(50, 50); أضف اﻷسطر التالية في نهاية الشيفرة السابقة: ctx.lineTo(150, 50); const triHeight = 50 * Math.tan(degToRad(60)); ctx.lineTo(100, 50 + triHeight); ctx.lineTo(50, 50); ctx.fill(); نرسم بداية خطًا من نقطة البداية إلى النقطة (150,50) وسيتجه مسارنا 100 بكسل إلى اليمين وفق المحور x. نحسب بعد ذلك ارتفاع المثلث متساوي اﻷضلاع باستخدام قواعد مثلثية بسيطة إذ نعلم أن زوايا المثلث هي 60 درجة. لهذا نستطيع تقسيم المثلث المتساوي اﻷضلاع الذي نوجهه للأسفل إلى مثلثين قائمين لكل منهما زاويتين حادتين قياسهما 30 و60 درجة. ونعرّف في المثلث القائم: الوتر hypotenuse: وهو أطول أضلاع المثلث القائم. المجاور adjacent: وهو هنا الضلع المجاور للزاوية 60 وطوله 50 بكسل لأنه يمثل نصف طول المسار الذي رسمناه سابقًا. المقابل opposite: وهو هنا الضلع المقابل للزاوية 60 ويمثل ارتفاع المثلث المتساوي اﻷضلاع الذي ننوي رسمه. يُعطى طول المجاور رياضيًا من خلال جداء المقابل بظل الزاوية tan: 50 * Math.tan(degToRad(60)) نستخدم هنا الدالة ()degToRad التي بنيناها سابقًا لتحويل الزاوية 60 درجة إلى راديان وهي القيمة التي يتوقعها التابع ()Math.tan الذي يحسب ظل الزاوية. بعد حساب اﻹرتفاع، نرسم خطًا آخر إلى النقطة (100, 50+triHeight) إلى نقطة أخرى لها إحداثي X يعادل نصف طول المسار المستقيمة السابق وإحداثي Y قيمته تعادل 50 زائدًا طول اﻹرتفاع، ذلك أن قاعدة المثلث تنزاح إلى داخل اللوحة مقدار 50 بكسل عن الحافة العليا لها. أما الخطوة التالية فهي رسم خط من آخر نقطة إلى نقطة البداية ليتكون المثلث. نستدعي في النهاية التابع ()ctx.fill ﻹنهاء المسار وملئ الشكل الناتج. رسم الدوائر لنلق نظرة على طريقة رسم الدوائر في اللوحة. تُنفّذ هذه العملية من خلال التابع ()arc الذي يرسم جزءًا من قوس الدائرة أو قوس الدائرة بأكمله ابتداءًا من نقطة محددة: ﻹضافة دائرة إلى لوحتنا ضع الشيفرة التالية في نهاية الشيفرة السابقة: ctx.fillStyle = "rgb(0 0 255)"; ctx.beginPath(); ctx.arc(150, 106, 50, degToRad(0), degToRad(360), false); ctx.fill(); يأخذ التابع ()arc ست معاملات، يحدد اﻷول والثاني اﻹحداثيين x و y لمركز الدائرة والثالث هو نصف قطر الدائرة، بينما يحدد المعاملين الخامس والسادس زاويتي البداية والنهاية لقوس الدائرة (0 و 360 يرسمان دائرة كاملة) ويحدد المعامل اﻷخير إذا ما كانت الدائرة سترسم باتجاه عقارب الساعة أو عكسها (تعني القيمة false أن الرسم باتجاه عقارب الساعة) ملاحظة: الزاوية 0 هي الزاوية الأفقية إلى اليمين. لنجرب إضافة قوس آخر: ctx.fillStyle = "yellow"; ctx.beginPath(); ctx.arc(200, 106, 50, degToRad(-45), degToRad(45), true); ctx.lineTo(200, 106); ctx.fill(); هناك اختلافان بسيطان عن النمط السابق: ضبطنا قيمة المعامل الأخير للتابع ()arc على القيمة true أي سيرسم القوس بعكس اتجاه عقارب الساعة، فحتى لو كانت زاوية البداية هي 45- وزاوية النهاية 45 درجة فإن القوس يغطي زاوية 270 درجة وليس 90 درجة والتي يمكن أن تحصل عليها إن كانت قيمة المعامل false. رسمنا خطًا إلى مركز الدائرة قبل استدعاء ()fill كي نحصل على دائرة اقتطع منها مثلث. وإن لم نرسم هذا الخط سيصل المتصفح نقطة البداية ونقطة النهاية ويملأ الشكل الناتج وهو دائرة اقتطع منها طرف. ستبدو نتيجة المثال السابق قريبة من التالي: ملاحظة: بإمكانك الاطلاع على الشيفرة كاملة لهذا المثال على جت-هب. رسم النصوص يتيح لك العنصر <canvas> رسم عبارات نصية، وهذا ما سنتعلمه بإيجاز تاليًا. لنبدأ بإنشاء نسخة جديدة عن قالب التطبيق كي نرسم المثال الجديد. ونستخدم في هذا المثال التابعين: ()fillText: الذي يملأ النص. ()strokeText: الذي يرسم الحواف الخارجية للنص. يأخذ كل تابع منهما ثلاثة خاصيات بشكله البسيط: النص الذي سيُرسم واﻹحداثيين x و y للنقطة التي يبدأ الرسم عندها. هذه النقطة هي عمليًا الزاوية السفلى اليسارية لصندوق النص الذي نرسمه (الصندوق الذي يحيط بالنص). قد يسبب اﻷمر إرباكًا أحيانًا بالنظر إلى أن عمليات الرسم اﻷخرى تميل إلى البدء من الزاويا العليا اليسارية، تذكر ذلك جيدًا. وهنالك أيضًا عدد من الخاصيات التي تساعد في إدارة تصيير النص مثل font التي تسمح بتخصيص عائلة الخط وحجمه وغيرها، وتأخذ قيمها وفق الصيغة نفسها التي نستخدمها مع خاصية CSS التي تحمل نفس الاسم. ولا يمكن لقارئات الشاشة الوصول إلى محتوى العنصر <canvas> لأن النص الذي يُرسم في اللوحة لا يُعد جزءًا من شجرة DOM، لهذا لا بد من جعله متاحًا لذوي الاحتياجات الخاصة. وفي مثالنا جعلنا النص المكتوب ضمن اللوحة قيمة للسمة aria-label. أضف اﻵن الشيفرة التالية إلى نهاية شيفرة جافا سكريبت: ctx.strokeStyle = "white"; ctx.lineWidth = 1; ctx.font = "36px arial"; ctx.strokeText("Canvas text", 50, 50); ctx.fillStyle = "red"; ctx.font = "48px georgia"; ctx.fillText("Canvas text", 50, 150); canvas.setAttribute("aria-label", "Canvas text"); رسمنا باستخدام الشيفرة السابقة سطرين أولهما مفرّغ واﻵخر ممتلئ، ويبدو الشكل النهائي للوحة شبيهًا بالتالي: ملاحظة: بإمكانك الاطلاع على الشيفرة كاملة لهذا المثال على جت-هب. رسم صور ضمن اللوحة بإمكانك أيضًا تصيّر صور خارجية كي تُرسم ضمن العنصر <canvas>، ويمكن أن تكون الصور بسيطةً أو إطارات من فيديو أو غير ذلك. وسنلقي نظرة على رسم صور بسيطة ضمن اللوحة. أنشئ نسخة جديدة من قالب التطبيق الذي نستخدمه لتنفيذ الرسوميات. إذ ترسم الصور ضمن اللوحة باستخدام التابع ()drawImage. ويأخذ التابع بأبسط أشكاله ثلاث معاملات هي مرجع إلى الصورة واﻹحداثيين x و y للزاوية العليا اليسارية من الصورة. لنبدأ بتحديد مصدر للصورة التي نريد رسمها، لهذا أضف الشيفرة التالية إلى ملف جافا سكريبت: const image = new Image(); image.src = "firefox.png"; أنشأنا في الشيفرة السابقة كائن HTMLImageElement جديد باستخدام الدالة البانية ()Image. وللكائن المعاد النوع ذاته الذي يُعاد عندما ننشئ مرجعًا إلى العنصر <img>، لهذا يمكن ضبط السمة src له كي تكون عنوان URL لصورة شعار فايرفوكس، وفي هذه المرحلة يبدأ المتصفح تحميل الصورة. يمكن اﻵن رسم الصورة ضمن اللوحة باستخدام ()drawImage، لكن علينا أولًا التأكد من اكتمال تحميل الصورة وإلا ستخفق العملية. نتحقق من ذلك عن طريق الحدث load الذي يقع فقط عند إنتهاء تحميل الصورة، لهذا أضف الشيفرة التالية: image.addEventListener("load", () => ctx.drawImage(image, 20, 20)); سترى إن أعدت تحميل اللوحة كيف رُسمت الصورة ضمن اللوحة. لكن بالطبع هناك المزيد. فماذا لو أردت رسم جزء من الصورة فقط أو أردت تغيير أبعادها؟ يمكننا بالطبع تنفيذ كلا اﻷمرين باستخدام صيغة أعقد للتابع ()drawImage. لهذا عدّل استدعاء التابع ()ctx.drawImage ليصبح كالتالي: ctx.drawImage(image, 20, 20, 185, 175, 50, 50, 185, 175); المعامل اﻷول هو مرجع إلى الصورة. يحدد المعاملان 2 و 3 إحداثيات الزاوية العليا اليسارية من المنطقة التي تريد اقتطاعها من الصورة المحمّلة، ولن يُرسم أي شئ أعلى أو إلى يسار قيمتي المعاملين السابقين. يحدد المعاملان 4 و 5 اتساع وارتفاع المنطقة التي تريد اقتطاعها من الصورة التي حملتها. يحدد المعاملان 6 و 7 إحداثيا النقطة التي نريد أن نبدأ فيها رسم الصورة المقتطعة انطلاقًا من الزاوية العليا اليسارية لها نسبة إلى الزاوية العليا اليسارية للوحة. يحدد المعاملان 8 و 9 اتساع وارتفاع المنطقة التي نريد أن نرسم فيها الصورة المقتطعة. وقد حددنا في مثالنا نفس أبعاد الصورة المقتطعة، لكن باﻹمكان إعادة تحجيم الصورة باستخدام قيم مختلفة للمعاملين. في حال غيّرت في الصورة تغييرًا واضحًا لابد من تحديث توصيف الصورة الخاص بسهولة الوصول accessibility. canvas.setAttribute("aria-label", "Firefox Logo"); ستبدو نتيجة المثال قريبة من التالي: ملاحظة: بإمكانك الاطلاع على الشيفرة كاملة لهذا المثال على جت-هب. الخلاصة تعرفنا في هذا المقال على أساسيات الرسم ضمن العنصر <canvas> من حيث إعداد العنصر وضبط معاملاته. ثم تدربنا على رسم الخطوط والمسارات والدوائر والنصوص والصور في بيئة ثنائية البعد. وسنتابع في الجزء الثاني من هذا المقال العمل مع الرسومات المتحركة ثنائية وثلاثية البعد. ترجمة -وبتصرف- للقسم اﻷول من مقال: Drawing graphics اقرأ أيضًا المقال السابق: واجهات برمجية خارجية في جافا سكريبت Third Party APIs الرسم عبر عنصر canvas في HTML5 التعامل مع عنصر Canvas باستخدام جافاسكربت (رسم الأشكال) التعامل مع العنصر Canvas باستخدام جافاسكربت (رسم الصور ) التعامل مع التصاميم، الألوان والخطوط باستخدام Canvas في جافاسكربت
  7. تُعد الواجهات البرمجية التي ذكرناها في مقالات سابقة واجهات مُضمَّنة في المتصفح، لكن ليست كل الواجهات البرمجية كذلك. إذ تقدم الكثير من الشركات مثل فيسبوك وجوجل و PayPal وغيرها، واجهات برمجية مخصصة تسمح للمطورين باستخدام بياناتها أو خدماتها (مثل عرض خريطة جوجل محددة في موقعك أو استخدام حساب فيسبوك لتسجيل المستخدمين في موقعك). لهذا نلقي نظرة في هذا المقال على الاختلافات بين الواجهات المضمنة في المتصفح والواجهات التي تقدمها أطراف أخرى والتي تعرف باسم third party APIs ونستعرض بعض الحالات النمطية لاستخدامها. ننصحك قبل المتابعة في قراءة هذه المقالات أن: تكون ملمًا بلغتي HTML و CSS. تكون ملمًا بلغة جافا سكريبت. تطّلع على سلاسل المقالات السابقة التي ناقشت أساسيات جافا سكريبت والكائنات في جافا سكريبت. تطّلع على أساسيات الواجهات البرمجية في طرف العميل. الواجهات البرمجية التي يقدمها طرف خارجي وهي واجهات يقدمها طرف خارجي (ليس أنت وليس المتصفح)، تسمح لك شركات مثل فيسبوك وتويتر وجوجل وغيرها بالوصول إلى بعض وظائف منتجاتها عبر جافا سكريبت واستخدامها في موقعك. ومن أكثر اﻷمثلة وضوحًا نجد الواجهات البرمجية للخرائط Maps API التي تعرض خرائط أماكن مخصصة على موقع الويب الخاص بك. دعنا نلقي نظرة على مثال بسيط يتعلق باستخدام الواجهة البرمجية Mapquest API يشرح الاختلاف بين الواجهات البرمجية التي يقدمها طرف آخر وتلك المضمنة في المتصفح. ملاحظة: بإمكانك تنزيل جميع ملفات اﻷمثلة دفعة واحدة ومن ثم البحث عن ملف المثال المطلوب الذي تحتاجه في كل قسم من المقال. الواجهات موجودة على خوادم الطرف الذي يقدمها تُضمن واجهات المتصفح البرمجية ضمن المتصفح وستتعامل معها من خلال جافا سكريبت مباشرة. ولقد رأيت في مقالنا التمهيدي كيف تعاملنا مع الواجهة البرمجية Web Audio API من خلال كائن جافا سكريبت اﻷصلي AudioContext: const audioCtx = new AudioContext(); // … const audioElement = document.querySelector("audio"); // … const audioSource = audioCtx.createMediaElementSource(audioElement); // etc. تتواجد الواجهات البرمجية التي يقدمها طرف خارجي على خوادم هذا الطرف، لهذا عليك أولًا الاتصال بتلك الواجهات حتى تتمكن من استخدامها في صفحاتك. وتقتضي هذه العملية بداية ربط صفحتك بمكتبة جافا سكريبت على ذلك الخادم عبر العنصر <script> كما في مثالنا عن الواجهة mapquest API. إليك شيفرة HTML: <script src="https://api.mqcdn.com/sdk/mapquest-js/v1.3.2/mapquest.js" defer></script> <link rel="stylesheet" href="https://api.mqcdn.com/sdk/mapquest-js/v1.3.2/mapquest.css" /> يمكنك اﻵن استخدام الكائنات التي تقدمها المكتبة: const map = L.mapquest.map("map", { center: [53.480759, -2.242631], layers: L.mapquest.tileLayer("map"), zoom: 12, }); تُنشئ الشيفرة السابقة متغيرًا لتخزين معلومات الخريطة، ثم تنشئ خريطة جديدة باستخدام التابع ()mapquest.map الذي يأخذ المعاملات التالية: معرّف العنصر <div> الذي تعرض الخريطة ضمنه وهو في مثالنا "map". كائن خيارات يضم تفاصيل الخريطة التي نريد عرضها، نذكر فيه إحداثيات الموقع وطبقة خريطة map layer نبنيها باستخدام التابع ()mapquest.titleLayer ومستوى تكبير الخريطة Zoom. هذه هي المعلومات التي تحتاجها الواجهة البرمجية mapquest API لعرض خريطة بسيطة، بينما يتكفل الخادم بمعالجة كل العمليات المعقدة مثل اختيار الخريطة الصحيحة للمنطقة المطلوبة وعرضها. ملاحظة: يختلف أسلوب الوصول إلى وظائف بعض الواجهات البرمجية عما عرضنا، إذ يطلب بعضها المطور الاتصال عن طريق طلب HTTP إلى عنوان URL معين للوصول إلى البيانات وتُدعى عندها RESTful APIs. تحتاج الواجهات البرمجية إلى مفاتيح وصول Access keys عادة تتعامل واجهات المتصفح البرمجية مع الأمور اﻷمنية من خلال عرض رسائل لتحديد السماحيات، والغاية من ذلك هو إعلام المستخدم بما يحصل على موقع الويب الذي يزوره وبالتالي لن يكون ضحية لاستخدامات مشبوهة للواجهة البرمجية. ويجري اﻷمر في الواجهات البرمجية التي يقدمها طرف خارجي على نحو مختلف قليلا، إذ تميل هذه الواجهات إلى استخدام مفاتيح لوصول المطورين إلى وظائف الواجهة، وهي مصممة غالبًا لحماية مقدّم الواجهة البرمجية بدلًا من المستخدم. إذ ستجد سطرًا برمجيًا مشابهًا للسطر التالي في مثالنا عن استخدام Mapquest API: L.mapquest.key = "YOUR-API-KEY-HERE"; يخصص هذا السطر مفتاح وصول إلى الواجهة لتستخدمه في تطبيقك. وينبغي أن يقدّم المطور طلبًا للحصول على مفتاح، ويمكنه بعدها تضمينه في الشيفرة كي يتمكن من الوصول إلى وظائف الواجهة البرمجية المطلوبة. وما عرضناه في السطر السابق هو بمثابة مفتاح افتراضي غير صحيح لتوضيح طريقة كتابته. ملاحظة: ستستخدم مفتاحك الخاص عندما تكتب تطبيقاتك الخاصة مكان المفتاح الافتراضي. وقد تطلب بعض الواجهات طريقة أخرى لتقديم مفتاحك، لكن الطرق متشابهة تقريبًا. إن الغاية من طلب مفتاح وصول وهو السماح لمقدمي الواجهة البرمجية معرفة مستخدمي الواجهة البرمجية وكيف يستخدمونها. فعندما يُمنح المطوّر مفتاحًا سيتعرف عليه مزوّد الواجهة، وسيتمكن المزود من اتخاذ الإجراءات المناسبة إن استخدم الواجهة بطريقة مشبوهة (مثل تعقب موقع شخص، أو محاولة إغراق الواجهة بالطلبات ﻹيقافها عن العمل). وستكون الطريق اﻷسهل للتعامل مع المطور هي إلغاء الامتيازات الخاصة به. توسيع مثال Mapquest لنضف اﻵن بعض الوظائف الجديدة إلى مثالنا الذي يستخدم الواجهة البرمجية mapquest وذلك لاستعراض بعض مزايا الواجهة: حتى نبدأ العمل في هذا القسم انسخ ملف المثال إلى مجلد جديد. وإن حضرت مسبقًا نسخة عن المستودع الذي يضم أمثلة المقال فستجد نسخة عن الملف المطلوب هنا في المجلد javascript/apis/mapquest/start. عليك تاليًا زيارة الموقع Mapquest developer site ثم إنشاء حساب والحصول على مفتاح مطوّر (يُدعى هذا المفتاح حتى لحظة كتابة هذه اﻷسطر "مفتاح المستهلك consumer key"، كما يُطلب إليك أثناء اﻹجراء تقديم عنوان رد النداء callback URL، لكن لا حاجة هنا لتقديم أي شيء لذا اتركه فارغًا). افتح ملف المثال واستبدل المفتاح الافتراضي بالمفتاح الذي حصلت عليه. تغيير نوع الخريطة هنالك أنواع مختلفة من الخرائط التي يمكن عرضها باستخدام الواجهة البرمجية mapquest API، ولاكتشاف الأمر، انتقل إلى السطر التالي: layers: L.mapquest.tileLayer("map"); وجرّب تغيير 'map' إلى 'hybrid' لعرض خريطة هجينة، وجرّب قيمًا أخرى أيضًا، من خلال الاطلاع على توثيق الخاصية titleLayer والخيارات المتاحة وغيرها من المعلومات. إضافة أدوات تحكم مختلفة يمكن استخدام عدة أدوات للتحكم بالخريطة، لكنها تعرض افتراضيًا فقط أدوات التكبير والتصغير. ولتوسيع أدوات التحكم المتاحة نستخدم التابع ()map.addControl، لهذا أضف السطر التالي إلى الشيفرة: map.addControl(L.mapquest.control()); يُنشئ التابع لوحة تحكم بسيطة كاملة الوظائف ويعرضها في الزاوية العليا اليمنى بشكل افتراضي. لكن بإمكانك تعديل موقع اللوحة بتخصيص كائن خيارات وتمريره كمعامل إلي التابع ()map.addControl بعد تحديد الموقع المطلوب. جرّب ما يلي: map.addControl(L.mapquest.control({ position: "bottomright" })); وهنالك أنواع أخرى من أدوات التحكم مثل اﻷدوات التي يقدمها التابعان ()mapquest.searchControl و ()mapquest.satelliteControl وبعضها معقدة وفعّالة. جرّب هذه اﻷدوات واكتشف إمكانياتها. إضافة علامة خاصة من السهل أيضًا إضافة علامة خاصة بك أو أيقونة إلى الخريطة، وذلك باستخدام التابع ()L.marker. أضف اﻵن الشيفرة التالية ضمن window.onload: L.marker([53.480759, -2.242631], { icon: L.mapquest.icons.marker({ primaryColor: "#22407F", secondaryColor: "#3B5998", shadow: true, size: "md", symbol: "A", }), }) .bindPopup("This is Manchester!") .addTo(map); وكما ترى يأخذ التابع بأبسط أشكاله معاملين: مصفوفة تضم الإحداثيات التي تريد عرض العلامة عندها، وكائن خيارات يضم الخاصية icon التي تحدد اﻷيقونة التي تُعرض في هذا المكان. تُعرّف الأيقونة باستخدام التابع ()mapquest.icons.marker الذي يتضمن معلومات مثل لون وحجم العلامة. وفي نهاية التابع نربط تابعًا آخر ('bindPopup('This is Manchester. يعرّف المحتوى الذي يُعرض عند النقر على العلامة. ثم نربط أخيرًا التابع (addTo(map. إلى نهاية السلسلة ﻹضافة العلامة فعليًا إلى الخريطة. اقرأ توثيق هذه الواجهة البرمجية وجرّب خيارات أخرى وراقب ما ستحصل عليه. إذ تقدم mapquest وظائف متقدمة مثل الاتجاهات والبحث وغيرها. ملاحظة: إن واجهتك أية مشاكل في تجربة المثال، قارن أمثلتك بالنسخة المكتملة منه. الواجهة البرمجية اﻹخبارية NYTimes (واجهة وفق معيار RESTful) لنلقِ نظرة اﻵن إلى مثال جديد مبني على الواجهة البرمجية لمجلة New York Times التي تسمح لك استخلاص اﻷخبار من المجلة وعرضها على صفحتك. يُعرف هذا النوع من الواجهات بواجهات RESTful والتي تحصل فيها على البيانات من خلال إرسال طلبات HTTP إلى عنوان URL محدد، وتُنفَّّذ عمليات البحث وغيرها من الخاصيات عن طريق تشفيرها ضمن عنوان URL (على شكل معاملات غالبًا). وهذا النوع شائع كثيرًا في الواجهات البرمجية إضافة إلى الواجهات التي تعتمد على ميزات مكتبات جافا سكريبت مثل mapquest. نهج لاستخدام الواجهات البرمجية التي يقدمها طرف خارجي سننتقل في المثال التالي خطوة خطوة لعرض طريقة استخدام الواجهة NYTimes والذي يقدم لك أيضًا خطوات عامة لتتبعها في العمل مع الواجهات البرمجية الجديدة. البحث عن التوثيق عندما تقرر العمل مع واجهة برمجية من طرف خارجي، عليك بداية إيجاد توثيق الواجهة والاطلاع على الميزات التي تقدمها وكيفية استخدامها. لهذا عليك الاطلاع على توثيق الواجهة البرمجية NYTimes قبل العمل على مثالنا. الحصول على مفتاح مطوّر تحتاج معظم الواجهات إلى استخدام مفتاح من نوع معين لأسباب إحصائية وآمنة. وللحصول على مفتاح للعمل على واجهة NYTimes اطلع على الخطوات اللازمة الواردة في صفحة المطورين. لنطلب مفتاحًا لاستخدام الواجهة في البحث عن مقال، لهذا أنشئ تطبيقًا جديدًا، واختر هذا الاستخدام ليكون الواجهة المطلوبة للتطبيق (املأ في النموذج اسم التطبيق ووصفًا له ثم اختر "Article search API" ثم انقر "Create"). انسخ المفتاح من الصفحة الناتجة عن التسجيل. حتى نبدأ المثال، انسخ جميع الملفات الموجودة في مجلد المثال إلى حاسوبك. وإن حضرت مسبقًا نسخة عن المستودع الذي يضم أمثلة المقال فستجد نسخة عن الملف المطلوب هنا في المجلد javascript/apis/nytimes/start. ستجد في الملف بعض المتغيرات التي تحتاجها ﻹعداد المثال، وسنملأ الملف بالشيفرة اللازمة لتزويده بالوظائف المطلوبة. سينتهي بك المطاف إلى تطبيق تكتب فيه معايير البحث مع إمكانية اختيار تاريخ بداية ونهاية فترة البحث، ومن ثم تستخدم هذه البيانات لاستعلام الواجهة NYTimes والبحث عن المطلوب. ربط الواجهة البرمجية مع التطبيق عليك أولًا ربط الواجهة البرمجية مع تطبيقك، وفي حالتنا، عليك أن تضيف المفتاح كمعامل للطلب get في كل مرة تطلب فيها بيانات من الخدمة على عنوان URL الصحيح. ابحث أولًا عن السطر التالي: const key = "INSERT-YOUR-API-KEY-HERE"; وأضف السطر التالي في ملف جافا سكريبت تحت التعليق: "// Event listeners to control the functionality". ومهمة هذا السطر تنفيذ الدالة ()submitSearch عند النقر على زر إرسال النموذج: searchForm.addEventListener("submit", submitSearch); أضف اﻵن تعريفي الدالتين ()submitSearch و ()fetchResults تحت السطر السابق: function submitSearch(e) { pageNumber = 0; fetchResults(e); } function fetchResults(e) { // Use preventDefault() to stop the form submitting e.preventDefault(); // Assemble the full URL let url = `${baseURL}?api-key=${key}&page=${pageNumber}&q=${searchTerm.value}&fq=document_type:("article")`; if (startDate.value !== "") { url = `${url}&begin_date=${startDate.value}`; } if (endDate.value !== "") { url = `${url}&end_date=${endDate.value}`; } } تضبط الدالة submitSearch رقم الصفحة على القيمة 0 ثم تستدعي الدالة ()fetchResults. نستخدم الدالة ()preventDefault العائدة لكائن الحدث كي لا تجري عملية إرسال مباشر للطلب قبل أن ننهي برمجة المثال. بعد ذلك، نشكل عنوان URL مكتمل بالعمل على بعض القيم النصية كي نستخدمه عند إرسال الطلب، وسنبدأ بالأجزاء الضرورية: قاعدة عنوان URL (تُؤخذ من المتغير baseURL). مفتاح الوصول إلى الواجهة البرمجية الذي يجب أن يُسند إلى المعامل api-key لعنوان URL (تؤخذ القيمة من المتغير key). رقم الصفحة الذي ينبغي إسناده إلى المعامل page لعنوان URL (تؤخذ القيمة من المتغير pageNumber). العبارة التي نبحث عنها، وتُسند إلى المعامل q لعنوان URL (تؤخذ القيمة من قيمة عنصر اﻹدخال <input> الذي يُدعى searchTerm). نوع المستند الذي نريد الحصول عليه، ويحدد من خلال التعبير الذي يُمرر إلى المعامل fq لعنوان URL. وفي حالتنا نريد أن يعيد البحث مقالًا. نستخدم تاليًا عبارتي ()if للتحقق من وجود قيم المتغيرين startDate و endDate. فإن كان اﻷمر كذلك وضعنا القيمتين في عنوان URL ضمن المعاملين الاختياريين begin_date و end_date. وسيبدو الشكل الكامل لعنوان URL شبيهًا بالعنوان التالي: https://api.nytimes.com/svc/search/v2/articlesearch.json?api-key=YOUR-API-KEY-HERE&page=0&q=cats&fq=document_type:("article")&begin_date=20170301&end_date=20170312 ملاحظة: بإمكانك الاطلاع على المعاملات اﻷخرى التي يمكن تضمينها ضمن عنوان URL في توثيق الواجهة البرمجية NYTimes. ملاحظة: يتحقق المثال بشكل مبسط من القيم المُدخلة. فيجب بداية إدخال نص البحث قبل إرسال الاستعلام باستخدام السمة required (مطلوب). كما يجب أن يضم حقل التاريخ 8 أرقام حتى يُرسل الطلب وذلك من خلال استخدام السمة pattern لتكون قيمتها {pattern=[0-9]{8. طلب البيانات من الواجهة البرمجية بعد أن شكلنا عنوان URL الخاص بالطلب لننفذ الطلب باستعمال الواجهة Fetch API. لهذا أضف الشيفرة التالية ضمن كتلة الدالة ()fetchResults: // لاستعلام الواجهة البرمجية fetch() استخدم fetch(url) .then((response) => response.json()) .then((json) => displayResults(json)) .catch((error) => console.error(`Error fetching data: ${error.message}`)); ننفذ الاستعلام بتمرير قيمة المتغير إلى الدالة ثم نحوّل الاستجابة إلى صيغة JSON عبر الدالة ()json ونمرر النتيجة إلى الدالة ()displayResults كي تُعرض البيانات على واجهة المستخدم. وبعدها يُعالج أي خطأ قد يقع باستخدام التابع ()catch.. عرض البيانات لننظر إلى الطريقة التي نعرض فيها البيانات على شاشة المستخدم. لهذا، أضف الدالة التالية تحت الدالة ()fetchResults مباشرة: function displayResults(json) { while (section.firstChild) { section.removeChild(section.firstChild); } const articles = json.response.docs; nav.style.display = articles.length === 10 ? "block" : "none"; if (articles.length === 0) { const para = document.createElement("p"); para.textContent = "No results returned."; section.appendChild(para); } else { for (const current of articles) { const article = document.createElement("article"); const heading = document.createElement("h2"); const link = document.createElement("a"); const img = document.createElement("img"); const para1 = document.createElement("p"); const keywordPara = document.createElement("p"); keywordPara.classList.add("keywords"); console.log(current); link.href = current.web_url; link.textContent = current.headline.main; para1.textContent = current.snippet; keywordPara.textContent = "Keywords: "; for (const keyword of current.keywords) { const span = document.createElement("span"); span.textContent = `${keyword.value} `; keywordPara.appendChild(span); } if (current.multimedia.length > 0) { img.src = `http://www.nytimes.com/${current.multimedia[0].url}`; img.alt = current.headline.main; } article.appendChild(heading); heading.appendChild(link); article.appendChild(img); article.appendChild(para1); article.appendChild(keywordPara); section.appendChild(article); } } } سنشرح فيما يلي النقاط التي عالجتها الشيفرة السابقة: تُستخدم الحلقة while عادة لحذف محتوى أي عنصر من عناصر شجرة DOM، وفي حالتنا لمسح محتوى العنصر <section>. إذ نتحقق في هذه الحلقة من وجد ابن أول first child للعنصر باستمرار ونحذفه إن وجد، ثم تنتهي الحلقة عندما لا يتبقى أبناء لهذا العنصر. نضبط قيمة المتغير articles لتكون قيمة json.reponse.docs وهي المصفوفة التي تضم كل الكائنات التي تمثل المقالات التي يعيدها البحث، وذلك لجعل لتبسيط الشيفرة التي تأتي لاحقًا. تتحقق الكتلة ()if اﻷولى من وجود 10 نتائج (لأن الواجهة تعيد حتى 10 نتائج في كل مرة)، فإن كان اﻷمر كذلك، تعرض الشيفرة العنصر <nav> الذي يضم زري التنقل بين الصفحات Previous 10 و Next 10. إما إن كان عدد النتائج أقل من عشرة فلن يُعرض الزران السابقان لأن الصفحة ستتسع للنتائج. ونناقش شيفرة زري التنقل في فقرة قادمة. تتحقق الكتلة ()if الثانية من عدم وجود مقالات يعيدها البحث، فإن كان اﻷمر كذلك، لن نعرض أي شيء، بل ننشئ عنصر فقرة <p> يضم النص "لا توجد نتائج No results return"، ونلحقها بالعنصر <section>. في حال وجود نتائج، ننشئ بداية العناصر اللازمة لعرض نتائج كل مقال إخباري ومن ثم ترتيب هذه العناصر ضمن بعضها بالشكل الصحيح وإلحاقها بشجرة DOM في المكان المناسب. ولمعرفة أية خاصيات لكائنات المقالات تحتوي على المعلومات التي نريد عرضها، عُد إلى مراجع البحث عن مقالات باستخدام الواجهة NYTimes. إن معظم العمليات السابقة واضحة، لكن بعضها يستحق التوقف والشرح: استخدمنا ()for ...of للتنقل بين جميع المفاتيح المرتبطة بكل مقال ومن ثم وضع كل مفتاح ضمن عنصر <span> مخصص داخل فقرة نصية <p> ليسهل تنسيق البيانات. استخدمنا الكتلة { }if (current.multimedia.length > 0) للتحقق إن احتوى أي مقال على صور، لأن بعضها لا يمتلك أيًا منها، ونعرض الصورة اﻷولى إن وجدت، وإلا سيُرمى خطأ. كتابة شيفرة أزرار التنقل بين الصفحات حتى يعمل زرا التنقل بين الصفحات لابد من زيادة قيمة المتغير pageNumber أو إنقاصها ومن ثم إعادة تنفيذ طلب إحضار البيانات بعد تحديث قيمة المتغير في عنوان URL. ويعمل هذا لأن الواجهة البرمجية تعيد 10 نتائج فقط في كل مرة، فإن توفّر أكثر من ذلك تعيد العشرة الأولى (من 0 إلى 9) إن كانت قيمة المعامل page هي 0 (أو لم يستخدم هذا المعامل في العنوان أصلًا) وستعيد المجموعة الثانية من النتائج (من 10-19) عندما تكون قيمة المعامل page هي 1. يتيح لنا ذلك كتابة دالة بسيطة للتنقل بين الصفحات. أضف الشيفرة التالية بعد الدالة ()addEventListener لكي تستدعي الدالتين ()nextPage و ()previousPage عند النقر على الزر الموافق: nextBtn.addEventListener("click", nextPage); previousBtn.addEventListener("click", previousPage); لنعرّف اﻵن بعد إضافة الشيفرة السابقة الدالتين السابقتين: function nextPage(e) { pageNumber++; fetchResults(e); } function previousPage(e) { if (pageNumber > 0) { pageNumber--; } else { return; } fetchResults(e); } تزيد الدالة اﻷولى قيمة المتغير pageNumber ومن ثم تنفّذ الدالة مجددًا لعرض نتائج الصفحة التالية. وتعمل الدالة الثانية بنفس الطريقة تمامًا لكن بالعكس، وعلينا اتخاذ خطوة إضافية للتحقق أن قيمة المتغير pageNumber ليست صفرًا قبل إنقاص القيمة، فقد يسبب تنفيذ طلب إحضار البيانات بقيمة سالبة لهذا المتغير خطأً. فإن كانت قيمته بالفعل 0 ننهي الدالة مباشرة بتنفيذ return، ولا حاجة ﻹعادة طلب نفس النتائج الموجودة مرة أخرى. ملاحظة: بإمكانك الاطلاع على النسخة المكتملة من التطبيق على جت-هب (وبإمكانك تجربته مباشرة أيضًا). مثال عن استخدام واجهة يوتيوب البرمجية نقدم لك مثالًا أيضًا عن واجهة يوتيوب البرمجية لتدرسه وتتعلم منه. لهذا الق نظرة على مثال YouTube video search example الذي يتضمن واجهتين مرتبطتين ببعضهما: الواجهة YouTube Data API لليحث عن فيديوهات على يوتيوب وإعادة النتائج. الواجهة YouTube IFrame Player API لعرض الفيديوهات التي يعيدها البحث ضمن إطار IFrame لتشغيل فيديو. تأتي أهمية هذا المثال من كونه يعرض طريقة للربط بين واجهتين برمجيتين يقدمهما طرف خارجي وتعملان معًا. اﻷولى تتوافق مع واجهات RESTful وتعمل اﻷخرى بطريقة مشابهة للواجهة mapquest (تستخدم توابع ودوال خاصة). وتجدر اﻹشارة إلى أن كلتا الواجهتين تتطلب استخدام مكتبة جافا سكريبت في الصفحة، وأن للواجهة RESTfull دوال للتعامل مع تحضير طلبات HTTP وإعادة النتائج. لن نعرض الكثير عن هذا المثال في مقالنا، لكنك ستجد الكثير من التفاصيل في التعليقات التي تشرح طريقة العمل. ولكي تشغّل المثال تحتاج إلى: قراءة توثيق الواجهة البرمجية. زيارة الصفحة Enabled APIs page والتأكد من حالة الواجهة YouTube Data API v3 (هل هي ON) ضمن قائمة الواجهات المعروضة. الحصول على مفتاح لاستخدام الواجهة من خلال Google Cloud. استبدال القيمة ENTER-API-KEY-HERE في الشيفرة المصدرية بالمفتاح الذي حصلت عليه. تنفيذ المثال من خلال خادم ويب، فلن يعمل إن شغّلته في متصفحك مباشرة. (من خلال العنوان //:file). الخلاصة يقدم هذا المقال مدخلًا مفيدة عن استخدام الواجهات البرمجية التي يقدمها طرف خارجي ﻹضافة وظائف جديدة إلى موقعك. ترجمة -وبتصرف- لمقال: Third-party APIs. اقرأ أيضًا المقال السابق: إحضار البيانات من الخادم باستخدام جافا سكريبت البرمجة غير المتزامنة في جافاسكريبت إرسال البيانات واستلامها عبر الشبكة في جافاسكربت استخدام Fetch مع الطلبات ذات الأصل المختلط Cross-Origin في جافاسكربت
  8. تطرقنا سابقًا للحديث عن واجهة برمجة التطبيقات واستخدامها في مهام مختلفة مثل معالجة مستندات الويب وشجرة DOM، ومن المهام اﻷخرى الشائعة لتطبيقات ومواقع الويب إحضار بيانات محددة من الخادم لتحديث بعض أجزاء صفحة الويب دون الحاجة إلى تحميل صفحة جديدة. وكان لهذا اﻷمر البسيط أثر هائل على أداء وسلوك صفحات الويب. سنشرح في هذا المقال التقنيات التي تسمح بتنفيذ هذه اﻷمور مثل وعلى وجه الخصوص الواجهة Fetch. ننصحك قبل المتابعة في قراءة هذه المقالات أن: تكون ملمًا بلغتي HTML و CSS. تكون ملمًا بلغة جافا سكريبت. تطلع على سلاسل المقالات السابقة التي ناقشت أساسيات جافا سكريبت والكائنات في جافا سكريبت. تطلع على أساسيات الواجهات البرمجية في طرف العميل. ما الذي يحدث عند طلب مورد من الخادم؟ تتكون صفحة الويب من صفحة HTML إضافة إلى عدة ملفات أخرى كملفات التنسيق CSS والسكريبتات والصور. وما يحدث وفق النموذج النمطي لتحميل الصفحة، أن متصفحك سيرسل عدة طلبات إلى الخادم لإحضار تلك الملفات اللازمة لعرض الصفحة بالشكل المطلوب، ومن المفترض أن يستجيب الخادم مرسلًا تلك الملفات. ويتكرر اﻷمر في كل مرة تزور فيها صفحة جديدة. يعمل هذا النموذج جيدًا في العديد من المواقع، لكن ماذا لو كان الموقع مخصصًا ﻹحضار بيانات من الخادم مثل موقع مكتبة؟ ومن الممكن أن تفكّر في هذا النوع من المواقع على أنه واجهة تربط المستخدم بقاعدة بيانات. فقد يسمح لك بالبحث عن نوع معين من الكتب أو قد ينصحك بكتب تستهويك وفقًا لقائمة الكتب التي استعرتها سابقًا. وعندما تفعل ذلك، لا بد من تحديث محتوى الصفحة بمجموعة الكتب الجديدة التي بحثت عنها أو نصحك بها الموقع. مع هذا، سيبقى جُل محتوى الصفحة كما هو دون تغيير مثل الترويسة والتذييل والأشرطة الجانبية. إن مشكلة النموذج النمطي في هذه الحالة هو أنه سيحضر البيانات ويعيد تحميل الصفحة بأكملها حتى لو أردنا تحديث جزء صغير منها، وهذا أمر عديم الجدوى كما يقدم تجربة سيئة لمستخدمي الموقع. ولتجنب هذا اﻷمر، تستخدم العديد من مواقع الويب واجهات جافا سكريبت البرمجية APIs لطلب البيانات من الخادم وتحديث محتوى الصفحة دون إعادة تحميلها. فعندما يبحث المستخدم عن منتج جديد مثلًا، سيطلب المتصفح فقط البيانات التي يحتاجها لتحديث الصفحة كأن يطلب قائمة بالكتب التي يجب عرضها. إن الواجهة البرمجية الرئيسية في هذه الحالة هي الواجهة Fetch التي تسمح لشيفرة جافا سكريبت في صفحة الويب بإرسال طلبات إلى الخادم لإحضار مورد محدد. وعندما يقدم الخادم البيانات المطلوبة، يمكن للشيفرة أن تستخدمها لتحديث محتوى الصفحة من خلال واجهة برمجية أخرى هي شجرة DOM عادة. وغالبًا ما تكون البيانات المطلوبة محضّرة وفق تنسيق JSON وهي صيغة مناسبة لنقل البيانات المهيكلة، لكن البيانات قد تكون أيضًا شيفرة HTML أو مجرد نص نمطي. وستجد هذا النموذج في الكثير من العديد من المواقع المصممة لتبادل البيانات مثل أمازون ويوتيوب وإي باي وغيرها. ومن خلال هذا النموذج: سيكون تحديث الصفحة أسرع بكثير ولن تُضطر إلى الانتظار حتى تُحمَّل الصفحة ككل، وهذا ما يعطي شعورًا بان الموقع أسرع وأكثر تجاوبًا. يُنزَّل كم قليل من البيانات عند كل تحديث للمحتوى وبالتالي هدرًا أقل لحزمة البيانات المخصصة للمستخدم. وعلى الرغم أن هذا الأمر لن يكون مشكلة في اﻷجهزة المكتبية المتصلة بحزمة اتصال عريضة broadband لكنها مهمة جدًا في اﻷجهزة المحمولة أو في الدول التي لا تمتلك خدمة انترنت سريعة. ملاحظة: عُرفت هذه التقنية في بداياتها باسم "جافا سكريبت غير المتزامنة و XML" واختصارًا أجاكس Ajax لأنها تميل إلى إحضار البيانات على شكل بيانات XML. وعلى الرغم أن البيانات المطلوبة حاليًا هي بيانات JSON، لكن الطريقة تبقى ذاتها ولازال المصطلح Ajax يشير إلى هذه التقنية ولتسريع اﻷمر أكثر، تُخزّن بعض المواقع أيضًا بيانات وأصولًا ضمن حاسوب المستخدم عند طلبها للمرة اﻷولى، وبالتالي ستُستخدم هذه النسخة المخزنة عند الزيارات اللاحقة بدلًا من تنزيل نسخ جديدة في كل مرة تُحمّل فيها الصفحة. ولن يُعاد تحميل المحتوى من الخادم إلا عندما يُحدّث هذا المحتوى على الخادم. الواجهة البرمجية Fetch سنتعلم أكثر عن هذه الواجهة من خلال المثاليين اﻵتيين. إحضار محتوى نصي سنطلب في هذا المثال بيانات نصية من عدة ملفات ونستخدمها لتحديث الجزء الذي يضم محتوى الصفحة. ستعمل سلسلة الملفات النصية السابقة كقاعدة بيانات مفترضة، لكن تجدر اﻹشارة إلا أننا نستخدم لطلب البيانات عادة لغة برمجة من طرف الخادم في حالات كهذه مثل PHP أو بايثون أو Node.js من قاعدة بيانات حقيقية. مع ذلك، نتوخى في مثالنا البساطة ونحاول التركيز على تقنية إحضار البيانات من طرف العميل. وحتى تبدأ العمل معنا حمّل نسختك من الملفات fetch-start.html و verse1.txt و verse2.txt و verse3.txt verse4.txt، ثم ضعها في مجلد جديد على حاسوبك. وما سنفعله لاحقًا هو إحضار أبيات محددة من قصيدة عندما نختار هذه اﻷبيات من قائمة منسدلة. أضف الشيفر التالية ضمن العنصر <script>. إذ تُخزّن هذه الشيفرة مرجعين إلى العنصرين <select> و <pre>، وعندما يختار المستخدم قيمةً، تمرر هذه القيمة كمعامل إلى الدالة ()updateDisplay: const verseChoose = document.querySelector("select"); const poemDisplay = document.querySelector("pre"); verseChoose.addEventListener("change", () => { const verse = verseChoose.value; updateDisplay(verse); }); لنعرّف بداية الدالة ()updateDisplay بوضع الشيفرة التالية تحت الشيفرة السابقة: function updateDisplay(verse) { } نبدأ كتابة شيفرة الدالة بإنشاء عنوان URL نسبي يشير إلى الملف النصي الذي نريد تحميله، إذ نحتاجه لاحقًا. وستكون قيمة العنصر <select> مطابقة دائمًا لمحتوى هذا العنصر ما لم نسند إليه قيمة أخرى من خلال السمة value مثل "Verse 1". في هذه الحالة سيكون الملف النصي الموافق هو الملف "verse1.txt" الموجود في نفس المجلد الذي يضم ملف HTML، لهذا يكفي استخدام اسم الملف كعنوان URL نسبي. وانتبه إلى أن الخوادم تتحس حالة اﻷحرف غالبًا لهذا السبب علينا إزالة الفراغ من القيمة "Verse 1" وكذلك تحويل الحرف "V" إلى الشكل الصغير "v" ومن ثم إضافة اللاحقة "txt.". ولتنفيذ ذلك، استخدم التابعين النصيين ()replace و ()toLowerCase إضافة إلى قالب حرفي template literal {...}$ كالتالي: verse = verse.replace(" ", "").toLowerCase(); const url = `${verse}.txt`; وأخيرًا أصبحنا مستعدين لاستخدام الواجهة البرمجية Fetch: //URL ومرر إليها عنوان `fetch()`استدع fetch(url) //وعدًا، وعندما يستجيب الخادم يًستدعى التابع fetch() تعيد الدالة //`then()` .then((response) => { // يرمي معالج الحدث خطأً إن أخفق الوعد if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } // وإن نحج الوعد، يعيد معالج الحدث الاستجابة على شكل نص باستدعاء التابع //الذي يعيد بدوره وعدًا، وعند إنجاز الوعد اﻷخير response.text() }) //`poemDisplay` الذي يعيد النص فننسخه إلى مربع النص `then()` يُستدعى .then((text) => { poemDisplay.textContent = text; }) //`poemDisplay` التقاط أية أخطاء أخرى وعرضها برسالة ضمن الصندوق .catch((error) => { poemDisplay.textContent = `Could not fetch verse: ${error}`; }); وإليك توضيحًا للشيفرة السابقة: إن مدخل الواجهة البرمجية Fetch هو الدالة العامة ()fetch التي تأخذ عنوان URL معاملًا لها (ولها أيضًا معامل آخر اختياري لأغراض خاصة، لكننا لم نستخدمه). الدالة ()fetch هي دالة غير متزامنة تعيد وعدًا Promise، بإمكانك مراجعة مقال استخدام الوعود في جافا سكريبت غير المتزامنة إن لم تكن على دراية بمفهوم الوعود، ثم العودة والمتابعة معنا، وستلاحظ أن هذا المقال يتحدث أيضًا عن الواجهة Fetch. تعيد الدالة ()fetch وعدًا، لهذا نمرر دالة إلى التابع ()then المرتبط بهذا الوعد. يُستدعى هذا التابع حالما يتلقى طلب HTTP ردًا من الخادم. ومن ثم نتحقق من نجاح الوعد (إنجازه) ضمن دالة معالج الحدث ونرمي رسالة خطأ إن لم ينجح. وعند النجاح، نستدعي الدالة ()response.text للحصول على جسم الاستجابة على شكل نص. إن الدالة ()response.text هي أيضًا دالة غير متزامنة، لهذا نعيد الوعد الذي تعيده ونمرره إلى التابع ()then المرتبطة بالوعد الجديد. يُستدعى هذا التابع عندما يجهز نص الاستجابة، ونضع ضمنه شيفرة تحديث محتوى العنصر <pre>. نربط أخيرًا دالة المعالجة ()catch في النهاية لالتقاط أية أخطاء ترميها أيًا من الدوال غير المتزامنة التي استدعيناها أو معالجات اﻷحداث المتعلقة بها. أحد مشكلات هذا المثال أنه لن يعرض أية قصيدة عندما يُحمّل للمرة الأولى. وﻹصلاح اﻷمر، أضف السطرين التاليين في نهاية شيفرتك (قبل وسم النهاية <script/>) لتحميل verse 1 افتراضيًا ولكي يأخذ العنصر <select> القيمة الصحيحة. updateDisplay("Verse 1"); verseChoose.value = "Verse 1"; تشغيل المثال على الخادم لن تسمح المتصفحات الحديثة بتنفيذ طلبات HTTP إن كنت تشغّل المثال على حاسوبك الشخصي بسبب قيود أمنية. وللالتفاف على الموضوع، عليك اختبار المثال على خادم ويب محلي. للمزيد من المعلومات اطلع على مقال دليل إعداد خادم ويب محلي خطوة بخطوة الذي نشرته أكاديمية حسوب. متجر معلبات أنشأنا في هذا المثال موقعًا بسيطًا يُدعى متجر المعلبات The Can Store، وهو سوبر ماركت يبيع المعلبات فقط. بإمكانك تجربة المثال مباشرة على جت-هب والاطلاع على شيفرته المصدرية. يعرض هذا الموقع افتراضيًا جميع المنتجات، لكنك تستطيع ترشيح أو فلترة هذه المنتجات والبحث عنها باستخدام أدوات التحكم الموجودة ضمن العمود اليساري للصفحة. قد تجد بعض التعقيد في شيفرة ترشيح المنتجات وفقًا للتصنيف مثل معايير البحث ومعالجة النصوص لعرض البيانات بشكل صحيح على واجهة المستخدم وغيرها. لن نشرح بالطبع كل التفاصيل في هذا المقال لكنك ستجد كما كبيرًا من التعليقات التي تشرح الشيفرة ضمن الملف can-script.js، مع ذلك سنشرح شيفرة الواجهة fetch. ستجد أولى الكتل البرمجية التي تستخدم fetch في مقدمة الملف: fetch("products.json") .then((response) => { if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } return response.json(); }) .then((json) => initialize(json)) .catch((err) => console.error(`Fetch problem: ${err.message}`)); تعيد الدالة ()fetch وعدًا، فإن أنجز هذا الوعد بنجاح، ستعاد الدالة الموجودة ضمن أول كتلة ()then تضم الاستجابة response من الشبكة، وما نفعله ضمن هذه الدالة هو: التحقق من عدم إرسال الخادم خطأً (مثل Not Found 404) وإن حدث ذلك، نرمي الخطأ. استدعاء التابع ()json للعمل على الاستجابة واستخلاص البيانات منها على شكل كائن JSON، ثم نعيد الوعد الذي يعيده response.json. نمرر دالة إلى التابع ()then المرتبط بالوعد المعاد، كما نمرر إلى هذه الدالة كائنًا يتضمن بيانات الاستجابة وفق تنسيق JSON بعد تمريرها إلى الدالة ()initialize التي تبدأ عملية عرض جميع المنتجات على واجهة المستخدم. ولمعالجة اﻷخطاء، نربط كتلة ()catch. في نهاية السلسلة، وستعمل شيفرة هذه الكتلة إذا وقع خطأ لسبب ما. نضع ضمن هذه الكتلة دالة يُمرر إليها الكائن err كمعامل ويُستخدم لتسجيل طبيعة الخطأ الذي حصل ونعرضه من خلال الدالة ()console.error. تتعامل المواقع المكتملة مع اﻷخطاء بطريقة شمولية أفضل، وذلك بعرض رسالة على شاشة المستخدم. كما قد تعرض أيضًا خيارات لحل المشكلة، لكننا لن نحتاج هنا إلا للتابع ()console.error. بإمكانك أيضًا اختبار حالات الفشل بنفسك: انسخ ملفات التمرين على حاسوبك. شغل الشيفرة باستخدام خادم ويب محلي. عدّل مسار الملف الذي نحضره مثل "produc.json" بدلًا من "product.json" (تأكد من ارتكاب خطأ كتابي). حمّل اﻵن الملف index.html في المتصفح (localhost:8000/index.html) ثم ألق نظرة على طرفية جافا سكريبت، وستجد رسالة خطأ مشابهة للرسالة "Fetch problem: HTTP error: 404". ستجد كتلة fetch الثانية ضمن الدالة ()fetchBlob: fetch(url) .then((response) => { if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } return response.blob(); }) .then((blob) => showProduct(blob, product)) .catch((err) => console.error(`Fetch problem: ${err.message}`)); تعمل هذه الشيفرة تمامًا كسابقتها ما عدا أننا استخدمنا التابع ()blob بدلًا من ()json لأننا نريد في هذه الحالة الحصول على ملف صورة في الاستجابة وسيكون حينها تنسيق البيانات على شكل كائن بيانات ثنائية Blob (اختصارًا للعبارة "كائن ضخم ثنائي Binry Large Object"). ويُستخدم هذا الكائن لتمثيل كائنات ضخمة مشابهة للملفات مثل الصور والفيديو. وبمجرد أن نحصل على الكائن blob، نمرره إلى الدالة ()showProduct التي تعرضه. الواجهة البرمجية XMLHttpRequest سترى في بعض اﻷحيان وخاصة في الشيفرة اﻷقدم واجهة برمجية أخرى تُدعى XMLHttpRequest (وتختصر أحيانًا إلى "XHR") تُستخدم في إجراء طلبات HTTP. وقد سبقت هذه الواجهة الواجهة البرمجية Fetch وكانت أولى الواجهات التي استخدمت على نطاق واسع لتنفيذ تقنية AJAX. لكن ننصحك باستخدام Fetch إن أمكن فهي واجهة أبسط وتضم ميزات أكثر من الواجهة XMLHttpRequest. لن نقدم مثالًا عن استخدام الواجهة XMLHttpRequest، لكننا سنعرض نسخة XMLHttpRequest من مثال متجر المعلبات. سيبدو الطلب كالتالي: const request = new XMLHttpRequest(); try { request.open("GET", "products.json"); request.responseType = "json"; request.addEventListener("load", () => initialize(request.response)); request.addEventListener("error", () => console.error("XHR error")); request.send(); } catch (error) { console.error(`XHR error ${request.status}`); } هناك خمس مراحل: إنشاء كائن XMLHttpRequest. جديد. استدعاء التابع ()open لتهيئة الكائن الجديد. إضافة مترصد للحدث load يُنفَّذ عند إكتمال الطلب بنجاح. ونستدعي الدالة ()initialize بعد تزويدها بالبيانات ضمن دالة مترصد الحدث. إضافة مترصد حدث إلى الحدث error الذي يقع عندما يواجهة الطلب خطأً. إرسال الطلب ولا بد من تغليف الشيفرة السابقة ضمن كتلة try...catch للتعامل مع الأخطاء التي قد تحدث عند استخدام التابعين ()open أو ()send. ومن الجيد أن تدرك أن الواجهة Fetch هي تطوير للواجهة XMLHttpRequest، وأن تفهم الطريقة المتبعة في التعامل مع اﻷخطاء في كلتا الواجهتين. الخلاصة شرحنا في هذا المقال طريقة استخدام الواجهة البرمجية Fetch في إحضار البيانات من الخادم، وسنتناول بعض المواضيع التي وردت في المقال بمزيد من التفصيل في مقالاتنا التالية. ترجمة-وبتصرف- للمقال: Fetching data from the server. اقرأ أيضًا المقال السابق: الواجهات البرمجية والتعامل مع شجرة DOM في جافا سكريبت إرسال البيانات واستلامها عبر الشبكة في جافاسكربت أساسيات بناء تطبيقات الويب التعامل مع طلبات HTTP في Node.js التخزين المؤقت Cache ومقابس الويب Webscockets في PHP
  9. بعد أن شرحنا كيفية التحقق من صحة البيانات المدخلة من طرف العميل، ستفكر بعدها في كيفية إرسال هذه البيانات إلى الخادم، وهذا ما نغطيه في هذا المقال. إذ سنناقش ما يحدث عندما ينقر المستخدم على زر اﻹرسال في استمارات أو نماذج الويب Web Forms، وأين تذهب البيانات التي نرسلها وما الذي نفعله بها عندما تصل وجهتها، كما نلقي نظرة على بعض الاعتبارات اﻷمنية المتعلقة بإرسال بيانات استمارة الويب إلى الخادم. ننصحك قبل المضي معنا في هذا المقال الاطلاع على أساسيات HTML وكذلك أساسيات بروتوكول HTTP وأن تلقي نظرة على أساسيات البرمجة في طرف الخادم. ما سنفعله تاليًا هو شرح ما يحدث للبيانات عندما تُرسل بيانات نموذج من العميل client (وهو عادة متصفح الويب) إلى الخادم server. معمارية عميل-خادم يعمل الويب وفق معمارية تسمى عميل-خادم لتبادل البيانات ويمكن تلخيصها على النحو التالي: يُرسل العميل طلبًا إلى الخادم (خادم ويب في أغلب اﻷحيان مثل Apache أو Nginx أو IIS و Tomcat وغيرها) باستخدام بروتوكول HTTP، ثم يجيب الخادم على الطلب باستخدام نفس البروتوكول كما توضح الصورة التالية" وهكذا ستكون الاستمارة في صفحة ويب طريقة ملائمة لتجميع البيانات التي يدخلها المستخدمون، وتهيئة طلب HTTP ﻹرسال تلك البيانات إلى الخادم. ملاحظة: لتطلع أكثر على آلية عمل معمارية عميل-خادم، اطلع على مقال أساسيات البرمجة في طرف الخادم. كيفية إرسال البيانات في طرف العميل يُعرّف العنصر <form> كيفية إرسال البيانات، وقد صممت جميع خاصياته كي تسمح لك بإعداد طلب اﻹرسال عندما ينقر المستخدم على زر اﻹرسال submit. وتُعد action و method أهم خاصيتين لتهيئة عملية إرسال الطلب وإليك شرحًا لكل منهما. الخاصية action تُعرِّف هذه الخاصية وجهة البيانات التي نرسلها، وتكون قيمتها عنوان URL مطلق أو نسبي صالح. وإن لم تخصص قيمة لهذه الخاصية ستُرسل الاستمارة إلى عنوان URL لصفحة الويب التي تضمها. تُرسل البيانات في المثال التالي إلى عنوان مطلق Absolute URL وهو https://example.com: <form action="https://example.com">…</form> بينا نستخدم في هذا المثال عنوان نسبي Relative URL ﻹرسال البيانات إلى عنوان URL آخر ضمن نفس النطاق، أي أنه يعتمد على موقع المورد الحالي. <form action="/somewhere_else">…</form> وفي حال لم نستخدم هذه الخاصية، ستُرسل بيانات الاستمارة إلى نفس الصفحة التي تضم الاستمارة أي لنفس عنوان URL الذي تم تحميل الاستمارة منه، ويمكن أن تُستخدم هذه الطريقة في بعض الحالات البسيطة مثل معالجة البيانات وعرض النتيجة على نفس الصفحة. <form>…</form> ملاحظة: من الممكن تخصيص عنوان URL يستخدم النسخة اﻵمنة من بروتوكول HTTP (بروتوكول HTTPS). وعندها تُشفَّر البيانات مع بقية أجزاء الطلب حتى لو كانت الاستمارة ضمن صفحة غير آمنة تستخدم بروتوكول HTTP. لكن، ومن ناحية أخرى، إن كانت الاستمارة ضمن صفحة آمنة وكان عنوان URL الذي خصصته للخاصية action غير آمن (يستخدم HTTP) سيعرض المتصفح رسالة تحذير في كل مرة يحول فيها المستخدم إرسال الاستمارة لأن البيانات لن تكون مشفَّرة. تُرسَل بيانات الاستمارة إن لم تكن ملفًا على شكل أزواج مكونة من اسم و قيمة name=value تربط بينها علامة &، ويجب أن تكون قيمة الخاصية action ملفًا على الخوادم التي تستطيع التعامل مع هذا النوع من الملفات وتتأكد من صلاحيتها. يستجيب الخادم بعد ذلك، وعادة ما يعالج البيانات ويحمّل عنوان URL الذي تحدده الخاصية action مسببًا إعادة تحميل الصفحة (أو تحديث الصفحة إن كانت موجودة، إن أشارت الخاصية action إلى نفس الصفحة). لكن كيف ترسل البيانات؟ يعتمد اﻷمر هنا على قيمة الخاصية method. الخاصية method تعرف هذه الخاصية كيفية إرسال البيانات. إذ يوفر بروتوكول HTTP عدة طرق لتنفيذ الطلبات، ويمكن للاستمارة أن تستخدمها في إرسال بياناتها إلى الوجهة المحددة، وأكثر هذه الطلبات شيوعًا GET و POST. ولفهم الاختلاف بين الطلبين السابقين، علينا معرفة آلية عمل بروتوكول HTTP. ففي كل مرة تحاول الحصول فيها على مورد من الويب، يرسل المتصفح طلبًا إلى عنوان URL. ويتكون هذا الطلب من قسمين: ترويسة الطلب header تضم مجموعة من البيانات الوصفية العامة حول إمكانيات المتصفح. جسم الطلب body الذي يضم البيانات الضرورية التي يحتاجها الخادم لمعالجة الطلب. الطلب GET يستخدم المتصفح هذا الطلب كي يطلب من الخادم موردًا محددًا: " مرحبا! أريد هذا المورد". فيُرسل المتصفح في هذه الحالة جسم طلب فارغ، وطالما أن الجسم فارغ ستُرسل بيانات الاستمارة إلى الخادم ملحقةً بعنوان URL. لنتأمل الحالة التالية: <form action="http://www.foo.com" method="GET"> <div> <label for="say">What greeting do you want to say?</label> <input name="say" id="say" value="Hi" /> </div> <div> <label for="to">Who do you want to say it to?</label> <input name="to" id="to" value="Mom" /> </div> <div> <button>Send my greetings</button> </div> </form> طالما أننا استخدمنا الطلب GET، سيكون عنوان URL كالتالي: www.foo.com/?say=Hi&to=Mom وسيظهر في شريط عنوان المتصفح بهذا الشكل عند النقر على زر اﻹرسال: لاحظ كيف ألحقت البيانات بعنوان URL على شكل أزواج اسم=قيمة تأتي بعد إشارة ? ويفصل بين كل زوج المحرف &. وقد مرنا في حالتنا البيانات التالية إلى الخادم: الاسم say قيمته Hi. الاسم to قيمته Mom. سيدو عنوان URL عندها بالشكل: GET /?say=Hi&to=Mom HTTP/2.0 Host: foo.com ملاحظة: لن تُلحق البيانات إن لم يكن عنوان URL المخصص قادرًا على التعامل مع الاستعلامات (أن يكون ملفًا مثلًا). الطلب POST يختلف هذا الطلب قليلًا عن GET، ويستخدمه المتصفح ليطلب من الخادم استجابة تتعلق بالبيانات التي يرسلها ضمن جسم طلب HTTP: " مرحبًا! ألق نظرة على هذه البيانات وأعد نتيجة مناسبة". تلحق الاستمارة البيانات بجسم طلب HTTP عند استخدام POST، ولكي نميز الفرق، ألق نظرة على نفس استمارة المثال السابق لكن باستخدام الطلب POST: <form action="http://www.foo.com" method="POST"> <div> <label for="say">What greeting do you want to say?</label> <input name="say" id="say" value="Hi" /> </div> <div> <label for="to">Who do you want to say it to?</label> <input name="to" id="to" value="Mom" /> </div> <div> <button>Send my greetings</button> </div> </form> لا تُلحق بيانات الاستمارة بعنوان URL في حال استخدمت الطلب POST بل ستكون ضمن جسم الطلب الذي سيبدو بالشكل: POST / HTTP/2.0 Host: foo.com Content-Type: application/x-www-form-urlencoded Content-Length: 13 say=Hi&to=Mom تشير الترويسة Content-Length إلى حجم جسم الطلب، والترويسة Content-Type إلى نوع المصدر الذي ترسله إلى الخادم، وسنناقش هاتين الترويستين لاحقًا. ملاحظة:سيُستخدم الطلب GET إن لم يكن عنوان URL المخصص قادرًا على التعامل مع جسم الطلب (كأن يكون على الشكل :data مثلًا). عرض طلبات HTTP لا تُعرض طلبات HTTP للمستخدم أبدًا، ولكن إن أردت رؤيتها ومعرفة تفاصيل الطلبات واستجابة الخادم عليك استخدام بعض اﻷدوات مثل أدوات مطوري ويب على كروم أو مراقب الشبكة على فايرفوكس. وكمثال على ذلك، بإمكانك استعراض بيانات المثال السابق ضمن نافذة شبكة كروم Chrome Network بعد إرسال الاستمارة كالتالي: افتح نافذة أدوات مطوري ويب developer tools. اختر "شبكة Network". اختر "الكل All". اختر الموقع الذي تريد وليكن "foo.com" في النافذة "اسم Name". اختر "ترويسات Headers". يمكنك عندها الحصول على بيانات النموذج Form Data كما في لقطة الشاشة التالية: ما عُرض على المستخدم هنا هو عنوان URL الذي استدعاه فقط. وكما قلنا سابقًا، سيرى المستخدم البيانات ملحقة بعنوان URL ضمن شريط عنوان المتصفح عندما يستخدم الطلب GET، لكنه لن يرى شيئًا إن كان الطلب هو POST، ولهذا اﻷمر أهميته لسببين: لا تستخدم GET على اﻹطلاق إن أردت إرسال كلمة مرور (أو غيرها من البيانات الحساسة)، لأن هذه البيانات ستُعرض ضمن العنوان وهذا غير آمن. يُفضّل استخدام POST إن احتجت إلى إرسال كمية كبيرة من البيانات. إذ تحدد بعض المتصفحات حجم عناوين URL، كما تحدد الخوادم أيضًا طول عناوين URL التي تقبلها. كيف تستخلص البيانات في طرف الخادم أيًا كان طلب HTTP الذي استخدمته، ستصل البيانات إلى الخادم على شكل نصي يُفسره لاستخلاص البيانات على شكل قائمة من الأزواج مفتاح/قيمة. أما طريقة التعامل مع هذه القائمة، فتعود إلى بيئة التطوير أو إطارات العمل التي تستخدمها. مثال على إرسال البيانات للخادم باستخدام لغة PHP صرفة تقدم PHP بعض الكائنات العامة للوصول إلى البيانات في طرف الخادم. فلو افترضنا أنك استخدمت الطلب POST كما في المثال التالي، سيعرض هذا المثال البيانات المرسلة للمستخدم فقط. وبالطبع، يعود اﻷمر إليك فيما تفعله بهذه البيانات، فقد تعرضها أو تخزنها في قاعدة بيانات أو ترسلها ضمن بريد إلكتروني وهكذا.. <?php // الوصول إلى البيانات المستلمة باسمها $_POST يتيح لك المتغير العام //$_GET نستخدم المتغير GET وللوصول إلى البيانات المرسلة باستخدام الطلب $say = htmlspecialchars($_POST['say']); $to = htmlspecialchars($_POST['to']); echo $say, ' ', $to; ?> يعرض هذا المثال صفحة ويب تضم البيانات التي أرسلناها، يضم ملف المثال نفس استمارة التمرين السابق وتكون قيمة الخاصية method هي POST وقيمة الخاصية action هي php-example.php. عند النقر على زر اﻹرسال، يُرسل المتصفح البيانات إلى الملف php-example.php الذي يضم الشيفرة التي عرضناها سابقًا، وستكون نتيجة تنفيذ هذه الشيفرة كالتالي: ملاحظة: لن يعمل المثال السابق إن حملته ضمن المتصفح مباشرة، فلا يمكن للمتصفحات تفسير لغة PHP. لهذا عندما ترسل الاستمارة، سيعرض المتصفح تحميل ملف PHP فقط. كي يعمل هذا الملف، لابد من تنفيذه على خادم يدعم PHP. ومن الخيارات المتاحة لاختبار PHP محليًا نقترح MAMP ( على نظام التشغيل ويندوز وماك أو إس) و AMPPS (على نظام ويندوز وماك و لينكس). وفي حال استخدمت MAMP ولم تكن قد ثبّته مسبقًا (أو كانت نسخة تجريبية انتهت صلاحيتها) فقد تواجهك بعض المشكلات عند تشغيل الملف. ولكي يعمل مجددًا، حمل التطبيق واختر NAMP>Preferences>PHP ثم اضبط قيمة Standard Version على*.7.2 وستختلف قيمة * وفقًا للنسخة التي ثبّتها. مثال على إرسال البيانات للخادم باستخدام لغة بايثون سنعرض في هذه الفقرة نفس المثال السابق لكن باستخدام لغة بايثون وإطار العمل فلاسك Flask لتصيير القوالب والتعامل مع بيانات الاستمارة. بإمكانك الاطلاع على ملف المثال على جيت-هاب: from flask import Flask, render_template, request app = Flask(__name__) @app.route('/', methods=['GET', 'POST']) def form(): return render_template('form.html') @app.route('/hello', methods=['GET', 'POST']) def hello(): return render_template('greeting.html', say=request.form['say'], to=request.form['to']) if __name__ == "__main__": app.run() يتعامل المثال السابق مع قالبين ينبغي أن يكونا ضمن مجلد فرعي باسمtemplates في نفس المجلد الذي يضم الملف python-example.py إن أردت تشغيل المثال بنفسك: القالب form.html: وهو ملف يضم نفس الاستمارة التي تعاملنا معها في المثال السابق، لكن ضبطت قيمة الخاصية action لتكون {{('helo')url_for}}. يُدعى هذا القالب قالب Jinja وهو أساسًا ملف HTML لكن يمكن أن يضم استدعاءات لشيفرة بايثون التي تعمل على الخادم بوضعها ضمن أقواس معقوصة. وتعني الشيفرة url_for('hello') : انتقل إلى الوجهة hello/ عند إرسال الاستمارة. القالب greeting.html: ويضم فقط شيفرة لتصيير البيانات التي نمررها إليه عندما تكون جاهزة. وينفذ اﻷمر باستخدام الدالة ()hello التي تُنفذ عند الانتقال إلى الوجهة hello/. ملاحظة: لن يعمل المثال السابق إن حملته ضمن المتصفح مباشرة، إذ تعمل لغة بايثون بشكل مختلف قليلًا عن PHP. لتنفيذ هذا المثال على متصفحك عليك تثبيت وإعداد Python ، ثم تثبيت Flask باستخدام اﻷمر pip3 install flask. وبعد الانتهاء من المفترض أن تتمكن من تشغيل المثال باستخدام python3 python-example.py. بعد ذلك اكتب العنوان التالي في متصفحك localhost:5042. لغات وإطارات عمل أخرى ستجد العديد من التقنيات المتوفرة للتعامل مع الاستمارات في طرف الخادم بما في ذلك بيرل Perl وجافا Java وروبي Ruby ودوت نت Net وغيرها. ومن الجدير بالذكر أن استخدام هذه التقنيات مباشرة ليس شائعًا لأن التعامل معها قد يكون مربكًا. لهذا من اﻷفضل العمل مع إطارات عمل عالية الجودة تُسهّل التعامل مع الاستمارات مثل: إطارات عمل بايثون: جانغو Django فلاسك Flask web2py (سهلة للمبتدئين). py4web (طوّرت من قبل نفس مطوري wep2py لكنها تشبه دجانغو قليلًا من ناحية اﻹعداد). إطارت عمل Node.js EXpress Next.js (لتطبيقات رياكت). Nuxt (لتطبيقات Vue). Remix إطارات PHP لارافيل Laravel سمفوني Symfony إطارات Ruby Ruby On Rails إطارات جافا Spring Boot وتجدر اﻹشارة إلى أن العمل مع الاستمارات لن يكون سهلًا بالضرورة عند استخدام أطر العمل، لكنه أسهل بكثير مقارنة مع كتابة جميع الوظائف الضرورية من الصفر، وتوفر عليك الوقت والجهد بالتأكيد. لن تتعلم بالطبع من أمثلة هذا المقال كل ما تحتاجه للعمل مع لغات البرمجة من طرف الخادم فهو غير مخصص لهذا اﻷمر، لكنك ستجد الكثير من المقالات المفيدة التي تساعدك على الانطلاق ضمن أكاديمية حسوب وموسوعة حسوب، إضافة إلى دورات البرمجة التي تبدأ بك من الصفر حتى الاحتراف وإعداد معرض اﻷعمال. حالة خاصة: إرسال ملف تُمثَّل البيانات في الملف على شكل بيانات ثنائية -أو تُعد كذلك- بينما تكون بقية أنواع البيانات المرسلة نصية. وطالما أن HTTP هو بروتوكول نصي، هناك متطلبات خاصة للتعامل مع البيانات الثنائية. الخاصية enctype تسمح لك هذه الخاصية بتحدبد قيمة الترويسة Content_Type في طلب HTTP الذي ينتج عن تسليم الاستمارة. وهذه الترويسة مهمة جدًا لأنها تخبر الخادم عن نوع البيانات التي أرسلت. تأخذ الخاصية افتراضيًا القيمة التالية application/x-www-form-urlencoded والتي تعني: "هذه بيانات استمارة شُفرت ضمن عنوان URL". لكن إن أردت إرسال ملف، عليك تنفيذ الخطوات اﻹضافية التالية: اضبط قيمة الخاصية method على POST لأن محتوى الملف لا يمكن وضعه ضمن عنوان URL. اضبط قيمة الخاصية enctype على multipart/form-data لأن البيانات ستُجزّأ إلى أقسام متعددة : قسم لكل ملف وقسم للبيانات النصية التي يضمها جسم الطلب (في حال وجدت بيانات نصية أخرى في الاستمارة). استخدام عنصر <'input type='file> أو أكثر لتسمح للمستخدم باختيار الملف أو الملفات التي يريد رفعها. إليك مثالًا: <form method="post" action="https://www.foo.com" enctype="multipart/form-data"> <div> <label for="file">Choose a file</label> <input type="file" id="file" name="myFile" /> </div> <div> <button>Send the file</button> </div> </form> ملاحظة: قد تُهيّأ الخوادم لتقبل حجمًا محدودًا للملفات أو طلبات HTTP لمنع الاستخدام غير الصحيح لمواردها. اعتبارات أمنية عليك الانتباه إلى أية اعتبارات أمنية قبل إرسال البيانات إلى الخادم، لأن استمارات HTML حتى اللحظة هي أكثر النقاط التي تهاجم الخوادم منها. وليست المشكلة في الاستمارة نفسها، بل بكيفية تعامل الخادم مع بيانات الاستمارة. ننصحك بالاطلاع على مقال "تعرف على أمان مواقع الويب" الذي يناقش أكثر الهجمات شيوعًا والطرق الممكنة لصدها، لكي تكوّن تصورًا واضحًا عما يمكن أن يحدث. كن مرتابًا ولا تثق أبدًا بالمستخدم إن القاعدة الأهم والتي عليك تذكرها دائمًا هي: لا تثق أبدًا بالمستخدمين بمن فيهم أنت، فقد يتعرض أي مستخدم حتى الموثوقون إلى هجمات سرقة البيانات. لهذا يجب أن تتحقق دائمًا من كل البيانات التي تصل إلى الخادم وتنقيها دون أي استثناء. تجاوز escape أي محرف قد يشكل خطرًا: تختلف المحارف التي يجب الانتباه إليها حسب سياق البيانات المدخلة ومنصة عمل الخادم. وتمتلك جميع لغات البرمجة التي تعمل من طرف الخادم دوال للتعامل مع هذه اﻷمور. أما اﻷشياء التي عليك مراقبتها فهي أي تسلسل لمحارف تبدو وكأنها شيفرة تنفيذية (مثل جافا سكريبت أو أوامر SQL). حدد كميات البيانات التي يمكن استقبالها: حتى تسمح بإرسال البيانات الضرورية فقط. احتجز الملفات المرفوعة إلى الخادم: خزّن هذه الملفات على خادم مختلف واسمح بالوصول إليها عبر نطاقات فرعية أو اﻷفضل عبر نطاقات مختلفة تمامًا عن نطاق موقعك. ستتجنب الكثير من المشاكل المتعلقة باﻷمان وحتى معظمها إن اتبعت الخطوات السابقة. مع ذلك، من اﻷفضل أن تنفذ اختبارًا امنيًا بمساعدة طرف خارجي، فلا تفترض أبدًا أنك حددت جميع المشاكل المحتملة. الخلاصة إن إرسال البيانات للخادم أمر سهل نسبيًا، لكن تأمين التطبيق من خطر هذه البيانات أمر مربك، وتذكر دائمًا أن مطور واجهة المستخدم ليس الشخص المسؤول عن وضع نموذج لتقييم سلامة البيانات. وعلى الرغم من إمكانية إجراء تقييم لصلاحية البيانات في طرف العميل، لكن لا يمكن للخادم الوثوق بهذا التقييم، لأنه لا يعرف إطلاقًا ما يحدث حقيقية في طرف العميل. وهكذا نكون قد وصلنا إلى نهاية هذه السلسلة التي تحدثنا فيها عن استمارات الويب وطرق إنشائها وتنسيقها والتحقق من صلاحية بياناتها ثم إرسال البيانات إلى الخادم. ترجمة -وبتصرف- للمقال: Sending form data اقرأ أيضًا المقال السابق: التحقق من صحة بيانات استمارة ويب في طرف العميل أمثلة على التعامل مع خادم عميل (Client server) باستعمال لغة Cpp إرسال الاستمارات (form submit) ومعالجتها في جافاسكربت نظرة على تفاعلات الخادم مع العميل في موقع ويب ديناميكي إنشاء تطبيق ويب باستخدام إطار عمل فلاسك Flask من لغة بايثون
  10. من الضروري التحقق من ملء بيانات جميع عناصر تحكم استمارة ويب وفق التنسيق الصحيح قبل إرسالها إلى الخادم، وتُدعى هذه العملية التحقق من الاستمارة في طرف العميل client-side form validation، وهي تساعد على ضمان توافق البيانات مع المتطلبات المخصصة لكل عنصر تحكم من عناصر الاستمارة قبل إرسالها للخادم. لهذا سيقودك هذا المقال عبر المفاهيم اﻷساسية التي تحتاجها للتحقق من البيانات في طرف العميل. ننصحك قبل المتابعة في قراءة هذا المقال الاطلاع على أساسيات HTML وعلى أساسيات تنسيق الصفحات باستخدام CSS وكذلك أساسيات لغة جافا سكريبت. إن التحقق من صحة البيانات التي يدخلها المستخدم أثناء كتابتها في المتصفح ميزة هامة، وتقدم للمستخدم تجربة أفضل، فهي تنبهه إلى اﻷخطاء التي ارتكبها كي يصححها مباشرة. فإرسال بيانات خاطئة إلى الخادم ستؤدي إلى رفضها إضافة إلى التأخير الذي يحدث نتيجة إرسال البيانات ومن ثم الانتظار حتى يتحقق منها الخادم ويعيدها طالبًا منك تصحيحها. مع ذلك لا يمكن أن نعتمد كليًا على التحقق من طرف العميل فقط وذلك لأسباب أمنية، فمن السهل جدًا تجاوز التحقق من طرف العميل من قبل مستخدمين مشبوهين وإرسال بيانات غير صحيحة أو ضارة إلى الخادم. لهذا يجب التحقق من صحة البيانات في كلا الطرفين. وعلى الرغم من أننا لن نتحدث عن حاليًا عن التحقق من البيانات في طرف الخادم، لكن عليك تذكر ذلك جيدًا. ما الذي نعنيه بالتحقق من الاستمارة؟ جرّب أن تزور أي موقع ويب مشهور يعرض استمارة تسجيل وستلاحظ أنه يقدم توصيات بتصحيح البيانات المدخلة إن لم تكتبها وفق الصيغة المطلوبة الذي تتوقعه الاستماره. فقد تتلقى رسائل مثل "هذا الحقل مطلوب": أي لا يمكنك ترك هذا الحقل فارغًا. "الرجاء إدخال رقم الهاتف وفق التنسيق xxx-xxxx": أي ينبغي إدخال البيانات بتنسيق معين حتى تُعد صالحة كرقم هاتف. "الرجاء إدخال عنوان بريد إلكتروني صالح": أي لم تدخل البريد الإلكتروني وفق الصيغة المطلوبة. "كلمة السر يجب أن تكون بين 8 إلى 30 محرفًا، وتضم على اﻷقل حرفًا كبيرًا ورمزًا ورقمًا": أي المطلوب إدخال بيانات وفق تنسيق مخصص ومحدد تمامًا. يُدعى ذلك التحقق من الاستمارة form validation. فعندما تُدخل البيانات إلى الاستمارة يتحقق المتصفح (والخادم) من أن هذه البيانات وفق الصيغة الصالحة أو الحدود التي يفرضها التطبيق. وعندما يتحقق المتصفح من البيانات ندعوه التحقق من طرف العميل client-side validation، بينما ندعوه تحقق من طرف الخادم server-side validation عندما ينفذ الخادم عملية التحقق، وسنقتصر في مقالنا كما وضحنا سابقًا شرح آلية التحقق من طرف العميل. إن كانت البيانات المدخلة إلى الاستمارة مطابقة للصيغة أو الحدود المطلوبة، ستُسلَّم الاستمارة إلى الخادم وقد تحفظ البيانات في قاعدة بيانات، بينما إن لم تكن البينات مطابقة لما هو مطلوب، فلن تّرسل إلى الخادم ويعرض المتصفح رسالة خطأ تشير إلى ما يجب عليك تصحيحه ويتيح لك المحاولة مجددًا. وطالما أننا نريد من عملية ملء الاستمارة أن تكون سهلة إلى أبعد الحدود، قد تتساءل لماذا نربك أنفسنا بالتحقق من كل البيانات التي نُدخلها؟ إليك ثلاثة أسباب أساسية: الحاجة إلى بيانات صحيحة وفق الصيغة الصحيحة. فلن يعمل التطبيق بالشكل المطلوب إن خُزّنت بيانات المستخدم بصيغة خاطئة أو كانت غير صالحة أو لم تكن موجودة أصلًا. الحاجة إلى حماية بيانات المستخدم. فإجبار المستخدم على إدخال كلمة سر قوية وفق صيغة محددة تُسهِّل حماية بيانات حسابه. الحاجة إلى حماية الموقع. فهناك طرق كثيرة يمكن فيها للمخترقين من إساءة استعمال الاستمارات غير المحمية لتخريب أمان مواقع الويب. تحذير: لا تثق أبدًا بالبيانات التي يرسلها المستخدم إلى الخادم. وحتى لو تحققت الاستمارة من صحة البيانات المدخلة ومنعت المستخدم من إرسال بيانات غير واضحة، يمكن للمخترقين تغيير طلبات الشبكة التي تصل المتصفح بالخادم. اﻷنواع المختلفة للتحقق في طرف العميل هناك نوعان مختلفان للتحقق في طرف العميل ستواجههما في الويب: التحقق المدمج built-in validation: يستخدم وسيلة التحقق التي تقدمها بعض عناصر HTML، وقد رأينا العديد منها سابقًا مثل حقل إدخال البريد اﻹلكتروني. إذ لا يتطلب هذا النوع من التحقق الكثير من شيفرات جافا سكريبت، وله أداء أفضل من التحقق باستخدام جافا سكريبت لكن الأسلوب الثاني أكثر قابلية للتخصيص. التحقق باستخدام جافا سكريبت JavaScript validation: يستخدم شيفرة جافا سكريبت للتحقق من بيانات الاستمارة، وهو أسلوب قابل للتخصيص بالكامل، لكن عليك كتابة جميع تفاصيل العملية بنفسك (أو استخدام مكتبات جاهزة بالطبع). استخدام التحقق المدمج من بيانات الاستمارة من أبرز ميزات عناصر التحكم الحديثة في الاستمارات هي قدرتها على التحقق من صلاحية البيانات المدخلة إليها دون الاعتماد على جافا سكريبت. يتحقق العنصر من البيانات باستخدام خاصيات التحقق لعناصر تحكم النموذج، وقد رأينا سابقًا العديد منها، سنحاول مراجعتها تاليًا: required: تحدد هذه الخاصية إذا ما كان على المستخدم ملء عنصر التحكم قبل إرسال البيانات أم لا. minlength و maxlength: تحددان الطول اﻷدنى واﻷعلى للبيانات النصية المدخلة. min و max: تحددان الحد اﻷدنى واﻷعلى لقيم مدخلات عددية. type: تحدد نوع عنصر اﻹدخال سواء نص أو بريد إلكتروني أو قيمة عددية وهكذا. pattern: تقبل تعبير نمطي regular expression يعرف نمط أو صيغة المدخلات التي يجب اتباعها عند إدخال البيانات إلى عنصر التحكم. فإن خضعت البيانات المدخلة إلى عنصر تحكم لجميع القواعد التي تحددها تلك الخاصيات، ستُعد بيانات صالحة وإلا تًعد العكس. وعندما تكون قيمة أي مُدخل صحيحة، ستكون النقاط التالية محققة: يمكن أن تستهدف العنصر عبر صنف التنسيق الزائف valid: لتطبيق تنسيق CSS محدد عليه. إن حاول المستخدم إرسال البيانات، فسيعمل المتصفح على تسليمها إن لم يكن هناك أمر آخر يمنع ذلك (مثل شيفرة جافا سكريبت). وعندما تكون البيانات المدخلة غير صالحة، فالنقاط التالية محققة: يمكن أن تستهدف العنصر من خلال الصنف الزائف invalid: وأصناف أخرى أحيانًا مثل out-of-range: وفقًا للخطأ في البيانات المدخلة، مما يسمح بتطبيق تنسيق مخصص على العنصر. سيمنع المتصفح المستخدم من تسليم الاستمارة إن حاول ذلك، ويعرض رسالة خطأ. ملاحظة: هناك الكير من اﻷخطاء التي تمنع تسليم الاستمارة مثل badInput و patternMismatch و rangeOverflow أو rangeUnderflow و stepMismatch و tooLong أو tooShort و typeMismatch و valueMissing أو customError. أمثلة عن استخدام التحقق المدمج من صحة بيانات عنصر التحكم نناقش في هذا القسم استخدام بعض الخاصيات التي ذكرناها سابقًا. ملف بسيط للبدء لنبدأ أمثلتنا بمثال بسيط يضم عنصر إدخال تختار فيه الموز أو الكرز. يستخدم الملف عنصر إدخال نصي <input> مع عنوان مرافق <label> وزر إرسال <button>. بإمكانك إيجاد الشيفرة المصدرية للمثال على جيت-هاب: <form> <label for="choose">Would you prefer a banana or cherry?</label> <input id="choose" name="i-like" /> <button>Submit</button> </form> وإليك شيفرة تنسيق CSS: input:invalid { border: 2px dashed red; } input:valid { border: 2px solid black; } See the Pen fruit-start by Hsoub Academy (@HsoubAcademy) on CodePen. وحتى تتابع معنا، انسخ ملف المثال على جهازك وضعه في مجلد جديد. الخاصيات المطلوبة تُعد الخاصية required من أبسط طرق التحقق في HTML. ولكي تجعل إدخال بيانات ضمن عنصر تحكمًا إجباريًا، أضف هذه الخاصية إلى العنصر، وعندها يمكنك استهداف العنصر باستخدام الصنف required:، ولن تُرسل الاستمارة وسيعيد المتصفح رسالة خطأ عندما تحاول تسليم الاستمارة وهذا العنصر فارغ. وسيعدُّ المتصفح العنصر غير صالح طالما أنه فارغ ويمكن استهدافه حينها بالصنف invalid:: <form> <label for="choose">Would you prefer a banana or cherry? (required)</label> <input id="choose" name="i-like" required /> <button>Submit</button> </form> ستكون شيفرة التنسيق مضمّنه في ملف HTML: input:invalid { border: 2px dashed red; } input:invalid:required { background-image: linear-gradient(to right, pink, lightgreen); } input:valid { border: 2px solid black; } يسبب التنسيق السابق ظهور حواف متقطعة باللون الأحمر عندما تكون القيمة المدخلة غير صالحة، وحواف متصلة باللون الأسود عندما تكون صحيحة. كما أضفنا خلفية لونية متدرجة عندما يكون عنصر اﻹدخال مطلوبًا وقيمته غير صالحة. جرّب المثال مباشرة في المحرر التفاعلي التالي: See the Pen Untitled by Hsoub Academy (@HsoubAcademy) on CodePen. جرب النقر على زر اﻹرسال عندما يكون المربع النصي فارغًا، سيكتسب عندها عنصر اﻹدخال تركيز الدخل وتظهر لك رسالة الخطأ التالية: "Please fill out this field" ولن تُسلًم بيانات الاستمارة. إن وجود الخاصية required ضمن أي عنصر يدعم هذه الخاصية، يعني إمكانية استهداف العنصر من قبل الصنف required: سواء كان العنصر فارغًا أو لا. بينما إن كان العنصر فارغًا، سيستهدفه أيضًا الصنف invalid:. ملاحظة: لتقديم تجربة مستخدم جيدة، بيّن للمستخدم الحقول المطلوبة. هذا اﻷمر مطلوب أيضًا وفق توجيهات WCAG لسهولة الوصول. وانتبه دائمًا لعدم إجبار المستخدم على إدخال بيانات غير ضرورية، فلا معنى أن تجبره على إدخال جنسه أو لقبه. تقييم صلاحية مدخلات باستخدام التعابير النمطية regex من الخاصيات المفيدة أيضًا نجد pattern التي تتوقع تعبيرًا نمطيًا regex قيمةً لها. والتعبير النمطي هو سلسلة من الرموز التي تستخدم ﻹيجاد تشكيلة محددة من المحارف ضمن سلسلة نصية، فهو مثالي جدًا للتحقق من المدخلات المختلفة، وله استخدامات كثيرة في جافا سكريبت. و التعابير النمطية هي بالفعل نماذج معقدة، ولا ننوي أن نشرحها في المقال، لكن إليك بعض اﻷمثلة البسيطة التي توضح مفهومها: a: تعبير نمطي يطابق الحرف a (وليس b أو aa وهكذا). abc: يطابق سلسلة من اﻷحرف تبدأ ب a يليه b يليه c. ab?c: يطابق سلسلة من اﻷحرف تبدأ ب a وقد يليه b فقط ثم c (يطابق "ac" و "abc" وليس "aac"). ab*c: يطابق سلسلة من اﻷحرف تبدأ ب a وقد يليه عدة محارف b فقط ثم c (يطابق "ac" و "abc" و "abbbc"). a|b: يبحث عن أحد المحرفين a أو b. abc|xyz: يطابق abc تمامًا أو xyz تمامًا. بالطبع هناك الكثير والكثير من اﻷنماط التي لم نذكرها، وللاطلاع على هذه اﻷنماط بأكملها وطريقة استخدامها ننصح بمطالعة مقال مقدمة في التعابير النمطية Regular Expressions. لنحاول تحديث المثال السابق من خلال إضافة الخاصية pattern واستخدام تعبير نمطي مناسب: <form> <label for="choose">Would you prefer a banana or a cherry?</label> <input id="choose" name="i-like" required pattern="[Bb]anana|[Cc]herry" /> <button>Submit</button> </form> إليك نتيجة العمل: See the Pen fruit-pattern by Hsoub Academy (@HsoubAcademy) on CodePen. يقبل عنصر اﻹدخال في هذه النسخة من المثال أربع احتمالات هي: "banana" أو "Banana" أو "cherry" أو "Cherry". ولأن التعبير النمطي حساس لحالة اﻷحرف، فقد جعلناه يدعم الكلمة التي تبدأ بحرف كبير أو صغير، من خلال وضع الخيارين ضمن قوسين مربعين [Bb]. حاول تغيير التعبير النمطي ضمن الخاصية pattern ليماثل بعض القواعد التي أشرنا إليها سابقًا وراقب تأثيرها على القيمة التي تُدخلها ضمن عنصر اﻹدخال. إن لم تتطابق القيمة المدخلة مع تسلسل التعبير النمطي في الخاصية pattern يتطابق عندها عنصر اﻹدخال والصنف invalid:. ملاحظة: لا تحتاج بعض عناصر اﻹدخال إلى الخاصية pattern حتى تُقيّم مُدخلاتها بالاعتماد على التعابير النمطية مثل البريد اﻹلكتروني email، إذ تُقيَّم المدخلات بالمقارنة مع نمط خاص بالبريد اﻹلكتروني، مع أو بدون فواصل في حال استُخدمت الخاصية multiple. ملاحظة: لا يدعم العنصر <textarea> الخاصية pattern. تحديد طول المدخلات بإمكانك تحديد عدد المحارف في حقل إدخال نصي من خلال الخاصيتين minlength و maxlength. وسيكون المُدخل غير صالح إن كان عدد المحارف أدنى من minlength أو أكثر من maxlength. لا تسمح المتصفحات للمستخدم عادة بكتابة عدد محارف أكثر من الحد الأعلى المسموح. ولتزيد من جودة تجربة المستخدم يمكنك -إضافة إلى استخدام الخاصية maxlength- إظهار عدّاد للمحارف المتبقية التي يمكن للمستخدم إدخالها، مثل تلك العدادات التي تظهر عندما تحاول كتابة منشور على وسائط التواصل الاجتماعي. وهنا يأتي دور جافا سكريبت مع الخاصية maxlength لتنفيذ اﻷمر. تحديد مجالات القيم للمدخلات تُستخدم الخاصيتان min و max لتحديد أعلى وأدنى قيمة يمكن إدخالها ضمن عنصر إدخال عددي مثل <"input type= "number>، وستكون القيمة خاطئة إن خرجت عن هذا المجال. لنلق نظرة على مثال آخر، ولنبدأ بإنشاء نسخة جديدة عن ملف مثال الفواكه السابق، ثم حذف محتوى العنصر <body> واستبداله بالشيفرة التالية: <form> <div> <label for="choose">Would you prefer a banana or a cherry?</label> <input type="text" id="choose" name="i-like" required minlength="6" maxlength="6" /> </div> <div> <label for="number">How many would you like?</label> <input type="number" id="number" name="amount" value="1" min="1" max="10" /> </div> <div> <button>Submit</button> </div> </form> حددنا عدد المحارف اﻷعلى واﻷدنى في حقل إدخال النصي ليكون 6 وهو نفس عدد الحروف في الكلمتين banana و cherry. وحددنا قيمة عنصر اﻹدخال العددي ليكون الحد اﻷدنى min هو 1 والحد اﻷعلى max هو 10، وسيكون أي عدد خارج هذا المجال غير صالح ولن يتمكن المستخدم من استخدام زر الزيادة واﻹنقاص خارجه. وإن أدخل المستخدم عدد خارج هذا المجال يدويًا سيكون غير صالح أيضًا. وطالما أن هذا العنصر غير مطلوب، فإزالة هذه القيمة ستُبقي قيمة العنصر صالحة! إليك نتيجة المثال: See the Pen fruit-length by Hsoub Academy (@HsoubAcademy) on CodePen. ملاحظة: يأخذ العنصر <"input type="number> وغيره من العناصر مثل (range و date) الخاصية step التي تحدد مقدار الزيادة واﻹنقاص عند استخدام زر التحكم المخصص. لكن في مثالنا السابق لم نستخدم هذه الخاصية وستكون القيمة الافتراضية لها هي 1 وبالتالي لن تكون القيم ذات الفاصلة صالحة مثل 3.4. المثال كاملًا إليك المثال الكامل الذي يعرض استخدام ميزات التحقق المضمنة مع عناصر HTML: شيفرة HTML: <form> <fieldset> <legend> Do you have a driver's license?<span aria-label="required">*</span> </legend> <!-- While only one radio button in a same-named group can be selected at a time, and therefore only one radio button in a same-named group having the "required" attribute suffices in making a selection a requirement --> <input type="radio" required name="driver" id="r1" value="yes" /><label for="r1" >Yes</label > <input type="radio" required name="driver" id="r2" value="no" /><label for="r2" >No</label > </fieldset> <p> <label for="n1">How old are you?</label> <!-- The pattern attribute can act as a fallback for browsers which don't implement the number input type but support the pattern attribute. Please note that browsers that support the pattern attribute will make it fail silently when used with a number field. Its usage here acts only as a fallback --> <input type="number" min="12" max="120" step="1" id="n1" name="age" pattern="\d+" /> </p> <p> <label for="t1" >What's your favorite fruit?<span aria-label="required">*</span></label > <input type="text" id="t1" name="fruit" list="l1" required pattern="[Bb]anana|[Cc]herry|[Aa]pple|[Ss]trawberry|[Ll]emon|[Oo]range" /> <datalist id="l1"> <option>Banana</option> <option>Cherry</option> <option>Apple</option> <option>Strawberry</option> <option>Lemon</option> <option>Orange</option> </datalist> </p> <p> <label for="t2">What's your email address?</label> <input type="email" id="t2" name="email" /> </p> <p> <label for="t3">Leave a short message</label> <textarea id="t3" name="msg" maxlength="140" rows="5"></textarea> </p> <p> <button>Submit</button> </p> </form> شيفرة CSS: form { font: 1em sans-serif; max-width: 320px; } p > label { display: block; } input[type="text"], input[type="email"], input[type="number"], textarea, fieldset { width: 100%; border: 1px solid #333; box-sizing: border-box; } input:invalid { box-shadow: 0 0 5px 1px red; } input:focus:invalid { box-shadow: none; } نتيجة عرض الشيفرة السابقة: See the Pen Validation-related attributes by Hsoub Academy (@HsoubAcademy) on CodePen. التحقق من الاستمارة باستخدام جافا سكريبت لا بد من استخدام جافا سكريبت إن أردت التحكم الكامل بمظهر وشكل رسائل الخطأ، وهذا ما نناقشه في هذا القسم من المقال. الواجهة البرمجية لتحديد صلاحية المُدخلات تتكون هذه الواجهة البرمجية من مجموعة من التوابع والخاصيات تقدمها واجهات عناصر DOM التالية: HTMLButtonElement: يمثل العنصر <button> HTMLFieldSetElement: يمثل العنصر <fieldset> HTMLInputElement: يمثل العنصر <Input>. HTMLOutputElement: يمثل العنصر <output>. HTMLSelectElement: يمثل العنصر <select>. HTMLTextAreaElement: يمثل العنصر <textarea>. وتضم الواجهة البرمجية الخاصيات التالية: validationMessage: تعيد رسالة مخصصة تشرح حدود صلاحية قيمة عنصر التحكم في حال لم تكن صالحة. لكن إن لم يكن العنصر قابلًا لتحديد الصلاحية (أي قيمة الخاصية willValidate هي false) أو كانت قيمة العنصر صالحة، ستعيد الخاصية قيمة نية فارغة. validity: تعيد كائن validityState يضم عدة خاصيات تصف حالة صلاحية العنصر. إليك أبرز هذه الخاصيات: patternMismatch: تعيد القيمة true إن لم تطابق القيمة النمط الذي تحدده قيمة الخاصية pattern، و false إن لم يوجد تطابق. وفي حال أعادت القيمة true يمكن استهداف العنصر عبر الصنف invalid:. tooLong: تعيد الخاصية القيمة true إن كانت القيمة المدخلة أطول من القيمة التي تحددها الخاصية maxlength و false إن كانت أقصر منها أو تساويها. وفي حال أعادت القيمة true يمكن استهداف العنصر عبر الصنف invalid:. tooShort: تعيد الخاصية القيمة true إن كانت القيمة المدخلة أقصر من القيمة التي تحددها الخاصية minlength و false إن كانت أقصر منها أو تساويها. وفي حال أعادت القيمة true يمكن استهداف العنصر عبر الصنف invalid:. rangeOverFlow: تعيد الخاصية القيمة true إن كانت القيمة المدخلة أكبر من القيمة التي تحددها الخاصية max و false إن كانت أقل منها أو تساويها. وفي حال أعادت القيمة true يمكن استهداف العنصر عبر الصنف invalid: و out-of-range:. rangeUnderFlow: تعيد الخاصية القيمة true إن كانت القيمة المدخلة أكبر من القيمة التي تحددها الخاصية min و false إن كانت أكبر منها أو تساويها. وفي حال أعادت القيمة true يمكن استهداف العنصر عبر الصنف invalid:. typeMismatch: تعيد الخاصية القيمة true إن لم تحقق قيمة الخاصية الصياغة المطلوبة (عندما يكون نوع عنصر اﻹدخال email أو url) و false إن كانت الصياغة صحيحة. وفي حال أعادت القيمة true يمكن استهداف العنصر عبر الصنف invalid:. valid: تعيد الخاصية القيمة true إن لم تحقق قيمة الخاصية جميع حدود الصلاحية المتعلقة بها، وتُعد حينها صالحة، و false إن لم تحقق أحد القيود المفروضة عليها. وفي حال أعادت القيمة true يمكن استهداف العنصر عبر الصنف valid: وإلا يمكن استهدافها بالصنف invalid:. valueMissing: تعيد الخاصية القيمة true إن ظهرت الخاصية required في العنصر وكان فارغًا وإلا أعادت false. وفي حال أعادت القيمة true يمكن استهداف العنصر عبر الصنف invalid:. willValidate: تعيد الخاصية القيمة true في حال وجب التحقق من صلاحية القيمة المُدخلة عند إرسال الاستمارة، وإلا أعادت القيمة false. كما تزودنا الواجهة البرمجية بتوابع تُطبق على العناصر السابقة وعلى عناصر الاستمارة: ()checkValidity: يعيد القيمة true إن لم تكن هناك أية مشاكل في التحقق من صلاحية قيمة، وإلا تعيد false. وإن كانت القيمة غير صالحة، سيقع الحدث invaild على العنصر. reportValidity(): يبلغ عن الحقول غير الصالحة باستخدام اﻷحداث. ولهذا التابع أهميته عندما يجتمع مع التابع ()preventDefault ضمن معالج الحدث onSubmit. (setCustomValidity(message: يضيف رسالة خطأ مخصصة إلى العنصر. وعندما تخصص رسالة خطأ، سيُعد العنصر غير صالح، وستُعرض رسالة الخطأ. يسمح هذا التابع باستخدام جافا سكريبت لتهيئة حالة إخفاق عملية التحقق بشكل مختلف عن الطريقة المعيارية التي تتبعها HTML في تقييم القيمة. تُعرض هذه الرسالة للمستخدم عند اﻹبلاغ عن المشكلة. تنفيذ رسالة خطأ مخصصة يعرض المتصفح رسالة خطأ في كل مرة يحاول فيها المستخدم إرسال قيمة غير صالحة وفقًا لمعايير تحقق HTML، وتختلف طريقة عرض هذه الرسالة على المتصفح. ولهذا اﻵلية سلبيتان أساسيتان: لا توجد طريقة معيارية متبعة لتغيير شكل وطبيعة الرسالة باستخدام CSS. تعتمد على اﻹعدادات المحلية للمتصفح، فقد تكون لغة الصفحة مختلفة عن اللغة الافتراضية للمتصفح كما في لقطة الشاشة التالية: وتخصيص هذا النوع من الرسائل هو الاستخدام اﻷكثر شيوعًا للواجهة البرمجية الخاصة بالتحقق من صلاحية المدخلات، لهذا سنعمل على مثال يصف آلية عملها. سنبدأ بملف HTML بسيط نضع ضمنه الشيفرة التالية: <form> <label for="mail"> I would like you to provide me with an email address: </label> <input type="email" id="mail" name="mail" /> <button>Submit</button> </form> أضف شيفرة جافا سكريبت التالية إلى الصفحة ضمن العنصر <script> const email = document.getElementById("mail"); email.addEventListener("input", (event) => { if (email.validity.typeMismatch) { email.setCustomValidity("I am expecting an email address!"); } else { email.setCustomValidity(""); } }); تخزن شيفرة جافا سكريبت السابقة مرجعًا إلى عنصر إدخال بريد إلكتروني، ثم تضيف مترصد أحداث ينفذ بعض الشيفرة في كل مرة تتغير فيها القيمة المدخلة في العنصر. تتحقق الشيفرة ضمن معالج الحدث إن أعادت الخاصية validity.typeMismatch لعنصر اﻹدخال القيمة true والتي تعني أن القيمة المدخلة غير صالحة، ولا تتطابق مع النمط المخصص لعنوان البريد اﻹلكتروني. عندها، نستدعي التابع ()setCustomValidity ليعرض رسالة الخطأ المخصصة. يُصيّر هذا التابع عنصر اﻹدخال بحيث تُعرض رسالة الخطأ المخصصة، ويُخفق تسليم الاستمارة. بينما إن أعادت الخاصية القيمة false، نستدعي حينها التابع ()setCustomValidity لكن مع رسالة فارغة، ويصيَّر عنصر اﻹدخال لينجح تسليم أو إرسال الاستمارة. See the Pen custom-error-message by Hsoub Academy (@HsoubAcademy) on CodePen. مثال أكثر تفصيلًا لنحاول اﻵن الاستفادة من الواجهة البرمجية لإنجاز مثال أكثر تعقيدًا للتحقق من صلاحية المدخلات. إليك شيفرة HTML: <form novalidate> <p> <label for="mail"> <span>Please enter an email address:</span> <input type="email" id="mail" name="mail" required minlength="8" /> <span class="error" aria-live="polite"></span> </label> </p> <button>Submit</button> </form> تستخدم الاستمارة البسيطة السابقة الخاصية novalidate ﻹلغاء التقييم التلقائي الذي ينفذه المتصفح. وهذا ما يسمح لشيفرتنا تولي مسؤولية التحقق. لن يمنع هذا اﻹلغاء بالطبع تقييم صلاحية المدخلات باستخدام الواجهة البرمجية ولا يمنع أيضًا استهداف العناصر عبر اﻷصناف الزائفة المناسبة. ويعني ذلك بكل بساطة أن المتصفح سيتنحى جانبًا ويكون عليك التحقق من صحة المدخلات وتنسيق الاستمارة بنفسك قبل تسليمها. إن عنصر اﻹدخال الذي نتحقق من صلاحية مدخلاته هو عنصر إدخال بريد إلكتروني email وقد استخدمت ضمنه الخاصية required حتى يكون مطلوبًا وقررنا أن يكون أدنى عدد مقبول من المحارف هو 8 من خلال الخاصية minlength. وهكذا سنتحقق من تحقيق المدخلات القيود السابقة من خلال شيفرتنا ونعرض رسالة خطأ في حال وجوده. ننوي عرض رسالة الخطأ ضمن عنصر <span>، واستخدمنا ضمنه الخاصية aria-live كي نتأكد من عرض رسالة الخطأ للجميع بمن فيهم مستخدمي قارئات الشاشة. ملاحظة: إن الفكرة المفتاحية هنا هو أن الخاصية novalidate في الاستمارة هي من تمنع عرض رسالة الخطأ الافتراضية وتسمح لنا بعرض الرسالة المخصصة ضمن شجرة DOM بالطريقة التي نشاء. إليك اآن بعض قواعد التنسيق البسيطة لتحسين مظهر الاستمارة وإظهار إشارات مناسبة عندما تكون البيانات المدخلة غير صالحة: body { font: 1em sans-serif; width: 200px; padding: 0; margin: 0 auto; } p * { display: block; } input[type="email"] { appearance: none; width: 100%; border: 1px solid #333; margin: 0; font-family: inherit; font-size: 90%; box-sizing: border-box; } /* This is our style for the invalid fields */ input:invalid { border-color: #900; background-color: #fdd; } input:focus:invalid { outline: none; } /* This is the style of our error messages */ .error { width: 100%; padding: 0; font-size: 80%; color: white; background-color: #900; border-radius: 0 0 5px 5px; box-sizing: border-box; } .error.active { padding: 0.3em; } إليك شيفرة جافا سكريبت: const form = document.querySelector("form"); const email = document.getElementById("mail"); const emailError = document.querySelector("#mail + span.error"); email.addEventListener("input", (event) => { // نتحقق في كل مرة نكتب فيها شيئًا من صلاحية القيمة المدخلة d. if (email.validity.valid) { // إن كانت هناك رسائل خطأ والقيم المدخلة صحيحة // أزل رسالة الخطأ emailError.textContent = ""; // Reset the content of the message emailError.className = "error"; // Reset the visual state of the message } else { // أظهر الخطأ في حال وجوده showError(); } }); form.addEventListener("submit", (event) => { // نسمح بإرسال الاستمارة إذا كانت القيمة المدخلة صالحة if (!email.validity.valid) { // اعرض رسالة الخطأ التالية showError(); // نمنع إرسال الاستمارة بإلغاء الحدث event.preventDefault(); } }); function showError() { if (email.validity.valueMissing) { // إن كان الحقل فارغًا // اعرض رسالة الخطأ التالية emailError.textContent = "You need to enter an email address."; } else if (email.validity.typeMismatch) { // إن لم يضم الحقل عنوان بريد إلكتروني // اعرض رسالة الخطأ التالية emailError.textContent = "Entered value needs to be an email address."; } else if (email.validity.tooShort) { // If the data is too short, // اعرض رسالة الخطأ التالية emailError.textContent = `Email should be at least ${email.minLength} characters; you entered ${email.value.length}.`; } // اضبط التنسيق بالشكل المناسب emailError.className = "error active"; } تشرح التعليقات المضمنة في الشيفرة ما نفعله جيدًا، لكن إن أردنا اﻹيجاز: في كل مرة نغير فيها القيمة المدخلة نتحقق إن كانت البيانات صالحة، فإن كانت كذلك، نزيل أية رسالة خطأ تُعرض، وننفذ الدالة ()showErrorخلاف ذلك، لعرض رسالة الخطأ المناسبة. نتحقق مجددًا من صلاحية البيانات في كل مرة نحاول فيها تسليم النموذج، ونسمح بإرسال النموذج فقط في حال كانت القيم صالحة وإلا ننفذ الدالة ()showError لعرض رسالة الخطأ المناسبة، ونمنع إرسال الاستمارة باستخدام التابع ()preventDefault. تستخدم الدالة ()showError خاصيات مختلفة للكائن valid لتحديد ماهية الخطأ ومن ثم عرض رسالة الخطأ المناسبة. إليك نتيجة الشيفرة السابقة: See the Pen detailed-custom-validation by Hsoub Academy (@HsoubAcademy) on CodePen. وهكذا نرى أن واجهة التحقق من صلاحية المدخلات هي أداة فعالة جدًا للتعامل مع مسائل تقييم المدخلات، وتسمح لك بمجال تحكم أوسع بكثير عما يمكن أن تقدمه HTML و CSS. تقييم النماذج دون استخدام الواجهة البرمجية المضمّنة لن تتمكن من استخدام الواجهة البرمجية المضمنّة في بعض الحالات مثل حالة عناصر التحكم المخصصة، لكن باﻹمكان استخدام شيفرة جافا سكريبت للتحقق من بيانات الاستمارة، وعليك في هذه الحالة كتابة الشيفرة المناسبة بنفسك. وقبل أن تبدأ عليك طرح اﻷسئلة التالية على نفسك: ما نوع التحقق الذي عليّ تنفيذه؟ عليك أن تحدد كيف تتحقق من صحة البيانات. هل ستستخدم العمليات على السلاسل النصية أو التحول بين اﻷنواع أو التعابير النمطية أو مزيج من كل ذلك، فاﻷمر يعود إليك. ما الذي علي فعله إن لم تنجح عملية التحقق؟ هذه المسألة متعلقة تمامًا بواجهة المستخدم. إذ عليك أن تقرر سلوك الاستمارة: هل ترسل البيانات؟ هل يجب تظليل الحقول غير الصالحة؟ هل سأعرض رسالة خطأ؟ كيف أساعد المستخدم على تصويب البيانات غير الصالحة؟ من المهم جدًا تقديم معلومات تساعد المستخدم على ملء الاستمارة قدر اﻹمكان حتى لا يُصاب باﻹحباط. فعليك مثلًا تقديم اقتراحات مسبقة حتى يتوقع المستخدم ما سيفعله إضافة إلى تزويده برسائل خطأ واضحة. مثال دون استخدام واجهة التحقق البرمجية إليك نسخة مبسطة من المثال السابق لتوضيح فكرة التحقق من صلاحية المدخلات دون استخدام الواجهة البرمجية المضمّنة. تبقى شيفرة HTML نفسها، لكننا أزلنا فقط الخاصية novalidate: <form> <p> <label for="mail"> <span>Please enter an email address:</span> <input type="text" id="mail" name="mail" /> <span class="error" aria-live="polite"></span> </label> </p> <button>Submit</button> </form> لم تتغير شيفرة CSS كثيرًا، لكننا حولنا الصنف الزائف invalid: إلى صنف حقيقي لتفادي استخدام محددات الخاصيات attribute selector: body { font: 1em sans-serif; width: 200px; padding: 0; margin: 0 auto; } form { max-width: 200px; } p * { display: block; } input#mail { appearance: none; width: 100%; border: 1px solid #333; margin: 0; font-family: inherit; font-size: 90%; box-sizing: border-box; } /* This is our style for the invalid fields */ input.invalid { border-color: #900; background-color: #fdd; } input:focus:invalid { outline: none; } /* This is the style of our error messages */ .error { width: 100%; padding: 0; font-size: 80%; color: white; background-color: #900; border-radius: 0 0 5px 5px; box-sizing: border-box; } .error.active { padding: 0.3em; } أما التغيير الجذري فهو في شيفرة جافا سكريبت، إذ ستحمل العبء اﻷكبر: const form = document.querySelector("form"); const email = document.getElementById("mail"); const error = email.nextElementSibling; // HTML وفق مواصفات const emailRegExp = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/; // يمكن اﻵن بناء حدود للتقييم // لأننا لم نعد نعتمد على اﻷصناف الزائفة window.addEventListener("load", () => { // نتحقق أن عنصر اﻹدخال فارغ (تذكر العنصر غير مطلوب) // إن لم يكن كذلك نتحقق أن المحتوى هو عنوان بريد إلكتروني صالح const isValid = email.value.length === 0 || emailRegExp.test(email.value); email.className = isValid ? "valid" : "invalid"; }); // تعرف الشيفرة التالية ما يحدث عندما تتغير القيمة المدخلة email.addEventListener("input", () => { const isValid = email.value.length === 0 || emailRegExp.test(email.value); if (isValid) { email.className = "valid"; error.textContent = ""; error.className = "error"; } else { email.className = "invalid"; } }); // تعرف الشيفرة التالية ما يحدث عندما يحاول المستخدم إرسال الاستمارة form.addEventListener("submit", (event) => { event.preventDefault(); const isValid = email.value.length === 0 || emailRegExp.test(email.value); if (!isValid) { email.className = "invalid"; error.textContent = "I expect an email, darling!"; error.className = "error active"; } else { email.className = "valid"; error.textContent = ""; error.className = "error"; } }); إليك النتيجة: See the Pen An example that doesnt use the constraint validation API sample by Hsoub Academy (@HsoubAcademy) on CodePen. وكما نرى، اﻷمر ليس صعبًا للغاية، وتكمن الصعوبة في جعل الحل عامًا بما يكفي لتستخدمه عبر المنصات المختلفة وعلى أي استمارة قد تحاول إنشاءها. وتجدر اﻹشارة إلى وجود العديد من المكتبات الجاهزة لتنفيذ عمليات التحقق من المدخلات مثل Validate.js. الخلاصة تحتاج عملية التحقق في طرف العميل أحيانًا إلى شيفرة جافا سكريبت إن أردت تخصيص التنسيق ورسائل الخطأ، لكن من المهم جدًا التفكير بحرص شديد عندما يتعلق اﻷمر بالمستخدم. وتذكر أن تساعده في تصويب البيانات التي يُدخلها واحرص على أن: تعرض رسائل خطأ صريحة. تقدم صيغة بسيطة إدخال القيم وأن تتساهل في ذلك. تشير إلى موقع الخطأ تمامًا وخاصة عندما يتكون الاستمارة كبيرة. وعندما تتأكد من صلاحية جميع القيم في الاستمارة يمكنك حينها تسليمها إلى الخادم. ترجمة -وبتصرف- للمقال: Client-side form validation اقرأ أيضًا المقال السابق: التنسيق المتقدم لاستمارات الويب نظرة على تفاعلات الخادم مع العميل في موقع ويب ديناميكي أنواع عناصر اﻹدخال في HTML5 آليات الاتصال المستمر مع الخادم في جافاسكربت المدخلات والمخرجات النصية في جافا
  11. نناقش في هذا المقال استخدام لغة CSS في تنسيق بعض عناصر التحكم التي يصعب تنسيقها في استمارة الويب. وكما رأينا في المقال السابق أن معظم العناصر سهلة التنسيق مثل الحقول النصية والأزرار، وبعضها اﻵخر صعب أو يسبب مشاكل أثناء التنسيق مثل صناديق التحقق وبعض أنواع عناصر اﻹدخال. ولكي نلخص ما ذكرناه سابقًا، فإن هناك نوعين من عناصر التحكم من ناحية التنسيق: صعبة التنسيق: تتطلب قواعد CSS معقدة أو استخدام حيل محددة نذكر من هذه العناصر صناديق التحقق check-boxes و أزرار الاختيار المتعدد radio buttons. غير قابلة للتنسيق الكامل: لا يمكن تنسيقها بشكل كامل باستخدام CSS مثل: حقل اﻹدخال من النوع color. حقل اﻹدخال من النوع datetime. حقل اﻹدخال من النوع range. حقل اﻹدخال من النوع file. عناصر القوائم منسدلة مثل العنصر <select> و <option> و <optgroup>. عناصر أشرطة التقدم مثل <meter> و <progress>. لهذا، سنتحدث في بداية عن الخاصية appearance التي تساعد في تنسيق تلك العناصر. الخاصية appearance والتنسيق على مستوى نظام التشغيل تحدثنا في مقال سابق كيف أن تنسيق عناصر التحكم في الاستمارات يعتمد على نظام التشغيل الذي يستضيف صفحة الويب، وهذا جزء من السبب الذي يجعل تنسيق بعض العناصر صعبًا. لهذا السبب أنشئت الخاصية appearance لتقدم طريقة للتحكم بتنسيق عناصر تحكم الاستمارة، فالقيمة التي ستتعامل معها (وقد لا ترى غيرها) لهذه الخاصية هي القيمة none. حيث تمنع هذه القيمة استخدام التنسيق على مستوى نظام التشغيل قدر اﻹمكان وتسمح ببناء تنسيق مخصص باستخدام CSS. لنلق نظرة على عناصر التحكم التالية: <form> <p> <label for="search">search: </label> <input id="search" name="search" type="search" /> </p> <p> <label for="text">text: </label> <input id="text" name="text" type="text" /> </p> <p> <label for="date">date: </label> <input id="date" name="date" type="datetime-local" /> </p> <p> <label for="radio">radio: </label> <input id="radio" name="radio" type="radio" /> </p> <p> <label for="checkbox">checkbox: </label> <input id="checkbox" name="checkbox" type="checkbox" /> </p> <p><input type="submit" value="submit" /></p> <p><input type="button" value="button" /></p> </form> إن تطبيق القاعدة التالية سيزيل التنسيق على مستوى نظام التشغيل: input { appearance: none; } عند تنفيذ تلك الشيفرة، ستكون لديك النتيجة التالية التي تضم عناصر التحكم التي طُبق عليها تنسيق نظام التشغيل (إلى اليسار) ونفس العناصر بعد تطبيق قاعدة CSS (إلى اليمين): See the Pen appearance-tester by Hsoub Academy (@HsoubAcademy) on CodePen. يكون التأثير في معظم الأحيان إزالة الحواف المنسقة وبالتالي سيسهل هذا قليلًا استخدام CSS، قد لا يبدو لك هذا باﻷمر المهم. لكن سيظهر تأثيره المفيد في حالات استخدام عنصر حقل اﻹدخال search وكذلك عند استخدام صناديق التحقق وأزرار الاختيار المتعدد. العمل مع صناديق البحث صندوق البحث هو نوع من أنواع عناصر اﻹدخال يشابه عنصر اﻹدخال النصي، لكن ما فائدة القاعدة appearance: none في هذه الحالة؟ الجواب هو أن متصفح سفاري يطبق بعض القيود على صناديق البحث، فلن تتمكن من تغيير ارتفاعه height أو حجم الخط font-size بحرية. باﻹمكان إصلاح هذا اﻷمر باستخدام القاعدة السابقة التي تعطل المظهر الافتراضي. إليك مثالًا: input[type="search"] { appearance: none; } سترى في المثال التالي صندوقي بحث متطابقين من ناحية التنسيق، لكن طبقت على الصندوق اليميني القاعدة appearance: none بينما لم تُطبق على الصندوق اليساري. إن نظرت جيدًا إلى الصندوقين في متصفح سفاري على نظام ماك أو إس macOS، قد تلاحظ أن أبعاد الصندوق اليساري لم تضبط بالشكل الصحيح. See the Pen search-appearance by Hsoub Academy (@HsoubAcademy) on CodePen. ومن المثير للاهتمام اختفاء مشكلة اﻷبعاد السابقة عند ضبط الحواف أو الخلفية لصندوق البحث. فلم تُطبق القاعدة appearance: none على صندوق البحث في المثال التالي، لكنه لم يعاني من ذات المشكلة عند عرضه في متصفح سفاري! See the Pen styled-search by Hsoub Academy (@HsoubAcademy) on CodePen. ملاحظة: لربما لاحظت أن اﻷيقونة x في حقل البحث (التي تظهر عندما يكتسب صندوق البحث تركز الدخل ولم يكن فارغًا) ستختفي عندما يفقد الصندوق تركيز الدخل في متصفحي كروم وإيدج لكنها تبقى في متصفح سفاري. لإزالتها باستخدام CSS بإمكاننا استخدام القاعدة التالية: input[type="search"]:not(:focus, :active)::-webkit-search-cancel-button { display: none; } تنسيق صناديق التحقق وأزرار الاختيار من متعدد إن تنسيق هذه العناصر مربك قليلًا، فلم يؤخذ بالاعتبار إمكانية تغيير أبعاد هذه العناصر وفق تصميمها الافتراضي، وسيختلف سلوك المتصفحات عندما تحاول ذلك. لنلق نظرة على الحالة التالية: <label ><span><input type="checkbox" name="q5" value="true" /></span> True</label > <label ><span><input type="checkbox" name="q5" value="false" /></span> False</label > span { display: inline-block; background: red; } input[type="checkbox"] { width: 100px; height: 100px; } تتعامل المتصفحات مع صندوق التحقق والعنصر <span> بطرق مختلفة وبعضها سيئ المظهر: المتصفح طريقة التصيير فايرفوكس 71 (على ماك أو إس) فايرفوكس 57 (ويندوز 10) كروم 77 (ماك أو إس)/ سفاري 13، أوبيرا كروم 63 (ويندوز 10) إيدج 16 (ويندوز 10) استخدام القاعدة appearance: none مع صناديق التحقق وأزرار الاختيار المتعدد رأينا سابقًا كيف يمكننا إزالة المظهر الافتراضي لهذه العناصر دفعة واحدة باستخدام القاعدة appearance: none. لنلق نظرة على هذا المثال: <form> <fieldset> <legend>Fruit preferences</legend> <p> <label> <input type="checkbox" name="fruit" value="cherry" /> I like cherry </label> </p> <p> <label> <input type="checkbox" name="fruit" value="banana" disabled /> I can't like banana </label> </p> <p> <label> <input type="checkbox" name="fruit" value="strawberry" /> I like strawberry </label> </p> </fieldset> </form> سننسق صناديق التحقق كي تأخذ تصميمًا خاصًا، وسنبدأ بإلغاء التنسيق اﻷصلي: input[type="checkbox"] { appearance: none; } نستطيع استخدام الصنفين الزائفين checked: و disabled: لتغيير مظهر صندوق التحقق وتغيير حالته: input[type="checkbox"] { position: relative; width: 1em; height: 1em; border: 1px solid gray; /* Adjusts the position of the checkboxes on the text baseline */ vertical-align: -2px; /* Set here so that Windows' High-Contrast Mode can override */ color: green; } input[type="checkbox"]::before { content: "✔"; position: absolute; font-size: 1.2em; right: -1px; top: -0.3em; visibility: hidden; } input[type="checkbox"]:checked::before { /* Use `visibility` instead of `display` to avoid recalculating layout */ visibility: visible; } input[type="checkbox"]:disabled { border-color: black; background: #ddd; color: gray; } سنتعرف على الأصناف الزائفة ونقاط أخرى في مقال قادم، لكن ما يفعله الصنفان السابقان هو التالي: checked: تُطبَّق عندما يكون صندوق التحقق أو زر الاختيار المتعدد في حالة التفعيل، أي أنه نقر من قبل المستخدم. disabled: تُطبَّق عندما يكون صندوق التحقق أو زر الاختيار المتعدد في حالة التعطيل، أي لا يمكن للمستخدم التعامل معه. إليك نتيجة الشيفرة السابقة: See the Pen checkboxes-styled by Hsoub Academy (@HsoubAcademy) on CodePen. ستجد هنا أيضًا مثالين آخرين لتوضيح الفكرة: المثال اﻷول عن تنسيق أزرار الاختيار المتعدد. المثال الثاني عن تنسيق صندوق التحقق ليبدو وكأنه زر تبديل toggle switch. إن حاولت استعراض صناديق التحقق السابقة في متصفح لا يدعم الخاصية appearance لن تشاهد التنسيق الذي خصصته، لكنها ستحافظ على شكل صندوق التحقق وتؤدي نفس الوظيفة. ما الذي يمكن فعله للعناصر التي لا يمكن تنسيقها باستخدام CSS فقط؟ ما الذي يمكن فعله لتنسيق عناصر لا يمكن تنسيقها بالكامل باستخدام CSS مثل القوائم المنسدلة والعناصر المركبة مثل منتقي الألوان color picker أو عنصر تحديد التاريخ date-time وعناصر مراقبة التقدم progress elements وغيرها؟ تكمن مشكلة هذه العناصر اﻷساسية في اختلاف الطريقة الافتراضية لعرضها من متصفح إلى آخر، وإن استطعت تنسيقها بطريقة ما، يستحيل تنسيق بعض أجزائها الداخلية. فإن كنت مستعدًا للتآلف مع بعض الاختلافات بالشكل والمظهر، يمكنك اتباع بعض أساليب التنسيق لتحقيق تناسق في اﻷبعاد، وتطبيق تنسيق موحد للخلفيات وكذلك استخدام الخاصية appearance للتخلص من بعض التنسيقات على مستوى نظام التشغيل. لنلق نظرة على المثال التالي الذي يعرض عددًا من العناصر صعبة التنسيق في استمارة الويب: See the Pen ugly-controls by Hsoub Academy (@HsoubAcademy) on CodePen. طُبقت قواعد التنسيق التالية على العناصر السابقة: body { font-family: "Josefin Sans", sans-serif; margin: 20px auto; max-width: 400px; } form > div { margin-bottom: 20px; } select { appearance: none; width: 100%; height: 100%; } .select-wrapper { position: relative; } .select-wrapper::after { content: "▼"; font-size: 1rem; top: 3px; right: 10px; position: absolute; } button, label, input, select, progress, meter { display: block; font-family: inherit; font-size: 100%; margin: 0; box-sizing: border-box; width: 100%; padding: 5px; height: 30px; } input[type="text"], input[type="datetime-local"], input[type="color"], select { box-shadow: inset 1px 1px 3px #ccc; border-radius: 5px; } label { margin-bottom: 5px; } button { width: 60%; margin: 0 auto; } كما ترى، استطعنا على نحو مقبول جدًا تنسيق تلك العناصر الصعبة لتبدو منتظمة عبر المتصفحات الحديثة. وطبقنا في الواقع بعض قواعد CSS العامة على جميع عناصر التحكم وعناوينها كي يكون لها جميعها نفس اﻷبعاد، وتمتلك خط كتابة العنصر اﻷب وهكذا. button, label, input, select, progress, meter { display: block; font-family: inherit; font-size: 100%; margin: 0; box-sizing: border-box; width: 100%; padding: 5px; height: 30px; } أضفنا أيضًا بعض الظلال المنتظمة والزوايا الدائرية في العناصر التي يبدو فيها اﻷمر منطقيًا: input[type="text"], input[type="datetime-local"], input[type="color"], select { box-shadow: inset 1px 1px 3px #ccc; border-radius: 5px; } لكننا لم نطبق هذه اﻷشياء على عناصر تحكم مثل عناصر المجالات وأشرطة التقدم، فكل ما ستعرضه هو صندوق قبيح المظهر حول عنصر التحكم، ولن يبدو استخدامها منطقيًا أبدًا. العنصر <select> وقوائم البيانات تقدم المتصفحات الحديثة تنسيقًا افتراضيًا مقبولًا لقوائم البيانات، ولن تضطر إلى تغيير تنسيقها كثيرًا. وبالنسبة للمظهر الأساسي للصناديق فقد حافظنا عليه مع بعض التغييرات الطفيفة لتبدو منتظمة ومتناسقة، وطالما أن عناصر القوائم هي في الواقع عناصر إدخال نصي <input> (قيمة الخاصية type له تساوي "text") فهي بسيطة التنسيق. أما ما يجعل اﻷمر صعبًا فهما ناحيتان: اﻷولى هي اختلاف أيقونة السهم في العنصر <select> التي تشير إلى اتجاه انسدال القائمة من متصفح ﻵخر، وقد تتغير هذه اﻷيقونة عند تغيير حجم صندوق الاختيار أو تتغير أبعادها بطريقة سيئة. وﻹصلاح هذه المشكلة استخدمنا بداية الخاصية appearance: none للتخلص من الأيقونة بالمطلق: select { appearance: none; } ثم أنشأنا أيقونة مخصصة باستخدام المحتوى المولَّد (باستخدام الخاصية content). ووضعنا عنصر تغليف إضافي حول عنصر التحكم لأن الصنفان after:: و before:: لا يعملان مع العنصر <select>. والسبب في ذلك أن موضع المحتوى المولَّد عن طريق الشيفرة سيكون منسوبًا لصندوق التنسيق الذي يحيط بها، لكن عناصر اﻹدخال تعمل كأنها عناصر مُستبدلة، إذ يقرر المتصفح طريقة عرضها ويضعها في مكانها فهي لا تمتلك صندوق تنسيق: <label for="select">Select a fruit</label> <div class="select-wrapper"> <select id="select" name="select"> <option>Banana</option> <option>Cherry</option> <option>Lemon</option> </select> </div> نستخدم بعدها محتوى مولَّد لتمثيل سهم نحو الأسفل ونضعه في مكانه الصحيح باستخدام خاصية التموضع position: .select-wrapper { position: relative; } .select-wrapper::after { content: "▼"; font-size: 1rem; top: 6px; right: 10px; position: absolute; } أما الناحية الثانية الأكثر أهمية، هي عدم قدرتنا على التحكم بمظهر الصندوق الذي يضم الخيارات عند النقر على صندوق العنصر <select> لفتحه. وعلى الرغم من وراثة خط الكتابة عن العنصر اﻷب، إلا أنك لن تكون قادرًا على ضبط التباعد بين الحروف أو اللون. وينطبق هذا اﻷمر على قوائم اﻹكمال التلقائي التي تظهر مع العنصر <datalist>. فإن أردت تحكمًا كاملًا عند تنسيق قوائم الخيارات، عليك استخدام نوع من المكتبات التي تقدم عنصر تحكم مخصص أو أن تبني بنفسك هذا العنصر. أو استخدم في حالة العنصر <select> الخاصية multiple والتي تعرض جميع الخيارات معًا متجنبًا هذه المشكلة بالتحديد: <label for="select">Select fruits</label> <select id="select" name="select" multiple> … </select> ملاحظة: قد لا يتناسب هذا الحل مع تصميمك لكنه يستحق المحاولة. تنسيق عنصر التقويم لجميع أنواع عنصر التقويم المسؤولة عن إدخال التاريخ والوقت مثل (datetime-local و time و week و month) نفس مشكلات التنسيق الرئيسية. فمن الممكن أن تنسق الصندوق الذي يضمها بكل سهولة كأي عنصر إدخال نصي، لكن لا يمكن تنسيق اﻷجزاء الداخلية منها مثل التقويم الذي تعرضه أو زر الزيادة والنقصان إطلاقًا، ولا يمكن إزالتها باستخدام القاعدة appearance: none فإن أردت تحكمًا كاملًا بهذه العناصر لا بد من الاستعانة بمكتبة خارجية تبني لك العنصر أو أن تحاول بناء عنصرك المخصص بنفسك. ملاحظة: علينا أن نذكر هنا عنصر اﻹدخال العددي <'input type = 'number> الذي يمتلك أيضًا زر لزيادة أو إنقاص القيمة العددية. فقد يعاني أيضًا من نفس المشكلة السابقة. لهذا إن كانت القيمة العددية بسيطة، يمكنك استخدام النوع tel فهو لا يعرض هذا الزر، وله مظهر النوع text ويعرض لوحة مفاتيح رقمية أيضًا. عنصر إدخال المجالات range من الصعب تنسيق حقول المجال range، لكن باﻹمكان الاستفادة من اﻷفكار السابقة بإزالة التنسيق الافتراضي كليًا واستبداله بتنسيق مخصص كالتالي: input[type="range"] { appearance: none; background: red; height: 2px; padding: 0; outline: 1px solid transparent; } مع ذلك، من الصعب جدًا استبدال تنسيق مقبض الجر drag handle، ولا بد من استخدام شيفرة CSS معقدة تتضمن العديد من العناصر غير المعيارية والمخصصة لمتصفحات معينة حتى تستطيع التحكم بتنسيق عنصر المجالات بشكل كامل. عنصر انتقاء اللون لا يبدو مظهر هذا العنصر سيئًا في المتصفحات التي تدعمه، إذ تميل معظمها إلى عرض كتلة من اﻷلوان لها طار صغير. يمكنك إزالة الإطار وإبقاء الكتلة اللونية فقط باستخدام قواعد كهذه: input[type="color"] { border: 0; padding: 0; } أما التحكم الكامل، فيحتاج إلى حل مخصص. عنصر انتقاء الملفات لا بأس بمظهر عناصر انتقاء الملفات من النوع file، ومن السهل كما رأينا في مثال سابق أن نجعلها تبدو متناسقة مع بقية أجزاء الصفحة. إذ يرث السطر الذي يعرض الملفات التي تنتقيها العنصر اﻷب إن أردت ذلك، كما تستطيع تنسيق قائمة أسماء الملفات بالطريقة التي تريدها. أما المشكلة الوحيدة في منتقي الملفات هو الزر الذي تنقر عليه لفتح نافذة انتقاء الملفات، فهو غير قابل للتنسيق بأي شكل من اﻷشكال. ومن الطرق المتبعة للالتفاف على هذا الأمر هو استخدام العنوان المرتبط مع هذا العنصر بدلًا من الزر اﻷساسي. لهذا يمكنك إخفاء عنصر انتقاء الملفات وتنسيق العنوان المرتبط به ليبدو وكأنه زر ثم النقر عليه لفتح نافذة انتقاء الملفات: input[type="file"] { height: 0; padding: 0; opacity: 0; } ثم ننسق العنوان المرتبط بالعنصر حتى يبدو وكأنه زر: label[for="file"] { box-shadow: 1px 1px 3px #ccc; background: linear-gradient(to bottom, #eee, #ccc); border: 1px solid rgb(169, 169, 169); border-radius: 5px; text-align: center; line-height: 1.5; } label[for="file"]:hover { background: linear-gradient(to bottom, #fff, #ddd); } label[for="file"]:active { box-shadow: inset 1px 1px 3px #ccc; } ستكون نتيجة عرض الشيفرة السابقة كما يلي: See the Pen styled-file-picker by Hsoub Academy (@HsoubAcademy) on CodePen. أشرطة التقدم والعدادات قد يكون العنصران <meter> و <progress> اﻷسوء من ناحية التنسيق. صحيح أننا تمكنا في مثال سابق من ضبط اتساعها بشكل دقيق نسبيًا، لكن خلاف ذلك، من الصعب تنسيقها بأي شكل. إذ لا يمكن مثلًا أن نضمن سلوك المتصفحات فيما يخص ارتفاع هذه العناصر، وعلى الرغم من إمكانية تغيير لون الخلفية، لكن لا يمكن تغيير اللون اﻷمامي، وتطبيق القاعدة appearance: none تزيد اﻷمر سوءًا. لهذا من اﻷفضل أن تجد حلًا مخصصًا لتتحكم بالمظهر الكامل لهذه العناصر، أو يمكنك استخدام أية حلول تقدمها أطراف خارجية مثل progressbar.js الخلاصة على الرغم من وجود بعض الصعوبات في تطبيق قواعد التنسيق المخصصة بلغة CSS على عناصر تحكم استمارات الويب، لكن توجد دائمًا طرق للالتفاف على هذه الصعوبات. وبالطبع لا توجد حلول عامة، لكن المتصفحات الحديثة تقدم إمكانيات جديدة. لهذا من اﻷفضل أن تتعلم أكثر عن طريقة تطبيق المتصفحات المختلفة لقواعد CSS التي تدعمها على عناصر استمارات HTML وتنسقها وفق ذلك. ترجمة -وبتصرف- للمقال: Advanced form styling اقرأ أيضًا المقال السابق: تنسيق استمارات الويب باستخدام CSS تعرف على أنواع الحقول الجديدة في نماذج HTML5 أساسيات العمل مع استمارات الويب Web forms تنسيق الصور والوسائط المتعددة والنماذج في CSS HTML و CSS للمبتدئين: مقدمة إلى تنسيقات CSS
  12. تعرفنا في مقال سابق على طريقة تنسيق استمارات الويب بشكل متقدم واستعرضنا أمثلة على بعض الحالات التي نستخدم فيها أصناف التنسيق الزائفة pseudo-classes مثل checked: لاستهداف صندوق التحقق في الحالة التي يكون فيها مفعّلًا. وسنتابع في هذا المقال التعرف أكثر على هذه اﻷصناف الزائفة واستخداماتها في تنسيق استمارات الويب. ننصحك قبل المضي معنا في هذا المقال الاطلاع على أساسيات HTML وعلى أساسيات تنسيق الصفحات باستخدام CSS، إضافة إلى إلقاء نظرة على مفهوم اﻷصناف والعناصر الزائفة. ما هي اﻷصناف الزائفة المتاحة؟ إليك قائمة باﻷصناف الزائفة اﻷصلية التي تتعلق بالاستمارات كما قدمتها CSS 2.1: الصنف الزائف hover: يختار العنصر عندما يتحرك مؤشر الفأرة فوقه. الصنف الزائف focus: يختار العنصر عندما يكتسب تركيز الدخل (عندما تنتقل إليه عبر الضغط على الزر Tab). الصنف الزائف active: يختار العنصر عندما يُفعّل -بالنقر عليه مثلًا- أو عند الضغط على الزر Enter. لا بد أن تكون هذه اﻷصناف الأساسية معروفة بالنسبة إليك، وتزودنا محددات CSS أصنافًا زائفةً أخرى للعمل مع استمارات HTML، وتستخدم لاستهداف عناصر الاستمارة عند تحقق شروط معينة سنناقشها بمزيد من التفصيل في هذا المقال: يستخدم الصنفان required: و optional: في استهداف عناصر HTML التي تمتلك الخاصية required والتي تحدد ما إذا كان هذا العنصر مطلوبًا أم اختياريًا. تستخدم الأصناف valid: و invalid: و in-range: و out-of-range: لاستهداف عناصر الاستمارة ذات القيمة الصالحة أو غير الصالحة وفقًا لقيود التحقق من القيمة وكذلك إن كانت ضمن المجال المطلوب أو خارجه. تستهدف الأصناف enabled: و disabled: و read-only: و write-only: العناصر التي يمكن تعطيلها وتفعيلها (تمتلك الخاصية disabled)، وتلك التي تسمح بالقراءة فقط أو الكتابة فقط (تمتلك الخاصية readonly). تستهدف الأصناف checked: و indeterminate: و default: بالترتيب صناديق التحقق وأزرار الاختيار المتعدد المفعّلة والعناصر في الحالة العائمة indeterminate (ليست مفعلة أو غير مفعلة)، والعنصر المختار افتراضيًا عند تحميل الصفحة مثل صندوق تحقق مفعل افتراضيًا أو عنصر استخدمت فيه الخاصية selected. هناك العديد أيضًا من هذه اﻷصناف لكن ما ذكرناه هنا هو أوضحها استعمالًا، وبعضها يُخصص لحل مشاكل محددة جدًا. إضافة إلى ذلك، فالأصناف السابقة مدعومة جيدًا في معظم المتصفحات، لكن عليك معرفة طريقة تطبيقها بشكل صحيح للتأكد من استهدافك العنصر المطلوب. ملاحظة: نستخدم عددًا من اﻷصناف الزائفة السابقة لتنسيق عناصر تحكم استمارة ويب وفقًا لصلاحية قيمها (قيمتها صالحة أو لا) وهذا ما سنراه في مقال لاحق، لذلك سنبقي اﻷمور بسيطة قدر اﻹمكان فيما يخص أمور التحقق من الصلاحية. تنسيق عناصر اﻹدخال في حال كانت مطلوبة أم لا من أهم اﻷساسيات التي تتعلق بالتحقق من صحة المدخلات في طرف العميل تحديد إن كان عنصر إدخال ما ضروريًا (يجب إدخال قيمته قبل إرسال الاستمارة) أم اختياريًا. ولبعض العناصر مثل <input> و <textarea> الخاصية required التي تشير عند ضبطها إلى ضرورة إدخال قيمة لهذا العنصر، وإلا ستُخفق عملية إرسال الاستمارة. إليك مثالًا: <form> <fieldset> <legend>Feedback form</legend> <div> <label for="fname">First name: </label> <input id="fname" name="fname" type="text" required /> </div> <div> <label for="lname">Last name: </label> <input id="lname" name="lname" type="text" required /> </div> <div> <label for="email"> Email address (include if you want a response): </label> <input id="email" name="email" type="email" /> </div> <div><button>Submit</button></div> </fieldset> </form> نجد في هذه الاستمارة أن الاسم الأول واﻷخير مطلوبان، لكن البريد اﻹلكتروني اختياري. ويمكنك استهداف الحالتين السابقتين باستخدام الصنفين required: و optional:. فلو طبقنا مثلًا قواعد CSS التالية: input:required { border: 1px solid black; } input:optional { border: 1px solid silver; } سيكون للحقول المطلوبة حواف أو إطار أسود اللون، وستكون حواف العنصر غير المطلوب رمادية اللون: See the Pen basic-required-optional by Hsoub Academy (@HsoubAcademy) on CodePen. جرّب أن تنقر زر اﻹرسال دون أن تملأ الاستمارة كي ترى رسالة الخطأ الافتراضية التي يعرضها المتصفح عندما يتحقق من المدخلات. وعلى الرغم من أن اﻷسلوب السابق ليس سيئًا كبداية، لكن اﻹشارة إلى أن عنصر اﻹدخال مطلوب عن طريق اللون فقط أمر لا يكفي في حالات عدة منها أن يكون المستخدم مريضًا بعمى اﻷلوان، إضافة إلى وجود عرف يقضي بتعليم العنصر المطلوب بإشارة * أو ربط الكلمة "مطلوب required" بالعنصر. لهذا السبب، سنلقي نظرة في القسم التالي على أمثلة أفضل عن استخدام الصنف required: وكذلك استخدام المحتوى المولَّد. ملاحظة: قد لا تستخدم الصنف optional:في الكثير من اﻷحيان، فعناصر اﻹدخال اختيارية افتراضيًا، لهذا سيطبق التنسيق الافتراضي على هذه العناصر ويبقى عليك تخصيص تنسيق للعناصر المطلوبة. ملاحظة: إن كان لأحد أزرار الاختيار المتعدد في مجموعة أزرار لها نفس الاسم الخاصية required فلن تتمكن من إرسال الاستمارة ما لم يجري اختيار أحد أزرار المجموعة، لكن التنسيق باستخدام required:سيطبق فقط على الزر الذي يمتلك تلك الخاصية. استخدام المحتوى المولَّد generated content مع الأصناف الزائفة اطلعنا في المقال السابق على استخدام المحتوى المولَّد باستخدام الخاصية content، لكننا سنناقشه بتفصيل أكبر في هذه الفقرة. تكمن الفكرة في إمكانية استخدام العنصرين الزائفين after:: و before:: مع الخاصية content ﻹظهار محتوى قبل أو بعد العنصر المستهدف. لن يضاف المحتوى الجديد إلى شجرة DOM وسيكون بالتالي مخفيًا عن قارئات الشاشة. وطالما أن المحتوى الجديد هو عنصر زائف، سنتمكن من تنسيقه بنفس اﻷسلوب الذي نستهدف فيه عنصرًا من DOM وننسقه. لهذا الأمر فائدته عندما ترغب في إضافة مؤشر مرئي إلى عنصر مثل عنوان أو أيقونة مع وجود مؤشر بديل متاح أيضًا للتأكد من سهولة وصول جميع المستخدمين. تستخدم الشيفرة التالية على سبيل المثال المحتوى المولّد لرسم دائرة متحركة داخل زر اختيار متعدد عند النقر عليه: input[type="radio"]::before { display: block; content: " "; width: 10px; height: 10px; border-radius: 6px; background-color: red; font-size: 1.2em; transform: translate(3px, 3px) scale(0); transform-origin: center; transition: all 0.3s ease-in; } input[type="radio"]:checked::before { transform: translate(3px, 3px) scale(1); transition: all 0.3s cubic-bezier(0.25, 0.25, 0.56, 2); } تظهر فائدة المحتوى المولّد في حالات كهذه أي عند النقر على صندوق تحقق أو زر اختيار متعدد. إذ سيعرف مستخدمو قارئات الشاشة أن هذا الصندوق مفعّل أو غير مفعّل ولا حاجة ﻹضافة عنصر DOM تنطقه قارئات الشاشة مرة ثانية فهذا أمر مربك. بينما لن يُقرأ المحتوى المولّد من قبل قارئات الشاشة ولن يربك من يستخدمها، ويحل في نفس الوقت مشكلة اﻷشخاص صحيحي البصر. وبالعودة إلى مثالنا السابق الذي ينسق العناصر المطلوبة، سنستخدم في هذه النسخة محتوىً مولّدًا ﻹضافة مؤشر إلى ضرورة هذا العنصر. نضيف بداية مقطعًا إلى بداية الاستمارة يوضح ما نريد فعله: <p>Required fields are labeled with "required".</p> ستنطق قارئات الشاشة الكلمة "required مطلوب" عندما يصل المستخدم إلى كل حق مطلوب، بينما سيرى المستخدمون الأصحاء عناونًا يوضح ذلك. وطالما أن عناصر اﻹدخال لا تدعم المحتوى المولّد (لأنها تسلك سلوك عناصر مستبدلة replaced elements، بينما يوضع المحتوى المولّد قبل أو بعد العنصر الذي يمتلك صندوق تنسيق فقط)، سنضيف عنصر <span> فارغ ليضم هذا المحتوى: <div> <label for="fname">First name: </label> <input id="fname" name="fname" type="text" required /> <span></span> </div> أما المشكلة التي ستظهر هنا هو انتقال هذا العنصر إلى سطر جديد ﻹن كل من العنوان وعنصر اﻹدخال قد ضبطا ليشغلا كامل الاتساع width: 100%. لهذا ننسق العنصر اﻷب <div> ليصبح حاوية مرنة، وننسقه أيضًا أن ينقل المحتوى إلى سطر جديد إن كان طويلًا جدًا: fieldset > div { margin-bottom: 20px; display: flex; flex-flow: row wrap; } يظهر تأثير هذا التنسيق بأن يكون العنوان وعنصر اﻹدخال ضمن سطرين منفصلين لأن كلاهما يمتد ليشمل كامل الاتساع المتاح له width:100%، وطالما أن للعنصر <span> له الاتساع 0، سيقع على نفس سطر عنصر اﻹدخال. علينا اﻵن توليد المحتوى باستخدام CSS ووضعه بعد العنصر <span> الذي يأتي بعد عنصر إدخال مطلوب: input + span { position: relative; } input:required + span::after { font-size: 0.7rem; position: absolute; content: "required"; color: white; background-color: black; padding: 5px 10px; top: -26px; left: -70px; } ضبطنا موقع العنصر <span> ليكون نسبيًا position: relative كي نتمكن من توليد محتوى توضُّعه مطلق position: absolute يمكن ضبط موقعه بالنسبة إلى العنصر <span> (وضعه نسبي) وليس بالنسبة إلى العنصر <body> (إذ يعمل المحتوى المولد وكأنه أبن للعنصر الذي يولّد عليه وذلك لتوضيعه بالشكل المناسب). بعدها حددنا المحتوى المطلوب وهو العبارة "required مطلوب"، وكانت النتيجة كالتالي: See the Pen required-optional-generated by Hsoub Academy (@HsoubAcademy) on CodePen. تنسيق عنصر التحكم وفقًا لصلاحية القيمة المدخلة من المعايير اﻷساسية المهمة أيضًا في عملية التحقق من الاستمارات هو صحة القيم المدخلة (في حالة القيم العددية أو ضمن مجال محدد أو خارجه). أي يمكن استهداف عناصر تحكم النموذج التي لها قيم محدودة وفقًا لصلاحية هذه القيمة أو لا. الصنفان valid: و invalid: لاستخدام هذين الصنفين مع عناصر تحكم الاستمارة لا بد أن تتذكر أن: تُعد العناصر التي لا حدود لقيمتها، صالحة دائمًا، ويستهدفها الصنف valid:. تُعد العناصر المطلوبة required الفارغة التي لا تضم قيمًا غير صالحة، وسوف يستهدفها الصنفان invalid: و required:. تُعد العناصر التي تتحقق من القيم المدخلة إليها وفق نمط محدد مثل عناصر إدخال البريد اﻹلكتروني أو عنوان URL غير صالحة إن لم تتطابق البيانات المدخلة مع نمط التحقق الخاص بالعنصر لكنها صالحة عندما تكون فارغة. تُعد العناصر التي تتجاوز قيمتها المجال المحدد بقيمتي الخاصيتين min و max غير صالحة كما يستهدفها أيضًا الصنف out-of-range:. سنتعرف لاحقًا على طرق أخرى لجعل العنصر مستهدفًا من قبل الصنفين valid: و invalid: لكننا سنُبقي اﻷمر بسيطًا في هذا المقال. لنلق نظرة اﻵن على مثال يستخدم الصنفين الزائفين السابقين. وكما فعلنا في المثال السابق نستخدم عنصر <span> إضافي لتوليد المحتوى الذي يُستخدم للإشارة إن كان المحتوى صالحًا أم لا: <div> <label for="fname">First name: </label> <input id="fname" name="fname" type="text" required /> <span></span> </div> نستخدم شيفرة CSS التالية لعرض هذه المؤشرات: input + span { position: relative; } input + span::before { position: absolute; right: -20px; top: 5px; } input:invalid { border: 2px solid red; } input:invalid + span::before { content: "✖"; color: red; } input:valid + span::before { content: "✓"; color: green; } ضبطنا موقع العنصر <span> ليكون نسبيًا position: relative كي نتمكن من توليد محتوى توضُّعه مطلق position: absolute بالنسبة للعنصر السابق. سيكون المحتوى إشارة ❌ أو ✅ ﻹظهار حالة طارئة يجدر متابعتها، كما أظهرنا إطارًا أحمر سميك ﻹظهار أن البيانات المدخلة غير صحيحة. ملاحظة: استخدمنا before: ﻹضافة هذه العناوين كما استخدمنا after: لوضع العنوان "required مطلوب". See the Pen valid-invalid by Hsoub Academy (@HsoubAcademy) on CodePen. لاحظ كيف تكون عناصر اﻹدخال المطلوبة غير صالحة عندما تكون فارغة لكنها صالحة عندما ندخل فيها شيئًا ما. أما البريد اﻹلكتروني فهو صالح طالما أنه فارغ ويصبح غير صالح عندما لا تدخل عنوان بريد إلكتروني صحيح. بيانات داخل وخارج مجال محدد يستهدف الصنفان in-range: و out-of-range: عناصر اﻹدخال العددية التي تقبل قيمًا ضمن مجال محدد تضبطه الخاصيتين min و max وذلك إن كانت القيمة المدخلة ضمن هذا المجال أو خارجه. ملاحظة: عناصر اﻹدخال العددية هي العناصر date و month و week و time و datetime-local و number و range. من الجدير ملاحظة أن القيم التي تقع ضمن المجال تستهدف من قبل الصنف valid: وإن كانت خارج المجال ستسهدف من قبل الصنف invalid:. هذا اﻷمر تحديدًا من الأمور الدلالية التي نهتم فيها بمدلول استخدام الصنف. فالصنف out-of-range: هو نوع ذو دلالة خاصة من أنواع عدم الصلاحية invalidation، فمن اﻷفضل في هذه الحالات إبلاغ المستخدم أنه أدخل قيمة خارج المجال بدلًا من أن تقول له أن القيمة غير صالحة وحسب، وقد ترغب في اﻹشارة إلى الحالتين معًا. لنلق نظرة على مثالنا عن القيم خارج المجال والذي بني على أساس المثال السابق لعرض رسالة خارج المجال للمدخلات العددية واﻹشارة إلى أن هذه القيمة مطلوبة أو لا. <div> <label for="age">Age (must be 12+): </label> <input id="age" name="age" type="number" min="12" max="120" required /> <span></span> </div> وستبدو تنسيقات CSS كالتالي: input + span { position: relative; } input + span::after { font-size: 0.7rem; position: absolute; padding: 5px 10px; top: -26px; } input:required + span::after { color: white; background-color: black; content: "Required"; left: -70px; } input:out-of-range + span::after { color: white; background-color: red; width: 155px; content: "Outside allowable value range"; left: -182px; } نطبق هنا ما فعلناه سابقًا مع مثال required: مع اختلاف واحد هو أننا فصلنا هنا التصريحات التي تُطبق على أي محتوى مولّد بعد العنصر after:: ضمن قاعدة خاصة، وأعطينا كل من الحالتين required: و out-of-range: محتوىً وتنسيقًا خاصًا بها: See the Pen valid-invalid by Hsoub Academy (@HsoubAcademy) on CodePen. فمن الممكن أن يكون الرقم المدخل مطلوبًا وخارج المجال في الوقت ذاته، فما الذي سيحدث عندها؟ طالما أن القاعدة out-of-range: تظهر بعد required: في الشيفرة المصدرية، ووفقًا لقواعد CSS، ستستخدم أولًا وتظهر رسالة out-of-range. يعمل هذا التنسيق جيدًا في الواقع. فعندما تحمل الصفحة للمرة الأولى ستظهر رسالة "required مطلوب" مع إشارة ❌ وحواف حمراء لعنصر اﻹدخال، وعندما تدخل رقمًا ضمن المجال المسموح (12-120) تصبح القيم المدخلة صالحة، وإلا ستظهر الرسالة "Outside allowable value range خارج المجال المسموح" بدلًا من "required". ملاحظة: عليك النقر على عنصر التحكم وكتابة الرقم ضمنه لتجرب ما ذكرناه، ولن يسمح لك زر الزيادة واﻹنقاص الذي يظهر بتجاوز الحدود المسموحة. تنسيق عناصر اﻹدخال المعطلّة والمفعلة وعناصر القراءة فقط والكتابة فقط العناصر المفعلة هي العناصر التي يمكن اختيارها أو نقرها أو الكتابة ضمنها، ويكون العنصر معطلًا إن لم يتمكن المستخدم من التفاعل معه بأي شكل، ولن ترسل بياناته إلى الخادم. يمكن استهداف العناصر وفقًا لتفعيلها من حلال الصنفين الزائفين enabled: و disabled:، لكن لماذا قد نستهدف عنصرًا معطلّا؟ إن لم تكن بيانات محددة ضرورية لبعض المستخدمين، فلن ترغب بإرسالها إلى الخادم عند إرسال الاستمارة. من اﻷمثلة على هذه الحالة استمارة شحن البضائع فقد يطلب منك وضع عنوان الدفع وعنوان وجهة البضاعة، وعندما يكون العنوان نفسه في الحالتين، عندها يمكن تعطيل حقل عنوان الدفع مثلًا. لنلق نظرة على مثال بسيط يوضح اﻷمر. ويضم المثل شيفرة HTML بسيطة تضم عناصر إدخال نصية وصندوق تحقق لتعطيل أو تفعيل عنوان الدفع الذي يكون معطلًا افتراضيًا: <form> <fieldset id="shipping"> <legend>Shipping address</legend> <div> <label for="name1">Name: </label> <input id="name1" name="name1" type="text" required /> </div> <div> <label for="address1">Address: </label> <input id="address1" name="address1" type="text" required /> </div> <div> <label for="pcode1">Zip/postal code: </label> <input id="pcode1" name="pcode1" type="text" required /> </div> </fieldset> <fieldset id="billing"> <legend>Billing address</legend> <div> <label for="billing-checkbox">Same as shipping address:</label> <input type="checkbox" id="billing-checkbox" checked /> </div> <div> <label for="name" class="billing-label disabled-label">Name: </label> <input id="name" name="name" type="text" disabled required /> </div> <div> <label for="address2" class="billing-label disabled-label"> Address: </label> <input id="address2" name="address2" type="text" disabled required /> </div> <div> <label for="pcode2" class="billing-label disabled-label"> Zip/postal code: </label> <input id="pcode2" name="pcode2" type="text" disabled required /> </div> </fieldset> <div><button>Submit</button></div> </form> أما الجزء المتعلق بموضوعنا في شيفرة CSS هو التالي: input[type="text"]:disabled { background: #eee; border: 1px solid #ccc; } .disabled-label { color: #aaa; } اخترنا مباشرة عناصر اﻹدخال النصي المعطلة باستخدام input[type="text"]:disabled، لكننا نريد أيضًا إظهار العناوين المرتبطة بها بلون رمادي. وطالما أن استهدافها ليس سهلًا، استخدمنا صنفًا يطبق عليها disabled-label. كي تُستهدف. استخدمنا أخيرًا جافا سكريبت للتنقل بين حالتي التعطيل والتفعيل لحقول عنوان الدفع: // انتظر حتى تنهي الصفحة تحميلها document.addEventListener( "DOMContentLoaded", () => { // إلى صندوق التحقق `change` إضافة مترصد الحدث document .getElementById("billing-checkbox") .addEventListener("change", toggleBilling); }, false, ); function toggleBilling() { // اختيار حقول عنوان الدفع const billingItems = document.querySelectorAll('#billing input[type="text"]'); // اختيار العناوين المرتبطة بحقول عنوان الدفع const billingLabels = document.querySelectorAll(".billing-label"); // تغيير حالة التعطيل والتفعيل للحقول والعناوين for (let i = 0; i < billingItems.length; i++) { billingItems[i].disabled = !billingItems[i].disabled; if ( billingLabels[i].getAttribute("class") === "billing-label disabled-label" ) { billingLabels[i].setAttribute("class", "billing-label"); } else { billingLabels[i].setAttribute("class", "billing-label disabled-label"); } } } تستخدم شيفرة جافا سكريبت مترصد للحدث change كي نسمح للمستخدم تعطيل أو تفعيل حقول عنوان الدفع وتغيير التنسيق العناوين المرتبطة بها في كل حالة. See the Pen enabled-disabled-shipping by Hsoub Academy (@HsoubAcademy) on CodePen. استخدام الصنفين read-only: و read-write: يستخدم الصنفان السابقان لاستهداف عناصر اﻹدخال وفقًا ﻹمكانية الكتابة والقراءة. فعناصر اﻹدخال المخصصة للقراءة فقط، سترسل محتواها إلى الخادم لكن المستخدم لن يتمكن من تعديلها. بينما يمكن للمستخدم تعديل محتوى العنصر في العناصر القابلة للقراءة والكتابة معًا. يمكن ضبط عنصر اﻹدخال ليكون مخصصًا للقراءة فقط باستخدام الخاصية readonly. وكمثال على ذلك، نفترض أن المطور قد صمم صفحة للتحقق من صحة جميع البيانات المدخلة. حيث تُنقل إليها جميع البيانات التي قدمها المستخدم حتى يتحقق منها دفعة واحدة ويؤكد الطلب قبل إرسالها إلى الخادم دفعة واحدة. لنلق نظرة اﻵن على الشكل التي قد تكون عليه الاستمارة، إليك جزءًا من شيفرة HTML يوضح استخدام الخاصية readonly: <div> <label for="name">Name: </label> <input id="name" name="name" type="text" value="Mr Soft" readonly /> </div> إن جربت المثال مباشرة قد تلاحظ أن عناصر الاستمارة العليا لا يمكن أن تتلقى تركيز الدخل، مع ذلك تُرسل قيمها عند تسليم النموذج. كما نسقنا النموذج بالاعتماد على الصنفين read-only: و read-write: input:read-only, textarea:read-only { border: 0; box-shadow: none; background-color: white; } textarea:read-write { box-shadow: inset 1px 1px 3px #ccc; border-radius: 5px; } يبدو المثال بشكله المكتمل كالتالي: See the Pen readonly-confirmation by Hsoub Academy (@HsoubAcademy) on CodePen. ملاحظة: قد لا تستخدم الصنفين enabled: و read-write: إلا نادرًا كون الحالتين الموافقتين لهما (التفعيل والقراءة والكتابة) هي الحالات الافتراضية للعناصر. تنسيق صناديق التحقق وأزرار الاختيار المتعدد وفق حالاتها يمكن لصناديق التحقق وأزرار الاختيار المتعدد أن تكون مفعلة أو غير مفعلة أو معطلة، لكن هنالك عدة حالات يمكن النظر إليها: default:: وهو صنف يستهدف الحالة التي يكون فيها العنصر مفعلًا افتراضيًا عند تحميل الصفحة أي استُخدمت ضمنه الخاصية checked. وتبقى هذه العناصر مستهدفة حتى لو ألغى المستخدم تفعيلها. indeterminate: إن لم يكن صندوق التحقق أو زر الاختيار مفعلًا أو غير مفعل، سنتمكن من استهدافه باستخدام هذا الصنف الزائف. الصنف checked: عندما يفعّل عنصر التحقق أو زر الاختيار سيستهدفه الصنف checked:. يشيع استخدام هذا الصنف عندما تُفعّل هذه العناصر، وخاصة عندما تزيل التنسيق الافتراضي باستخدام appearance:none وتريد تنسيقه بنفسك، وقد رأينا أمثلة في المقال السابق عن هذا الاستخدام. وإذا عدنا إلى مثال تنسيق أزرار الاختيار المتعدد في مقال "التنسيق المتقدم لاستمارات الويب"، سنرى أننا استخدمنا الشيفرة التالية في التنسيق: input[type="radio"]::before { display: block; content: " "; width: 10px; height: 10px; border-radius: 6px; background-color: red; font-size: 1.2em; transform: translate(3px, 3px) scale(0); transform-origin: center; transition: all 0.3s ease-in; } input[type="radio"]:checked::before { transform: translate(3px, 3px) scale(1); transition: all 0.3s cubic-bezier(0.25, 0.25, 0.56, 2); } بإمكانك تجربتها في اﻹطار التالي: See the Pen radios-styled by Hsoub Academy (@HsoubAcademy) on CodePen. لقد نسقنا في تلك الشيفرة مظهر الدائرة الداخلية التي تظهر ضمن أزرار الاختيار المتعدد باستخدام العنصر الزائف before:: ثم طبقنا تحويلًا باستخدام الخاصية transform قيمته (scale(0. ومن ثم طبقنا انتقالًا transition لتوليد محتوى متحرك ضمن الزر عندما يُفعَّل أو يُلغى تفعيله. إن ميزة استخدام التحويل بدلًا من تطبيق انتقال على الارتفاع والاتساع هو إمكانية استخدام الخاصية transform-origin حتى يبدأ الرسم المتحرك من مركز الدائرة ثم ينمو وليس من طرف الدائرة ثم يقفز بعدها إلى المركز. أما الارتداد الخفيف للدائرة نحو المركز فكان باستخدام تابع بيزيه التكعيبي. الصنفان default: و indeterminate: يستهدف الصنف default: عناصر التحقق وأزرار الاختيار المتعدد في الحالة التي تكون فيها مفعلةً افتراضيًا عند تحميل الصفحة حتى لو نقرها المستخدم بعد ذلك. وقد يكون استخدامها مفيدًا في الحالات التي نريد فيها اﻹشارة إلى الخيارات المفعلة افتراضيًا كي يتذكرها المستخدم في حال أراد أن يعيد العناصر إلى وضعها الأصلي. كما يستهدف الصنف الزائف indeterminate: تلك العناصر عندما تكون غير محددة الحالة أو عائمة أي ليست مفعلة وليست غير مفعلة، ونصادف هذه الحالة عندما: عندما تكون جميع أزرار التحقق في نفس المجموعة غير مفعلّة. عندما تُضبط الخاصية indeterminate لصندوق التحقق على القيمة true. العناصر <progress> التي ليس لها قيمة. قد لا تستخدم هذا الصنف كثيرًا، وأكثر الحالات الواضحة الاستخدام هي تذكير المستخدم بضرورة النقر على أحد خيارات قائمة قبل الانتقال إلى مكان آخر. سنلقي نظرة اﻵن على نسختين مختلفتين من المثال السابق، اﻷولى تذكر المستخدم ما هي الخيارات التي كانت مفعلة افتراضيًا والثانية ننسق فيها عناوين أزرار الاختيار المتعدد عندما تكون في الحالة غير المحددة. ولكلتا النسختين شيفرة HTML التالية: <p> <input type="radio" name="fruit" value="cherry" id="cherry" /> <label for="cherry">Cherry</label> <span></span> </p> أضفنا في النسخة اﻷولى الخاصية checked إلى زر الاختيار الأوسط الذي سيكون مختارًا بشكل افتراضي عند تحميل الصفحة، وننسقه كالتالي: input ~ span { position: relative; } input:default ~ span::after { font-size: 0.7rem; position: absolute; content: "Default"; color: white; background-color: black; padding: 5px 10px; right: -65px; top: -3px; } تولد هذه الشيفرة محتوى نصي عنوانه "Default" إلى جوار العنصر الذي كان مختارًا افتراضيًا عند تحميل الصفحة. لاحظ كيف استخدمنا محدد التجميع (~) الذي يستهدف الأشقاء بدلًا من محدد التجميع (+) الذي يستهدف أشقاء متجاورين، ذلك أن العنصر <span> لا يأتي بالضرورة بعد العنصر <input> مباشرة في ترتيب الشيفرة المصدرية. إليك نتيجة الشيفرة: See the Pen radios-checked-default by Hsoub Academy (@HsoubAcademy) on CodePen. لم نختر زر افتراضي في النسخة الثانية من المثال، لأننا لن نحصل هكذا على حالة عائمة أو غير محددة للأزرار يمكن استهدافها بتنسيق معين. إليك شيفرة CSS المستخدمة في التنسيق: input[type="radio"]:indeterminate { outline: 2px solid red; animation: 0.4s linear infinite alternate outline-pulse; } @keyframes outline-pulse { from { outline: 2px solid red; } to { outline: 6px solid red; } } تعطي الشيفرة السابقة أسلوبًا رسوميًا لطيفًا للإشارة أنك لم تختر أي زر من اﻷزار، على أمل أن تنتبه وتختار أحدها. إليك نتيجة هذا المثال: See the Pen radios-checked-indeterminate by Hsoub Academy (@HsoubAcademy) on CodePen. أصناف زائفة أخرى لنتحدث قليًلا عن بعض اﻷصناف التي لم نغطيها لكنها تستحق الوقت الذي قد تقضيه في الاطلاع عليها: focus-within:: يستهدف العنصر الذي يتلقى تركيز الدخل أو الذي يضم عنصرًا تلقى تركيز الدخل. ويستخدم مثلًا إن أردت اﻹشارة إلى استمارة تلقى أحد عناصرها تركيز الدخل. focus-visible:: يستهدف العناصر التي تلقت تركيز الدخل عبر لوحة المفاتيح (بدلًا من النقر بالفأرة)، ولهذا الصنف فائدته إن أردت التمييز بين الحالتين. placeholder_shown:: ويستهدف العناصر التي تعرض حدود موضعها المؤقت placeholder وذلك عندما تستخدم الخاصية placeholder. وهذه الأصناف مميزة أيضًا لكنها غير مدعومة جيدًا من قبل المتصفحات: blank:: يستهدف عناصر التحكم الفارغة (التي لا تضم محتوى). empty:: يستهدف العناصر التي لا تضم أبناءً وبالتالي تستهدف أيضًا العناصر الخالية void elements التي لا يمكن أن تضم أبناء أصلًا مثل عناصر <input> و <hr>. وتعد أكثر دعمًا من سابقتها، لأن blank: صنف لا يزال في فترة إعداد التوصيفات الخاصة به ولم يُدعم بعد. user-invaild:: سيكون مشابهًا -عندما يُدعم- للصنف invaild: لكنه سيقدم تجربة مستخدم أفضل. فعندما تكون القيمة المدخلة صالحة عندما يتلقى العنصر التركيز، قد يستهدفه الصنف invaild: وكذلك عندما يدخل المستخدم قيمة غير صالحة مؤقتًا. بينما سيستهدف الصنف user-invaild: العنصر إن كانت القيمة المدخلة غير صالحة وفقد العنصر تركيز الدخل. وإذا كانت القيمة غير صالحة أصلًا، يستهدف كلا الصنفين العنصر طالما أنه يحتفظ بتركيز الدخل. وسيتوقف استهداف العنصر من كلا الصنفين عندما تصبح القيمة صالحة. الخلاصة لقد أنهينا في هذا المقال اﻹطلاع على اﻷصناف الزائفة التي تستهدف عناصر واجهة المستخدم وخاصة الاستمارات. يبقى عليك متابعة التجربة والعمل على هذه اﻷصناف لتألف طريقة استهدافها للعناصر وبالتالي تطبيق التنسيق المطلوب عليها. ترجمة -وبتصرف- للمقال: UI pseudo-classes اقرأ أيضًا المقال السابق: التنسيق المتقدم لاستمارات الويب محددات الأصناف الزائفة pseudo-classes والعناصر الزائفة pseudo-elements في CSS 5 أصناف زائفة (pseudo-class) يجب عليك معرفتها في CSS أنواع محددات التنسيق في CSS استخدام النماذج في HTML5 مع الأصناف الزّائفة (pseudo-classes) في CSS
  13. بدأنا في مقال سابق التعرف على استمارات الويب web forms وطريقة بنائها باستخدام عناصر <HTML>، وسنتعلم في هذا المقال كيفية تنسيق هذه الاستمارات بصورة احترافية باستخدام CSS. ننصحك قبل المتابعة في قراءة هذا المقال الاطلاع على أساسيات HTML وعلى أساسيات تنسيق الصفحات باستخدام CSS. تحديات تنسيق عناصر تحكم الاستمارة أُدخلت عناصر تحكم الاستمارات في مواصفات HTML2 عام 1995، لكن لم تظهر لغة CSS حتى أواخر عام 1996، ولم تكن مدعومة من معظم المتصفحات حتى السنوات اللاحقة. لهذا السبب، اعتمدت المتصفحات على أنظمة التشغيل لعرض أو تصيير render عناصر التحكم. وحتى بعد ظهور CSS، تجاهل منتجو المتصفحات في البداية دعم تنسيق عناصر التحكم، لأن المستخدمين اعتادوا على مظهرها ضمن متصفحاتهم. أما حاليًا فقد تغير اﻷمر، ومعظم عناصر التحكم قابلة للتنسيق مع بعض الاستثناءات. أنواع عناصر التحكم تُصتف عناصر التحكم وفقًا لسهولة تنسيقها إلى: عناصر سهلة التنسيق نذكر منها: عنصر الاستمارة <form> عناصر تجميع الحقول <fieldset> و <legend> عناصر اﻹدخال وحيدة السطر <input> مثل text و url و email (ماعدا search). حقل الإدخال متعدد الأسطر <textarea> اﻷزرار الفعلية <button> وأزرار الإدخال <input>. عنصر التسمية <label> عنصر الإخراج <output> عناصر صعبة التنسيق صناديق التحقق checkboxes وأزرار الاختيار من متعدد radio buttons. عنصر اﻹدخال <input> من النوع search. سنعرض طريقة تنسيقها في مقال لاحق. عناصر لها تنسيق داخلي ولا يمكن تنسيقها باستخدام CSS وحدها حقل اﻹدخال من النوع color. حقل اﻹدخال من النوع datetime. حقل اﻹدخال من النوع range. عنصر اﻹدخال من النوع file. عناصر إنجاز قوائم منسدلة مثل <select> و <option> و <optgroup>. عنصر إنجاز أشرطة التقدم مثل <meter> و <progress>. فعنصر اختيار التاريخ أو الزر الذي يعرض قائمة العناصر ضمن العنصر <select> لا يمكن تنسيقهما باستخدام CSS لوحدها. سنرى لاحقًا في مقال قادم طريقة تنسيق هذه العناصر. ملاحظة: يمكن لبعض عناصر التنسيق الزائفة في CSS تنسيق مثل هذه العناصر، نذكر منها moz-range-track-:: لكنها غير مدعومة في جميع المتصفحات لهذا لا يمكن الاعتماد عليها. تنسيق عناصر تحكم استمارة بسيطة باﻹمكان الاستفادة من تقنيات التنسيق التي عرضناها في المقالين "تنسيق استمارة ويب بسيطة" و "أساسيات لغة CSS" في تنسيق العناصر سهلة التنسيق، كما ستجد محددات تنسيق selectors مخصصة تُدعى أصناف واجهة المستخدم الزائفة UI pseudo classes تمكنك من تنسيق العناصر وفقًا للحالة الراهنة لواجهة المستخدم. سنعمل على مثال تطبيقي خلال بقية أقسام المقال، لكن سنناقش أولًا بعض جوانب تنسيق الاستمارات التي يُفضل الاطلاع عليها. النصوص والخطوط يمكن تنسيق النصوص والخطوط بسهولة في عناصر التحكم ويمكنك أيضًا استخدام font-face@. لكن سلوك المتصفحات ليس متماثلًا. فلا ترث بعض عناصر التحكم الخاصية font-family و font-size افتراضيًا من العنصر اﻷب، وتستخدم بعض المتصفحات المظهر الافتراضي لنظام التشغيل. لهذا، ولكي تجعل الاستمارة متناسقة مع باقي المحتوى، بإمكانك إضافة القواعد التالية ضمن ورقة التنسيق: button, input, select, textarea { font-family: inherit; font-size: 100%; } تضبط القيمة inherit القيمة المحسوبة للخاصية لتعادل قيمة نفس الخاصية للعنصر اﻷب، أي ترث قيمة الخاصية من العنصر اﻷب. توضح لقطة الشاشة التالية الفرق. على اليسار المظهر الافتراضي للعناصر <"input type = "text> و <"input type = "date> و <select> و <textarea> و <button> و <input "type = "submit> في متصفح كروم على ماك أو إس وفقًا للتنسيق الافتراضي للنظام، وعلى اليمين نفس العناصر وقد طبقنا عليها قواعد التنسيق السابقة: يختلف التنسيق الافتراضي من نواحٍ عدة، فالوراثة تجعل الخط المستخدم هو نفسه خط العنصر اﻷب. وفي حالتنا كان الخط الافتراضي serif للعنصر اﻷب، وقد طُبقت القاعدة على جميع العناصر ما عدا استثناء وحيد هو العنصر <"input type = "submit> الذي لم يرث تنسيق العنصر اﻷب في متصفح كروم. وقد استخدم المتصفح بدلًا من ذلك القاعدة font-family: system-ui، لهذا من اﻷفضل استخدام العنصر <button> بدلًا من عنصر اﻹدخال <input> المكافئ. لا يزال الجدل قائمًا بين أفضلية ترك تنسيق النماذج مطابقًا للتنسيق الافتراضي للنظام، أو تنسيقها بما يلائم المحتوى، والقرار يعود لك دائمًا. تحديد أبعاد الصندوق تدعم العناصر النصية جميع الخاصيات التعلقة بنموذج الصندوق في CSS مثل الاتساع width والارتفاع height والحاشية padding والهوامش margin والحواف border. وكما ذكرنا ، يعتمد المتصفح على نظام التشغيل في تنسيق عناصر التحكم وعرضها، ويعود اﻷمر إليك في اختيار ما يناسب المحتوى في صفحتك. وإن قررت أن تستخدم نظام التنسيق الافتراضي، فقد تواجه بعض المشاكل عندما تحاول جعل أبعاد العناصر متناسقةً، وذلك لأن لكل عنصر تحكم قواعد خاصة في ضبط الحواف والحاشية والهوامش. وﻹعطاء نفس اﻷبعاد لجميع عناصر التحكم، استخدم الخاصية box-sizing مع بعض القيم المتناسقة لبقية الخاصيات: input, textarea, select, button { width: 150px; padding: 0; margin: 0; box-sizing: border-box; } تعرض لقطة الشاشة التالية التصيير الافتراضي للعناصر:<"input type="radio> و <"input type="checkbox> و <"input type="range> و <"input type="text> و <"inputtype="date"> و <select> و <textarea> و <"input type="submit> و <button> على يسار الشاشة، ويعرض نفس العناصر على يمين الشاشة بعد تطبيق قواعد التنسيق السابقة عليها. لاحظ كيف تبدو العناصر إلى اليمين وكأنها تشغل نفس المساحة بغض النظر عن قواعد التنسيق الافتراضية لنظام التشغيل: ما قد لا يكون واضحًا في اللقطة السابقة هو زر الاختيار المتعدد وصندوق التحقق إذا يبدوان متشابهين في كلتا الحالتين، لكنهما يتمركزان في وسط المساحة التي يشغلانها والتي تحددها قيمة الخاصية width(هنا 150 بكسل). قد لا توضِّع متصفحات أخرى هذه العناصر في المنتصف لكنها تتقيد بالمساحة المحددة. إزاحة العلامات Legends باﻹمكان تنسيق عنصر العلامة <legend>، لكن من الصعب إزاحته بالنسبة لموضعه اﻷصلي. يتوضع العنصر تلقائيًا قرب الزاوية اليسارية العليا للعنصر اﻷب، ولوضعه في مكان آخر، كأن يكون ضمن عنصر <fieldset> أو قرب الزاوية اليسارية السفلى، عليك أن تعتمد في هذه الحالة على خاصيات التوضّع positioning. إليك مثالًا: See the Pen styling-web-forms by Hsoub Academy (@HsoubAcademy) on CodePen. لكي نضع العلامة في المكان الذي تعرضه الشيفرة السابقة، استخدمنا قواعد التنسيق التالية: fieldset { position: relative; } legend { position: absolute; bottom: 0; right: 0; } ينبغي أيضًا تحديد موضع العنصر <fieldset> أيضًا وبالتالي سيُحدد موضع العلامة بالنسبة إلى هذا العنصر. وإن لم نفعل ذلك سيوضِّع المتصفح العلامة بالنسبة إلى العنصر <body>. وللعنصر <legend> أهمية كبيرة عند أخذ سهولة الوصول بعين الاعتبار، إذ ستقرأ التقنيات المساعدة محتوى العلامة كجزء من عنوان كل عنصر تحكم ضمن <fieldset>. لن يغير التنسيق السابق شيئًا من ناحية سهولة الوصول، إذ ستُقرأ العلامة بنفس الطريقة. ملاحظة: بإمكانك أيضًا استخدام الخاصية transform لتساعدك في تحديد موضع العنصر <legend>. فإن استخدمت القاعدة ;()transform: translateY مثلًا، ستتحرك العلامة فعلًا لكنها ستترك خلفها فراغًا مزعجًا لا يسهل التخلص منه ضمن العنصر <fieldset>. تمرين تطبيقي: تنسيق مخصص لاستمارة ويب لنلق نظرة اﻵن على تمرين أساسي يعرض تنسيق استمارات HTML، نبني فيه استمارة اتصال لها شكل بطاقة بريدية جميلة المظهر. بإمكانك الاطلاع على النسخة الكاملة من التمرين على جت-هاب. إما إن أردت العمل معنا خطوة خطوة، لا بد من إنشاء نسخة محلية من ملف التمرين على جهازك ومتابعة اﻹرشادات في الفقرات القادمة. شيفرة HTML إن الشيفرة المستخدمة أوسع من الشيفرة التي عملنا معها في مقال " إضافة تنسيق بسيط لاستمارة الويب"، إذ تضم ترويسة وبعض المعرّفات: <form> <h1>to: Mozilla</h1> <div id="from"> <label for="name">from:</label> <input type="text" id="name" name="user_name" /> </div> <div id="reply"> <label for="mail">reply:</label> <input type="email" id="mail" name="user_email" /> </div> <div id="message"> <label for="msg">Your message:</label> <textarea id="msg" name="user_message"></textarea> </div> <div class="button"> <button type="submit">Send your message</button> </div> </form> ضع الشيفرة السابقة ضمن العنصر<body> لملف HTML. تنظيم الأصول قبل أن نبدأ كتابة الشيفرة نحتاج إلى المواد المساعدة التالية: خلفية للبطاقة البريدية، نزّل صورة الخلفية وضعها في نفس المجلد الذي يضم ملف HTML. خط مميز للكتابة مثل "Mom's Typewriter" من موقع "dafont.com"، نزّل الملف ذو الامتداد TTf. في نفس المجلد السابق. خط يماثل كتابة اليد مثل "Journal" من موقع dafont نزّل الملف ذو الامتداد TTf. في نفس المجلد السابق. وعليك معالجة ملفات الخطوط قليلًا قبل أن تستخدمها: انتقل إلى تطبيق Webfont Generator على موقع fontsquirrel حمّل ملفي الخطوط من خلال الاستمارة الموجودة في التطبيق لتوليد خط ويب، ونزّل الملفين الناتجين على حاسوبك. استخرج محتويات الملفين. ضمن المجلد الناتج عن الاستخراج ستجد بعض ملفات الخطوط مثل woff. و woff2. (قد تختلف هذه الملفات مستقبلًا). انقل الملفات إلى مجلد جديد باسم fonts ضمن المجلد الذي يضم شيفرة HTML. ونستخدم ملفي خط لكل من عائلتي الخطين السابقين لتعزيز التوافقية مع متصفحات مختلفة. بإمكانك الاطلاع على مقال استخدام خطوط الكتابة في الويب مع CSS لمزيد من التفاصيل. شيفرة CSS ضع الشيفرة الموجودة في اﻷسفل داخل العنصر <style> في ملف HTML. التخطيط الإجمالي عرّفنا بداية القاعدة font-face@ وأعددنا التنسيقات الأساسية للعنصر <body> ولعناصر الاستمارة <form>. إن كانت الملفات الناتجة عن تطبيق تحويل الخطوط مختلفة عما شرحناه، ستجد كتلة القاعدة font-face@ ضمن الملف المضغوط الذي نزّلته. استبدل القاعدة font-face@ الموجودة في الملف stylesheet.css بالكتلة الموجودة في الملف المضغوط، وانتبه لتحديث المسارات لتطابق المسار الذي وضعت فيه التمرين. @font-face { font-family: "handwriting"; src: url("fonts/journal-webfont.woff2") format("woff2"), url("fonts/journal-webfont.woff") format("woff"); font-weight: normal; font-style: normal; } @font-face { font-family: "typewriter"; src: url("fonts/momot___-webfont.woff2") format("woff2"), url("fonts/momot___-webfont.woff") format("woff"); font-weight: normal; font-style: normal; } body { font: 1.3rem sans-serif; padding: 0.5em; margin: 0; background: #222; } form { position: relative; width: 740px; height: 498px; margin: 0 auto; padding: 1em; box-sizing: border-box; background: #fff url(background.jpg); /* we create our grid */ display: grid; grid-gap: 20px; grid-template-columns: repeat(2, 1fr); grid-template-rows: 10em 1em 1em 1em; } لاحظ كيف استخدمنا تخطيط الشبكة CSS Grid وتخطيط الصندوق المرن flexbox لتنظيم الاستمارة، وسيسهل ذلك وضع جميع العناصر بما في ذلك العنوان الرئيسي ضمن الاستمارة: h1 { font: 1em "typewriter", monospace; align-self: end; } #message { grid-row: 1 / 5; } #from, #reply { display: flex; } العناوين وعناصر التحكم يمكننا اﻵن تنسيق عناصر الاستمارة. ونتأكد بداية أن العناوين <label> لها نفس خط الكتابة المطلوب: label { font: 0.8em "typewriter", sans-serif; } يحتاج العنصر <fieldset> إلى بعض التنسيقات التي تطبق على جميع عناصره، لهذا أزلنا التنسيقات المتعلقة بالحواف bordersوالخلفية background وأعدنا تضبط الخاصيتين padding و margin: input, textarea { font: 1.4em/1.5em "handwriting", cursive, sans-serif; border: none; padding: 0 10px; margin: 0; width: 80%; background: none; } عندما يكتسب أحد هذه العناصر تركيز الدخل، نظلل خلفيته باللون الرمادي الفاتح مع إكسابها بعض الشفافية، فمن المهم أن تعطي تنسيقًا مميزًا للعنصر عندما يكتسب تركيز الدخل لسهولة الاستخدام وسهولة الوصول إليه عبر لوحة المفاتيح: input:focus, textarea:focus { background: rgb(0 0 0 / 10%); border-radius: 5px; } بعد اكتمال تنسيق الحقول النصية، لا بد من تعديل طريقة عرض النصوص أحادية ومتعددة اﻷسطر، لأنها لن تظهر متشابهة في التنسيق الافتراضي. تغييرات على المربع النصي متعدد الأسطر textarea تعرض العناصر <textarea> والتي تمثل مربعًا نصيًا مكونًا من عدة أسطر على شكل عناصر كتلية سطرية inline-block افتراضيًا . وما يهمنا من خاصيات تنسيق في هذا المضمار هما الخاصيتان resize و overflow. وطالما أن تصميمنا للاستمارة ثابت الحجم، يمكننا استخدام الخاصية resize لمنع المستخدم من تغيير أبعاد المربع النصي، لكن من اﻷفضل ألا نفعل ذلك فقد يرغب المستخدم أن يغير الحجم. وتُستخدم الخاصية overflow للتحكم بالطريقة التي يتصرف فيها المتصفح عند زيادة كمية المحتوى عما يمكن للمساحة المخصصة استيعابها. تستخدم بعض المتصفحات القيمة الافتراضية autoبينما تستخدم متصفحات أخرى القيمة scroll، لهذا من اﻷفضل أن تضبط هذه القيمة على auto لجميع الحقول النصية: textarea { display: block; padding: 10px; margin: 10px 0 0 -10px; width: 100%; height: 90%; border-right: 1px solid; /* resize : none; */ overflow: auto; } تنسيق زر التسليم submit يُعد الزر الفعلي <button> أكثر ملائمة للتنسيق باستخدام CSS مقارنةً باستخدام عناصر أخرى، إذ يمكنك تطبيق ما تريده من قواعد التنسيق بما في ذلك اﻷصناف الزائفة: button { padding: 5px; font: bold 0.6em sans-serif; border: 2px solid #333; border-radius: 5px; background: none; cursor: pointer; transform: rotate(-1.5deg); } button:after { content: " >>>"; } button:hover, button:focus { background: #000; color: #fff; } النتيجة النهائية ستبدو الاستمارة شبيهة بهذه البطاقة البريدية: ملاحظة: إن لم يعمل التمرين بالطريقة المتوقعة، وأردت التحقق من عملك، يمكنك تجربته مباشرة على جت-هاب والاطلاع أيضًا على شيفرته المصدرية. الخلاصة رأينا في هذا المقال سهولة تنسيق استمارات الويب التي تتضمن أزرارًا وحقولًا نصية فقط، وسنتابع في المقال القادم العمل مع العناصر صعبة التنسيق في هذه الاستمارات. ترجمة -وبتصرف- للمقال Styling web forms اقرأ أيضًا المقال السابق: تعرف على عناصر تحكم متنوعة يمكن إضافتها لاستمارات الويب الاستمارات (forms) في متصفح الويب وكيفية التعامل معها في جافاسكربت الدليل الموجز إلى تصميم موقع إلكتروني النماذج (Forms) في HTML5
  14. نلقي نظرة في هذا المقال وبشيء من التفصيل على عناصر استمارات ويب web forms متنوعة ليست عناصر <input>، إذ يمكنك أن تضيف للاستمارات عناصر أخرى مثل القوائم المنسدلة drop-down menus، وحقول نصية متعددة اﻷسطر، وعناصر خرج <output> (كالتي شرحناها في المقال السابق)، وأشرطة التقدم progress bars. ننصحك قبل المتابعة في قراءة هذا المقال أن تطلع على سلسلة المقالات "مدخل إلى HTML" للتعرف على هيكلية صفحات ويب باستخدام لغة HTML وتتعرف على عناصرها. الحقول النصية متعددة الأسطر نستخدم في هذه الحالة العنصر <textarea> بدلًا من عنصر اﻹدخال <input> <textarea cols="30" rows="8"></textarea> ويصيّر العنصر كالتالي: See the Pen Untitled by Hsoub Academy (@HsoubAcademy) on CodePen. وللعنصر <textarea> وسم إغلاق أيضًا تضع قبله النص الذي تريده أن يظهر ضمن العنصر، بينما يُعد العنصر <input> فارغًا ليس له وسم إغلاق، وتوضع أية قيمة افتراضية لهذا العنصر ضمن الخاصية value. وعلى الرغم من إمكانية وضع أية محتوى ضمن العنصر <textarea> سواء عناصر HTML اخرى أو تنسيقات CSS أو جافا سكريبت نظرًا لطبيعة هذا العنصر إلا أنها تُصيَّر renders جميعها في النهاية على شكل نص. وتزودنا الخاصية contenteditable عند استخدامها مع العناصر التي لا تنتمي إلى استمارة بواجهة برمجية لتمييز شيفرة HTML أو المحتوى النصي المنسّق rich text الموجود ضمن نص صرف. يلتف النص المدخل ضمن هذا العنصر افتراضيًا (أي ينتقل من سطر إلى آخر تلقائيًا)، وباﻹمكان أيضًا تغيير حجم العنصر. إذ توفر المتصفحات الحديثة مقبضًا يُستخدم لتغيير حجم الصندوق <textarea> تكبيرًا أو تصغيرًا. تعرض لقطة الشاشة التالية العنصر <textarea> في الحالة الافتراضية وحالتي اكتساب تركيز الدخل والتعطيل في المتصفحين فايرفوكس 71 و سفاري 13 على ماك أو إس وكذلك إيدج 17 وياندكس 14 وفايرفوكس 71 و كروم 79 على ويندوز 10: ملاحظة: بإمكانك إلقاء نظرة على مثال أكثر أهمية يتعلق باستخدام <textarea> على جت-هاب والاطلاع على شيفرته المصدرية أيضًا. التحكم بتصيير النص في حالة اﻷسطر المتعددة يمكنك استخدام ثلاث خاصيات للتحكم بتصيير النص في العنصر <textarea> وهي: cols: وتحدد الاتساع المرئي للعنصر (عدد أعمدته) مقاسًا بالاتساع الوسطي للمحرف الواحد. وتمثل هذه القيمة عمليًا الاتساع الابتدائي، لأن العنصر قابل لزيادة الحجم أو تغيير تلك القيمة باستخدام CSS. القيمة الافتراضية لاتساع العنصر في حال لم تحدد يدويًا هي 20. rows: وتحدد عدد أسطر النص وكذلك الارتفاع الإبتدائي للعنصر، لأنه قابل لزيادة الحجم أو تغيير تلك القيمة باستخدام CSS. القيمة الافتراضية لهذه الخاصية هي 20. wrap: تحدد طريقة التفاف النص ضمن العنصر. القيمة الافتراضية لهذه الخاصية هي soft وفيها لا يُرسل محرف الانتقال إلى سطر جديد إلى الخادم لكنه يستخدم في عرض النص في المتصفح، كما يمكن أن تأخذ الخاصية القيمة hard، وفيها يرسل محرف الانتقال إلى سطر جديد إلى الخادم مع النص المُرسل (لا بد حينها من ضبط قيمة الخاصية cols)، وأخيرًا القيمة off وفيها لا يلتف النص أبدًا (لا ينتقل إلى سطر جديد عند بلوغ نهاية السطر). التحكم بطريقة تغيير أبعاد العنصر <textarea> تتحكم الخاصية resize في لغة CSS بإمكانية تغيير أبعاد العنصر، وتأخذ إحدى القيم التالية: both: تسمح بتغييرات أبعاد العنصر أفقيًا وشاقوليًا، وهي القيمة الافتراضية. horizontal: تسمح فقط بتغيير البعد اﻷفقي. vertical: تسمح فقط بتغيير البعد الشاقولي. none: تمنع تغيير أبعاد العنصر. block و inline: قيم تجريبية تسمح بتغيير اﻷبعاد وفق أحد اتجاهي الانسياب: الكتلي block أو السطري inline. راجع مقال "التحكم باتجاه انسياب النصوص باستخدام CSS" لمعلومات أكثر عن اتجاه انسياب العناصر في صفحة HTML. عنصر القائمة المنسدلة Dropdown List توفر القائمة المنسدلة طريقة بسيطة تتيح للمستخدم اختيار عنصر من بين عدة عناصر دون أن تشغل حيزًا كبيرًا من واجهة المستخدم. وتقدم لغة HTML طريقتين ﻹنشاء القوائم المنسدلة الاولى صندوق اختيار select box والثانية صندوق اﻹكمال التلقائي autocomplete box. ملاحظة: يمكنك إيجاد بعض اﻷمثلة التي تخص القوائم المنسدلة على جيت-هب وكذلك الاطلاع على شيفرتها المصدرية. صندوق الاختيار ننشئ صندوق الاختيار باستخدام العنصر <select>، ونضع ضمنه عنصر <option> أو أكثر كأبناء له ويمثل كل عنصر منها أحد القيم التي يمكن أن نختارها. إليك مثالًا بسيطًا: See the Pen select sample by Hsoub Academy (@HsoubAcademy) on CodePen. يمكن أن نستخدم الخاصية selected لاختيار عنصر افتراضيًا، وعندها سيُختار هذا العنصر تلقائيًا عند تحميل الصفحة. استخدام عنصر مجموعة الخيارات <optgroup> باﻹمكان وضع عناصر <option> داخل عنصر مجموعة الخيارات <optgroup> ﻹنشاء مجموعات مترابطة من القيم: <select id="groups" name="groups"> <optgroup label="fruits"> <option>Banana</option> <option selected>Cherry</option> <option>Lemon</option> </optgroup> <optgroup label="vegetables"> <option>Carrot</option> <option>Eggplant</option> <option>Potato</option> </optgroup> </select> See the Pen optgroup sample by Hsoub Academy (@HsoubAcademy) on CodePen. تظهر قيمة الخاصية label لعنصر مجموعة الخيارات <optgroup> قبل قيمة الخيارات الموجودة ضمنها، وعادة ما يفصل المتصفح قيمة تلك الخاصية عن بقية الخيارات (بجعل الخط أسمك ووفق مستوى إزاحة مختلف) وبذلك لن يختلط الأمر بين عنوان المجموعة وعناصرها. استخدام الخاصية value إن كان للخاصية value العائدة للعنصر <option> قيمة صريحة، ستُرسل هذه القيمة عند تسليم النموذج مع العنصر الذي اخترته. لكن إن أهملت استخدام هذه الخاصية كما في المثال السابق فإن قيمة هذه الخاصية ستكون محتوى العنصر <option>. وبالتالي لا حاجة لاستخدام الخاصية value ما لم ترد -لسبب وجيه- إرسال قيمة صغيرة لهذه الخاصية إلى الخادم تختلف عن المحتوى الذي يعرضه العنصر <option>: <select id="simple" name="simple"> <option value="banana">Big, beautiful yellow banana</option> <option value="cherry">Succulent, juicy cherry</option> <option value="lemon">Sharp, powerful lemon</option> </select> يكفي ارتفاع صندوق الاختيار لعرض قيمة مفردة، لهذا بإمكانك استخدام الخاصية size لتحديد عدد الخيارات التي تريد عرضها ضمن الصندوق في الحالة التي لا يمتلك فيها صندوق الاختيار تركيز الدخل. صندوق متعدد الخيارات يسمح صندوق الاختيار للمستخدم اختيار عنصر واحد افتراضيًا، لكن عند استخدام الخاصية multiple مع العنصر <select>، سيتمكن المستخدم من اختيار أكثر من عنصر. يختار المستخدم عدة قيم وفقًا للطريقة الافتراضية التي يتيحها نظام التشغيل، وكمثال على ذلك الضغط المستمر على الزر Cmd/Ctrl ثم النقر على الخيارات التي يريدها في معظم الحواسب المكتبية. <select id="multi" name="multi" multiple size="2"> <optgroup label="fruits"> <option>Banana</option> <option selected>Cherry</option> <option>Lemon</option> </optgroup> <optgroup label="vegetables"> <option>Carrot</option> <option>Eggplant</option> <option>Potato</option> </optgroup> </select> See the Pen Untitled by Hsoub Academy (@HsoubAcademy) on CodePen. ملاحظة: في الحالة التي يدعم فيها صندوق الاختيار عدة خيارات، لن يُعرض الصندوق على شكل قائمة منسدلة، بل ستعرض جميع الخيارات معًا ضمن قائمة نمطية تحدد ارتفاعها الخاصية size. ملاحظة: تدعم جميع المتصفحات الخاصية multiple إذا دعمت العنصر <select>. صندوق اﻹكمال التلقائي بإمكانك تزويد عنصر التحكم بقائمة من القيم المقترحة والتي تُكمَّل تلقائيًا من خلال العنصر <datalist> الذي يضم عناصر <option> أبناء تحدد القيم التي ستُعرض. ولا بد في هذه الحالة من ضبط قيمة الخاصية id للعنصر <datalist>. نربط بعدها العنصر <datalist> بعنصر التحكم <input> (من النوع النصي أو البريد اﻹلكتروني وغيره) باستخدام الخاصية list العائدة إلى عنصر التحكم، إذ نضبط قيمتها على قيمة id للعنصر <datalist> المطلوب. وبمجرد أن ننفذ ذلك، تُستخدم عناصر <datalist> ﻹكمال النص الذي يدخله المستخدم تلقائيًا، وتعرض القيم المقترحة نمطيًا على شكل قائمة منسدلة تضم الخيارات المطابقة للنص الذي يكتبه ضمن عنصر اﻹدخال. إليك مثالًا بسيطًا: <label for="myFruit">What's your favorite fruit?</label> <input type="text" name="myFruit" id="myFruit" list="mySuggestion" /> <datalist id="mySuggestion"> <option>Apple</option> <option>Banana</option> <option>Blackberry</option> <option>Blueberry</option> <option>Lemon</option> <option>Lychee</option> <option>Peach</option> <option>Pear</option> </datalist> See the Pen Autocomplete box by Hsoub Academy (@HsoubAcademy) on CodePen. دعم العنصر <datalist> في المتصفحات وأسلوب التراجع تدعم معظم المتصفحات العنصر <datalist>، لكن إن أردت أن تدعم متصفحات أقدم مثل إنترنت إكسبلورر 10 والنسخ اﻷقدم، إليك هذه الحيلة: <label for="myFruit">What is your favorite fruit? (With fallback)</label> <input type="text" id="myFruit" name="fruit" list="fruitList" /> <datalist id="fruitList"> <label for="suggestion">or pick a fruit</label> <select id="suggestion" name="altFruit"> <option>Apple</option> <option>Banana</option> <option>Blackberry</option> <option>Blueberry</option> <option>Lemon</option> <option>Lychee</option> <option>Peach</option> <option>Pear</option> </select> </datalist> See the Pen datalist by Hsoub Academy (@HsoubAcademy) on CodePen. تتجاهل المتصفحات التي تدعم <datalist> جميع العناصر الموجودة ضمن هذا العنصر ما عدا العناصر <option> وبالتالي ستعمل قائمة اﻹكمال التلقائي كما يجب. أما المتصفحات التي لا تدعم اﻹكمال التلقائي، فستعرض العنوان وصندوق الاختيار <select>. توضح لقطة الشاشة التراجع عن استخدام قائمة اﻹكمال التلقائي في المتصفح سفاري 6 الذي لا يدعمها: وفي حال استخدمت أسلوب التراجع السابق، تأكد أن جمع بيانات العنصرين <input> و <select> ستجري في طرف الخادم. استخدامات غير شائعة لقائمة اﻹكمال التلقائي تشير مواصفات HTML إلى إمكانية استخدام الخاصية list والعنصر <datalist>مع أي نوع من عناصر اﻹدخال، وهذا ما يجعل بعض الاستخدامات لقائمة اﻹكمال التلقائي غير شائعة. إذ يمكنك على سبيل المثال استخدام هذه القائمة مع عنصر اﻹدخال من النوع range لعرض علامة صغيرة فوق المزلاج توافق خيارًا من خيارات قائمة اﻹكمال التلقائي ومن المفترض أن تعرض المتصفحات التي تدعم العنصرين <datalist> و <input> من النوع color لوحة ألوان مخصصة (تتعلق بعناصر <datalist>) مع إمكانية عرض منتقي الألوان بشكل كامل. وبالتالي سيختلف سلوك المتصفحات في هذه الحالة، لهذا يُعد هذا الاستخدام متقدمًا، وعليك التأكد من إيجاد طريقة تراجع صحيحة. ميزات أخرى لاستمارات الويب ستجد ميزات أخرى للاستمارات ليست واضحة كتلك التي ناقشناها حتى اﻵن، ولها بالطبع فوائدها في بعض الحالات، لهذا سنذكرها في هذا القسم بإيجاز. ملاحظة: بإمكانك إيجاد أمثلة هذا القسم على جت-هاب وتجربتها مباشرة. العدّادات وأشرطة التقدم وهي أدوات تقدّم تمثيلًا بصريًا لقيم عددية، تدعمها معظم المتصفحات الحديثة. ننشئ العدّاد باستخدام العنصر <meter> وشريط التقدم باستخدام العنصر <progress>. العداد Meter يمثل العداد قيمة ثابتة ضمن مجال محدد بين قيمتي الخاصيتين min و max، وتُصيَّر هذه القيمة بصريًا على شكل شريط. ولكي تعرف كيف سيبدو هذا الشريط، عليك أن تقارن هذه القيمة مع مجموعة أخرى من القيم وهي: القيمتان الدنيا low والعليا high: وهما قيمتان تقسمان المجال إلى ثلاثة أقسام: القسم الأول بين القيمتين min و low ضمنًا. القسم الثاني بين القيمتين low و high دون هاتين القيمتين. القسم الثالث بين القيمتين high و max ضمنًا. القيمة optimum: تحدد القيمة المثلى للعداد وتحدد مع إلى القيمتين low و high القسم المفضل من المجال: إن كانت القيمة optimum في القسم السفلي من المجال، يُعد حينها الجزء السفلي هو القسم المفضل، ويعد القسم اﻷوسط هو القسم الوسطي، والقسم اﻷعلى هو القسم اﻷسوأ. إن كانت القيمة optimum في القسم الوسط من المجال، يُعد حينها الجزء السفلي هو القسم الوسطي، ويعد القسم اﻷوسط هو المفضل، والقسم اﻷعلى وسطي أيضًا. إن كانت القيمة optimum في القسم الأعلى من المجال، يُعد حينها الجزء السفلي هو القسم الأسوأ، ويعد القسم اﻷوسط هو القسم الوسطي، والقسم اﻷعلى هو القسم المفضل. تستخدم معظم المتصفحات التي تدعم العنصر <meter> تلك القيم لتغيير لون شريط العداد: إن كانت القيم الحالية ضمن القسم المفضل من المجال سيكون الشريط أخضر اللون. إن كانت القيم الحالية ضمن القسم الوسطي من المجال سيكون الشرط أصفر اللون. إن كانت القيمة ضمن القسم اﻷسوأ من المجال سيكون الشريط أحمر اللون. يمكنك الاستفادة من الشيفرة التالية ﻹنجاز أي نوع من العدادات مثل الشريط الذي يعرض المساحة المتبقية أو المستخدمة من قرص تخزين: <meter min="0" max="100" value="75" low="33" high="66" optimum="0">75</meter> See the Pen Untitled by Hsoub Academy (@HsoubAcademy) on CodePen. أما إذا رأيت عناصر داخل العنصر <meter> فهي للتراجع في حال لم يدعم المتصفح هذا العنصر، أو لدعم التقنيات المساعدة مثل قارئات الشاشة. شريط التقدم Progress bar يمثل شريط التقدم قيمة تتغير مع الوقت حتى تبلغ قيمة عظمى تحددها الخاصية max. ننشئ شريط التقدم باستخدام العنصر <progress>: <progress max="100" value="75">75/100</progress> See the Pen progress sample by Hsoub Academy (@HsoubAcademy) on CodePen. يستخدم هذا العنصر لتنفيذ أي حالة تتطلب تقريرًا بتقدم العملية مثل النسبة المئوية المنزلة من ملف أو عدد اﻷسئلة التي جاوبت عليها في استبيان وهكذا. أما إذا رأيت عناصر داخل العنصر <progress> فهي للتراجع في حال لم يدعم المتصفح هذا العنصر، أو لدعم التقنيات المساعدة مثل قارئات الشاشة. الخلاصة درسنا في سلسلة المقالات اﻷخيرة الكثير من أنواع عناصر التحكم التي يُمكن استخدامها عند بناء الاستمارات. ولا حاجة بالطبع لتذكر كل التفاصيل التي شرحناها، بل يمكنك العودة إلى هذه المقالات عند الحاجة للتحقق مما نسيته. ترجمة -وبتصرف- للمقال Other form controls. اقرأ أيضًا المقال السابق: أنواع عناصر اﻹدخال في HTML5 الأزرار، النماذج والدعوات إلى الإجراء في مجال تجربة المستخدم النماذج (Forms) في HTML5 HTTP والاستمارات في جافاسكربت
  15. تعرفنا في مقال سابق على عنصر اﻹدخال <input>، وغطينا الأنواع اﻷصلية لهذا العنصر والتي تتغير بتغير قيمة الخاصية type. لقد ظهرت الأنواع اﻷصلية مع بداية ظهور HTML، وأضيف إليها لاحقًا -في اﻹصدار HTML5- عناصر تحكم متعددة لها وظائف جديدة كي تسمح بجمع أنواع مخصصة من البيانات، وهذا ما نفصّله في مقالنا. ملاحظة1: تدعم معظم المتصفحات أغلب الميزات التي نناقشها في هذا المقال، وسنذكر أية استثناءات في حال وجودها. ملاحظة2: يلجأ المطورون إلى بناء استمارات ويب مخصصة، نظرًا لاختلاف طريقة عرض عناصر التحكم عن رؤية المصمم، وسنغطي هذا اﻷمر في مقال لاحق. ننصحك قبل المتابعة في قراءة هذا المقال أن تطلع على مقال مدخل إلى HTML للتعرف على هيكيلية صفحات ويب باستخدام لغة HTML وتتعرف على عناصرها الأساسية. حقل إدخال البريد اﻹلكتروني يمكن إنشاء حقل مخصص لإدخال البريد اﻹلكتروني باستخدام العنصر <input> وضبط الخاصية type على القيمة email كما يلي: <input type="email" id="email" name="email" /> عند استخدام هذا النوع من عناصر اﻹدخال، سيُطلب من المستخدم إدخال عنوان بريد إلكتروني صحيح، ويعرض المتصفح رسالة خطأ عند تسليم الاستمارة في حال إدخال أي محتوى مخالف. لاحظ كيف يعمل هذا الحقل في لقطة الشاشة التالية: ومن الممكن استخدام الخاصية multiple مع عنصر إدخال البريد اﻹلكتروني ﻹدخال أكثر من بريد إلكتروني في نفس الحقل (تفصل بينها فاصلة ,). <input type="email" id="email" name="email" multiple /> عندما يكتسب حقل البريد اﻹلكتروني تركيز الدخل في بعض الأجهزة وخاصة في أجهزة اللمس التي تعرض لوحات مفاتيح ديناميكية، تظهر لوحات مفاتيح مخصصة أكثر ملائمة ﻹدخال عناوين بريد إلكتروني، كأن تعرض الرمز @. لاحظ لقطة الشاشة التالية لنظام أندرويد: وهذا سبب آخر لاستخدام اﻷنواع الجديدة من عناصر اﻹدخال فهو يساهم في تحسين تجربة المستخدم على تلك اﻷجهزة. ملاحظة: بإمكانك إيجاد أمثلة عن استخدام أنواع عناصر اﻹدخال النصية البسيطة على جت-هاب وبإمكانك أيضًا الاطلاع على شيفرتها المصدرية. التحقق من صحة المدخلات في طرف العميل رأينا في الفقرة السابقة أن حقل البريد اﻹلكتروني (إضافة لأنواع أخرى) يزودنا بطريقة للتحقق من صحة مدخلات المستخدم قبل إرسالها إلى الخادم. هذا يساعد المستخدم على ملء الاستمارة بالقيم الصحيحة وتوفر وقته، حيث سيعرف مباشرة أن مدخلاته غير صحيحة ولن يكون عليه الانتظار حتى تأتيه رسالة خطأ من الخادم. ومن اﻷفضل طبعًا ألا نعد هذه الطريقة آمنة، بل ينبغي أن يتحقق التطبيق من صحة وسلامة البيانات في طرف الخادم أيضًا لحمايته من الهجمات الأمنية، لأنه من السهل جدًا إلغاء التحقق في طرف العميل، وسيتمكن المستخدمون المشبوهون من إرسال بيانات غير آمنة إلى الخادم. لن نتحدث عن مفاهيم التحقق من صحة وسلامة البيانات في طرف الخادم في سلسلة مقالاتنا الحالية وسنركز على شرح استمارات الويب، لكن عليك تذكر ذلك دائمًا. لاحظ أن البريد اﻹلكتروني من الشكل a@b هو عنوان صالح من وجهة نظر آلية التحقق الافتراضية، لأن عنصر إدخال البريد اﻹلكتروني يسمح بعناوين البريد اﻹلكتروني على الشبكات المحلية إنترانيت intranet افتراضيًا. وﻹنجاز آلية تحقق مخصصة، يمكنك استخدام الخاصية pattern وتخصيص رسالة الخطأ وسنشرح ذلك لاحقًا. ملاحظة: عندما تدخل نصًا خاطئًا في حقل البريد اﻹلكتروني، سيُفعّل صنف التنسيق الزائف invalid: وتعيد الخاصية validityState.typeMismatch القيمة true. حقل البحث خُصصت حقول البحث search ﻹنشاء صناديق البحث عن البيانات ضمن صفحة الويب أو التطبيق، ونستخدم ﻹنشاء هذا الحقل العنصر <input> ونضبط قيمة الخاصية type على search. <input type="search" id="search" name="search" /> إن الفرق الرئيسي بين الحقل textوالحقل search هو طريقة تنسيق المتصفح لكل منهما. تُصيّر (render) صناديق البحث غالبًا بزوايا دائرية مع أيقونة "Ⓧ" في طرف الصندوق أحيانًا كي تحذف محتوى الصندوق عند نقرها. إضافة إلى ذلك سيُعرض زر عنوانه "بحث search" أو عليه أيقونة المكبرة في اﻷجهزة التي تعرض لوحات مفاتيح ديناميكية. تعرض لقطات الشاشة التالية صندوق بحث ومحتواه كما يظهر في فايرفوكس 71 وسفاري 13 و كروم 79 على نظام ماك او إس ثم ايدج 18 وكروم 79 على ويندوز 10. ويمكن ملاحظة أن أيقونة المسح ستظهر إن ضم صندوق البحث نصًا وامتلك تركيز الدخل (ما عدا حالة سفاري). ومن الميزات اﻷخرى التي يجب التنويه إليها، هي إمكانية حفظ قيم حقل البحث واستخدامها في اﻹكمال التلقائي للبيانات عبر صفحات مختلفة لنفس الموقع، وتحدث العملية تلقائيًا في معظم المتصفحات الحديثة. حقل رقم الهاتف وهو حقل مخصص لإضافة أرقام الهواتف وننشئه باستخدام العنصر <input> وضبط قيمة الخاصية type على tel: <input type="tel" id="tel" name="tel" /> تعرض أجهزة اللمس عند النقر على حقل الهاتف بلوحة مفاتيح تضم أرقامًا فقط، ويعني ذلك أن هذا الحقل مفيد في جميع اﻷماكن التي نحتاجها فيها إدخال أعداد وليس فقط لإدخال أرقام الهواتف. تعرض لقطة الشاشة التالية لوحة مفاتيح رقمية في نظام أندرويد: ونظرًا للتنوع الكبير في تنسيق أرقام الهواتف عالميًا، لا يفرض هذا الحقل أية قيود على القيمة المدخلة من قبل المستخدم (يعني ذلك إمكانية إدخال أحرف أيضًا). وكما أسلفنا، يمكن استخدام الخاصية pattern لوضع القيود على المدخلات وسنناقش ذلك بمزيد من التفصيل لاحقًا. حقل إدخال عناوين URL وهو نوع خاص من الحقول، يُستخدم في إدخال عناوين URL عن طريق ضبط قيمة الخاصية type على url: <input type="url" id="url" name="url" /> يضيف استخدام هذا الحقل بعض القيود على القيم التي يدخلها المستخدم. إذ يعطي المتصفح خطأ على سبيل المثال إن لم يحدد المستخدم البروتوكول (:http مثلًا) أو لم تكن صيغة العنوان صحيحة. تعرض لوحات المفاتيح الديناميكية في أجهزة اللمس عادة لوحة مفاتيح تضم كل أو معظم محارف الفواصل والنقاط والشرطات المائلة اﻷمامية والخلفية. إليك لقطة شاشة للوحة المفاتيح في نظام أندرويد: ملاحظة: ليس بالضرورة أن يشير عنوان URL الصحيح إلى موقع حقيقي موجود! حقل ﻹدخال اﻷعداد وهو عنصر تحكم ﻹدخال اﻷعداد number وينتج عن طريق ضبط قيمة الخاصية type للعنصر <input> على القيمة number. يبدو شكل هذا النصر شبيهًا بحق اﻹدخال النصي، لكنه يقبل فقط أعدادًا بصيغة الفاصلة العائمة floating-point، ويعرض عادة زر تدوير لزيادة أو إنقاص العدد. كما تُعرض في أجهزة اللمس عادة لوحة مفاتيح رقمية كما في لقطة الشاشة التالية: يمكنك تقييد الحد اﻷدنى والأعلى للرقم الذي تريد إدخاله عند استخدام هذا الحقل وذلك بضبط قيمتي الخاصيتين min و max. كما يمكنك استخدام الخاصية step لتحديد مقدار الزيادة أو النقصان عند النقر على زر التدوير. تُقيَّم مدخلات هذا الحقل افتراضيًا إن كانت أعدادًا صحيحةً، لكن باﻹمكان السماح بزيادة أو إنقاص اﻷعداد العشرية بضبط قيمة الخاصية stepعلى "any"، وستكون القيمة الافتراضية لهذه الخاصية 1 عندما لا تُستخدم (تُحذف) وعندها سيقبل الحقل اﻷرقام الصحيحة فقط. لنلق نظرة على بعض الأمثلة: ننشئ في المثال الأول عنصر تحكم يعرض أعدادًا بين 1 و 10 ويزيد زر التدوير العدد المدخل أو يُنقصه بمقدار 2: <input type="number" name="age" id="age" min="1" max="10" step="2" /> ننشئ في المثال الثاني حقل أعداد محدودًا بين 1 و 10 ضمنًا، ويزيد زر التدوير العدد المدخل أو يُنقصه بمقدار 0.01: <input type="number" name="change" id="pennies" min="0" max="1" step="0.01" /> يستخدم حقل اﻷعداد إن كان مجال اﻷعداد المستخدمة محدودًا مثل عمر أو طول الشخص، ويمكن الاستفادة في هذه الحالة من زر التدوير. بينما إن كان المجال واسعًا مثل الرمز البريدي الذي قد يتألف من خمس أو ست منازل، فمن اﻷفضل حينها استخدام النوع tel لأن زر التدوير لن يفيدنا كثيرًا في هذه الحالة وتكفينا لوحة المفاتيح الرقمية التي سيعرضها الجهاز. عنصر شريط التمرير من الطرق اﻷخرى التي يمكن أن نستخدمها لانتقاء عدد ما هو المزلاج أو شريط التمرير Slider، ولربما صادفته في مواقع كتلك التي تبيع عقارات أو منتجات وتسمح لك بتحديد الحد اﻷعلى للسعر الذي يناسبك. إليك مثالًا: يُستخدم المزلاج في اختيار أرقام لا تهمنا كثيرًا قيمها الدقيقة، فهو أقل دقة من الحقول النصية. وننشئ المزلاج باستخدام عنصر اﻹدخال <input> وضبط قيمة الخاصية type على القيمة range. يمكن تحريك مؤشر المزلاج بالفأرة أو باللمس أو عبر أسهم لوحة المفاتيح. ومن المهم أن تضبط المزلاج جيدًا، لهذا لا بد من استخدام الخاصيات max و min و step التي تضبط الحد الأدنى واﻷعلى للقيمة ومقدار الزيادة أو النقصان. لنلق نظرة اﻵن على الشيفرة التي استخدمناها في إنجاز المثال السابق: شيفرة HTML: <label for="price">Choose a maximum house price: </label> <input type="range" name="price" id="price" min="50000" max="500000" step="100" value="250000" /> <output class="price-output" for="price"></output> ينشئ المثال السابق مزلاجًا تتراوح قيمه في المجال 50000 و 500000 وتزيد وتنقص القيمة بمقدار 100. وضبطنا قيمته الافتراضية لتكون 250000باستخدام الخاصية value. وتكمن مشكلة المزلاج بأنه لا يعرض افتراضيًا القيمة الحالية له بشكل مرئي، لهذا أضفنا العنصر <output> كي يضم القيمة الحالية للمزلاج. وباﻹمكان طبعًا عرض قيمة عنصر اﻹدخال أو نتيجة حسابات ما ضمن أي عنصر، لكن للعنصر <output> ميزة خاصة تشبه تلك التي يتمتع بها العنصر <label> من ناحية استخدام الخاصية for التي تسمح لك بربطه بالعنصر أو العناصر التي تعطيه قيمته. ولعرض القيمة الحالية للمزلاج وتحديثها كلما تغيّرت، لا بد من استخدام جافا سكريبت: شيفرة جافا سكريبت: const price = document.querySelector("#price"); const output = document.querySelector(".price-output"); output.textContent = price.value; price.addEventListener("input", () => { output.textContent = price.value; }); نخزّن في هذه الشيفرة مرجعًا إلى عنصر المزلاج range وإلى عنصر اﻹخراج <output> ضمن متغيرين. ومن ثم نضبط مباشرة قيم الخاصية textcontent للعنصر output لتكون قيمة عنصر شريط التمرير الحالية. نضبط أخيرًا مترصد أحداث للتأكد من تغيير قيمة الخاصية textcontent للخرج عندما يتحرك المزلاح إلى قيمة جديدة. عنصر انتقاء الوقت والتاريخ كان من الصعب على المطورين بداية تزويد الاستمارات بطريقة لتحديد الوقت والتاريخ، ولتحسين تجربة المستخدم، كان لا بد من وجود واجهة مستخدم مناسبة تعمل كتقويم، كي تمكن المستخدمين من اختيار التاريخ دون الحاجة إلى الانتقال إلى تطبيق التقويم اﻷصلي أو حتى إدخال هذه البيانات يدويًا بتنسيقات مختلفة يصعب تفسيرها. فقد أمكن التعبير عن الدقيقة اﻷخيرة في اﻷلفية الماضية مثلًا بطرق مختلفة مثل: 1999/12/31, 23:59 // أو 12/31/99T11:59PM. تقدّم HTML عنصر تحكم للتعامل مع هذا النوع من البيانات، وذلك من خلال عنصر التقويم الذي ننشئه باستخدام العنصر <input> وضبط قيمة الخاصية type على قيمة مناسبة تعتمد على نوع البيانات التي تريد جمعها كالتاريخ أو الوقت أو كليهما. إليك مثالًا يتراجع فيه المتصفح إلى العنصر <select> إن لم يدعم عنصر التقويم: لنلق نظرة سريعة اﻵن على اﻷنواع المتوفرة لعنصر التقويم (قيم الخاصية type). وانتبه إلى أن استخدام هذه اﻷنواع معقد، وخاصة فيما يتعلق بدعم المتصفح لهذا النوع. النوع datetime-local تنشئ القيمة datetime-local عنصر تقويم يعرض التاريخ ويسمح لك بانتقاء الوقت والتاريخ دون تحديد منطقة زمنية: <input type="datetime-local" name="datetime" id="datetime" /> النوع month تنشئ القيمة month عنصر تقويم لعرض واختيار شهر مع السنة: <input type="month" name="month" id="month" /> النوع time تنشئ القيمة time عنصر تقويم لعرض واختيار الوقت، ويُعيد الوقت وفق تنسيق 24 ساعة: <input type="time" name="time" id="time" /> النوع week تنشئ القيمة week عنصر تقويم لعرض واختيار رقم اﻷسبوع مع السنة. يبدأ اﻷسبوع يوم الاثنين ويستمر حتى اﻷحد. وإضافة إلى ذلك، يضم الأسبوع اﻷول من السنة أول يوم خميس في هذا العام، وبالتالي قد لا يضم اليوم الأول من السنة، أو قد يضم آخر أيام من العام السابق. <input type="week" name="week" id="week" /> تحديد قيم الوقت والتاريخ يمكن تحديد قيم جميع اﻷنواع السابقة لعنصر التقويم من خلال الخاصيتين min و max وكذلك الخاصية step التي قد تتغير قيمتها وفقًا لنوع عنصر التقويم المستخدم. <label for="myDate">When are you available this summer?</label> <input type="date" name="myDate" min="2013-06-01" max="2013-08-31" step="7" id="myDate" /> عنصر انتقاء اﻷلوان من الصعب دائمًا التعامل مع اﻷلوان لوجود طرق كثيرة في التعبير عنها مثل منظومة RGB أو HSL أو بذكر اسم اللون راحة وهكذا. لهذا قدمت HTML5 عنصر انتقاء اللون الذي ننشئه باستخدام العنصر <input> والقيمة color للخاصية type: <input type="color" name="color" id="color" /> ويؤدي النقر على عنصر انتقاء اللون عادة إلى عرض منتقي اللون الافتراضي في نظام التشغيل. إليك مثالًا: وتكون القيمة التي يعيدها منتقي اللون هي قيمة ست عشرية hexadecimal مكونة من 6 خانات تمثل اللون الذي اخترته. الخلاصة تعرفنا في هذا المقال على بعض عناصر اﻹدخال الجديدة التي قدمتها HTML5، كما يقدم هذا اﻹصدار من اللغة مجموعة أخرى من عناصر الدخل التي يسهل تجميعها ضمن مجموعات نظرًا لسلوكها الخاص، وهذا ما نناقشه في مقال قادم. ترجمة -وبتصرف- للمقال: The HTML5 input types اقرأ أيضًا المقال السابق: أدوات التحكم اﻷصلية في استمارات الويب النماذج (Forms) في HTML5 عناصر الإدخال: إنشاء استمارة دفع في بوتستراب التركيز على عناصر صفحة الويب تنسيق الصور والوسائط المتعددة والنماذج في CSS
×
×
  • أضف...