ساخت ویجت های سفارشی فرم در HTML — راهنمای جامع

۲۳۶ بازدید
آخرین به‌روزرسانی: ۱۲ شهریور ۱۴۰۲
زمان مطالعه: ۲۰ دقیقه
ساخت ویجت های سفارشی فرم در HTML — راهنمای جامع

موارد زیادی وجود دارند که ویجت‌های آماده فرم HTML کافی نیستند. اگر می‌خواهید استایل‌بندی پیشرفته‌ای را اجرا کنید یا برخی ویجت‌ها مانند <select> داشته باشید، یا اگر می‌خواهید رفتارهای سفارشی داشته باشید چاره‌ای به جز ساخت ویجت های سفارشی فرم خود را نخواهید داشت. در این مقاله، به بررسی روش ساخت چنین ویجت‌هایی می‌پردازیم. به این منظور روی یک مثال کار می‌کنیم و عنصر <select> را بازسازی می‌کنیم. برای مطالعه بخش قبلی این مجموعه مقالات آموزشی به لینک زیر رجوع کنید:

نکته: ما در این مقاله روی ساخت ویجت‌ها متمرکز شده‌ایم و کاری به روش‌های ژنریک ساختن کد و ایجاد قابلیت استفاده مجدد نداریم. این موارد نیازمند برخی کدهای نسبتاً پیچیده‌تر جاوا اسکریپت و دستکاری DOM هستند که خارج از حیطه این مقاله است.

طراحی، ساختار و معناشناسی

پیش از ساخت یک ویجت سفارشی، باید شروع به بررسی این موضوع بکنید که دقیقاً چه می‌خواهید. بدین ترتیب صرفه‌جویی زیادی در زمان شما ایجاد می‌شود. به طور خاص، نکته حائز اهمیت این است که حالت‌های ویجت را به روشنی تعریف کنید. به این منظور بهتر است از یک ویجت موجود که حالت‌ها و رفتارهای آن به خوبی شناخته شده آغاز کنید. به این ترتیب می‌توانید این موارد را تا حد امکان تقلید کنید.

ما در مثال خودمان عنصر <select> را بازسازی می‌کنیم. نتیجه‌ای که می‌خواهیم به دست آوریم به صورت زیر است:

ویجت های سفارشی

تصاویر فوق سه حالت عمده ویجت را نمایش می‌دهند که یکی حالت نرمال (سمت چپ)، حالت فعال (وسط) و حالت باز (سمت راست) است. برحسب رفتار، ما می‌خواهیم ویجتمان با استفاده صرف از یک ماوس و همچنین کیبورد همانند ویجت اصلی قابل استفاده باشد. کار خود را با تعریف کردن شیوه رسیدن ویجت به هر حالت آغاز می‌کنیم.

ویجت در موارد زیر به حالت نرمال می‌رود:

  • صفحه بارگذاری شود.
  • ویجت فعال بوده و کاربر هر جایی خارج از ویجت کلیک می‌کند.
  • ویجت فعال بوده و کاربر فوکوس را با استفاده از کیبورد روی ویجت دیگری می‌برد.

نکته: جابجایی فوکوس در بخش‌های مختلف صفحه معمولاً از طریق فشردن کلید tab صورت می‌گیرد؛ اما این روش استاندارد برای همه مرورگرها نیست. برای نمونه در سافاری به صورت پیش‌فرض با استفاده از ترکیب کلیدهای Option + Tab روی بخش‌های مختلف می‌چرخیم.

ویجت در موارد زیر فعال می‌شود:

  • کاربر روی آن کلیک می‌کند.
  • کاربر کلید tab را بزند و فوکوس را روی ویجت ببرد.
  • ویجت در حالت باز باشد و کاربر روی ویجت کلیک کند.

ویجت در موارد زیر به حالت باز می‌رود:

  • ویجت در هر حالتی به جز باز باشد و کاربر روی آن کلیک کنید.

زمانی که روش تغییر حالت‌ها را دانستیم اینک مهم است که به تعریف روش تغییر مقدار ویجت بپردازیم:

مقدار ویجت زمانی تغییر می‌یابد که:

  • کاربر زمانی که ویجت باز است، روی یک گزینه کلیک کند.
  • کاربر زمانی که ویجت در حالت فعال است، کلیدهای جهتی بالا یا پایین را روی کیبورد بزند.

در نهایت به تعریف روش رفتار گزینه‌های ویجت می‌پردازیم:

  • زمانی که ویجت باز است، گزینه‌های انتخاب‌شده هایلایت می‌شوند.
  • زمانی که ماوس روی یک گزینه می‌رود، آن گزینه هایلایت می‌شود و گزینه هایلایت شده قبلی به حالت نرمال خود بازمی‌گردد.

ما با توجه به مقاصدی که از مثال خود داریم، به این مقدار بسنده می‌کنیم، اما اگر خواننده با دقتی باشید احتمالاً متوجه شده‌اید که برخی رفتارها تعریف نشده‌اند. برای نمونه اگر کاربر روی کلید tab بزند و ویجت در حالت باز باشد چه اتفاقی رخ می‌دهد؟ پاسخ هیچ است. در واقع رفتار صحیح بدیهی به نظر می‌رسد، اما واقعیت است که چون این رفتار در فضای ما تعریف نشده است، می‌توانیم به سادگی آن را نادیده بگیریم. این مورد به خصوص در محیط‌هایی که افراد طراح رفتار ویجت از افرادی که آن را پیاده‌سازی می‌کنند متفاوت باشند، مصداق خواهد داشت.

مثال جالب دیگر: اگر کاربر کلیدهای جهتی بالا و پایین را در زمانی بزند که ویجت در حالت باز قرار دارد، چه اتفاقی رخ می‌دهد؟ این مورد کمی پیچیده‌تر است. اگر در نظر بگیرید که حالت فعال و حالت باز کاملاً متفاوت هستند، پاسخ این بار نیز این است که هیچ اتفاقی رخ نمی‌دهد، زیرا ما هیچ تعامل کیبوردی برای حالت باز تعریف نکرده‌ایم. از سوی دیگر اگر در نظر بگیریم که حالت فعال و حالت باز کمی همپوشانی دارند، مقدار ویجت ممکن است تغییر پیدا کند، اما گزینه مربوطه قطعاً به صورت متناظر هایلایت نمی‌شود، زیرا ما هیچ تعامل کیبورد روی گزینه‌ها برای حالتی که ویجت در حالت باز قرار دارد تعریف نکرده‌ایم. ما تنها تعریف کرده‌ایم که وقتی ویجت باز است چه اتفاقی رخ دهد، اما برای بعد از آن چیزی تعریف نکرده‌ایم.

در مثال مورد بررسی، این مشخصات مفقود بدیهی هستند، زیرا ما آن‌ها را مدیریت خواهیم کرد، اما در مورد ویجت‌های جدید ممکن است دردسرساز باشند، چون هیچ کس ایده روشنی در مورد این که رفتار باید چگونه باشد ندارد. بنابراین بهتر است همواره زمانی را صرف مرحله طراحی بکنید، زیرا اگر رفتارها را به صورت ضعیفی طراحی کرده باشید، یا تعریف برخی موارد را فراموش کنید، بازتعریف آن زمانی که کاربران عادت به استفاده از ویجت پیدا کنند دشوار خواهد بود. اگر در این مورد دچار شک و تردید هستید، از نظرات افراد دیگر کمک بگیرید و اگر بودجه مناسبی دارید، می‌توانید اقدام به اجرای تست‌های کاربر بکنید. این فرایند طراحی UX نام دارد.

نکته: ضمناً در اغلب سیستم‌ها روشی برای باز کردن عنصر <select> برای دیدن همه گزینه‌ها وجود دارد که از طریق فشردن Alt+Down در ویندوز اجرا می‌شود و در این مثال پیاده‌سازی نشده است، اما اجرای آن چندان دشوار نیست، چون مکانیسم آن قبلاً در رویداد Click پیاده‌سازی شده است.

تعریف کردن ساختار و معناشناسی HTML

اکنون که در مورد کارکرد ابتدایی ویجت تصمیم‌گیری کرده‌ایم، زمان آن رسیده است که آن را به ویجت خود اتصال دهیم. نخستین گام تعریف کردن ساختار HTML و ارائه نوعی معناشناسی مقدماتی برای آن است. در ادامه آنچه برای بازسازی عنصر <select> لازم است را می‌بینید:

1<!-- This is our main container for our widget.
2     The tabindex attribute is what allows the user to focus the widget. 
3     We'll see later that it's better to set it through JavaScript. -->
4<div class="select" tabindex="0">
5  
6  <!-- This container will be used to display the current value of the widget -->
7  <span class="value">Cherry</span>
8  
9  <!-- This container will contain all the options available for our widget.
10       Because it's a list, it makes sense to use the ul element. -->
11  <ul class="optList">
12    <!-- Each option only contains the value to be displayed, we'll see later
13         how to handle the real value that will be sent with the form data -->
14    <li class="option">Cherry</li>
15    <li class="option">Lemon</li>
16    <li class="option">Banana</li>
17    <li class="option">Strawberry</li>
18    <li class="option">Apple</li>
19  </ul>
20
21</div>

به استفاده از نام‌های کلاس دقت کنید. این موارد هر بخش مربوطه را صرف نظر از این که عنصر مورد استفاده HTML واقعی چیست مشخص می‌سازند. لازم است مطمئن شوید که CSS و جاوا اسکریپت را به ساختار قوی HTML اتصال نداده‌اید، چون در این صورت قابلیت پیاده‌سازی تغییرهای بعدی را بدون از کار افتادن کدی که برای ویجت استفاده کرده‌ایم از دست می‌دهید. برای نمونه اگر بخواهیم معادل عنصر <optgroup> را پیاده‌سازی کنیم با مشکل مواجه می‌شویم.

ایجاد حس و ظاهر ویجت های سفارشی فرم با CSS

اکنون که ساختار را در اختیار داریم می‌توانیم شروع به طراحی کردن ویجت خود بکنیم. نکته اصلی ساخت این ویجت سفارشی آن است که بتوانیم آن را دقیقاً آن چنان که می‌خواهیم استایل‌بندی کنیم. به این منظور کد CSS خود را به دو بخش تقسیم می‌کنیم که بخش نخست قواعد CSS هستند که مطلقاً برای رفتار ویجت ما به عنوان یک عنصر <select> ضروری هستند و بخش دوم شامل استایل های زیبایی است که ظاهر مورد نظر ما را ایجاد می‌کند.

استایل‌های الزامی

استایل های الزامی شامل مواردی هستند که برای مدیریت سه حالت ویجت ضروری هستند:

1.select {
2  /* This will create a positioning context for the list of options */
3  position: relative;
4 
5  /* This will make our widget become part of the text flow and sizable at the same time */
6  display : inline-block;
7}

ما باید یک کلاس اضافی active نیز برای تعریف حس و ظاهر ویجت خود در زمانی که در حالت فعال قرار دارد، داشته باشیم. از آنجا که ویجت ما قابلیت دریافت فوکوس را دارد، باید یک کپی از این استایل سفارشی به همراه شبه کلاس focus: بگیریم تا مطمئن شویم که رفتار یکسانی دارد.

1.select .active,
2.select:focus {
3  outline: none;
4 
5  /* This box-shadow property is not exactly required, however it's so important to be sure
6     the active state is visible that we use it as a default value, feel free to override it. */
7  box-shadow: 0 0 3px 1px #227755;
8}

اینک به مدیریت لیست گزینه‌ها می‌پردازیم:

1/* The .select selector here is syntactic sugar to be sure the classes we define are
2   the ones inside our widget. */
3.select .optList {
4  /* This will make sure our list of options will be displayed below the value
5     and out of the HTML flow */
6  position : absolute;
7  top      : 100%;
8  left     : 0;
9}

ما به یک کلاس اضافی برای مدیریت زمانی که گزینه‌ها پنهان هستند نیاز داریم. این کلاس برای مدیریت تفاوت بین حالت فعال و حالت باز که دقیقاً منطبق نیستند ضروری است.

1.select .optList.hidden {
2  /* This is a simple way to hide the list in an accessible way, 
3     we will talk more about accessibility in the end */
4  max-height: 0;
5  visibility: hidden;
6}

زیباسازی

بدین ترتیب اکنون کارکرد مقدماتی ویجت را در اختیار داریم و می‌توانیم به بخش جذاب آن بپردازیم. در ادامه صرفاً نمونه‌ای از یک حالت‌های ممکن ارائه کرده‌ایم که با تصویر ابتدای این مقاله مطابقت دارد. با این حال شما می‌توانید به بررسی گزینه‌های مختلف بپردازید و بسته به سلیقه خود ظاهر آن را تغییر دهد.

1.select {
2  /* All sizes will be expressed with the em value for accessibility reasons
3     (to make sure the widget remains resizable if the user uses the  
4     browser's zoom in a text-only mode). The computations are made
5     assuming 1em == 16px which is the default value in most browsers.
6     If you are lost with px to em conversion, try http://riddle.pl/emcalc/ */
7  font-size   : 0.625em; /* this (10px) is the new font size context for em value in this context */
8  font-family : Verdana, Arial, sans-serif;
9
10  -moz-box-sizing : border-box;
11  box-sizing : border-box;
12
13  /* We need extra room for the down arrow we will add */
14  padding : .1em 2.5em .2em .5em; /* 1px 25px 2px 5px */
15  width   : 10em; /* 100px */
16
17  border        : .2em solid #000; /* 2px */
18  border-radius : .4em; /* 4px */
19  box-shadow    : 0 .1em .2em rgba(0,0,0,.45); /* 0 1px 2px */
20  
21  /* The first declaration is for browsers that do not support linear gradients.
22     The second declaration is because WebKit based browsers haven't unprefixed it yet.
23     If you want to support legacy browsers, try http://www.colorzilla.com/gradient-editor/ */
24  background : #F0F0F0;
25  background : -webkit-linear-gradient(90deg, #E3E3E3, #fcfcfc 50%, #f0f0f0);
26  background : linear-gradient(0deg, #E3E3E3, #fcfcfc 50%, #f0f0f0);
27}
28
29.select .value {
30  /* Because the value can be wider than our widget, we have to make sure it will not
31     change the widget's width */
32  display  : inline-block;
33  width    : 100%;
34  overflow : hidden;
35
36  vertical-align: top;
37
38  /* And if the content overflows, it's better to have a nice ellipsis. */
39  white-space  : nowrap;
40  text-overflow: ellipsis;
41}

ما به یک عنصر اضافی برای طراحی کلید جهتی پایین نیاز نداریم و به جای آن از شبه کلاس after: استفاده می‌کنیم. با این حال می‌توان آن را با استفاده از یک تصویر پس‌زمینه روی کلاس select پیاده‌سازی کرد.

1.select:after {
2  content : "▼"; /* We use the unicode character U+25BC; see http://www.utf8-chartable.de */
3  position: absolute;
4  z-index : 1; /* This will be important to keep the arrow from overlapping the list of options */
5  top     : 0;
6  right   : 0;
7
8  -moz-box-sizing : border-box;
9  box-sizing : border-box;
10
11  height  : 100%;
12  width   : 2em;  /* 20px */
13  padding-top : .1em; /* 1px */
14
15  border-left  : .2em solid #000; /* 2px */
16  border-radius: 0 .1em .1em 0;  /* 0 1px 1px 0 */
17
18  background-color : #000;
19  color : #FFF;
20  text-align : center;
21}

سپس به استایل‌بندی لیست گزینه‌های خود می‌پردازیم.

1.select .optList {
2  z-index : 2; /* We explicitly said the list of options will always overlap the down arrow */
3
4  /* this will reset the default style of the ul element */
5  list-style: none;
6  margin : 0;
7  padding: 0;
8
9  -moz-box-sizing : border-box;
10  box-sizing : border-box;
11
12  /* This will ensure that even if the values are smaller than the widget,
13     the list of options will be as large as the widget itself */
14  min-width : 100%;
15
16  /* In case the list is too long, its content will overflow vertically 
17     (which will add a vertical scrollbar automatically) but never horizontally 
18     (because we haven't set a width, the list will adjust its width automatically. 
19     If it can't, the content will be truncated) */
20  max-height: 10em; /* 100px */
21  overflow-y: auto;
22  overflow-x: hidden;
23
24  border: .2em solid #000; /* 2px */
25  border-top-width : .1em; /* 1px */
26  border-radius: 0 0 .4em .4em; /* 0 0 4px 4px */
27
28  box-shadow: 0 .2em .4em rgba(0,0,0,.4); /* 0 2px 4px */
29  background: #f0f0f0;
30}

برای این گزینه‌ها باید یک کلاس highlight نیز اضافه کنیم که ما را قادر به شناسایی مقداری که کاربر انتخاب کرده می‌کند:

1.select .option {
2  padding: .2em .3em; /* 2px 3px */
3}
4
5.select .highlight {
6  background: #000;
7  color: #FFFFFF;
8}

حیات بخشیدن به ویجت با استفاده از جاوا اسکریپت

اکنون که کار طراحی و ساختاردهی ویجت به پایان رسیده است، می‌توانیم کد جاوا اسکریپت مورد نیاز برای عملکرد واقعی آن را بنویسیم.

هشدار: کد زیر آموزشی است و نباید به همین صورت در پروژه‌ها مورد استفاده قرار گیرد. علاوه بر همه موارد دیگر، قابلیت استفاده از این کد به اثبات نرسیده است و روی مرورگرهای قدیمی کار نخواهد کرد. همچنین برخی بخش‌های تکراری دارد که باید در کد پروداکشن بهینه‌سازی شوند.

نکته: ایجاد ویجت‌های با قابلیت استفاده مجدد موضوعی است که تا حدودی پیچیده است. پیش‌نویس کامپوننت وب W3C یکی از پاسخ‌هایی است که به این مشکل داده شده است. پروژه X-Tag یک پیاده‌سازی تست از این مشخصه‌ها است که پیشنهاد می‌کنیم مورد مطالعه قرار دهید.

چرا ویجت کار نمی‌کند؟

پیش از آغاز لازم است نکته‌ای بسیار مهم را در مورد جاوا اسکریپت یادآوری کنیم: جاوا اسکریپت درون مرورگر، یک فناوری غیر قابل اعتماد محسوب می‌شود. زمانی که ویجت‌های سفارشی می‌سازید، مجبور هستید روی جاوا اسکریپت تکیه کنید، زیرا تنها گزینه ضروری برای پیوند دادن همه چیز است. با این حال، موارد زیادی شامل آن‌ها که در ادامه توضیح داده شده وجود دارند که در آن‌ها جاوا اسکریپت نمی‌تواند در مرورگر اجرا شود:

  • کاربر جاوا اسکریپت را غیرفعال کرده باشد: البته این غیر معمول‌ترین حالت است و افراد خیلی کمی هستند که امروزه به غیر فعال‌سازی جاوا اسکریپت بپردازند.
  • اسکریپت بارگذاری نشده باشد: این یکی از رایج‌ترین حالت‌ها است. به طور خاص در مورد دنیای موبایل که شبکه قابل اعتماد نیست بیشتر شاهد این موضوع هستیم.
  • اسکریپت دارای باگ باشد: ما همواره باید این موضوع را در نظر داشته باشیم.
  • اسکریپت در تعارض با اسکریپت ثالث باشد: این اتفاق در مورد اسکریپت‌های ردگیری یا هر بوکمارکلت که کاربر استفاده می‌کند رخ می‌دهد.
  • اسکریپت در تعارض یا تحت تأثیر یک اکستنشن مرورگر باشد: مثلاً اکستنشن NoScript فایرفاکس یا اکستنشن NoScript کروم چنین حالتی دارند.
  • استفاده کاربر از یک مرورگر قدیمی: کاربر از یک مرورگر قدیمی استفاده کند و یکی از قابلیت‌هایی که لازم است پشتیبانی نشود. این اتفاق زمانی که از API–های کاملاً جدید استفاده می‌کنید مکرر رخ می‌دهد.

به دلیل ریسک‌های فوق، لازم است که به دقت بررسی کنید، در صورت عدم کارکرد جاوا اسکریپت چه اتفاقی رخ می‌دهد. بررسی جزییات این وضعیت خارج از حیطه این مقاله است، زیرا ارتباط نزدیکی با روش عمومی ساختن اسکریپت و ایجاد قابلیت استفاده مجدد دارد، اما در این مقاله ما صرفاً با مبانی قضیه سر و کار داریم.

در مثال مورد بررسی، اگر کد جاوا اسکریپت اجرا نشود، از یک fallback برای نمایش عنصر استاندارد <select> استفاده می‌کنیم. به این منظور به دو چیز نیاز داریم. ابتدا باید یک عنصر <select> معمولی قبل از هر ویجت سفارشی اضافه کنیم. همچنین این امر نیازمند توانایی ارسال داده‌های ویجت سفارشی همراه با بقیه داده‌های فرم است که این مورد در ادامه بیشتر توضیح داده می‌شود.

1<body class="no-widget">
2  <form>
3    <select name="myFruit">
4      <option>Cherry</option>
5      <option>Lemon</option>
6      <option>Banana</option>
7      <option>Strawberry</option>
8      <option>Apple</option>
9    </select>
10
11    <div class="select">
12      <span class="value">Cherry</span>
13      <ul class="optList hidden">
14        <li class="option">Cherry</li>
15        <li class="option">Lemon</li>
16        <li class="option">Banana</li>
17        <li class="option">Strawberry</li>
18        <li class="option">Apple</li>
19      </ul>
20    </div>
21  </form>
22
23</body>

دومین چیزی که نیاز داریم دو کلاس است که امکان پنهان‌سازی و پدیدارسازی عنصر را فراهم می‌سازد. یعنی عنصر واقعی <select> در صورت عدم کارکرد جاوا اسکریپت نمایش می‌یابد و در صورتی که اجرا شود ویجت سفارشی‌مان را نمایش می‌دهیم. توجه کنید که به صورت پیش‌فرض کد HTML ویجت سفارشی ما را پنهان می‌سازد.

1.widget select,
2.no-widget .select {
3  /* This CSS selector basically says:
4     - either we have set the body class to "widget" and thus we hide the actual <select> element
5     - or we have not changed the body class, therefore the body class is still "no-widget",
6       so the elements whose class is "select" must be hidden */
7  position : absolute;
8  left     : -5000em;
9  height   : 0;
10  overflow : hidden;
11}

اینک صرفاً نیازمند یک کد جاوا اسکریپت هستیم که تعیین کند آیا اسکریپت اجرا می‌شود یا نه. این سوئیچ بسیار ساده است. اگر در زمان بارگذاری صفحه اسکریپت ما اجرا شود، کلاس no-widget را حذف خواهد کرد و کلاس widget را اضافه می‌کند. بدین ترتیب پدیداری عنصر <select> و پدیداری ویجت سفارشی با هم عوض می‌شوند.

1window.addEventListener("load", function () {
2  document.body.classList.remove("no-widget");
3  document.body.classList.add("widget");
4});

نکته: اگر می‌خواهید کد خود را عمومی‌تر کنید، به طوری که قابلیت استفاده مجدد داشته باشد به جای اجرای سوئیچ کلاس، راه‌حل بسیار بهتر این است که یک کلاس ویجت برای پنهان‌سازی عناصر <select> اضافه کنید و به صورت دینامیک به درخت DOM اضافه کنید تا پس از هر عنصر <select> ویجت سفارشی را در صفحه نمایش دهد.

آسان‌تر ساختن کار

ما در کدی که می‌خواهیم بنویسیم از API استاندارد DOM برای انجام همه کارهای مورد نیاز استفاده می‌کنیم. با این حال، گرچه API مربوط به DOM در مرورگرها پشتیبانی بسیار بهتری دارد، اما در مرورگرهای قدیمی با مشکلاتی همراه است.

اگر می‌خواهید از بروز مشکل در مرورگرهای قدیمی جلوگیری کنید، دو روش وجود دارد: یکی این که از فریمورک‌های اختصاصی مانند jQuery ،$dom ،prototype ،Dojo ،YUI و موارد مشابه استفاده کنید و یا این که قابلیت مفقود را که قرار است استفاده شود polyfill کنید. این کار به سادگی از طریق کتابخانه‌هایی مانند yepnope ممکن است.

قابلیت‌هایی که ما قصد داریم استفاده کنیم به ترتیب از گزینه‌های پر ریسک تا امن‌تر به صورت زیر است:

  1. classList
  2. addEventListener
  3. forEach (این جزء DOM نیست بلکه JavaScript مدرن است)
  4. querySelector و querySelectorAll

علاوه بر موجود بودن این قابلیت‌های خاص همچنان یک مشکل باقی مانده است. شیء بازگشتی از سوی تابع ()querySelectorAll به جای این که یک آرایه باشد، یک NodeList است. این امر مهمی است زیرا اشیای Array از تابع forEach پشتیبانی می‌کنند، اما NodeList یک چنین پشتیبانی ندارند. از آنجا که NodeList در واقع مانند یک آرایه به نظر می‌رسد و همچنین از آنجا که استفاده از forEach بسیار آسان است، می‌توانیم به صورت زیر به سادگی پشتیبانی از forEach را نیز به NodeList اضافه کنیم تا همه کارها آسان‌تر شوند:

1NodeList.prototype.forEach = function (callback) {
2  Array.prototype.forEach.call(this, callback);
3}

چنان که می‌بینید اجرای این کار واقعاً آسان است.

ساخت Callback-های رویداد

اینک زمینه کار آماده شده است و می‌توانیم شروع به تعریف کردن همه تابع‌هایی بکنیم که هر بار در تعامل‌های کاربر با ویجت مورد استفاده قرار خواهند گرفت:

1// This function will be used each time we want to deactivate a custom widget
2// It takes one parameter
3// select : the DOM node with the `select` class to deactivate
4function deactivateSelect(select) {
5
6  // If the widget is not active there is nothing to do
7  if (!select.classList.contains('active')) return;
8
9  // We need to get the list of options for the custom widget
10  var optList = select.querySelector('.optList');
11
12  // We close the list of option
13  optList.classList.add('hidden');
14
15  // and we deactivate the custom widget itself
16  select.classList.remove('active');
17}
18
19// This function will be used each time the user wants to (de)activate the widget
20// It takes two parameters:
21// select : the DOM node with the `select` class to activate
22// selectList : the list of all the DOM nodes with the `select` class
23function activeSelect(select, selectList) {
24
25  // If the widget is already active there is nothing to do
26  if (select.classList.contains('active')) return;
27
28  // We have to turn off the active state on all custom widgets
29  // Because the deactivateSelect function fulfill all the requirement of the
30  // forEach callback function, we use it directly without using an intermediate
31  // anonymous function.
32  selectList.forEach(deactivateSelect);
33
34  // And we turn on the active state for this specific widget
35  select.classList.add('active');
36}
37
38// This function will be used each time the user wants to open/closed the list of options
39// It takes one parameter:
40// select : the DOM node with the list to toggle
41function toggleOptList(select) {
42
43  // The list is kept from the widget
44  var optList = select.querySelector('.optList');
45
46  // We change the class of the list to show/hide it
47  optList.classList.toggle('hidden');
48}
49
50// This function will be used each time we need to highlight an option
51// It takes two parameters:
52// select : the DOM node with the `select` class containing the option to highlight
53// option : the DOM node with the `option` class to highlight
54function highlightOption(select, option) {
55
56  // We get the list of all option available for our custom select element
57  var optionList = select.querySelectorAll('.option');
58
59  // We remove the highlight from all options
60  optionList.forEach(function (other) {
61    other.classList.remove('highlight');
62  });
63
64  // We highlight the right option
65  option.classList.add('highlight');
66};

این همه آن چیزی است که برای مدیریت حالت‌های مختلف ویجت سفارشی نیاز داریم. در ادامه این تابع‌ها را به رویدادهای متناظر اتصال می‌دهیم:

1// We handle the event binding when the document is loaded.
2window.addEventListener('load', function () {
3  var selectList = document.querySelectorAll('.select');
4
5  // Each custom widget needs to be initialized
6  selectList.forEach(function (select) {
7
8    // as well as all its `option` elements
9    var optionList = select.querySelectorAll('.option');
10
11    // Each time a user hovers their mouse over an option, we highlight the given option
12    optionList.forEach(function (option) {
13      option.addEventListener('mouseover', function () {
14        // Note: the `select` and `option` variable are closures
15        // available in the scope of our function call.
16        highlightOption(select, option);
17      });
18    });
19
20    // Each times the user click on a custom select element
21    select.addEventListener('click', function (event) {
22      // Note: the `select` variable is a closure
23      // available in the scope of our function call.
24
25      // We toggle the visibility of the list of options
26      toggleOptList(select);
27    });
28
29    // In case the widget gain focus
30    // The widget gains the focus each time the user clicks on it or each time
31    // they use the tabulation key to access the widget
32    select.addEventListener('focus', function (event) {
33      // Note: the `select` and `selectList` variable are closures
34      // available in the scope of our function call.
35
36      // We activate the widget
37      activeSelect(select, selectList);
38    });
39
40    // In case the widget loose focus
41    select.addEventListener('blur', function (event) {
42      // Note: the `select` variable is a closure
43      // available in the scope of our function call.
44
45      // We deactivate the widget
46      deactivateSelect(select);
47    });
48  });
49});

در این نقطه، ویجت ما حالت خود را بر اساس طراحی‌مان تغییر می‌دهد، اما هنوز مقدار آن به‌روزرسانی نمی‌شود. این موضوع را در بخش بعدی مدیریت می‌کنیم.

مدیریت مقدار ویجت

اکنون که ویجت عملیاتی شده است، باید کد به‌روزرسانی مقدار آن را نیز بر اساس ورودی کاربر اضافه کرده و آن را آماده ارسال مقدار همراه با داده‌های فرم بکنیم.

ساده‌ترین روش برای انجام این کار استفاده از یک ویجت نیتیو در پس‌زمینه است. یک چنین ویجتی رد همه مقادیر ویجت ما را با استفاده از کنترل‌های داخلی که از سوی مرورگر ارائه شده نگهداری می‌کند و این مقادیر در زمان ارسال داده‌های فرم به سرور فرستاده می‌شوند. بدین ترتیب هیچ نیازی به اختراع مجدد چرخ نخواهد بود، چون همه این کارها از قبل برای ما مهیا شده است.

چنان که پیش‌تر دیدیم، ما هم اینک از یک ویجت select نیتیو به عنوان fallback در صورت بروز مشکلات دسترس‌پذیری استفاده می‌کنیم. بدین ترتیب می‌توانیم مقادیر آن را با مقادیر ویجت سفارشی خود همگام‌سازی کنیم.

1// This function updates the displayed value and synchronizes it with the native widget.
2// It takes two parameters:
3// select : the DOM node with the class `select` containing the value to update
4// index  : the index of the value to be selected
5function updateValue(select, index) {
6  // We need to get the native widget for the given custom widget
7  // In our example, that native widget is a sibling of the custom widget
8  var nativeWidget = select.previousElementSibling;
9
10  // We also need  to get the value placeholder of our custom widget
11  var value = select.querySelector('.value');
12
13  // And we need the whole list of options
14  var optionList = select.querySelectorAll('.option');
15
16  // We set the selected index to the index of our choice
17  nativeWidget.selectedIndex = index;
18
19  // We update the value placeholder accordingly
20  value.innerHTML = optionList[index].innerHTML;
21
22  // And we highlight the corresponding option of our custom widget
23  highlightOption(select, optionList[index]);
24};
25
26// This function returns the current selected index in the native widget
27// It takes one parameter:
28// select : the DOM node with the class `select` related to the native widget
29function getIndex(select) {
30  // We need to access the native widget for the given custom widget
31  // In our example, that native widget is a sibling of the custom widget
32  var nativeWidget = select.previousElementSibling;
33
34  return nativeWidget.selectedIndex;
35};

با استفاده از این دو تابع می‌توانیم ویجت‌های نیتیو را به ویجت‌های سفارشی اتصال دهیم:

1// We handle event binding when the document is loaded.
2window.addEventListener('load', function () {
3  var selectList = document.querySelectorAll('.select');
4
5  // Each custom widget needs to be initialized
6  selectList.forEach(function (select) {
7    var optionList = select.querySelectorAll('.option'),
8        selectedIndex = getIndex(select);
9
10    // We make our custom widget focusable
11    select.tabIndex = 0;
12
13    // We make the native widget no longer focusable
14    select.previousElementSibling.tabIndex = -1;
15
16    // We make sure that the default selected value is correctly displayed
17    updateValue(select, selectedIndex);
18
19    // Each time a user clicks on an option, we update the value accordingly
20    optionList.forEach(function (option, index) {
21      option.addEventListener('click', function (event) {
22        updateValue(select, index);
23      });
24    });
25
26    // Each time a user uses their keyboard on a focused widget, we update the value accordingly
27    select.addEventListener('keyup', function (event) {
28      var length = optionList.length,
29          index  = getIndex(select);
30
31      // When the user hits the down arrow, we jump to the next option
32      if (event.keyCode === 40 && index < length - 1) { index++; }
33
34      // When the user hits the up arrow, we jump to the previous option
35      if (event.keyCode === 38 && index > 0) { index--; }
36
37      updateValue(select, index);
38    });
39  });
40});

در کد فوق، لازم به ذکر است که از مشخصه tabIndex استفاده شده است. استفاده از این مشخصه برای تضمین این موضوع که ویجت نیتیو هرگز فوکوس نخواهد گرفت ضروری است. همچنین این اطمینان را حاصل می‌کند که ویجت سفارشی ما زمانی که کاربر از کیبورد یا ماوس خود استفاده می‌کند فوکوس را دریافت خواهد کرد. بدین ترتیب کار به پایان رسیده است، اما اندکی صبر کنید، چون هنوز کار ما به طور کامل به پایان نرسیده است. در بخش بعدی در این خصوص بیشتر توضیح می‌دهیم.

دسترس‌پذیر ساختن

با این که ویجت سفارشی ما فاصله زیادی با یک کادر انتخاب با امکانات کامل دارد، اما اکنون چیزی ساخته‌ایم که کار می‌کند. با این حال کاری که تا به اینجا انجام داده‌ایم، چیزی به جز دستکاری DOM نبوده است. این ویجت هنوز هیچ خصوصیت معناشناختی ندارد و هر چند شبیه به یک کادر انتخاب به نظر می‌رسد، اما از نظر مرورگر چنین چیزی نیست، بنابراین فناوری‌های حمایتی نمی‌توانند آن را به عنوان یک کادر انتخاب تشخیص دهند. به طور خلاصه، کادر انتخاب کاملاً جدید ما دسترس‌پذیر نیست.

خوشبختانه برای این مشکل راه‌حلی به نام ARIA وجود دارد. ARIA اختصاری برای عبارت «Accessible Rich Internet Application» (اپلیکیشن اینترنتی کامل دسترس‌پذیر) است و مشخصات W3C آن به طور خاص برای کاری که اینک قصد داریم انجام دهیم یعنی دسترس‌پذیر ساختن ویجت‌های سفارشی و اپلیکیشن‌ها طراحی شده است. ARIA اساساً به مجموعه‌ای از خصوصیت‌ها گفته می‌شود که HTML را طوری بسط می‌دهند که می‌تواند نقش‌ها، حالت‌ها و مشخصه‌ها را بهتر توضیح دهد. استفاده از این خصوصیت‌ها کاملاً ساده است و در بخش بعدی آن‌ها را بررسی می‌کنیم

خصوصیت role

خصوصیت کلیدی مورد استفاده از سوی ARIA به نام خصوصیت role شناخته می‌شود. خصوصیت role یک مقدار می‌گیرد که به تعریف کاربرد یک عنصر می‌پردازد. هر role الزامات و رفتارهای خاص خود را تعریف می‌کند. ما در مثال خودمان از نقش listbox استفاده می‌کنیم. این نقش در واقع یک نقش ترکیبی است، یعنی عناصری که این نقش را می‌پذیرند دارای فرزندانی هستند که هر یک نقشی خاص دارند. در این مورد دست‌کم یک فرزند با نقش option وجود دارد.

همچنین لازم به ذکر است که ARIA نقش‌هایی تعریف می‌کند که به صورت پیش‌فرض روی markup استاندارد HTML اعمال می‌شوند. برای نمونه عنصر <table> با نقش grid مطابقت دارد و عنصر <ul> نیز با نقش list مطابقت پیدا می‌کند. از آنجا که ما از عنصر <ul> استفاده می‌کنیم، می‌خواهیم مطمئن شویم که نقش listbox ویجت ما نقش list عنصر <ul> را سرکوب می‌کند. به این منظور از نقش presentation استفاده می‌کنیم. این نقش صرفاً به منظور ارائه اطلاعات استفاده می‌شود. ما آن را روی عنصر <ul> به کار می‌گیریم.

برای پشتیبانی از نقش listbox کافی است HTML خود را به صورت زیر به‌روزرسانی کنیم:

1<!-- We add the role="listbox" attribute to our top element -->
2<div class="select" role="listbox">
3  <span class="value">Cherry</span>
4  <!-- We also add the role="presentation" to the ul element -->
5  <ul class="optList" role="presentation">
6    <!-- And we add the role="option" attribute to all the li elements -->
7    <li role="option" class="option">Cherry</li>
8    <li role="option" class="option">Lemon</li>
9    <li role="option" class="option">Banana</li>
10    <li role="option" class="option">Strawberry</li>
11    <li role="option" class="option">Apple</li>
12  </ul>
13</div>

نکته: گنجاندن خصوصیت role و خصوصیت class تنها در صورتی ضروری است که بخواهیم از مرورگرهای قدیمی پشتیبانی کنیم که از سلکتورهای خصوصیت CSS پشتیبانی نمی‌کنند.

خصوصیت aria-selected

استفاده از خصوصیت role کافی نیست. ARIA همچنین حالت‌ها و خصوصیت‌های مشخصه زیادی را ارائه می‌کند. هر چه از آن‌ها بیشتر و بهتر استفاده کنید، ویجت شما به روش بهتری از سوی فناوری‌های حمایتی درک خواهد شد. در این مورد ما استفاده خود را محدود به یک خصوصیت یعنی aria-selected می‌کنیم.

خصوصیت aria-selected برای نمایش گزینه‌ای که هم اینک انتخاب شده استفاده می‌شود. بدین ترتیب فناوری‌های حمایتی می‌توانند به کاربر اطلاع دهند که انتخاب کنونی چیست. ما از آن به صورت دینامیک در جاوا اسکریپت استفاده می‌کنیم تا هر بار که کاربر گزینه‌ای را انتخاب می‌کند آن را علامت‌گذاری کنیم. به این منظور باید تابع ()updateValue خود را بازبینی کنیم:

1function updateValue(select, index) {
2  var nativeWidget = select.previousElementSibling;
3  var value = select.querySelector('.value');
4  var optionList = select.querySelectorAll('.option');
5
6  // We make sure that all the options are not selected
7  optionList.forEach(function (other) {
8    other.setAttribute('aria-selected', 'false');
9  });
10
11  // We make sure the chosen option is selected
12  optionList[index].setAttribute('aria-selected', 'true');
13
14  nativeWidget.selectedIndex = index;
15  value.innerHTML = optionList[index].innerHTML;
16  highlightOption(select, optionList[index]);
17};

سخن پایانی

در این مقاله با همه مبانی ساخت یک ویجت سفارشی آشنا شدیم، اما چنان که می‌بینید اجرای آن چندان ساده نیست و در اغلب موارد بهتر است که به جای کدنویسی ویجت خودمان از صفر تا صد از کتابخانه‌های شخص ثالث استفاده کنیم.

در ادامه چند کتابخانه می‌بینید که بهتر است پیش از کدنویسی ویجت خودتان مورد بررسی قرار دهید:

  • jQuery UI
  • msDropDown
  • Nice Forms
  • و بسیاری دیگر

اگر می‌خواهید پا را فراتر از موارد مطرح‌شده در این راهنما بگذارید، پیشنهاد می‌کنیم برخی بهبودها روی مثال ارائه شده در این راهنما اجرا کنید تا آن را عمومی‌تر ساخته و قابلیت استفاده مجدد به آن ببخشید. این تمرینی است که می‌توانید انجام دهید و در این مسیر دو سرنخ به شما کمک می‌کند: نخستین آرگومان همه تابع‌ها ما یکسان است، یعنی این تابع‌ها به context یکسانی نیاز دارند. ساخت یک شیء برای اشتراک آن context راه‌حل معقولی است و ضمناً باید آن را feature-proof کنید، یعنی باید کارکرد آن را روی انواع مختلف مرورگرها که سازگاری‌شان با استانداردهای وب متفاوت است بهبود ببخشید. برای مطالعه بخش بعدی این سری مقالات به لینک زیر رجوع کنید:

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۳ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
developer.mozilla
نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *