Небольшой аналог Instagram.
В процессе мы создадим несколько фильтров, которые будут применяться к фотографии в блоке .photo
. Переключатели фильтров мы разместим в блоке .toggle-controls
, а само переключение будет работать на JavaScript. Cначала подготовим разметку для нашего будущего фотоприложения.
Теперь давайте создадим первый набор фильтров для класса .walden
. Набор фильтров применится к большой фото и к переключателю.
Давайте добавим заготовку для ещё одного переключателя. Класс для второго фильтра будет называться toaster
.
Затем «отрепетируем» переключение фильтра без использования JavaScript. Для этого пока будем менять HTML-код вручную. При переключении фильтра происходит два события:
Первое. В переключателе класс active
перемещается на текущий фильтр:
1 2 3 4 5 6 7 8 9 10 11 |
Было: <ul class="toggle-controls"> <li class="filter1 active"></li> <li class="filter2"></li> </ul> Стало: <ul class="toggle-controls"> <li class="filter1"></li> <li class="filter2 active"></li> </ul> |
Второе. У блока с большим фото меняется класс текущего фильтра:
1 2 |
Было: <div class="photo filter1"></div> Стало: <div class="photo filter2"></div> |
Мы «переключили» текущий фильтр, а чтобы увидеть его эффект, зададим его стили.
Этот фильтр будет делать фото ещё винтажнее, чем walden
: сделает его малоконтрастным, с яркими цветами, небольшим поворотом по цветовому кругу и эффектом сепии.
Всё готово для «оживления» интерфейса с помощью JavaScript. HTML-код Кекстаграма приведён в исходное состояние: фильтр ещё не выбран и к фото не применён.
Сначала включим фильтр toaster
и применим его к фотографии. Для этого:
1. Найдём элемент списка с классом toaster
и сохраним его в переменную control
.
var control = document.querySelector('li.toaster');
2. Найдём элемент с классом photo
и сохраним его в переменную photo
.
var photo = document.querySelector('.photo');
3. Теперь к элементу списка, хранящемуся в переменной control
, добавим класс active
:
control.classList.add('active');
4. А к блоку большой фотографии, она уже в переменной photo
, добавим класс toaster
:
photo.classList.add('toaster');
Теперь попробуйте повторить это сами. Обратите внимание, что при добавлении класса с помощью classList.add
точка в начале строки не пишется.
На предыдущем шаге мы «включили» фильтр toaster
из исходного состояния, а сейчас переключим его на walden
. Для этого:
1. Снова найдём элемент списка с классом toaster
, сохраним его в переменную toasterControl
и удалим у него класс active
, добавленный ранее:
var toasterControl = document.querySelector('li.toaster'); toasterControl.classList.remove('active');
2. Найдём элемент списка с классом walden
, сохраним его в переменную waldenControl
и добавим ему класс active
:
var waldenControl = document.querySelector('li.walden'); waldenControl.classList.add('active');
3. Найдём элемент с классом photo
и сохраним его в переменную photo
.
var photo = document.querySelector('.photo');
4. Удалим у фото класс toaster
и добавим класс walden
:
photo.classList.remove('toaster'); photo.classList.add('walden');
Разберёмся подробнее в том коде, который мы написали. Вот один из фрагментов:
1 2 3 4 |
var toasterControl = document.querySelector('li.toaster'); if (toasterControl) { toasterControl.classList.remove('active'); } |
var toasterControl
обозначает, что мы создаём переменную с именем toasterControl
.
document
– это специальная переменная, в которой хранится корневой элемент HTML-документа, будем называть его просто «документ». В нём хранятся все остальные теги.
querySelector
– это метод документа, который по указанному селектору ищет и возвращает первый найденный элемент, подходящий под селектор. В этом методе можно использовать любые CSS-селекторы, уже знакомые вам.
Метод querySelector
не всегда может найти элемент и тогда переменная остаётся пустой. Чтобы проверить, что в переменной есть элемент и с ним можно работать, используется условный оператор if
.
У элементов, которые мы находим с помощью querySelector
, есть свойство classList
, в котором хранится список классов элемента.
Список классов можно изменять, удаляя или добавляя в него классы с помощью методов add
и remove
свойства classList
.
Теперь немного отдохнём от JavaScript и создадим ещё один фильтр.
Вернёмся к JavaScript и немного улучшим наш код. Посмотрите эти строчки: var control = document.querySelector('li.toaster'); photo.classList.add('toaster');
Название фильтра toaster
в коде повторяется два раза, Карл. Чтобы «переключить» фильтр, придётся поменять код в двух местах. А это лишняя работа!
Избавимся от дублирования. В начале кода создадим переменную с названием фильтра:
var filterName = 'toaster'; В метод поиска элементов мы передаём строку li.toaster
. Чтобы получить такую же строку из переменной, воспользуемся операцией «склеивания» строк:
'li.' + filterName // результат: 'li.toaster'
В коде для поиска переключателя заменим строку на выражение с переменной:
1 2 |
Было: var control = document.querySelector('li.toaster'); Стало: var control = document.querySelector('li.' + filterName); |
Внутри photo.classList.add
находится такое же значение, как и в переменной, поэтому просто заменяем строку на переменную:
1 2 |
Было: photo.classList.add('toaster'); Стало: photo.classList.add(filterName); |
Продолжим улучшать наш JavaScript. Теперь упакуем весь код, отвечающий за переключение фильтров, в функцию. Это позволит проще его использовать и изменять.
Функция создаётся выражением, начинающимся с ключевого слова function
. Например, в этом коде мы создаём функцию для суммирования чисел c именем sum
:
1 2 3 4 5 6 |
// Определение функции function sum(a, b) { return a + b; } sum(1, 5); // Вызов функции. Результат выполнения: 6. |
У функции есть набор параметров, в нашем случае это переменные a
и b
, и тело, которое выполняется при вызове функции. Тело функции заключается в фигурные скобки. Количество параметров может быть любым, их может и не быть вовсе.
Обернём наш код для переключения фильтров в функцию toggleFilter
, у которой будет один параметр — filterName
. А затем вызовем функцию с разными названиями фильтра в качестве параметра.
Кстати, переменная filterName
нам больше не нужна, так как название фильтра передаётся в функцию в параметре с таким же именем, поэтому в коде создание переменной закомментировано.
На первый взгляд функция работает правильно. Но только при одном переключении фильтра, когда мы запускаем функцию только один раз. Ведь при каждом обновлении кода страница в мини-браузере обновляется целиком. Поэтому при первом запуске мы имеем дело с «чистым» исходным состоянием.
Если же вызвать созданную функцию несколько раз подряд с разными параметрами: toggleFilter('toaster'); toggleFilter('kelvin'); То активными станут сразу несколько переключателей, а к фотографии применятся несколько фильтров одновременно.
У HTML-элементов есть возможность создавать специальные атрибуты, в которых можно хранить вспомогательную информацию и легко передавать её в JavaScript. Такие атрибуты начинаются с префикса data-
.
При этом data-атрибуты валидны и никак не влияют на отображение элементов в браузере.
Давайте добавим переключателям data-атрибуты data-filter
, в которых будем хранить название каждого фильтра.
Чтобы с помощью JavaScript считать значение data-атрибутов, нужно использовать свойство dataset
. Пример:
1 2 3 4 5 6 7 |
HTML: <div class="control" data-filtername="walden"></div> JavaScript: var control = document.querySelector('.control'); var filter = control.dataset.filtername; // в переменной filter теперь строка «walden» |
В свойстве dataset
HTML-элемента хранятся все значения его data-атрибутов. Обратиться к ним можно по названию data-атрибута, удалив из названия приставку data-
.
Добавлять содержимое в HTML-элемент через JavaScript можно с помощью свойства innerHTML
:
1 2 |
var control = document.querySelector('.control'); control.innerHTML = 'walden'; |
Присвоенная свойству innerHTML
строка заменяет всё содержимое HTML-элемента. В этой строке можно использовать любой HTML-код.
Если нужно совершить несколько однотипных действий, то можно использовать цикл for
. Вот его синтаксис:
1 2 3 4 |
for (var num = 0; num <= 5; num++) { console.log(num); } // Выведет в «консоль» числа 0, 1, 2, 3, 4 и 5 |
Ранее мы использовали метод querySelector
, который возвращает только один элемент: первый элемент, соответствующий селектору.
Другой метод querySelectorAll
возвращает все элементы, соответствующие селектору.
С помощью for
удобно перебирать найденные элементы:
1 2 3 4 5 |
var controls = document.querySelectorAll('.toggle-controls li'); for (var i = 0; i < controls.length; i++) { controls[i].innerHTML = 'переключатель'; } // Во все элементы списка переключателей запишется строка «переключатель». |
В Кекстаграме может быть много фильтров, количество и названия которых мы можем не знать. Поэтому нельзя искать каждый переключатель по его классу и задавать его название.
Лучше найти все переключатели внутри списка и перебрать их с помощью цикла. И внутри цикла задавать название каждому переключателю.
Начнём исправлять ошибки в функции переключения фильтров, которые возникают, когда она запускается несколько раз.
Первая ошибка заключается в том, что все переключатели подсвечиваются как активные. Причина в том, что внутри функции класс active
добавляется текущему переключателю, но не удаляется у ставших неактивными.
Простейшим решением проблемы будет сначала удалять класс active
у всех переключателей, а затем добавлять его текущему.
На предыдущем шаге мы уже находили все переключатели и сохраняли их в переменную controls
:
var controls = document.querySelectorAll('.toggle-controls li'); Теперь мы можем использовать эту переменную внутри функции — добавить в начало функции ещё один цикл, который пройдётся по всем переключателям и удалит у них класс.
Ошибку с выделением активного переключателя исправили. Осталась вторая ошибка: фильтр на большой фотографии применяется неправильно. Причина — лишние классы фильтров у большой фотографии. Вот что происходит с HTML, когда функция переключения вызывается несколько раз: <div class="photo walden toaster kelvin"></div>
Чтобы исправить ошибку, надо удалять классы фильтров у блока фотографии при каждом переключении. Но класс photo
удалять нельзя. Классы фильтров мы знаем, ведь они хранятся в data-атрибутах переключателей.
Поэтому в том же цикле, где сбрасывается класс active
, можно у каждого переключателя брать название фильтра и удалять этот класс у большого фото:
1 2 3 4 |
for (var i = 0; i < controls.length; i++) { ... photo.classList.remove(имя фильтра из dataset-атрибута переключателя); } |
И ещё одна важная деталь. Переменная photo
теперь используется в самом начале функции, поэтому мы вынесем строчку поиска переменной из функции в самый верх кода:
var controls = document.querySelectorAll('.toggle-controls li'); var photo = document.querySelector('.photo');
А внутри функции переменную photo
надо удалить. Заодно мы ускорим работу скрипта, избавившись от ненужного поиска элемента .photo
, который происходил при каждом переключении фильтра.
В предыдущих шагах мы подготовили функцию для переключения фильтров и запускали её из JavaScript-кода. Сейчас мы сделаем так, чтобы пользователь сам мог менять фильтры, щёлкая по переключателям мышкой.
Для этого нам нужно научиться отслеживать и обрабатывать события, которые происходят на веб-странице. Для этого в JavaScript, существует метод addEventListener
:
1 2 3 4 |
var toaster = document.querySelector('li.toaster'); toaster.addEventListener('click', function() { toggleFilter(toaster.dataset.filter); }); |
В этом фрагменте кода мы сделали следующее:
- Нашли элемент списка и у него вызвали метод
addEventListener
. - Указали отслеживать событие
click
или «щелчок мыши». - Для щелчков указали функцию-обработчик без названия, внутри которой вызвали функцию переключения фильтров.
Метод addEventListener
был вызван у одного элемента, поэтому будут обрабатываться события только этого элемента. Первый параметр задаёт тип события, второй — функцию-обработчик.
Мы добавили обработку щелчка для одного переключателя. Не забываем, что переключателей может быть много, их количество и названия наперёд мы знать не можем.
Поэтому, чтобы обрабатывать щелчки по всем переключателям, лучше воспользоваться циклом. Тем более, что цикл по всем переключателям у нас уже есть.
А чтобы сократить код в цикле, создадим новую функцию:
1 2 3 4 5 |
function clickControl(control) { control.addEventListener('click', function() { toggleFilter(control.dataset.filter); }); }; |
Функция clickControl
принимает найденный элемент и добавляет ему обработчик щелчков мыши, в котором вызывается функция переключения фильтра. Название фильтра для функции переключения берётся из data-атрибута самого элемента.
Благодаря clickControl
нам нужно добавить только одну строчку в цикл, чтобы все переключатели заработали:
1 2 3 4 |
for (var i = 0; i < controls.length; i++) { ... clickControl(controls[i]); } |
И снова улучшим код. В функции переключения есть проблема: в качестве параметра используются названия фильтра, по которому каждый раз происходит поиск переключателя. Но ведь можно передавать в функцию сами переключатели. Для этого.
1. Изменим обработчик внутри clickControl
:
1 2 |
Было: toggleFilter(control.dataset.filter); Стало: toggleFilter(control); |
2. Изменим параметр у toggleFilter
, теперь это не строка, а элемент:
1 2 |
Было: function toggleFilter(filterName) Стало: function toggleFilter(control) |
3. В toggleFilter
передаётся переключатель и искать его уже не надо, удаляем лишний код
4. Название фильтра для фото теперь берём из data-атрибута переключателя:
1 2 |
Было: photo.classList.add(filterName); Стало: photo.classList.add(control.dataset.filter); |
А теперь вы самостятельно сделаете последний штрих! При загрузке страницы должен быть выбран фильтр по умолчанию. Для этого нужно в конце кода добавить новую переменную, сохранить в неё нужный переключатель и с этой переменной вызвать функцию переключения.
- Менять левую координату ползунка-разделителя, чтобы он двигался вправо или влево.
- Менять ширину блока с изображением-оригиналом так, чтобы граница фотографий оставалась под ползунком.
К счастью, мы подготовили очень удобную вёрстку, в которой начало координат у обоих блоков совпадает. Поэтому достаточно задавать одинаковые значения для левой координаты разделителя и ширины блока с оригиналом фото.
Чтобы изменить CSS-свойство элемента в скрипте, нужно обратиться к свойству style
элемента. Например:
var element = document.querySelector('.photo-original'); element.style.width = '10px';
В этом коде элементу задаётся ширина 10px
.
С помощью element.style
можно и получать, и изменять значения свойств.
Но названия свойств в JavaScript не всегда совпадают с их названиями в CSS. Например, CSS-свойство left
совпадает с style.left
, но CSS-свойство baсkground-color
уже отличается: style.backgroundColor
.
Результат:
HTML:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="utf-8"> <title>Кекстаграм: финал</title> <base href="/assets/course98/"> <link href="style.css" rel="stylesheet"> <link href="course.css" rel="stylesheet"> </head> <body class="keksta"> <div class="photos"> <div class="photo"></div> <div class="photo-original"></div> <div class="separator">↔</div> </div> <ul class="toggle-controls"> <li class="walden" data-filter="walden"></li> <li class="toaster" data-filter="toaster"></li> <li class="kelvin" data-filter="kelvin"></li> <li class="oldie" data-filter="oldie"></li> </ul> <script src="script.js"></script> <script src="separator.js"></script> </body> </html> |
CSS:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
.walden { filter: contrast(0.9) brightness(1.2) hue-rotate(-20deg) saturate(1.7) sepia(0.4); } .toaster { filter: saturate(2.5) hue-rotate(-30deg) contrast(0.67) sepia(0.4); } .kelvin { filter: contrast(1.1) brightness(1.3) saturate(2.4) sepia(0.4); } .oldie { filter: contrast(2) saturate(5) hue-rotate(-180deg); } .willow { filter: saturate(0.02) contrast(0.85) brightness(1.2) sepia(0.02); } .photos { position: relative; } .photo, .photo-original { position: absolute; width: 100%; height: 100%; background-size: cover; } .photo { z-index: 10; background-image: url("img/spb-1.jpg"); } .photo-original { z-index: 20; width: 50%; background-image: url("img/spb-1.jpg"); } .separator { position: absolute; top: 50%; left: 50%; z-index: 30; width: 30px; height: 30px; border: 2px solid #ffffff; border-radius: 50%; transform: translate(-50%, -50%); } |
JS script:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
var controls = document.querySelectorAll('.toggle-controls li'); var photo = document.querySelector('.photo'); for (var i = 0; i < controls.length; i++) { controls[i].innerHTML = controls[i].dataset.filter; clickControl(controls[i]); } function toggleFilter(control) { for (var i = 0; i < controls.length; i++) { controls[i].classList.remove('active'); photo.classList.remove(controls[i].dataset.filter); } control.classList.add('active'); if (photo) { photo.classList.add(control.dataset.filter); } } function clickControl(control) { control.addEventListener('click', function() { toggleFilter(control); }); } var defaultFilter = document.querySelector('li.oldie'); toggleFilter(defaultFilter); |
JS separator:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
var separator = document.querySelector('.separator'); var originalPhoto = document.querySelector('.photo-original'); var filteredPhoto = document.querySelector('.photo'); var photoContainer = document.querySelector('.photos'); var flag = false; separator.addEventListener('mousedown', function(event) { event.preventDefault(); flag = true; }, false); document.addEventListener('mouseup', function(event) { flag = false; }, false); photoContainer.addEventListener('mousemove', function(event) { var res = event.pageX - this.offsetLeft; if (flag && (res > 0) && (res < filteredPhoto.offsetWidth)) { separator.style.left = res + 'px'; originalPhoto.style.width = res + 'px'; } }, false); |
Просто пример:
HTML:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="utf-8"> <title>Испытание: игровые фишки</title> <base href="/assets/course98/"> <link href="style.css" rel="stylesheet"> <link href="course.css" rel="stylesheet"> </head> <body class="exam-table"> <ul class="chips"> <li data-number="1"></li> <li data-number="2"></li> <li data-number="3"></li> <li data-number="4"></li> <li data-number="9"></li> <li data-number="10"></li> <li data-number="11"></li> <li data-number="12"></li> <li data-number="5"></li> <li data-number="6"></li> <li data-number="7"></li> <li data-number="8"></li> <li data-number="13"></li> <li data-number="14"></li> <li data-number="15"></li> <li data-number="16"></li> </ul> </body> </html> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
li.color-1 { border-color: #001f3f; } li.color-2 { border-color: #0074d9; } li.color-3 { border-color: #7fdbff; } li.color-4 { border-color: #39cccc; } li.color-5 { border-color: #3d9970; } li.color-6 { border-color: #2ecc40; } li.color-7 { border-color: #01ff70; } li.color-8 { border-color: #ffdc00; } li.color-9 { border-color: #ff851B; } li.color-10 { border-color: #ff4136; } li.color-11 { border-color: #85144b; } li.color-12 { border-color: #f012Be; } li.color-13 { border-color: #b10dc9; } li.color-14 { border-color: #111111; } li.color-15 { border-color: #aaaaaa; } li.color-16 { border-color: #ffffff; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
// Добавление класса элементу, переданному в качестве параметра. Название класса составляется с помощью data-атрибута этого элемента function setNumber(element) { element.classList.add('color-' + element.dataset.number); } // Добавление элементу четырёхцветного фона function reColor(element) { var baseColor = getComputedStyle(element).borderTopColor; var color = baseColor.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i); element.style.borderTopColor = colorShift(color, 40); element.style.borderRightColor = colorShift(color, 60); element.style.borderBottomColor = colorShift(color, 80); element.style.borderLeftColor = colorShift(color, 20); } // Смещение цветового тона function colorShift(color, shift) { var rgb = '#'; for (var i = 1; i <= 3; i++) { var part = parseInt(color[i]) - shift; part = Math.round(Math.min(Math.max(0, part), 255)); part = part.toString(16); rgb += ('00' + part).substr(part.length); } return rgb; } var controls = document.querySelectorAll('.chips li'); for (var i = 0; i < controls.length; i++) { setNumber(controls[i]); reColor(controls[i]); controls[i].innerHTML = controls[i].dataset.number; } |