// Подключение функционала "Чертогов Фрилансера"
import { isMobile, _slideUp, _slideDown, _slideToggle, FLS } from "../files/functions.js";
import { flsModules } from "../files/modules.js";
import { formValidate } from "../files/forms/forms.js";
// Подключение файла стилей
// Базовые стили поключаются в src/scss/forms.scss
// Файл базовых стилей src/scss/forms/select.scss
/*
Документация:
Снипет (HTML): sel
*/
/*
// Настройки
Для селекта (select):
class="имя класса" - модификатор к конкретному селекту
multiple - мультивыбор
data-class-modif= "имя модификатора"
data-tags - режим тегов, только для (только для multiple)
data-scroll - включит прокрутку для выпадающего списка, дополнительно можно подключить кастомный скролл simplebar в app.js. Указанное число для атрибута ограничит высоту
data-checkbox - стилизация элементов по checkbox (только для multiple)
data-show-selected - отключает скрытие выбранного элемента
data-search - позволяет искать по выпадающему списку
data-open - селект открыт сразу
data-submit - отправляет форму при изменении селекта
data-one-select - селекты внутри оболочки с атрибутом будут показываться только по одному
data-pseudo-label - добавляет псевдоэлемент к заголовку селекта с указанным текстом
Для плейсхолдера (Плейсхолдер - это option с value=""):
data-label для плейсхолдера, добавляет label к селекту
data-show для плейсхолдера, показывает его в списке (только для единичного выбора)
Для элемента (option):
data-class="имя класса" - добавляет класс
data-asset="путь к картинке или текст" - добавляет структуру 2х колонок и данными
data-href="адрес ссылки" - добавляет ссылку в элемент списка
data-href-blank - откроет ссылку в новом окне
*/
/*
// Возможные доработки:
попап на мобилке
*/
// Класс построения Select
class SelectConstructor {
constructor(props, data = null) {
let defaultConfig = {
init: true,
logging: true,
}
this.config = Object.assign(defaultConfig, props);
// CSS классы модуля
this.selectClasses = {
classSelect: "select", // Главный блок
classSelectBody: "select__body", // Тело селекта
classSelectTitle: "select__title", // Заголовок
classSelectValue: "select__value", // Значение в заголовке
classSelectLabel: "select__label", // Лабел
classSelectInput: "select__input", // Поле ввода
classSelectText: "select__text", // Оболочка текстовых данных
classSelectLink: "select__link", // Ссылка в элементе
classSelectOptions: "select__options", // Выпадающий список
classSelectOptionsScroll: "select__scroll", // Оболочка при скролле
classSelectOption: "select__option", // Пункт
classSelectContent: "select__content", // Оболочка контента в заголовке
classSelectRow: "select__row", // Ряд
classSelectData: "select__asset", // Дополнительные данные
classSelectDisabled: "_select-disabled", // Запрешен
classSelectTag: "_select-tag", // Класс тега
classSelectOpen: "_select-open", // Список открыт
classSelectActive: "_select-active", // Список выбран
classSelectFocus: "_select-focus", // Список в фокусе
classSelectMultiple: "_select-multiple", // Мультивыбор
classSelectCheckBox: "_select-checkbox", // Стиль чекбокса
classSelectOptionSelected: "_select-selected", // Выбранный пункт
classSelectPseudoLabel: "_select-pseudo-label", // Псевдолейбл
}
this._this = this;
// Запуск инициализации
if (this.config.init) {
// Получение всех select на странице
const selectItems = data ? document.querySelectorAll(data) : document.querySelectorAll('select');
if (selectItems.length) {
this.selectsInit(selectItems);
this.setLogging(`Проснулся, построил селектов: (${selectItems.length})`);
} else {
this.setLogging('Сплю, нет ни одного select zzZZZzZZz');
}
}
}
// Конструктор CSS класса
getSelectClass(className) {
return `.${className}`;
}
// Геттер элементов псевдоселекта
getSelectElement(selectItem, className) {
return {
originalSelect: selectItem.querySelector('select'),
selectElement: selectItem.querySelector(this.getSelectClass(className)),
}
}
// Функция инициализации всех селектов
selectsInit(selectItems) {
selectItems.forEach((originalSelect, index) => {
this.selectInit(originalSelect, index + 1);
});
// Обработчики событий...
// ...при клике
document.addEventListener('click', function (e) {
this.selectsActions(e);
}.bind(this));
// ...при нажатии клавиши
document.addEventListener('keydown', function (e) {
this.selectsActions(e);
}.bind(this));
// ...при фокусе
document.addEventListener('focusin', function (e) {
this.selectsActions(e);
}.bind(this));
// ...при потере фокуса
document.addEventListener('focusout', function (e) {
this.selectsActions(e);
}.bind(this));
}
// Функция инициализации конкретного селекта
selectInit(originalSelect, index) {
const _this = this;
// Создаем оболочку
let selectItem = document.createElement("div");
selectItem.classList.add(this.selectClasses.classSelect);
// Выводим оболочку перед оригинальным селектом
originalSelect.parentNode.insertBefore(selectItem, originalSelect);
// Помещаем оригинальный селект в оболочку
selectItem.appendChild(originalSelect);
// Скрываем оригинальный селект
originalSelect.hidden = true;
// Присваиваем уникальный ID
index ? originalSelect.dataset.id = index : null;
// Работа с плейсхолдером
if (this.getSelectPlaceholder(originalSelect)) {
// Запоминаем плейсхолдер
originalSelect.dataset.placeholder = this.getSelectPlaceholder(originalSelect).value;
// Если включен режим label
if (this.getSelectPlaceholder(originalSelect).label.show) {
const selectItemTitle = this.getSelectElement(selectItem, this.selectClasses.classSelectTitle).selectElement;
selectItemTitle.insertAdjacentHTML('afterbegin', `${this.getSelectPlaceholder(originalSelect).label.text ? this.getSelectPlaceholder(originalSelect).label.text : this.getSelectPlaceholder(originalSelect).value}`);
}
}
// Конструктор основных элементов
selectItem.insertAdjacentHTML('beforeend', `
`);
// Запускаем конструктор псевдоселекта
this.selectBuild(originalSelect);
// Запоминаем скорость
originalSelect.dataset.speed = originalSelect.dataset.speed ? originalSelect.dataset.speed : "150";
// Событие при изменении оригинального select
originalSelect.addEventListener('change', function (e) {
_this.selectChange(e);
});
}
// Конструктор псевдоселекта
selectBuild(originalSelect) {
const selectItem = originalSelect.parentElement;
// Добавляем ID селекта
selectItem.dataset.id = originalSelect.dataset.id;
// Получаем класс оригинального селекта, создаем модификатор и добавляем его
originalSelect.dataset.classModif ? selectItem.classList.add(`select_${originalSelect.dataset.classModif}`) : null;
// Если множественный выбор, добавляем класс
originalSelect.multiple ? selectItem.classList.add(this.selectClasses.classSelectMultiple) : selectItem.classList.remove(this.selectClasses.classSelectMultiple);
// Cтилизация элементов под checkbox (только для multiple)
originalSelect.hasAttribute('data-checkbox') && originalSelect.multiple ? selectItem.classList.add(this.selectClasses.classSelectCheckBox) : selectItem.classList.remove(this.selectClasses.classSelectCheckBox);
// Сеттер значения заголовка селекта
this.setSelectTitleValue(selectItem, originalSelect);
// Сеттер элементов списка (options)
this.setOptions(selectItem, originalSelect);
// Если включена опция поиска data-search, запускаем обработчик
originalSelect.hasAttribute('data-search') ? this.searchActions(selectItem) : null;
// Если указана настройка data-open, открываем селект
originalSelect.hasAttribute('data-open') ? this.selectAction(selectItem) : null;
// Обработчик disabled
this.selectDisabled(selectItem, originalSelect);
}
// Функция реакций на события
selectsActions(e) {
const targetElement = e.target;
const targetType = e.type;
if (targetElement.closest(this.getSelectClass(this.selectClasses.classSelect)) || targetElement.closest(this.getSelectClass(this.selectClasses.classSelectTag))) {
const selectItem = targetElement.closest('.select') ? targetElement.closest('.select') : document.querySelector(`.${this.selectClasses.classSelect}[data-id="${targetElement.closest(this.getSelectClass(this.selectClasses.classSelectTag)).dataset.selectId}"]`);
const originalSelect = this.getSelectElement(selectItem).originalSelect;
if (targetType === 'click') {
if (!originalSelect.disabled) {
if (targetElement.closest(this.getSelectClass(this.selectClasses.classSelectTag))) {
// Обработка клика на тег
const targetTag = targetElement.closest(this.getSelectClass(this.selectClasses.classSelectTag));
const optionItem = document.querySelector(`.${this.selectClasses.classSelect}[data-id="${targetTag.dataset.selectId}"] .select__option[data-value="${targetTag.dataset.value}"]`);
this.optionAction(selectItem, originalSelect, optionItem);
} else if (targetElement.closest(this.getSelectClass(this.selectClasses.classSelectTitle))) {
// Обработка клика на заголовок селекта
this.selectAction(selectItem);
} else if (targetElement.closest(this.getSelectClass(this.selectClasses.classSelectOption))) {
// Обработка клика на элемент селекта
const optionItem = targetElement.closest(this.getSelectClass(this.selectClasses.classSelectOption));
this.optionAction(selectItem, originalSelect, optionItem);
}
}
} else if (targetType === 'focusin' || targetType === 'focusout') {
if (targetElement.closest(this.getSelectClass(this.selectClasses.classSelect))) {
targetType === 'focusin' ? selectItem.classList.add(this.selectClasses.classSelectFocus) : selectItem.classList.remove(this.selectClasses.classSelectFocus);
}
} else if (targetType === 'keydown' && e.code === 'Escape') {
this.selectsСlose();
}
} else {
this.selectsСlose();
}
}
// Функция закрытия всех селектов
selectsСlose(selectOneGroup) {
const selectsGroup = selectOneGroup ? selectOneGroup : document;
const selectActiveItems = selectsGroup.querySelectorAll(`${this.getSelectClass(this.selectClasses.classSelect)}${this.getSelectClass(this.selectClasses.classSelectOpen)}`);
if (selectActiveItems.length) {
selectActiveItems.forEach(selectActiveItem => {
this.selectСlose(selectActiveItem);
});
}
}
// Функция закрытия конкретного селекта
selectСlose(selectItem) {
const originalSelect = this.getSelectElement(selectItem).originalSelect;
const selectOptions = this.getSelectElement(selectItem, this.selectClasses.classSelectOptions).selectElement;
if (!selectOptions.classList.contains('_slide')) {
selectItem.classList.remove(this.selectClasses.classSelectOpen);
_slideUp(selectOptions, originalSelect.dataset.speed);
}
}
// Функция открытия/закрытия конкретного селекта
selectAction(selectItem) {
const originalSelect = this.getSelectElement(selectItem).originalSelect;
const selectOptions = this.getSelectElement(selectItem, this.selectClasses.classSelectOptions).selectElement;
// Если селекты помещенны в элемент с дата атрибутом data-one-select
// закрываем все открытые селекты
if (originalSelect.closest('[data-one-select]')) {
const selectOneGroup = originalSelect.closest('[data-one-select]');
this.selectsСlose(selectOneGroup);
}
if (!selectOptions.classList.contains('_slide')) {
selectItem.classList.toggle(this.selectClasses.classSelectOpen);
_slideToggle(selectOptions, originalSelect.dataset.speed);
}
}
// Сеттер значения заголовка селекта
setSelectTitleValue(selectItem, originalSelect) {
const selectItemBody = this.getSelectElement(selectItem, this.selectClasses.classSelectBody).selectElement;
const selectItemTitle = this.getSelectElement(selectItem, this.selectClasses.classSelectTitle).selectElement;
if (selectItemTitle) selectItemTitle.remove();
selectItemBody.insertAdjacentHTML("afterbegin", this.getSelectTitleValue(selectItem, originalSelect));
}
// Конструктор значения заголовка
getSelectTitleValue(selectItem, originalSelect) {
// Получаем выбранные текстовые значения
let selectTitleValue = this.getSelectedOptionsData(originalSelect, 2).html;
// Обработка значений мультивыбора
// Если включен режим тегов (указана настройка data-tags)
if (originalSelect.multiple && originalSelect.hasAttribute('data-tags')) {
selectTitleValue = this.getSelectedOptionsData(originalSelect).elements.map(option => `${this.getSelectElementContent(option)}`).join('');
// Если вывод тегов во внешний блок
if (originalSelect.dataset.tags && document.querySelector(originalSelect.dataset.tags)) {
document.querySelector(originalSelect.dataset.tags).innerHTML = selectTitleValue;
if (originalSelect.hasAttribute('data-search')) selectTitleValue = false;
}
}
// Значение(я) или плейсхолдер
selectTitleValue = selectTitleValue.length ? selectTitleValue : (originalSelect.dataset.placeholder ? originalSelect.dataset.placeholder : '');
// Если включен режим pseudo
let pseudoAttribute = '';
let pseudoAttributeClass = '';
if (originalSelect.hasAttribute('data-pseudo-label')) {
pseudoAttribute = originalSelect.dataset.pseudoLabel ? ` data-pseudo-label="${originalSelect.dataset.pseudoLabel}"` : ` data-pseudo-label="Заполните атрибут"`;
pseudoAttributeClass = ` ${this.selectClasses.classSelectPseudoLabel}`;
}
// Если есть значение, добавляем класс
this.getSelectedOptionsData(originalSelect).values.length ? selectItem.classList.add(this.selectClasses.classSelectActive) : selectItem.classList.remove(this.selectClasses.classSelectActive);
// Возвращаем поле ввода для поиска или текст
if (originalSelect.hasAttribute('data-search')) {
// Выводим поле ввода для поиска
return `
`;
} else {
// Если выбран элемент со своим классом
const customClass = this.getSelectedOptionsData(originalSelect).elements.length && this.getSelectedOptionsData(originalSelect).elements[0].dataset.class ? ` ${this.getSelectedOptionsData(originalSelect).elements[0].dataset.class}` : '';
// Выводим текстовое значение
return ``;
}
}
// Конструктор данных для значения заголовка
getSelectElementContent(selectOption) {
// Если для элемента указан вывод картинки или текста, перестраиваем конструкцию
const selectOptionData = selectOption.dataset.asset ? `${selectOption.dataset.asset}` : '';
const selectOptionDataHTML = selectOptionData.indexOf('img') >= 0 ? `` : selectOptionData;
let selectOptionContentHTML = ``;
selectOptionContentHTML += selectOptionData ? `` : '';
selectOptionContentHTML += selectOptionData ? `` : '';
selectOptionContentHTML += selectOptionData ? selectOptionDataHTML : '';
selectOptionContentHTML += selectOptionData ? `` : '';
selectOptionContentHTML += selectOptionData ? `` : '';
selectOptionContentHTML += selectOption.textContent;
selectOptionContentHTML += selectOptionData ? `` : '';
selectOptionContentHTML += selectOptionData ? `` : '';
return selectOptionContentHTML;
}
// Получение данных плейсхолдера
getSelectPlaceholder(originalSelect) {
const selectPlaceholder = Array.from(originalSelect.options).find(option => !option.value);
if (selectPlaceholder) {
return {
value: selectPlaceholder.textContent,
show: selectPlaceholder.hasAttribute("data-show"),
label: {
show: selectPlaceholder.hasAttribute("data-label"),
text: selectPlaceholder.dataset.label
}
}
}
}
// Получение данных из выбранных элементов
getSelectedOptionsData(originalSelect, type) {
// Получаем все выбранные объекты из select
let selectedOptions = [];
if (originalSelect.multiple) {
// Если мультивыбор
// Убираем плейсхолдер, получаем остальные выбранные элементы
selectedOptions = Array.from(originalSelect.options).filter(option => option.value).filter(option => option.selected);
} else {
// Если единичный выбор
selectedOptions.push(originalSelect.options[originalSelect.selectedIndex]);
}
return {
elements: selectedOptions.map(option => option),
values: selectedOptions.filter(option => option.value).map(option => option.value),
html: selectedOptions.map(option => this.getSelectElementContent(option))
}
}
// Конструктор элементов списка
getOptions(originalSelect) {
// Настрока скролла элементов
let selectOptionsScroll = originalSelect.hasAttribute('data-scroll') ? `data-simplebar` : '';
let selectOptionsScrollHeight = originalSelect.dataset.scroll ? `style="max-height:${originalSelect.dataset.scroll}px"` : '';
// Получаем элементы списка
let selectOptions = Array.from(originalSelect.options);
if (selectOptions.length > 0) {
let selectOptionsHTML = ``;
// Если указана настройка data-show, показываем плейсхолдер в списке
if ((this.getSelectPlaceholder(originalSelect) && !this.getSelectPlaceholder(originalSelect).show) || originalSelect.multiple) {
selectOptions = selectOptions.filter(option => option.value);
}
// Строим и выводим основную конструкцию
selectOptionsHTML += selectOptionsScroll ? `