Popit для бедных или жонглируем событиями
Hello friend! Не так давно стала популярной новая релакс игрушка и я решил попробовать запилить ее аналог для веба.
Хотелось бы, чтобы она была более менее реалистичной внешне и по фичам тоже не отставала.
В этой статье я немного расскажу о том, как я работал с событиями,о некоторых ограничениях с которыми столкнулся и как стилизовал сие чудо.
Разметка и стили
Не знаю на сколько мне удалось добиться внешнего сходства (надеюсь хотябы на 50%), но из-за ограниченности времени остановился на текущей версии.
За реализм у меня ответчают box-shadow
и radial-gradient
, которые позволяют внешним границам не казаться плоскими, а пупрками более-менее выпуклыми. Для впуклых пупырок я использовал фильтр яркости.
Стоит сказать, что в качестве разметки я решил использовать массив чебоксов, которые легко можно стилизовать в зависимости от состояния элемента и удобно подписываться на событие checked
, однако, возникшие ограничения заставили меня использовать другой подход (об этом чуть позже).
Я определил соответствие цветов фона для строк с цветом пупырок на них и в зависимости от этого настроил цвета градиентов и контуров.
Например так:
.pop-row.green .pop-item:before{
border-color: #24b904;
background-image: radial-gradient(circle, rgb(6 255 1) 0%, rgb(6 255 1) 30%, rgb(4 156 1) 100%);
}
А вот так накидываю фильтр для впуклой ячейки:
.pop-item.checked:before {
filter: brightness(0.8);
}
Кстати, про класс .checked
. Как известно, если несколько способов привязки лейбла к инпуту:
1.
<input type="checkbox" id="happy" name="happy" value="yes">
<label for='happy'>Это чекбокс</label>
<label>
<input type="checkbox" name="happy" value="yes">Это чекбокс
</label>
Я выбрал второй, потому как решил, что слишком долго будет писать разметку, добавляя уникальные id для чекбоксов в таком массиве. И это, на самом деле, было ошибкой, потому что в плане стилизации 1 способ удобнее, т.к. дает возможность стилизовать элемент через соседний селектор +
и псевдокласс :checked
. А во втором случае вообще нет смысла использовать чекбокс.
Отсюда и появился класс .checked
, который просто накинуть или убрать с элемента. Чекбокс получается у меня остался по историческим соображениям (типа работает - не трогай).
Кстати, есть еще вариант с такой разметкой, который можно стилзовать также через input:checked + .targtet-to-style
. Но это уже совсем другая история!
<label>
<input type="checkbox" name="happy" value="yes">
<span class="tagtet-to-style"></span>
</label>
Накидав 3 разметки для разных размеров экрана (отличающихся числом строк и числом элементов в каждой строке), я закончил с разметкой.
События
Начинаем жонглирование.
Имеем подписчики
let checkboxLists = document.querySelectorAll(".popit");
let body = document.querySelector("body");
body.addEventListener('mousedown', onMouseDown);
body.addEventListener('mouseup', onMouseUp);
body.addEventListener('touchstart', onTouchstart);
body.addEventListener('touchend', onTouchend);
checkboxLists.addEventListener('change', onCheckboxChange);
checkboxList.addEventListener('mouseover', onLabelMouseover);
checkboxList.addEventListener('touchmove', onToucheMove);
Реализация обычного клика. Подписываемся на change чекбокса и наслаждаемся.
checkboxLists.addEventListener('change', onCheckboxChange);
Реализация массивного пробега с зажатой левой кнопкой. Задаем глобальную переменную isMouseDown
, на событие mouseDown устанавливаем isMouseDown = true
, на mouseover - проверяем что isMouseDown === true
и если находимся над лейблом чекбокса (т е пупыркой), то выполняем пук (основное действие - вдавливание или выдавливание).
На mouseup
выставляем isMouseDown=false
.
body.addEventListener('mousedown', onMouseDown);
body.addEventListener('mouseup', onMouseUp);
checkboxList.addEventListener('mouseover', onLabelMouseover);
Все бы ничего, однако, если вы начали свой массовый пробег с label элемента, то на нем наше действие не сработает. А вот почему:
- mouseover срабатывает на ячейке (isMouseDown еще false)
- зажимаем левую кнопку (isMouseDown становится true)
- идем дальше по полю и mouseover сработает уже только на следующем элементе
Один из вариантов решений - убрать подписку на change
и обрабатывать отдельные клики на onMouseDown
function onMouseDown(e) {
isMouseDown = true;
if(isPopLabel(e.target)) {
onPopClick(e);
}
}
Данное решение есть результат различных проб и ошибок с подписаками на родительский элемент и эксперименты с погружением и useCapture = true
.
(Если у вас есть мысли на этот счет, буду рад послушать их в коментах)
На этом казалось бы стоит успокоиться - все работает. Однако на мобилках - нет! Ведь там вместо клика (или не вместо, а в дополнение к клику) есть touch и нужно обработать и эти события.
Mousedown
отработает и так, а вот с touchmove
придется попотеть. touchmove
в событии не содержит информации о том, на каком элементе возникло событие. Вместо этого вам нужно будет добыть координаты и самому понять, над каким элементом мы находимся.
let myLocation = e.changedTouches[0];
let realTarget = document.elementFromPoint(myLocation.clientX, myLocation.clientY);
Ну а дальше - по накатанной дорожке. Только стоит сравнивать текущий target события и предыдущий и, если они равны, пропускать выполнение (т к touchmove
генерируется постоянно пока вы ведете пальцем по экрану)
Казалось бы, тут уже все, финиш. Но есть ограничение, которое я так и не смог побороть. Это проигрывание звука на мобиле. Не во всех случаях, конечно, а только если вы с самомго начала решили массово давить ячейки. Google Chrome ограничивает автовоспроизведение звуков на сайтах, до тех пор пока пользователь не проявит какой то интерактив.
touchmove
, к сожалению, к таким интерактивам не относится, только touchend (ну и клики). Как это обходят в html гемдеве (если такой есть), я увы не нашел.
Кстати, хотел продемонстрировать свой костыль для частого проигрывания звуков щелчков. При массовой давке вы получится паузы и прерывания при обычном вызове play:
const audio = new Audio('scripts/pop.mp3');
audio.play();
Поэтому я создал несколько экземпляров audio и проверяю их состояние так:
function playSound() {
if(!audio.paused){
if(!audio1.paused){
audio2.play();
} else {
audio1.play();
}
} else {
audio.play();
}
}
На этом пожалуй все: тестируйте работу тут, код смотрите тут.
Всякую подобную дичь я пишу тут!
Спасибо за прочтение!