Видеокурс «Практический JavaScript», часть четвертая
02.07.2021
Теги: JavaScript • Web-разработка • Модуль • Практика
Еще одни проект, но гораздо интереснее первых трех — создание своей библиотеки. Во-первых, в ней будет возможность работы с элементами страницы — отобрать по селектору, скрыть, показать. Во-вторых, будет возможность быстро создать слайдер, аккордеон, вкладки, модальное окно, 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-разработка • Модуль • Практика