import { createSelected, customStyles, getFormatItem, customStylesFormat, } from './components/utils'; import { createBreadcrumb } from './components/create-element'; /** * @class Описание класса DropDown * @description Этот класс реализовывает функционал кастомного селекта, с возможностями кастомизации. *@author Овсяников Максим */ export class DropDown { /** * Созданный HTML елемент * @type {HTMLElement} */ #element; /** * Созданный список(ul), с классом list * @type {HTMLElement} */ #list; /** * Настройки селекта передаваемые при создании экземпляра класса * @type {object} */ #options; /** * Переменная для управления каретки * @type {HTMLElement} */ #caret; /** * Массив переданных элементов * @type {object[]} */ #items; /** * Переданные категории * @type {string} */ #category; /** * Выбранный или массив выбранных элементов из списка * @type {object[] | object} */ #selectedItems; /** * Массив индексов выбранных элементов * @type {number[]} */ #indexes = []; /** * Метод экземпляра класса DropDown * @returns {string[] | string | null} Возвращает выбранные элемент(ы) в виде массива/элемента/null * @description Геттер возвращающий выбранные элемент(ы) селекта */ get value() { return this.#selectedItems ?? null; } /** * Метод экземпляра класса DropDown * @returns {number | number[]}Возвращает индексы выбранных элемента(ов) в виде массива/пустой массив * @description Геттер возвращающий индексы выбранных элемента(ов) селекта */ get indexes() { return this.#indexes ?? []; } /** * * @param {object} options Объект принимающий настройки селекта * @constructor Конструктор класса DropDown * @description Конструктор принимает объект и рендерит селект. * @example * options = { * selector: 'Уникальный селектор', selected: 'Выбранный элемент', placeholder: '...', items: [string|number|object], styles: { head: { background: '...', }, list: {...}, chips: {...}, caret: {...}, placeholder: {...}, }, event: '...', url: 'http/...', multiselect: true/false, multiselectTag: true/false, * } */ constructor(options = {}) { this.#init(options); this.#render(); this.#initEvent(); } /** * Метод экземпляра класса DropDown * @param {string | object} item добавляемый елемент * @description добавляет переданный элемент в конец списка и перерисовывает список. Не может использоваться при передачи элементов с категорями * @method addItem */ addItem(item) { if (this.#category) { console.log('can`t add item to category'); return; } if (!item) { return false; } const index = this.#items.length; this.#items.push(getFormatItem(item, index)); this.#render(); } /** * Метод экземпляра класса DropDown * @param {number} index индекс удаляемого элемента * @description удаляет елемент по индексу из списка и перерисовывает его. Не может использоваться при передачи элементов с категорями. * @method deleteItem */ deleteItem(index) { if (this.#category) { console.log('can`t add item to category'); return; } const item = this.#items[index]; this.#items.splice(index, 1); this.#render(); } /** * Метод экземпляра класса DropDown * @description удаляет все елементы из списка и перерисовывает его. * @method deleteItemAll */ deleteItemAll() { this.#items.splice(0, this.#items.length); this.#render(); } /** * Метод экземпляра класса DropDown * @param {number} index индекс выбранного элемента * @description выбирает элемент который будет изначально отрисовываться в селекте * @method selectIndex */ selectIndex(index) { if (this.#category) { console.log('can`t add item to category'); return; } const options = this.#element.querySelectorAll('.list__item'); if (index > options.length) { return; } const select = options[index].innerText; this.#render(select); } /** * Метод экземпляра класса DropDown * @param {number} numberItem номер возвращаемого элемента * @returns {HTMLElement} возвращает ссылку на выбранный HTML элемент * @method getElement */ getElement(numberItem) { if (numberItem > this.#items.length) { return; } return this.#items[numberItem]; } /** * Метод экземпляра класса DropDown * @param {boolean} value - Передаваемый параметр для добавления атрибута disabled; * @description Метод позволяющий переключать состояние селекта disabled, * @method disabled */ disabled(value) { if (typeof value !== 'boolean') { return; } const select = this.#element.querySelector('.cg-select'); if (value === true) { this.#element.setAttribute('disabled', true); select.classList.add('disabled'); } else { this.#element.removeAttribute('disabled'); select.classList.remove('disabled'); } } /** * Метод экземпляра класса DropDown * @param {HTMLInputElement} button - HTML кнопка * @param {string} method - метод открытия open/close * @description Метод позволяющий открывать/закрывать селект с помощью кнопок * @method buttonControl */ buttonControl(button, method) { button.addEventListener('click', () => { if (method === 'open') { this.#open(true); } else if (method === 'close') { this.#close(); } else { return; } }); } /** * Приватный метод инициализации экземпляра класса DropDown * @method #init * @member * @protected * @param {object} options передаваемые настройки селекта * @description Приватный метод. Общая инициализация селекта. Получение настоек и преобразвание элементов селекта. * @example * { selector: '.cg-dropdown_one', placeholder: 'Выберите авто', items: [ 'BMW', { id: '213sade', title: 'Opel', value: 1, }, 'Mersedes', 'MAN', 'max', ], multiselect: true, multiselectTag: true, } */ #init(options) { this.#options = options; const { items, multiselect, url } = this.#options; const elem = document.querySelector(options.selector); if (!elem) { throw new Error(`Element with selector ${options.selector}`); } this.#element = elem; this.#element.addEventListener('click', () => { this.#open(); }); this.#items = []; if (multiselect) { this.#selectedItems = []; } if (!items && url) { this.#renderUrl(); return; } items.forEach((dataItem, index) => { if (dataItem.category && dataItem.categoryItems) { this.#category = dataItem.category; this.#items.push(this.#category); dataItem.categoryItems.forEach((categoryItem, indexCategory) => { this.#items.push(getFormatItem(categoryItem, indexCategory)); }); } else { this.#items.push(getFormatItem(dataItem, index)); } }); } /** * Привaтный метод экземпляра класса DropDown * * @method #initSelected * @param {string} select необязательный елемент. Используется в методе selectIndex * @description Отрисовывает и стилизует селект * @protected */ #initSelected(select) { const { styles, selected, placeholder } = this.#options; if (selected) { createSelected(this.#element, selected); } else if (placeholder) { createSelected(this.#element, placeholder); } else { createSelected(this.#element, 'Select...'); } if (styles) { customStyles(this.#element, styles); } if (select) { createSelected(this.#element, select, styles); } } /** * Приватный метод рендера экземпляра класса DropDown *@protected * @method #render * @param {string} select необязательный елемент. Передаеться в метод initSelected * @description Рендер елементов в селекте. */ #render(select) { const { styles, multiselect } = this.#options; if (select || (select && styles)) { this.#initSelected(select); customStyles(this.#element, styles); } else { this.#initSelected(); } const ulList = document.createElement('ul'); ulList.classList.add('list'); if (styles) { const { list } = styles; customStylesFormat(list, ulList); } this.#element.appendChild(ulList); this.#items.forEach((dataItem) => { const liItem = document.createElement('li'); const strongItem = document.createElement('strong'); liItem.classList.add('list__item'); strongItem.classList.add('category'); if (multiselect) { const checkBox = document.createElement('input'); checkBox.type = 'checkbox'; checkBox.setAttribute('id', `chbox-${dataItem.id}`); liItem.appendChild(checkBox); } let textNode = ''; if (dataItem.title) { textNode = document.createTextNode(dataItem.title); liItem.appendChild(textNode); ulList.appendChild(liItem); } else { textNode = document.createTextNode(dataItem); strongItem.appendChild(textNode); ulList.appendChild(strongItem); } }); this.#items.filter((item, index) => { if (typeof item !== 'object') { this.#items.splice(index, 1); } return item; }); this.#addOptionsBehaviour(); } /** * Приватный метод рендера экземпляра класса DropDown *@protected * @method #renderUrl * @description Рендер елементов в селекте переданных с URL и их настойка */ async #renderUrl() { const { url, items, multiselect } = this.#options; if (items) { return; } if (!url) { return; } const response = await fetch(url); const dataUrl = await response.json(); dataUrl.forEach((dataItem, index) => { const item = { id: dataItem.id, title: dataItem.name, value: index, }; const ulUrl = this.#element.querySelector('.list'); const liUrl = document.createElement('li'); const textUrl = document.createTextNode(item.title); if (multiselect) { const checkBox = document.createElement('input'); checkBox.type = 'checkbox'; checkBox.setAttribute('id', `chbox-${item.id}`); liUrl.appendChild(checkBox); } liUrl.classList.add('list__item'); liUrl.appendChild(textUrl); ulUrl.appendChild(liUrl); this.#items.push(item); }); this.#items.filter((item, index) => { if (typeof item !== 'object') { this.#items.splice(index, 1); } return item; }); this.#addOptionsBehaviour(); } /** * Приватный метод экземпляра класса DropDown * @protected * @param {boolean} oneClick необязательный параметр передаваемый из функции buttonControl * @description Открывает список для выбора элемента * @method #open */ #open(oneClick) { this.#list = this.#element.querySelector('.list'); this.#caret = this.#element.querySelector('.caret'); if (oneClick === true) { this.#list.classList.add('open'); this.#caret.classList.add('caret_rotate'); } else { this.#list.classList.toggle('open'); this.#caret.classList.toggle('caret_rotate'); } } /** * Приватный метод экземпляра класса DropDown * @protected * @description Закрывает список * @method #close */ #close() { this.#list.classList.remove('open'); this.#caret.classList.remove('caret_rotate'); } /** * Приватный метод экземпляра класса DropDown * @protected * @description Метод реализовывающий выбор элементов в разных режимах. Обычный/Мультиселект/Мультиселект + Мультиселект Таг. * @method #addOptionsBehaviour */ #addOptionsBehaviour() { const { multiselect, placeholder, selected, multiselectTag } = this.#options; const options = this.#element.querySelectorAll('.list__item'); const select = this.#element.querySelector('.selected'); const ul = document.createElement('ul'); if (multiselect) { ul.classList.add('multiselect-tag'); select.classList.add('overflow-hidden'); } options.forEach((option, index) => { option.addEventListener('click', (event) => { const item = this.#items[index]; if (multiselect) { event.stopPropagation(); option.classList.toggle('active'); const checkBox = option.querySelector('input[type="checkbox"]'); if (checkBox) { if (!(event.target instanceof HTMLInputElement)) { checkBox.checked = !checkBox.checked; } const checkIndex = this.#indexes.indexOf(index); if (checkIndex === -1) { this.#indexes.push(index); select.innerText = ''; if (multiselectTag) { this.#selectedItems.push(item); select.appendChild(ul); const data = { option: this.#options, element: this.#element, indexes: this.#indexes, selectedItems: this.#selectedItems, }; ul.appendChild(createBreadcrumb(data, item.title, index, item.id)); } else { this.#selectedItems.push(item.title); select.innerText = this.#selectedItems; } } else { if (multiselectTag) { const tagItem = document.getElementById(`tag-${index}-${item.id}`); ul.removeChild(tagItem); } this.#indexes.splice(checkIndex, 1); this.#selectedItems.splice(checkIndex, 1); } if (!this.#selectedItems.length) { if (placeholder) { select.innerText = placeholder; } else if (selected) { select.innerText = selected; } else { select.innerText = 'Select...'; } } else { if (multiselectTag) { select.appendChild(ul); } else { select.innerText = this.#selectedItems; } } } } else { select.innerText = item.title; this.#selectedItems = item; options.forEach((option) => { option.classList.remove('active'); }); option.classList.add('active'); } }); }); } /** * Приватный метод экземпляра класса DropDown * @protected * @description Открывает и закрывает список по переданному эвенту * @method #initEvent */ #initEvent() { const { event } = this.#options; if (!event) { return; } if (event) { if (event === 'mouseenter') { this.#element.addEventListener(event, () => { this.#open(); }); this.#element.addEventListener('mouseleave', () => { this.#close(); }); } } } }