// Подключение функционала "Чертогов Фрилансера" 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 ? `
` : ''; selectOptions.forEach(selectOption => { // Получаем конструкцию конкретного элемента списка selectOptionsHTML += this.getOption(selectOption, originalSelect); }); selectOptionsHTML += selectOptionsScroll ? `
` : ''; return selectOptionsHTML; } } // Конструктор конкретного элемента списка getOption(selectOption, originalSelect) { // Если элемент выбран и включен режим мультивыбора, добавляем класс const selectOptionSelected = selectOption.selected && originalSelect.multiple ? ` ${this.selectClasses.classSelectOptionSelected}` : ''; // Если элемент выбрани и нет настройки data-show-selected, скрываем элемент const selectOptionHide = selectOption.selected && !originalSelect.hasAttribute('data-show-selected') && !originalSelect.multiple ? `hidden` : ``; // Если для элемента указан класс добавляем const selectOptionClass = selectOption.dataset.class ? ` ${selectOption.dataset.class}` : ''; // Если указан режим ссылки const selectOptionLink = selectOption.dataset.href ? selectOption.dataset.href : false; const selectOptionLinkTarget = selectOption.hasAttribute('data-href-blank') ? `target="_blank"` : ''; // Строим и возвращаем конструкцию элемента let selectOptionHTML = ``; selectOptionHTML += selectOptionLink ? `` : ``; return selectOptionHTML; } // Сеттер элементов списка (options) setOptions(selectItem, originalSelect) { // Получаем объект тела псевдоселекта const selectItemOptions = this.getSelectElement(selectItem, this.selectClasses.classSelectOptions).selectElement; // Запускаем конструктор элементов списка (options) и добавляем в тело псевдоселекта selectItemOptions.innerHTML = this.getOptions(originalSelect); } // Обработчик клика на элемент списка optionAction(selectItem, originalSelect, optionItem) { if (originalSelect.multiple) { // Если мультивыбор // Выделяем классом элемент optionItem.classList.toggle(this.selectClasses.classSelectOptionSelected); // Очищаем выбранные элементы const originalSelectSelectedItems = this.getSelectedOptionsData(originalSelect).elements; originalSelectSelectedItems.forEach(originalSelectSelectedItem => { originalSelectSelectedItem.removeAttribute('selected'); }); // Выбираем элементы const selectSelectedItems = selectItem.querySelectorAll(this.getSelectClass(this.selectClasses.classSelectOptionSelected)); selectSelectedItems.forEach(selectSelectedItems => { originalSelect.querySelector(`option[value="${selectSelectedItems.dataset.value}"]`).setAttribute('selected', 'selected'); }); } else { // Если единичный выбор // Если не указана настройка data-show-selected, скрываем выбранный элемент if (!originalSelect.hasAttribute('data-show-selected')) { // Сначала все показать if (selectItem.querySelector(`${this.getSelectClass(this.selectClasses.classSelectOption)}[hidden]`)) { selectItem.querySelector(`${this.getSelectClass(this.selectClasses.classSelectOption)}[hidden]`).hidden = false; } // Скрываем выбранную optionItem.hidden = true; } originalSelect.value = optionItem.hasAttribute('data-value') ? optionItem.dataset.value : optionItem.textContent; this.selectAction(selectItem); } // Обновляем заголовок селекта this.setSelectTitleValue(selectItem, originalSelect); // Вызываем реакцию на изменение селекта this.setSelectChange(originalSelect); } // Реакция на измененение оригинального select selectChange(e) { const originalSelect = e.target; this.selectBuild(originalSelect); this.setSelectChange(originalSelect); } // Обработчик изменения в селекте setSelectChange(originalSelect) { // Моментальная валидация селекта if (originalSelect.hasAttribute('data-validate')) { formValidate.validateInput(originalSelect); } // При изменении селекта отправляем форму if (originalSelect.hasAttribute('data-submit') && originalSelect.value) { let tempButton = document.createElement("button"); tempButton.type = "submit"; originalSelect.closest('form').append(tempButton); tempButton.click(); tempButton.remove(); } const selectItem = originalSelect.parentElement; // Вызов коллбэк функции this.selectCallback(selectItem, originalSelect); } // Обработчик disabled selectDisabled(selectItem, originalSelect) { if (originalSelect.disabled) { selectItem.classList.add(this.selectClasses.classSelectDisabled); this.getSelectElement(selectItem, this.selectClasses.classSelectTitle).selectElement.disabled = true; } else { selectItem.classList.remove(this.selectClasses.classSelectDisabled); this.getSelectElement(selectItem, this.selectClasses.classSelectTitle).selectElement.disabled = false; } } // Обработчик поиска по элементам списка searchActions(selectItem) { const originalSelect = this.getSelectElement(selectItem).originalSelect; const selectInput = this.getSelectElement(selectItem, this.selectClasses.classSelectInput).selectElement; const selectOptions = this.getSelectElement(selectItem, this.selectClasses.classSelectOptions).selectElement; const selectOptionsItems = selectOptions.querySelectorAll(`.${this.selectClasses.classSelectOption}`); const _this = this; selectInput.addEventListener("input", function () { selectOptionsItems.forEach(selectOptionsItem => { if (selectOptionsItem.textContent.toUpperCase().indexOf(selectInput.value.toUpperCase()) >= 0) { selectOptionsItem.hidden = false; } else { selectOptionsItem.hidden = true; } }); // Если список закрыт открываем selectOptions.hidden === true ? _this.selectAction(selectItem) : null; }); } // Коллбэк функция selectCallback(selectItem, originalSelect) { document.dispatchEvent(new CustomEvent("selectCallback", { detail: { select: originalSelect } })); } // Логгинг в консоль setLogging(message) { this.config.logging ? FLS(`[select]: ${message}`) : null; } } // Запускаем и добавляем в объект модулей flsModules.select = new SelectConstructor({});