-
المساهمات
164 -
تاريخ الانضمام
-
تاريخ آخر زيارة
نوع المحتوى
ريادة الأعمال
البرمجة
التصميم
DevOps
التسويق والمبيعات
العمل الحر
البرامج والتطبيقات
آخر التحديثات
قصص نجاح
أسئلة وأجوبة
كتب
دورات
كل منشورات العضو ابراهيم الخضور
-
أشرنا في المقال السابق إلى إمكانية استخدام قاعدة البيانات المدمجة في المتصفح 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
-
تُعد الواجه البرمجية 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 التحقق من صحة بيانات استمارة ويب في طرف العميل نظرة على تفاعلات الخادم مع العميل في موقع ويب ديناميكي الواجهات البرمجية للتعامل مع الصوت والفيديو في جافا سكريبت
-
تقدم المتصفحات الحديثة مجموعة من التقنيات المختلفة التي تسمح بتخزين بيانات تتعلق بمواقع الويب ثم استرجاعها عند الضرورة، مما يسمح لنا بالحفاظ على البيانات لفترة أطول أو تخزينها للعمل دون اتصال باﻹنترنت وغير ذلك. لهذا سنناقش في مقالنا أبسط اﻷساسيات المتعلقة بهذا اﻷمر وكيفية عملها. ننصحك قبل المتابعة في قراءة هذه المقالات أن: تكون ملمًا بلغتي 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
-
تضم لغة 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
-
غطينا في مقالنا السابق بعض أساسيات الرسوم ثنائية البعد ضمن العنصر <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 - إضافة التفاصيل إلى سطح مجسَّم مدخل إلى صناعة ألعاب المتصفح الرسم على لوحة في جافاسكربت
-
يتضمن المتصفح مجموعة أدوات برمجية فعّالة للتعامل الرسوميات ابتداءً من لغة إنشاء الرسوميات الشعاعية 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 في جافاسكربت
-
تُعد الواجهات البرمجية التي ذكرناها في مقالات سابقة واجهات مُضمَّنة في المتصفح، لكن ليست كل الواجهات البرمجية كذلك. إذ تقدم الكثير من الشركات مثل فيسبوك وجوجل و 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 في جافاسكربت
-
تطرقنا سابقًا للحديث عن واجهة برمجة التطبيقات واستخدامها في مهام مختلفة مثل معالجة مستندات الويب وشجرة 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
-
بعد أن شرحنا كيفية التحقق من صحة البيانات المدخلة من طرف العميل، ستفكر بعدها في كيفية إرسال هذه البيانات إلى الخادم، وهذا ما نغطيه في هذا المقال. إذ سنناقش ما يحدث عندما ينقر المستخدم على زر اﻹرسال في استمارات أو نماذج الويب 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 من لغة بايثون
-
من الضروري التحقق من ملء بيانات جميع عناصر تحكم استمارة ويب وفق التنسيق الصحيح قبل إرسالها إلى الخادم، وتُدعى هذه العملية التحقق من الاستمارة في طرف العميل 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 آليات الاتصال المستمر مع الخادم في جافاسكربت المدخلات والمخرجات النصية في جافا
-
نناقش في هذا المقال استخدام لغة 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
-
تعرفنا في مقال سابق على طريقة تنسيق استمارات الويب بشكل متقدم واستعرضنا أمثلة على بعض الحالات التي نستخدم فيها أصناف التنسيق الزائفة 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
-
بدأنا في مقال سابق التعرف على استمارات الويب 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
-
نلقي نظرة في هذا المقال وبشيء من التفصيل على عناصر استمارات ويب 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 والاستمارات في جافاسكربت
-
تعرفنا في مقال سابق على عنصر اﻹدخال <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
-
شرحنا في مقال سابق مثالًا عمليًا على استمارات الويب، وتعرفنا على بعض عناصر التحكم في الاستمارة والهيكليات الشائعة في بنائها، وركزنا اهتمامنا على أفضل الممارسات التي تُعنى بسهولة الوصول accessibility وتضمن إتاحة المحتوى لجميع الأشخاص. سنتابع في هذا المقال التعرف على وظائف عناصر التحكم المختلفة بتفصيل أكبر، ودراسة جميع الخيارات المتاحة لجمع اﻷنواع المختلفة من البيانات، كما سنلقي نظرة على مجموع عناصر التحكم اﻷصلية باستمارات الويب المتوفرة في جميع متصفحات الويب. تعرفنا مسبقًا على بعض أنواع عناصر التحكم مثل <form> و <legend> و <label> و <fieldset> و <textarea> و <input>. ونستكشف في هذا المقال الأمور التالية: اﻷنواع المختلفة لعناصر الدخل <input> وهي button و checkbox و file و hidden و image و password و radio و reset و submitو text. بعض السمات المشتركة بين جميع عناصر التحكم باستمارة الويب. ملاحظة: سنغطي أيضًا بعض عناصر التحكم الأكثر فعالية في مقالات لاحق وننصحك قبل المتابعة في قراءة هذا المقال أن تطلع على سلسلة مقالات مدخل إلى HTML للتعرف على هيكلية صفحات ويب باستخدام لغة HTML وتتعرف على عناصرها. حقول اﻹدخال النصية تُعد حقول اﻹدخال النصية <input> من أبسط أشكال عناصر التحكم بالاستمارات تزود المستخدم بطريقة ملائمة ﻹدخال أي نوع من البيانات كما رأينا في أمثلة سابقة. ملاحظة: إن الحقول النصية التي تقدمها HTML هي حقول نصية بسيطة لا يمكن استخدامها في إنشاء عناصر إدخال غنية كالتي تعرض خطوطًا سميكة أو مائلة أو ملونة، وجميع العناصر التي تصادفها من هذا النوع هي في الواقع عناصر تحكم معدّلة باستخدام CSS و جافا سكريبت. تشترك جميع عناصر اﻹدخال النصي في استمارات الويب بالسلوك التالي: يمكن أن تكون جميع هذه العناصر بحالة للقراءة فقط readonly أي لا يمكن للمستخدم تعديل محتواها لكن بياناتها تُسلَّم مع الاستمارة، كما يمكن أن تكون معطّلة disabled أي لا يمكن للمستخدم تغيير محتواها ولن تُسلَّم مع بقية بيانات الاستمارة. تملك جميع هذه العناصر السمة placeholder التي تعرض نصًا توضيحيًا داخل العنصر يصف باختصار الغاية من عنصر التحكم هذا أو نوع البيانات المتوقع إدخالها فيه. يمكن أن نحدد قياسها size (أي القياس الفيزيائي للصندوق)، وعدد المحارف اﻷعظمي التي يمكن أن تستوعبها maxlength. يمكن التحقق فيها من الصحة اﻹملائية للمحتوى باستخدام السمة spellcheck إن كان المتصفح يدعم ذلك. ملاحظة: تُعد عناصر التحكم <inputs> فريدة في HTML لأنها قد تأخذ أشكالًا مختلفة وفقًا لقيمة السمة type، وتُستخدم في بناء معظم عناصر التحكم بما في ذلك حقول اﻹدخال النصي وحيدة ومتعددة اﻷسطر وعناصر عرض الوقت والتاريخ وعناصر أخرى مثل صناديق التحقق وأزرار الخيار المتعدد radio buttons وعناصر انتقاء اﻷلوان واﻷزرار النمطية. الحقول النصية ذات السطر المفرد ننشئ هذا الحقل باستخدام العنصر <input> بعد ضبط قيمة السمة type على text أو دون استخدام السمة type نهائيًا، لأن القيمة الافتراضية ستكون text في هذه الحالة. وتُعد القيمة text قيم تراجع (قيم احتياطية) fallback إن كانت القيمة التي أسندتها إلى الخاصية type غير معروفة من قبل المتصفح (كأن تختار القيمة color في حين لا يدعم متصفح عنصر أصلي لانتقاء اللون). ملاحظة: بإمكانك إيجاد أمثلة عن مختلف أنواع حقل اﻹدخال النصي المفرد على جت-هب وتجربتها مباشرة أيضًا. إليك مثالًا بسيطًا عن حقل إدخال نصي مفرد السطر: <input type="text" id="comment" name="comment" value="I'm a text field" /> هنالك قيد وحيد لحقل اﻹدخال النصي المفرد وهو أنه سيحذف جميع محارف الانتقال إلى سطر جديد إن وجدت قبل إرسال البيانات إلى الخادم، وهكذا تبقى البيانات على نفس السطر. تعرض لقطة الشاشة التالية مربع إدخال نصي في الحالة الطبيعية، وعندما يأخذ تركيز الدخل focus وفي حالته المعطلّة disabled. وتشير معظم المتصفحات إلى حالة اكتساب تركيز الدخل بعرض إطار ملون حول العنصر، بينما تعرض الحالة المعطلة بجعل الخلفية رمادية والنص رماديًا أو أن يبدو العنصر شبه شفاف. التقطت الصورة السابقة من متصفح كروم على نظام ماك أو إس (macOS)، وقد تكون هناك اختلافات طفيفة بين الحقول أو اﻷزرار من متصفح إلى آخر، لكن تقنية تظليل العناصر لتمييز حالتها تبقى متشابهة. حقل كلمة المرور يُعد عنصر إدخال كلمة المرور من النمط password من عناصر الإدخال اﻷصلية في HTML: <input type="password" id="pwd" name="pwd" /> تعرض لقطة الشاشة التالية عنصر إدخال كلمة سر يظهر ضمنه كل محرف على شكل نقطة. لا تضيف القيمة password أية قيود إضافية على النص المُدخل، لكنها تحجب القيمة المدخلة في الحقل (على شكل نقاط أو نجوم) كي لا يطلع عليها أحد. لكن تذكر أن هذا الحقل مجرد واجهة مستخدم، فإن أرسلت محتوى حقل كلمة السر دون حيطة أمنية فسترسل كلمة السر على شكل نص واضح وهذا أمر سيء، فقد يعترض طرف ما البيانات المرسلة ويسرق كلمة المرور أو تفاصيل بطاقة اﻹئتمان أو أية بيانات قد ترسلها. وأفضل الطرق لتحمي المستخدم هي استضافة الصفحات التي تحتوي على استمارة ويب ضمن قناة اتصال آمنة (أي أن يبدأ عنوان الصفحة بالبروتوكول //:https)، وهكذا تُشفَّر البيانات قبل إرسالها. ملاحظة: تميز المتصفحات المخاطر والتبعات اﻷمنية ﻹرسال البيانات من خلال اتصال غير آمن (أي بدون استخدام بروتوكول HTTPS المشفر)، وتحذر المستخدم من استخدام استمارات ويب غير آمنة. المحتوى المخفي من اﻷنواع الأخرى لعناصر اﻹدخال هي العناصر المخفية hidden، وتُستخدم ﻹنشاء عنصر تحكم غير مرئي للمستخدم لكنه قادر على إرسال البيانات إلى الخادم مع بقية عناصر الاستمارة. فقد ترغب مثلا بإرسال فترة زمنية تشير إلى تاريخ تنفيذ الطلب. وهكذا لن يتمكن المستخدم من رؤية هذا العنصر أو تعديل قيمته عمدًا، فلن يتلقى هذا العنصر تركيز الدخل أبدًا، ولن تلاحظه أيضًا قارئات الشاشة. <input type="hidden" id="timestamp" name="timestamp" value="1286705410" /> عندما تنشئ مثل هذه العناصر، لا بد من ضبط قيمتي السمتين name و value. ويمكن ضبط قمتي السمتين ديناميكيًا باستخدام جافا سكريبت. ولا يجب أن يُربط أيضًا أي عنوان بعنصر اﻹدخال المخفي، وسنغطي بقية أنواع عناصر اﻹدخال في مقال قادم. صناديق التحقق Checkboxes وأزرار الاختيار المتعدد Radio buttons عناصر الاختيار هي عناصر تحكم تتغير حالتها عند النقر عليها أو على العناوين المرتبطة بها. وهناك نوعان منها: صناديق التحقق وأزرار الاختيار المتعدد، ويستخدم كلا النوعين السمة checked للإشارة أن العنصر مفعّل أو معطل افتراضيًا. وتجدر اﻹشارة أن هذه العناصر لا تسلك نفس سلوك بقية عناصر تحكم استمارات الويب. فبمجرد النقر على زر التسليم، يُرسل محتوى كل عناصر التحكم التي تمتلك السمة name حتى لو كانت فارغة، بينما لا تُرسل قيمة عناصر الاختيار ما لم تكن مفعّلة checked. ولا ترسل قيم هذه العناصر إطلاقًا ما لم تكن مفعّلة ولا حتى أسماءها، بينما تُرسل اﻷسماء إن كانت مفعلة لكن دون قيمة. ملاحظة: بإمكانك الاطلاع على مثال هذا القسم على جت-هب وتجربته مباشرة أيضًا. ولتقديم أفضل طريقة استخدام تدعم شمولية الوصول، أحط كل قائمة من هذه العناصر لها نفس الغاية بالعنصر <fieldset> واستخدم العنصر <legend> ﻹعطاء توصيف كامل للقائمة. كما ينبغي أن يكون كل زوج <label> / <input> ضمن عنصر قائمة خاص به. ويوضع العنوان المرافق لكل عنصر قبله أو بعده مباشرة، بينما يوضع وصف عمل مجموع أزرار الاختيار المتعدد أو صناديق التحقق ضمن العنصر <legend>. الق نظرة على المثال السابق لترى هيكلة هذه العناصر في الاستمارة. صندوق التحقق Checkbox ننشئ صندوق التحقق checkbox باستخدام العنصر <input> والسمة type = checkbox كما يلي: <input type="checkbox" id="questionOne" name="subscribe" value="yes" checked /> ويجب أن تحمل صناديق التحقق التي ترتبط ببعضها نفس قيمة الخاصية name، وعندما تضيف السمة checked سيكون صندوق التحقق مفعّلًا افتراضيًا عند تحميل الصفحة، وعند النقر على صندوق التحقق أو العنوان المرتبط به تتغير حالة الصندوق من التفعيل إلى الحالة غير المفعّلة وبالعكس: <fieldset> <legend>Choose all the vegetables you like to eat</legend> <ul> <li> <label for="carrots">Carrots</label> <input type="checkbox" id="carrots" name="vegetable" value="carrots" checked /> </li> <li> <label for="peas">Peas</label> <input type="checkbox" id="peas" name="vegetable" value="peas" /> </li> <li> <label for="cabbage">Cabbage</label> <input type="checkbox" id="cabbage" name="vegetable" value="cabbage" /> </li> </ul> </fieldset> تعرض لقطة الشاشة التالية صندوق التحقق في الحالة التي يكون فيها مفعلًا أو يمتلك تركيز الدخل أو معطلًا. وتكون صناديق التحقق مفعلّة افتراضيًا وفي الحالة المعطلّة وغير مفعلة عندما تكتسب تركيز الدخل ويحيط بها إطار ملوّن. ملاحظة: يتطابق تنسيق صناديق التحقق وأزرار الاختيار المتعدد التي تأتي مع السمة checked مع محدد الصنف الزائف default: حتى لو ألغي تفعيلها لاحقًا، بينما يتطابق تنسيق العناصر التي فُعلت حاليًا مع محدد الصنف الزائف checked:. ونظرًا لطبيعة هذه العناصر التي تتأرجح بين التفعيل أو عدمه، فهي تُعدّ أزرار تبديل toggle buttons. حيث يعمد الكثير من المطورين إلى توسيع التنسيق الافتراضي لهذه العناصر ﻹنشاء أزرار تبدو وكأنها مفاتيح تبديل (الق نظرة على المثال الذي يوضح اﻷمر واطلع على شيفرته المصدرية إن أردت). أزرار الاختيار المتعدد Radio buttons ننشئ أزرار الاختيار المتعدد باستخدام العنصر <input> والسمة type = radio كما يلي: <input type="radio" id="soup" name="meal" value="soup" checked /> يمكن ربط عدة أزرار اختيار متعدد معًا إذا كان لهذه اﻷزرار نفس السمة name، وتُعد الأزرار في هذه الحالة مجموعة واحدة يمكنك فقط اختيار (تفعيل) زر واحد منها، ويعني ذلك أن النقر على زر آخر يفعله ويلغي تفعيل كل الأزرار الباقية في المجموعة. وتُرسل فقط قيمة الزر المفعّل عند إرسال أو تسليم الاستمارة. وإن لم يكن أي من الأزرار مفعلة، تُعد حالة المجموعة غير معلومة ولا تُرسل أي قيمة مع الاستمارة. وبمجرد أن تفعل أحد أزرار المجموعة المترابطة، لن تتمكن بعدها من إلغاء تفعيل اﻷزرار جميعها إلا بإعادة ضبط الاستمارة reset: <fieldset> <legend>What is your favorite meal?</legend> <ul> <li> <label for="soup">Soup</label> <input type="radio" id="soup" name="meal" value="soup" checked /> </li> <li> <label for="curry">Curry</label> <input type="radio" id="curry" name="meal" value="curry" /> </li> <li> <label for="pizza">Pizza</label> <input type="radio" id="pizza" name="meal" value="pizza" /> </li> </ul> </fieldset> تعرض لقطة الشاشة التالية زر اختيار متعدد في الحالة الافتراضية والمعطلة، مع زر غير مفعل اكتسب تركيز الدخل: اﻷزرار الفعلية في استمارات الويب لا تُعد أزرار الاختيار المتعدد أزرارًا فعلية على الرغم من اسمها، لهذا سنلقي نظرة في هذه الفقرة على أنواع اﻷزرار الحقيقية. هنالك ثلاث أنواع لعنصر الدخل تعطي أزرار فعلية وهي: submit: التي ترسل بيانات الاستمارة إلى الخادم. كما ينتج هذا النوع عن العنصر <button> عندما نهمل السمة type أو نسند إليها قيمة غير معروفة. reset: يعيد عناصر تحكم الاستمارة إلى وضعها الافتراضي. button: ليس لها تأثير تلقائي، لكن باﻹمكان تخصيصها باستخدام جافا سكريبت. ولدينا أيضًا العنصر <button> الذي يمتلك السمة type وتاخذ إحدى القيم submit و reset و button ليحاكي سلوك اﻷنواع الثلاث لعنصر اﻹدخال. أما الاختلاف الحقيقي، فيكمن في سهولة تنسيق الزر الفعلي: <input type="submit" value="Submit this form" /> <input type="reset" value="Reset this form" /> <input type="button" value="Do Nothing without JavaScript" /> <button type="submit">Submit this form</button> <button type="reset">Reset this form</button> <button type="button">Do Nothing without JavaScript</button> See the Pen Basic native form controls by Hsoub Academy (@HsoubAcademy) on CodePen. كما تلاحظ سيعرض عنصر اﻹدخال image على شكل زر، وسنغطي شرح هذا لاحقًا. ملاحظة: بإمكانك الاطلاع على مثال هذا القسم على جت-هب وتجربته مباشرة أيضًا. إليك أمثلة عن استخدام عناصر اﻹدخال ﻹنشاء زر واستخدام الزر الفعلي <button> المكافئ: الزر submit <button type="submit">This is a <strong>submit button</strong></button> <input type="submit" value="This is a submit button" /> الزر reset <button type="reset">This is a <strong>reset button</strong></button> <input type="reset" value="This is a reset button" /> الزر غير المحدد <button type="button">This is an <strong>anonymous button</strong></button> <input type="button" value="This is an anonymous button" /> تسلك الأزرار الحقيقية السلوك نفسه سواءً استخدمت العنصر <button> أو عنصر إدخال <input>. ويسمح لكن عنصر الزر <button> باستخدام عناصر HTML ضمن محتواها، بوضع العنصر بين وسمي البداية والنهاية، أما عناصر اﻹدخال فهي عناصر فارغة (عقيمة أو بدون أبناء) ولا يمكن وضع محتوى فيها إلا على شكل قيمة للسمة value، وبالتالي لن تقبل سوى المحتوى النصي. تعرض لقطة الشاشة التالية زرًا في الحالة التي يكون فيها مفعلًا أو يمتلك تركيز الدخل أو معطلًا. وعندما يكتسب الزر تركيز الدخل يحيط به إطار ملوّن، ويظهر على شكل رمادي في الحالة المعطّلة. زر الصورة يُصيّر عنصر التحكم "زر الصورة image button" كما يُصير عنصر الصورة <img> تمامًا ما عدا أنه يسلك سلوك زر اﻹرسال submit عند النقر عليه. ننشئ زر الصورة باستخدام العنصر <input> والسمة type = image ويمتلك جميع الخاصيات التي يمتلكها زر الاستمارة: <input type="image" alt="Click me!" src="my-img.png" width="80" height="30" /> إن استخدمنا زر الصورة في تسليم بيانات الاستمارة، لن يسلم هذا العنصر قيمته، بل اﻹحداثيين X و Y لنقطة النقر على الصورة (منسوبةً إلى الزاوية العليا اليسارية للصورة التي تمثل النقطة (0,0)). وتُرسل اﻹحداثيات على شكل زوجين مفتاح/قيمة: قيمة الخاصية name مع قيمة x (على شكل قيمة نصية). قيمة الخاصية name مع قيمة y (على شكل قيمة نصية). فلو نقرت مثلًا على النقطة (123,456) من زر الصورة وسلمت محتوى الاستمارة من خلال طلب من النوع get سترى أن القيمة الملحقة بعنوان URL الخاص بالطلب كالتالي: http://foo.com?pos.x=123&pos.y=456 وهذه طريقة مناسبة ﻹنشاء خارطة تتبع hot map وفهم كيفية تفاعل المستخدمين مع عناصر صفحات الويب. وسنتكلم في مقال لاحق بالتفصيل عن طريق إرسال واستخلاص هذه البيانات. عنصر انتقاء الملفات File Picker يعد منتقي الملفات أحد أنواع عناصر اﻹدخال التي ظهرت مبكرًا في HTML. إذ تمتلك الاستمارات القدرة على إرسال ملفات كاملة إلى الخادم، لهذا يستخدم منقي الملفات في اختيار ملف أو أكثر ﻹرساله. نستخدم عنصر اﻹدخال <input> ﻹنشاء منتقي ملفات بإسناد القيمة file للسمة type. ويمكن تحديد نوع الملف الذي تريد إرساله باستخدام السمة accept، وإن أردت السماح للمستخدم اختيار عدة ملفات، يمكنك استخدام السمة multiple: مثال ننشئ في هذا المثال منتقي ملفات لرفع صور إلى الخادم، ويُسمح للمستخدم باختيار عدة ملفات كي يرسلها: <input type="file" name="file" id="file" accept="image/*" multiple /> يمكن لمنتقي الملفات في بعض الهواتف المحمولة الوصول إلى الصور ومقاطع الفيديو والصوت التي تلتقط مباشرة من كاميرا وميكروفون الجهاز بإضافة القيمة capture إلى السمة accept: <input type="file" accept="image/*;capture=camera" /> <input type="file" accept="video/*;capture=camcorder" /> <input type="file" accept="audio/*;capture=microphone" /> تعرض لقطة الشاشة التالية منتقي ملفات في الحالة الافتراضية والمعطلة وعند اكتسب تركيز الدخل، في الحالة التي لم نختار فيها ملفًا بعد: السمات المشتركة تمتلك بعض العناصر التي نستخدمها في استمارات الويب سمات خاصة بها، وهنالك أيضًا سمات مشتركة بين جميع تلك العناصر. إليك قائمة بهذه السمات: اسم السمة القيمة الافتراضية الوصف autofocus false تسمح لك هذه السمة ذات القيمة المنطقية تحديد إن كان العنصر سيكتسب تركيز الدخل تلقائيًا عند تحميل الصفحة. ويمكن تخصيص قيمة هذه السمة لعنصر واحد فقط في الصفحة. disabled false قيمة منطقية تشير إلى أن المستخدم غير قادر على التفاعل مع هذا العنصر. وفي حال لم تستخدم هذه السمة، سيرث العنصر قيمة هذه السمة من العنصر اﻷب. فالعنصر <fieldset> مثلًا سيكون مفعّلًا إن لم يكن ضمن عنصر أب ضبط قيمة هذه السمة فيه على disable. form وتمثل الاستمارة التي نربط بها عنصر التحكم، وتستخدم عندما لا يكون العنصر موضوعًا داخل حاوية الاستمارة. وينبغي أن تكون قيمة هذه السمة هي قيمة المعرف id للاستمارة في نفس الصفحة، وتساعدك هذه السمة في ربط عنصر باستمارة حتى لو كان خارجها أو حتى ضمن استمارة أخرى. name اسم العنصر، وتسلم هذه القيمة مع البيانات عند تسليم الاستمارة. value القيمة اﻷولية للعنصر. الخلاصة غطينا في هذا المقال أنواع عناصر اﻹدخال، وهي الأنواع التي أدخلت في بدايات لغة HTML لهذا فهي مدعومة جيدًا في جميع المتصفحات، وسنتعرف لاحقًا على أنواع أحدث لعناصر اﻹدخال. ترجمة -وبتصرف- للمقال: Basic native form controls اقرأ أيضًا المقال السابق: هيكلة استمارة الويب Web Forms أساسيات العمل مع استمارات الويب Web forms أدوات سهولة الوصول Accessibility اللازمة في عملية تطوير الويب تعرف على أنواع الحقول الجديدة في نماذج HTML5
-
تعرفنا في مقال سابق على أساسيات العمل مع استمارات الويب وسنلقي نظرة أقرب في هذا المقال على العناصر التي تُستخدم لهيكلة استمارة الويب وإعطاء كل عنصر دلالته الخاصة. فمرونة استمارات ويب تجعلها إحدى الهيكليات اﻷكثر تعقيدًا في HTML، حيث تستطيع بناء أي نوع من استمارات الويب اﻷساسية باستخدام وسم الاستمارات المخصص <form> وسماته. وبالتالي سيكون اختيار الهيكلية الصحيحة لاستمارة الويب أساسيًا لضمان قابلية استخدام هذه الاستمارة وسهولة الوصول إليها accessible form. كما ننصحك قبل المتابعة في قراءة هذا المقال أن تطلع على سلسلة المقالات "مدخل إلى HTML" للتعرف على هيكلية صفحات ويب باستخدام لغة HTML وتتعرف على أهم عناصرها. العنصر <form> يُعرّف العنصر <form> أنه استمارة أو نموذج مع سمات تحدد سلوكه. وكلما أردت بناء استمارة ويب عليك أن تبدأ باستخدام العنصر <form> وتضع ضمنه المحتوى المطلوب. وتستطيع العديد من تقنيات ويب الحديثة وإضافات المتصفحات browser plugins اكتشاف عناصر الاستمارات وتنفيذ خطافات مخصصة لها كي يسهل استخدامها. تحذير: يمنع صراحة وضع عنصر <form> داخل عنصر <form> آخر، لأن تداخل هذه العناصر قد يؤدي إلى سلوك غير متوقع. ومن الممكن دائمًا استخدام عناصر تحكم بالاستمارة تقع خارج العنصر <form>. وفي هذه الحالة، لن يكون هناك أي ترابط بين عناصر التحكم والاستمارة ما لم تربط بنفسك هذه العناصر بالاستمارة باستخدام السمة for لعناصر التحكم. وقد أدخلت هذه السمة كي تربط العناصر الموجودة خارج الاستمارة ضمنيًا باستمارة ويب محددة. وسنتعرف تاليًا على العناصر الهيكلية التي تجدها داخل استمارة الويب. العنصران <fieldset> و <legend> يُعد العنصر <fieldsset> مناسبًا ﻹنشاء مجموعات من عناصر التحكم لها نفس الغاية سواء للتنسيق أو إعطاء دلالة خاصة. وبإمكانك وضع عنوان للعنصر <fieldset> بتضمين العنصر <legend> تحت وسم البداية الخاص به، وهكذا يصف محتوى العنصر <legend> الغاية من استخدام العنصر <fieldset>. تستخدم الكثير من التقنيات المساعدة العنصر <legend> على أنه جزء من عنوان كل عنصر تحكم يقع داخل <fieldset> وتقرأ بعض قارئات الشاشات مثل Jaws و NVDA محتوى العنصر <legend> قبل قراءة عنوان عنصر التحكم. إليك مثالًا: <form> <fieldset> <legend>Fruit juice size</legend> <p> <input type="radio" name="size" id="size_1" value="small" /> <label for="size_1">Small</label> </p> <p> <input type="radio" name="size" id="size_2" value="medium" /> <label for="size_2">Medium</label> </p> <p> <input type="radio" name="size" id="size_3" value="large" /> <label for="size_3">Large</label> </p> </fieldset> </form> ملاحظة: ستجد هذا المثال على جت-هب وبإمكانك تجربته مباشرة. عند قراءة الشيفرة السابقة، ستنطق قارئات الشاشة العبارة "Fruit juice size small" المتعلقة بعنصر التحكم اﻷول، والعبارة "Fruit juice size medium" للعنصر الثاني، والعبارة "Fruit juice size large" للثالث. إن حالة الاستخدام التي يعرضها هذا المثال مهمة جدًا؛ فإن كان لديك مجموعة من أزرار الاختيار radio buttons، عليك في هذه الحالة وضعها ضمن عنصر <fieldset> واحد. ستجد العديد من حالات الاستخدام لهذه العناصر، لكنها تُستخدم عمومًا في تجزئة الاستمارة. إذ توزّع استمارات الويب الطويلة على عدة صفحات، لكن إن كان لا بد من وضعها في صفحة واحدة، من الأفضل حينها تجزئة الاستمارة من خلال وضع العناصر التي تؤدي غاية محددة ضمن عناصر <fieldset> مخصصة لتحسين قابلية استخدام الاستمارة. ونظرًا لتأثير العنصر <fieldset> على التقنيات المساعدة، فهو عنصر أساسي في بناء استمارات تدعم شمولية الوصول أو سهولة الوصول accessibility، وتبقى عليك مسؤولية استخدامها بالشكل الصحيح. حاول إن أمكن أن تسمع ما تنطقه قارئات الشاشة عندما تبني استمارتك، فإن بدا الأمر غريبًا، جرّب أن تحسن هيكلية الاستمارة الخاصة بك. العنصر <label> رأينا في مقال سابق أن العنصر <label> يعرّف لنا عنوانًا لوصف عنصر التحكم الموجود في استمارة الويب. وهذا العنصر شديد اﻷهمية لاسيما إن أردت بناء استمارة يدعم سهولة الوصول، فعندما تستخدمها بالشكل الصحيح، ستنطقها قارئات الشاشة مع أية توجيهات أخرى، كما أنها مفيدة للمستخدمين الأصحاء كذلك لتوضح لهم دلالة مكونات الاستمارة. إليك المثال الذي خبرناه في المقال السابق: <label for="name">Name:</label> <input type="text" id="name" name="user_name" /> عندما يُربط العنوان بالعنصر <input> من خلال السمة for التي تحمل قيمة معرّف عنصر اﻹدخال، ستنطق قارئات الشاشة العبارة "Name, edit text". وهناك طريقة أخرى لربط عنصر تحكم الاستمارة بالعناوين عن طريق وضع عنصر التحكم داخل العنوان <label> صراحة كما يلي: <label for="name"> Name: <input type="text" id="name" name="user_name" /> </label> وحتى في هذه الحالات، يُعد استخدام السمة for ممارسة مفضلة كي نضمن فهم التقنيات المساعدة للعلاقة الموجودة بين العنوان وعنصر التحكم، فإن لم يكن هناك عنوان أو لم يرتبط عنصر التحكم في استمارة ويب صراحة أو ضمنيًا بعنوان، ستنطق قارئات الشاشة عبارة مثل "Edit text blank" وهذا لن يساعد أبدًا. العناوين قابلة للنقر أيضًا من الإيجابيات اﻷخرى لإعداد عناوين لعناصر تحكم استمارة الويب هو إمكانية النقر على العنوان لتفعيل عنصر التحكم المرتبط به. ولهذا اﻷمر فائدته عند استخدام عناصر اﻹدخال النصية، إذ سيتلقى العنصر تركيز الدخل بالنقر عليه أو على العنوان المرافق له. وتظهر أهميته الفعلية عند استخدام أزرار الاختيار radio buttons وصناديق التحقق check boxes، فقد تكون المنطقة التي يشغلها الزر ضيقة يصعب نقرها في بعض اﻷجهزة لذلك من السهل حينها النقر على العنوان الموافق لتفعيل الخيار. فالنقر على العنوان "I like cherry" في مثالنا التالي سيغير حالة عنصر اﻹدخال taste_cherry: <form> <p> <input type="checkbox" id="taste_1" name="taste_cherry" value="cherry" /> <label for="taste_1">I like cherry</label> </p> <p> <input type="checkbox" id="taste_2" name="taste_banana" value="banana" /> <label for="taste_2">I like banana</label> </p> </form> ملاحظة: ستجد هذا المثال على جت-هب وبإمكانك أيضًا تجربته مباشرة. عناوين متعددة لا يوجد في الواقع ما يمنعك من وضع عدة عناوين لعنصر التحكم نفسه، لكن الفكرة سيئة من منظور اﻷدوات المساعدة، فقد تُخطئ تلك اﻷدوات في التعامل معها. وإن أردت استخدام عناوين متعددة، عليك وضع عنصر التحكم مع عناوينه المرتبطة به ضمن عنصر <label> وحيد: <p>Required fields are followed by <span aria-label="required">*</span>.</p> <!-- ينجح اﻷمر كالتالي --> <!--div> <label for="username">Name:</label> <input id="username" type="text" name="username" required> <label for="username"><span aria-label="required">*</label> </div--> <!-- لكن من اﻷفضل أن ينجز كالتالي --> <!--div> <label for="username"> <span>Name:</span> <input id="username" type="text" name="username" required> <span aria-label="required">*</span> </label> </div--> <!-- وهذا هو التنفيذ اﻷفضل --> <div> <label for="username">Name: <span aria-label="required">*</span></label> <input id="username" type="text" name="username" required /> </div> See the Pen web- form-structure1 by Hsoub Academy (@HsoubAcademy) on CodePen. توضح الفقرة النصية في بداية الشيفرة أن الحقول المعلمة * ضرورية. ولا بد من تضمين هذه الفقرة في البداية وقبل أن يطبق محتواها، لكي يتمكن المستخدمون ضعاف البصر أو حتى اﻷصحاء -الذين يعتمدون على قارئات الشاشة- من فهم معناها قبل أن يصلوا إلى الحقول الضرورية في الاستمارة. وعلى الرغم من أنها علامة * مفيدة للأصحاء وواضحة، لكن لا يمكن الاعتماد عليها كليًا. إذ ستلفظ قارئات الشاشة هذا المحرف على الشكل "star"، ولن تكون مفيدة هنا، ولا بد أن تظهر الكلمة "مطلوب required" عندما يمرر المستخدم السليم مؤشر الفأرة فوق العنوان، ويُنجز هذا اﻷمر باستخدام السمة title، أما نطق قارئات الشاشة لقيمة هذه السمة، فيعتمد على إعداداتها. لهذا من اﻷسلم أن نضيف أيضًا السمة aria-label التي تنطقها قارئات الشاشة دائمًا. تتفاوت فعالية الطرق في المثال السابق في تنفيذ اﻷمر: لا يُقرأ العنوان في الطريقة اﻷولى إطلاقًا مع عنصر الدخل، فما تحصل عليه هو لفظ العبارة "edit text blank" إضافة إلى العناوين الفعلية التي تُقرأ بشكل منفصل، وستربك العناوين المتعددة قارئ الشاشة. تبدو اﻷشياء أوضح بقليل في الطريقة الثانية، إذ يُقرأ العنوان مع عنصر الدخل "name star name edit text required"، لكن العناوين ستُقرأ أيضًا بشكل منفصل، وهذا مربك قليلًا. تحسن الوضع هنا لوجود عنوان مرتبط بالعنصر <input>. الطريقة الثالثة هي اﻷفضل، إذ يُقرأ العنوان الفعلي في نفس الوقت ويُقرأ عنصر العنوان بالشكل "name required edit text". ملاحظة: قد تختلف النتائج قليلًا وفقًا لقارئ الشاشة الذي تستخدمه. ملاحظة: ستجد هذا المثال على جت-هب وبإمكانك تجربته مباشرة. لا تحاول اختيار المثال بشكله الحالي، ابق على طريقة واحدة وحوّل الطريقتين الباقيتين إلى تعليقات، كي لا تُربك قارئات الشاشة بوجود عناوين متعددة وعناصر إدخال متعدد لها نفس الاسم. هيكليات شائعة لتنظيم استمارات الويب تذكر دائمًا أن هيكلة استمارة الويب تكون باستخدام HTML، وتستطيع استخدام كامل إمكانيات لغة التوصيف هذه في هيكلة الاستمارة. وقد تلاحظ من اﻷمثلة السابقة أن تغليف العنوان وعنصر التحكم ضمن العنصر <li> ممارسة شائعة وتوضع هذه العناصر بدورها ضمن عنصر قائمة <ul> أو <ol>، كما يُستخدم أيضًا العنصر <div>. وينصح باستخدام القوائم في حال كانت عناصر اﻹدخال على شكل صناديق تحقق أو أزرار خيارات متعددة. وإضافة إلى العنصر <fieldset>، يشيع استخدام عناصر العناوين الرئيسية في HTML مثل <h1> و <h2> والعنصر <section> لبناء هيكليات أكثر تعقيدًا لاستمارات الويب. ويعود اﻷمر إليك في انتقاء اﻷسلوب المناسب لكتابة الشيفرة على أن تكون النتيجة استمارة قابلةً للاستخدام وتراعي شمولية الوصول. وينبغي أن يوضع كل قسم يضم وظيفة محددة داخل عنصر <section> منفصل واستخدام العنصر <fieldset> لاحتواء أزرار الاختيارات المتعددة. تطبيق عملي: بناء هيكل لاستمارة ويب لنضع اﻵن ما تعلمناه من أفكار موضع التنفيذ ونبني استمارة أكثر عمقًا للاستمارات بإضافة وسيلة دفع. يتضمن الاستمارة الجديدة عددًا من أنواع عناصر التحكم التي قد لا تفهمها، لكن لا بأس حاليًا، إذ سنعرض تفاصيل أكثر عنها في مقال لاحق. وكل ما عليك اﻵن قراءة الوصف بتمعن أثناء متابعتك للتعليمات وفهمك للطريقة التي نغلف فيها العناصر في الاستمارة كي نبني هيكليتها، ولماذا فعلنا ذلك. أنشئ نسخة محلية من ملف التطبيق في مجلد جديد على حاسوبك. أنشئ تاليًا استمارةك باستخدام العنصر <form>. <form> أضف ترويسة وعنوان رئيسي ضمن العنصر <form>، لتعرض للمستخدم كيفية تمييز الحقول المطلوبة: <h1>Payment form</h1> <p> Required fields are followed by <strong><span aria-label="required">*</span></strong>. </p> نضيف تاليًا قسمًا أكبر من الشيفرة تحت الشيفرة السابقة، وسترى كيف نغلّف حقول معلومات الاتصال ضمن عنصر <section> مستقل. لدينا أيضًا مجموعة من ثلاث أزرار اختيار من متعدد، نضع كل منها ضمن عنصر قائمة <li> مستقل. ولدينا عنصرا إدخال نصيان <input> مع العنوان المرافق لكل منهما يوضع كل منهما ضمن فقرة نصية <p>، ولدينا أيضًا عنصر إدخال نصي لإدخال كلمة المرور: <section> <h2>Contact information</h2> <fieldset> <legend>Title</legend> <ul> <li> <label for="title_1"> <input type="radio" id="title_1" name="title" value="A" /> Ace </label> </li> <li> <label for="title_2"> <input type="radio" id="title_2" name="title" value="K" /> King </label> </li> <li> <label for="title_3"> <input type="radio" id="title_3" name="title" value="Q" /> Queen </label> </li> </ul> </fieldset> <p> <label for="name"> <span>Name: </span> <strong><span aria-label="required">*</span></strong> </label> <input type="text" id="name" name="username" required /> </p> <p> <label for="mail"> <span>Email: </span> <strong><span aria-label="required">*</span></strong> </label> <input type="email" id="mail" name="usermail" required /> </p> <p> <label for="pwd"> <span>Password: </span> <strong><span aria-label="required">*</span></strong> </label> <input type="password" id="pwd" name="password" required /> </p> </section> يضم القسم الثاني <section> من الاستمارة معلومات الدفع حيث نضيف ثلاثة عناصر تحكم مع عناوينها ويغلف كل منها ضمن فقرة نصية <p>. العنصر اﻷول هو قائمة منسدلة <select> لاختيار نوع بطاقة االائتمان، والثاني عنصر إدخال نصي <input> من النوع tel ﻹدخال رقم بطاقة اﻹئتمان (يمكن اختيار النوع number لكننا لم نجد شكله مناسبًا لواجهة المستخدم)، والثالث عنصر إدخال من النوع text ﻹدخال تاريخ إنتهاء صلاحية البطاقة، ويتضمن العنصر اﻵخير السمة placeholder التي تشير إلى التنسيق الصحيح، والسمة pattern التي تختبر صحة التنسيق الذي يدخله المستخدم. وقد أدخلت هذه العناصر اﻷخيرة في HTML5: <section> <h2>Payment information</h2> <p> <label for="card"> <span>Card type:</span> </label> <select id="card" name="usercard"> <option value="visa">Visa</option> <option value="mc">Mastercard</option> <option value="amex">American Express</option> </select> </p> <p> <label for="number"> <span>Card number:</span> <strong><span aria-label="required">*</span></strong> </label> <input type="tel" id="number" name="cardnumber" required /> </p> <p> <label for="expiration"> <span>Expiration date:</span> <strong><span aria-label="required">*</span></strong> </label> <input type="text" id="expiration" name="expiration" required placeholder="MM/YY" pattern="^(0[1-9]|1[0-2])\/([0-9]{2})$" /> </p> </section> نضيف القسم اﻷخير البسيط الذي يضم زرًا <button> من النوع submit ﻹرسال البيانات، لهذا ضع الشيفرة التالية أسفل شيفرة الاستمارة: <section> <p> <button type="submit">Validate the payment</button> </p> </section> أكمل الاستمارة بإضافة وسم اﻹغلاق للعنصر <form>: </form> طبقنا كذلك تنسيقات CSS إضافية على الاستمارة، فإن أردت إجراء تغييرات على مظهره، انسخ التنسيقات التي تحتاجها وعدّل عليه. See the Pen Untitled by Hsoub Academy (@HsoubAcademy) on CodePen. الخلاصة بهذا نكون قد استعرضنا أهم العناصر المستخدمة لهيكلة استمارة الويب وإعطاء كل عنصر دلالته الخاصة، وشرحنا الطريقة المثلى لاستخدام هذه العناصر في هيكلة استمارة الويب لتكون قابلةً للاستخدام وتدعم شمولية الوصول بالطريقة الصحيحة، وسنتوسع في شرح الميزات التي عرضناها هنا في مقالات قادمة. ترجمة -وبتصرف- للمقال: How to structure a web form اقرأ أيضًا المقال السابق: إضافة تنسيق بسيط لاستمارة الويب وإرسال بياناتها للخادم متحكمات واجهة المستخدم وكيفية عرضها: متحكمات الدخل أدوات سهولة الوصول Accessibility اللازمة في عملية تطوير الويب معالجة مشاكل سهولة الوصول Accessibility الشائعة للتوافق مع المتصفحات
-
تعرفنا في المقال السابق على أساسيات العمل مع استمارات الويب Web forms وأهميتها كوسيلة للتفاعل بين المستخدم وموقع الويب، ووضحنا بمثال عملي كيفية بناء استمارة تواصل بسيطة باستخدام عناصر HTML الأساسية، وسنشرح في مقال اليوم كيفية تنسيق هذه الاستمارة لمنحها منظرًا أكثر احترافية، وطريقة التعامل مع إرسال البيانات التي يكتبها المستخدم في هذه الاستمارة إلى خادم الويب. التنسيق اﻷساسي لاستمارة الويب باستخدام CSS بعد أن انتهيت في المقال السابق من كتابة شيفرة HTML التي تحدد مكونات أو عناصر الاستمارة الأساسية وعرضت الصفحة في المتصفح، لن يعجبك مظهر هذه الاستمارة بالتأكيد، حيث سيبدو منظرها بدائيًا جدًا كما في الصورة التالية: ملاحظة: إن كنت تعتقد أن شيفرتك ليس صحيحة، قارن الكود الذي كتبته مع هذه النسخة المكتملة من التطبيق على جت-هاب للاطلاع على النسخة بعد تنسيقها (وبإمكانك تجربتها مباشرة) أيضًا. لتحسين مظهر الاستمارة نحتاج لتنسيقها باستخدام لغة CSS وسنركز في مقال اليوم فقط على إضافة بعض تنسيقات CSS البسيطة كي نحسّن مظهر الاستمارة العام، لكننا لن نتطرق إلى تنسيق عناصر الاستمارات بشكل مكثف فهذا اﻷمر واسع ومتشعب، فاستمارات الويب تتكون عادة من عدة عناصر مختلفة مثل الحقول النصية والأزرار ومربعات الاختيار والقوائم المنسدلة وغيرها وكل نوع من هذه العناصر قد يتطلب تنسيقًا خاصًا به، كما أن هناك تنسيقات تتعلق بسهولة الوصول accessibility، وتنسيقات أخرى تتعلق بعرض رسائل خطأ عند إدخال بيانات غير صحيحة وغيرها من التفاصيل التي تقع خارج نطاق مقالنا حاليًا. لتنسيق الاستمارة أضف بداية العنصر <style> إلى كود صفحتك ضمن الترويسة <head>، وسيبدو اﻷمر كالتالي: <style> … </style> بعدها أضف الشيفرة التالية ضمن وسمي البداية والنهاية <style> <style/> للعنصر السابق: body { /* ضبط موقع الاستمارة منتصف الصفحة */ text-align: center; } form { display: inline-block; /* اﻹطار الخارجي للاستمارة */ padding: 1em; border: 1px solid #ccc; border-radius: 1em; } ul { list-style: none; padding: 0; margin: 0; } form li + li { margin-top: 1em; } label { /* تنسيق الحجم والمحاذاة بشكل منتظم */ display: inline-block; min-width: 90px; text-align: right; } input, textarea { /*للتأكد من أن كل الحقول النصية لها نفس إعدادات الخط monospace خط أحادي الفراغ textarea افتراضيًا، يكون للعناصر */ font: 1em sans-serif; /* حجم متساوي لجميع الحقول النصية */ width: 300px; box-sizing: border-box; /* ضبط حواف حقول الاستمارة */ border: 1px solid #999; } input:focus, textarea:focus { /* تظليل إضافي للعناصر التي تتلقى تركيز الدخل */ border-color: #000; } textarea { /* محاذاة الحقول النصية إلى جوار عناوينها*/ vertical-align: top; /* تأمين مساحة للكتابة ضمنها */ height: 5em; } .button { /* محاذاة الزر مع الحقول النصية */ padding-left: 90px; /* نفس قياس العناوين */ } button { /*هوامش إضافية تماثل المسافة بين العناوين والحقول النصية المقابلة */ margin-left: 0.5em; } احفظ التغيّرات وأعد تحميل الصفحة، وسيبدو الاستمارة أفضل حالًا كما يلي: See the Pen webform-1 by Hsoub Academy (@HsoubAcademy) on CodePen. ملاحظة: يمكنك مقارنة الكود الذي كتبته مع هذه النسخة من التطبيق على جت-هاب للاطلاع على النسخة بعد تنسيقها ، وبإمكانك تجربتها مباشرة أيضًا. إرسال بيانات الاستمارة إلى الخادم نأتي إلى القسم الأخير من التطبيق وقد يكون الأصعب، وهو التعامل مع تسليم بيانات هذه الاستمارة إلى الخادم أو بمعنى آخر التخاطب بين العميل والخادم، حيث يحدد العنصر <form> كما ذكرنا في المقال السابق أين ستذهب البيانات وكيف ستنقل للخادم من خلال السمتين action و method. إذ تحدد action الوجهة النهائية للبيانات المُدخلة في الاستمارة، بينما تحدد method الطريقة التي سيتم بها إرسال البيانات إلى الخادم وهناك طريقتان رئيسيتان يمكن استخدامهما: الأولى هي GET حيث تلحق بيانات الاستمارة بعنوان URL وترسل كجزء من رابط الصفحة ولا تناسب هذه الطريقة إرسال بيانات حساسة لأن البيانات المرسلة ستكون مرئية في شريط العنوان، والثانية هي POST حيث ترسل البيانات كجزء من جسم طلب HTTP وهي تناسب إرسال البيانات الحساسة أو إرسال كميات كبيرة من البيانات من العميل للخادم. وكنا قد زودنا سابقًا كل عنصر من عناصر الاستمارة بالسمة name وهي ضرورية عند العمل مع طرفي العميل والخادم. إذ تخبر هذه السمة المتصفح على الاسم الذي سيطلقه على كل جزء من البيانات، أما الخادم فسيتعامل مع هذه البيانات وفق هذا الاسم، ثم ترسل البيانات إلى الخادم على شكل أزواج أو ثنائيات مكونة من اسم/قيمة. فلتسمية البيانات في الاستمارة ستحتاج إلى السمة name لكل عنصر تحكم يجمع جزءًا من البيانات المطلوبة. لنلق نظرة على شيفرة الاستمارة ونتذكر عناصرها: <form action="/my-handling-form-page" method="post"> <ul> <li> <label for="name">Name:</label> <input type="text" id="name" name="user_name" /> </li> <li> <label for="mail">Email:</label> <input type="email" id="mail" name="user_email" /> </li> <li> <label for="msg">Message:</label> <textarea id="msg" name="user_message"></textarea> </li> <li class="button"> <button type="submit">Send your message</button> </li> </ul> </form> على جانب العميل، سيرسل النموذج ثلاثة أجزاء من البيانات أسماؤها هي "user_name" و "user_email" و "user_message" إلى عنوان URL التالي "/my-handling-form-page/" باستخدام الطلب HTTP POST. على جانب الخادم، سيكون لدينا سكريبت بلغة برمجة معينة موجود على العنوان "/my-handling-form-page/" يستقبل البيانات المرسلة من النموذج وفق ثلاثة قوائم من الشكل مفتاح/قيمة ضمن طلب HTTP. أما طريقة تعامل السكريبت مع هذه البيانات فهو أمر عائد للمبرمج، فلكل لغة برمجة تعمل على الخادم آلية مخصصة للتعامل مع البيانات. هذا الموضوع خارج إطار مقالنا الحالي لكننا سنعرض بعض الأمثلة المختلفة مع شرحها بالتفصيل في مقال لاحق. الخلاصة تهانينا! لقد بنيت أول استمارة ويب خاصة بك ونسقتها بطريقة أنيقة وتعرفت على أساسيات حول التعامل مع بياناتها، لا زالنا بالطبع في بداية التعامل مع الاستمارات، وسنتابع الغوص في بناء استمارات ويب وهيكلتها وتنسيقها وإرسال بياناتها للخادم في مقالات قادمة، فإمكانات استمارات الويب أكبر بكثير مما عرضناه في هذا المقال. ترجمة -وبتصرف- للجزء الثاني من المقال: Your first form اقرأ أيضًا المقال السابق: أساسيات التعامل مع استمارات الويب Web Forms كيفية تنسيق الموقع الإلكتروني باستخدام تعليمات HTML تعرف على أساسيات لغة CSS التحقق من سهولة الوصول لصفحات الويب معالجة بيانات طلبيات HTTP والتعامل مع أخطاء رفع الملفات في PHP
-
سنبدأ في هذا المقال والمقالات التالية شرح أساسيات التعامل مع استمارات الويب Web forms. فاستمارات الويب هي أداة قوية للتفاعل مع المستخدمين وخاصة في جمع بيانات عنهم أو السماح لهم في التحكم بواجهة المستخدم، سنحرص على تغطية كامل ميزات هذه الأداة القوية ونشرح كل ما يتعلق بها بما في ذلك كتابة شيفرة HTML وتنسيق عناصر التحكم وتسليم البيانات إلى الخادم. متطلبات العمل على هذه السلسلة الاطلاع على سلسلة المقالات مدخل إلى HTML، وعندها لن تجد صعوبة في فهم القسم التمهيدي من مقالات السلسلة، وستكون قادرًا على الاستفادة من معلومات مقال أدوات التحكم اﻷصلية لاستمارات الويب. تعلم أساسيات التنسيق باستخدام CSS وأساسيات البرمجة بلغة جافا سكريبت. إذ عليك تعلم بعض التقنيات الخاصة في تنسيق عناصر التحكم التي تضيفها للاستمارة، وكتابة سكريبتات التعامل مع تقييم بيانات الاستمارة وإنشاء استمارات ويب مخصصة. ولهذا كان لا بد من عرض استمارات الويب في سلسلة مقالات خاصة بها ولم نخلطها مع سلاسل المقالات التي تحدثنا فيها عن HTML و CSS وجافا سكريبت. فعناصر الاستمارات أكثر تعقيدًا من عناصر HTML الأخرى، وتتطلب استخدامًا محددًا لتقنيات CSS وجافا سكريبت إن أردت الحصول على كامل إمكانيات الاستمارات. ملاحظة: إن كنت تستخدم حاسوبًا أو جهازًا لوحيًا أو غيره من الأجهزة التي لاتمكّنك من إنشاء ملفاتك الخاصة وتنفيذ الشيفرات البرمجية محليًا، فجرِّب الشيفرة التي ستجدها في الأمثلة من خلال برامج كتابة شيفرة على الإنترنت مثل Glitch أو JSBin فهذه البيئات تساعدك على كتابة شيفرتك وتنفيذها دون الحاجة إلى تثبيت برمجيات خاصة على جهازك. سنبدأ في هذا المقال التمهيدي بالتعرف بشكل مفصل على استمارات الويب وأهميتها وننشئ أول استمارة ويب خاصة بنا من خلال تصميم استمارة تواصل بسيطة contact form وتنفيذها برمجيًا باستخدام عناصر HTML، ونستكمل في المقال التالي تنسيقها. ما هي استمارات الويب Web Forms تُعد استمارات ويب إحدى النقاط اﻷساسية التي تعزز التواصل بين المستخدم و موقع الويب أو التطبيق. إذ تتيح للمستخدم إدخال البيانات التي تُرسل عادة إلى الخادم كي يعالجها ويخزنها في قواعد البيانات، أو تستخدم لتحديث تصميم واجهة المستخدم مباشرة بطريقة ما (مثل إضافة عنصر جديد إلى قائمة أو إظهار وإخفاء بعض معالم الواجهة). تتكون استمارات الويب المبنية على أساس HTML من عنصر تحكم بالاستمارة أو أكثر، إضافة إلى عناصر أخرى تساعد في هيكلة الاستمارة عمومًا، لهذا يُشار إليها أحيانًا باسم استمارات HTML. وقد تكون عناصر التحكم حقولًا نصية مفردة أو متعددة اﻷسطر أو قوائم منسدلة drop-down box أو أزرار أو صناديق تحقق check boxes أو أزرار اختيار من متعدد radio buttons، وتنشأ هذه العناصر عادة باستخدام العنصر <input> مع وجود بعض العناصر اﻷخرى التي سنطلع عليها تباعًا. يمكن برمجة عناصر التحكم أيضًا كي نجبر المستخدم إدخال صيغ أو قيم مخصصة (من أجل تقييم الاستمارة) وأن نقرن هذه العناصر بنصوص توضيحية تصف الغاية من هذه العناصر لضعيفي البصر والمستخدم السليم لتعزيز الشمولية وسهولة الوصول Accessibility. تصميم استمارة الويب اﻷولى من الجيد التراجع إلى الخلف قليلًا قبل الشروع في كتابة الشيفرة والتفكير مليًا بالاستمارة الذي نريد تصميمه. إذ يساعدك تصور مخطط أولي لاستمارةك في تحديد مجموعة البيانات التي تريد أن تطلبها من المستخدم. وتذكّر أن الاستمارات الكبيرة تزيد عدد المستخدمين الذي يرفضون إكمالها وذلك من منظور تجربة المستخدم UX. لهذا أبق الاستمارة بسيطًا وركّز على البيانات التي تحتاجها بالفعل. إن خطوة تصميم الاستمارة مهمة جدًا في بناء الموقع أو التطبيق. لن نغطي في هذا المقال تصميم استمارات الويب من منظور تجربة المستخدم لكن ستجد الكثير من اﻷفكار في كتاب مدخل إلى تجربة المستخدم من إنتاج أكاديمية حسوب. سنبني استمارة ويب تتيح للمستخدمين التواصل معنا، ولتبدأ بالرسم التوضيحي لهذا الاستمارة كما في الشكل التالي: تتضمن الاستمارة ثلاث حقول نصية وزر واحد، وتطلب من المستخدم إدخال اسمه وبريده اﻹلكتروني والرسالة التي يريد إرسالها كي تُخزّن على الخادم عند النقر على زر الإرسال أسفل الاستمارة. تطبيق عملي: إنجاز شيفرة HTML الخاصة بالاستمارة نستخدم في استمارةنا عناصر HTML التالية: عنصر استمارة ويب <form>. عناصر <input>. زر <button>. وقبل أن تتابع معنا، أنشئ نسخة خاصة بك من قالب HTML الخاص بالتطبيق الذي ستضع استمارتك ضمنه. العنصر <form> تبدأ الاستمارات جميعها بالعنصر <form> كالتالي: <form action="/my-handling-form-page" method="post">…</form> يحدد العنصر السابق رسميًا استمارة HTML، ويمثل حاوية تشابه العنصر <select> أو <footer> لكنه مخصص للاستمارات، ويتضمن عدة سمات تحدد سلوك الاستمارة. وتُعد جميع سمات العنصر اختيارية، لكن، ومن الناحية التطبيقية، تُستخدم سميتين على اﻷقل هما action و method: تحدد السمة action موقع عنوان URL الذي تُرسل إليه بيانات الاستمارة عند تسليمها. تحدد السمة method طلب HTTP المستخدم في إرسال البيانات ( get أو post عادة). ملاحظة: سنرى طريقة استخدام السمتين السابقتين في مقالات قادمة. العنصران <textarea> و <input> إن الاستمارة التي نبنيها هي استمارة اتصال contact form بسيطة تتضمن قسم إدخال البيانات فيه ثلاثة حقول نصية مع عناوين <label> تستخدم للدلالة على ما يجب إدخاله في كل حقل: يتكون عنصر إدخال اسم المستخدم من عنصر إدخال نصي وحيد السطر. يتكون عنصر إدخال البريد اﻹلكتروني عنصر إدخال من النوع email وهو حقل إدخال نصي يقبل عناوين بريد إلكتروني صحيحة فقط. يتكون عنصر إدخال الرسالة على عنصر نصي متعدد اﻷسطر هو <textarea>. إذًا سنحتاج إلى شيفرة HTML التالية لتنفيذ الاستمارة: <form action="/my-handling-form-page" method="post"> <ul> <li> <label for="name">Name:</label> <input type="text" id="name" name="user_name" /> </li> <li> <label for="mail">Email:</label> <input type="email" id="mail" name="user_email" /> </li> <li> <label for="msg">Message:</label> <textarea id="msg" name="user_message"></textarea> </li> </ul> </form> حدّث شيفرتك لتكون مماثلة للشيفرة السابقة. نستخدم عناصر القائمة <li> لهيكلة الشيفرة وتسهيل تنسيق الاستمارة (ما سنراه لاحقًا في هذا المقال). كما وضعنا عنوانًا صريحًا لكل عصر تحكم في الاستمارة لسهولة الاستخدام وسهولة الوصول Accessibility. لاحظ أيضًا استخدام السمة for في جميع العناوين، وتأخذ هذه السمة قيمة المعرّف id لعنصر التحكم المرتبطة به، وهي الطريقة التي تربط فيها عنوانًا بعنصر تحكم. إن الفائدة من استخدام هذا اﻷسلوب كبيرة، إذ يسمح ذلك للمستخدم بالنقر على العنوان لتفعيل عنصر التحكم المرتبط به، كما تعطي اسمًا مقروءًا بالنسبة لقارئات الشاشة عند مرور مؤشر الفأرة فوقه، وستجد تفاصيل أكثر في مقالات قادمة. وبالنسبة لعناصر اﻹدخال <input>، ستجد أن السمة type هي الأهم، لأنها تُعرّف الشكل الذي يظهر فيه عنصر اﻹدخال وسلوكه. استخدمنا في مقالنا السمة type بالشكل type = text لعنصر اﻹدخال اﻷول وهي القيمة الافتراضية لهذه السمة، وتمثل مربعًا نصيًا بسيطًا من سطر واحد يقبل أية مدخلات نصية. واستخدمنا السمة بالشكل type = email لعنصر اﻹدخال الثاني ليكون عنصر إدخال نصي وحيد السطر يقبل عناوين بريد إلكتروني صحيحة كمدخل له. إذ تحوّل هذه القيمة عنصر اﻹدخال النصي إلى نوع من العناصر الذكية القادرة على إجراء تقييم للبيانات المدخلة إليه من قبل المستخدم، كما يسبب عرض لوحة مفاتيح مناسبة أكثر ﻹدخال بريد إلكتروني (في أجهزة الهاتف الذكية) كأن يعرض الرمز @افتراضيًا. وسنتحدث في مقال لاحق عن مفهوم التحقق من صحة البيانات المدخلة. وأخيرًا وليس أخرًا، لاحظ الاختلاف في الصياغة القواعدية لعنصر اﻹدخال <input> (دون وسم إغلاق) مقارنة بالعنصر <textarea></textarea> (وجود وسم إغلاق)، وهو أحد اﻷمور الغريبة في HTML. وسبب ذلك أن العنصر <input> عنصر فارغ void لا يحتاج إلى وسم إغلاق، بينما العنصر <textarea> ليس فارغًا لهذا يجب إغلاقه. ولهذا اﻷمر تأثيره على نواحي خاصة في هيكلة الاستمارة، إذ علينا في حالة عنصر اﻹدخال استخدام السمة value لتقديم نص افتراضي داخل العنصر: <input type="text" value="by default this element is filled with this text" /> بينما إن أردت وضع نص افتراضي داخل العنصر <textarea> سيكون ذلك بين وسمي البداية والنهاية: <textarea> by default this element is filled with this text </textarea> العنصر <button> نحتاج إلى الزر <button> في الاستمارة ﻹرسال بيانات المستخدم (أو تسليمها submit) عندما يملأ جميع الحقول المطلوبة. لهذا سنضيف الشيفرة التالية إلى ملف HTML فوق وسم اﻹغلاق <ul/> مباشرة: <li class="button"> <button type="submit">Send your message</button> </li> يقبل العنصر <button> السمة type أيضًا ولها ثلاثة قيم هي submit, reset, button: يؤدي النقر على زر من النوع submit، وهي القيمة الافتراضية للسمة، إلى إرسال بيانات الاستمارة إلى صفحة الويب التي تحددها السمة action للعنصر <form>. ويؤدي النقر على زر من النوع reset إلى ضبط قيم عناصر التحكم في الاستمارة إلى القيم الافتراضية مباشرة. ويُعد هذا الأمر ممارسة سيئة من منظور تجربة المستخدم، لهذا عليك تفادي استخدام هذا النوع من الأزرار ما لم يكن لديك سبب وجيه جدًا في استخدامه. لا يؤدي النقر على زر من النوع button إلى أي شيء، وقد يبدو هذا اﻷمر بلا معنى لكنه عظيم الفائدة في بناء عناصر تحكم مخصصة، إذ تستطيع تحديد وظيفة هذه الأزرار باستخدام جافا سكريبت. ملاحظة: بإمكانك استخدام عنصر اﻹدخال <input> مع السمة type ﻹنشاء زر مثل <'input type = 'submit>، لكن لا يمكن إضافة سوى محتوى نصي إلى عنوان الزر، بينما يتيح لك العنصر <button> إضافة محتوى معقد إلى الزر كالصور مثلًا. الخلاصة قدمنا في مقال اليوم لأحد أهم عناصر صفحات الويب وهي الاستمارات forms، وألقينا نظرة عامة على مختلف عناصر التحكم في استمارات الويب مثل الحقول النصية والقوائم المنسدلة والأزرار، وسنتابع في المقال التالي تنسيق هذه الاستمارة ونوضح طريقة تسليم بياناتها للخادم، كما سنشرح في مقالات تالية المزيد من الأمور المتعلقة باستمارات الويب مثل التحقق من صحة مدخلاتها والاعتبارات اﻷمنية المرتبطة بإرسال هذه البيانات للخادم. ترجمة -وبتصرف- لمقال: Web form building blocks ومقال: Your first form اقرأ أيضًا النماذج (Forms) في HTML5 استخدام نماذج الويب والتحقق منها في فلاسك باستخدام الإضافة Flask-WTF العمل مع الاستمارات Forms في تطبيقات جانغو الأزرار والأيقونات والنماذج في إطار العمل Bootstrap
-
من اﻷمور التي ستحتاجها غالبًا عند كتابة شيفرة برمجية لصفحات وتطبيقات الويب هي التعامل مع مستندات الويب بطريقة أو بأخرى. وعادة ما يجري ذلك من خلال شجرة DOM وهي واجهة برمجية للتحكم بملف HTML وتنسيق المعلومات التي تستخدم الكائن Document بكثرة. سنفرد هذا المقال للحديث عن استخدام DOM بالتفصيل، إضافة إلى بعض الواجهات التي يمكنها تغيير بيئة العمل بطرائق مفيدة. ننصحك قبل المتابعة في قراءة هذه المقالات أن: تكون ملمًا بلغتي HTML و CSS. تكون ملمًا بلغة جافا سكريبت. تطلع على سلاسل المقالات السابقة التي ناقشت أساسيات جافا سكريبت والكائنات في جافا سكريبت. اﻷجزاء المهمة في متصفح الويب تُعد المتصفحات برمجيات معقدة تضم الكثير من اﻷجزاء التي لا يمكن لمطور الويب التعامل معها أو التحكم بها باستخدام جافا سكريبت. وقد تعتقد أن هذه المحدودية أمر سيء، لكن الأسباب الكامنة وراء إقفال بعض أجزاء المتصفحات وجيهة بالفعل ويتعلق معظمها باﻷمان. تخيّل مثلًا أن تتمكن صفحة ويب ما من الوصول إلى كلمات السر التي تخزنها في المتصفح أو غيرها من المعلومات الحساسة ومن ثم تستخدم هذه البيانات في الدخول إلى صفحات أخرى! وعلى الرغم من تلك المحدوديات، تمنح واجهات الويب البرمجية إمكانية الوصول إلى الكثير من الوظائف التي تمكنك من تنفيذ أشياء مفيدة جدًا في صفحات الويب، وهنالك بالفعل بعض النقاط الواضحة التي تراها باستمرار وتستخدمها في الشيفرة. تأمل مثلًا المخطط التالي الذي يمثل الأجزاء الرئيسية من المتصفح التي تشارك مباشرة في عرض صفحة الويب: النافذة window: وهي الجزء الذي تُحمّل ضمنه صفحة الويب ويُمثّل في جافا سكريبت بالكائن Window. ونتمكن باستخدام التوابع التي يقدمها هذا الكائن من تنفيذ أشياء عديدة مثل الحصول على حجم النافذة (باستخدام خاصيات مثل Window.innerWidth و Window.innerHeight) أو التعامل مع المستند الذي يُحمّل ضمنها وتخزين بيانات متعلقة به في طرف العميل (مثل استخدام قاعدة بيانات محلية أو غيرها من آليات التخزين) أو ربط معالج أحداث بالنافذة الحالية وغيرها الكثير. المستكشف navigator: ويمثّل حالة وهوية المتصفح (العميل الذي يستخدم المتصفح) عندما يتصفح الويب. يُمثَّل المستكشف في جافا سكريبت عن طريق الكائن Navigator الذي يُستخدم في الحصول على معلومات متعلقة بالمستخدم مثل اللغة المفضلة والتقاط بث كاميرا الويب الخاصة به وغيرها. المستند document: تمثله شجرة DOM في المتصفح وهو في الواقع صفحة الويب التي تُحمّل ضمن النافذة. ويُمثَّل في جافا سكريبت من خلال الكائن Document. ويُستخدم هذا الكائن في التعامل مع عناصر HTML وأصناف CSS في المستند واستخلاص المعلومات منها أو تعديلها مثل الحصول على مرجع لأحد عناصر شجرة DOM وتغيير محتواه النصي وتنسيقه، كما يمكّنك من إنشاء عناصر جديدة وإضافتها إلى العنصر الحالي كعناصر أبناء، وكذلك حذفهم جميعًا. ونركز في مقالنا بشكل أساسي على التعامل مع المستند، مع بعض اﻹضافات اﻷخرى. شجرة DOM (نموذج كائن المستند) يُمثّل المستند الذي يُحمَّل ضمن كل نافذة فرعية من المتصفح على شكل شجرة من العناصر تُعرف بنموذج كائن المستند Document Object Model أو شجرة DOM اختصارًا. وتسهّل هذه الشجرة التي يُنشئها المتصفح الوصول إلى عناصر HTML برمجيًا. كما يستخدم المتصفح هذه الشجرة لتطبيق التنسيقات وغيرها من المعلومات على العنصر الصحيح عند تصيير الصفحة. وستتمكن أنت كمطوّر من التعامل مع شجرة DOM من خلال جافا سكريبت بعد أن تُصيّر الصفحة. نعرض تاليًا مثالًا بسيطًا لتوضيح اﻷمر (يمكنك تجربته مباشرة أيضًا). لهذا جرّب أن تفتح الملف في متصفحك وسترى صفحة بسيطة جدًا تضم العنصر <section> وضمنه صورة وفقرة نصية تضم رابطًا تشعبيًا. <!doctype html> <html lang="en-US"> <head> <meta charset="utf-8" /> <title>Simple DOM example</title> </head> <body> <section> <img src="dinosaur.png" alt="A red Tyrannosaurus Rex: A two legged dinosaur standing upright like a human, with small arms, and a large head with lots of sharp teeth." /> <p> Here we will add a link to the <a href="https://www.mozilla.org/">Mozilla homepage</a> </p> </section> </body> </html> تبدو شجرة DOM كالتالي: يُدعى كل مدخل في الشجرة عقدة node وبإمكانك ملاحظة أن بعض العقد في المخطط السابق تمثل عناصر مثل HTML و HEAD و META، كما يمثل غيرها نصوصًا (معرّفة بالوسم text#). وهنالك أيضًا أنواع أخرى من العقد لكن ما ذكرناه هي العقد الرئيسية التي تواجهها. يُشار إلى العقد أيضًا بموقعها ضمن الشجرة نسبةً إلى عقد أخرى: عقدة جذرية root node: وهي أعلى عقدة في الشجرة وهي دائمًا العقدة HTML في ملفات HTML (وتختلف من لغة تأشير إلى أخرى). عقدة ابن child node: وهي عقدة تقع مباشرة ضمن عقدة أخرى، مثل العنصر IMG داخل العنصر SECTION في المثال السابق. عقدة سليلة Descendant node: وهي عقد تقع في أي مكان داخل عقدة أخرى، مثل العنصر IMG داخل العنصر SECTION في المثال السابق فهي أيضًا عقدة سليلة. وهي ليست عقدة ابن للعنصر BODY لأنها تبعد مستويين عنه لكنها عقدة سليلة له. عقدة أم Parent node: وهي عقدة تحتوي على عقد أخرى ضمنها، مثل العقدة BODY التي تمثل عقدة أم للعنصر SECTION. عقد شقيقة Sibling nodes: وهي عقد تقع في نفس المستوى من شجرة DOM مثل العنصرين IMG و P. ومن اﻷفضل طبعًا أن تعتاد على هذه المصطلحات قبل العمل مع DOM، إذ تُستخدم في العديد من مصطلحات الشيفرة التي تصادفها، ومن المحتمل أن تكون صادفتها فعلًا إن درست صفحات التنسيق CSS (مثل المحدد السليل والمحدد الابن). تطبيق عملي: أساسيات العمل مع شجرة DOM لنبدأ العمل مع شجرة DOM من خلال التطبيق العملي التالي: أنشئ نسخة محلية من صفحة التطبيق والصورة التي ترافقها. أضف العنصر <script></script> إلى ملف الشيفرة بعد الوسم <body/> مباشرة. ولكي تتعامل مع عنصر ضمن شجرة DOM عليك تخزين مرجع إليه ضمن متغير، لهذا أضف الشيفرة التالية ضمن العنصر <script>: const link = document.querySelector("a"); وهكذا سيكون بإمكانك اﻵن التعامل مع العنصر الذي تخزن مرجعًا إليه باستخدام الخاصيات والتوابع المتاحة لهذا العنصر (تُعرَّف هذه الخاصيات والتوابع ضمن واجهات خاصة نذكر منها HTMLAnchorElement التي تتعامل مع الرابط التشعبي <a> وكذلك الواجهة اﻷم HTMLElement والواجهة Node التي تمثل جميع عقد الشجرة). سنبدأ العمل بتغيير النص ضمن الرابط التشعبي بتحديث قيم الخاصية Node.textContent، لهذا أضف هذا السطر تحت السطر السابق: link.textContent = "Mozilla Developer Network"; ولا بد أيضًا من تغيير عنوان URL الذي يشير إليه الرابط التشعبي كي لا ينقل المستخدم إلى وجهة خاطئة عند النقر عليه، لهذا نضيف السطر التالي تحت اﻷسطر السابقة: link.href = "https://developer.mozilla.org"; وتجدر اﻹشارة إلى وجود عدة طرق لاختيار العنصر الذي نريد التعامل معه في جافا سكريبت، ويُعد التابع ()Document.querySelector مقاربة حديثة ننصح بها لأنها تسمح لك باختيار العنصر باستخدام محددات تنسيق CSS. إذ يبحث التابع السابق عند استدعائه في شيفرتنا عن أول عنصر <a> في المستند، وإن أردت البحث عن المزيد العناصر كي تتعامل معها دفعة واحدة، تستطيع استخدام التابع ()Document.querySelectorAll الذي يبحث عن كل عنصر في الصفحة له نفس مُحدد CSS الذي نستهدفه ومن ثم يخزّن مراجعًا إلى هذه العناصر ضمن كائن شبيه بالمصفوفة يُدعى قائمة عقد NodeList. وتصادف أيضًا طرقًا أخرى للحصول على مراجع للعناصر ضمن الشجرة منها: ()Document.getElementByID: يبحث هذا التابع عن عنصر ذو قيمة محددة للسمة id. فلو أردنا الوصول إلى العنصر <p id="myId">My paragraph</p>، نمرر قيمة هذه السمة إلى التابع كالتالي: const elementRef = document.getElementById('myId'); ()Document.getElementsByTagName: تعيد كائنًا يشبه المصفوفة يضم كل العناصر من النوع نفسه مثل عناصر الفقرات النصية <p> أو الروابط التشعبية <a> وغيرها. يُمرر نوع العنصر إلى التابع كما في المثال التالي: const elementRefArray = document.getElementsByTagName('p'); يعمل هذان التابعان بشكل أفضل مع المتصفحات القديمة، لكنهما أقل ملائمة مقارنة بالتابع ()Document.querySelector. إنشاء عقد جديدة ووضعها ضمن شجرة DOM رأيت مما سبق ما يمكن إنجازه، لكننا سنتقدم الآن خطوة للأمام لإلقاء نظرة على كيفية إنشاء عناصر جديدة. بالعودة إلى الشيفرة السابقة سنحاول إنشاء مرجع إلى العنصر <section>، لهذا ضع الشيفرة التالية في نهاية السكريبت (وهذا ما سنفعله مع اﻷسطر التي نضيفها تاليًا): const sect = document.querySelector("section"); لننشئ الآن فقرة نصية جديدة باستخدام ()DocumentCreateElement ونضع فيها بعض العبارات وفق نفس اﻷسلوب الذي اتبعناه سابقًا: const para = document.createElement("p"); para.textContent = "We hope you enjoyed the ride."; نُلحق الفقرة النصية الجديدة بنهاية العنصر <section> من خلال التابع ()Node.appendChild: sect.appendChild(para); لنُضف أخيرًا عقدة نصية text node إلى الفقرة النصية التي تضم الرابط التشعبي، لهذا ننشئ أولًا العقدة النصية باستخدام التابع ()Document.createTextNode: const text = document.createTextNode( " — the premier source for web development knowledge.", ); ثم ننشئ مرجعًا إلى الفقرة النصية ونلحق بها العقدة النصية: const linkPara = document.querySelector("p"); linkPara.appendChild(text); هذا كل ما تحتاجه غالبًا ﻹضافة عقد إلى شجرة DOM وستستخدم التوابع السابقة كثيرًا عند بناء واجهة ديناميكية. نقل وإزالة عناصر تحتاج أحيانًا إلى نقل عقدة أو حذفها وهذا أمر ممكن، فإن أردت نقل الفقرة النصية التي تضم الرابط التشعبي إلى نهاية العنصر <section>: sect.appendChild(linkPara); ولن تحدث بالطبع عملية نسخ للفقرة النصية إلى المكان الجديد لأن المتغير linkPara هو مرجع إلى النسخة الوحيدة فقط من هذه الفقرة. لكن إن أردت إنشاء نسخة عنها وإضافتها إلى المكان الذي تريد، فهذا أمر ممكن أيضًا من خلال التابع ()Node.cloneNode. أما إزالة العقدة فهو أمر سهل وخاصة عندما يكون لديك مرجع إلى العقدة التي تريد إزالتها ومرجع إلى العقدة اﻷم لها، وعندها نستدعي التابع Node.removeChild: sect.removeChild(linkPara); باﻹمكان أيضًا إزالة عقدة بناء على مرجعها فقط، وهذا أمر شائع باستخدام التابع ()Element.remove: linkPara.remove(); لا تدعم المتصفحات الأقدم هذا التابع، إذ لا تمتلك أي طريقة لتطلب من عقدة حذف نفسها، لهذا عليك اﻹلتفاف على الموضوع كالتالي: linkPara.parentNode.removeChild(linkPara); أضف السطر السابق إلى شيفرتك. العمل مع تنسيقات العناصر بإمكاننا تعديل تنسيق CSS لعناصر الصفحة من خلال أكواد جافا سكريبت بطرق كثيرة. وكبداية بإمكانك الحصول على قائمة بكل ملفات التنسيق المرتبطة بالصفحة من خلال التابع Document.stylesheets الذي يعيد كائنًا مشابهًا للمصفوفات يضم عناصر من النوع CSSStyleSheet. وعندها ستتمكن من إضافة أو إزالة التنسيقات كما تشاء. ولن نتوسع في هذا اﻷسلوب لأنه قديم نوعًا ما ويصعب التعامل مع التنسيقات باستخدامه، وهنالك بالطبع طرق أسهل. تقتضي الطريقة اﻷولى إضافة تنسيقات سطرية inline style مباشرة ضمن العناصر التي تريد تعديل تنسيقها ديناميكيًا. يُنفَّذ الأمر من خلال الخاصية HTMLElemet.style التي تضم معلومات عن التنسيق السطري لكل عنصر في الصفحة. وعندما تغيير قيمة هذه الخاصية سيتغير تنسيق العنصر مباشرة. جرّب تغيير تنسيقات الفقرة النصية في شيفرتنا: para.style.color = "white"; para.style.backgroundColor = "black"; para.style.padding = "10px"; para.style.width = "250px"; para.style.textAlign = "center"; ثم أعد تحميل الصفحة لتشاهد التنسيقات الجديدة وقد طُبّقت على الفقرة النصية. ولو حاولت تفحّص الفقرة النصية من خلال المتصفح ستجد أن تنسيقًا سطريًا قد أضيف إلى شيفرتها: <p style="color: white; background-color: black; padding: 10px; width: 250px; text-align: center;"> We hope you enjoyed the ride. </p> ملاحظة: لاحظ كيف كُتبت نسخ جافا سكريبت من تنسيقات CSS بأسلوب سنام الجمل camelCase (مثلًا backgroundColor) مقارنة مع أسلوب CSS اﻷصلي وهو أسلوب الكباب lower-kebab-case (مثلًا background-color). احرص ألا تختلط عليك اﻷمور. وهنالك أيضًا طريقة شائعة أخرى للتعامل مع تنسيق العناصر دينياميكيًا، سنلقي عليها نظرة اﻵن: احذف اﻷسطر الخمسة اﻷخيرة التي أضفتها. أضف ما يلي ضمن ترويسة الصفحة <head>: <style> .highlight { color: white; background-color: black; padding: 10px; width: 250px; text-align: center; } </style> نستخدم التابع ()Element.setAttribute للتعامل مع عناصر HTML عمومًا، ويأخذ هذا التابع وسيطان أولهما الخاصية التي تريد ضبطها والثاني قيمة هذه الخاصية. وفي حالتنا ستكون الخاصية هي صنف الفقرة النصية class وقيمتها هي محدد CSS الذي نريد إسناده إلى الفقرة النصية لتنسيقها: para.setAttribute("class", "highlight"); حدّث صفحتك ولن ترى أية تغيير في شيفرة HTML للفقرة النصية لأن التنسيق قد طّبق عليها بمنحها صنف تنسيق وليس بإضافة تنسيق سطري إليها. إن اعتماد أسلوب معين هي حرية شخصية، ولكل اﻷساليب إيجابياتها وسلبياتها. فاﻷسلوب الأول بسيط ولا يتطلب إعدادات خاصة وهو جيد في بعض الحالات، بينما يعزز اﻷسلوب الثاني مبدأ فصل شيفرة التنسيق CSS عن شيفرة الصفحة HTML والذي يعُدّ ممارسة جيدة). وقد تميل مع تقدمك في مسيرة بناء تطبيقات أضخم وأعقد إلى استخدام اﻷسلوب الثاني أكثر، لكن في النهاية اﻷمر كما ذكرنا يعود إليك. ربما لم تلمس حتى اللحظة فائدة استخدام جافا سكريبت من إنشاء عناصر ساكنة، ومن الممكن كتابتها في شيفرة HTML مباشرة دون الحاجة إلى جافا سكريبت. فاستخدام جافا سكريبت أعقد وإنشاء المحتوى باستخدامها ينطوي على مشاكل أخرى تتعلق مثلًا بقدرة محركات البحث على قراءتها. لهذا سننقل في الفقرة القادمة إلى تطبيق آخر يُظهر استخدامًا عمليًا أكثر. ملاحظة: يمكنك إلقاء نظرة على التمرين التطبيقي المنتهي) (أو تجربته مباشرة أيضًا). تطبيق عملي: قائمة تسوّق ديناميكية نريد في هذا التطبيق إنشاء قائمة تسوّق بسيطة تسمح بإضافة عناصر بشكل ديناميكي إلى القائمة باستخدام نموذج إدخال مكوّن من مربع نصي وزر. وينبغي عند إضافة مُدخل إلى المربع النصي والنقر على الزر: أن يُعرض العنصر الجديد ضمن القائمة. أن يجاور كل عنصر زر لتتمكن من حذف هذا العنصر عند النقر على الزر. أن يُفرّغ مربع اﻹدخال ويكتسب تركيز الدخل بعد إضافة عنصر إلى القائمة استعدادًا ﻹدخال عنصر آخر. قد يبدو الشكل النهائي للتطبيق كالتالي: وﻹكمال التمرين، اتبع الخطوات التالية وتأكد أن سلوك القائمة سيكون مطابقًا لما أشرنا إليه: نزّل نسخة عن ملف HTML الخاص بالتطبيق، وضعها في مكان مناسب. وسترى أن الملف يضم تنسيقًا بسيطًا وعنصر <div> وعنوان وعنصر إدخال <input> وزر وقائمة فارغة وعنصر <script> تضع ضمنه كل الشيفرة التي تحتاجها. أنشئ ثلاثة متغيرات لتحمل مراجع إلى القائمة <ul> والزرين <button>. أنشئ دالة تعمل عند النقر على الزر. خزّن ضمن الدالة القيمة الحالية لعنصر اﻹدخال ضمن متغير. فرّغ عنصر اﻹدخال من المحتوى بإسناد القيمة '' له. أنشئ ثلاثة عناصر جديدة هي عنصر قائمة <li> وعنصر <span> وعنصر <button>. ألحق الزر والعنصر <span> كابنين لعنصر القائمة. اجعل المحتوى النصي للعنصر <span> مطابقًا لقيمة عنصر اﻹدخال واجعل العبارة 'Delete' المحتوى النصي للزر. ألحق عنصر القائمة بالقائمة كعنصر ابن. اربط معالج حدث بزر الحذف كي يُحذف عنصر القائمة بأكمله (<li>...</li>) عند النقر على الزر. استخدم أخيرًا التابع ()focus لنقل تركيز الدخل إلى عنصر اﻹدخال كي يكون جاهزًا ﻹدخال العنصر التالي في القائمة. ملاحظة: يمكنك إلقاء نظرة على التمرين التطبيقي المنتهي (أو تجربته مباشرة أيضًا). الخلاصة وصلنا اﻵن إلى نهاية مقالنا عن استخدام DOM وتعديلها من خلال جافا سكريبت، ومن المفترض في هذه المرحلة أن تكون قد أدركت اﻷجزاء المهمة من متصفح ويب فيما يتعلق بالتحكم بالمستندات وغيرها من النواحي التي تؤثر على تجربة مستخدم الويب. ومن المهم اﻷكثر أن تستوعب ماهية شجرة DOM وكيفية التعامل معها لتقديم ميزات مفيدة لتطبيقك. ترجمة -وبتصرف- للمقال: Manipulating documents المقال السابق: مدخل إلى واجهات الويب البرمجية Web APIs تعرف على البنية الشجرية لـ DOM مدخل إلى DOM مكونات الويب: التعامل مع شجرة DOM الخفية مكونات الويب: عناصر HTML المخصصة وقوالبها
-
نلقي نظرة في هذا المقال على الواجهات البرمجية في جافا سكريبت، ونشرح ماهيتها وكيفية عملها، وآلية استخدامها في الشيفرة و أساليب هيكلتها. كما نلقي نظرة على اﻷصناف اﻷساسية للواجهات البرمجية، وإمكانيات استخدامها. ننصحك قبل المتابعة في قراءة هذه المقالات أن: تكون ملمًا بلغتي HTML و CSS. تكون ملمًا بلغة جافا سكريبت. تطلع على سلاسل المقالات السابقة التي ناقشت أساسيات جافا سكريبت والكائنات في جافا سكريبت. ما هي الواجهات البرمجية؟ واجهات التطبيقات البرمجية أو الواجهات البرمجية API هي بُنىً برمجية تقدمها لغات البرمجة لتتيح للمطورين إنشاء مهام معقدة بسهولة أكبر. إذ تساهم في تحييدهم عن الشيفرات المعقدة وتقدم صياغة قواعدية أوضح وأسهل. فكّر كمثال عن الواجهة البرمجية بالتغذية الكهربائية في منزلك، فإن أردت تشغيل جهاز كهربائي ستضع قابس الجهاز في مأخذ الكهرباء المخصص وسيعمل الجهاز، ولن تضطر بالطبع إلى إجراء التوصيلات الكهربائية إلى مصدر التغذية مباشرة، على الرغم من إمكانية ذلك، لكن إن لم تكن كهربائيًا مختصًا فالأمر خطر وصعب. وبالمثل، لو أردت برمجة رسوميات ثلاثية اﻷبعاد، من اﻷسهل حينها استخدام واجهة برمجية مكتوبة بلغة عالية المستوى مثل جافا سكريبت أو بايثون بدلًا من كتبتها باستخدام لغات أكثر تعقيدًا تتحكم مباشرة بوحدة المعالجة الرسومية GPU أو غيرها من الوظائف الرسومية مثل C و ++C. واجهات جافا سكريبت البرمجية في طرف العميل تقدم لغة جافا سكريبت في طرف العميل تحديدًا العديد من الواجهات البرمجية المهمة. ولا تُعد هذه الواجهات جزءًا من اللغة بحد ذاتها وإنما بُنيت على أساس جافا سكريبت لزيادة إمكانات شيفرتها، وتصنف هذه الواجهات عمومًا ضمن فئتين رئيسيتين: واجهات المتصفح البرمجية Browser API: وهي برمجيات مضمّنة في المتصفح وقادرة على عرض البيانات الموجودة في المتصفح و البيئة الحاسوبية التي تستضيفه، وتنفيذ عمليات مركّبة على تلك البيانات. وكمثال على هذه الواجهات نذكر Web Audio API التي تقدم هيكلية لجافا سكريبت تساعد في التعامل مع الصوتيات في المتصفح، علمًا أن المتصفح يتعامل مع الموارد الصوتية عمليًا من خلال شيفرة معقدة مكتوبة بلغات أخرى مثل (++C و Rust)، مع ذلك ما تقدمه تلك الواجهة هو تفادي تعقيد تلك العمليات. واجهات يقدمها طرف آخر Third-party APIs: لا تُضمّن هذه الواجهات ضمن المتصفح افتراضيًا، وعليك الحصول على شيفرتها والمعلومات المتعلقة بها من الويب. فالواجهة Google Maps API على سبيل المثال تمنحك القدرة على عرض خرائط تفاعلية على موقعك، وتقدم هيكلية مخصصة تساعدك في استعلام خدمة خرائط جوجل Google Maps للحصول على معلومات محددة. العلاقة بين جافا سكريبت و الواجهات البرمجية و أدوات جافا سكريبت تحدثنا في الفقرة السابقة عن طبيعة واجهات جافا سكريبت البرمجية في طرف العميل وعلاقتها بلغة جافا سكريبت، لهذا دعونا نسترجع ما شرحناها لتوضيح الأمر أكثر واﻹشارة إلى أدوات جافا سكريبت الملائمة: لغة جافا سكريبت: وهي لغة سكريبت عالية المستوى مضمنة ضمن المتصفحات، وتسمح لك بتنفيذ مهام مختلفة في صفحات وتطبيقات الويب. وتجدر اﻹشارة أن جافا سكريبت متاحة أيضًا في بيئات أخرى غير بيئة المتصفحات مثل بيئة Node.js. واجهات المتصفح البرمجية: وهي هيكليات مضمنة في المتصفح أساسها جافا سكريبت وتسمح بتنفيذ وظائف مختلفة بسهولة أكبر. واجهات برمجية يقدمها طرف آخر: وهي هيكليات مبنية على منصات مختلفة (مثل فيسبوك) تتيح لك استخدام بعض وظائف هذه المنصة في موقعك الخاص. مكتبات جافا سكريبت: وهي عادة ملف جافا سكريبت أو أكثر تتضمن دوال مخصصة يمكنك ربطها بصفحات الويب الخاصة بك لتسريع تنفيذ أو استخدام وظائف معينة. من اﻷمثلة عليها المكتبات jQuery و Mootools و React. إطارات عمل جافا سكريبت: وهي تقنية أكثر تقدمًا من المكتبات مثل Angular و Ember وتتكون عادة من حزم HTML و CSS وجافا سكريبت وتقنيات أخرى تُثبّت لتساعدك في كتابة تطبيقات ويب كاملة من الصفر. أما الاختلاف الجوهري بين المكتبات وإطارات العمل فهو "التحكم المعكوس Inversion of Control". إذ يتحكم المطور باستدعاء دوال من مكتبة، لكن إطار العمل هو من يستدعي شيفرة المطور. ما الذي يمكن أن تقدّمه الواجهات البرمجية؟ ستجد كمًا هائلًا من الواجهات البرمجية المتاحة التي تسمح لك بتنفيذ مهام ووظائف متنوعة في شيفرتك، لهذا نلقي نظرة في الفقرات التالية على أهم هذه الواجهات ووظائفها. واجهات المتصفح البرمجية اﻷكثر شيوعًا من أكثر فئات واجهات المتصفح البرمجية شيوعًا والتي سنشرحها في هذه المقالات نجد: الواجهات البرمجية التي تتعامل مع المستندات تُحمّل هذه الواجهات ضمن المتصفح ومن أكثرها وضوحًا الواجهة DOM التي تتيح لك العمل مع HTML و CSS مثل إنشاء العناصر وإزالتها وتنسيقها ديناميكيًا وتطبيق التغييرات على صفحة الويب وغيرها. فكلما رأيت نافذ منبثقة من الصفحة أو لاحظت تغيرًا في المحتوى المعروض فهو من فعل واجهة DOM. الواجهات التي تحضر البيانات من الخادم وتستخدم هذه الواجهات مثلًا في تحديث محتوى جزء من الصفحة فقط، ولهذا اﻷمر الذي يبدو بسيطًا أثرًا هائلًا على أداء وسلوك المواقع. فلو أردت أن تحدث فقط قائمة البضائع في متجر أو عرض قائمة بالقصص الجديدة المتوفرة وتمكنت من تنفيذ اﻷمر مباشرة دون الحاجة إلى إعادة تحميل الصفحة، سيبدو حينها الموقع أو التطبيق أكثر تجاوبًا. ومن الواجهات الرئيسية المستخدمة في تحقيق ذلك نجد Fetch وكذلك XMLHttpRequest التي تستخدمها الشيفرات اﻷقدم. وقد تمر بالمصطلح AJAX الذي يصف تمامًا هذه التقنية القديمة. الواجهات البرمجية الخاصة بالرسوميات وهي واجهات مدعومة جيدًا في معظم المتصفحات الحديثة وأكثرها شهرة Canvas و WebGL. إذ تسمح هذه الواجهات بتغيير بيانات كل بكسل من بكسلات عنصر HTML المخصص <canvas> لتكوين مشاهد ثنائية وثلاثية اﻷبعاد. وتتمكن أيضًا من رسم أشكال ضمن هذا العنصر مثل المربعات والدوائر أو عرض صورة وتطبيق مرشحات عليها كأن تحولها إلى اللون الرمادي وذلك باستخدام الواجهة البرمجية Canvas. وتتيح لك الواجهة WebGL إنشاء مشاهد ثلاثية أبعاد مركّبة مع إضاءة وخامة texture وغيرها. وتُستخدم هذه الواجهات عادة مع واجهات أخرى لتنفيذ حلقات رسومية للتحريك مثل ()window.requestAnimationFrame وغيرها كي تحدّث باستمرار المشهد كما في الرسوم المتحركة واﻷلعاب. الواجهات البرمجية الخاصة بالصوت والفيديو تسمح لك هذه الواجهات بتنفيذ الكثير من المهام المتعلقة بتشغيل الوسائط المتعدد وإنشاء واجهات تحكم مخصصة للتعامل مع ملفات الصوت والفيديو وعرض معلومات نصية عن المقاطع والمسارات إضافة إلى عناوينها وكلماتها. كما تساعدك في التقاط مقاطع فيديو باستخدام كاميرا الويب والتعديل عليها بمساعدة الواجهة البرمجية Canvas أو عرضها على حاسوب شخص آخر أو إضافة تأثيرات إلى المسارات الصوتية. نذكر من هذه الواجهات HTMLMediaElement و Web Audio API و WebRTC الواجهات البرمجية التي تتعامل مع الأجهزة تمكنك هذه الواجهات من التفاعل مع العتاد الصلب لجهازك مثل الوصول إلى شريحة GPS لتحديد موقع المستخدم من خلال الواجهة Geolocation API. الواجهات البرمجية التي تتعامل مع تخزين البيانات في طرف العميل تمكنك هذه الواجهات من تخزين البيانات على جهاز العميل، وبالتالي ستكون قادرًا على إنشاء تطبيق يحتفظ بحالته أثناء تحميل الصفحات المختلفة وحتى العمل دون الإتصال باﻹنترنت. وستجد العديد من الخيارات المتاحة مثل التخزين البسيط المبني على مبدأ اسم/قيمة من خلال الواجهة Web Storage API أو تخزين البيانات ضمن قواعد بيانات أعقد مثل الواجهة IndexedDB API. الواجهات البرمجية التي تطورها أطراف أخرى تتنوع هذه الواجهات وتغطي استخدامات متنوعة، لكننا سنذكر منها الأكثر شعبية، فقد تضطر إلى استخدامها عاجلًا أم آجلًا. واجهات الخرائط: مثل Mapquest و Google Maps API، وتسمح لك بتنفيذ الكثير من اﻷشياء التي تتعلق بالخرائط ضمن موقعك. مجموعة واجهات فيسبوك Facebook suite of APIs: تمكنك هذه الواجهات من استخدام اﻷجزاء المختلفة لمنظومة فيسبوك لدعم تطبيقك مثل تقديم آلية لتسجيل الدخول اعتمادًا على حساب فيسبوك واستخدام بوابات الدفع اﻹلكتروني في تطبيقك وإدارة حملات دعائية وغيرها. واجهات تلغرام Telegram APIs: وتتيح لك إدراج محتوى أقنية تلغرام في موقعك، وتقديم دعم للبرامج اﻵلية bots. واجهة يوتيوب YouTube API: وتتيح لك إدراج مقاطع فيديو من يوتيوب في موقعك أو البحث ضمن يوتيوب أو إنشاء قوائم تشغيل وغيرها. الواجهة Twilio API: وتقدم إطار عمل لبناء وظائف تتعلق بمكالمات الصوت والفيديو في تطبيقك، وإرسال رسائل SMS و MMS من التطبيق وغيرها. الواجهة Disqus API: تزودك بمنصةّ للتعليقات يمكن إضافتها إلى موقعك اﻹلكتروني. الواجهة Mastodon API: تساعدك في التعامل مع ميزات شبكة Mastodon للتواصل الاجتماعي برمجيًا. الواجهة IFTTT API: وتساعدك على دمج عدة واجهات برمجية ضمن منصة واحدة. كيف تعمل الواجهات البرمجية؟ توجد بعض الاختلافات في طريقة عمل واجهات جافا سكريبت البرمجية، إلا أنها تشترك بالعديد من الميزات وسمات العمل. وسنشرح أوجه التشابه في الفقرات التالية. الاعتماد على الكائنات تتواصل شيفرتك مع الواجهات البرمجية باستخدام كائن أو أكثر من كائنات جافا سكريبت. وتعمل هذه الكائنات كحاويات لتخزين البيانات التي تستخدمها الواجهة (من خلال خاصيات الكائن properties) وكوسيلة لإيصال الوظائف التي تقدمها الواجهة البرمجية (من خلال توابع الكائن methods). ملاحظة: إن لم تكن على دراية بطريقة عمل الكائنات، ننصحك بالعودة إلى مقال "أساسيات العمل مع الكائنات في جافا سكريبت". لو عدنا اﻵن إلى واجهة الويب الخاصة بالصوتيات Web Audio API وهي واجهات معقدة نوعًا ما، ستجد أنها تضم عددًا من الكائنات أكثرها وضوحًا: AudioContext: ويقدم وسيلة للتعامل مع تشغيل الصوتيات ضمن المتصفح ويتضمن عدة توابع وخاصيات تساعدك في ذلك. MediaElementAudioSourceNode: ويمثّل العنصر <audio> الذي يضم المقطع الصوتي المطلوب تشغيله ضمن الكائن AudioContext. AudioDestinationNode: ويمثّل الطرفية التي تصدر الصوت فعليًا في جهازك، وعادة ما تكون مكبّر الصوت أو سماعات الرأس. لكن كيف تتفاعل هذه الكائنات مع بعضها؟ ألق نظرة على مثال تشغيل الصوتيات في المتصفح (بإمكانك تجربة التطبيق مباشرة أيضًا) وستجد شيفرة HTML التالية: <audio src="outfoxing.mp3"></audio> <button class="paused">Play</button> <br /> <input type="range" min="0" max="1" step="0.01" value="1" class="volume" /> أول ما فعلناه هو استخدام العنصر <audio> الذي يرتبط بمقطع صوتي MP3، ولم نضف أية أدوات تحكم افتراضية بالصوت مضمّنة في المتصفح. كما أضفنا بعد ذلك العنصر <button> الذي نستخدمه لتشغيل وإيقاف المقطع الصوتي، وعنصر إدخال <input> من النوع "مجال "type="range" الذي نستخدمه للتحكم بمستوى الصوت أثناء التشغيل. ونبدأ شيفرة جافا سكريبت بإنشاء نسخة عن الكائن AudioContext للتعامل مع الملف الصوتي: const AudioContext = window.AudioContext || window.webkitAudioContext; const audioCtx = new AudioContext(); ننشئ بعد ذلك ثوابت لتخزين مراجع إلى العناصر <audio> و <button> و <input> ومن ثم نستخدم التابع ()AudioContext.createMediaElementSource ﻹنشاء الكائن MediaElementAudioSourceNode الذي يمثّل بدوره المصدر الذي ينبع منه الصوت وهو العنصر <audio>. const audioElement = document.querySelector("audio"); const playBtn = document.querySelector("button"); const volumeSlider = document.querySelector(".volume"); const audioSource = audioCtx.createMediaElementSource(audioElement); نستخدم تاليًا معالجي أحداث أحدهما للتنقل بين وضعي التشغيل واﻹيقاف عند النقر على الزر واﻵخر ﻹعادة الملف الصوتي إلى بدايته عند انتهاء تشغيله. // تشغيل وايقاف الملف الصوتي playBtn.addEventListener("click", () => { // check if context is in suspended state (autoplay policy) if (audioCtx.state === "suspended") { audioCtx.resume(); } // إن توقف تشغيل الملف أعد تشغيله if (playBtn.getAttribute("class") === "paused") { audioElement.play(); playBtn.setAttribute("class", "playing"); playBtn.textContent = "Pause"; // إن كان الملف في وضع التشغيل أوقفه } else if (playBtn.getAttribute("class") === "playing") { audioElement.pause(); playBtn.setAttribute("class", "paused"); playBtn.textContent = "Play"; } }); // عند إنتهاء اﻷغنية audioElement.addEventListener("ended", () => { playBtn.setAttribute("class", "paused"); playBtn.textContent = "Play"; }); ملاحظة: قد يلاحظ البعض أن التابعين ()play و ()pause المستخدمان في تشغيل وايقاف الملف الصوتي ليسا جزءًا من الواجهة Web Audio API، بل جزءًا من الواجهة البرمجية HTMLMediaElement التي تختلف قليلًا عنها. ننشئ بعد ذلك الكائن GainNode باستخدام التابع ()AudioContext.createGain وذلك لضبط مستوى الصوت، ثم ننشئ معالج حدث آخر يغيّر قيمة مستوى الصوت كلما تغير موقع الزالقة: // مستوى الصوت const gainNode = audioCtx.createGain(); volumeSlider.addEventListener("input", () => { gainNode.gain.value = volumeSlider.value; }); آخر ما نفعله ﻹنجاز مثالنا هو ربط العقد المختلفة معًا عن طريق التابع ()AudioNode.connect الموجود في كل عقدة audioSource.connect(gainNode).connect(audioCtx.destination); نبدأ بمصدر الصوت ومن ثم نربط به عقدة التحكم بمستوى الصوت والتي تتصل بدورها إلى العقدة التي تشغّل الصوت فعليًا في جهازك (تمثّل الخاصية AudioContext.destination الوجهة الافتراضية AudioDestinationNode المتاحة على حاسوبك لتشغيل الصوت مثل المكبرات). لجميع الواجهات مداخل مميزة عليك أن تعرف تمامًا مداخل entry points الواجهات البرمجية قبل أن تتعامل معها، وهذا اﻷمر بسيط نوعًا ما في الواجهة Web Audio API، فمدخلها هو الكائن AudioContext الذي تحتاجه للتعامل مع أي ملف صوتي. وللواجهة البرمجية DOM مدخل بسيط أيضًا، إذ تتواجد معظم ميزاتها ضمن الكائن Document، أو ضمن نسخة عن كان HTML الذي نريد التعامل معه: const em = document.createElement("em"); //إنشاء عنصر جديد const para = document.querySelector("p"); //موجود <p> مرجع إلى عنصر em.textContent = "Hello there!"; //em إسناد محتوى إلى المتغير para.appendChild(em); //para ضمن المتغير em وضع وتعتمد الواجهة أيضًا على كائن سياق context object للتعامل مع المقادير المختلفة. وعلى الرغم من أن السياق في هذه الحالة رسومي وليس صوتي، إلا أن كائن السياق الخاص سيُنشأ من خلال مرجع إلى العنصر <canvas> الذي تريد الرسم ضمنه ومن ثم تستدعي التابع ()HTMLCanvasElement.getContext: const canvas = document.querySelector("canvas"); const ctx = canvas.getContext("2d"); وكل ما تحتاجه بعد ذلك للعمل مع canvas هو استدعاء الخاصيات والتوابع لكائن السياق الرسومي (وهو في هذه الحالة نسخة عن CanvasRenderingContext2D? Ball.prototype.draw = function () { ctx.beginPath(); ctx.fillStyle = this.color; ctx.arc(this.x, this.y, this.size, 0, 2 * Math.PI); ctx.fill(); }; ملاحظة: بإمكانك رؤية هذه الشيفرة وهي تعمل ضمن تطبيق الكرات القافزة التجريبي (وبإمكانك تجربته مباشرة أيضًا) تستخدم اﻷحداث للتعامل مع تغيرات الحالة ناقشنا في مقالمدخل إلى اﻷحداث في جافا سكريبت موضوع اﻷحداث والتعامل معها وكيفية استخدامها في الشيفرة. فإن لم تكن على دراية بها، ننصحك بالعودة إلى هذا المقال والعمل عليه قبل المتابعة. لا تتضمن بعض واجهات الويب البرمجية أحداثًا ويضم بعضها اﻵخر القليل منها، ولقد رأينا عمل عدد من معالجات اﻷحداث في مثالنا عن الواجهة Web Audio APi: // play/pause audio playBtn.addEventListener("click", () => { // check if context is in suspended state (autoplay policy) if (audioCtx.state === "suspended") { audioCtx.resume(); } // if track is stopped, play it if (playBtn.getAttribute("class") === "paused") { audioElement.play(); playBtn.setAttribute("class", "playing"); playBtn.textContent = "Pause"; // if track is playing, stop it } else if (playBtn.getAttribute("class") === "playing") { audioElement.pause(); playBtn.setAttribute("class", "paused"); playBtn.textContent = "Play"; } }); // if track ends audioElement.addEventListener("ended", () => { playBtn.setAttribute("class", "paused"); playBtn.textContent = "Play"; }); تمتلك آليات إضافية لمسائل اﻷمان عند الحاجة لواجهات الويب البرمجية الاعتبارات اﻷمنية نفسها التي نواجهها في جافا سكريبت وفي غيرها من تقنيات الويب (مثل سياسة اﻷصل المشترك same-origin policy) لكنها تتمتع في بعض اﻷحيان بميزات أمنية إضافة. فلن تعمل على سبيل المثال بعض واجهات الويب البرمجية الحديثة سوى في الصفحات التي تُخدَّم وفق بروتوكول HTTPS وذلك لإمكانية نقلها بيانات حساسة (من اﻷمثلة عليها عمال الخدمة service workers والواجهة Push). وإضافة إلى ذلك، تطلب بعض الواجهات الحصول على بعض اﻷدونات من المستخدم عندما يستدعيها من خلال الشيفرة مثل الواجهة Notifications API التي تطلب من المستخدم إذنًا لعرض النوافذ المنبثقة pop-ups: وتخضع الواجهتين Web Audio و HTMLMediaElement إلى آلية أمنية تُدعى سياسة التشغيل التلقائي autoplay policy، وتعني مبدئيًا أنك لن تستطيع تشغيل الملف الصوتي تلقائيًا عند تحميل الصفحة، وعليك أن تدفع المستخدم إلى تشغيل المقطع بالنقر على زر مثلًا. والسبب في ذلك أن تشغيل المقطع الصوتي تلقائيًا أمر مزعج ولا ينبغي فرض اﻷمر على المستخدم. ملاحظة: تبعًا لصرامة المتصفح الذي تستخدمه، فقد لا تسمح سياسات أمنية كالتي ذكرناها تشغيل المثال السابق محليًا (على حاسوبك) إن حمّلت ملف المثال وحاولت تشغيله بدلًا من تشغيله مباشرة على الخادم. الخلاصة من المفترض في نهاية هذا المقال أن تمتلك فكرة لا بأس بها عن الواجهات البرمجية وفوائدها عندما تستخدمها في جافا سكريبت. وأن تكون قد أصبحت جاهزًا نوعًا ما لاستخدام الواجهات التي شرحناها لتنفيذ مهام مختلفة، وهذا ما سنراه في مقالات لاحقة. ترجمة -وبتصرف- للمقال Introduction to web APIs اقرأ أيضًا المقال السابق: تحريك سلسلة رسوم متحركة باستخدام الوعود في جافا سكريبت ما هي الواجهة البرمجية للتطبيقات API؟ دليل استخدام ChatGPT API لتحسين خدماتك عبر الإنترنت دليلك لربط واجهة OpenAI API مع Node.js إنشاء واجهة برمجية API في Laravel
-
سنتعلم في هذا المقال كيفية استخدام الوعود (Promises) في جافا سكريبت لتحريك مجموعة من الصورأو الرسومات بترتيب محدد باستخدام الواجهة البرمجية Web Animations API. حيث سنطبق بشكل عملي كل كود برمجي يعرض صورًا ثابتة ثم نعدله شيفرة معينة لتحقيق حركة تسلسلية لهذه الصور وفق تتابع محدد بحيث تتحرك الصورة الأولى، وعند انتهائها تتحرك الصورة الثانية، وعند انتهائها تتحرك الصورة الثالثة. ملفات التطبيق لدينا في مجلد التطبيق الملفات الأساسية التالية: index.html style.css main.js style.css وملف الصورة التي سنحركها alice.svg لنوضح دلالة كل ملف من هذه الملفات الملف index.html بداية نكتب الكود التالي في مستند HTML لإنشاء صفحة ويب تحتوي على ثلاث صور متماثلة مرتبة قطريًا، والتي سيتم تحريكها باستخدام جافا سكريبت <!DOCTYPE html> <html lang="en-US"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width"> <title>Sequencing animations</title> <script type="text/javascript" src="main.js" defer></script> <link href="style.css"rel="stylesheet"> </head> <body> <div id="alice-container"> <img id="alice1" src="alice.svg" role="img" alt="silhouette of crouching long haired character in dress and short boots"> <img id="alice2" src="alice.svg" role="img" alt="silhouette of crouching long haired character in dress and short boots"> <img id="alice3" src="alice.svg" role="img" alt="silhouette of crouching long haired character in dress and short boots"> </div> </body> </html> الملف style.css سنكتب في هذا الملف التنسيقات اللازمة لتنسيق الصور الثلاثة لعرضها في حاوية في وسط الصفحة باستخدام تخطيط الشبكة Grid Layout. ونعين مناطق محددة لكل صورة ضمن الشبكة لضمان ترتيبها بشكل قطري كما يلي: body { background: #6c373f; display: flex; justify-content: center; } #alice-container { width: 90vh; display: grid; place-items: center; grid-template-areas: "a1 . ." ". a2 ." ". . a3"; } #alice1 { grid-area: a1; } #alice2 { grid-area: a2; } #alice3 { grid-area: a3; } الملف main.js سنكتب في هذا الملف كود لاختيار الصور الثلاثة من الصفحة باستخدام معرفاتها الفريدة وتخزينها في متغيرات لتسهيل استخدامها لاحقًا في تحريكها. ونعرف ثابت aliceTumbling يحدد كيفية الحركة من خلال التحولات transforms التي ستقع للصورة وثابت aliceTiming يحدد تفاصيل توقيت الحركة. const aliceTumbling = [ { transform: 'rotate(0) scale(1)' }, { transform: 'rotate(360deg) scale(0)' } ]; const aliceTiming = { duration: 2000, iterations: 1, fill: 'forwards' } const alice1 = document.querySelector("#alice1"); const alice2 = document.querySelector("#alice2"); const alice3 = document.querySelector("#alice3"); لو فتحت اﻵن الملف "index.html" ضمن متصفحك سترى ثلاث صور مرتبة قطريًا على النحو التالي: انتبه إلى أن الملف الوحيد الذي ستعدله في الخطوات التالية من أجل تحريك الصور هو الملف "main.js". كتابة كود التحريك في جافا سكريبت نريد تحديث صفحة الويب السابقة لتحريك الصور الثلاث واحدة تلو اﻷخرى باستخدام فكرة الوعود Promises في جافا سكريبت، فعندما تنتهي حركة الصورة اﻷولى، نحرّك الصورة الثانية بعدها مباشرة، ومن ثم نحرك الصورة الثالثة. لتحقيق ذلك سنعدل شيفرة التحريك في الملف "main.js" لتدوير الصور وتقليص حجمها حتى تختفي، وانتبه إلى أن عملية التحريك تجري مرة واحدة لذا عليك إعادة تحميل الصفحة لمشاهدة الحركة مجددًا اتبع الخطوات المشروحة في اﻷقسام التالية. تحريك الصورة الأولى نستخدم الواجهة البرمجية Web Animations API لتحريك الصورة، وبالتحديد التابع ()element.animate، لهذا، حدّثنا الملف "main.js" باستدعاء التابع ()alice1.animate كالتالي: const aliceTumbling = [ { transform: "rotate(0) scale(1)" }, { transform: "rotate(360deg) scale(0)" }, ]; const aliceTiming = { duration: 2000, iterations: 1, fill: "forwards", }; const alice1 = document.querySelector("#alice1"); const alice2 = document.querySelector("#alice2"); const alice3 = document.querySelector("#alice3"); alice1.animate(aliceTumbling, aliceTiming); أعد تحميل الصفحة، وسترى كيف تدور الصورة اﻷولى وتتقلص. تحريك جميع الصور نريد الآن تحريك الصورة alice2 عند اكتمال حركة الصورة alice1 ثم تحريك alice3 عند اكتمال alice2. ويعيد التابع ()animate الكائن Animation الذي يمتلك الخاصية finished وهذه الاخيرة هي وعد Promise يُنجز عند انتهاء تحريك الصورة. لهذا بإمكانك استخدام هذا الوعد لتحديد متى تحرّك الصورة التالية. const aliceTumbling = [ { transform: 'rotate(0) scale(1)' }, { transform: 'rotate(360deg) scale(0)' } ]; const aliceTiming = { duration: 2000, iterations: 1, fill: 'forwards' } const alice1 = document.querySelector("#alice1"); const alice2 = document.querySelector("#alice2"); const alice3 = document.querySelector("#alice3"); alice1.animate(aliceTumbling, aliceTiming).finished .then(() => alice2.animate(aliceTumbling, aliceTiming).finished) .then(() => alice3.animate(aliceTumbling, aliceTiming).finished) .catch(error => console.error(`Error animating Alices: ${error}`)); عند تنفيذ الكود أعلاه ستحصل على الخرج التالي(اضغط على زر Rerun في الأسفل لمشاهدة الحركة من جديد): See the Pen sequencing-animations by Hsoub Academy (@HsoubAcademy) on CodePen. تنفيذ التسلسل باستخدام الوعود بطرق مختلفة نطلب منك أن تجرّب عدة طرق مختلفة في تنفيذ اﻷمر باستخدام الوعود: نفّذ الأمر باستخدام نسخة "جحيم الاستدعاءات Callback Hell" التي تحدثنا عنها في مقال سابق. نفّذ اﻷمر من جديد باستخدام سلسلة الوعود، وانتبه إلى وجود عدة طرق لكتابة الشيفرة نظرًا للأشكال المختلفة التي يمكنك من خلالها كتابة دالة سهمية. لهذا جرّب بعض اﻷشكال واستنتج الشكل اﻷسهل واﻷوضح قراءة. نفّذ اﻷمر باستخدام التعليمتين await و async. وتذكّر أن ()element.animate لا تعيد وعدًا Promise، بل كائن Animation له الخاصية finished وهي بحد ذاتها وعد. الخلاصة استعرضنا في هذا المقال طريقة تحريك مجموعة من الصور في جافا سكريبت من خلال تطبيق عملي بسيط لكود برمجي يعرض صورًا ثابتة ثم عدلناه لتحقيق حركة تسلسلية لهذه الصور بحيث تتحرك الصورة الأولى، وعند انتهائها تتحرك الصورة الثانية، وعند انتهائها تتحرك الصورة الثالثة باستخدام باستخدام الوعود لتحقيق التسلسل والواجهة البرمجية Web Animations API لتحقيق الحركة، وبإمكانك إلقاء نظرة على التمرين بشكله النهائي وبهذا الأسلوب، يمكنك تطبيق الوعود في مشاريعك لتحريك أي رسومات بترتيب محدد وإضافة ديناميكية وتفاعلية أكبر لصفحات الويب الخاصة بك. ترجمة -وبتصرف- للمقال: Sequencing animations اقرأ أيضًا المقال السابق: مدخل إلى عمّال Workers جافا سكريبت مقدمة إلى ردود النداء callbacks في جافاسكربت الواجهة البرمجية Promise في JavaScript إنشاء الحركات عبر جافاسكربت فهم الأحداث في جافاسكربت
-
نتحدث في مقالنا عن عمّال جافا سكريبت workers وهي تقنية تساعدك في تنفيذ المهام ضمن خيوط معالجة threads منفصلة. ولقد أشرنا في مقالات سابقة إلى ما يحدث عندما تنفّذ عملية متزامنة طويلة في برنامجك، فقد يصبح البرنامج غير متجاوب بالكامل. ويعود السبب أساسًا إلى أن البرنامج يعمل على خيط معالجة واحد single-threaded. يُعرّف خيط المعالجة بأنه سلسلة من التعليمات المتلاحقة التي يتبعها البرنامج. فإن كان البرنامج وحيد الخيط سيُنفَّذ تعليمة تلو اﻷخرى، وسينتظر البرنامج انتهاء أي عملية متزامنة طويلة التنفيذ حتى يتابع ولا يمكنه أثناء ذلك تنفيذ أي شيء. لهذا السبب ظهرت تقنية العمال workers لتمنحك القدرة على إنجاز بعض المهام ضمن خيوط معالجة مختلفة، إذ تبدأ حينها مهمة معينة ثم تنتقل (وهي لا تزال قيد المعالجة) لتعمل على أخرى (كأن تعالج مدخلات المستخدم). لكن ما يجب الانتباه إليه هو قدرة العمال على الوصول إلى البيانات المشتركة وتغييرها بشكل مستقل وغير متوقع أحيانًا وهذا ما يسبب بعض الثغرات التي يصعب إيجادها. لهذا ولكي نتفادى هذا النوع من المشاكل وخاصة في تطبيقات الويب، لا ينبغي لشيفرة العمال وللشيفرة اﻷساسية الوصول إلى متغيرات اﻷخرى، وقد يشتركا ببعض البيانات في حالات خاصة جدًا. تُنفذ الشيفرة اﻷساسية وشيفرة العمال في عالمين منفصلين تمامًا، وتتواصلان مع بعضهما من خلال تبادل رسائل. ويعني ذلك تحديدًا عدم قدرة العمال على الوصول إلى شجرة DOM (مثل النوافذ أو المستند أو عناصر صفحة الويب). وهناك ثلاث أنواع مختلفة من العمال: عمال مختصون Dedicated workers. عمال مشتركون Shared workers. عمال خدمة Service workers. سنتعرف على النوع الأول من خلال مثال تطبيقي، ثم نناقش النوعين اﻵخرين باختصار. استخدام عمال ويب هل تتذكر تلك الصفحة التي توّلد أعدادًا أولية في مقال سابق؟ سنعود إليها ونستخدم عاملًا لتنفيذ الحسابات حتى تبقى الصفحة متجاوبة مع ما يفعله المستخدم. مولّد اﻷعداد اﻷولية المتزامن لنلق نظرة في البداية إلى شيفرة جافا سكريبت الموافقة: function generatePrimes(quota) { function isPrime(n) { for (let c = 2; c <= Math.sqrt(n); ++c) { if (n % c === 0) { return false; } } return true; } const primes = []; const maximum = 1000000; while (primes.length < quota) { const candidate = Math.floor(Math.random() * (maximum + 1)); if (isPrime(candidate)) { primes.push(candidate); } } return primes; } document.querySelector("#generate").addEventListener("click", () => { const quota = document.querySelector("#quota").value; const primes = generatePrimes(quota); document.querySelector("#output").textContent = `Finished generating ${quota} primes!`; }); document.querySelector("#reload").addEventListener("click", () => { document.querySelector("#user-input").value = 'Try typing in here immediately after pressing "Generate primes"'; document.location.reload(); }); ستتوقف استجابة البرنامج يعد أن نستدعي الدالة ()generatePrimes. مولّد أرقام أولية باستخدام عامل ويب Web worker عليك أولًا قبل أن تشرع العمل معنا تنزيل نسخة من ملفات البرنامج، وهي أربعة ملفات: index.html style.css main.js generate.js ستجد أن الملفين "index.html" و "style.css" مكتملان. إليك أولًا ملف HTML: <!doctype html> <html lang="en-US"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width" /> <title>Prime numbers</title> <script src="main.js" defer></script> <link href="style.css" rel="stylesheet" /> </head> <body> <label for="quota">Number of primes:</label> <input type="text" id="quota" name="quota" value="1000000" /> <button id="generate">Generate primes</button> <button id="reload">Reload</button> <textarea id="user-input" rows="5" cols="62"> Try typing in here immediately after pressing "Generate primes" </textarea> <div id="output"></div> </body> </html> وهذا محتوى ملف CSS: textarea { display: block; margin: 1rem 0; } كما ستجد الملفين "main.js" و "generate.js" فارغين وسنضع الشيفرة الرئيسية في الملف "main.js " وشيفرة العامل ضمن الملف "generate.js". إذا ما نلاحظه أولًا كيفية فصل شيفرة العامل عن الشيفرة الرئيسية، كما سترى أننا أضفنا فقط الشيفرة اﻷساسية إلى صفحة الويب "index.html" ضمن العنصر <script>. انسخ اﻵن الشيفرة التالية إلى الملف "main.js": //"generate.js" أنشئ عاملًا جديدًا، ونسند إليه الشيفرة الموجودة في الملف const worker = new Worker("./generate.js"); //أرسل رسالة إلى العامل "Generate primes" عندما ينقر المستخدم //"quota" وتضم أيضًا القيمة "generate" اﻷمر الموجود في الرسالة هو //وهي عدد اﻷرقام اﻷولية التي نولّدها document.querySelector("#generate").addEventListener("click", () => { const quota = document.querySelector("#quota").value; worker.postMessage({ command: "generate", quota, }); }); //عندما يعيد العامل ارسال إلى خيط المعالجة الرئيسي //حدّث صندوق الخرج برسال إلى المستخدم تتضمن عدد اﻷعداد الأولية التي ولدناها // والمأخوذة من بيانات الرسالة اﻷصلية worker.addEventListener("message", (message) => { document.querySelector("#output").textContent = `Finished generating ${message.data} primes!`; }); document.querySelector("#reload").addEventListener("click", () => { document.querySelector("#user-input").value = 'Try typing in here immediately after pressing "Generate primes"'; document.location.reload(); }); ننشئ بداية عاملًا باستخدام البانية ()Worker، ونمرر إليها عنوان URL يشير إلى سكريبت العامل.تُنفَّذ شيفرة العامل بمجرد إنشاءه. وكما هو الحال في النسخة المتزامنة من التطبيق نضيف معالجًا للحدث click خاصًا بالزر "Generate primes". لكن وبدلًا من استدعاء الدالة ()generatePrimes، نرسل رسالة إلى العامل باستخدام التابع ()worker.postMessage الذي يأخذ وسيطًا واحدًا. لهذا نمرر له كائن JSON يضم خاصيتين: command: وتضم قيمة نصية تخبر العامل ما عليه فعله (في حال كان باستطاعته تنفيذ أكثر من شيء). quota: عدد اﻷعداد اﻷولية التي يولّدها. نضيف تاليًا معالج الحدث message إلى العامل، لكي يبلغنا العامل من انتهاء عمله ويعيد أية نتائج نريدها. يأخذ معالج الحدث بياناته من الخاصية data العائدة للرسالة ويطبعها ضمن عنصر الخرج (البيانات هنا هي نفسها قيمة الخاصية quota، لذا لا حاجة لها عمليًا ووضعناها لعرض مبدأ العمل فقط). أضفنا اخيرة شيفرة معالج حدث النقر click للزر "Reload"، وهي مشابهة تمامًا لشيفرة النسخة المتزامنة. انقل اﻵن الشيفرة التالية إلى الملف "generate.js": // Listen for messages from the main thread. // If the message command is "generate", call `generatePrimes()` addEventListener("message", (message) => { if (message.data.command === "generate") { generatePrimes(message.data.quota); } }); // Generate primes (very inefficiently) function generatePrimes(quota) { function isPrime(n) { for (let c = 2; c <= Math.sqrt(n); ++c) { if (n % c === 0) { return false; } } return true; } const primes = []; const maximum = 1000000; while (primes.length < quota) { const candidate = Math.floor(Math.random() * (maximum + 1)); if (isPrime(candidate)) { primes.push(candidate); } } // When we have finished, send a message to the main thread, // including the number of primes we generated. postMessage(primes.length); } وتذكّر أن هذه الشيفرة ستُنفَّذ بمجرد إنشاء عامل جديد. يترصّد العامل بداية الرسائل التي ترسلها الشيفرة الرئيسية من خلال الدالة ()addEventListener وهي دالة عامة في العامل. وتضم الخاصية data الموجودة ضمن معالج حدث الرسالة message نسخة من الوسيط الذي تمرره الشيفرة الرئيسية. فإذا مررت الشيفرة الرئيسية اﻷمر generate، نستدعي حينها الدالة ()generatePrimes ونمرر لها القيمة quota من الحدث message. تشبه الدالة ()generatePrimes مقابلتها في النسخة المتزامنة من التمرين ما عدا أننا نرسل رسالة إلى السكريبت الرئيسي عند الانتهاء بدلًا من إعادة قيمة. ونستخدم في هذه الحالة التابع ()postMessage والذي يشبه الدالة ()addEventListener بأنه عام في شيفرة العامل أيضًا. وكما رأينا، يستمع السكريبت الرئيسي إلى الرسالة ويُحدّث شجرة DOM عند استقبال الرسالة. ملاحظة: لتشغيل هذا الموقع، عليك تشغيل خادم محلي على حاسوبك،إذ لا يُسمح بتحميل شيفرة العامل من الوجهة. وإن صادفتك أي مشاكل في إنشاء نسختك من التمرين` بإمكانك الاطلاع على النسخة المكتملة) منه على جت-هب أو تجربته مباشرة. أنواع أخرى من العمال workers يُدعى العامل الذي أنشأناه في المثال السابق بالعامل المخصص dedicated worker. ويعني ذلك أنه استخدم من قبل سكريبت واحد. وهنالك نوعين آخرين هما: العمال المشتركون shared workers: ويمكن مشاركتهم بين أكثر من سكريبت أثناء تنفيذها في نوافذ مختلفة للمتصفح. عمّال الخدمة service workers: ويعملون كخوادم وكيلة proxy servers أو لتخزين الموارد مؤقتًا كي تعمل صفحة ويب عندما لا يكون المتصفح متصلا بالشبكة، فهي مكوّن أساسي من مكوّنات تطبيقات الويب المتقدمة Progressive Web Apps الخلاصة تعرّفنا في هذا المقال على عمال ويب web workers، وهي تقنية تمكّن تطبيق ويب من نقل المهام إلى خيط معالجة آخر. ورأينا أن خيط المعالجة اﻷساسي وخيط العمال لا يتشاركان المتغيرات، بل يتواصلان من خلال إرسال الرسائل التي يتلقاها الطرف اﻵخر على شكل أحداث للكائن message. يمكن أن تقدم هذه التقنية طريقة فعالة ﻹبقاء التطبيق اﻷساسي متجاوبًا، على الرغم من عدم قدرة العمال على الوصول إلى كل الواجهات البرمجية التي يصلها التطبيق اﻷساسي وخصوصًا عناصر شجرة DOM. ترجمة -وبتصرف- للمقال: Introducing workers اقرأ أيضًا: المقال السابق: إنجاز واجهة برمجية في جافا سكريبت تعتمد على الوعود معالجة الأحداث في جافا سكريبت مدخل إلى جافا سكريبت غير المتزامنة المزخرفات decorators والتمرير forwarding في جافاسكربت
-
شرحنا في المقال السابق طريقة استخدام واجهات برمجة التطبيقات التي تعيد الوعود promises، وسنتحدث في هذا المقال عن تعريف الواجهات البرمجية التي تعيد وعودًا في لغة البرمجة جافا سكريبت، ونلقي نظرة على كيفية إنجاز هذه الواجهات البرمجية بأنفسنا، وعلى الرغم من أن مهمة كهذه ليست شائعة الاستخدام كثيرًا لكن من المفيد لك معرفتها. ملاحظة: عند تنفيذ واجهة برمجة تطبيقات معتمدة على الوعود promise-based API ستحتاج لأن نغلّف عملية غير متزامنة (مثل اﻷحداث events، أو دوال رد النداء callbacks، أو نموذج يعمل على تمرير الرسائل message-passing model) وسيكون عليك هيكلة كائن Promise للتعامل مع حالات نجاح تنفيذ هذه العملية resolve أو إخفاقها reject. تنفيذ الواجهة البرمجية ()alarm ما سنفعله في هذا المثال هو إنجاز واجهة برمجية لمنبّه تُدعى ()alarm. تقبل هذه الواجهة وسيطًا هو اسم الشخص الذي ستوقظه كما تأخذ وسيطًا آخر هي الفترة الزمنية التي ينتظرها المنبه بالميلي ثانية قبل إيقاظ الشخص. بعد ذلك تُرسل الدالة الرسالة "Wake up" (أي استيقظ) يليها اسم الشخص. تغليف الدالة ()setTimeout نستخدم الواجهة ()setTimeout في إنجاز واجهتنا ()alarm، وتأخذ هذه الواجهة وسيطين: دالة تستدعيها عند بلوغ زمن التنبيه، واﻵخر زمن التنبيه بالميلي ثانية. وعندما تُستدعى الواجهة ()setTimeout، تبدأ بالعد وصولًا إلى زمن التنبيه وتستدعي عندها الدالة التي مررناها إليها. نستدعي في مثالنا التالي الدالة ()setTimeout مع وسيطيها دالة رد النداء وزمن التنبيه الذي سيكون 1000 ميلي ثانية. إليك شيفرة HTML: <button id="set-alarm">Set alarm</button> <div id="output"></div> ثم شيفرة جافا سكريبت: const output = document.querySelector("#output"); const button = document.querySelector("#set-alarm"); function setAlarm() { setTimeout(() => { output.textContent = "Wake up!"; }, 1000); } button.addEventListener("click", setAlarm); إليك نتيجة التنفيذ: See the Pen promise-based API1 by Hsoub Academy (@HsoubAcademy) on CodePen. الدالة البانية ()Promise تُعيد الدالة وعدًا Promise يُنجز عندما ينقضي الوقت المخصص للمنبّه، ويمرر الرسالة إلى معالج ()then بينما يُرفض الوعد إن كان زمن التنبيه سالبًا. إن المفتاح الرئيسي للعمل هنا هي الدالة البانية ()Promise التي تأخذ دالة استدعاء كوسيط لها وندعوها دالة منفّذة executer وعليك كتابة شيفرة هذه الدالة عند إنشاء وعد جديد. وتأخذ الدالة المنفذة دالتين كوسيط لها تُدعيان تقليديًا الدالة resolve والدالة reject. تستدعي الدالة المنفذة الدالة غير المتزامنة فإن أنجز الوعد ستُستدعى الدالة resolve وإن أخفق تُستدعى الدالة reject. إما إذا رمت الدالة المنفذة خطأً، ستُستدعى reject تلقائيًا. باﻹمكان تمرير أية قيم إلى الدالتين resolve و reject. لذا بإمكانك كتابة شيفرة الواجهة ()alarm كالتالي: function alarm(person, delay) { return new Promise((resolve, reject) => { if (delay < 0) { throw new Error("Alarm delay must not be negative"); } setTimeout(() => { resolve(`Wake up, ${person}!`); }, delay); }); } تُنشئ الدالة السابقة وعدًا Promise وتعيده، وضمن الدالة المنفذة: نتحقق من قيمة زمن التنبيه delay حتى لا تكون سالبة. نستدعي الدالة ()setTimeout ونمرر لها القيمة delay، ومن ثم تُستدعى دالة رد النداء الموجودة ضمن الدالة السابقة عند انقضاء الوقت المحدد ومن ثم تٌستدعى الدالة resolve بعد أن نمرر لها الرسالة "!Wake up". استخدام الواجهة البرمجية ()alarm من المفترض أن تكون الشيفرة التالية مألوفة من المقال السابق. إذ نستطيع استدعاء الواجهة ()alarm ومن ثم نستدعي ()then و ()catch لكائن الوعد الذي تعيده للتعامل مع حالتي إنجاز الوعد أو رفضه: const name = document.querySelector("#name"); const delay = document.querySelector("#delay"); const button = document.querySelector("#set-alarm"); const output = document.querySelector("#output"); function alarm(person, delay) { return new Promise((resolve, reject) => { if (delay < 0) { throw new Error("Alarm delay must not be negative"); } setTimeout(() => { resolve(`Wake up, ${person}!`); }, delay); }); } button.addEventListener("click", () => { alarm(name.value, delay.value) .then((message) => (output.textContent = message)) .catch((error) => (output.textContent = `Couldn't set alarm: ${error}`)); }); إليك نتيجة التنفيذ: See the Pen promise-based API2 by Hsoub Academy (@HsoubAcademy) on CodePen. جرب تحديد قيم مختلفة لكل من person و delay وحاول تمرير قيم delay سالبة وراقب الخرج الناتج. استخدام التعليمين async و await مع الواجهة البرمجية alarm() طالما أن الواجهة ()alarmتعيد وعدًا، نستطيع أن نطبق عليه ما يُطبق على أي وعد آخر، مثل ربطه ضمن سلسلة وعود promise-chain أو استخدام ()all. وكذلك wait/async. const name = document.querySelector("#name"); const delay = document.querySelector("#delay"); const button = document.querySelector("#set-alarm"); const output = document.querySelector("#output"); function alarm(person, delay) { return new Promise((resolve, reject) => { if (delay < 0) { throw new Error("Alarm delay must not be negative"); } setTimeout(() => { resolve(`Wake up, ${person}!`); }, delay); }); } button.addEventListener("click", async () => { try { const message = await alarm(name.value, delay.value); output.textContent = message; } catch (error) { output.textContent = `Couldn't set alarm: ${error}`; } }); إليك نتيجة التنفيذ: See the Pen promise-based API3 by Hsoub Academy (@HsoubAcademy) on CodePen. الخلاصة تعرفنا في هذا المقال على كيفية استخدام الواجهات البرمجية التي تعيد وعودًا promises، وألقينا نظرة على كيفية إنجاز الواجهات البرمجية التي تعيد الوعود من خلال مثال تطبيقي ينجز واجهة منبه للمستخدم بعد انقضاء وقت معين. ترجمة -وبتصرف- للمقال: How to implement a promise-based API اقرأ أيضًا: المقال السابق: استخدام الوعود Promises في جافا سكريبت اللاتزامن والانتظار async/await في جافاسكربت مدخل إلى جافا سكريبت غير المتزامنة البرمجة غير المتزامنة في جافاسكريبت الوعود Promise في جافاسكربت
-
تُعد فكرة الوعود Promises أساسًا للغة جافا سكريبت غير المتزامنة. والوعد هو كائن يُعاد من الدالة غير المتزامنة ويمثّل الوضع الراهن للعملية. ولا تكون العملية قد انتهت بعد في الوقت الذي تعيد في الدالة الوعد إلى مستدعيها، لكن كائن الوعد المُعَاد يمتلك توابع لمعالجة التنفيذ الناجح أو المخفق للعملية. تحدثنا في المقال السابق عن استخدام الاستدعاءات لإنجاز الدوال غير المتزامنة. إذ نستدعي وفق هذا اﻷسلوب الدالة غير المتزامنة ممررين إليها دالة استدعاء أخرى تسمى دالة رد النداء callback، عندها تُعيد هذه الدالة قيمتها مباشرة، ثم تستدعي بعد ذلك دالة رد النداء التي مررناها عندما تنتهي العملية. وما يحدث في الواجهات البرمجية المبنية على الوعود، أن الدالة غير المتزامنة تبدأ عملية ما وتعيد كائن وعد Promise، وبإمكانك حينها ربط هذا الكائن بمعالجات أحداث ستُنفَّذ عند نجاح أو إخفاق هذه العملية. استخدام الواجهة البرمجية fetch ملاحظة: سنتعلم مفهوم الوعود في هذا المقال بنسخ عينات من الكود البرمجي من الصفحة إلى طرفية جافا سكريبت في المتصفح. وﻹعداد هذا اﻷمر: انتقل إلى الموقع: https://example.org افتح طرفية جافا سكريبت الموجودة ضمن أدوات مطوري الويب في نفس النافذة الفرعية. عندما نعرض مثالًا ما، انسخه إلى الطرفية، وعليك حينها إعادة تحميل الصفحة في كل مرة تُلصق فيها مثالًا جديدًا، وإلا تعترض الطرفية لأنك أعدت تصريح المتغير fetchPromise. سننزل في هذا المثال ملف JSON ونسجّل بعض المعلومات المتعلقة به، ولتنفيذ اﻷمر نرسل طلب HTTP إلى الخادم يتضمن رسالة مرسلة إليه وننتظر الاستجابة. ففي مثالنا سنطلب ملف JSON من الخادم. وكما أشرنا في المقال السابق، نستخدم الواجهة البرمجية XMLHttpRequest لتنفيذ طلبات HTTP، لكننا سنستخدم في هذا المقال الواجهة البرمجية ()fetch وهي بديل أحدث عن كائن XMLHttpRequest ومبنية على الوعود. انسخ اﻵن الشيفرة التالية إلى طرفية جافا سكريبت في متصفحك: const fetchPromise = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); console.log(fetchPromise); fetchPromise.then((response) => { console.log(`Received response: ${response.status}`); }); console.log("Started request…"); ما فعلناه في هذه الشيفرة هو التالي: استدعينا الواجهة()fetchوأسندنا القيمة التي تعيدها إلى المتغير fetchPromise. سجلنا بعد ذلك مباشرة قيمة المتغير، ومن المفترض أن تشبه النتيجة ما يلي: Promise { <state>: "pending" }، لتخبرنا أنه لدينا كائن وعد Promise له حالة قيمتها "pending" ويعني ذلك أن عملية إحضار الملف لا تزال قيد التنفيذ. مررنا دالة معالجة إلى التابع ()then العائد لكائن الوعد، فإن نجحت العملية وعندما تنتهي، يستدعي الوعد دالة المعالجة التي نمرر إليها كائن الاستجابة Response الذي يضم استجابة الخادم. طبعنا الرسالة "…Started request" لتدل على أننا بدأنا تنفيذ الطلب. من المفترض أن يكون خرج الشيفرة السابقة كالتالي: Promise { <state>: "pending" } Started request… Received response: 200 لاحظ كيف ظهرت العبارة "Started request" على الشاشة قبل تلقي الاستجابة. فعلى خلاف الدوال المتزامنة، تعيد()fetch قيمتها قبل أن يكتمل الطلب، مما يسمح للبرنامج بمتابعة التنفيذ. ثم يعيد البرنامج بعد ذلك رمز الحالة 200 ويعني أن الطلب قد نُفِّذ بنجاح. قد يبدو هذا المثال مشابهًا للمثال في المقال السابق الذي استخدمنا فيه معالجات أحداث على الكائن XMLHttpRequest، لكننا مررنا هذه المرة دالة معالجة إلى التابع ()then العائد لكائن الوعد الذي تعيده الدالة()fetch. سلسلة من الوعود بمجرد حصولك على كائن استجابة Response باستخدام الواجهة()fetch، عليك استدعاء دالة أخرى للحصول على بيانات الاستجابة، ونريدها في هذه الحالة على هيئة بيانات JSON لهذا نستدعي التابع ()json العائد للكائن Respond. وكذلك الأمر، التابع ()json غير متزامن، هنا سنكون أمام حالة نستدعي فيها دالتين غير متزامنتين على التوالي. جرّب اﻵن ما يلي: const fetchPromise = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); fetchPromise.then((response) => { const jsonPromise = response.json(); jsonPromise.then((data) => { console.log(data[0].name); }); }); أضفنا في هذا المثال أيضًا التابع ()then إلى الوعد الذي تعيده ()fetch. لكن المعالج سيستدعي هذه المرة()response.json ومن ثم يمرر معالج ()then جديد إلى الوعد الذي يعيده التابع ()response.json. يُفترض في هذه الحالة طباعة العبارة في الطرفية (وهو اسم أول منتج في القائمة التي يضمها الملف "products.json"). لكن مقارنة مع الاستدعاءات التي شرحناها في مقال سابق، سنجد أننا نستدعي التابع ()then ضمن تابع ()then آخر وهذا مشابهة لفكرة استدعاء دالة استدعاء ضمن دالة استدعاء بشكل متعاقب، وكنا قد قلنا بأن هذا اﻷمر سيزيد من صعوبة قراءة الشيفرة وفهمها، وأطلقنا عليه اسم "جحيم الاستدعاء callback hell"، وما يحدث هنا أمر مشابه لكن مع الدالة ()then! نعم، اﻷمر نفسه تمامًا، لكن الوعد يقدم ميزة خاصة وهي أن التابع ()then يعيد هو أيضًا وعدًا يكتمل بنتيجة الدالة التي مُرر إليها. لهذا من اﻷفضل إعادة كتابة المثال السابق كالتالي: const fetchPromise = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); fetchPromise .then((response) => response.json()) .then((data) => { console.log(data[0].name); }); فبدلًا من استدعاء تابع ()then آخر ضمن معالج التابع ()then الأول، يمكننا إعادة الوعد الذي يعيده التابع ()json ثم نستدعي التابع ()then الثاني على القيمة المُعادة. يُدعى هذا اﻷمر بسلسلة الوعود Promise chaining، ونستطيع من خلال هذه الميزة تفادي زيادة مستوى التداخل عندما نضطر إلى استدعاء الدوال غير المتزامنة بشكل متتالٍ. قبل الانتقال إلى الخطوة التالية، علينا إضافة شيء آخر، وهو التأكد من قبول الخادم للطلب وقدرته على معالجته قبل أن نحاول قراءة الاستجابة، ولتنفيذ اﻷمر نتحقق من رمز حالة الطلب ونعرض خطأ إن لم يكن رمز الحالة 200 (أو OK) const fetchPromise = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); fetchPromise .then((response) => { if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } return response.json(); }) .then((data) => { console.log(data[0].name); }); التقاط اﻷخطاء وصلنا حاليًا إلى الجزء اﻷخير المتعلق بمعالجة الأخطاء. إذ يمكن للواجهة البرمجية ()fetch رمي أخطاء لأسباب عديدة مثل عدم وجود اتصال بالشبكة أو تمرير عنوان URL غير صالح وغيرها من الأسباب، وبإمكاننا أيضًا رمي أخطاء بأنفسنا إن أعاد الخادم حالة خطأ. ورأينا في المقال السابق أن معالجة اﻷخطاء قد يغدو صعبًا عند تداخل الاستدعاءات ويدفعنا لمعالجة اﻷخطاء في كل مستوى على حدة. ولمعالجة اﻷخطاء، يقدّم الكائن Promise التابع ()catch الذي يشبه كثيرًا التابع ()then من حيث استدعاؤه وتمريره إلى دالة المعالجة. لكن ما يحدث أن استدعاء التابع ()catch يكون عند إخفاق العملية غير المتزامنة وليس نجاحها. فلو أضفت ()catch إلى نهاية سلسلة الوعود، سيُستدعى هذا التابع إن أخفقت أية دالة غير متزامنة في السلسلة. أي بإمكانك إنجاز أي عملية غير متزامنة على شكل سلسلة من الدوال غير المتزامنة المتتابعة التي تنتهي بتابع يعالج جميع اﻷخطاء. جرّب هذه النسخة التي تستخدم ()catch وتُعدّل عنوان URL حتى تُخفق العملية: const fetchPromise = fetch( "bad-scheme://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); fetchPromise .then((response) => { if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } return response.json(); }) .then((data) => { console.log(data[0].name); }) .catch((error) => { console.error(`Could not get products: ${error}`); }); نفّذ الشيفرة السابقة، ومن المفترض أن ترى الخطأ الذي يعرضه التابع ()catch. المصطلحات الخاصة بالوعود تستخدم الوعود مجموعة مخصصة من المصطلحات التي لا بد من توضيحها. بداية للوعد حالة من ثلاث حالات وهي كالتالي: قيد التنفيذ pending: يُنشأ كائن الوعد في هذه الحالة، لكن الدالة غير المتزامنة المرتبطة به لم تنجح أو تخفق بعد. وتوافق هذه الحالة إعادة الوعد إلى الدالة بعد استدعاء()fetch بينما لا يزال الطلب قيد التنفيذ. منجز fulfilled: وهي حالة نجاح العملية غير المتزامنة، ويُستدعى حينها التابع ()then. مرفوض rejected: وهي حالة إخفاق العملية غير المتزامنة، ويُستدعى عندها التابع ()catch. أما معنى "النجاح" أو "اﻹخفاق" فيعود للواجهة البرمجية المستخدمة. فالواجهة()fetch مثلًا ترفض الوعد المعاد لأسباب منها خطأ في الشبكة يمنع إرسال الطلب، وتنجزه عندما يعيد الخادم الاستجابة حتى لو كانت اﻹستجابة حالة خطأ مثل 404 Not Found. كما نستخدم أيضًا مصطلح "مسوّىً settled" ليشير إلى حالتي الرفض أو اﻹنجاز. ونقول عن الوعد أنه "مقضي resolved" إن جرت "تسويته settled" أو كان "مقفلًا locked in" بانتظار حالة وعد آخر. الجمع بين عدة وعود إن تكوّنت العملية غير المتزامنة من عدة دوال نستخدم حينها سلسلة من الوعود، ولا بد حينها من تسوية كل وعد قبل الانتقال إلى اﻵخر. لكنك قد تحتاج أحيانًا إلى الجمع بين عدة استدعاءات لدوال غير المتزامنة، لهذا تزوّدك الواجهة البرمجية ()Promis ببعض الدوال المساعدة. قد يتطلب الأمر في بعض التطبيقات إنجاز عدة وعود لا تتعلق ببعضها البعض. والطريقة اﻷكثر فعالية لتنفيذ هذا اﻷمر هي استدعاء جميع الدوال في نفس الوقت، ثم الحصول على تنبيه عندما تنجز جميعها، وهذا ما يقدمه التابع ()Promise.all فهو يُعيد مصفوفة من الوعود كما يعيد وعدًا واحدًا، ويكون هذا الوعد: منجزَا: عندما تُنجز كل الوعود في المصفوفة. ويُستدعى عند ذلك معالج التابع ()then وتُمرّر له مصفوفة من الاستجابات وبنفس ترتيب الوعود التي مُررت إلى التابع ()all. مرفوضًا: إن رٌفض أي من وعود المصفوفة، ويُستدعى عند ذلك معالج التابع ()catch ويُمرر له الخطأ الناتج عن الوعد الذي رُفض. إليك مثالًا: const fetchPromise1 = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); const fetchPromise2 = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found", ); const fetchPromise3 = fetch( "https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json", ); Promise.all([fetchPromise1, fetchPromise2, fetchPromise3]) .then((responses) => { for (const response of responses) { console.log(`${response.url}: ${response.status}`); } }) .catch((error) => { console.error(`Failed to fetch: ${error}`); }); نفّذنا في الشيفرة السابقة ثلاثة طلبات()fetchإلى ثلاثة عناوين URL مختلفة، فإن نجحت هذه الطلبات، نعرض حالة الاستجابة لكل طلب، وإن أخفقت إحداها، نطبع الخطأ. ينبغي أن تُنجز الطلبات جميعها لأن العناوين صحيحة، مع ملاحظة أن رقم الحالة للطلب الثاني سيكون 404 بدلًا من 200 لأن الملف الذي نطلبه غير موجود حقيقة. لهذا سيكون خرج الكود السابق على النحو التالي: https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json: 200 https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found: 404 https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json: 200 جرّب أن تنفّذ الشيفرة السابقة بعد كتابة العناوين بشكل خاطئ: const fetchPromise1 = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); const fetchPromise2 = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found", ); const fetchPromise3 = fetch( "bad-scheme://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json", ); Promise.all([fetchPromise1, fetchPromise2, fetchPromise3]) .then((responses) => { for (const response of responses) { console.log(`${response.url}: ${response.status}`); } }) .catch((error) => { console.error(`Failed to fetch: ${error}`); }); نتوقع اﻵن أن تُنفَّذ دالة المعالجة الخاصة بالتابع ()catch، ومن المفترض حينها أن يكون الخرج مشابهًا للتالي: Failed to fetch: TypeError: Failed to fetch وقد تريد في بعض الحالات أن يُنجز أحد الوعود ولا يهم أيها، عندها يمكنك الاستفادة من التابع ()Promise.any الذي يشابه ()Promise.all لكنه يُنجز بمجرد إنجاز أي وعد في مصفوفة الوعود، ويُرفض إن رُفضت جميع الوعود: const fetchPromise1 = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); const fetchPromise2 = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found", ); const fetchPromise3 = fetch( "https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json", ); Promise.any([fetchPromise1, fetchPromise2, fetchPromise3]) .then((response) => { console.log(`${response.url}: ${response.status}`); }) .catch((error) => { console.error(`Failed to fetch: ${error}`); }); ملاحظة: لا يُمكن في هذه الحالة توقّع أي وعد سيُنجز أولًا. للتعرف على بقية التوابع التي يمكن استخدامها للجمع بين الوعود راجع توثيق ()Promis. استخدام التعليمتين async و await تُسهّل التعليمة async عمل الشيفرة غير المتزامنة التي تعتمد على الوعود، فإضافة هذه التعليمة إلى بداية الدالة يجعلها دالة غير متزامنة: async function myFunction() { // This is an async function } وباﻹمكان استخدام التعليمة await قبل استدعاء الدالة التي تعيد وعدًا. وهذا ما يجعل الشيفرة تنتظر عند هذه النقطة حتى يسوّى الوعد وعندها تُعد قيمة الوعد المنجز هي القيمة المعادة من قبل الدالة أو ترمي الدالة قيمة الوعد المرفوض كخطأ. وهكذا ستتمكن من كتابة شيفرة غير متزامنة مع أنها تبدو كذلك. لهذا سنحاول كتابة مثال()fetch كالتالي: async function fetchProducts() { try { //`()fetch` تنتظر الدالة بعد هذا السطر حتى يسوّى الاستدعاء // الذي سيُعسد استجابة أو يرمي خطأ const response = await fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } //`response.json()` تنتظر الدالة بعد هذا السطر حتى يسوّى الاستدعاء //أو يرمي خطأ JSON الذي يعيد كائن const data = await response.json(); console.log(data[0].name); } catch (error) { console.error(`Could not get products: ${error}`); } } fetchProducts(); نستدعي هنا الدالة ()await fetch وسنحصل على كائن استجابة Response مكتمل بدلًا من الوعد ()Promisوكأن()fetchدالة متزامنة. ونستطيع أيضًا استخدام الكتلة try...catch لمعالجة اﻷخطاء كما لو كنا نكتب شيفرة متزامنة. وتذكر أن الدوال غير المتزامنة تُعيد وعدًا دائمًا، لهذا لا يمكن أن نكتب شيفرة كهذه: async function fetchProducts() { try { const response = await fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } const data = await response.json(); return data; } catch (error) { console.error(`Could not get products: ${error}`); } } const promise = fetchProducts(); console.log(promise[0].name); //هو كائن وعد فلن تعمل هذه الشيفرة "promise" ويجب عليك تصحيح الكود السابق على النحو التالي: async function fetchProducts() { try { const response = await fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } const data = await response.json(); return data; } catch (error) { console.error(`Could not get products: ${error}`); } } const promise = fetchProducts(); promise.then((data) => console.log(data[0].name)); وتذكر أن استخدام await يكون ضمن دالة، إلا في الحالة التي تكون فيها الشيفرة ضمن وحدة JavaScript module وليس ضمن سكريبت نمطي: try { // using await outside an async function is only allowed in a module const response = await fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } const data = await response.json(); console.log(data[0].name); } catch (error) { console.error(`Could not get products: ${error}`); } قد تستخدم دوال async كثيرًا مقارنة باستخدام سلسلة الوعود لكونها تجعل العمل مع الوعود أكثر وضوحًا. وتذكر أن await -كما هو حال سلسلة الوعود- تجبر العمليات المتزامنة على التنفيذ المتسلسل، وهذا أمر ضروري إن اعتمدت نتيجة العملية الثانية على سابقتها، أما إن لم يكن الوضع كذلك، ففكرّ في هذه الحالة باستخدام الدالة ()Promise.all. الخلاصة تُعرفنا في مقال اليوم على مفهوم الوعود التي تعد أساسًا للبرمجة غير المتزامنة في جافا سكريبت الحديثة. إذ تجعل تسلسل العمليات غير المتزامنة أكثر وضوحًا وإدراكًا بدلًا من التداخل المفرط للاستدعاءات، كما تدعم نمطًا من معالجة الأخطاء مشابهًا ﻵلية try...catch. وتسّهل التعليمتان async و await بناء عمليات باستدعاء سلسلة من الدوال غير المتزامنة المتتابعة دون الحاجة إلى سلاسل صريحة من الوعود وكتابة شيفرة شبيهة بالشيفرة المتزامنة. تعمل الوعود في النسخ اﻷخيرة من معظم المتصفحات الحديثة، وستكون المشكلة فقط مع متصفحي أوبرا ميني Opera mini وإنترنت إكسبلورر 11 والنسخ اﻷقدم. لم نناقش بالتأكيد كل ميزات الوعود في مقالنا الحالي، بل سلطنا الضوء على الميزات اﻷكثر أهمية واستخدامًا، وستتعلم خلال مسيرتك في تعلم جافا سكريبت الكثير من الميزات والتقنيات المفيدة الأخرى، ويجدر بالذكر أن الكثير من واجهات الويب البرمجية مبنية أساسًا على الوعود مثل WebRTC و Web Audio API Media Capture and Streams API وغيرها الكثير. ترجمة -وبتصرف- للمقال: How to use promises اقرأ أيضًا: المقال السابق: مدخل إلى جافا سكريبت غير المتزامنة اللاتزامن والانتظار async/await في جافاسكربت الوعود Promise في جافاسكربت البرمجة غير المتزامنة في جافاسكريبت