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

02.07.2021

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

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

Разработка библиотеки

1. Структура проекта

Общая структура директорий проекта:

[project]
    [dist]
        index.html
        script.js
        style.css
    [src]
        [js]
            [lib]
                [modules]
                    display.js
                core.js
                lib.js
            main.js
        [sass]
            style.scss
        index.html
    gulpfile.js
    package.json

2. Сборка проекта

Файл gulpfile.js для сборки проекта:

'use strict';

const gulp = require('gulp');
const webpack = require('webpack-stream');
const browsersync = require('browser-sync');
const sass = require('gulp-sass');
const autoprefixer = require('autoprefixer');
const cleanCSS = require('gulp-clean-css');
const postcss = require('gulp-postcss');

const dist = './dist';

gulp.task('copy-html', () => {
    return gulp.src('./src/index.html')
        .pipe(gulp.dest(dist))
        .pipe(browsersync.stream());
});

gulp.task('build-sass', () => {
    return gulp.src('./src/sass/style.scss')
        .pipe(sass().on('error', sass.logError))
        .pipe(gulp.dest(dist))
        .pipe(browsersync.stream());
});

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);
});

gulp.task('watch', () => {
    browsersync.init({
        server: './dist/',
        port: 4000,
        notify: true
    });
    
    gulp.watch('./src/index.html', gulp.parallel('copy-html'));
    gulp.watch('./src/js/**/*.js', gulp.parallel('build-js'));
    gulp.watch('./src/sass/**/*.scss', gulp.parallel('build-sass'));
});

gulp.task('build', gulp.parallel('copy-html', 'build-js', 'build-sass'));

gulp.task('prod', () => {
    gulp.src('./src/sass/style.scss')
        .pipe(sass().on('error', sass.logError))
        .pipe(postcss([autoprefixer()]))
        .pipe(cleanCSS())
        .pipe(gulp.dest(dist));

    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));
});

gulp.task('default', gulp.parallel('watch', 'build'));

Устанавливаем все необходимые пакеты из package.json:

> cd /path/to/project
> npm install

3. Ядро библиотеки

Ядро нашей библиотеки, файл src/lib/core.js:

const $ = function (selector) {
    return new init(selector);
};

const init = function(selector) {
    if (!selector) return;
    const elements = document.querySelectorAll(selector);
    Object.assign(this, elements);
    this.length = elements.length;
};

// Мы используем конструктор new F(), чтобы создать объект; в этот объект добавляем все элементы,
// которые соответствуют css-селектору selector. Мы знаем, что свойство F.prototype конструктора
// применяется в качестве прототипа этого объекта. Но мы планируем дальше расширять библиотеку,
// поэтому [[Prototype]] этого объекта и свойство extensions будут указывать на один объект.
// Свойство extensions нам нужно, чтобы через него добавлять в прототип объекта новые методы.
init.prototype = $.extensions = {};

window.$ = $;

export default $;

Файл src/lib/lib.js нужен для того, чтобы расширять ядро библиотеки:

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

export default $;

4. Показать/скрыть элементы

Модуль src/lib/modules/display.js позволяет показать/скрыть элементы:

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

$.extensions.show = function(display = 'block') {
    for (let i = 0; i < this.length; i++) {
        if (this[i].style) {
            this[i].style.display = display;
        }
    }
    return this;
};

$.extensions.hide = function() {
    for (let i = 0; i < this.length; i++) {
        if (this[i].style) {
            this[i].style.display = 'none';
        }
    }
    return this;
};

$.extensions.toggle = function(display = 'block') {
    for (let i = 0; i < this.length; i++) {
        if (this[i].style) {
            if (this[i].style.display === 'none') {
                this[i].style.display = display;
            } else {
                this[i].style.display = 'none';
            }
        }
    }
    return this;
};

Минимальный функционал у нас есть, так что давайте опробуем его в работе. Добавим несколько div-блоков на страницу index.html:

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Library</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div>1</div>
    <div class="active">2</div>
    <div>3</div>

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

И в файле src/js/main.js импортируем нашу библиотеку и скроем div-элемент с css-классом active:

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

$('.active').toggle();

5. Добавить/удалить css-классы

Добавим еще один модуль src/lib/modules/display.js, который позволит добавлять/удалять css-классы для элементов:

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

$.extensions.addClass = function (...classNames) {
    // после rest-оператора ... переменная classNames это массив ["one", "two"]
    for (let i = 0; i < this.length; i++) {
        // после spread-оператора ... функция получит две строки: "one", "two"
        this[i].classList.add(...classNames);
    }
    return this;
};

$.extensions.removeClass = function (...classNames) {
    for (let i = 0; i < this.length; i++) {
        this[i].classList.add(...classNames);
    }
    return this;
};

$.extensions.toggleClass = function (className) {
    for (let i = 0; i < this.length; i++) {
        this[i].classList.toggle(className);
    }
    return this;
};

$.extensions.hasClass = function (className) {
    for (let i = 0; i < this.length; i++) {
        if (this[i].classList.contains(className)) {
            return true;
        }
    }
    return false;
};

В файле src/lib/lib.js импортируем новый модуль библиотеки:

import $ from './core.js';
import './modules/display.js';
import './modules/classes.js';

export default $;

Попробуем добавить и удалить css-класс(ы) для div-элементов:

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

$('div:not(.active)').addClass('hello', 'world');
$('.active').removeClass('active');

И вот что получилось в итоге — вроде все отработало правильно:

<body>
    <div class="hello world">1</div>
    <div class>2</div>
    <div class="hello world">3</div>

    <script src="script.js"></script>
</body>

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

Добавим еще один модуль src/lib/modules/handlers.js, который позволит добавлять/удалять обработчики событий:

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

$.extensions.on = function (eventName, callback) {
    for (let i = 0; i < this.length; i++) {
        this[i].addEventListener(eventName, callback);
    }
    return this;
};

$.extensions.off = function (eventName, callback) {
    for (let i = 0; i < this.length; i++) {
        this[i].removeEventListener(eventName, callback);
    }
    return this;
};

$.extensions.click = function (handler) {
    for (let i = 0; i < this.length; i++) {
        if (handler) {
            // передан handler — назначаем его как обработчик события
            this[i].addEventListener('click', handler);
        } else {
            // в противном случае вызываем событие клика на элементе
            this[i].click();
        }
    }
    return this;
};

$.extensions.focus = function (handler) {
    for (let i = 0; i < this.length; i++) {
        if (handler) {
            this[i].addEventListener('focus', handler);
        } else {
            this[i].focus();
        }
    }
    return this;
};

$.extensions.blur = function (handler) {
    for (let i = 0; i < this.length; i++) {
        if (handler) {
            this[i].addEventListener('blur', handler);
        } else {
            this[i].blur();
        }
    }
    return this;
};

В файле src/lib/lib.js импортируем новый модуль библиотеки:

import $ from './core.js';
import './modules/display.js';
import './modules/classes.js';
import './modules/handlers.js';

export default $;

Попробуем добавить обработчик события для div-элемента active:

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

var clickHandler = function() {
    console.log('Клик по элементу')
};

$('.active').on('click', clickHandler);

7. Рефакторинг ядра

Давайте немного усложним задачу. Добавим на страницу кнопку, назначим для нее обработчик события, а обработчик добавит для кнопки css-класс. Но для этого нужно сначала создать этот css-класс, поэтому редактируем src/sass/style.scss.

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}
button {
    width: 150px;
    height: 50px;
    text-align: center;
}
.active {
    background-color: skyblue;
    color: red;
}
<body>
    <button>Click me!</button>
    <script src="script.js"></script>
</body>
import $ from './lib/lib.js';

$('button').on('click', function() {
    $(this).addClass('active');
});
Uncaught DOMException: Failed to execute 'querySelectorAll' on 'Document':
'[object HTMLButtonElement]' is not a valid selector.

Причина ошибки понятна. Внутри функции обработчика события this ссылается на тот элемент, на котором висит обработчик, то есть на элемент кнопки. Но функция $() ожидает в качестве параметра селектор, а не объект HTMLElement. Так что давайте доработаяем ядро нашей библиотеки, чтобы избежать ошибки.

const $ = function (selector) {
    return new init(selector);
};

const init = function(selector) {
    if (!selector) return;
    // если передана строка — это css-селектор элементов
    if (typeof selector === "string") {
        const elements = document.querySelectorAll(selector);
        Object.assign(this, elements);
        this.length = elements.length;
        return;
    }
    // если передан объект — это один элемент или коллекция
    if (typeof selector === "object") {
        if (selector instanceof HTMLElement) {
            this[0] = selector
            this.length = 1;
            return;
        }
        if (selector instanceof NodeList) {
            Object.assign(this, selector);
            this.length = selector.length;
            return;
        }
    }
};

init.prototype = $.extensions = {};

window.$ = $;

export default $;

8. Содержимое элементов

Добавим еще один модуль src/lib/modules/content.js, который позволяет изменять содержимое элементов:

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

$.extensions.html = function (content) {
    for (let i = 0; i < this.length; i++) {
        if (content !== undefined) {
            this[i].innerHTML = content;
        } else {
            return this[i].innerHTML;
        }
    }
    return this;
};

$.extensions.text = function (content) {
    for (let i = 0; i < this.length; i++) {
        if (content !== undefined) {
            this[i].textContent = content;
        } else {
            return this[i].textContent;
        }
    }
    return this;
};

$.extensions.empty = function () {
    this.text('');
    return this;
};

9. Элементы коллекции

Добавим еще один модуль src/lib/modules/collection.js, который позволяет работать с элементами коллекции:

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

// Возвращает элемент коллекции по индексу
$.extensions.valueOf = function (i) {
    if (i >= this.length) return this.zero();
    const item = this[i];
    this.zero();
    this[0] = item;
    this.length = 1;
    return this;
};

// Возвращает первый элемент коллекции
$.extensions.first = function () {
    return this.valueOf(0);
};

// Возвращает последний элемент коллекции
$.extensions.last = function () {
    return this.valueOf(this.length - 1);
};

// Возвращает массив элементов коллекции
$.extensions.toArray = function () {
    return this._(this);
};

// Удаляет все элементы из коллекции
$.extensions.zero = function () {
    for (let i = 0; i < this.length; i++) {
        delete this[i];
    }
    this.length = 0;
    return this;
};

Опробуем новые методы в работе:

<body>
    <button>Кнопка</button>
    <button>Кнопка</button>
    <button>Кнопка</button>
</body>
$('button').addClass('button').first().click(function() {
    $(this).addClass('active').text('Клик по первой кнопке');
});
В модуле collection.js есть несколько вспомогательных методов, которые здесь не упоминаются, но которые используются в коде.

10. Рефакторинг ядра

Сейчас функция $(…) может принимать селектор элементов, объект NodeList или объект HTMLElement. Давайте доработаем ядро, чтобы функция $(…) могда еще принимать и объект $(…) — это нам потребуется для дальнейшей работы над библиотекой.

const $ = function (selector) {
    return new init(selector);
};

const init = function (selector) {
    let elements = this._(selector);
    for (let i = 0; i < elements.length; i++) {
        this[i] = elements[i];
    }
    this.length = elements.length;
};

init.prototype = $.extensions = {};

init.prototype._ = function(selector) {
    let elements = [];
    // selector может быть селектором элементов, объектом NodeList или объектом $(…)
    if (typeof selector === "string") {
        let elems = document.querySelectorAll(selector);
        for (let i = 0; i < elems.length; i++) {
            elements[i] = elems[i];
        }
    }
    if (typeof selector === "object") {
        if (selector instanceof NodeList) {
            for (let i = 0; i < selector.length; i++) {
                elements[i] = selector[i];
            }
        }
        if (selector instanceof init) {
            for (let i = 0; i < selector.length; i++) {
                elements[i] = selector[i];
            }
        }
        if (selector instanceof HTMLElement) {
            elements[0] = selector;
        };
    }
    return elements;
}

window.$ = $;

export default $;

11. Элементы коллекции

Добавим еще два метода, которые позволят находить индекс элемента в коллекции и находить элементы среди потомков элементов коллекции по css-селектору.

// Возвращает индекс элемента в коллекции; подразумевается, что объект $(…) содержит коллекцию
// однотипных элементов (например, элементы слайдера) и нужно найти индекс отдельного элемента
$.extensions.indexOf = function (selector) {
    let elements = this._(selector);
    for (let i = 0; i < this.length; i++) {
        if (this[i] == elements[0]) return i;
    }
    return -1;
};

// Находит элементы среди потомков элементов коллекции по css-селектору и возвращает
// объект $(…) с новой коллекцией
$.extensions.find = function (selector) {
    let items = this.toArray(); // массив элементов исходной коллекции
    this.zero(); // будем записывать в this новую коллекцию
    let counter = 0;
    for (let i = 0; i < items.length; i++) {
        let children = items[i].querySelectorAll(selector);
        for (let j = 0; j < children.length; j++) {
            if (!this.oneExist(children[j])) { // исключаем дубли
                this[counter++] = children[j]; // записываем в this новую коллекцию
                this.length = counter;
            }
        }
    }
    return this;
};

Примеры использования методов index() и find():

<body>
    <div>
        <button>Кнопка</button>
        <button>Кнопка</button>
        <button>Кнопка</button>
    </div>
</body>
// находим индекс той кнопки в коллекции, по которой был клик
let buttons = $('div > button');
buttons.click(function() {
    console.log(buttons.index(this));
});
<body>
    <section>
        <div>1</div>
        <div>
            <div class="some"></div>
            <div class="more"></div>
            <div class="some"></div>
            <div class="more"></div>
            <div class="some"></div>
        </div>
        <div>3</div>
    </section>
</body>
// находим потомков div с css-классом some
console.log($('section > div').find('.some'));

12. Элементы коллекции

Продолжаем добавлять методы в библиотеку — метод filter() позволяет отфильтровать коллекцию, методы plus() и minus() добавляют/удаляют элементы коллекции.

// Фильтрует элементы коллекции по css-селектору и возвращает объект $(…) с новой коллекцией
$.extensions.filter = function (selector) {
    let elements = this._(selector);
    if (elements.length === 0) {
        return this; // если фильтр пустой, ничего не делаем
    }
    let items = this.toArray(); // массив элементов исходной коллекции
    this.zero(); // будем записывать в this новую коллекцию
    let counter = 0;
    for (let i = 0; i < items.length; i++) {
        for (let j = 0; j < elements.length; j++) {
            if (items[i] == elements[j]) {
                this[counter++] = items[i]; // записываем в this новую коллекцию
            }
        }
    }
    this.length = counter;
    return this;
};

// Добавляет к коллекции элементы по css-селектору и возвращает объект $(…) с новой коллекцией
$.extensions.plus = function (selector) {
    let elements = this._(selector);
    let counter = this.length;
    for (let i = 0; i < elements.length; i++) {
        this[counter++] = elements[i];
    }
    this.length = counter;
    return this;
};

// Удаляет из коллекции элементы по css-селектору и возвращает объект $(…) с новой коллекцией
$.extensions.minus = function (selector) {
    let oneItems = this.toArray(); // массив элементов исходной коллекции
    let twoItems = this._(selector); // массив элементов второй коллекции

    this.zero(); // будем записывать в this новую коллекцию
    let counter = 0, add;
    for (let i = 0; i < oneItems.length; i++) {
        add = true; // добавлять или нет элемент в this?
        for (let j = 0; j < twoItems.length; j++) {
            // элемент исходной коллекции есть во второй — нам не подходит
            if (oneItems[i] == twoItems[j]) add = false;
        }
        if (add) this[counter++] = oneItems[i];
    }
    this.length = counter;
    return this;
};

13. Элементы коллекции

Продолжаем добавлять методы в библиотеку — методы nextSibling(), prevSibling() и siblings(), позволяют получить смежные элементы.

$.extensions.nextSibling = function () {
    let items = this.toArray(); // массив элементов исходной коллекции
    this.zero(); // будем записывать в this новую коллекцию
    let counter = 0, next;
    for (let i = 0; i < items.length; i++) {
        next = items[i].nextElementSibling;
        if (next) {
            this[counter++] = next;
        }
    }
    this.length = counter;
    return this;
};

$.extensions.prevSibling = function () {
    let items = this.toArray(); // массив элементов исходной коллекции
    this.zero(); // будем записывать в this новую коллекцию
    let counter = 0, prev;
    for (let i = 0; i < items.length; i++) {
        prev = items[i].previousElementSibling;
        if (prev) {
            this[counter++] = prev;
        }
    }
    this.length = counter;
    return this;
};

$.extensions.siblings = function () {
    let items = this.toArray(); // массив элементов исходной коллекции
    this.zero(); // будем записывать в this новую коллекцию
    let counter = 0, parent, children;
    for (let i = 0; i < items.length; i++) {
        let parent = items[i].parentElement;
        if (parent) {
            children = parent.children;
            for (let j = 0; j < children.length; j++) {
                if (children[j] == items[i]) continue;
                if (this.oneExist(children[j])) continue; // исключаем дубли
                this[counter++] = children[j];
                this.length = counter;
            }
        }
    }
    return this;
};

14. Элементы коллекции

Продолжаем добавлять методы в библиотеку — методы parent() и children() позволяют получить родителя и дочерние элементы.

$.extensions.children = function () {
    let items = this.toArray(); // массив элементов исходной коллекции
    this.zero(); // будем записывать в this новую коллекцию
    let counter = 0, children;
    for (let i = 0; i < items.length; i++) {
        children = items[i].children;
        for (let j = 0; j < children.length; j++) {
            this[counter++] = children[j];
        }
    }
    this.length = counter;
    return this;
};

$.extensions.parent = function () {
    let items = this.toArray(); // массив элементов исходной коллекции
    this.zero(); // будем записывать в this новую коллекцию
    let counter = 0, parent;
    for (let i = 0; i < items.length; i++) {
        parent = items[i].parentElement;
        if (parent && !this.oneExist(parent)) {
            this[counter++] = parent;
            this.length = counter;
        }
    }
    return this;
};

$.extensions.firstChild = function () {
    let items = this.toArray(); // массив элементов исходной коллекции
    this.zero(); // будем записывать в this новую коллекцию
    let counter = 0, firstChild;
    for (let i = 0; i < items.length; i++) {
        firstChild = items[i].firstElementChild;
        if (firstChild) {
            this[counter++] = firstChild;
        }
    }
    this.length = counter;
    return this;
};

$.extensions.lastChild = function () {
    let items = this.toArray(); // массив элементов исходной коллекции
    this.zero(); // будем записывать в this новую коллекцию
    let counter = 0, lastChild;
    for (let i = 0; i < items.length; i++) {
        lastChild = items[i].lastElementChild;
        if (lastChild) {
            this[counter++] = lastChild;
        }
    }
    this.length = counter;
    return this;
};

15. Добавление стилей

Добавим еще один модуль src/lib/modules/styles.js, который позволяет изменять инлайн-стили элементов:

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

$.extensions.css = function(name, value = '') {
    for (let i = 0; i < this.length; i++) {
        if (typeof name === "object") {
            for (let prop in name) {
                this[i].style[prop] = name[prop];
            }
        }
        if (typeof name === "string") {
            this[i].style[name] = value;
        }
    }
    return this;
};
$('div').css({
    'border': '2px solid red',
    'background-color': '#ddddff'
}).css('color', 'red');

16. Анимация элементов

У нас уже есть методы show() и hide(), которые позволяют показать или скрыть элемент(ы). Но хотелось бы иметь методы, которые могли бы это сделать плавно. Так что добавим еще один модуль src/lib/modules/effects.js.

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

$.extensions.animate = function (duration, callback, complete) {
    let startTime = null;

    // вспомогательная функция, которую будем передавать в requestAnimationFrame()
    function animate(currentTime) {
        // первый вызов этой функции
        if (!startTime) {
            startTime = currentTime;
        }
        // прогресс анимации, от 0 до 1
        let progress = (currentTime - startTime) / duration;
        if (progress > 1) progress = 1;

        // при каждом вызове этой функции, она вызывает callback()
        callback(progress);

        if (progress < 1) {
            requestAnimationFrame(animate); // анимация еще не завершилась
        } else {
            if (typeof complete === "function") complete(); // анимация завершилась
        }
    }

    return animate;
};

$.extensions.fadeIn = function (duration = 1000, display = 'block', complete) {
    let animate;
    for (let i = 0; i < this.length; i++) {
        // если элемент не скрыт, то ничего делать не нужно
        if (getComputedStyle(this[i]).display !== 'none') continue;
        this[i].style.display = display;
        animate = this.animate(
            // общая продолжительность анимации в миллисекундах
            duration,
            // функция будет вызвана несколько раз, progress изменяется от 0 до 1
            progress => this[i].style.opacity = progress,
            // после завершения анимации будет вызвана функция complete()
            complete
        );
        requestAnimationFrame(animate);
    }
};

$.extensions.fadeOut = function (duration = 1000, complete) {
    let animate;
    for (let i = 0; i < this.length; i++) {
        // если элемент уже скрыт, то ничего делать не нужно
        if (getComputedStyle(this[i]).display === 'none') continue;
        animate = this.animate(
            // общая продолжительность анимации в миллисекундах
            duration,
            // функция будет вызвана несколько раз, progress изменяется от 0 до 1
            progress => {
                this[i].style.opacity = 1 - progress;
                if (progress === 1) this[i].style.display = 'none';
            },
            // после завершения анимации будет вызвана функция complete()
            complete
        );
        requestAnimationFrame(animate);
    }
};

$.extensions.fadeToggle = function (duration = 1000, display = 'block', complete) {
    for (let i = 0; i < this.length; i++) {
        if (getComputedStyle(this[i]).display === 'none') {
            $(this[i]).fadeIn(duration, display, complete);
        } else {
            $(this[i]).fadeOut(duration, complete);
        }
    }
};

$.extensions.slideUp = function (duration = 1000, complete) {
    let animate, height, paddingTop, paddingBottom, borderTopWidth, borderBottomWidth, marginTop, marginBottom;
    for (let i = 0; i < this.length; i++) {
        // если элемент уже скрыт, то ничего делать не нужно
        if (getComputedStyle(this[i]).display === 'none') continue;
        this[i].style.overflow = 'hidden';
        height = parseInt(getComputedStyle(this[i]).height);
        paddingTop = parseInt(getComputedStyle(this[i]).paddingTop);
        paddingBottom = parseInt(getComputedStyle(this[i]).paddingBottom);
        borderTopWidth = parseInt(getComputedStyle(this[i]).borderTopWidth);
        borderBottomWidth = parseInt(getComputedStyle(this[i]).borderBottomWidth);
        marginTop = parseInt(getComputedStyle(this[i]).marginTop);
        marginBottom = parseInt(getComputedStyle(this[i]).marginBottom);
        animate = this.animate(
            // общая продолжительность анимации в миллисекундах
            duration,
            // функция будет вызвана несколько раз, progress изменяется от 0 до 1
            progress => {
                this[i].style.height = Math.round((1 - progress) * height) + 'px';
                this[i].style.paddingTop = Math.round((1 - progress) * paddingTop) + 'px';
                this[i].style.paddingBottom = Math.round((1 - progress) * paddingBottom) + 'px';
                this[i].style.borderTopWidth = Math.round((1 - progress) * borderTopWidth) + 'px';
                this[i].style.borderBottomWidth = Math.round((1 - progress) * borderBottomWidth) + 'px';
                this[i].style.marginTop = Math.round((1 - progress) * marginTop) + 'px';
                this[i].style.marginBottom = Math.round((1 - progress) * marginBottom) + 'px';
                if (progress === 1) {
                    this[i].style.display = 'none';
                    this[i].style.height = '';
                    this[i].style.overflow = '';
                    this[i].style.paddingTop = '';
                    this[i].style.paddingBottom = '';
                    this[i].style.borderTopWidth = '';
                    this[i].style.borderBottomWidth = '';
                    this[i].style.marginTop = '';
                    this[i].style.marginBottom = '';
                }
            },
            // после завершения анимации будет вызвана функция complete()
            complete
        );
        requestAnimationFrame(animate);
    }
};

$.extensions.slideDown = function (duration = 1000, display = 'block', complete) {
    let animate, height, paddingTop, paddingBottom, borderTopWidth, borderBottomWidth, marginTop, marginBottom;
    for (let i = 0; i < this.length; i++) {
        // если элемент не скрыт, то ничего делать не нужно
        if (getComputedStyle(this[i]).display !== 'none') continue;
        this[i].style.display = display;
        this[i].style.overflow = 'hidden';
        height = parseInt(getComputedStyle(this[i]).height);
        paddingTop = parseInt(getComputedStyle(this[i]).paddingTop);
        paddingBottom = parseInt(getComputedStyle(this[i]).paddingBottom);
        borderTopWidth = parseInt(getComputedStyle(this[i]).borderTopWidth);
        borderBottomWidth = parseInt(getComputedStyle(this[i]).borderBottomWidth);
        marginTop = parseInt(getComputedStyle(this[i]).marginTop);
        marginBottom = parseInt(getComputedStyle(this[i]).marginBottom);
        animate = this.animate(
            // общая продолжительность анимации в миллисекундах
            duration,
            // функция будет вызвана несколько раз, progress изменяется от 0 до 1
            progress => {
                this[i].style.height = Math.round(progress * height) + 'px';
                this[i].style.paddingTop = Math.round(progress * paddingTop) + 'px';
                this[i].style.paddingBottom = Math.round(progress * paddingBottom) + 'px';
                this[i].style.borderTopWidth = Math.round(progress * borderTopWidth) + 'px';
                this[i].style.borderBottomWidth = Math.round(progress * borderBottomWidth) + 'px';
                this[i].style.marginTop = Math.round(progress * marginTop) + 'px';
                this[i].style.marginBottom = Math.round(progress * marginBottom) + 'px';
                if (progress === 1) {
                    this[i].style.height = '';
                    this[i].style.overflow = '';
                    this[i].style.paddingTop = '';
                    this[i].style.paddingBottom = '';
                    this[i].style.borderTopWidth = '';
                    this[i].style.borderBottomWidth = '';
                    this[i].style.marginTop = '';
                    this[i].style.marginBottom = '';
                }
            },
            // после завершения анимации будет вызвана функция complete()
            complete
        );
        requestAnimationFrame(animate);
    }
};

$.extensions.slideToggle = function (duration = 1000, display = 'block', complete) {
    for (let i = 0; i < this.length; i++) {
        if (getComputedStyle(this[i]).display === 'none') {
            $(this[i]).slideDown(duration, display, complete);
        } else {
            $(this[i]).slideUp(duration, complete);
        }
    }
};

Чтобы this внутри функции complete указывал на объект HTMLElement, на котором отработала анимация, можно сделать так:

animate = this.animate(
    // общая продолжительность анимации в миллисекундах
    duration,
    // функция будет вызвана несколько раз, progress изменяется от 0 до 1
    progress => {.....},
    // после завершения анимации будет вызвана функция complete()
    typeof complete === "function" ? complete.bind(this[i]) : undefined
);

17. Создание компонентов

Компонентов будет много, так что потребуется подготовительная работа. Создаем директории src/sass/general, src/sass/helpers и src/sass/components.

Файл src/sass/general/_base.scss:

* {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}
.container {
    max-width: 900px;
    padding: 0px 15px;
    margin: 0px auto;
}

Файл src/sass/general/_variables.scss:

$primary: #025fff;
$success: #2a9924;
$danger: #ea2e41;
$warning: #ffc907;
$dark: #343a40;

Файл src/sass/general/_typography.scss:

@import url('https://fonts.googleapis.com/css?family=Roboto:300,400,700&display=swap&subset=cyrillic-ext');
* {
    font-family: 'Roboto', sans-serif;
}

Файл src/sass/style.scss:

@import 'general/base.scss';
@import 'general/variables.scss';
@import 'general/typography.scss';

Файл src/sass/helpers/_common.scss:

@import 'align.scss';
@import 'display.scss';
@import 'font.scss';
@import 'margin.scss';
@import 'padding.scss';
@import 'size.scss';

Файл src/sass/helpers/_align.scss:

.text-center {
    text-align: center;
}
.block-center {
    margin: 0 auto;
}

Файл src/sass/helpers/_display.scss:

.hidden {
    visibility: hidden;
}
.visible {
    visibility: visible;
}
.d-none {
    display: none;
}
.d-block {
    display: block;
}
.d-flex {
    display: flex;
}
.f-centered {
    justify-content: center;
    align-items: center;
}
.f-space-around {
    justify-content: space-around;
}
.f-space-between {
    justify-content: space-between;
}

Файл src/sass/helpers/_display.scss:

.fz-16 {
    font-size: 16px;
}
.fz-20 {
    font-size: 20px;
}
.fz-24 {
    font-size: 24px;
}
.bold {
    font-weight: bold;
}
.thin {
    font-weight: 300;
}
.italic {
    font-style: italic;
}
.text-color-primary {
    color: $primary;
}
.text-color-danger {
    color: $danger;
}
.text-color-success {
    color: $success;
}
.text-color-warning {
    color: $warning;
}
.text-color-dark {
    color: $dark;
}

Файл src/sass/helpers/_margin.scss:

.m-10 {
    margin: 10px !important;
}
.m-20 {
    margin: 20px !important;
}
.mt-10 {
    margin-top: 10px !important;
}
.mr-10 {
    margin-right: 10px !important;
}
.mb-10 {
    margin-bottom: 10px !important;
}
.ml-10 {
    margin-left: 10px !important;
}
.mt-20 {
    margin-top: 20px !important;
}
.mr-20 {
    margin-right: 20px !important;
}
.mb-20 {
    margin-bottom: 20px !important;
}
.ml-20 {
    margin-left: 20px !important;
}

Файл src/sass/helpers/_padding.scss:

.p-10 {
    padding: 10px !important;
}
.p-20 {
    padding: 20px !important;
}
.pt-10 {
    padding-top: 10px !important;
}
.pr-10 {
    padding-right: 10px !important;
}
.pb-10 {
    padding-bottom: 10px !important;
}
.pl-10 {
    padding-left: 10px !important;
}
.pt-20 {
    padding-top: 20px !important;
}
.pr-20 {
    padding-right: 20px !important;
}
.pb-20 {
    padding-bottom: 20px !important;
}
.pl-20 {
    padding-left: 20px !important;
}

Файл src/sass/helpers/_size.scss:

.w-100 {
    width: 100%;
}
.w-50 {
    width: 50%;
}
.w-300 {
    width: 300px;
}
.w-500 {
    width: 500px;
}
.h-100 {
    height: 100%;
}
.h-50 {
    height: 50%;
}
.h-300 {
    height: 300px;
}
.h-500 {
    height: 500px;
}

Файл src/sass/style.scss:

@import 'general/base.scss';
@import 'general/variables.scss';
@import 'general/typography.scss';

@import 'helpers/common.scss';

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

Теперь создаем первый компонент src/sass/components/button.scss:

.btn {
    display: inline-block;
    padding: 10px 20px;
    border: 1px solid $primary;
    background-color: transparent;
    border-radius: 4px;
    cursor: pointer;
    color: $dark;
    font-weight: 400;
    text-align: center;
    vertical-align: middle;
    font-size: 14px;
    line-height: 1.5;
    transition: all 0.2s;
    &:hover {
        background-color: lighten($primary, 10%);
        color: #fff;
    }
    &-primary {
        background-color: $primary;
        border-color: $primary;
        color: #fff;
        &:hover {
            background-color: darken($primary, 10%);
        }
    }
    &-success {
        background-color: $success;
        border-color: $success;
        color: #fff;
        &:hover {
            background-color: darken($success, 10%);
        }
    }
    &-danger {
        background-color: $danger;
        border-color: $danger;
        color: #fff;
        &:hover {
            background-color: darken($danger, 10%);
        }
    }
    &-warning {
        background-color: $warning;
        border-color: $warning;
        color: #fff;
        &:hover {
            background-color: darken($warning, 10%);
        }
    }
    &-dark {
        background-color: $dark;
        border-color: $dark;
        color: #fff;
        &:hover {
            background-color: darken($dark, 10%);
        }
    }
    &-outline-primary {
        border-color: $primary;
        color: $primary;
        &:hover {
            background-color: $primary;
        }
    }
    &-outline-success {
        border-color: $success;
        color: $success;
        &:hover {
            background-color: $success;
        }
    }
    &-outline-danger {
        border-color: $danger;
        color: $danger;
        &:hover {
            background-color: $danger;
        }
    }
    &-outline-warning {
        border-color: $warning;
        color: $warning;
        &:hover {
            background-color: $warning;
        }
    }
    &-outline-dark {
        border-color: $dark;
        color: $dark;
        &:hover {
            background-color: $dark;
        }
    }
    &-block {
        display: block;
        width: 100%;
    }
}

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

@import 'button.scss';

Файл src/sass/style.scss:

@import 'general/base.scss';
@import 'general/variables.scss';
@import 'general/typography.scss';

@import 'helpers/common.scss';

@import 'components/common.scss';

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