Видеокурс «Практический JavaScript», часть первая
10.03.2021
Теги: JavaScript • Web-разработка • Практика
Купил на Udemy видеокурс «Практический JavaScript» от Ивана Петриченко, но моих знаний для просмотра оказалось явно маловато. Постоянно вылезают проблемы, с которыми раньше не сталкивался. Так что решил сделать для себя краткий конспект — как решал ту или иную проблему. Первое, с чем столкнулся — не запускается сборка проекта.
> gulp gulp : Имя «gulp» не распознано как имя командлета, функции, файла сценария или выполняемой программы. Проверьте правильность написания имени, а также наличие и правильность пути, после чего повторите попытку.
Оказалось, что нужно глобально установить пакет gulp-cli
:
> npm install -g gulp-cli
Хорошо, следующая попытка запустить gulp
:
> gulp gulp : Невозможно загрузить файл C:\Users\Evgeniy\AppData\Roaming\npm\gulp.ps1, так как выполнение сценариев отключено в этой системе. Для получения дополнительных сведений см. about_Execution_Policies по адресу https:/go.microsoft.com/fwlink /?LinkID=135170.
Политика Windows выполнения скриптов запрещает выполнять эти самые скрипты. Узнать текущее значение политики можно командой:
> Get-ExecutionPolicy Restricted
Чтобы разрешить выполнение файлов с расширением ps1
, т.е. чтобы запускать скрипты PowerShell в Windows, выполняем команду:
> Set-ExecutionPolicy Unrestricted Изменение политики выполнения Политика выполнения защищает компьютер от ненадежных сценариев. Изменение политики выполнения может поставить под угрозу безопасность системы, как описано в разделе справки, вызываемом командой about_Execution_Policies и расположенном по адресу https:/go.microsoft.com/fwlink/?LinkID=135170 . Вы хотите изменить политику выполнения? [Y] Да - Y [A] Да для всех - A [N] Нет - N [L] Нет для всех - L [S] Приостановить - S [?] Справка (значением по умолчанию является "N"): Y
Причем PowerShell для выполнения этой команды надо запускать от имени администратора.
Файл gulpfile.js
для сборки проекта:
'use strict'; const gulp = require('gulp'); const webpack = require('webpack-stream'); const browsersync = require('browser-sync'); const dist = './dist/'; /* * ЗАДАЧА COPY-HTML. Копирует файл src/index.html в dist/index.html * и обновляет страницу в браузере */ gulp.task('copy-html', () => { return gulp.src('./src/index.html') .pipe(gulp.dest(dist)) .pipe(browsersync.stream()); }); /* * ЗАДАЧА BUILD-JS. Транспиляция исходных js-файлов + минификация js-кода + * создание source-map и создание файла dist/script.js для dev-сервера */ gulp.task('build-js', () => { return gulp.src('./src/js/main.js') .pipe(webpack({ mode: 'development', output: { filename: 'script.js' }, watch: false, devtool: 'source-map', module: { rules: [ { test: /\.m?js$/, exclude: /(node_modules|bower_components)/, use: { loader: 'babel-loader', options: { presets: [['@babel/preset-env', { debug: true, corejs: 3, useBuiltIns: 'usage' }]] } } } ] } })) .pipe(gulp.dest(dist)) .on('end', browsersync.reload); }); /* * ЗАДАЧА COPY-ASSETS. Копирует файлы из src/assets в директорию * dist/assets и обновляет страницу в браузере */ gulp.task('copy-assets', () => { return gulp.src('./src/assets/**/*.*') .pipe(gulp.dest(dist + '/assets')) .on('end', browsersync.reload); }); /* * ЗАДАЧА WATCH. Запускает веб-сервер browsersync на порту 400, чтобы он * обслуживал директорию dist. При изменении файла index.html, файлов из * assets или js — запустить задачи copy-html, copy-assets, build-js */ gulp.task('watch', () => { browsersync.init({ server: './dist/', port: 4000, notify: true }); gulp.watch('./src/index.html', gulp.parallel('copy-html')); gulp.watch('./src/assets/**/*.*', gulp.parallel('copy-assets')); gulp.watch('./src/js/**/*.js', gulp.parallel('build-js')); }); /* * ЗАДАЧА BUILD. Запускает задачи copy-html, copy-assets, build-js */ gulp.task('build', gulp.parallel('copy-html', 'copy-assets', 'build-js')); /* * ЗАДАЧА BUILD-PROD-JS. Транспиляция исходных js-файлов + минификация * js-кода и создание файла dist/script.js для production-сервера */ gulp.task('build-prod-js', () => { return gulp.src('./src/js/main.js') .pipe(webpack({ mode: 'production', output: { filename: 'script.js' }, module: { rules: [ { test: /\.m?js$/, exclude: /(node_modules|bower_components)/, use: { loader: 'babel-loader', options: { presets: [['@babel/preset-env', { corejs: 3, useBuiltIns: 'usage' }]] } } } ] } })) .pipe(gulp.dest(dist)); }); /* * ЗАДАЧА DEFAULT (по умолчанию, если задача не указана) */ gulp.task('default', gulp.parallel('watch', 'build'));
Слайдер на странице
В курсе предлагается доработать проект — это простой landing на несколько экранов. Для создания слайдера используется jquery-плагин slick
. На странице index.html
подключаются библиотека jQuery, js-файл плагина (все файлы плагина находятся в директории src/assets/slick
) и js-файл слайдера slider.js
.
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script> <script src="assets/slick/slick.min.js"></script> <script src="assets/js/slider.js"></script>
В окончательном варианте проекта весь js-код будет в файле dist/script.js
. Поэтому удаляем из index.html
подключение трех js-файлов, вместо этого подключаем один файл script.js
. Потом устанавливаем пакеты jquery
и slick-carousel
с помощью менеджера пакетов.
<script src="script.js"></script>
> npm install jquery --save > npm install slick-carousel --save
Файл src/assets/js/slider.js
теперь нужно исправить, чтобы импортировать все необходимое для работы слайдера:
import $ from 'jquery'; import 'slick-carousel'; $(document).ready(function() { $('.glazing_slider').slick({ /* ... */ }); $('.decoration_slider').slick({ /* ... */ }); });
Кроме того, создаем файл src/js/main.js
, где будем подключать все модули:
import './slider';
slick
нужно как-то опубликовать директорию node_modules/slick-carousel/slick
, потому что в index.html
подключаются стили из этой директории. Но мы продолжаем публиковать директорию src/assets/slick
, которая досталась нам от предыдущего разработчика.
Модальное окно
При клике на определенные элементы нужно показывать модальное окно. Создаем директорию src/js/modules
, а внутри нее — файл modals.js
.
const modals = () => { function bindModal(triggerSelector, modalSelector, closeSelector) { const trigger = document.querySelectorAll(triggerSelector), modal = document.querySelector(modalSelector), close = document.querySelector(closeSelector); // на странице есть две ссылки для открытия одного окна, // мы сразу навешиваем обработчик события на обе ссылки trigger.forEach(item => { item.addEventListener('click', (e) => { if (e.target) { e.preventDefault(); } modal.style.display = 'block'; document.body.style.overflow = 'hidden'; }); }); // закрыть модальное окно при клике на крестик close.addEventListener('click', () => { modal.style.display = 'none'; document.body.style.overflow = ''; }); // закрыть окно при клике за пределами окна modal.addEventListener('click', (e) => { if (e.target === modal) { modal.style.display = 'none'; document.body.style.overflow = ''; } }); } // открыть модальное окно через delay секунд function showModalByTime(selector, delay) { setTimeout(function() { document.querySelector(selector).style.display = 'block'; document.body.style.overflow = 'hidden'; }, delay); } bindModal('.popup_engineer_btn', '.popup_engineer', '.popup_engineer .popup_close'); bindModal('.phone_link', '.popup', '.popup .popup_close'); // showModalByTime('.popup', 60000); }; export default modals;
Все модальные окна похожи, есть только незначительные отличия в css-селекторах, по которым к ним можно обращаться:
<!-- кнопка --> <button class="header_btn text-uppercase text-left popup_engineer_btn"> Вызвать замерщика </button> .......... <!-- окно --> <div class="popup_engineer" style="display: none;"> <div class="popup_dialog"> <div class="popup_content text-center"> <button type="button" class="popup_close"><strong>×</strong></button> <div class="popup_form"> <form class="form" action="#"> <h2>Запишитесь сегодня на <br><span>бесплатный замер</span></h2> <input class="form-control form_input" name="name" required type="text" placeholder="Введите ваше имя"> <input class="form-control form_input" name="phone" required type="text" placeholder="Введите телефон"> <button class="text-uppercase btn-block button" name="submit" type="submit">Вызвать замерщика!</button> <p class="form_notice">Ваши данные конфиденциальны</p> </form> </div> </div> </div> </div>
И осталось только изменить src/js/main.js
:
import './slider'; import modals from './modules/modals'; window.addEventListener('DOMContentLoaded', () => { modals(); });
Переключение вкладок
В двух местах landing-а есть вкладки, но пока нет возможности их переключать — это следующая задача, которую будем решать.
<!-- переключение вкладок --> <div class="glazing_slider"> <div class="glazing_block text-center wow fadeInUp"> <img src="assets/img/glazing/icons/1.png" alt="#"> <a class="tree_link">Деревянное <br>остекление</a> </div> <div class="glazing_block text-center wow fadeInUp" data-wow-delay="0.1s"> <img src="assets/img/glazing/icons/2.png" alt="#"> <a class="aluminum_link">Алюминиевое <br>остекление</a> </div> <div class="glazing_block text-center wow fadeInUp" data-wow-delay="0.2s"> <img src="assets/img/glazing/icons/3.png" alt="#"> <a class="plastic_link">Остекление <br>пластиковыми <br>рамами</a> </div> <div class="glazing_block text-center wow fadeInUp" data-wow-delay="0.3s"> <img src="assets/img/glazing/icons/4.png" alt="#"> <a class="french_link">Французское <br>остекление <br>(панорамное)</a> </div> <div class="glazing_block text-center wow fadeInUp" data-wow-delay="0.4s"> <img src="assets/img/glazing/icons/5.png" alt="#"> <a class="rise_link">Остекление <br>с выносом</a> </div> </div> <!-- содержимое вкладок --> <div class="row tree glazing_content"> содержимое первой вкладки </div> <div class="row aluminum glazing_content"> содержимое второй вкладки </div> <div class="row plastic glazing_content"> содержимое третьей вкладки </div> <div class="row french glazing_content"> содержимое четвертой вкладки </div> <div class="row rise glazing_content"> содержимое пятой вкладки </div>
<!-- переключение вкладок --> <div class="decoration_slider"> <div class="decoration_item wow fadeInUp"> <div class="internal_link no_click after_click"><a>Внутренняя отделка</a></div> </div> <div class="decoration_item wow fadeInUp" data-wow-delay="0.1s"> <div class="external_link no_click"><a>Внешняя отделка</a></div> </div> <div class="decoration_item wow fadeInUp" data-wow-delay="0.2s"> <div class="rising_link no_click"><a>Выносное остекление</a></div> </div> <div class="decoration_item wow fadeInUp" data-wow-delay="0.3s"> <div class="roof_link no_click"><a>Крыша на балкон</a></div> </div> </div> <!-- содержимое вкладок --> <div class="decoration_content"> <div class="row"> <div class="internal"> содержимое первой вкладки </div> <div class="external"> содержимое второй вкладки </div> <div class="rising"> содержимое третьей вкладки </div> <div class="roof"> содержимое четвертой вкладки </div> </div> </div>
Создаем новый модуль tabs.js
в директории modules
:
const tabs = (headerSelector, tabSelector, bodySelector, activeClass) => { const header = document.querySelector(headerSelector), // контейнер для переключателей tabLinks = document.querySelectorAll(tabSelector), // переключатели вкладок tabBodies = document.querySelectorAll(bodySelector); // содержимое вкладок // скрывает содержимое всех вкладок function hideTabContent() { tabBodies.forEach(item => { item.style.display = 'none'; }); tabLinks.forEach(item => { item.classList.remove(activeClass); }); } // показывает содержимое вкладки с номером i function showTabContent(i = 0) { tabBodies[i].style.display = 'block'; tabLinks[i].classList.add(activeClass); } // сразу после загрузки страницы скрываем все вкладки // и показываем содержимое первой вкладки hideTabContent(); showTabContent(); // при клике на контейнере переключателей выясняем, по какой ссылке // был клик — делаем эту вкладку активной и показываем этот контент header.addEventListener('click', (e) => { const target = e.target, tabSelectorClass = tabSelector.replace('.', ''), tabLinkHasClass = target.classList.contains(tabSelectorClass), parentHasClass = target.parentNode.classList.contains(tabSelectorClass); if (target && (tabLinkHasClass || parentHasClass)) { tabLinks.forEach((item, i) => { if (target == item || target.parentNode == item) { hideTabContent(); showTabContent(i); } }); } }); }; export default tabs;
И осталось только изменить src/js/main.js
:
import './slider'; import modals from './modules/modals'; import tabs from './modules/tabs'; window.addEventListener('DOMContentLoaded', () => { modals(); tabs('.glazing_slider ', '.glazing_block', '.glazing_content', 'active'); tabs('.decoration_slider', '.no_click', '.decoration_content > div > div', 'after_click'); });
Отправка простой формы
Внутри модальных окон есть формы. Их нужно отправлять посредством ajax и захватывать все введенные данные. Создаем новый модуль forms.js
в директории modules
:
const forms = () => { const allForms = document.querySelectorAll('form'), allInputs = document.querySelectorAll('input'), phoneInputs = document.querySelectorAll('input[name="user_phone"]'); // сообщения при отправке данных формы на сервер const message = { loading: 'Отправка данных формы...', success: 'Спасибо! Скоро мы с вами свяжемся', failure: 'Что-то пошло не так...' }; // для полей ввода номера телефона удаляет все, кроме цифр phoneInputs.forEach(item => { item.addEventListener('input', () => { item.value = item.value.replace(/\D/, ''); }); }); // отправляет данные data на сервер на указанный url const sendPostData = async (url, data) => { document.querySelector('.status').textContent = message.loading; let response = await fetch(url, { method: 'POST', body: data }); return await response.text(); }; // очищает все поля input всех форм на странице const clearInputs = () => { allInputs.forEach(item => { item.value = ''; }); }; // обработчик события отправки формы для всех форм на странице, // отправляет данные посредством ajax и показывает сообщения allForms.forEach(item => { item.addEventListener('submit', (e) => { e.preventDefault(); // создаем div-блок, в котором показываем сообщение let statusMessage = document.createElement('div'); statusMessage.classList.add('status'); item.appendChild(statusMessage); const formData = new FormData(item); sendPostData('assets/server.php', formData) .then(response => { // данные формы отправлены успешно statusMessage.textContent = message.success; }) .catch(() => statusMessage.textContent = message.failure) // ошибка .finally(() => { // скрыть сообщение, очистить все формы clearInputs(); setTimeout(() => { statusMessage.remove(); }, 5000); }); }); }); }; export default forms;
Сервер просто отправляет post-данные обратно в виде строки:
<?php echo var_dump($_POST);
И осталось только изменить src/js/main.js
:
import './slider'; import modals from './modules/modals'; import tabs from './modules/tabs'; import forms from './modules/forms'; window.addEventListener('DOMContentLoaded', () => { 'use strict'; modals(); tabs('.glazing_slider ', '.glazing_block', '.glazing_content', 'active'); tabs('.decoration_slider', '.no_click', '.decoration_content > div > div', 'after_click'); forms(); });
Отправка сложной формы
На странице есть сложная форма, которая показывается в нескольких модальных окнах. В первом окне заполняем поля — нажимаем кнопку «Далее», во втором окне запоняем поля — нажимаем кнопку «Далее», в третьем окне заполняем поля — нажимаем кнопку «Отправить». Причем только третье окно содержит тег <form>
— так что наш код отправки будет работать неправильно. Потому что будут отправлены только данные формы из третьего окна — а нужно добавить к ним данные из первого и второго окна.
Для начала нам надо решить проблему закрытия первого и второго окна при клике на кнопку «Далее» — нам не нужно, чтобы модальные окна мешали друг другу. Для этого добавим атрибут data-modal
для всех модальных окон на странице. И теперь можем в модуле modals.js
отбирать все окна и закрывать их перед открытием нового.
const modals = () => { function bindModal(triggerSelector, modalSelector, closeSelector) { const trigger = document.querySelectorAll(triggerSelector), modal = document.querySelector(modalSelector), close = document.querySelector(closeSelector), // NEW получаем все модальные окна на странице allModals = document.querySelectorAll('[data-modal]'); // на странице есть две ссылки для открытия одного окна, // мы сразу навешиваем обработчик события на обе ссылки trigger.forEach(item => { item.addEventListener('click', (e) => { if (e.target) { e.preventDefault(); } // NEW если есть открытое модальное окно — закрываем его allModals.forEach(item => { item.style.display = 'none'; document.body.style.overflow = ''; }); modal.style.display = 'block'; document.body.style.overflow = 'hidden'; }); }); // закрыть модальное окно при клике на крестик close.addEventListener('click', () => { /* ... */ }); // закрыть окно при клике за пределами окна modal.addEventListener('click', (e) => { /* ... */ }); } // открыть модальное окно через delay секунд function showModalByTime(selector, delay) { /* ... */ } bindModal('.popup_engineer_btn', '.popup_engineer', '.popup_engineer .popup_close'); bindModal('.phone_link', '.popup', '.popup .popup_close'); // NEW открытие первого модального окна с формой bindModal('.popup_calc_btn', '.popup_calc', '.popup_calc_close'); // NEW открытие второго модального окна с формой bindModal('.popup_calc_button', '.popup_calc_profile', '.popup_calc_profile_close'); // NEW открытие третьего модального окна с формой bindModal('.popup_calc_profile_button', '.popup_calc_end', '.popup_calc_end_close'); // showModalByTime('.popup', 60000); }; export default modals;
В первом модальном окне надо выбрать форму балкона. Собственно говоря, это вкладки — при клике на маленькую картинку показывается увеличенное изображение.
<div class="balcon_icons"> <span class="balcon_icons_img do_image_more"> <img src="assets/img/modal_calc/balkon/ba_01.png" alt="Тип1"> </span> <span class="balcon_icons_img"> <img src="assets/img/modal_calc/balkon/ba_02.png" alt="Тип2"> </span> <span class="balcon_icons_img"> <img src="assets/img/modal_calc/balkon/ba_03.png" alt="Тип3"> </span> <span class="balcon_icons_img"> <img src="assets/img/modal_calc/balkon/ba_04.png" alt="Тип4"> </span> </div> <div class="big_img text-center"> <img src="assets/img/modal_calc/balkon/type1.png" alt="Тип1"> <img src="assets/img/modal_calc/balkon/type2.png" alt="Тип2"> <img src="assets/img/modal_calc/balkon/type3.png" alt="Тип3"> <img src="assets/img/modal_calc/balkon/type4.png" alt="Тип4"> </div>
Модуль для работы с вкладками у нас уже есть, надо только в main.js
добавить вызов tabs()
с нужными селекторами.
/* ... */ window.addEventListener('DOMContentLoaded', () => { /* ... */ tabs('.balcon_icons', '.balcon_icons_img', '.big_img > img', 'do_image_more', 'inline-block'); /* ... */ });
Но при показе содержимого вкладок надо использовать другое значение свойства display
— это inline-block
вместо block
.
const tabs = (headerSelector, tabSelector, contentSelector, activeClass, display = 'block') => { /* ... */ function showTabContent(i = 0) { content[i].style.display = display; tab[i].classList.add(activeClass); } /* ... */ }; export default tabs;
Теперь будем все значения, которые выбрал пользователь на первой и второй вкладке, сохранять в переменную modalState
:
/* ... */ window.addEventListener('DOMContentLoaded', () => { /* ... */ let modalState = {}; changeModalState(modalState); modals(); forms(modalState); });
Для этого создаем новый модуль changeModalState.js
в директории modules
:
const changeModalState = (state) => { const windowShape = document.querySelectorAll('.balcon_icons_img'), windowWidth = document.querySelectorAll('#width'), windowHeight = document.querySelectorAll('#height'), windowType = document.querySelectorAll('#view_type'), windowProfile = document.querySelectorAll('.checkbox'); function bindActionToNodes(event, node, prop) { node.forEach((item, i) => { item.addEventListener(event, () => { switch(item.nodeName) { case 'SPAN' : state[prop] = i + 1; break; case 'INPUT' : if (item.getAttribute('type') === 'checkbox') { i === 0 ? state[prop] = 'cold' : state[prop] = 'warm'; node.forEach((box, j) => { box.checked = false; if (i === j) { box.checked = true; } }); } else { state[prop] = item.value; } break; case 'SELECT' : state[prop] = item.value; break; } }); }); } // выбор формы окна для остекления bindActionToNodes('click', windowShape, 'shape'); // ширина и высота в миллиметрах bindActionToNodes('input', windowHeight, 'height'); bindActionToNodes('input', windowWidth, 'width'); // тип остекления балкона bindActionToNodes('change', windowType, 'type'); // теплое или холодное bindActionToNodes('change', windowProfile, 'profile'); }; export default changeModalState;
Изменяем модуль form.js
, чтобы перед отправкой формы из третьего модального окна — добавить к отправке на сервер данные из первого и второго окна.
const forms = (state) => { /* ... */ allForms.forEach(item => { item.addEventListener('submit', (e) => { e.preventDefault(); // создаем div-блок, в котором показываем сообщение let statusMessage = document.createElement('div'); statusMessage.classList.add('status'); item.appendChild(statusMessage); const formData = new FormData(item); // NEW дополняем данными из первого и второго модального окна if (item.getAttribute('data-calc') === 'end') { for (let key in state) { formData.append(key, state[key]); } } sendPostData('assets/server.php', formData) .then(response => { // данные формы отправлены успешно statusMessage.textContent = message.success; }) .catch(() => statusMessage.textContent = message.failure) // ошибка .finally(() => { // скрыть сообщение, очистить все формы clearInputs(); setTimeout(() => { statusMessage.remove(); }, 5000); }); }); }); /* ... */ }; export default forms;
И осталось только добавить атрибут data-calc
для формы в третьем модальном окне:
<form class="form" action="#" data-calc="end"> <h2>Спасибо за обращение! <br>Оставьте свои данные</h2> <input class="form-control form_input" name="user_name" required type="text" placeholder="Введите ваше имя"> <input class="form-control form_input" name="user_phone" required type="text" placeholder="Введите телефон"> <button class="text-uppercase btn-block button" name="submit" type="submit">Рассчитать стоимость</button> <p class="form_notice">Перезвоним в течение 10 минут</p> </form>
Таймер окончания акции
На странице есть таймер, который отсчитывает дни, часы и минуты до оконцания акции. Создаем новый модуль timer.js
в директории modules
:
const timer = (timerSelector, promoEndTime) => { // вспомогательная функция, добавляет ноль перед днями, // часами, минутами и секундами: 1:2:3:4 => 01:02:03:04 const addZero = (number) => { if (number <= 9) { return '0' + number; } else { return number; } }; const getTimeRemaining = () => { // сколько миллисекунд осталось до окончания акции const delta = Date.parse(promoEndTime) - Date.parse(new Date()), seconds = Math.floor((delta / 1000) % 60), minutes = Math.floor((delta / (1000 * 60) % 60)), hours = Math.floor((delta / (1000 * 60 * 60)) % 24), days = Math.floor((delta / (1000 * 60 * 60 * 24))); return { 'total': delta, 'days': days, 'hours': hours, 'minutes': minutes, 'seconds': seconds }; }; const setClock = () => { const timer = document.querySelector(timerSelector), days = timer.querySelector('#days'), hours = timer.querySelector('#hours'), minutes = timer.querySelector('#minutes'), seconds = timer.querySelector('#seconds'), // обновлять таймер будем каждую секунду timeInterval = setInterval(updateClock, 1000); // обновляем таймер сразу после загрузки страницы updateClock(); function updateClock() { const delta = getTimeRemaining(); if (delta.total <= 0) { // акция закончилась days.textContent = '00'; hours.textContent = '00'; minutes.textContent = '00'; seconds.textContent = '00'; clearInterval(timeInterval); return; } days.textContent = addZero(delta.days); hours.textContent = addZero(delta.hours); minutes.textContent = addZero(delta.minutes); seconds.textContent = addZero(delta.seconds); } }; // запускаем таймер отсчета времени до окончания акции setClock(); }; export default timer;
В файле main.js
задаем время окончания акции и запускаем таймер:
import './slider'; import modals from './modules/modals'; import tabs from './modules/tabs'; import forms from './modules/forms'; import changeModalState from './modules/changeModalState'; import timer from './modules/timer'; window.addEventListener('DOMContentLoaded', () => { 'use strict'; let modalState = {}; let deadline = '2021-04-01'; changeModalState(modalState); modals(); tabs('.glazing_slider ', '.glazing_block', '.glazing_content', 'active'); tabs('.decoration_slider', '.no_click', '.decoration_content > div > div', 'after_click'); tabs('.balcon_icons', '.balcon_icons_img', '.big_img > img', 'do_image_more', 'inline-block'); forms(modalState); timer('.container1', deadline); });
Изображение в модальном окне
На странице есть галерея выполненных работ, нужно при клике по изображению открывать большое изображение с затемнением остальной страницы.
<section class="works"> <div class="container"> <div class="section_header"> <h2>Наши работы</h2> <div class="section_header_sub"></div> </div> <div class="row"> <div class="col-lg-3 col-md-4 col-sm-6 col-xs-12 text-center wow fadeIn"> <a href="assets/img/our_works/big_img/1.png"> <img class="preview" src="assets/img/our_works/1.png" alt="window"> </a> </div> <div class="col-lg-3 col-md-4 col-sm-6 col-xs-12 text-center wow fadeIn" data-wow-delay="0.1s"> <a href="assets/img/our_works/big_img/2.png"> <img class="preview" src="assets/img/our_works/2.png" alt="window"> </a> </div> <div class="col-lg-3 col-md-4 col-sm-6 col-xs-12 text-center wow fadeIn" data-wow-delay="0.2s"> <a href="assets/img/our_works/big_img/3.png"> <img class="preview" src="assets/img/our_works/3.png" alt="window"> </a> </div> <div class="col-lg-3 col-md-4 col-sm-6 col-xs-12 text-center wow fadeIn" data-wow-delay="0.3s"> <a href="assets/img/our_works/big_img/4.png"> <img class="preview" src="assets/img/our_works/4.png" alt="window"> </a> </div> <div class="col-lg-3 col-md-4 col-sm-6 col-xs-12 text-center wow fadeIn" data-wow-delay="0.4s"> <a href="assets/img/our_works/big_img/5.png"> <img class="preview" src="assets/img/our_works/5.png" alt="window"> </a> </div> <div class="col-lg-3 col-md-4 col-sm-6 col-xs-12 text-center wow fadeIn" data-wow-delay="0.5s"> <a href="assets/img/our_works/big_img/6.png"> <img class="preview" src="assets/img/our_works/6.png" alt="window"> </a> </div> <div class="col-lg-3 col-md-4 col-sm-6 col-xs-12 text-center wow fadeIn" data-wow-delay="0.6s"> <a href="assets/img/our_works/big_img/7.png"> <img class="preview" src="assets/img/our_works/7.png" alt="window"> </a> </div> <div class="col-lg-3 col-md-4 col-sm-6 col-xs-12 text-center wow fadeIn" data-wow-delay="0.7s"> <a href="assets/img/our_works/big_img/8.png"> <img class="preview" src="assets/img/our_works/8.png" alt="window"> </a> </div> </div> </div> </section>
Создаем новый модуль images.js
в директории modules
:
const images = () => { // создаем div-элемент модального окна const imgPopup = document.createElement('div'); // секция, где расположены все изображения const workSection = document.querySelector('.works'); // создаем img-элемент большого изображения const bigImage = document.createElement('img'); // добавляем css-класс, который затемняет страницу imgPopup.classList.add('popup'); // модальное окно вставляем как дочерний элемент секции workSection.appendChild(imgPopup); // центрируем изображение по горизонтали и вертикали imgPopup.style.justifyContent = 'center'; imgPopup.style.alignItems = 'center'; imgPopup.style.display = 'none'; // внутрь модального окна вставляем большое изображение imgPopup.appendChild(bigImage); // обработчик события клика вешаем на секцию, а потом // проверяем, по какому именно изображению кликнули workSection.addEventListener('click', (e) => { e.preventDefault(); let target = e.target; // если был клик по маленькому изображению if (target && target.classList.contains('preview')) { imgPopup.style.display = 'flex'; const path = target.parentNode.getAttribute('href'); bigImage.setAttribute('src', path); } // если был клик за пределами большого изображения, // значит надо закрыть модальное окно изображения if (target && target.matches('div.popup')) { imgPopup.style.display = 'none'; } }); }; export default images;
И осталось использовать новый модуль в файле main.js
:
/* ... */ import images from './modules/images'; window.addEventListener('DOMContentLoaded', () => { /* ... */ images(); });
Небольшие улучшения
При открытии модального окна страница дергается вправо. Это потому, что для body
мы устанавливаем свойсто overflow
в значение hidden
. Давайте это исправим — будем добавлять margin-left
для body
— который будет равен ширине scroll
.
const modals = () => { // NEW вычисляем ширину полосы прокрутки const scrollWidth = calcScrollWidth(); function bindModal(triggerSelector, modalSelector, closeSelector) { const trigger = document.querySelectorAll(triggerSelector), modal = document.querySelector(modalSelector), close = document.querySelector(closeSelector), allModals = document.querySelectorAll('[data-modal]'); // на странице есть две ссылки для открытия одного окна, // мы сразу навешиваем обработчик события на обе ссылки trigger.forEach(item => { item.addEventListener('click', (e) => { if (e.target) { e.preventDefault(); } // если есть открытое модальное окно — закрываем его allModals.forEach(item => { item.style.display = 'none'; document.body.style.overflow = ''; // NEW убираем margin-left для body document.body.style.marginRight = '0px'; }); modal.style.display = 'block'; document.body.style.overflow = 'hidden'; // NEW добавляем margin-left для body document.body.style.marginRight = `${scrollWidth}px`; }); }); // закрыть модальное окно при клике на крестик close.addEventListener('click', () => { modal.style.display = 'none'; document.body.style.overflow = ''; // NEW убираем margin-left для body document.body.style.marginRight = '0px'; }); // закрыть окно при клике за пределами окна modal.addEventListener('click', (e) => { if (e.target === modal) { modal.style.display = 'none'; document.body.style.overflow = ''; // NEW убираем margin-left для body document.body.style.marginRight = '0px'; } }); } // открыть модальное окно через delay секунд function showModalByTime(selector, delay) { setTimeout(function() { document.querySelector(selector).style.display = 'block'; document.body.style.overflow = 'hidden'; // NEW добавляем margin-left для body document.body.style.marginRight = `${scrollWidth}px`; }, delay); } // вычислить ширину скролла страницы; создаем div-элемент // с прокруткой и вычисляем разницу между полной шириной // этого элемента и шириной без учета полосы прокрутки function calcScrollWidth() { let div = document.createElement('div'); div.style.width = '50px'; div.style.height = '50px'; div.style.overflowY = 'scroll'; div.style.visibility = 'hidden'; document.body.appendChild(div); let scrollWidth = div.offsetWidth - div.clientWidth; div.remove(); return scrollWidth; } bindModal('.popup_engineer_btn', '.popup_engineer', '.popup_engineer .popup_close'); bindModal('.phone_link', '.popup', '.popup .popup_close'); bindModal('.popup_calc_btn', '.popup_calc', '.popup_calc_close'); bindModal('.popup_calc_button', '.popup_calc_profile', '.popup_calc_profile_close'); bindModal('.popup_calc_profile_button', '.popup_calc_end', '.popup_calc_end_close'); // showModalByTime('.popup', 60000); }; export default modals;
Демо-сайт здесь, исходные коды здесь.
Поиск: JavaScript • Web-разработка • Практика