Кастомный селект на Vanilla JS
Часто дизайн требует стилизовать селект. Лучшим решением было бы стилизовать только контейнер, а его options оставить стандартными.
Если такой подход не устраивает, то необходимо делай свой.
Из готовых решений есть:
— Select2 на jQuery;
— Chosen тоже на jQuery;
— Choices на JavaScript без зависимостей.
Здесь мы попробуем сделать свое.
HTML
<select data-custom-select-class="select">
<option value="All">All frameworks</option>
<option value="React">React</option>
<option value="Vue">Vue</option>
<option value="Swelte">Swelte</option>
</select>
Добавим селект с вариантами. Сам атрибут data-custom-select-class даст нам понять, что этот селект нужно стилизовать, а его значение — какой класс использовать в новой разметке.
JavaScript
Вот полный код скрипта, можете быстро пройтись по нему, чуть ниже будет детальный разбор
const findElements = (object) => {
const instance = object;
const { node, select } = instance;
instance.toggle = node.children[0];
instance.holder = node.children[1];
instance.isActive = false;
instance.options = select.options;
instance.active = select.selectedIndex >= 0 ? select.selectedIndex : 0;
return instance;
};
const isOption = (target, { className }) => target.classList.contains(`${className}__option`);
const shouldDropdown = (target, { className }) => target.classList.contains(`${className}__option`);
const createBaseHTML = (value, className) => (`
<div class="${className}">
<button class="${className}__toggle" type="button">${value}</button>
<div class="${className}__options"></div>
</div>
`);
const insertBase = (select, className) => {
const selectedIndex = select.selectedIndex >= 0 ? select.selectedIndex : 0;
const value = select.options[selectedIndex].textContent;
const html = createBaseHTML(value, className);
select.insertAdjacentHTML('afterend', html);
};
const renderOption = (html, option, index, active, className) => {
const activeClassName = index === active ? `${className}__option--active` : '';
return `
${html}
<button class="${className}__option ${activeClassName}" type="button" data-index="${index}">${option.textContent}</button>
`;
};
const renderOptions = (options, active, className) => {
return [...options].reduce((acc, option, index) => renderOption(acc, option, index, active, className), '');
};
const pickOption = (object) => {
const instance = object;
const { select, active, customOptions, className } = instance;
select.selectedIndex = active;
instance.optionActive.classList.remove(`${className}__option--active`);
instance.optionActive = customOptions[active];
instance.optionActive.classList.add(`${className}__option--active`);
instance.toggle.textContent = instance.optionActive.textContent;
};
const onOptionsClick = (event, object) => {
event.preventDefault();
const instance = object;
const { select, hideDropdown } = instance;
const { target } = event;
if (isOption(target, instance)) {
instance.active = target.dataset.index;
pickOption(instance);
}
if (shouldDropdown(target, instance)) {
hideDropdown();
}
};
const initOptionsEvents = (instance) => {
instance.holder.addEventListener('click', event => onOptionsClick(event, instance));
};
const render = (object) => {
const instance = object;
const { holder, options, className, active } = instance;
const html = renderOptions(options, active, className);
holder.insertAdjacentHTML('afterbegin', html);
instance.customOptions = [...holder.children];
instance.optionActive = instance.customOptions[active];
initOptionsEvents(instance);
};
const hideSelect = ({ node, select }) => node.appendChild(select);
const wrapSelect = (object) => {
const instance = object;
const { select, className } = instance;
return new Promise((resolve) => {
requestIdleCallback(() => {
insertBase(select, className);
instance.node = select.nextElementSibling;
hideSelect(instance);
resolve(instance);
});
});
};
const unsubscribeDocument = ({ hideDropdown }) => document.removeEventListener('click', hideDropdown);
const subscribeDocument = ({ hideDropdown }) => document.addEventListener('click', hideDropdown);
const hideOptions = (object) => {
const instance = object;
const { node, className } = instance;
instance.isActive = false;
node.classList.remove(`${className}--active`);
unsubscribeDocument(instance);
};
const showOptions = (object) => {
const instance = object;
const { node, className } = instance;
instance.isActive = true;
node.classList.add(`${className}--active`);
subscribeDocument(instance);
};
const toggleOptions = (instance) => {
if (instance.isActive) hideOptions(instance);
else showOptions(instance);
};
const onNodeClick = event => event.stopPropagation();
const initEvents = (object) => {
const instance = object;
const { node, toggle } = instance;
const showDropdown = () => { showOptions(instance); };
const hideDropdown = () => { hideOptions(instance); };
const toggleDropdown = () => { toggleOptions(instance); };
instance.showDropdown = showDropdown;
instance.hideDropdown = hideDropdown;
instance.toggleDropdown = toggleDropdown;
toggle.addEventListener('click', toggleDropdown);
node.addEventListener('click', onNodeClick);
return instance;
};
const constructor = (select) => {
const instance = {
select,
className: select.dataset.customSelectClass,
};
const init = () => {
wrapSelect(instance)
.then(findElements)
.then(initEvents)
.then(render);
};
init();
};
const selects = document.querySelectorAll('[data-custom-select-class]');
selects.forEach(constructor);
Также Demo можно посмотреть на codepen.
Ищем селекты
// селектов, которые нужно стилизовать может быть несколько
// находим их по атрибут
const selects = document.querySelectorAll('[data-custom-select-class]');
// и через цикл передаем их в "конструктор"
selects.forEach(constructor);
Constructor
// нода селекта передается как аргумент функции
const constructor = (select) => {
// создаем объект
const instance = {
// в котором будет нода селекта
select,
// и класс, который будет использоваться в новом селекта
className: select.dataset.customSelectClass,
};
// Инициилизируем функцию
const init = () => {
// которая
// 1. обернет настоящий селект
// в контейнер нового
wrapSelect(instance)
// 2. сохранит в instance новые элементы
.then(findElements)
// 3. создаст события для открытия/закрытия и выбора варианта
.then(initEvents)
// 4. нарисует все options
.then(render);
};
init();
};
const wrapSelect = (object) => {InsertBase
const instance = object;
const { select, className } = instance;
// создавать контейнер будем асинхронно
return new Promise((resolve) => {
requestIdleCallback(() => {
// вставляем контейнер нового селекта
insertBase(select, className);
// сохраняем его в instance
instance.node = select.nextElementSibling;
// прячем настоящий селект в контейнер
hideSelect(instance);
resolve(instance);
});
});
};
// разметка с контейнером нового селекта
const createBaseHTML = (value, className) => (`
<div class="${className}">
<button class="${className}__toggle" type="button">${value}</button>
<div class="${className}__options"></div>
</div>
`);
const insertBase = (select, className) => {
// ищем активный option
const selectedIndex = select.selectedIndex >= 0 ? select.selectedIndex : 0;
// вынимаем его содержимое
const value = select.options[selectedIndex].textContent;
// создаем контейнер
const html = createBaseHTML(value, className);
// и вставляем его сразу после настоящего селекта
select.insertAdjacentHTML('afterend', html);
};
hideSelect
// прячем настоящий селект внутрь контейнера
const hideSelect = ({ node, select }) => node.appendChild(select);
2. findElements
Функция сохраняет в instance все необходимые элементы.
Для наглядности продублирую createBaseHTML
const createBaseHTML = (value, className) => (`
<div class="${className}">
// станет instance.toggle
<button class="${className}__toggle" type="button">${value}</button>
// станет instance.holder
<div class="${className}__options"></div>
</div>
`);
Смотрим и соотносим элементы:
const findElements = (object) => {
const instance = object;
const { node, select } = instance;
// кнопка открытия/закрытия селекта
instance.toggle = node.children[0];
// родитель кастосных options
instance.holder = node.children[1];
// флаг отвечает за то
// открыт или закрыт селект
instance.isActive = false;
// копируем options в instance
instance.options = select.options;
// сохраняем индекс активного селекта
instance.active = select.selectedIndex >= 0 ? select.selectedIndex : 0;
return instance;
};
3. initEvents
const initEvents = (object) => {
const instance = object;
const { node, toggle } = instance;
// отвечает за открытие селекта
const showDropdown = () => { showOptions(instance); };
// за закрытие селекта
const hideDropdown = () => { hideOptions(instance); };
// открывает или закрывает селект
// в зависимости от текущего состояния
const toggleDropdown = () => { toggleOptions(instance); };
instance.showDropdown = showDropdown;
instance.hideDropdown = hideDropdown;
instance.toggleDropdown = toggleDropdown;
// создаем слушатель для кнопки
toggle.addEventListener('click', toggleDropdown);
// для всего контейнера создаем функцию
// которая остановит всплытие
// ниже подробней
node.addEventListener('click', onNodeClick);
return instance;
};
Открытие/закрытие селекта
Читаем от нижней функции к верхней
// дабы функция не срабатывала при закрытом селекта
// удалим listener
const unsubscribeDocument = ({ hideDropdown }) => document.removeEventListener('click', hideDropdown);
const hideOptions = (object) => {
const instance = object;
const { node, className } = instance;
// меняем статус на неактивый
instance.isActive = false;
// убиваем класс "видимости"
node.classList.remove(`${className}--active`);
убираем слушатель со всего документа
unsubscribeDocument(instance);
};
// если пользователь кликнет на что угодно кроме селекта
// селект закроется
const subscribeDocument = ({ hideDropdown }) => document.addEventListener('click', hideDropdown);
const showOptions = (object) => {
const instance = object;
const { node, className } = instance;
// меняем статус на активый
instance.isActive = true;
// добавляем класс, отвечающий за видимость options
node.classList.add(`${className}--active`);
// создаем слушатель на весь документ
subscribeDocument(instance);
};
const toggleOptions = (instance) => {
// если селект уже открыт - закрываем
if (instance.isActive) hideOptions(instance);
// иначе - открываем
else showOptions(instance);
};
onNodeClick
Т.к. мы делаем слушатель на весь документ, то он сработает даже тогда когда мы будем кликать на наш селект.
Чтобы такое не происходило — уберем всплытие.
const onNodeClick = event => event.stopPropagation();
Подробнее про это можно прочитать на learn.javascript.ru
4. render
const render = (object) => {
const instance = object;
const { holder, options, className, active } = instance;
// создаем html новых options
const html = renderOptions(options, active, className);
// вставляем его в их родитель
holder.insertAdjacentHTML('afterbegin', html);
// сохраняем их в instance
instance.customOptions = [...holder.children];
// а также активный из них
instance.optionActive = instance.customOptions[active];
// создадим события для выбора options
initOptionsEvents(instance);
};
renderOptions
const renderOption = (html, option, index, active, className) => {
// генерим активный класс если option - активный
const activeClassName = index === active ? `${className}__option--active` : '';
return `
// склеиваем все ранее сгенерированные options с текущим
${html}
<button class="${className}__option ${activeClassName}" type="button" data-index="${index}">${option.textContent}</button>
`;
};
const renderOptions = (options, active, className) => {
// напоминаю, что options - скопированные options настоящего селекта
return [...options].reduce((acc, option, index) => renderOption(acc, option, index, active, className), '');
};
initOptionsEvent
const isOption = (target, { className }) => target.classList.contains(`${className}__option`);isOption и shouldDropdown
const onOptionsClick = (event, object) => {
event.preventDefault();
const instance = object;
const { select, hideDropdown } = instance;
const { target } = event;
// если кликнутый элемент - option
if (isOption(target, instance)) {
// обновляем идекс активного элемента в instance
instance.active = target.dataset.index;
// выбираем option
pickOption(instance);
}
// если select нужно закрыть
if (shouldDropdown(target, instance)) {
// закрываем
hideDropdown();
}
};
const initOptionsEvents = (instance) => {
// добавляем слушатель на родитель options
instance.holder.addEventListener('click', event => onOptionsClick(event, instance));
};
// оба предиката возвращают наличие класса optionpickOption
const isOption = (target, { className }) => target.classList.contains(`${className}__option`);
const shouldDropdown = (target, { className }) => target.classList.contains(`${className}__option`);
const pickOption = (object) => {
const instance = object;
const { select, active, customOptions, className } = instance;
// устанавливаем активный option настоящему селекту
select.selectedIndex = active;
// удаляем класс active у предыдущего активного option
instance.optionActive.classList.remove(`${className}__option--active`);
// находим новый активный option
instance.optionActive = customOptions[active];
// и задаем ему класс active
instance.optionActive.classList.add(`${className}__option--active`);
// меняем текст у toggle
instance.toggle.textContent = instance.optionActive.textContent;
};
hideDropdown
Это функция hideOptions, записанная в instance. Смотри выше.
Это всё. Если какой-то момент оказался не понятным — пишите в комментарии — попробую разъяснить, а также смотрите демо в codepen.
Настольная кастомная игра по мотивам Warcraft
Всем привет!
Меня зовут Артём. Хочу поделиться своей историей.
В один из жарких летних дней 2021 года я заболел COVID-19 и мне было абсолютно нечего делать. На летней распродаже в Steam я купил The Witcher 3 со всеми DLC. И прошел его. Три раза подряд. Так как я большой любитель настольных игр, я был рад знакомству с такой игрой, как Гвинт. Я видел огромный потенциал в этой игре и на тот момент уже слышал об отдельной игре на основе Гвинта. Я сразу его скачал и опробовал. Игра отличная, но лично у меня она не вызывала никаких эмоций. Что-то неуловимое было потеряно и эта мысль не давал мне покоя. Я большой поклонник вселенной Warcraft. И я подумал - почему бы не совместить принцип Гвинта с персонажами из вселенной Warcraft? Я потратил некоторое время, чтобы понять, как это все должно выглядеть. Я старался сохранить основные дизайнерские принципы и одновременно добавить свое видение, добавил новые механики для разнообразия. Когда последняя карта была составлена, я все это распечатал в типографии. И тут я столкнулся с неожиданной проблемой - в моем окружении нет людей, которые бы могли провести полноценное тестирование игрового баланса. Я долгое время пытался тестировать баланс сам с собой, но это сложно. Поэтому этот проект я отложил на дальнюю полку до лучших времен. Время пришло.
Если вкратце, то ищу единомышленников, сделал для этого сайт, жду любой обратной связи - будь то поддержка или критика.
Все карты уже составлены - качай, печатай, играй. Осталось настроить баланс, а одному это сделать тяжело.
Более подробно с этим проектом можно ознакомиться здесь:
https://www.battleoftimelostarmies.com/
Весь материал для разработки я брал из того что мог найти в свободном доступе.
Вот пример того как выглядят карты:
Роспись футболки красками по ткани Декола
Контакты:
Инстаграм - https://www.instagram.com/derkach__art/
Вконтакте - https://vk.com/belka1392
Девушка заказала роспись футболки мужу к 23му февраля 😇
А вы уже готовите подарки своим защитникам?☺️ Нет?😲А пора бы! 😁 Пока дойдёт по почте...
Задача была поставлена такая: выбрать эскиз к профессии автомаляр (человек, который красит машины).
Мы искали, искали, но ничего, особо, не нравилось☹️
После я решила поискать рисунки на тему: графитчики. И нашла очень харизматичного персонажа, но с баллончиком в руке🤷🏻♀️ (последнее фото).
Но и это нас не остановило!
Просто вместо баллончика нарисовали краскопульт😁🤘🏻 Заменили цвет на оранжевый, он очень хорошо сочетается с цветом футболки)
И вуаля! Все довольны😁
Листайте ниже, там ещё фото и видео этой работы, а также фрагменты процесса, который вы так любите😍
Спасибо вам за лайки и комментарии😇
Honda Civic на футболке красками по ткани. Ручная роспись
Мой инстаграм: https://www.instagram.com/derkach__art/
Для заказа пишите в директ🤗
Мечта...
У вас есть мечта? Вы к ней идёте?
Всегда нужно стремиться к своей мечте! Пусть это будут маленькие шаги, но всё же вы будете уже на шаг впереди.
Не всегда мы можем позволить себе сразу то, что хочется.
Не можешь купить машину здесь и сейчас?! Но ты можешь нарисовать её на футболке и быть уже на шаг ближе к мечте😉🤗
Мечтайте и идите к своей мечте, а иначе останетесь ни с чем. Согласны?)
Как ОТКРЫТЬ Хот Вилс? СВЕРЛИМ машинку HotWheels
Как ОТКРЫТЬ Хот Вилс? СВЕРЛИМ машинку HotWheels