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

10.07.2021

Теги: JavaScriptWeb-разработкаКомпонентМодульПрактика

Один компонент готов, осталось еще шесть — Dropdown, Card, Modal, Tab, Accordion и Carousel. Для компонента Button потребовалось только задать стили, но для других компонентов нужно будет добавить js-код, который их «оживит». После этого создадим сервисы для работы с сервером и приступим к проекту с использованием нашей библиотеки.

18. Компонент «Dropdown»

Файл src/sass/components/_dropdown.scss:

.dropdown {
    position: relative;
    display: inline-block;
    &-toggle {
        &:after {
            content: '';
            display: inline-block;
            margin-left: 5px;
            vertical-align: 3px;
            border-top: 4px solid;
            border-right: 4px solid transparent;
            border-bottom: 0;
            border-left: 4px solid transparent;
        }
    }
    &-menu {
        position: absolute;
        top: 100%;
        left: 0;
        z-index: 1000;
        display: none;
        border: 1px solid rgba(0, 0, 0, 0.15);
        border-radius: 2px;
        background-color: #fff;
        min-width: 100%;
        padding: 10px 0;
    }
    &-item {
        display: block;
        width: 100%;
        padding: 7px 10px;
        color: $dark;
        background-color: transparent;
        border: 0;
        text-decoration: none;
        &:hover {
            background-color: rgba(0, 0, 0, 0.05);
        }
    }
}

Файл src/js/components/dropdown.js:

import $ from '../core.js';

$.extensions.dropdown = function() {
    for (let i = 0; i < this.length; i++) {
        let button = this[i].querySelector('.dropdown-toggle');
        let menu = this[i].querySelector('.dropdown-menu');
        $(menu).hide(); // изначально меню должно быть скрыто
        $(button).click(() => {
            $(menu).toggleFade(300);
        });
    }
    return this;
};

Теперь создадим несколько меню и превратим их в dropdown:

<body>
    <div class="dropdown">
        <button class="btn btn-primary dropdown-toggle">Dropdown button</button>
        <div class="dropdown-menu">
            <a href="#" class="dropdown-item">Action #1</a>
            <a href="#" class="dropdown-item">Action #2</a>
            <a href="#" class="dropdown-item">Action #3</a>
        </div>
    </div>
    <div class="dropdown">
        <button class="btn btn-primary dropdown-toggle">Dropdown button</button>
        <div class="dropdown-menu">
            <a href="#" class="dropdown-item">Action #4</a>
            <a href="#" class="dropdown-item">Action #5</a>
            <a href="#" class="dropdown-item">Action #6</a>
        </div>
    </div>
</body>
$('.dropdown').dropdown();

В bootstrap можно «оживить» dropdown-меню, если просто добавить в разметку атрибут. Давайте и мы сделаем аналогично — если у элемента есть атрибут data-dropdown, то сразу сделаем из него dropdown-меню.

<body>
    <div class="container">
        <div class="dropdown" data-dropdown="true">
            <button class="btn btn-primary dropdown-toggle">Dropdown button</button>
            <div class="dropdown-menu">
                <a href="#" class="dropdown-item">Action #1</a>
                <a href="#" class="dropdown-item">Action #2</a>
                <a href="#" class="dropdown-item">Action #3</a>
            </div>
        </div>
        <div class="dropdown" data-dropdown="true">
            <button class="btn btn-primary dropdown-toggle">Dropdown button</button>
            <div class="dropdown-menu">
                <a href="#" class="dropdown-item">Action #4</a>
                <a href="#" class="dropdown-item">Action #5</a>
                <a href="#" class="dropdown-item">Action #6</a>
            </div>
        </div>
    </div>
</body>
import $ from '../core.js';

$.extensions.dropdown = function() {
    /* ..... */
};

$('.dropdown[data-dropdown="true"]').dropdown();

19. Компонент «Card»

Файл src/sass/components/_card.scss:

.card {
    display: flex;
    justify-content: space-between;
    &-item {
        width: 32%;
        background-color: #fff;
        border: 1px solid rgba(0, 0, 0, 0.2);
        border-radius: 4px;
        box-shadow: 5px 5px 30px rgba(0, 0, 0, 0.1);
    }
    &-img {
        width: 100%;
    }
    &-body {
        padding: 20px;
    }
    &-title {
        font-size: 20px;
        margin-bottom: 10px;
    }
    &-text {
        margin-bottom: 10px;
    }
    a {
        text-decoration: none;
    }
}

Пример использования:

<body>
    <div class="container">
        <div class="cards">
            <div class="card">
                <img class="card-img" src="..." alt="photo">
                <div class="card-body">
                    <div class="card-title">Card title #1</div>
                    <p class="card-text">
                        Lorem ipsum dolor sit amet consectetur adipisicing elit. Officia itaque placeat qui suscipit.
                    </p>
                    <a href="#" class="btn btn-primary">Link to</a>
                </div>
            </div>
            <div class="card">
                <img class="card-img" src="..." alt="photo">
                <div class="card-body">
                    <div class="card-title">Card title #2</div>
                    <p class="card-text">
                        Lorem ipsum dolor sit amet consectetur adipisicing elit. Officia itaque placeat qui suscipit.
                    </p>
                    <a href="#" class="btn btn-primary">Link to</a>
                </div>
            </div>
            <div class="card">
                <img class="card-img" src="..." alt="photo">
                <div class="card-body">
                    <div class="card-title">Card title #3</div>
                    <p class="card-text">
                        Lorem ipsum dolor sit amet consectetur adipisicing elit. Officia itaque placeat qui suscipit.
                    </p>
                    <a href="#" class="btn btn-primary">Link to</a>
                </div>
            </div>
        </div>
    </div>
</body>

20. Компонент «Modal»

Файл src/sass/components/_modal.scss:

.modal {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 1050;
    display: none;
    width: 100%;
    height: 100%;
    overflow: hidden;
    background-color: rgba(0, 0, 0, 0.5);
    &-dialog {
        max-width: 500px;
        margin: 40px auto;
    }
    &-content {
        position: relative;
        width: 100%;
        background-color: #fff;
        border: 1px solid rgba(0, 0, 0, 0.2);
        border-radius: 4px;
        max-height: 80vh;
        overflow-y: auto;
        .close {
            position: absolute;
            top: 10px;
            right: 14px;
            font-size: 30px;
            color: #000;
            opacity: 0.5;
            font-weight: 700;
            border: none;
            background-color: transparent;
            cursor: pointer;
        }
    }
    &-header, &-body {
        padding: 15px;
        border-bottom: 1px solid #dee2e6;
    }
    &-title {
        font-weight: 500;
        font-size: 20px;
    }
    &-footer {
        padding: 15px;
    }
}

Файл src/js/components/modal.js:

import $ from '../core.js';

$.extensions.modal = function() {
    for (let i = 0; i < this.length; i++) {
        // получаем идентификатор окна, которое надо открыть
        const target = this[i].getAttribute('data-target');
        // модального окна не существует — ничего не делаем
        if (!document.querySelector(target)) continue;
        /*
         * Навешиваем обработчик события на кнопку/ссылку открытия
         */
        $(this[i]).click((event) => {
            event.preventDefault();
            $(target).fadeIn(500);
            // чтобы контент страницы нельзя было прокручивать
            document.body.style.overflow = 'hidden';
        });
        /*
         * Навешиваем обработчик события на кнопку/ссылку закрытия
         */
        const closeButtons = document.querySelectorAll(`${target} [data-close]`);
        closeButtons.forEach(item => {
            $(item).click(() => {
                $(target).fadeOut(500);
                document.body.style.overflow = '';
            });
        });
        /*
         * Закрыть окно, если был клик за пределами модального окна
         */
        $(target).click((event) => {
            if (event.target.classList.contains('modal')) {
                $(target).fadeOut(500);
                document.body.style.overflow = '';
            }
        });
    }
};

$('[data-modal="true"]').modal();

Создадим разметку и посмотрим в действии:

<body>
    <div class="container">
        <button class="btn btn-primary" data-modal="true" data-target="#modal-one">Открыть окно #one</button>
        <button class="btn btn-primary" data-modal="true" data-target="#modal-two">Открыть окно #two</button>
    </div>

    <div class="modal" id="modal-one">
        <div class="modal-dialog">
            <div class="modal-content">
                <button class="close" data-close>
                    <span>&times;</span>
                </button>
                <div class="modal-header">
                    <div class="modal-title">
                        Модальное окно #one
                    </div>
                </div>
                <div class="modal-body">
                    Lorem ipsum dolor sit amet consectetur adipisicing elit. Quam ipsa doloremque ducimus beatae minima
                    dignissimos ut laborum rerum nesciunt ab, debitis aliquam assumenda doloribus veritatis impedit?
                </div>
                <div class="modal-footer">
                    <button class="btn btn-danger" data-close>Закрыть</button>
                    <button class="btn btn-success">Сохранить</button>
                </div>
            </div>
        </div>
    </div>

    <div class="modal" id="modal-two">
        <div class="modal-dialog">
            <div class="modal-content">
                <button class="close" data-close>
                    <span>&times;</span>
                </button>
                <div class="modal-header">
                    <div class="modal-title">
                        Модальное окно #two
                    </div>
                </div>
                <div class="modal-body">
                    Lorem ipsum dolor sit amet consectetur adipisicing elit. Quam ipsa doloremque ducimus beatae minima
                    dignissimos ut laborum rerum nesciunt ab, debitis aliquam assumenda doloribus veritatis impedit?
                </div>
                <div class="modal-footer">
                    <button class="btn btn-danger" data-close>Закрыть</button>
                    <button class="btn btn-success">Сохранить</button>
                </div>
            </div>
        </div>
    </div>
</body>

Иногда возникает необходимость создать модальное окно «на лету», на основе полученных от сервера данных:

import $ from '../core.js';

$.extensions.modal = function () {
    for (let i = 0; i < this.length; i++) {
        // получаем идентификатор окна, которое надо открыть
        const target = this[i].getAttribute('data-target');
        // модального окна не существует — ничего не делаем
        if (!document.querySelector(target)) continue;
        /*
         * Навешиваем обработчик события на кнопку/ссылку открытия
         */
        if (!this[i].hasAttribute('data-click')) { // если еще не навесили
            $(this[i]).click((event) => {
                event.preventDefault();
                $(target).fadeIn(500);
                // чтобы контент страницы нельзя было прокручивать
                document.body.style.overflow = 'hidden';
                this[i].setAttribute('data-click', '');
            });
        }
        /*
         * Навешиваем обработчик события на кнопку/ссылку закрытия
         */
        const closeButtons = document.querySelectorAll(`${target} [data-close]`);
        closeButtons.forEach(item => {
            $(item).click((event) => {
                event.preventDefault();
                $(target).fadeOut(500);
                document.body.style.overflow = '';
            });
        });
        /*
         * Закрыть окно, если был клик за пределами модального окна
         */
        $(target).click((event) => {
            if (event.target.classList.contains('modal')) {
                $(target).fadeOut(500);
                document.body.style.overflow = '';
            }
        });
    }
};

$('[data-modal="true"]').modal();

$.extensions.createModal = function({content, buttons} = {}) {
    for (let i = 0; i < this.length; i++) {
        // создаем модальное окно «на лету»; окно уже могло быть создано
        // при предыдущем вызове этой функции, так что надо это проверить
        let modal = document.querySelector(this[i].getAttribute('data-target'));
        if (modal) modal.remove();
        modal = document.createElement('div');
        modal.classList.add('modal');
        modal.setAttribute('id', this[i].getAttribute('data-target').slice(1));
        /*
         * ВХОДНЫЕ ПАРАМЕТРЫ ФУНКЦИИ createModal()
         * data = {
         *     content: {title: '...', body: '...'},
         *     buttons: [
         *         {classes: ['one', 'two'], text: '...', close: true, callback: function() {...}},
         *         {classes: ['one', 'two'], text: '...', close: true, callback: function() {...}}
         *     ]
         * };
         */
        const btns = []; // кнопки в подвале модального окна
        for (let j = 0; j < buttons.length; j++) {
            let btn = document.createElement('button');
            btn.classList.add('btn', ...buttons[j].classes);
            btn.textContent = buttons[j].text;
            if (buttons[j].close) {
                btn.setAttribute('data-close', '');
            }
            if (typeof buttons[j].callback === "function") {
                btn.addEventListener('click', buttons[j].callback);
            }
            btns.push(btn);
        }
        modal.innerHTML = `
        <div class="modal-dialog">
            <div class="modal-content">
                <button class="close" data-close>
                    <span>&times;</span>
                </button>
                <div class="modal-header">
                    <div class="modal-title">
                        ${content.title}
                    </div>
                </div>
                <div class="modal-body">
                    ${content.body}
                </div>
                <div class="modal-footer"></div>
            </div>
        </div>
        `;
        modal.querySelector('.modal-footer').append(...btns); // добавляем кнопки
        modal.querySelector('.modal-footer button').after(' '); // пробел между кнопками
        document.body.append(modal);
        // чтобы не навешивать обработчик клика для окрытия окна при вызове функции modal,
        // потому что обработчик будет открывать модальное окно, а мы окно открываем сами;
        // нам от функции modal нужно, только чтобы она навесила обработчики закрытия окна
        this[i].setAttribute('data-click', '');
        $(this[i]).modal(); // назначаем обработчик клика для закрытия окна
        $(modal).fadeIn(500); // сразу открываем это модальное окно
    }
};

Пример создания модального окна «на лету»:

<body>
    <div class="container">
        <button class="btn btn-primary" id="modal-open" data-target="#modal-three">Открыть окно #three</button>
    </div>
</body>
let data = {
    content: {
        title: 'Модальное окно #three (динамическое)',
        body: 'Lorem ipsum dolor sit amet consectetur adipisicing elit.'
    },
    buttons: [
        {classes: ['btn', 'btn-danger'], text: 'Закрыть', close: true, callback: () => alert('Закрыть')},
        {classes: ['btn', 'btn-success'], text: 'Сохранить', close: false, callback: () => alert('Сохранить')}
    ]
};

$('#modal-open').click(() => $('#modal-open').createModal(data));

21. Компонент «Tab»

Файл src/sass/components/_tab.scss:

.tab {
    &-panel {
        display: flex;
        width: 100%;
        min-height: 50px;
        border: 1px solid rgba(0, 0, 0, 0.2);
        border-radius: 4px 4px 0 0;
    }
    &-item {
        font-size: 18px;
        line-height: 27px;
        padding: 10px 20px;
        border-right: 1px solid rgba(0, 0, 0, 0.2);
        cursor: pointer;
        &:last-child {
            border-right: none;
        }
        &-active {
            background-color: rgba(0, 0, 0, 0.1);
        }
    }
    &-content {
        display: none;
        padding: 20px;
        border: 1px solid rgba(0, 0, 0, 0.2);
        border-top: none;
        border-radius: 0 0 4px 4px;
        &-active {
            display: block;
        }
    }
}

Файл src/js/components/tab.js:

import $ from '../core.js';

$.extensions.tab = function() {
    for (let i = 0; i < this.length; i++) {
        $(this[i]).click(() => {
            $(this[i])
                .addClass('tab-item-active')
                .siblings()
                .removeClass('tab-item-active')
                .closest('.tab')
                .find('.tab-content')
                .removeClass('tab-content-active')
                .valueOf($(this[i]).parent().children().indexOf(this[i]))
                .addClass('tab-content-active');
        });
    }
};

$('[data-tab="true"] .tab-item').tab();

Создадим разметку и посмотрим в действии:

<body>
    <div class="container">
        <div class="tab mt-20 block-center" data-tab="true">
            <div class="tab-panel">
                <div class="tab-item tab-item-active">Content 1</div>
                <div class="tab-item">Content 2</div>
                <div class="tab-item">Content 3</div>
            </div>
            <div class="tab-content tab-content-active">
                <h3>Content 1</h3>
                Lorem ipsum dolor sit, amet consectetur adipisicing elit. Quaerat laboriosam blanditiis omnis? Ab
                molestias tempora beatae ex itaque cumque, velit modi, molestiae debitis earum dolor quae quis.
            </div>
            <div class="tab-content">
                <h3>Content 2</h3>
                Lorem ipsum dolor sit, amet consectetur adipisicing elit. Quaerat laboriosam blanditiis omnis? Ab
                molestias tempora beatae ex itaque cumque, velit modi, molestiae debitis earum dolor quae quis.
            </div>
            <div class="tab-content">
                <h3>Content 3</h3>
                Lorem ipsum dolor sit, amet consectetur adipisicing elit. Quaerat laboriosam blanditiis omnis? Ab
                molestias tempora beatae ex itaque cumque, velit modi, molestiae debitis earum dolor quae quis.
            </div>
        </div>
    </div>
</body>

22. Компонент «Accordion»

Файл src/sass/components/_accordion.scss:

.accordion {
    border: 1px solid rgba(0, 0, 0, 0.2);
    border-radius: 4px;
    &-heading {
        min-height: 50px;
        font-size: 18px;
        line-height: 27px;
        padding: 10px 20px;
        background-color: rgba(0, 0, 0, 0.1);
        border-top: 1px solid rgba(0, 0, 0, 0.2);
        cursor: pointer;
        &-active {
            color: $primary;
        }
    }
    &-content {
        padding: 10px 20px;
        border-top: 1px solid rgba(0, 0, 0, 0.2);
        display: none;
        &-active {
            display: block;
        }
    }
    & > :first-child {
        border-top: none;
    }
}

Файл src/js/components/accordion.js:

import $ from '../core.js';

$.extensions.accordion = function() {
    for (let i = 0; i < this.length; i++) {
        $(this[i]).click(() => {
            // если клик по элементу, который уже открыт — ничего делать не нужно
            if ($(this[i]).hasClass('accordion-heading-active')) return;
            // находим тот элемент(ы) аккордеона, который сейчас открыт — и плавно
            // его закрываем; по окончании анимации — плавно открываем другой
            $(this[i]).nextSibling().parent().find('.accordion-content-active').slideUp(500, () => {
                // все открытые элементы закрыты, удаляем css-класс active
                $(this[i])
                    .parent()
                    .find('.accordion-heading-active')
                    .removeClass('accordion-heading-active');
                $(this[i])
                    .nextSibling()
                    .parent()
                    .find('.accordion-content-active')
                    .removeClass('accordion-content-active');
                // теперь открываем контент элемента, по которому кликнули
                $(this[i]).nextSibling().slideDown(500, 'block', () => {
                    // элемент открыт, добавляем css-класс active
                    $(this[i]).addClass('accordion-heading-active');
                    $(this[i]).nextSibling().addClass('accordion-content-active');
                });
            });
        });
    }
};

$('[data-accordion="true"] .accordion-heading').accordion();

Создадим разметку и посмотрим в действии:

<body>
    <div class="container">
        <div class="accordion mt-20 block-center" data-accordion="true">
            <div class="accordion-heading accordion-heading-active">
                Первый элемент
            </div>
            <div class="accordion-content accordion-content-active">
                Lorem ipsum dolor sit amet consectetur, adipisicing elit. Autem voluptatibus fuga est cupiditate
                harum repudiandae modi quia. Enim nobis recusandae totam error veniam quod consequatur nulla.
            </div>
            <div class="accordion-heading">
                Второй элемент
            </div>
            <div class="accordion-content">
                Lorem ipsum dolor sit amet consectetur, adipisicing elit. Autem voluptatibus fuga est cupiditate
                harum repudiandae modi quia. Enim nobis recusandae totam error veniam quod consequatur nulla.
            </div>
            <div class="accordion-heading">
                Третий элемент
            </div>
            <div class="accordion-content">
                Lorem ipsum dolor sit amet consectetur, adipisicing elit. Autem voluptatibus fuga est cupiditate
                harum repudiandae modi quia. Enim nobis recusandae totam error veniam quod consequatur nulla.
            </div>
        </div>
    </div>
</body>

23. Компонент «Carousel»

Файл src/sass/components/_carousel.scss:

.carousel {
    margin: 0 auto;
    position: relative;
    &-indicators {
        position: absolute;
        bottom: 0;
        left: 50%;
        transform: translateX(-50%);
        z-index: 15;
        display: flex;
        justify-content: center;
        padding: 10px;
        list-style: none;
        li {
            flex: 0 1 auto;
            width: 20px;
            height: 20px;
            margin-right: 5px;
            margin-left: 5px;
            cursor: pointer;
            background-color: $dark;
            border: 2px solid #fff;
            border-radius: 50%;
        }
        .active {
            background-color: $primary;
        }
    }
    &-window {
        width: 100%;
        height: 500px;
        position: relative;
        overflow: hidden;
    }
    &-slides {
        height: 100%;
        display: flex;
        transition: transform 0.5s;
    }
    &-item {
        height: 100%;
        img {
            width: 100%;
            height: 100%;
            object-fit: cover;
        }
    }
    &-prev, &-next {
        position: absolute;
        top: 0;
        bottom: 0;
        z-index: 1;
        display: flex;
        justify-content: center;
        align-items: center;
        width: 15%;
        color: #fff;
        opacity: 0.7;
        text-align: center;
        font-size: 40px;
        text-decoration: none;
        &:hover {
            opacity: 1;
            background-color: rgba(0, 0, 0, 0.2);
        }
    }
    &-prev {
        left: 0;
    }
    &-next {
        right: 0;
    }
}

Файл src/js/components/carousel.js:

import $ from '../core.js';

$.extensions.carousel = function() {
    for (let i = 0; i < this.length; i++) {
        /*
         * <div class="carousel"> width: 900px
         *     <div class="carousel-window"> width: 100%; height: 500px; overflow: hidden
         *         <div class="carousel-slides"> display: flex, style.width = 300%
         *             <div class="carousel-item">...</div>
         *             <div class="carousel-item">...</div>
         *             <div class="carousel-item">...</div>
         *         </div>
         *     </div>
         * </div>
         * Можно сказать, что carousel-window представляет собой окно просмотра 900x500px,
         * в этом окне просмотра виден одновременно только один кадр (слайд). Элемент
         * carousel-slides представляет из себя цепочку из трех кадров (как в кино). Эти
         * кадры выстроены по горизонтали благодаря display:flex. При клике на кнопки
         * next и prev — цепочка смещается влево, и в окне просмотра появляется очередной
         * кадр (слайд).
         */
        // окно просмотра слайдов
        const viewWindow = this[i].querySelector('.carousel-window');
        // ширина окна просмотра
        const viewWindowWidth = getComputedStyle(viewWindow).width;
        // все кадры (слайды)
        const allFrames = $(this[i]).find('.carousel-item');
        // цепочка кадров
        const frameChain = $(this[i]).find('.carousel-slides');
        // индикатор текущего кадра
        const indicators = $(this[i]).find('.carousel-indicators li');

        // ширина цепочки кадров будет равна ширине всех кадров, т.е. 300%
        frameChain.css('width', 100 * allFrames.length + '%');
        // все кадры должны быть одной ширины, равной ширине окна просмотра
        allFrames.css('width', viewWindowWidth);

        let offset = 0; // на сколько пикселей смещена цепочка кадров от начала
        let index = 0; // индекс кадра, который сейчас в окне просмотра

        $(this[i]).find('[data-slide-next]').click((e) => { // клик по кнопке next
            e.preventDefault();
            // в окне просмотра последний кадр, а следующий — первый (offset ноль)
            if (offset === parseInt(viewWindowWidth) * (allFrames.length - 1)) {
                offset = 0;
            } else { // если кадр не последний — показываем следующий
                offset = offset + parseInt(viewWindowWidth);
            }
            // сдвигаем цепочку кадров влево, чтобы показать в окне следующий кадр
            frameChain.css('transform', `translateX(-${offset}px)`);
            // в окне просмотра последний кадр, а следующий — первый (индекс ноль)
            if (index === allFrames.length - 1) {
                index = 0;
            } else { // если кадр не последний — показываем следующий
                index++;
            }

            indicators.removeClass('active');
            indicators.[index].classList.add('active');
        });

        $(this[i]).find('[data-slide-prev]').click((e) => { // клик по кнопке prev
            e.preventDefault();
            // в окне просмотра первый кадр, а следующий — последний (offset=900*2)
            if (offset === 0) {
                offset = parseInt(viewWindowWidth) * (allFrames.length - 1);
            } else { // если кадр не первый — показываем предыдущий
                offset = offset - parseInt(viewWindowWidth);
            }

            /*
             * Может показаться странным, что для кнопки next мы сдвигали цепочку влево,
             * и для кнопки prev опять сдвигаем влево. Допустим, что при клике на next
             * мы переходим от второго кадра к третьму. Перед переходом значение offset
             * равно 900, после перехода — 1800, значение увеличиватеся. Допустим теперь,
             * что при клике на prev мы переходим от третьего кадра ко второму. Перед
             * переходом значение offset равно 1800, после перехода — 900, значение
             * уменьшается. Но при просмотре создается ощущение, что сдвигается вправо.
             * 
             * Сдвигать цепочку вправо мы вообще не можем: у нас три кадра, изначально
             * первый из них в окне просмотра, справа еще два, а слева — пустота. Если
             * мы передадим функции translateX() положительное значение — увидим пустоту.
             * Сдвиг всегда происходит влево, просто offset увеличивается или уменьшается.
             */

            // сдвигаем цепочку кадров влево, чтобы показать в окне предыдущий кадр
            frameChain.css('transform', `translateX(-${offset}px)`);

            if (index === 0) {
                index = allFrames.length - 1;
            } else {
                index--;
            }

            indicators.removeClass('active');
            indicators.[index].classList.add('active');
        });

        indicators.click(event => {
            const slideGoto = event.target.getAttribute('data-slide-goto');

            index = parseInt(slideGoto);
            offset = parseInt(viewWindowWidth) * index;

            frameChain.css('transform', `translateX(-${offset}px)`);

            indicators.removeClass('active');
            indicators.[index].classList.add('active');
        }); 
    }
};

$('.carousel').carousel();

Создадим разметку и посмотрим в действии:

<body>
    <div class="container">
        <div class="carousel" id="example">
            <ol class="carousel-indicators">
                <li class="active" data-slide-goto="0"></li>
                <li data-slide-goto="1"></li>
                <li data-slide-goto="2"></li>
            </ol>
            <div class="carousel-window">
                <div class="carousel-slides">
                    <div class="carousel-item">
                        <img src="img/slide-1.jpg" alt="">
                    </div>
                    <div class="carousel-item">
                        <img src="img/slide-2.jpg" alt="">
                    </div>
                    <div class="carousel-item">
                        <img src="img/slide-3.jpg" alt="">
                    </div>
                </div>
            </div>
            <a href="#" class="carousel-prev" data-slide-prev>
                <span class="carousel-prev-icon">&lt;</span>
            </a>
            <a href="#" class="carousel-next" data-slide-next>
                <span class="carousel-next-icon">&gt;</span>
            </a>
        </div>
    </div>
</body>

24. Компонент «Carousel»

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

import $ from '../core.js';

$.extensions.carousel = function(autoplay = true) {

    /*
     * <div class="carousel"> width: 100% (от контейнера .container 900px)
     *     <div class="carousel-window"> width: 100% (от родителя 900px); height: 500px
     *         <div class="carousel-slides"> display: flex, style.width = 300% (2700px)
     *             <div class="carousel-item">...</div> style.width = 33.33333% (900px)
     *             <div class="carousel-item">...</div> style.width = 33.33333% (900px)
     *             <div class="carousel-item">...</div> style.width = 33.33333% (900px)
     *         </div>
     *     </div>
     * </div>
     * Можно сказать, что carousel-window представляет собой окно просмотра 900x500px,
     * в этом окне просмотра виден одновременно только один кадр (слайд). Элемент
     * carousel-slides представляет из себя цепочку из трех кадров (как в кино). Эти
     * кадры выстроены по горизонтали благодаря display:flex. При клике на кнопки
     * next и prev — цепочка смещается влево, и в окне просмотра появляется очередной
     * кадр (слайд).
     */

    class Slider {
        constructor(slider, autoplay = true) {
            this.slider = slider;
            // все кадры (слайды)
            this.allFrames = $(slider).find('.carousel-item');
            // цепочка кадров
            this.frameChain = $(slider).find('.carousel-slides');
            // кнопка «вперед»
            this.nextButton = $(slider).find('.carousel-next');
            // кнопка «назад»
            this.prevButton = $(slider).find('.carousel-prev');

            this.index = 0; // индекс кадра, который сейчас в окне просмотра
            this.length = this.allFrames.length; // сколько всего есть кадров
            this.autoplay = autoplay; // включить автоматическую прокрутку?
            this.paused = null; // чтобы можно было выключать автопрокрутку

            this.dotButtons = this.dots(); // создать индикатор текущего кадра
        }

        init() {
            // все кадры должны быть одной ширины, равной ширине окна просмотра;
            // если кадров три, то ширина каждого кадра будет 100/3 = 33.33333%
            // от ширины контейнера .carousel-slides, то есть 900 пикселей
            this.allFrames.css('width', 100/this.length + '%');
            // ширина цепочки кадров должна равна ширине всех кадров, то есть
            // 900*3 = 2700 пикселей; но удобнее задать в процентах от родителя,
            // если кадров три, то ширина контейнера кадров будет 100*3 = 300%
            this.frameChain.css('width', 100 * this.length + '%');

            this.nextButton.click(event => { // клик по кнопке «вперед»
                event.preventDefault();
                this.next();
            });

            this.prevButton.click(event => { // клик по кнопке «назад»
                event.preventDefault();
                this.prev();
            });

            this.dotButtons.click(event => { // клик по кнопке индикатора
                event.preventDefault();
                const index = this.dotButtons.indexOf(event.target);
                if (index === this.index) return;
                this.goto(index);
            });

            if (this.autoplay) { // включить автоматическую прокрутку?
                this.play();
                // когда мышь над слайдером — останавливаем автоматическую прокрутку
                $(this.slider).on('mouseenter', () => {
                    clearInterval(this.paused);
                });
                // когда мышь покидает пределы слайдера — опять запускаем прокрутку
                $(this.slider).on('mouseleave', () => {
                    this.play();
                });
            }
        }

        // перейти к кадру с индексом index
        goto(index) {
            // изменить текущий индекс...
            if (index > this.length - 1) {
                this.index = 0;
            } else if (index < 0) {
                this.index = this.length - 1;
            } else {
                this.index = index;
            }
            // ...и выполнить смещение
            this.move();
        }

        // перейти к следующему кадру
        next() {
            this.goto(this.index + 1);
        }

        // перейти к предыдущему кадру
        prev() {
            this.goto(this.index - 1);
        }

        // рассчитать и выполнить смещение
        move() {
            // на сколько нужно сместить, чтобы нужный кадр попал в окно
            const offset = 100/this.length * this.index;
            this.frameChain.css('transform', `translateX(-${offset}%)`);
            this.dotButtons.removeClass('active');
            this.dotButtons[this.index].classList.add('active');
        }

        // запустить автоматическую прокрутку
        play() {
            this.paused = setInterval(() => this.next(), 3000);
        }

        // создать индикатор текущего слайда
        dots() {
            const ol = document.createElement('ol');
            ol.classList.add('carousel-indicators');
            for (let i = 0; i < this.length; i++) {
                let li = document.createElement('li');
                if (i === 0) li.classList.add('active');
                ol.append(li);
            }
            this.slider.prepend(ol);
            return $(ol).children();
        }
    }

    for (let i = 0; i < this.length; i++) {
        new Slider(this[i], autoplay).init();
    }
};

$('.carousel').carousel();
<body>
    <div class="container">
        <div class="carousel">
            <div class="carousel-window">
                <div class="carousel-slides">
                    <div class="carousel-item">
                        <img src="img/slide-1.jpg" alt="">
                    </div>
                    <div class="carousel-item">
                        <img src="img/slide-2.jpg" alt="">
                    </div>
                    <div class="carousel-item">
                        <img src="img/slide-3.jpg" alt="">
                    </div>
                </div>
            </div>
            <a href="#" class="carousel-prev">
                <span class="carousel-prev-icon">&lt;</span>
            </a>
            <a href="#" class="carousel-next">
                <span class="carousel-next-icon">&gt;</span>
            </a>
        </div>
    </div>
</body>

25. Компонент «Carousel»

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

import $ from '../core.js';

$.extensions.carousel = function({autoplay = true, inFrame = 1, offset = 1} = {}) {

    /*
     * <div class="carousel"> width: 100% (от контейнера .container 900px)
     *     <div class="carousel-window"> width: 100% (от родителя 900px); height: 500px
     *         <div class="carousel-slides"> display: flex, style.width = 300% (2700px)
     *             <div class="carousel-item">...</div> style.width = 33.33333% (900px)
     *             <div class="carousel-item">...</div> style.width = 33.33333% (900px)
     *             <div class="carousel-item">...</div> style.width = 33.33333% (900px)
     *         </div>
     *     </div>
     * </div>
     */

    class Slider {
        constructor({slider, autoplay = true, inFrame = 1, offset = 1} = {}) {
            this.slider = slider;
            // кол-во элементов в одном кадре
            this.inFrame = inFrame;
            // на сколько элементов смещать
            this.offset = offset;

            // все элементы слайдера
            this.allItems = $(slider).find('.carousel-item');
            // сколько всего элементов
            this.itemCount = this.allItems.length;

            // все кадры слайдера
            this.allFrames = this.frames();
            // сколько всего кадров
            this.frameCount = this.allFrames.length;
            // индекс кадра в окне просмотра
            this.frameIndex = 0;

            // контейнер для элементов
            this.wrapper = $(slider).find('.carousel-slides');
            // кнопка «вперед»
            this.nextButton = $(slider).find('.carousel-next');
            // кнопка «назад»
            this.prevButton = $(slider).find('.carousel-prev');

            this.autoplay = autoplay; // включить автоматическую прокрутку?
            this.paused = null; // чтобы можно было выключать автопрокрутку

            this.dotButtons = this.dots(); // создать индикатор текущего кадра
        }

        init() {
            // если всего 10 элементов, то ширина одного элемента составляет 1/10
            // ширины контейнера .carousel-slides, то есть 100/10 = 10%
            this.allItems.css('width', 100 / this.itemCount + '%');
            // ширина контейнера должна вмещать все элементы: если элементов 10,
            // в окне просмотра 3 элемента, тогда ширина контейнера равна ширине
            // трех окон просмотра (300%) плюс ширина одного элемента 33.33333%,
            let wrapperWidth = this.itemCount / this.inFrame * 100;
            this.wrapper.css('width', wrapperWidth + '%');

            this.nextButton.click(event => { // клик по кнопке «вперед»
                event.preventDefault();
                this.next();
            });

            this.prevButton.click(event => { // клик по кнопке «назад»
                event.preventDefault();
                this.prev();
            });

            this.dotButtons.click(event => { // клик по кнопке индикатора
                event.preventDefault();
                const frameIndex = this.dotButtons.indexOf(event.target);
                if (frameIndex === this.frameIndex) return;
                this.goto(frameIndex);
            });

            if (this.autoplay) { // включить автоматическую прокрутку?
                this.play();
                // когда мышь над слайдером — останавливаем автоматическую прокрутку
                $(this.slider).on('mouseenter', () => {
                    clearInterval(this.paused);
                });
                // когда мышь покидает пределы слайдера — опять запускаем прокрутку
                $(this.slider).on('mouseleave', () => {
                    this.play();
                });
            }
        }

        // перейти к кадру с индексом index
        goto(index) {
            if (index > this.frameCount - 1) {
                this.frameIndex = 0;
            } else if (index < 0) {
                this.frameIndex = this.frameCount - 1;
            } else {
                this.frameIndex = index;
            }
            // ...и выполнить смещение
            this.move();
        }

        // перейти к следующему кадру
        next() {
            this.goto(this.frameIndex + 1);
        }

        // перейти к предыдущему кадру
        prev() {
            this.goto(this.frameIndex - 1);
        }

        // рассчитать и выполнить смещение
        move() {
            // на сколько нужно сместить, чтобы нужный кадр попал в окно
            const offset = 100 / this.itemCount * this.allFrames[this.frameIndex];
            this.wrapper.css('transform', `translateX(-${offset}%)`);
            this.dotButtons.removeClass('active');
            this.dotButtons[this.frameIndex].classList.add('active');
        }

        // запустить автоматическую прокрутку
        play() {
            this.paused = setInterval(() => this.next(), 2000);
        }

        // создать индикатор текущего кадра
        dots() {
            const ol = document.createElement('ol');
            ol.classList.add('carousel-indicators');
            for (let i = 0; i < this.frameCount; i++) {
                let li = document.createElement('li');
                if (i === 0) li.classList.add('active');
                ol.append(li);
            }
            this.slider.prepend(ol);
            return $(ol).children();
        }

        // индекс первого элемента каждого кадра
        frames() {
            // все наборы элементов, которые потенциально могут быть кадрами
            let temp = [];
            for (let i = 0; i < this.itemCount; i++) {
                // этот набор из this.inFrame элементов без пустого места
                if (this.allItems[i + this.inFrame - 1] !== undefined) {
                    temp.push(i);
                }
            }
            // с учетом того, что смещение this.offset может быть больше 1,
            // реальных кадров будет меньше или столько же
            let allFrames = [];
            for (let i = 0; i < temp.length; i = i + this.offset) {
                allFrames.push(temp[i]);
            }
            // в конце могут быть элементы, которые не могут образовать целый кадр (без пустоты),
            // такой кадр вообще не попадает в окно просмотра; вместо него показываем последний
            // целый кадр из числа потенциальных; при этом смещение будет меньше this.offset
            if (allFrames[allFrames.length - 1] !== temp[temp.length - 1]) {
                allFrames.push(temp[temp.length - 1]);
            }
            return allFrames;
        }
    }

    for (let i = 0; i < this.length; i++) {
        new Slider({
            slider: this[i],
            autoplay: autoplay,
            inFrame: inFrame,
            offset: offset
        }).init();
    }
};

$('.carousel').carousel();
<body>
    <div class="container">
        <div class="carousel">
            <div class="carousel-window">
                <div class="carousel-slides">
                    <div class="carousel-item">
                        <img src="img/slide-1.jpg" alt="">
                    </div>
                    <div class="carousel-item">
                        <img src="img/slide-2.jpg" alt="">
                    </div>
                    <div class="carousel-item">
                        <img src="img/slide-3.jpg" alt="">
                    </div>
                </div>
            </div>
            <a href="#" class="carousel-prev">
                <span class="carousel-prev-icon">&lt;</span>
            </a>
            <a href="#" class="carousel-next">
                <span class="carousel-next-icon">&gt;</span>
            </a>
        </div>
    </div>
</body>

26. Работа с сервером

Файл src/js/services/requests.js:

import $ from '../core.js';

$.extensions.get = async function (url, responseType = 'json') {
    let response = await fetch(url);
    if (!response.ok) {
        throw new Error(`Could not fetch ${url}, status ${response.status}`);
    }
    switch(responseType) {
        case 'json':
            return await response.json();
        case 'text':
            return await response.text();
        case 'blob':
            return await response.blob();
        default:
            return response.body;
    }
};

$.extensions.post = async function (url, data, options = {}, responseType = 'json') {
    let settings = {
        method: 'POST',
        body: data
    };
    for (let key in options) {
        settings[key] = options[key];
    }

    let response = await fetch(url, settings);

    if (!response.ok) {
        throw new Error(`Could not fetch ${url}, status ${response.status}`);
    }

    switch(responseType) {
        case 'json':
            return await response.json();
        case 'text':
            return await response.text();
        case 'blob':
            return await response.blob();
        default:
            return response.body;
    }
};

Для тестирования создадим php-скрипт dist/server.php:

<?php
$data = [];
if ($_SERVER['REQUEST_METHOD'] == 'GET') {
    $data = [
        'method' => 'GET',
        'message' => 'Ответ на GET-запрос',
        'data' => ['response', 'get', 'request']
    ];
}
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
    $data = [
        'method' => 'POST',
        'message' => 'Ответ на POST-запрос',
        'data' => ['response', 'post', 'request'],
        'body' => json_decode(file_get_contents('php://input'))
    ];
}
header('Content-Type: application/json');
echo json_encode($data);

Выполним GET-запрос на сервер и посмотрим ответ:

$().get('server.php').then(response => console.log(response));
{
    "method": "GET",
    "message": "Ответ на GET-запрос",
    "data": ["response","get","request"]
}

Выполним POST-запрос на сервер и посмотрим ответ:

let user = {
    name: 'Сергей',
    surname: 'Иванов'
};
let data = JSON.stringify(user);
let options = {
    headers: {'Content-Type': 'application/json;charset=utf-8'}
};
$().post('server.php', data, options).then(response => console.log(response));
{
    "method": "POST",
    "message": "Ответ на POST-запрос",
    "data": ["response","post","request"],
    "body": {
        "name": "Сергей",
        "surname": "Иванов"
    }
}

Использование библиотеки

Для начала нам нужно получить файлы библиотеки, для этого выполняем команду gulp prod, чтобы получить минифицированные версии src/style.css и src/script.js. Создаем директорию test, внтутри нее еще три директории — css, js и img. Минифицированный src/style.css кладем в директорию test/css под именем lib.css, минифицированный src/script.css кладем в директорию test/js под именем lib.js. В директорию test/img кладем любые картинки слайдов и карточек, найденные в интернете.

Файл test/index.html:

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Магазин инструментов</title>
    <link rel="stylesheet" href="css/lib.css">
    <link rel="stylesheet" href="css/style.css">
</head>
<body>
    <header>
        <div class="container">
            <p>Магазин инструментов</p>
        </div>
    </header>

    <div class="container">
        <h1 class="text-center">Лучшие дрели по лучшим ценам</h1>
    </div>

    <!-- Слайдер -->
    <section class="promo">
        <div class="container">
            <h2 class="text-center mt-20">Это предложение именно для вас</h2>
            <div class="carousel">
                <div class="carousel-window">
                    <div class="carousel-slides">
                        <div class="carousel-item">
                            <img src="img/slide-1.jpg" alt="">
                        </div>
                        <div class="carousel-item">
                            <img src="img/slide-2.jpg" alt="">
                        </div>
                        <div class="carousel-item">
                            <img src="img/slide-3.jpg" alt="">
                        </div>
                        <div class="carousel-item">
                            <img src="img/slide-4.jpg" alt="">
                        </div>
                        <div class="carousel-item">
                            <img src="img/slide-5.jpg" alt="">
                        </div>
                    </div>
                </div>
                <a href="#" class="carousel-prev">
                    <span class="carousel-prev-icon">&lt;</span>
                </a>
                <a href="#" class="carousel-next">
                    <span class="carousel-next-icon">&gt;</span>
                </a>
            </div>
        </div>
    </section>

    <!-- Карточки -->
    <section class="goods">
        <div class="container">
            <h2 class="text-center">У нас самый богатый выбор дрелей!</h2>
            <div class="cards">
                <div class="card">
                    <img class="card-img" src="img/card-1.jpg" alt="">
                    <div class="card-body">
                        <div class="card-title">Дрель раз</div>
                        <p class="card-text">
                            Lorem ipsum dolor, sit amet consectetur adipisicing elit. Magnam, dolore,
                            soluta dicta sapiente error amet, asperiores molestias modi atque minus.
                        </p>
                        <button class="btn btn-primary" data-modal="true" data-target="#modal-one">
                            Добавить в корзину
                        </button>
                    </div>
                </div>
                <div class="card">
                    <img class="card-img" src="img/card-2.jpg" alt="">
                    <div class="card-body">
                        <div class="card-title">Дрель два</div>
                        <p class="card-text">
                            Lorem ipsum dolor sit amet consectetur, adipisicing elit. Facere, velit a
                            ut suscipit voluptates, quidem aperiam ab ratione aspernatur repudiandae.
                        </p>
                        <button class="btn btn-primary" data-modal="true" data-target="#modal-two">
                            Добавить в корзину
                        </button>
                    </div>
                </div>
                <div class="card">
                    <img class="card-img" src="img/card-3.jpg" alt="">
                    <div class="card-body">
                        <div class="card-title">Дрель три</div>
                        <p class="card-text">
                            Lorem ipsum dolor sit amet consectetur adipisicing elit. Autem, tempore aut
                            corrupti dicta delectus ducimus, molestias quas reprehenderit sed voluptatem.
                        </p>
                        <button class="btn btn-danger">Товар закончился</button>
                    </div>
                </div>
                <div class="card">
                    <img class="card-img" src="img/card-4.jpg" alt="">
                    <div class="card-body">
                        <div class="card-title">Дрель четыре</div>
                        <p class="card-text">
                            Lorem ipsum dolor sit amet consectetur adipisicing elit. Autem, tempore autem
                            delectus ducimus, molestias quas reprehenderit sed voluptatem earum.
                        </p>
                        <button class="btn btn-danger">Товар закончился</button>
                    </div>
                </div>
                <div class="card">
                    <img class="card-img" src="img/card-5.jpg" alt="">
                    <div class="card-body">
                        <div class="card-title">Дрель пять</div>
                        <p class="card-text">
                            Lorem ipsum dolor sit amet consectetur adipisicing elit. Autem, tempore aut
                            corrupti dicta delectus ducimus, molestias quas reprehenderit sed voluptatem.
                        </p>
                        <button class="btn btn-danger">Товар закончился</button>
                    </div>
                </div>
            </div>
        </div>
    </section>

    <!-- Аккордеон -->
    <section class="advantages">
        <div class="container">
            <h2 class="text-center">Наши преимущества</h2>
            <div class="accordion mt-20 block-center" data-accordion="true">
                <div class="accordion-heading accordion-heading-active">
                    Преимущество раз
                </div>
                <div class="accordion-content accordion-content-active">
                    Lorem ipsum dolor sit amet consectetur adipisicing elit. Harum ratione fugiat iusto,
                    magnam ducimus tempora saepe odio nemo alias reiciendis, neque quaerat commodi
                    expedita ex sapiente repudiandae earum consequuntur iure beatae minus nisi quidem.
                </div>
                <div class="accordion-heading">
                    Преимущество два
                </div>
                <div class="accordion-content">
                    Lorem ipsum dolor sit amet consectetur adipisicing elit. Qui voluptatem possimus
                    nesciunt sint earum temporibus libero deserunt laborum dolorum unde? Ipsum quas
                    ea quaerat dicta tempora asperiores itaque eos iure laudantium et, dolores eum.
                </div>
                <div class="accordion-heading">
                    Преимущество три
                </div>
                <div class="accordion-content">
                    Lorem ipsum dolor sit, amet consectetur adipisicing elit. Ex, nostrum? Numquam,
                    voluptatibus nobis? Assumenda rem et consequuntur temporibus consequatur, eveniet
                    tenetur nostrum laborum aperiam atque, veritatis quam facilis saepe.
                </div>
            </div>
        </div>
    </section>

    <!-- Вкладки -->
    <section class="steps">
        <div class="container">
            <h2 class="text-center">Как стать счастливым обладателем дрели?</h2>

            <div class="tab" data-tab="true">
                <div class="tab-panel" data-tabpanel>
                    <div class="tab-item tab-item-active">Шаг раз</div>
                    <div class="tab-item">Шаг два</div>
                    <div class="tab-item">Шаг три</div>
                </div>
                <div class="tab-content tab-content-active">
                    <h3>Шаг раз</h3>
                    Lorem ipsum dolor sit amet consectetur adipisicing elit. Recusandae quas expedita
                    quibusdam voluptate laboriosam iure totam quam ut perspiciatis in nemo vitae animi
                    consequatur impedit explicabo, pariatur officia obcaecati voluptatum?
                </div>
                <div class="tab-content">
                    <h3>Шаг два</h3>
                    Lorem ipsum dolor sit, amet consectetur adipisicing elit. Mollitia consequuntur
                    labore repudiandae molestiae a nemo quisquam ipsum nobis corrupti. Cumque consectetur
                    consequuntur ratione omnis. Dicta, ducimus et quo eius blanditiis quis, reiciendis.
                </div>
                <div class="tab-content">
                    <h3>Шаг три</h3>
                    Lorem ipsum dolor sit amet consectetur adipisicing elit. Rem placeat accusamus,
                    eligendi ratione mollitia quasi, culpa tenetur et alias laudantium qui, voluptatibus
                    in! Quo quae omnis in, architecto sequi velit error, laborum quas cum soluta, quia.
                </div>
            </div>
        </div>
    </section>

    <!-- Модальные окна -->
    <div class="modal" id="modal-one">
        <div class="modal-dialog">
            <div class="modal-content">
                <button class="close" data-close>
                    <span>&times;</span>
                </button>
                <div class="modal-header">
                    <div class="modal-title">
                        Дрель раз
                    </div>
                </div>
                <div class="modal-body">
                    Товар добавлен в корзину. Можете оформить заказ или продолжить выбор.
                </div>
                <div class="modal-footer">
                    <button class="btn btn-primary" data-close>Продолжить</button>
                    <button class="btn btn-success">Оформить заказ</button>
                </div>
            </div>
        </div>
    </div>

    <div class="modal" id="modal-two">
        <div class="modal-dialog">
            <div class="modal-content">
                <button class="close" data-close>
                    <span>&times;</span>
                </button>
                <div class="modal-header">
                    <div class="modal-title">
                        Дрель два
                    </div>
                </div>
                <div class="modal-body">
                    Товар добавлен в корзину. Можете оформить заказ или продолжить выбор.
                </div>
                <div class="modal-footer">
                    <button class="btn btn-primary" data-close>Продолжить</button>
                    <button class="btn btn-success">Оформить заказ</button>
                </div>
            </div>
        </div>
    </div>

    <footer>
        <div class="container">
            <p class="text-center">&copy; All copyrights reserved</p>
        </div>
    </footer>
    <script src="js/lib.js"></script>
    <script src="js/script.js"></script>
</body>
</html>

Файл test/css/style.css:

section {
    margin-bottom: 20px;
}
h1 {
    font-size: 36px;
    margin: 20px 0;
}
h2 {
    font-size: 24px;
    margin: 20px 0;
}
header, footer {
    background-color: #444;
    color: #fff;
    padding: 15px 0;
}
header {
    font-size: 26px;
}
footer {
    font-size: 12px;
}

Файл test/js/script.js:

// создание модального окна «на лету»
let data = {
    content: {
        title: 'Только три дня',
        body: 'Обратите внимание, предложение ограничено! Рекламная акция заканчивается через три дня.'
    },
    buttons: [
        {classes: ['btn', 'btn-danger'], text: 'Закрыть', close: true},
    ]
};
// чтобы открыть модальное окно, на странице должна существовать кнопка, которая его открывает;
// а мы хотим открыть модальное окно без такой кнопки, через 10 секунд пребывания на странице;
// тут явная недоработка нашей библиотеки, но будем работать с тем, что есть — создадим кнопку
let button = document.createElement('button');
button.setAttribute('data-target', '#campaing-modal');
button.style.display = 'none';
document.body.append(button);
setTimeout(() => $(button).createModal(data), 10000);

Демо-сайт здесь и здесь, исходные коды здесь.

Поиск: 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.