+ 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();
+ });
+ }
+ }
+ }
+}
+
+
+