|
|
<!--
|
|
|
CO_OP_TRANSLATOR_METADATA:
|
|
|
{
|
|
|
"original_hash": "30f8903a1f290e3d438dc2c70fe60259",
|
|
|
"translation_date": "2025-08-24T12:04:37+00:00",
|
|
|
"source_file": "3-terrarium/3-intro-to-DOM-and-closures/README.md",
|
|
|
"language_code": "fa"
|
|
|
}
|
|
|
-->
|
|
|
# پروژه تراریوم بخش ۳: دستکاری DOM و Closure
|
|
|
|
|
|

|
|
|
> طراحی توسط [Tomomi Imura](https://twitter.com/girlie_mac)
|
|
|
|
|
|
## آزمون پیش از درس
|
|
|
|
|
|
[آزمون پیش از درس](https://ff-quizzes.netlify.app/web/quiz/19)
|
|
|
|
|
|
### مقدمه
|
|
|
|
|
|
دستکاری DOM یا "مدل شیء سند" یکی از جنبههای کلیدی توسعه وب است. طبق گفته [MDN](https://developer.mozilla.org/docs/Web/API/Document_Object_Model/Introduction)، "مدل شیء سند (DOM) نمایش دادهای از اشیاء است که ساختار و محتوای یک سند در وب را تشکیل میدهند." چالشهای مربوط به دستکاری DOM در وب اغلب دلیل استفاده از فریمورکهای جاوااسکریپت به جای جاوااسکریپت خالص برای مدیریت DOM بوده است، اما ما خودمان این کار را انجام خواهیم داد!
|
|
|
|
|
|
علاوه بر این، این درس ایدهای از یک [Closure در جاوااسکریپت](https://developer.mozilla.org/docs/Web/JavaScript/Closures) را معرفی میکند، که میتوانید آن را به عنوان یک تابع درون تابع دیگر تصور کنید، به طوری که تابع داخلی به محدوده تابع خارجی دسترسی دارد.
|
|
|
|
|
|
> Closureها در جاوااسکریپت موضوعی گسترده و پیچیده هستند. این درس به ایده ابتدایی اشاره میکند که در کد این تراریوم، یک Closure وجود دارد: یک تابع داخلی و یک تابع خارجی که به گونهای ساخته شدهاند که تابع داخلی به محدوده تابع خارجی دسترسی داشته باشد. برای اطلاعات بیشتر در مورد نحوه عملکرد این موضوع، لطفاً به [مستندات گسترده](https://developer.mozilla.org/docs/Web/JavaScript/Closures) مراجعه کنید.
|
|
|
|
|
|
ما از یک Closure برای دستکاری DOM استفاده خواهیم کرد.
|
|
|
|
|
|
DOM را به عنوان یک درخت تصور کنید که تمام روشهایی را که میتوان یک سند صفحه وب را دستکاری کرد، نشان میدهد. APIهای مختلفی (رابطهای برنامهنویسی کاربردی) نوشته شدهاند تا برنامهنویسان بتوانند با استفاده از زبان برنامهنویسی مورد نظر خود به DOM دسترسی پیدا کرده و آن را ویرایش، تغییر، بازآرایی و مدیریت کنند.
|
|
|
|
|
|

|
|
|
|
|
|
> نمایشی از DOM و کد HTML که به آن ارجاع میدهد. از [Olfa Nasraoui](https://www.researchgate.net/publication/221417012_Profile-Based_Focused_Crawler_for_Social_Media-Sharing_Websites)
|
|
|
|
|
|
در این درس، پروژه تعاملی تراریوم خود را با ایجاد جاوااسکریپتی که به کاربر اجازه میدهد گیاهان را در صفحه دستکاری کند، تکمیل خواهیم کرد.
|
|
|
|
|
|
### پیشنیاز
|
|
|
|
|
|
شما باید HTML و CSS تراریوم خود را ساخته باشید. تا پایان این درس، قادر خواهید بود گیاهان را با کشیدن و رها کردن به داخل و خارج از تراریوم منتقل کنید.
|
|
|
|
|
|
### وظیفه
|
|
|
|
|
|
در پوشه تراریوم خود، یک فایل جدید به نام `script.js` ایجاد کنید. این فایل را در بخش `<head>` وارد کنید:
|
|
|
|
|
|
```html
|
|
|
<script src="./script.js" defer></script>
|
|
|
```
|
|
|
|
|
|
> توجه: هنگام وارد کردن یک فایل جاوااسکریپت خارجی به فایل HTML از `defer` استفاده کنید تا جاوااسکریپت فقط پس از بارگذاری کامل فایل HTML اجرا شود. همچنین میتوانید از ویژگی `async` استفاده کنید که به اسکریپت اجازه میدهد در حین تجزیه فایل HTML اجرا شود، اما در مورد ما، مهم است که عناصر HTML قبل از اجرای اسکریپت کشیدن به طور کامل در دسترس باشند.
|
|
|
---
|
|
|
|
|
|
## عناصر DOM
|
|
|
|
|
|
اولین کاری که باید انجام دهید این است که به عناصری که میخواهید در DOM دستکاری کنید، ارجاع دهید. در مورد ما، این عناصر ۱۴ گیاه هستند که در حال حاضر در نوارهای کناری منتظر هستند.
|
|
|
|
|
|
### وظیفه
|
|
|
|
|
|
```html
|
|
|
dragElement(document.getElementById('plant1'));
|
|
|
dragElement(document.getElementById('plant2'));
|
|
|
dragElement(document.getElementById('plant3'));
|
|
|
dragElement(document.getElementById('plant4'));
|
|
|
dragElement(document.getElementById('plant5'));
|
|
|
dragElement(document.getElementById('plant6'));
|
|
|
dragElement(document.getElementById('plant7'));
|
|
|
dragElement(document.getElementById('plant8'));
|
|
|
dragElement(document.getElementById('plant9'));
|
|
|
dragElement(document.getElementById('plant10'));
|
|
|
dragElement(document.getElementById('plant11'));
|
|
|
dragElement(document.getElementById('plant12'));
|
|
|
dragElement(document.getElementById('plant13'));
|
|
|
dragElement(document.getElementById('plant14'));
|
|
|
```
|
|
|
|
|
|
چه اتفاقی در اینجا میافتد؟ شما به سند ارجاع میدهید و در DOM آن به دنبال عنصری با یک Id خاص میگردید. به یاد دارید که در اولین درس HTML به هر تصویر گیاه یک Id جداگانه دادید (`id="plant1"`)؟ اکنون از آن تلاش استفاده خواهید کرد. پس از شناسایی هر عنصر، آن را به تابعی به نام `dragElement` که در یک لحظه خواهید ساخت، ارسال میکنید. بنابراین، عنصر در HTML اکنون قابلیت کشیدن دارد، یا به زودی خواهد داشت.
|
|
|
|
|
|
✅ چرا عناصر را با Id ارجاع میدهیم؟ چرا نه با کلاس CSS؟ ممکن است به درس قبلی در مورد CSS مراجعه کنید تا به این سؤال پاسخ دهید.
|
|
|
|
|
|
---
|
|
|
|
|
|
## Closure
|
|
|
|
|
|
اکنون آمادهاید تا Closure `dragElement` را ایجاد کنید، که یک تابع خارجی است که یک یا چند تابع داخلی را در بر میگیرد (در مورد ما، سه تابع خواهیم داشت).
|
|
|
|
|
|
Closureها زمانی مفید هستند که یک یا چند تابع نیاز به دسترسی به محدوده تابع خارجی داشته باشند. در اینجا یک مثال آورده شده است:
|
|
|
|
|
|
```javascript
|
|
|
function displayCandy(){
|
|
|
let candy = ['jellybeans'];
|
|
|
function addCandy(candyType) {
|
|
|
candy.push(candyType)
|
|
|
}
|
|
|
addCandy('gumdrops');
|
|
|
}
|
|
|
displayCandy();
|
|
|
console.log(candy)
|
|
|
```
|
|
|
|
|
|
در این مثال، تابع `displayCandy` تابعی را در بر میگیرد که یک نوع آبنبات جدید را به آرایهای که قبلاً در تابع وجود دارد، اضافه میکند. اگر این کد را اجرا کنید، آرایه `candy` تعریف نشده خواهد بود، زیرا یک متغیر محلی است (محلی برای Closure).
|
|
|
|
|
|
✅ چگونه میتوانید آرایه `candy` را قابل دسترسی کنید؟ سعی کنید آن را خارج از Closure منتقل کنید. به این ترتیب، آرایه به جای اینکه فقط در محدوده محلی Closure باقی بماند، به صورت سراسری در دسترس خواهد بود.
|
|
|
|
|
|
### وظیفه
|
|
|
|
|
|
زیر اعلان عناصر در `script.js`، یک تابع ایجاد کنید:
|
|
|
|
|
|
```javascript
|
|
|
function dragElement(terrariumElement) {
|
|
|
//set 4 positions for positioning on the screen
|
|
|
let pos1 = 0,
|
|
|
pos2 = 0,
|
|
|
pos3 = 0,
|
|
|
pos4 = 0;
|
|
|
terrariumElement.onpointerdown = pointerDrag;
|
|
|
}
|
|
|
```
|
|
|
|
|
|
`dragElement` شیء `terrariumElement` خود را از اعلانهای بالای اسکریپت دریافت میکند. سپس، برخی موقعیتهای محلی را برای شیء ارسالشده به تابع روی `0` تنظیم میکنید. اینها متغیرهای محلی هستند که برای هر عنصر هنگام افزودن قابلیت کشیدن و رها کردن در Closure دستکاری خواهند شد. تراریوم با این عناصر کشیدهشده پر خواهد شد، بنابراین برنامه باید مکان آنها را پیگیری کند.
|
|
|
|
|
|
علاوه بر این، `terrariumElement` که به این تابع ارسال میشود، یک رویداد `pointerdown` اختصاص داده میشود، که بخشی از [web APIs](https://developer.mozilla.org/docs/Web/API) طراحیشده برای کمک به مدیریت DOM است. `onpointerdown` زمانی اجرا میشود که یک دکمه فشار داده شود، یا در مورد ما، یک عنصر قابل کشیدن لمس شود. این رویداد در هر دو [مرورگر وب و موبایل](https://caniuse.com/?search=onpointerdown) کار میکند، با چند استثنا.
|
|
|
|
|
|
✅ [رویداد `onclick`](https://developer.mozilla.org/docs/Web/API/GlobalEventHandlers/onclick) پشتیبانی بسیار بیشتری در مرورگرهای مختلف دارد؛ چرا از آن در اینجا استفاده نمیکنید؟ به نوع دقیق تعامل صفحهای که میخواهید ایجاد کنید فکر کنید.
|
|
|
|
|
|
---
|
|
|
|
|
|
## تابع Pointerdrag
|
|
|
|
|
|
عنصر `terrariumElement` آماده است تا کشیده شود؛ وقتی رویداد `onpointerdown` اجرا میشود، تابع `pointerDrag` فراخوانی میشود. این تابع را درست زیر این خط اضافه کنید: `terrariumElement.onpointerdown = pointerDrag;`:
|
|
|
|
|
|
### وظیفه
|
|
|
|
|
|
```javascript
|
|
|
function pointerDrag(e) {
|
|
|
e.preventDefault();
|
|
|
console.log(e);
|
|
|
pos3 = e.clientX;
|
|
|
pos4 = e.clientY;
|
|
|
}
|
|
|
```
|
|
|
|
|
|
چندین اتفاق میافتد. اول، شما رویدادهای پیشفرضی که معمولاً در `pointerdown` رخ میدهند را با استفاده از `e.preventDefault();` غیرفعال میکنید. به این ترتیب کنترل بیشتری بر رفتار رابط کاربری دارید.
|
|
|
|
|
|
> وقتی فایل اسکریپت را به طور کامل ساختید، به این خط بازگردید و آن را بدون `e.preventDefault()` امتحان کنید - چه اتفاقی میافتد؟
|
|
|
|
|
|
دوم، فایل `index.html` را در یک پنجره مرورگر باز کنید و رابط کاربری را بررسی کنید. وقتی روی یک گیاه کلیک میکنید، میتوانید ببینید که چگونه رویداد 'e' ثبت میشود. به جزئیات رویداد نگاه کنید تا ببینید چقدر اطلاعات با یک رویداد pointerdown جمعآوری میشود!
|
|
|
|
|
|
سپس، توجه کنید که چگونه متغیرهای محلی `pos3` و `pos4` به `e.clientX` تنظیم میشوند. میتوانید مقادیر `e` را در پنجره بررسی مشاهده کنید. این مقادیر مختصات x و y گیاه را در لحظهای که روی آن کلیک میکنید یا آن را لمس میکنید، ثبت میکنند. شما به کنترل دقیق رفتار گیاهان هنگام کلیک و کشیدن آنها نیاز دارید، بنابراین مختصات آنها را پیگیری میکنید.
|
|
|
|
|
|
✅ آیا اکنون واضحتر شده است که چرا کل این برنامه با یک Closure بزرگ ساخته شده است؟ اگر اینطور نبود، چگونه میتوانستید محدوده هر یک از ۱۴ گیاه قابل کشیدن را مدیریت کنید؟
|
|
|
|
|
|
تابع اولیه را با افزودن دو دستکاری دیگر رویداد pointer زیر `pos4 = e.clientY` کامل کنید:
|
|
|
|
|
|
```html
|
|
|
document.onpointermove = elementDrag;
|
|
|
document.onpointerup = stopElementDrag;
|
|
|
```
|
|
|
|
|
|
اکنون شما مشخص میکنید که میخواهید گیاه همراه با نشانگر هنگام حرکت کشیده شود و حرکت کشیدن زمانی که گیاه را از انتخاب خارج میکنید متوقف شود. `onpointermove` و `onpointerup` همه بخشی از همان API هستند که `onpointerdown` است. رابط کاربری اکنون خطاهایی ایجاد میکند زیرا هنوز توابع `elementDrag` و `stopElementDrag` را تعریف نکردهاید، بنابراین آنها را در مرحله بعد بسازید.
|
|
|
|
|
|
## توابع elementDrag و stopElementDrag
|
|
|
|
|
|
شما Closure خود را با افزودن دو تابع داخلی دیگر که مدیریت میکنند چه اتفاقی میافتد وقتی یک گیاه را میکشید و کشیدن آن را متوقف میکنید، کامل خواهید کرد. رفتاری که میخواهید این است که بتوانید هر گیاه را در هر زمان بکشید و آن را در هر جایی از صفحه قرار دهید. این رابط کاربری کاملاً بدون محدودیت است (به عنوان مثال، هیچ منطقه رهاسازی وجود ندارد) تا به شما اجازه دهد تراریوم خود را دقیقاً همانطور که دوست دارید طراحی کنید، با افزودن، حذف و جابجایی گیاهان.
|
|
|
|
|
|
### وظیفه
|
|
|
|
|
|
تابع `elementDrag` را درست بعد از بسته شدن آکولاد `pointerDrag` اضافه کنید:
|
|
|
|
|
|
```javascript
|
|
|
function elementDrag(e) {
|
|
|
pos1 = pos3 - e.clientX;
|
|
|
pos2 = pos4 - e.clientY;
|
|
|
pos3 = e.clientX;
|
|
|
pos4 = e.clientY;
|
|
|
console.log(pos1, pos2, pos3, pos4);
|
|
|
terrariumElement.style.top = terrariumElement.offsetTop - pos2 + 'px';
|
|
|
terrariumElement.style.left = terrariumElement.offsetLeft - pos1 + 'px';
|
|
|
}
|
|
|
```
|
|
|
|
|
|
در این تابع، شما بسیاری از موقعیتهای اولیه ۱-۴ را که به عنوان متغیرهای محلی در تابع خارجی تنظیم کردهاید، ویرایش میکنید. چه اتفاقی در اینجا میافتد؟
|
|
|
|
|
|
هنگام کشیدن، `pos1` را با برابر کردن آن با `pos3` (که قبلاً به عنوان `e.clientX` تنظیم شده بود) منهای مقدار فعلی `e.clientX` دوباره اختصاص میدهید. عملیات مشابهی را برای `pos2` انجام میدهید. سپس، `pos3` و `pos4` را به مختصات جدید X و Y عنصر بازنشانی میکنید. میتوانید این تغییرات را در کنسول هنگام کشیدن مشاهده کنید. سپس، سبک CSS گیاه را دستکاری میکنید تا موقعیت جدید آن را بر اساس موقعیتهای جدید `pos1` و `pos2` تنظیم کنید و مختصات X و Y بالای گیاه را بر اساس مقایسه آفست آن با این موقعیتهای جدید محاسبه کنید.
|
|
|
|
|
|
> `offsetTop` و `offsetLeft` ویژگیهای CSS هستند که موقعیت یک عنصر را بر اساس موقعیت والد آن تنظیم میکنند؛ والد آن میتواند هر عنصری باشد که به صورت `static` موقعیتدهی نشده باشد.
|
|
|
|
|
|
تمام این محاسبات مجدد موقعیت به شما امکان میدهد رفتار تراریوم و گیاهان آن را به دقت تنظیم کنید.
|
|
|
|
|
|
### وظیفه
|
|
|
|
|
|
آخرین وظیفه برای تکمیل رابط کاربری این است که تابع `stopElementDrag` را بعد از بسته شدن آکولاد `elementDrag` اضافه کنید:
|
|
|
|
|
|
```javascript
|
|
|
function stopElementDrag() {
|
|
|
document.onpointerup = null;
|
|
|
document.onpointermove = null;
|
|
|
}
|
|
|
```
|
|
|
|
|
|
این تابع کوچک رویدادهای `onpointerup` و `onpointermove` را بازنشانی میکند تا بتوانید پیشرفت گیاه خود را با شروع دوباره کشیدن آن از سر بگیرید یا شروع به کشیدن یک گیاه جدید کنید.
|
|
|
|
|
|
✅ چه اتفاقی میافتد اگر این رویدادها را به null تنظیم نکنید؟
|
|
|
|
|
|
اکنون پروژه خود را تکمیل کردهاید!
|
|
|
|
|
|
🥇تبریک میگوییم! شما تراریوم زیبای خود را به پایان رساندید. 
|
|
|
|
|
|
---
|
|
|
|
|
|
## 🚀چالش
|
|
|
|
|
|
یک رویداد جدید به Closure خود اضافه کنید تا کاری بیشتر با گیاهان انجام دهید؛ به عنوان مثال، با دوبار کلیک روی یک گیاه، آن را به جلو بیاورید. خلاق باشید!
|
|
|
|
|
|
## آزمون پس از درس
|
|
|
|
|
|
[آزمون پس از درس](https://ff-quizzes.netlify.app/web/quiz/20)
|
|
|
|
|
|
## مرور و مطالعه شخصی
|
|
|
|
|
|
در حالی که کشیدن عناصر در صفحه به نظر ساده میرسد، روشهای زیادی برای انجام این کار و مشکلات زیادی وجود دارد که بسته به اثری که میخواهید ایجاد کنید، ممکن است پیش بیاید. در واقع، یک [API کشیدن و رها کردن](https://developer.mozilla.org/docs/Web/API/HTML_Drag_and_Drop_API) کامل وجود دارد که میتوانید امتحان کنید. ما از آن در این ماژول استفاده نکردیم زیرا اثری که میخواستیم کمی متفاوت بود، اما این API را در پروژه خود امتحان کنید و ببینید چه چیزی میتوانید به دست آورید.
|
|
|
|
|
|
اطلاعات بیشتری درباره رویدادهای pointer در [مستندات W3C](https://www.w3.org/TR/pointerevents1/) و [مستندات وب MDN](https://developer.mozilla.org/docs/Web/API/Pointer_events) پیدا کنید.
|
|
|
|
|
|
همیشه قابلیتهای مرورگر را با استفاده از [CanIUse.com](https://caniuse.com/) بررسی کنید.
|
|
|
|
|
|
## تکلیف
|
|
|
|
|
|
[کمی بیشتر با DOM کار کنید](assignment.md)
|
|
|
|
|
|
**سلب مسئولیت**:
|
|
|
این سند با استفاده از سرویس ترجمه هوش مصنوعی [Co-op Translator](https://github.com/Azure/co-op-translator) ترجمه شده است. در حالی که ما تلاش میکنیم دقت را حفظ کنیم، لطفاً توجه داشته باشید که ترجمههای خودکار ممکن است حاوی خطاها یا نادرستیهایی باشند. سند اصلی به زبان اصلی آن باید به عنوان منبع معتبر در نظر گرفته شود. برای اطلاعات حساس، ترجمه حرفهای انسانی توصیه میشود. ما هیچ مسئولیتی در قبال سوءتفاهمها یا تفسیرهای نادرست ناشی از استفاده از این ترجمه نداریم. |