10 Марта 2022
39

Detskaya lyubov'

Где-то в конце 80-х отправили меня в пионерлагер. Тульская область, город Алексин. Не в этом суть. Понравился мне там один мальчик, звали его красиво(по крайней мере, тогда мне так казалось) - Валентин. Я только третий класс закончила, он был чуть старше. И не знала я, как к нему подкатить. И решила написать письмо. Типа от иностранки. Русскими словами, но латиницей. "Privet, Valentin. Menya zovut..." и т.д... Естественно, он раскусил эту фигню,он уже учил английский... Прошло больше 30 лет, а до сих пор стыдно за такую тупость) Валёк, вдруг ты здесь есть. Я та, что побольше)

Detskaya lyubov'
Показать полностью 1

Кастомный селект на 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();
};


1. WrapSelect

const wrapSelect = (object) => {
const instance = object;
const { select, className } = instance;
// создавать контейнер будем асинхронно
return new Promise((resolve) => {
requestIdleCallback(() => {
// вставляем контейнер нового селекта
insertBase(select, className);
// сохраняем его в instance
instance.node = select.nextElementSibling;
// прячем настоящий селект в контейнер
hideSelect(instance);
resolve(instance);
});
});
};

InsertBase

// разметка с контейнером нового селекта
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`);
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));
};
isOption и shouldDropdown
// оба предиката возвращают наличие класса option
const isOption = (target, { className }) => target.classList.contains(`${className}__option`);
const shouldDropdown = (target, { className }) => target.classList.contains(`${className}__option`);
pickOption
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.

Показать полностью
13

Momentus заканчивает доводку своего космического буксира Vigoride 3, и планирует запуск летом

Momentus заканчивает доводку своего космического буксира Vigoride 3, и планирует запуск летом

Калифорнийская Momentus Space сообщила, что успешно продвигается к первому запуску своего космического буксира Vigoride..

Руководители компании заявили, что ее буксир Vigoride 3 недавно завершил термовакуумные испытания на системном уровне, и в настоящее время инженеры работают над неуказанными проблемами с космическим кораблем, обнаруженным во время этих испытаний.
«Мы по-прежнему считаем, что наш Vigoride 3 находится на пути к завершению наземных испытаний к нашему первому запуску, запланированному на июнь, хотя график плотный», — сказал Джон Руд, исполнительный директор компании.

Vigoride 3 в настоящее время планируется запустить в рамках специальной миссии SpaceX Transporter-5 в июне. Momentus заявила, что подписала соглашение со SpaceX о втором порте для этого запуска, а также о опции для следующих четырех миссий Transporter, запуск которых запланирован на октябрь 2022 и октябрь 2023 года. Эти дополнительные запуски служат резервом, если миссия Vigoride 3 будет отложена, или послужат в будущих миссиях.

Vigoride — космический буксир , разрабатываемый компанией Momentus Space, для обслуживания спутников клиентов . Он может заниматься изменением орбиты спутников, разгонять дополнительно спутники до 2000 км.ч, сводить их с орбиты, используя микроволновый электротермический двигатель (МЕТ) и воду в качестве топлива.
В будущем планируется оснастить буксиры этой серии роботоризированной "рукой".

Vigoride является лишь одним из представителей линейки Momentus. Компания также разрабатывает другие буксиры, в том числе Ardoride, который сможет доставлять спутники с низкой околоземной орбиты на низкую лунную орбиту, запуск которого планируется в 2024 году .

Показать полностью
2

А вы заметили что...

Недавно, буквально последние дни, заметил что больше не могу, в играх на андройде, получить воскрешение/бонусные очки. потому что больше не показывают рекламу в приложениях. Это нас так наказали, да?
ну это.. спасибо что ли.

3

Нуждаюсь в средстве чампикс

Народ, если у кого завалялось сие средство, то куплю по разумной цене. Очень надо! Реально работающая дрянь против табачной отравы. Знаю пятерых людей кто принимал и все бросили курить. Им тоже как и мне было не брость, все эти Аланы Карры, Никоретте, Табексы- как гомеопатия, просто никакого толку.
Всё бы хорошо, но этих таблеток как пол года нет в городе. Нашёл где- то в Крыму, но они пересылом не занимаются.
Нахожусь в Питере.

Мои подписки
Подписывайтесь на интересные вам теги, сообщества, авторов, волны постов — и читайте свои любимые темы в этой ленте.
Чтобы добавить подписку, нужно авторизоваться.

Отличная работа, все прочитано! Выберите