|
|
<!--
|
|
|
CO_OP_TRANSLATOR_METADATA:
|
|
|
{
|
|
|
"original_hash": "30f8903a1f290e3d438dc2c70fe60259",
|
|
|
"translation_date": "2025-08-25T21:12:41+00:00",
|
|
|
"source_file": "3-terrarium/3-intro-to-DOM-and-closures/README.md",
|
|
|
"language_code": "ru"
|
|
|
}
|
|
|
-->
|
|
|
# Проект "Террариум", часть 3: Манипуляции с DOM и замыкание
|
|
|
|
|
|

|
|
|
> Скетчноут от [Tomomi Imura](https://twitter.com/girlie_mac)
|
|
|
|
|
|
## Предварительный тест
|
|
|
|
|
|
[Предварительный тест](https://ff-quizzes.netlify.app/web/quiz/19)
|
|
|
|
|
|
### Введение
|
|
|
|
|
|
Манипуляция с DOM, или "Document Object Model" (объектная модель документа), является ключевым аспектом веб-разработки. Согласно [MDN](https://developer.mozilla.org/docs/Web/API/Document_Object_Model/Introduction), "Document Object Model (DOM) — это представление данных объектов, составляющих структуру и содержимое документа в интернете". Сложности, связанные с манипуляцией DOM, часто становились причиной использования JavaScript-фреймворков вместо чистого JavaScript для управления DOM, но мы справимся самостоятельно!
|
|
|
|
|
|
Кроме того, в этом уроке будет введено понятие [замыкания в JavaScript](https://developer.mozilla.org/docs/Web/JavaScript/Closures), которое можно представить как функцию, заключённую внутри другой функции, так что внутренняя функция имеет доступ к области видимости внешней функции.
|
|
|
|
|
|
> Замыкания в JavaScript — это обширная и сложная тема. В этом уроке мы затронем самую базовую идею: в коде террариума вы найдёте замыкание — внутреннюю и внешнюю функции, сконструированные таким образом, чтобы внутренняя функция имела доступ к области видимости внешней. Для более подробной информации о том, как это работает, посетите [обширную документацию](https://developer.mozilla.org/docs/Web/JavaScript/Closures).
|
|
|
|
|
|
Мы будем использовать замыкание для манипуляции с DOM.
|
|
|
|
|
|
Представьте DOM как дерево, которое отображает все способы, с помощью которых можно манипулировать документом веб-страницы. Были разработаны различные API (интерфейсы прикладного программирования), чтобы программисты могли с помощью выбранного языка программирования получить доступ к DOM и редактировать, изменять, перестраивать и управлять им.
|
|
|
|
|
|

|
|
|
|
|
|
> Представление DOM и HTML-разметки, которая на него ссылается. Автор: [Olfa Nasraoui](https://www.researchgate.net/publication/221417012_Profile-Based_Focused_Crawler_for_Social_Media-Sharing_Websites)
|
|
|
|
|
|
В этом уроке мы завершим наш интерактивный проект террариума, создав JavaScript-код, который позволит пользователю манипулировать растениями на странице.
|
|
|
|
|
|
### Предварительные требования
|
|
|
|
|
|
У вас должны быть готовы HTML и CSS для вашего террариума. К концу этого урока вы сможете перемещать растения в террариум и из него, перетаскивая их.
|
|
|
|
|
|
### Задание
|
|
|
|
|
|
В папке террариума создайте новый файл с именем `script.js`. Подключите этот файл в секции `<head>`:
|
|
|
|
|
|
```html
|
|
|
<script src="./script.js" defer></script>
|
|
|
```
|
|
|
|
|
|
> Примечание: используйте `defer` при подключении внешнего JavaScript-файла в HTML, чтобы JavaScript выполнялся только после полной загрузки HTML-файла. Вы также можете использовать атрибут `async`, который позволяет скрипту выполняться во время парсинга HTML-файла, но в нашем случае важно, чтобы HTML-элементы были полностью доступны для перетаскивания до выполнения скрипта.
|
|
|
|
|
|
---
|
|
|
|
|
|
## Элементы DOM
|
|
|
|
|
|
Первое, что вам нужно сделать, — это создать ссылки на элементы, которые вы хотите манипулировать в DOM. В нашем случае это 14 растений, которые сейчас находятся в боковых панелях.
|
|
|
|
|
|
### Задание
|
|
|
|
|
|
```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, чтобы ответить на этот вопрос.
|
|
|
|
|
|
---
|
|
|
|
|
|
## Замыкание
|
|
|
|
|
|
Теперь вы готовы создать замыкание `dragElement`, которое представляет собой внешнюю функцию, заключающую одну или несколько внутренних функций (в нашем случае их будет три).
|
|
|
|
|
|
Замыкания полезны, когда одной или нескольким функциям нужно получить доступ к области видимости внешней функции. Вот пример:
|
|
|
|
|
|
```javascript
|
|
|
function displayCandy(){
|
|
|
let candy = ['jellybeans'];
|
|
|
function addCandy(candyType) {
|
|
|
candy.push(candyType)
|
|
|
}
|
|
|
addCandy('gumdrops');
|
|
|
}
|
|
|
displayCandy();
|
|
|
console.log(candy)
|
|
|
```
|
|
|
|
|
|
В этом примере функция `displayCandy` окружает функцию, которая добавляет новый тип конфет в массив, уже существующий в функции. Если вы выполните этот код, массив `candy` будет неопределённым, так как он является локальной переменной (локальной для замыкания).
|
|
|
|
|
|
✅ Как сделать массив `candy` доступным? Попробуйте переместить его за пределы замыкания. Таким образом, массив станет глобальным, а не останется доступным только в локальной области видимости замыкания.
|
|
|
|
|
|
### Задание
|
|
|
|
|
|
Под объявлениями элементов в `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` для объекта, переданного в функцию. Это локальные переменные, которые будут изменяться для каждого элемента, когда вы добавите функциональность перетаскивания в замыкание для каждого элемента. Террариум будет заполняться этими перетаскиваемыми элементами, поэтому приложению нужно отслеживать, где они размещены.
|
|
|
|
|
|
Кроме того, элемент `terrariumElement`, переданный в эту функцию, получает событие `pointerdown`, которое является частью [веб-API](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 растения в момент, когда вы на него нажимаете или касаетесь. Вам нужен детальный контроль над поведением растений при их перетаскивании, поэтому вы отслеживаете их координаты.
|
|
|
|
|
|
✅ Становится ли более понятно, почему всё приложение построено с использованием одного большого замыкания? Если бы его не было, как бы вы поддерживали область видимости для каждого из 14 перетаскиваемых растений?
|
|
|
|
|
|
Завершите начальную функцию, добавив ещё две манипуляции с событиями указателя под `pos4 = e.clientY`:
|
|
|
|
|
|
```html
|
|
|
document.onpointermove = elementDrag;
|
|
|
document.onpointerup = stopElementDrag;
|
|
|
```
|
|
|
Теперь вы указываете, что хотите, чтобы растение перемещалось вместе с указателем при его перемещении, и чтобы жест перетаскивания завершался, когда вы убираете выделение с растения. `onpointermove` и `onpointerup` являются частью того же API, что и `onpointerdown`. Интерфейс будет выдавать ошибки сейчас, так как вы ещё не определили функции `elementDrag` и `stopElementDrag`, поэтому создайте их далее.
|
|
|
|
|
|
## Функции elementDrag и stopElementDrag
|
|
|
|
|
|
Вы завершите своё замыкание, добавив ещё две внутренние функции, которые будут обрабатывать то, что происходит, когда вы перетаскиваете растение и прекращаете его перетаскивание. Желаемое поведение заключается в том, чтобы вы могли перетаскивать любое растение в любое время и размещать его в любом месте на экране. Этот интерфейс довольно гибкий (например, нет зоны сброса), чтобы вы могли настроить свой террариум так, как вам нравится, добавляя, удаляя и перемещая растения.
|
|
|
|
|
|
### Задание
|
|
|
|
|
|
Добавьте функцию `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';
|
|
|
}
|
|
|
```
|
|
|
В этой функции вы много работаете с изменением начальных позиций 1-4, которые вы установили как локальные переменные во внешней функции. Что здесь происходит?
|
|
|
|
|
|
Во время перетаскивания вы переназначаете `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?
|
|
|
|
|
|
Теперь ваш проект завершён!
|
|
|
|
|
|
🥇Поздравляем! Вы завершили свой красивый террариум. 
|
|
|
|
|
|
---
|
|
|
|
|
|
## 🚀Челлендж
|
|
|
|
|
|
Добавьте новый обработчик событий в своё замыкание, чтобы сделать что-то ещё с растениями; например, двойной клик по растению, чтобы переместить его на передний план. Проявите креативность!
|
|
|
|
|
|
## Тест после лекции
|
|
|
|
|
|
[Тест после лекции](https://ff-quizzes.netlify.app/web/quiz/20)
|
|
|
|
|
|
## Обзор и самостоятельное изучение
|
|
|
|
|
|
Хотя перетаскивание элементов по экрану кажется тривиальной задачей, существует множество способов сделать это и множество подводных камней, в зависимости от желаемого эффекта. На самом деле существует целый [API для перетаскивания и сброса](https://developer.mozilla.org/docs/Web/API/HTML_Drag_and_Drop_API), который вы можете попробовать. Мы не использовали его в этом модуле, так как эффект, который мы хотели, был несколько другим, но попробуйте этот API в своём собственном проекте и посмотрите, чего вы сможете достичь.
|
|
|
|
|
|
Найдите больше информации о событиях указателя в [документации W3C](https://www.w3.org/TR/pointerevents1/) и на [MDN web docs](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). Несмотря на наши усилия обеспечить точность, автоматические переводы могут содержать ошибки или неточности. Оригинальный документ на его родном языке следует считать авторитетным источником. Для получения критически важной информации рекомендуется профессиональный перевод человеком. Мы не несем ответственности за любые недоразумения или неправильные интерпретации, возникшие в результате использования данного перевода. |