Рубрикатор

Здесь можно найти крутой материал на любую тему из digital : через поиск, рубрику и даже #тег.

{{ errors.first('search') }}

Выберите категорию

Все Все Разработка Агентство Разработка Дизайн Разработка Новости Разработка Образование Разработка Продвижение Разработка Разработка

Облако #тегов

#digital news #SMM #TAGREE #TikTok #бизнес #вдохновение #веб-дизайн #дизайн #кейс #маркетинг #мир #программирование #продвижение #разработка #реклама #сайты #социальные сети #таргетинг #типографика #фронтенд
Школа 11.01.2022
Курс: Junior frontend-разработчик

Введение в JavaScript. Часть 3

Изображение

1. JavaScript в браузере

1.1 Встроенный и внешний код

Как выполнить JavaScript-код на нашей веб-странице? Самый простой способ — написать код непосредственно в HTML-разметке внутри парного тега <script>. Данный тег может находиться в любом месте <body> или в <head>. Код будет выполнен при загрузке веб-страницы.

Следующий пример кода выведет стандартное модальное окно с заданным сообщением:

<!DOCTYPE html>
<html lang="ru">
  <head>
    <meta charset="UTF-8" />
    <title>Page title</title>
  </head>
  <body>

    <script>
      // Это комментарий.
      // Следующая строка кода выведет модальное окно с сообщением.
      alert("Hello!");
    </script>

  </body>
</html>

Второй способ — подключить внешний js-файл с помощью атрибута src в том же самом теге <script>.

<!DOCTYPE html>
<html lang="ru">
  <head>
    <meta charset="UTF-8" />
    <title>Page title</title>
  </head>
  <body>

    <script src="scripts/main.js"></script>

  </body>
</html>

Содержимое файла main.js в таком же примере будет выглядеть точно так же:

// Это комментарий.
// Следующая строка кода выведет модальное окно с сообщением.
alert("Hello!");

При этом стоит помнить, что код, расположенный внутри теге <script> c атрибутом src никогда не будет выполнен.

<!DOCTYPE html>
<html lang="ru">
  <head>
    <meta charset="UTF-8" />
    <title>Page title</title>
  </head>
  <body>

    <script src="scripts/main.js">
      // Последующий код выполнен не будет.
      alert("Hello!");
    </script>

  </body>
</html>

1.2 Порядок подключения

На одной странице мы можем подключить любое количество скриптов. По умолчанию выполняться они будут поочерёдно в порядке своего расположения. Если сначала расположен большой скрипт, а затем меньшего размера, то последний будет ждать.

<!DOCTYPE html>
<html lang="ru">
  <head>
    <meta charset="UTF-8" />
    <title>Page title</title>
  </head>
  <body>

    <script src="scripts/big_script.js"></script>
    <script src="scripts/small_script.js"></script>

  </body>
</html>

Браузеры ищут и загружают скрипты параллельно в целях увеличения производительности. В данном примере маленький скрипт скорее всего загрузится первым, но выполнен будет вторым.

Скрипты обычно весят больше, чем HTML, и дольше обрабатываются. И до тех пор, пока скрипт не будет загружен и выполнен, дальнейшая обработка кода страницы приостанавливается. Это ведёт к следующим проблемам:

  1. скрипты не видят элементы на странице, расположенные ниже, а значит наш код не сможет с ними взаимодействовать;
  2. если вверху страницы находится объёмный скрипт, он блокирует страницу, и пользователь не видит её содержимое, пока данный скрипт не загрузится и не выполнится.
<!DOCTYPE html>
<html lang="ru">
  <head>
    <meta charset="UTF-8" />
    <title>Page title</title>
  </head>
  <body>

    <script src="scripts/main.js"></script>

    <p>Этот текст не будет отображён, пока скрипт выше не обработается</p>

  </body>
</html>

Самый простой способ решения проблемы — всегда подключать все ваши скрипты в самом конце <body> перед закрывающим тегом.

<!DOCTYPE html>
<html lang="ru">
  <head>
    <meta charset="UTF-8" />
    <title>Page title</title>
  </head>
  <body>
    <h1>Заголовок страницы</h1>
    <p>Прочее содержимое</p>

    <script src="scripts/main.js"></script>
  </body>
</html>

В некоторых случаях это решение может быть не очень хорошим. Если HTML-документ очень большой, это может создать заметную задержку при медленном интернет-соединении, так как браузер сначала вынужден будет загрузить всю HTML-разметку и лишь потом начать загружать скрипт.

1.3 Атрибуты defer и async

Атрибут defer сообщает браузеру, что он должен продолжать обрабатывать страницу и загружать скрипт в фоновом режиме, а затем запустить этот скрипт, когда DOM-дерево (объектная модель тегов документа) будет полностью построено. При этом произойдёт это до события DOMContentLoaded, наступающего когда браузер полностью загрузил HTML, было построено DOM-дерево, но внешние ресурсы, такие как изображения и стили, могли быть ещё не загружены. О DOM и событиях поговорим чуть позже.

<!DOCTYPE html>
<html lang="ru">
  <head>
    <meta charset="UTF-8" />
    <title>Page title</title>

    <script defer src="scripts/big_script.js"></script>
    <script defer src="scripts/small_script.js"></script>
  </head>

  <body>
    <p>Содержимое страницы</p>
  </body>
</html>

Отложенные с помощью атрибута defer скрипты также сохраняют порядок относительно друг друга.

Логический атрибут async указывает браузеру, что загружать скрипт, указанный в атрибуте src, нужно асинхронно:

  • содержимое страницы обрабатывается и отображается независимо от загрузки и выполнения скрипта;
  • такой скрипт не ждёт другие скрипты, а другие скрипты не ждут данный скрипт;
  • событие DOMContentLoaded не зависит от загрузки и выполнения такого скрипта.

То есть, если у нас подключено несколько асинхронных скриптов, то выполниться они могут в любом порядке, никто никого ждать не будет.

<!DOCTYPE html>
<html lang="ru">
  <head>
    <meta charset="UTF-8" />
    <title>Page title</title>

    <script async src="scripts/big_script.js"></script>
    <script async src="scripts/small_script.js"></script>
  </head>

  <body>
    <p>Содержимое страницы</p>
  </body>
</html>

Таким образом, и async, и defer не блокируют отрисовку страницы. При этом оба атрибута могут быть применены только для внешних скриптов, подключенных с помощью атрибута src.

На практике defer следует применять для для скриптов, которым требуется доступ ко всему DOM-дереву, а также когда важен порядок выполнения.

Атрибут async хорошо подходит для независимых скриптов, порядок выполнения которых не играет роли, например для счётчиков, рекламной аналитики и прочего.

Не указывайте оба атрибута вместе. Это могло иметь смысл для для старых браузеров, поддерживающих отложенное выполнение скрипов, но не поддерживающих асинхронное. Во всех современных браузерах атрибут async имеет приоритет, а defer будет проигнорирован, если они указаны вместе.

1.4 Браузерное окружение

Как мы уже знаем, код JavaScript может использоваться не только в браузере, но и на веб-сервере, или в другой подходящей среде. Такая среда в дополнение к стандартным возможностям предоставляет свои собственные свойства и методы и называется окружением.

Глобальным объектом, то есть объектом, предоставляющим свои свойства и методы в любом месте исполняемого кода, в браузере является window (окно). Ко всем свойствам глобального объекта можно обращаться напрямую. Таким образом следующие две строчки в примере являются эквивалентными:

//эти вызовы равнозначны:
alert("Hello!");
window.alert("Hello!");

Объект window предоставляет нам огромное количество свойств и методов. Так, window.innerHeight позволит нам получить внутреннюю высоту окна браузера, а window.location — текущий URL и методы работы с ним.

Но прежде всего нас интересует Document Object Model (DOM).

2. Document Object Model

2.1 Дерево документа

Document Object Model – это объектная модель документа, дерево, представляющее всё содержимое страницы в виде объектов, к которым мы можем получить доступ и взаимодействовать. Доступ к DOM осуществляется с помощью объекта window.document или же просто document.

HTML-теги образуют узлы-элементы, внутри которых могут располагаться дочерние узлы и дальнейшие потомки, а текст внутри элементов — текстовые узлы, которые не могут содержать потомков.

Отметим, что дочерними узлами называются элементы, лежащие непосредственно внутри данного узла на первом уровне вложенности, а потомками — вообще все элементы, которые лежат внутри на любом уровне вложенности.

Директива <!DOCTYPE>, HTML-комментарии также являются узлами. Кроме того полноценными текстовыми узлами являются переводы строк и пробелы.

2.2 Доступ к узлам документа

Корневым элементом веб-страницы является элемент <html>, доступ к нему мы можем получить через document.documentElement.

Кроме того простой доступ можно получить к следующим элементам:

  • элемент <head> доступен как document.head;
  • элемент <body> доступен как document.body;
Коллекция childNodes

Ко всем дочерним элементам любого узла можно получить доступ с помощью DOM-коллекции childNodes.

Коллекция похожа на массив, но на самом деле им не является. Это особый перебираемый псевдомассив и он доступен только для чтения. Изменить его элементы подобно массивам не получится.

Мы можем перебрать коллекцию с помощью цикла for of:

for (let node of document.childNodes) {
  console.log(node);
}

Однако методы массивов работать не будут:

alert(document.childNodes.filter); //undefined
alert(document.childNodes.find); //undefined

Тем не менее все популярные современные браузеры для перебора коллекций поддерживают метод forEach:

document.childNodes.forEach(function(node) {
  console.log(node);
});

Если нам нужны методы массива для работы с коллекцией, то мы можем легко создать из коллекции в массив:

Array.from(document.childNodes).filter(function (node) {
  return true;
});
//получим массив [<!DOCTYPE html>, html]

Или воспользоваться встроенным методом call, первый аргумент которого становится this для его колбэк-функции:

Array.prototype.filter.call(document.childNodes, function (node) {
  return true;
});
//получим массив [<!DOCTYPE html>, html]
Доступ к элементам коллекции

К элементам коллекции, как и к элементам массива можно получить доступ по индексу:

console.log(document.childNodes[0]); //<!DOCTYPE html>
Свойства для доступа к узлам

Можно воспользоваться одним из следующих свойств:

  • firstChild — обеспечивает доступ к первому дочернему элементу узла;
  • lastChild — обеспечивает доступ к последнему дочернему элементу узла;
  • nextSibling — обеспечивает доступ к следующему узлу того же родителя;
  • previousSibling — обеспечивает доступ к предыдущему узлу того же родителя;
  • parentNode — обеспечивает доступ к родительскому узлу.
alert(document.childNodes[0] === document.firstChild); //true
alert(document.documentElement.parentNode === document); //true

2.3 Доступ к элементам документа

Не все узлы документа являются элементами. Чаще всего нам нет нужды работать с текстовыми узлами или, скажем, комментариями. Работать непосредственно с узлами-элементами можно следующим образом:

  • children — коллекция дочерних узлов-элементов;
  • firstElementChild — первый дочерний элемент;
  • lastElementChild -последний дочерний элемент;
  • nextElementSibling  — следующий элемент;
  • previousElementSibling — предыдущий элемент;
  • parentElement – родительский элемент.

Рассмотрим отличие на примере родительского узла корневого элемента:

console.log(document.documentElement.parentNode); //document
console.log(document.documentElement.parentElement); //null

Объект document является родительским узлом для <html>, но не является элементом.

3. Поиск элементов

Свойства доступа к узлам-элементам DOM, зачастую очень неудобны в работе, так как не позволяют просто получить произвольный элемент документа. Для решения задачи существуют несколько методов, рассмотрим их.

3.1 Универсальные методы querySelector() и querySelectorAll()

Метод querySelector()

Метод querySelector() возвращает только первый элемент документа, соответствующий указанному CSS-селектору или группе селекторов. Если совпадений не найдено, возвращает значение null.

<div class="my-class">Первый элемент</div>
<div class="my-class">Второй элемент</div>
let element = document.querySelector(".my-class");
//получим <div class="my-class">Первый элемент</div>

Метод принимает строку, содержащую любые допустимые CSS-селекторы, однако стоит помнить, что символы, не являющиеся частью стандартного синтаксиса CSS, должны быть экранированы, иначе возникнет ошибка:

<div class="my:class"></div>
let element = document.querySelector(".my:class");
/*
  DOMException: Failed to execute 'querySelector' on 'Document':
  '.my:class' is not a valid selector.
*/

Экранирование символов осуществляется с помощью обратной косой черты (\). Но обратная косая черта также является экранирующим символом в JavaScript, а значит экранировать нужно дважды:

let element = document.querySelector(".my\\:class");
//получим <div class="my:class"></div>

Избегайте подобных селекторов.

Выполнить поиск можно не только по документу, но и найти первый удовлетворяющий селектору потомок данного элемента:

<ul>
  <li></li>
  <li></li>
</ul>
let ul = document.querySelector("ul");
let list = ul.querySelector("li");
//получим первый элемент списка
Метод querySelectorAll()

Метод querySelectorAll() возвращает коллекцию всех узлов-элементов, соответствующих указанному селектору. Коллекция является статической, и любые изменения в DOM не отражаются на её содержании.

<div class="my-class">Первый элемент</div>
<div class="my-class">Второй элемент</div>
let element = document.querySelectorAll(".my-class");
//получим NodeList(2) [div.my-class, div.my-class]

Мы можем получить список совпадений по нескольким селекторам. В коллекцию попадут все элементы, удовлетворяющие хотя бы одному из селекторов:

<div class="class_1">Первый элемент</div>
<div class="class_2">Второй элемент</div>
let elements = document.querySelectorAll(".class_1, .class_2");
//получим NodeList(2) [div.class_1, div.class_2]

Метод querySelectorAll() может вернуть коллекцию потомков указанного элемента, удовлетворяющих указанному селектору:

<ul>
  <li></li>
  <li></li>
</ul>
let ul = document.querySelector("ul");
let list = ul.querySelectorAll("li");
//получим NodeList(2) [li, li]

3.2 Метод getElementById()

Метод getElementById() возвращает ссылку на элемент с указанным идентификатором id, или null, если такой элемент не найден. Параметр id чувствителен к регистру.

<div id="my-id"></div>
let element = document.getElementById("my-id");
//получим <div id="my-id"></div>

Метод getElementById() в отличие от querySelector() не имеет ограничений на допустимое значение CCS-селектора:

<div id="1"></div>
let elementById = document.getElementById("1");
//получим <div id="1"></div>

let elementBySelector = document.querySelector("#1");
/* 
  DOMException: Failed to execute 'querySelector' on 'Document':
  '#1' is not a valid selector.
*/

3.3 Динамические коллекции getElementsBy*

Перечисленные далее методы возвращают динамическую коллекцию, что означает, что любые изменения в DOM отражаются на самой коллекции.

  • document.getElementsByTagName(tagName) — возвращает коллекцию элементов документа с указанным тегом;
  • element.getElementsByTagName(tagName) — выполнит поиск по тегу в поддереве указанного элемента;
  • document.getElementsByClassName(ClassName) — возвращает коллекцию элементов документа с указанным классом;
  • element.getElementsByClassName(ClassName) — выполнит поиск по классу в поддереве указанного элемента;
  • document.getElementsByName(name) — возвращает коллекцию элементов документа с заданным атрибутом name.

3.4 Отличие динамической и статической коллекции

Как было уже сказано, любые изменения в DOM отражаются на динамической коллекции, а на статической — нет. Для того, чтобы наглядно увидеть разницу между динамической и статической коллекциями, воспользуемся двумя методами:

  1. document.createElement(tagName) — создаёт элемент c тем тегом, указанным в аргументе;
  2. element.appendChild(child) — добавляет узел child в конец списка дочерних элементов указанного родительского узла-элемента.

Создадим простую разметку, состоящую из родного элемента списка:

<ul>
  <li></li>
</ul>

Проверим длину коллекции, затем добавим ещё один элемент списка и снова проверим длину той же коллекции:

let ul = document.querySelector("ul");

let list = ul.getElementsByTagName("li");
alert(list.length); //1

ul.appendChild(document.createElement("li"));
alert(list.length); //2

В случае с динамической коллекцией количество её элементов автоматически изменилось. Однако со статической коллекцией такого не произойдёт:

let ul = document.querySelector("ul");

let list = ul.querySelectorAll("li");
alert(list.length); //1

ul.appendChild(document.createElement("li"));
alert(list.length); //1

Примечание: В большинстве случаев стоит использовать статические коллекции, чтобы избежать возможных ошибок. К примеру следующий код будет выполняться бесконечно:

let ul = document.querySelector("ul");
let list = ul.getElementsByTagName("li");

for(let i = 0; i < list.length ; i++){
  ul.appendChild(document.createElement("li")); 
  alert(list.length); //2, 3, 4,...
}

На каждой итерации длина коллекции динамически изменяется, что приводит к бесконечной петле.

3.5 Метод closest()

Метод element.closest() возвращает сам элемент, или его ближайшего предка, соответствующего указанному CSS-селектору, или же null, если такой элемент не найден.

<ul class="element">
  <li class="element"></li>
</ul>
let li = document.querySelector("li");

let ul = li.closest("ul"); //<ul class="element">...</ul>
let element = li.closest(".element"); //<li class="element">...<li>

4. Работа с классами

Одна из самых частых задач при работе с элементами документа — изменение их классов.

Свойство элемента classList возвращает псевдомассив, содержащий все классы данного элемента и методы для работы с ними.

<div id="element" class="class_1 class_2"></div>
let element = document.querySelector("#element");
console.log(element.classList); //['class_1', 'class_2']

Методы:

  • Element.classList.add() — добавляет элементу указанные классы, перечисленные через запятую;
  • Element.classList.remove() — удаляет у элемента указанные классы, перечисленные через запятую;
  • Element.classList.toggle() — переключает указанный класс: если он есть — удаляет, иначе добавляет. Вторым значением может принимать Boolean-параметр: если true, добавляет класс, false — удаляет.
  • Element.classList.contains() — проверяет, есть ли указанный класс у элемента и возвращает true или false.

Пример с той же разметкой:

element.classList.add("class_3", "class_4"); 
//получим ['class_1', 'class_2', class_3', 'class_4']

element.classList.remove("class_1", "class_2");
//получим ['class_3', 'class_4']

element.classList.toggle("class_4");
//получим ['class_3']

element.classList.toggle("class_5");
//получим ['class_3', 'class_5']

alert(element.classList.contains("class_5")); //true

Кроме того с помощью свойства className мы можем получить строку, состоящую из классов элемента, разделённых пробелом:

alert(element.className); //class_3 class_5

5. Работа с атрибутами

5.1 Стандартные и нестандартные атрибуты

Атрибут class является одним из стандартных и доступ к нему, как мы только что узнали, осуществляется с помощью свойства className. Расхождение в названиях сложилось из-за того, что во многих языках, работающих с DOM, слово class является зарезервированным.

Особенности HTML-атрибутов:

  • значения атрибутов всегда являются строками (иногда пустыми);
  • имена не зависят от регистра (id равнозначно ID)

Большинство стандартных HTML-атрибутов узла-элемента автоматически становятся свойствами соответствующего DOM-объекта.

<div id="my-element" title="Мой элемент"></div>
let element = document.querySelector("#my-element");
alert(element.id); //my-element
alert(element.title); //Мой элемент

Стоит помнить, что стандартный атрибут одного элемента может быть нестандартным для другого, в таком случае мы получим undefined:

<button type="submit"></button>
<article type="someType"></article>
let button = document.querySelector("button");
alert(button.type); //submit

let article = document.querySelector("article");
alert(article.type); //undefined

Поскольку узлы-элементы являются объектами, значение стандартных атрибутов может быть легко изменено:

<div id="id_1"></div>
let element = document.querySelector("#id_1");

alert(element.id); //id_1
element.id = "id_2"
alert(element.id); //id_2

5.2 Методы для атрибутов

Мы можем работать с любыми, в том числе и с нестандартными атрибутами, с помощью следующих методов:

  • Element.getAttribute() — получает значение указанного атрибута;
  • Element.setAttribute() — принимает два параметра — имя атрибута, и значение которое данному атрибуту необходимо установить;
  • Element.removeAttribute() — удаляет указанный атрибут;
  • Element.hasAttribute() — проверяет наличие указанного атрибута и возвращает true или false.

Пример:

<article my-title="Статья"></article>
let article = document.querySelector("article");
alert(article.getAttribute("my-title")); //Статья

article.setAttribute("my-title", "Новостная статья");
alert(article.getAttribute("my-title")); //Новостная статья

alert(article.hasAttribute("my-title")); //true

5.3 Свойства логического типа

Свойства DOM-элементов не всегда являются строками. Например, существуют логические типы, например checked для чекбокса может иметь значение true или false:

<input type="checkbox" checked>
let input = document.querySelector("input");

alert(input.checked); //true
alert(input.getAttribute('checked')); //получим пустую строку

5.4 Свойства-объекты на примере style

Некоторые свойства могут представлять из себя объект.

Таковым является свойство style, в то время как значение аналогичного атрибута представляет собой строку.

<h1 style="color:red; font-size: 32px;">Заголовок</h1>
let title = document.querySelector("h1");

alert(title.getAttribute('style')); //color:red; font-size: 32px;

alert(typeof title.style); //object
alert(title.style.color); //red
alert(title.style.fontSize); //32px

Как видно из примера, для CSS-свойств из нескольких слов используется camelCase.

Мы можем изменять значения любых стилей, добавлять новые и удалять существующие:

title.style.fontSize = "24px" //изменили размер шрифта
title.style.opacity = 0.5; //сделали заголовок полупрозрачным
title.style.color = ""; //сбросили цвет

5.5 Дата-атрибуты

Очень часто при работе возникает необходимость в использовании нестандартных атрибутов для работы с пользовательскими данными. В этом случае стоит избегать полностью произвольных имён, так как могут возникнуть конфликты и неожиданные эффекты.

Для озвученной цели существуют специальные дата-атрибуты, начинающиеся с соответствующего префикса data-. Такие атрибуты доступны с помощью свойства dataset.

<article data-caption="Статья"></article>
let article = document.querySelector("article");

alert(article.dataset.caption); //Статья

6. События DOM

6.1 Виды событий

События — это случаи, о наступлении которых которых система сообщает вам для того, чтобы вы могли с ними взаимодействовать. Например, если пользователь нажимает кнопку скрытого меню на веб-странице, мы сможем на это отреагировать и отобразить соответствующий элемент на экране. 

Существует большое количество различных видов событий.

  • click — клик левой кнопки мыши и касание на сенсорных устройствах;
  • mousemove — движение мыши по элементу;
  • mouseover — наведение курсора на элемент, в том числе при переходе с его потомка;
  • mouseenter — наведение курсора на элемент, не происходит при наведении на потомка;
  • mouseout — уход курсора с элемента, в том числе при переходе на его потомка;
  • mouseleave — уход курсора с элемента, не происходит при наведении на потомка;
  • keydown — нажатие клавиши на клавиатуре;
  • keyup — отпуск клавиши на клавиатуре;
  • focus — фокусировка на элементе;
  • blur — потеря фокуса элементом;
  • input — изменение значения элемента, например при вводе текста в <input>;
  • change — окончательное изменение значения, для текстовых полей ввода произойдёт при потере ими фокуса;
  • submit — отправка данных веб-формы <form>;
  • resize — изменение размера окна браузера, применимо к объекту window;
  • DOMContentLoaded — завершение загрузки веб-страницы, применимо к document;
  • и другие.

6.2 Обработчики событий

Каждое событие имеет обработчик событий — блок кода, выполняемый при наступлении данного события.

Самый ранний из введённых способов зарегистрировать обработчик событий основан на соответствующих HTML-атрибутах, значением которых является JavaScript-код. Это встроенные обработчики событий.

Пример:

<button type="button" onclick="alert('Клик!')">Нажми</button>

Использовать встроенные обработчики событий на данный момент не рекомендуется и считается плохой практикой. Для тех же целей мы можем использовать свойства обработчиков событий:

<button type="button" id="my-button">Нажми</button>
const btn = document.querySelector('#my-button');
btn.onclick = function() {
  alert("Клик!");
};

Таким образом, для события click используется обработчик onclick, для focus — onfocus, для blur — onblur, и так далее.

6.3 Методы addEventListener() и removeEventListener()

Метод addEventListener() регистрирует указанный обработчик события, вызванного на объекте, способном генерировать события (Element, documetn, window и другие).

Пример:

<button type="button" id="my-button">Нажми</button>
const btn = document.querySelector('#my-button');
btn.addEventListener("click", function() {
  alert("Клик!");
});

Методы addEventListener() принимает два параметра — имя события функцию-обработчик.

Данный механизм имеет преимущества перед рассмотренными выше. К примеру, мы можем повесить несколько обработчиков на одно и то же событие при необходимости. Кроме того, обработчик может быть удалён с помощью метода removeEventListener(), который принимает имя события и функцию-обработчик, которая была передана в addEventListener():

const btn = document.querySelector('#my-button');

function showClick() {
  alert("Клик!");
  
  btn.removeEventListener("click", showClick);
  btn.disabled = true;
}

btn.addEventListener("click", showClick);

В нашем примере после первого клика обработчик будет удалён после первого же нажатия, а кнопка будет отключена.

6.4 Объект события

Любое событие представляет собой объект интерфейса Event и может иметь дополнительные свойства и методы. Некоторые из них рассмотрим далее. Объект события передаётся в виде параметра обработчику событий и мы можем с ним взаимодействовать.

<button type="button" id="my-button">Нажми</button>
const btn = document.querySelector('#my-button');

function showClick(event) {
  alert(typeof event); //object
  alert(event.type); //click
  alert(event.target.id); //my-button
}

btn.addEventListener("click", showClick);

В данном случае мы видим, что тип объекта события действительно Object, тип самого события — «click«, а элемент, который вызвал событие, доступен нам через event.target.

6.5 Предотвращение поведения по умолчанию

Часто возникают ситуации, когда нам нужно прервать действие события по умолчанию. К примеру, мы хотим предотвратить стандартную отправку веб-формы, чтобы далее отправить данные с помощью Ajax-запроса (Asynchronous JavaScript and XML). Для этого существует метод Event.preventDefault().

Пример:

<form id="my-form">
  <input type="text" name="name">
  <button type="submit" id="my-button">Отправить</button>
</form>
const form = document.querySelector('#my-form');

form.addEventListener("submit", function(event) {
  event.preventDefault();
  alert("Отправка формы предотвращена");
});

6.5 Всплытие событий

Когда событие инициируется элементом, который имеет родительские элементы, происходит следующее:

  • проверяется, имеет ли элемент, который вызвал какое-либо событие, обработчик этого события, и запускает его, если это так;
  • далее происходит переход к следующему родительскому элементу и выполняется то же самое, затем к следующему и так далее до корневого элемента <html> и потом к объекту document.

Таким образом, мы можем зарегистрировать обработчик на предке, но отловить событие на потомке при необходимости:

<button type="button" id="my-button">Нажми</button>
const btn = document.querySelector('#my-button');

function showClick(event) {
  if (btn.contains(event.target)) {
    alert("Нажата кнопка");
  } else {
    alert("Клик вне кнопки");
  }
}

document.addEventListener("click", showClick);

В данном примере с помощью метода Node.contains() мы проверяем, что наша кнопка является элементом, инициализирующий событие, или содержит таковой, и выводим соответствующее сообщение.

Давайте рассмотрим ещё один пример:

<button type="button" id="my-button">Нажми</button>
const btn = document.querySelector('#my-button');

document.addEventListener("click", function() {
  alert("Клик по документу");
});

btn.addEventListener("click", function() {
  alert("Нажата кнопка");
});

В данном случае при единственном клике по кнопке отработают сразу два обработчика событий — самой кнопки и по документа, и нам будет выведено оба окна с сообщением. Такое поведение мы можем предотвратить с помощью метода Event.stopPropagation(), предотвращающего всплытие события:

const btn = document.querySelector('#my-button');

document.addEventListener("click", function() {
  alert("Клик по документу");
});

btn.addEventListener("click", function(event) {
  event.stopPropagation();
  alert("Нажата кнопка");
});

Теперь при клике на кнопку событие всплывать не будет и будет выведено только сообщение о нажатии на кнопку.


7. JavaScript для учебного проекта

7.1 Структура

Мы изучили самые основы JavaSctipt. Это лишь вершина айсберга, но для начинающего разработчика предоставленная информация позволить закрыть некоторые простые задачи. Давайте приступим.

Для начала, в каталоге src/scripts/dev учебного проекта создайте файл main.js и подключите его либо в конце <head> с атрибутом defer, либо перед закрывающим тегом </body>.
Весь код мы будем писать внутри анонимной функции, чтобы к нему невозможно было получить доступ снаружи. Не забудем и про строгий режим.

//Содержимое файла main.js
(function () {
  "use strict";

  //ваш код
})();

Код мы будем писать не самый современный и оптимальный, но, надеемся, наиболее понятный для начинающего разработчика.

Давайте договоримся, для поиска элементов мы не будем использовать классы, отвечающие за стили. Старайтесь никогда не смешивать разные сущности.

Если нам нужен элемент для работы с JavaScript, мы задаём ему новый класс или идентификатор id с префиксом js-.

7.2 Главное навигационное меню

Первое с чего начнём — сделаем так, чтобы наше главное меню на планшетах и мобильных устройствах открывалось и закрывалось. Доступность навигации по ресурсу — одна из первостепенных задач.

Сперва дополним разметку нашей кнопки-гамбургера:

<button type="button" class="page-header__nav-toggle" id="js-navToggle"></button>

Напомним, навигационное меню у нас для мобильных устройств скрыто с помощью CSS-свойства transform. Кроме того при открытии меню необходимо отключить прокрутку на странице, а также превратить наш гамбургер в крестик. Проще всего сделать это с помощью одного единственного микс-класса, который мы будем добавлять корневому элементу документа:

.show-nav {
  body {
    @media @bw1020 {
      overflow: hidden;
    }
  }

  .page-header__nav {
    @media @bw1020 {
      transform: translateX(0);
    }
  }

  .page-header__nav-toggle {
    &::before {
      transform: translate(0px, 6px) rotate(45deg);
      box-shadow: none;
    }
    &::after {
      transform: translate(0px, -6px) rotate(-45deg);
    }
  }
}

Обратите внимание: CSS-свойство overflow со значением hidden для <body> в данном случае мы применяем вместе с медиавыражением, это важно! К примеру, на десктопе пользователь в любой момент может открепить окно от экрана, в результате чего у него появится возможность открыть мобильное меню. Однако если после этого развернуть браузер на полный экран, кнопка-гамбургер пропадёт, а на странице будет отсутствовать прокрутка, что вызовет проблему. Другой очевидный сценарий — просто повернуть на бок планшет при открытом главном меню.

Добавим плавности для полосок кнопки-гамбургера:

.page-header {
  /*...*/
  &__nav-toggle {
    /*...*/
    &::before,
    &::after {
      /*...*/
      transform-origin: 50% 50%;
      transition: transform 0.4s, box-shadow 0.4s;
    }
  }
}

Ну и наконец, код JavaScript. Он очень простой в данном случае:

  const root = document.documentElement;

  const navToggle = document.querySelector("#js-navToggle");
  navToggle.addEventListener("click", function () {
    root.classList.toggle("show-nav");
  });

7.3 Попап с формой заявки

На главной странице у нас есть раздел «Я хочу» с кнопкой «Провести мероприятие», по нажатию которой мы должны открыть попап с формой. Давайте реализуем.

Скорее всего, на одной странице у нас не будет никогда более одной такой кнопки, поэтому используем идентификатор, иначе вместо него можно добавить точно такой же класс.

<button type="button" class="offers__btn btn btn--red" id="js-eventOpenBtn">
  Провести мероприятие
</button>

На уроке, посвящённом формам, мы уже создали вёрстку попапа, прописали ему идентификатор и даже добавили микс-класс. Всё, что надо сделать — добавить префикс js-, о котором мы договаривались.

Ещё одна деталь — кнопки закрытия у нас две, дадим им одинаковый js-класс:

<div class="pp" id="js-eventPP">
  <!-- Разметка попапа -->
  
  <button class="pp__x-btn x-btn js-ppCloseBtn">
    <svg class="x-btn__icon" width="12" height="12">
      <use xlink:href="assets/icons/symbols.svg#x"></use>
    </svg>
  </button>
  
  <!-- Разметка попапа -->
  
  <button type="button" class="form__close-btn js-ppCloseBtn">
    Закрыть
  </button>
  
  <!-- Разметка попапа -->
</div>
.show-event-popup {
  body {
    overflow: hidden;
  }

  #js-eventPP {
    display: flex;
  }
}

Примечание: данный попап есть лишь на отдельных страницах нашего сайта, а значит нам незачем выполнять весь код, связанный с ним, если попап отсутствует. Это и лишние ресурсы, и возможность получить ненужную ошибку. Мы обязательно должны проверить его наличие на странице.

const eventPP = document.querySelector("#js-eventPP");

if (eventPP) {
  const eventOpenBtn = document.querySelector("#js-eventOpenBtn");


  eventOpenBtn.addEventListener("click", function () {
    root.classList.add("show-event-popup");
  });
 
  eventPP.addEventListener("click", function (event) {
    if (
      event.target === this ||
      event.target.classList.contains("js-ppCloseBtn")
    ) {
      root.classList.remove("show-event-popup");
    }
  });
}

Обратите внимание, для закрытия попапа мы воспользовались всплыванием события. Таким образом попап мы будем закрывать как по нажатию на соответствующую кнопку, так и по клику на полупрозрачный фон.

Так как при клике на закрывающую кнопку мы просто проверяем наличие соответствующего класса, нужно избежать взаимодействия с любыми вложенными элементами кнопок:

.x-btn {
  &__icon {
    pointer-events: none;
  }
}

Остался ещё один момент: попапы стоит закрывать по нажатию на клавишу Esc, это повысит удобство для пользователя. Код совсем не сложный, нужно проверить нажимаемую клавишу:

if (eventPP) {
  //предыдущий код

  document.addEventListener("keyup", function (event) {
    if (event.key === "Escape" || event.keyCode === 27) {
      root.classList.remove("show-event-popup");
    }
  });
}

Что ж, всё работает, наш попап открывается и закрывается. Однако есть небольшая проблема — наши слушатели всегда активны, даже те, что нужны для закрытия попапа, когда наш попап и так закрыт. В нашем случае это не страшно, наш код совсем простой и даже в самом финале его будет не так много.

Но, исходя из полученных знаний, мы можем слегка переписать код, добавляя обработчики событий, когда они нужны, и убирая, когда не нужны.

const eventPP = document.querySelector("#js-eventPP");

  if (eventPP) {
    const eventOpenBtn = document.querySelector("#js-eventOpenBtn");

    const closeEventPP = function (event) {
      function close() {
        document.removeEventListener("keyup", closeEventPP);
        eventPP.removeEventListener("click", closeEventPP);

        root.classList.remove("show-event-popup");
      }

      switch (event.type) {
        case "keyup":
          if (event.key === "Escape" || event.keyCode === 27) close();
          break;
        case "click":
          if (
            event.target === this ||
            event.target.classList.contains("js-ppCloseBtn")
          )
            close();
          break;
      }
    };

    eventOpenBtn.addEventListener("click", function () {
      root.classList.add("show-event-popup");

      document.addEventListener("keyup", closeEventPP);
      eventPP.addEventListener("click", closeEventPP);
    });
  }

7.4 Слайдер мероприятий на главной странице

Подключение плагина

Часто для решения определённых задач стоит использовать код сторонних разработчиков — различные плагины, которые позволят вам многократно сэкономить время разработки. Как правило, популярные плагины имеют отличный функционал, грамотно написанный код и длительную поддержку командой разработчиков.

Не забывайте проверять, что позволяет лицензия того или иного плагина, и можно ли его, к примеру, использовать в коммерческой разработке.

Различных слайдеров очень много, но в качестве примера для слайдера на главной странице мы предлагаем воспользоваться Swiper. Поскольку мы не используем модули и полноценные сборщики в нашей работе, то плагин можно подключить одним из предложенных вариантов:

  1. Подключить стили и скрипт с CDN по следующей инструкции;
  2. Сохранить стили и скрипт у себя локально и добавить в проект.

Если выбираем второй вариант, то поступаем следующим образом.

Стили кладём в каталог src/styles/vendor. Мы можем подключить CSS-файл в styles.less:

@import (less) "vendor/swiper.css";

Либо просто переименовать swiper.css в swiper.less и подключить как обычно.

Скрипт кладём в каталог src/scripts/vendor и подключаем в нашем файле index.html:

<html>
  <body>
    <!-- Содержимое страницы -->

    <script src="scripts/swiper.min.js"></script>
    <script src="scripts/main.min.js"></script>
  </body>
</html>

Обратите внимание, сторонние скрипты мы подключаем до нашего основного файла main.js.

Иногда разработчики объединяют все скрипты в один (конкатенация), и это легко сделать с помощью того же Gulp, однако, на наш взгляд это плохая практика.

Сторонние плагины нам редко нужны вообще на всех страницах сайта, незачем заставлять пользователя загружать код, который ему не нужен. Кроме того, скрипты кэшируются, и если нам понадобится внести изменения в наш код, при раздельном подключении будет повышена версия только конкретного скрипта и обновится только он сам, что снова избавит пользователя от лишнего траффика.

Разметка

И так, мы всё подключили. Теперь нужно создать разметку. Мы можем создать новый HTML-блок в src/html/blocks, чтобы не засорять разметку самой страницы и вынести всё в отдельный файл.

Примерна разметка блока:

<section class="upcoming @@class">
  <div class="upcoming__wrapper">
    <div class="upcoming__conainer">
      <h2 class="upcoming__title section-title">Мероприятия</h2>

      <div class="upcoming__swiper swiper swiper--3s js-swiper">
        <div class="swiper-wrapper">
          <div class="swiper-slide">
            <!-- prettier-ignore -->
            @@include('event-card.html', {
              "class": "",
              "img": "assets/images/events/nirvana.jpg",
              "img_webp": "assets/images/events/nirvana.webp",
              "alt": "nirvana",
              "title": "Собираемся и слушаем альбом Nirvana.",
              "descr": "Это третий альбом группы...",
              "time": "07.12.2022 | начало 18.00",
              "datetime": "2022-12-07T18:00"
            })
          </div>

          <!-- Здесь добавьте ещё хотя бы три слайда по аналогии -->
        </div>

        <footer class="swiper-footer">
          <button type="button" class="swiper-arrow-prev arrow arrow--left">
            <svg class="arrow__icon" width="92" height="62">
              <use xlink:href="assets/icons/symbols.svg#arrow"></use>
            </svg>
          </button>

          <div class="swiper-pagination"></div>

          <button type="button" class="swiper-arrow-next arrow">
            <svg class="arrow__icon" width="92" height="62">
              <use xlink:href="assets/icons/symbols.svg#arrow"></use>
            </svg>
          </button>
        </footer>
      </div>

      <a href="#" class="upcoming__link link">Смотреть все</a>
    </div>
  </div>
</section>
Стилизация

Родных стилей плагина Swiper нам недостаточно. Необходимо стилизовать кнопки навигации слайдера, кроме того хотелось бы сделать карточки на протяжении адаптива по возможности резиновыми. Для этого придётся высчитать их размер в процентах.

Для того, чтобы задать отступы между слайдами у плагина Swiper есть соответствующая настройка, однако она потребует задания точек перелома и может вызвать некоторые проблемы, мы можем имитировать эти отступы с помощью свойства padding, заданного каждому слайду.

Для кастомной стилизации плагина создайте новый less-файл swiper_theme.less и подключите его после основных стилей:

@import "vendor/swiper.less";
@import "vendor/swiper_theme.less";

Приведём весь less-код, он не сложный, проблему может вызвать только понимание расчета ширины слайдов. Наша цель здесь просто добиться отступов, соответствующих макету:

.swiper-slide {
  height: auto;
  display: flex;
}

.swiper--3s {
  width: 100%;
  .swiper-slide {
    box-sizing: border-box;
    width: calc(33.3333% + 20px);
    min-width: 340px;
    padding-right: 60px;
    @media @bw1660 {
      width: calc(33.3333% + 13.3333px);
      min-width: 320px;
      padding-right: 40px;
    }
    @media @bw1340 {
      width: calc(33.3333% + 10px);
      min-width: 310px;
      padding-right: 30px;
    }
    @media @bw768 {
      width: calc(33.3333% + 6.6666px);
      min-width: 300px;
      padding-right: 20px;
    }
    &:last-child {
      width: calc(33.3333% - 40px);
      min-width: 280px;
      padding-right: 0;
      @media @bw1660 {
        width: calc(33.3333% - 26.6666px);
      }
      @media @bw1340 {
        width: calc(33.3333% - 20px);
      }
      @media @bw768 {
        width: calc(33.3333% - 13.3333px);
      }
    }
    & > * {
      width: 100%;
    }
    .event-card {
      &__link {
        cursor: pointer;
        cursor: grab;
        &:active {
          cursor: pointer;
          cursor: grabbing;
        }
      }
      &__title {
        cursor: pointer;
      }
    }
  }
}

И стили для пагинации и навигации в том же файле:

.swiper-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 530px;
  max-width: 100%;
  margin: 70px auto 0;
  @media @bw1340 {
    width: 300px;
  }
  @media @bw768 {
    margin: 60px auto 0;
  }
}

.swiper-pagination-bullets.swiper-pagination-horizontal {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  bottom: auto;
  left: auto;
  width: auto;
  margin: 0 auto;
  padding: 0 15px;
}

.swiper-pagination-bullet {
  width: 15px;
  height: 2px;
  border-top: 5px solid transparent;
  border-bottom: 5px solid transparent;
  border-radius: 2px;
  background-color: @gray_dark;
  background-clip: padding-box;
  opacity: 1;
}

.swiper-pagination-bullet.swiper-pagination-bullet-active {
  background-color: @black;
  background-clip: padding-box;
}

.swiper-pagination-horizontal.swiper-pagination-bullets
  .swiper-pagination-bullet {
  margin: 2px 3px;
}
Инициализация плагина

Итак, дело осталось за малым, просто добавить в наш main.js следующий код:

const swipers = document.querySelectorAll(".js-swiper");

swipers.forEach(function (swpr) {
  new Swiper(swpr, {
    updateOnWindowResize: true,
    slidesPerView: "auto",
    freeMode: true,
    spaceBetween: 0,
    speed: 500,
    grabCursor: true,
    pagination: {
      el: ".swiper-pagination",
      clickable: true
    },
    navigation: {
      nextEl: ".swiper-arrow-next",
      prevEl: ".swiper-arrow-prev",
      disabledClass: "arrow--disabled"
    }
  });
});

Данный код будет корректно выполнен во всех современных браузерах и для любого количества слайдеров на странице. Всё готово!

7.5 Карта

На главной странице присутствует карта в разделе контакты. Реализация возможна различными способами. Мы можем использовать Яндекс карты, Google карты, Mapbox, Leaflet. Все они предоставляют собственное api для разработчиков, обладают своими плюсами и минусами.

Создадим карту, используя Google Maps JavaScript Api.

С разметкой всё просто, нам нужен пустой контейнер:

<div class="" id="js-contactsMap"></div>

На главной странице нужно подключить скрипт api:

<html>
  <body>
    <!-- Содержимое страницы -->

    <script src="https://maps.googleapis.com/maps/api/js?key="></script>
    <script src="scripts/main.min.js"></script>
  </body>
</html>

Обратите внимание на запись key= в значении атрибута src. После знака равенства должен быть добавлен ключ доступа к api, который должен будет предоставить ваш заказчик. Без него карта работать будет, но с выводом сообщения об ошибке и без возможности стилизации карты:

Стилизовать карты нам поможет Google Map Styling Wizard. Вам нужно выбрать подходящий стиль и скопировать предложенный JSON.

[
  {
	elementType: "geometry",
	stylers: [
	  {
		color: "#242f3e"
	  }
	]
  },
  {
	elementType: "labels.text.fill",
	stylers: [
	  {
		color: "#746855"
	  }
	]
  },
  {
	elementType: "labels.text.stroke",
	stylers: [
	  {
		color: "#242f3e"
	  }
	]
  },
  {
	featureType: "administrative",
	elementType: "geometry",
	stylers: [
	  {
		visibility: "off"
	  }
	]
  },
  {
	featureType: "administrative.locality",
	elementType: "labels.text.fill",
	stylers: [
	  {
		color: "#d59563"
	  }
	]
  },
  {
	featureType: "poi",
	stylers: [
	  {
		visibility: "off"
	  }
	]
  },
  {
	featureType: "poi",
	elementType: "labels.text.fill",
	stylers: [
	  {
		color: "#d59563"
	  }
	]
  },
  {
	featureType: "poi.park",
	elementType: "geometry",
	stylers: [
	  {
		color: "#263c3f"
	  }
	]
  },
  {
	featureType: "poi.park",
	elementType: "labels.text.fill",
	stylers: [
	  {
		color: "#6b9a76"
	  }
	]
  },
  {
	featureType: "road",
	elementType: "geometry",
	stylers: [
	  {
		color: "#38414e"
	  }
	]
  },
  {
	featureType: "road",
	elementType: "geometry.stroke",
	stylers: [
	  {
		color: "#212a37"
	  }
	]
  },
  {
	featureType: "road",
	elementType: "labels.icon",
	stylers: [
	  {
		visibility: "off"
	  }
	]
  },
  {
	featureType: "road",
	elementType: "labels.text.fill",
	stylers: [
	  {
		color: "#9ca5b3"
	  }
	]
  },
  {
	featureType: "road.highway",
	elementType: "geometry",
	stylers: [
	  {
		color: "#746855"
	  }
	]
  },
  {
	featureType: "road.highway",
	elementType: "geometry.stroke",
	stylers: [
	  {
		color: "#1f2835"
	  }
	]
  },
  {
	featureType: "road.highway",
	elementType: "labels.text.fill",
	stylers: [
	  {
		color: "#f3d19c"
	  }
	]
  },
  {
	featureType: "transit",
	stylers: [
	  {
		visibility: "off"
	  }
	]
  },
  {
	featureType: "transit",
	elementType: "geometry",
	stylers: [
	  {
		color: "#2f3948"
	  }
	]
  },
  {
	featureType: "transit.station",
	elementType: "labels.text.fill",
	stylers: [
	  {
		color: "#d59563"
	  }
	]
  },
  {
	featureType: "water",
	elementType: "geometry",
	stylers: [
	  {
		color: "#17263c"
	  }
	]
  },
  {
	featureType: "water",
	elementType: "labels.text.fill",
	stylers: [
	  {
		color: "#515c6d"
	  }
	]
  },
  {
	featureType: "water",
	elementType: "labels.text.stroke",
	stylers: [
	  {
		color: "#17263c"
	  }
	]
  }
]

Инициализируем нашу карту с подходящими настройками опций:

const contactsMap = document.querySelector("#js-contactsMap");

if (contactsMap) {
  const mapStyles = []; //здесь должен быть задан JSON со стилями

  const mapCenter = new google.maps.LatLng(56.49387, 84.96274);

  const mapOptions = {
    center: mapCenter,
    disableDefaultUI: true,
    draggable: true,
    gestureHandling: "cooperative",
    scrollwheel: false,
    styles: mapStyles,
    zoom: 15,
    zoomControl: true,
    zoomControlOptions: {
      position: google.maps.ControlPosition.RIGHT_BOTTOM
    }
  };

  const map = new google.maps.Map(contactsMap, mapOptions);
}

Для того, чтобы легко получить координаты центра карты, вы можете воспользоваться Геолокатором Яндекс.

Осталось добавить точку на карту. Мы можем перевести иконку в base64, воспользовавшись одним из соответствующих онлайн-сервисов, либо указать ссылку на файл.

if (contactsMap) {
  //предыдущий код
  
  const point = new google.maps.LatLng(56.49385, 84.96274);

  const mapPin = new google.maps.MarkerImage(
    "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFsAAABHCAMAAABf/MtLAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAJcEhZcwAACxMAAAsTAQCanBgAAADAUExURUdwTK8wILwyJL0zI70yJbwyI79AIL0yJL8wILs0JLw0JLsyJL0zJbwyJbwzJB8eHv///3Nycjs6OldWVsRMP4+OjsRNP8fHx+Pj492ZkUlISOazrauqqu7MyMfGxsBAMS0sLMlZTc1mW/vy8sBAMvvy8WVkZMVMQPbl48lZTtmMg81mWtmMhMxmW4+Pj/fm4/fl5OGmnvHx8Z2cnMVNQPLZ1tWAdt6ZktBzaHNzc9V/duq/u+Gmn8hZTNFzabm5uYZR+N4AAAAOdFJOUwAQz9/f3xB/EIDPz9/ftWbT5QAAAWxJREFUWMPt1VlTwjAQAGDEaluPJiQhYGjtAeUQxPu+/v+/cpO2DC+OI7PRYcw+JOlO55vMdpO2WmtBNo3W9+FsZzvb2c529l/Y45hXi/iG6yA8fuKMkGUMqS5X1RvLONvA7tJOtaD9au5AginSo5A6hUem3+hRhmUTwhubjNFttbKzDwT7GqqtGpv0G5vE2Ptes9FrsvqW/9FmXDGIjHCuZwYJc0wUHCn9aYlJKO7uKmf/sh39LJxtxx7lObJ9e1emMA1oISN5OUmFmNMrIQSGnd/TM2M/w3iRwCDpCdK+i9EwMfY5jMkU056l0SvNtf0GT0PUfT9K+a6tAYUKP0wFpl3oSpTansuXchIh2jM9LKDWet/SlATNLkxjg2lqsjAtg9cnOqCbU1Ev9Ji6+wTTdv/LLbJdbHnsh160GwZW6ANz+Ns28LC+WXwL9lFt71mwv7gSUcKzuG/fYr2DtqEPd2w0YeAfR55vg/4EpN3f8dlAXnoAAAAASUVORK5CYII=",
    new google.maps.Size(91, 71), //size
    new google.maps.Point(0, 0),  //origin point
    new google.maps.Point(0, 71)  //offset point
  );

  new google.maps.Marker({
    position: point,
    map: map,
    icon: mapPin,
    title: "TAGREE digital"
  });
}

На этом всё, карта готова!

{{ netItem.title }}

Меню

Список уроков

1. Введение. Знакомство с HTML 2. Основы CSS и методологии 3. Блочная модель и позиционирование 4. Flexbox и Grid Layout 5. Оптимизация и автоматизация 6. Формы, таблицы и текстовый контент 7. Адаптивная вёрстка 8. Графика 9. Введение в JavaScript 10. Введение в JavaScript. Часть 2 11. Введение в JavaScript. Часть 3 12. Финал

Навигация по странице

Email

hi@tagree.ru

Телефон

+7 499 350 0730

Telegram

t.me/tagree_ru

Москва

+7 499 350 0730

Долгоруковская 7, БЦ «Садовая Плаза», 4 эт.

Томск

+7 382 270 0368

Белая 8, БЦ «Tagree»

Петербург

+7 812 509-31-09

​Большая Монетная, 16

vkyt
Политика конфиденциальности Пользовательское соглашение Cookies

Пользуясь сайтом, соглашаюсь с политикой использования cookies

© 2022, tagree digital agency.