Видеокурс «Практический JavaScript», часть первая

10.03.2021

Теги: JavaScriptWeb-разработкаПрактика

Купил на 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-разработка • Практика

Каталог оборудования
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Производители
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Функциональные группы
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.