قابلیت کشیدن و رها کردن در جاوا اسکریپت — به زبان ساده
در این مقاله به توضیح شیوه پیادهسازی قابلیت کشیدن و رها کردن در جاوا اسکریپت میپردازیم که با API بکاند تعامل یافته و بهروزرسانی میشود. به این منظور عناصر DOM را بر اساس دادههای API رندر شده اضافه میکنیم. همچنین آن را طوری طراحی میکنیم که عناصر قابل کشیدن و رها شدن شوند و API را نیز در پاسخ به این Drag and Drop بهروزرسانی میکنیم. در این فرایند برخی از کامپوننتهای ابتدایی «کشیدن و رها کردن» (Drag and Drop) را نیز معرفی میکنیم.
کارکردی که میخواهیم بسازیم چیزی مانند تصویر زیر است:
کد کامل این اپلیکیشن را میتوانید در این ریپوی گیتهاب (+) ملاحظه کنید.
گام 0: ذخیره عناصر HTML به صورت متغیر
ما دو کانتینر برای کارهای خود داریم که در چند جا به آنها نیاز خواهیم داشت.
بنابراین پیش از آغاز کار باید آنها را به صورت متغیرهای سراسری در ابتدای فایل جاوا اسکریپت ذخیره کنیم. این کانتینرها صرفاً عناصر <div> در فایل HTML هستند:
1const unfinishedTasks = document.querySelector('#unfinished-tasks');
2const finishedTasks = document.querySelector('#finished-tasks');
گام 1: دریافت دادهها از بکاند
بکاند به صورت Rails API نیز برخی دادهها را در مورد وظایف (Tasks) در این اپلیکیشن ارائه میکند. هر وظیفه دو خصوصیت به نامهای content با نوع رشته و finished با نوع بولی دارد. میتوانیم از ()fetch برای دریافت دادههای وظیفه استفاده کنیم:
1fetch('http://localhost:3000/tasks')
2.then(res => res.json())
3.then(tasksArr => {
4 tasksArr.forEach(task => handleTask(task))
5});
پس از تحلیل دادهها یک آرایه از اشیای وظیفه خواهیم داشت و با تعریف حلقهای روی این آرایه اقدام به فراخوانی تابع ()handleTask روی هر شیء وظیفه میکنیم.
گام 2: تبدیل دادهها به عناصر DOM قابل کشیدن
متد ()handleTask را که در بخش قبل توضیح دادیم به صورت زیر تعریف میکنیم:
1function handleTask(task) {
2 let taskPTag = document.createElement('p');
3 taskPTag.innerText = task.content;
4 taskPTag.id = `task ${task.id}`;
5 taskPTag.classList.add('task');
6 taskPTag.dataset.databaseId = task.id;
7 if (task.finished === true) {
8 taskPTag.dataset.id = 'finished';
9 finishedTasks.append(taskPTag);
10 } else {
11 taskPTag.dataset.id = 'unfinished'
12 unfinishedTasks.append(taskPTag);
13 };
14 taskPTag.setAttribute('draggable', 'true');
15 addDragEventListeners(task, taskPTag);
16};
غالب کد این تابع صرفاً به ساخت عنصر <p> برای هر وظیفه اختصاص دارد و به آن یک متن و یک دسته از خصوصیتها میدهد و آن را در کانتینر وظیفه مناسب خود قرار میدهد. به جز class که برای استایلبندی است، هر یک از خصوصیتها مقصود خاصی در کد جاوا اسکریپت ما دارند.
دو خط آخر این تابع جایی است که کارکرد کشیدن و رها کردن در عمل آغاز میشود. خط اول خصوصیت جدید دیگر <p> به نام draggable را ارائه کرده و مقدار آن را True تنظیم میکند. خط دوم متدی به نام ()addDragEventListeners را فراخوانی کرده و هر دو شیء task را به همراه <p> جدیداً ایجاد شده به عنوان آرگومان به آن میفرستد. این متد را در بخش بعدی تعریف و آنالیز میکنیم.
گام 3: افزودن شنوندههای رویداد به عناصر قابل کشیدن
اگر فراخوانی متد ()addDragEventListeners را در بخش قبلی کامنت کنیم، کارکرد ما صورت زیر اجرا میشود:
چنان که میبینید امکان کشیدن وجود دارد، اما زمانی که آنها را میکشیم، اتفاقی رخ نمیدهد. برای اصلاح این مشکل ابتدا باید برخی شنوندههای رویداد به <p>-ها اضافه کنیم. این کار با استفاده از تابع ()addDragEventListeners انجام مییابد. این تابع را در گام قبلی فراخوانی کردیم و اکنون آن را تعریف میکنیم:
1function addDragEventListeners(task, taskPTag) {
2 taskPTag.addEventListener('dragstart', evt => {
3 evt.dataTransfer.setData('text', evt.target.id);
4 }, false);
5 taskPTag.addEventListener('dragend', evt => {
6 evt.dataTransfer.clearData();
7 if (document.querySelector('.over')) {
8 document.querySelector('.over').classList.remove('over');
9 };
10 });
11};
برای هر یک از <p>-ها دو شنونده رویداد اضافه میکنیم که یکی برای آغاز کشیدن (dragstart) و دیگری برای پایان کشیدن (dragend) است. در dragstart اقدام به دریافت id آن <p> که کشیده شده میکنیم و آن را به مشخصه رویداد کشیدن به نام dataTransfer ارسال میکنیم. این کار به ما امکان میدهد که به id مربوطه از یک شنونده رویداد متفاوت دسترسی داشته باشیم که در ادامه از آن برای دسترسی به خود عنصر کشیده شده استفاده خواهیم کرد.
در dragend این اطلاعات را از dataTransfer پاک میکنیم و سپس از گزاره if برای استایلبندی بهره خواهیم گرفت. کلاس over سایه سبزرنگ را به کانتینرها میدهد و نمیخواهیم زمانی که چرخه کشیدن پایان مییابد، تعللی در این خصوص روی دهد. افزودن شنوندههای رویداد کشیدن به عناصر قابل کشیدن تنها نیمی از کار محسوب میشود. اکنون باید مناطق رها کردن را تنظیم کنیم.
گام 4: تنظیم مناطق رها کردن
عناصری که کشیده میشوند از سوی مناطق رها کردن پذیرفته نمیشوند، مگر این که آنها را این گونه تنظیم کنیم. این کار از طریق ارائه شنوندههای رویداد کشیدن به آنها صورت میگیرد. دو منطقه رها کردن داریم که هر یک باید شنوندههای رویداد یکسانی داشته باشند.
در این بخش قصد داریم یک تابع به نام ()addDropzoneEventListeners تعریف کرده و این تابع را برای هر یک از منطقههای کشیدن فراخوانی کنیم. این همان جایی است که مسائل کمی بغرنج میشوند، بنابراین کار را گام به گام پیش میبریم.
1addDropzoneEventListeners(unfinishedTasks);
2addDropzoneEventListeners(finishedTasks);
3function addDropzoneEventListeners(dropzone) {
4 dropzone.addEventListener('dragover', evt => {
5 evt.preventDefault();
6 evt.currentTarget.classList.add('over');
7 }, false);
8 dropzone.addEventListener('dragleave', evt => {
9 evt.currentTarget.classList.remove('over');
10 }, false);
11 ...
12};
تابع ()addDropzoneEventListeners سه شنونده رویداد به هر یک از مناطق رها کردن اضافه میکند که دو مورد از آنها در تصویر فوق دیده میشوند. هر دوی این dragover و dragleave مقاصدی دوگانه دارند. مقصود نخست آن است که از رفتار پیشفرض رویداد واقعی dragover جلوگیری کنند. رفتار پیشفرض امکان رخداد فرایند رها کردن را میسر نمیسازد، از این رو باید جلوی آن را بگیریم تا رها کردن رخ بدهد. مقصود دیگر این شنوندههای رویداد، استایلبندی است. بدین ترتیب همچنان که کلاس over را اضافه کردیم، آن را روی منطقه رها کردن حذف میکنیم. چنان که پیشتر به اختصار اشاره کردیم، این همان جایی است که سایه کادر سبزرنگ ظاهر میشود. البته این موضوع برای کارکرد صحیح ضروری نیست، اما از جهت تجربه کاربری مطلوب است.
اینک در آخرین بخش، رها کردن را در عمل مدیریت میکنیم. این بخشی است که DOM ما بهروزرسانی میشود و با بکاند تعامل مییابد. شنونده رویداد آخر را نیز اضافه میکنیم:
1addDropzoneEventListeners(unfinishedTasks);
2addDropzoneEventListeners(finishedTasks);
3function addDropzoneEventListeners(dropzone) {
4 ...
5 dropzone.addEventListener('drop', evt => {
6
7 let elemId = evt.dataTransfer.getData('text');
8 let droppedElem = document.getElementById(elemId);
9 if (droppedElem.dataset.id !== evt.currentTarget.dataset.id) {
10 evt.currentTarget.append(droppedElem);
11 evt.dataTransfer.clearData();
12 const dataIdToggle = {
13 'finished': 'unfinished',
14 'unfinished': 'finished'
15 };
16 droppedElem.dataset.id = dataIdToggle[droppedElem.dataset.id];
17 const databaseAttributeMap = {
18 'finished': true,
19 'unfinished': false
20 };
21 let isFinished = databaseAttributeMap[droppedElem.dataset.id];
22 let id = droppedElem.dataset.databaseId;
23
24 fetch(`http://localhost:3000/tasks/${id}`, {
25 method: 'PATCH',
26 headers: {
27 'Content-Type': 'application/json',
28 Accept: 'application/json'
29 },
30 body: JSON.stringify({
31 'finished': isFinished
32 })
33 })
34 .then(res => res.json())
35 .then(task => {
36 task.finished === true ? alert('Way to Go!!!') : alert('Darn
37 ?')
38 });
39 };
40 }, false);
41};
در این بخش کد فوق را کمی توضیح میدهیم. ابتدا از evt.dataTransfer.getData() برای دسترسی به id عناصر dragged/dropped بهره میگیریم و سپس عنصر واقعی را مییابیم. سپس یک گزاره if داریم که منظور از آن تعیین این است که آیا وظیفه به نام finished در کانتینر وظیفه unfinished رها شده است یا نه و همچنین برعکس. اگر چنین باشد، ادامه میدهیم، اگر چنین نباشد چرخه کشیدن پایان مییابد و عنصر کشیده شده به آنجایی که بود بازمیگردد. اگر وظیفه در همان کانتینر رها شده باشد، لازم نیست اتفاقی بیفتد.
اگر گزاره if مقدار true بازگشت دهد، به مقصود کارکرد خود دست یافتهایم. در ادامه برخی از بخشهای کد را بیشتر توضیح میدهیم. دو خط اول باعث جابجایی عنصر کشیده شده به منطقه رها کردن میشوند و دادهها را از شیء dataTransfer پاک میکنند:
1evt.currentTarget.append(droppedElem);
2evt.dataTransfer.clearData();
توجه داشته باشید که این بهروزرسانی به صورت خوشبینانهای رندر میشود که برای یک کد که به ردگیری وظایف ساده میپردازد معقول به نظر میرسد. سپس خصوصیت id مجموعه داده عنصر جابجا شده را تغییر میدهیم تا با مکان جدید مطابقت یابد:
1const dataIdToggle = {
2 'finished': 'unfinished',
3 'unfinished': 'finished'
4};
5droppedElem.dataset.id = dataIdToggle[droppedElem.dataset.id];
این کار به ما امکان میدهد که عناصر را به سمت جلو و عقب بین کانتینرهای وظیفه finished و unfinished در فرانتاند حرکت دهیم و همچنین به ما کمک میکند که دادههای وظیفه را به طرز مناسبی در بکاند بهروزرسانی کنیم. پیش از آن که درخواست خود را برای بهروزرسانی API بکاند ارائه کنیم، چند داده را باید در اختیار داشته باشیم. یکی از این موارد شامل اطلاعات جدیدی است که با آن وظیفه را بهروزرسانی میکنیم. همچنین شناسه وظیفه خاصی که قرار است بهروزرسانی شود، نیز باید در اختیار ما باشد:
1const databaseAttributeMap = {
2 'finished': true,
3 'unfinished': false
4};
5let isFinished = databaseAttributeMap[droppedElem.dataset.id];
6let id = droppedElem.dataset.databaseId;
در نهایت درخواست خود را با ()fetch ارائه میکنیم:
1fetch(`http://localhost:3000/tasks/${id}`, {
2 method: 'PATCH',
3 headers: {
4 'Content-Type': 'application/json',
5 Accept: 'application/json'
6 },
7 body: JSON.stringify({
8 'finished': isFinished
9 })
10})
11.then(res => res.json())
12.then(task => {
13 task.finished === true ? alert('Way to Go!!!') : alert('Darn ?')
14});
بدین ترتیب وظیفه مورد نظر در بکاند نیز بهروزرسانی میشود، به طوری که اگر کاربر صفحه را رفرش کند، وظیفه در محل جدید خود باقی میماند. توجه کنید که ترتیب وظایف در لیست ممکن است تغییر یابد چون به ترتیب آنها در API وابسته است. حفظ ترتیب وظایف در فرانتاند خارج از موضوع این مقاله است. همچنین فیدبک دیگری به شکل یک هشدار سفارشیسازی شده در اختیار کاربر قرار میدهیم. در نهایت کارکرد مورد نظر ما آماده شده است:
سخن پایانی
قابلیت «کشیدن و رها کردن» (drag and drop) بسیار کاربرپسند است. کارکرد آن شهودی است و بازخورد بصری مناسبی در اختیار کاربر قرار میدهد. پیادهسازی آن نیز چندان دشوار نیست. یک قابلیت بسیار ساده کشیدن و رها کردن را میتوان در چند گام اجرا کرد. همچنین با افزودن مقداری تعاملپذیری بکاند میتوان قدرت و کارکرد قابلیت را به مقدار زیادی افزایش داد.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای JavaScript (جاوا اسکریپت)
- مجموعه آموزشهای برنامهنویسی
- آموزش جاوا اسکریپت (JavaScript)
- ۶ متد آرایه جاوا اسکریپت برای کدنویسی بهینهتر — راهنمای کاربردی
- مقادیر NaN در جاوا اسکریپت — به زبان ساده
==