cg-select/src/cg-selectTS.ts

769 lines
23 KiB
TypeScript
Raw Normal View History

2023-01-06 13:40:18 +03:00
import {
2023-01-09 18:01:31 +03:00
createBreadCrumb,
createInputSearch,
2023-01-06 13:40:18 +03:00
createNativeSelect,
createNativeSelectOption,
} from './components/create-element/create-elementTs';
2023-01-09 18:01:31 +03:00
import { ICreateBreadCrumb } from './components/create-element/create-element.interface';
2023-01-09 17:57:56 +03:00
import {
2023-01-10 14:33:09 +03:00
clearSelect,
createSelected,
customStyles,
customStylesFormat,
getFormatItem,
getSelectText,
nativeOptionMultiple,
nativeOptionOrdinary,
} from './components/utils/utilsTs';
2023-01-10 14:33:09 +03:00
import { IDataItem, ISelectedItems } from './components/utils/urils.interface';
2023-01-09 17:57:56 +03:00
import { ICgSelect, IStyle } from './interfaces/cg-select.interface';
2023-01-05 18:21:10 +03:00
import { IItems } from './interfaces/items.interface';
2023-01-11 18:41:33 +03:00
import { ru, en } from './language/languageTS';
import { ILanguage } from './interfaces/language.interface';
2023-01-09 18:01:31 +03:00
2023-01-05 18:21:10 +03:00
import './main.scss';
2023-01-06 14:57:14 +03:00
export class CGSelect implements ICgSelect {
2023-01-05 18:21:10 +03:00
selector: string;
selected?: string;
placeholder?: string;
items?: IItems[] | string[] | any;
darkTheme?: boolean;
searchMode?: boolean;
closeOnSelect?: boolean;
nativeSelectMode?: boolean;
listDisplayMode?: boolean;
language?: string;
lable?: string;
styles?: IStyle;
2023-01-05 18:21:10 +03:00
event?: string;
url?: string;
multiselect?: boolean;
multiselectTag?: boolean;
private element: Element | null;
2023-01-06 13:40:18 +03:00
private list: Element | null | undefined;
2023-01-06 14:57:14 +03:00
private options: ICgSelect;
2023-01-06 13:40:18 +03:00
private randomId: string;
private caret: Element | null | undefined;
2023-01-05 18:21:10 +03:00
private category: string;
2023-01-06 21:14:16 +03:00
private selectedItems: string[] | string;
2023-01-05 18:21:10 +03:00
private itemsSelect: IItems[] | string[] | any;
private indexes: number[] = [];
2023-01-06 14:57:14 +03:00
constructor(setting: ICgSelect) {
2023-01-05 18:21:10 +03:00
this.init(setting);
this.render();
2023-01-06 14:07:37 +03:00
this.closeSelectClick();
this.initEvent();
2023-01-05 18:21:10 +03:00
}
2023-01-06 13:40:18 +03:00
/**
* Приватный метод инициализации экземпляра класса DropDown
2023-01-06 14:07:37 +03:00
* @method init
2023-01-06 13:40:18 +03:00
* @member
* @protected
* @param {ISgSelect} setting передаваемые настройки селекта
* @description Приватный метод. Общая инициализация селекта. Получение настоек и преобразвание элементов селекта.
* @example
* {
selector: '.cg-dropdown_one',
placeholder: 'Выберите авто',
items: [
'BMW',
{
id: '213sade',
title: 'Opel',
value: 1,
},
'Mersedes',
'MAN',
'max',
],
darkTheme: true,
multiselect: true,
multiselectTag: true,
}
*/
2023-01-06 14:57:14 +03:00
private init(setting: ICgSelect): void {
2023-01-06 13:40:18 +03:00
const { items, multiselect, url, selector } = setting;
2023-01-05 18:21:10 +03:00
this.options = setting;
const elem = document.querySelector(selector);
this.element = elem;
this.element?.addEventListener('click', (e) => {
e.preventDefault();
2023-01-06 13:40:18 +03:00
this.open();
2023-01-05 18:21:10 +03:00
});
this.itemsSelect = [];
2023-01-06 13:40:18 +03:00
2023-01-05 18:21:10 +03:00
if (!items && url) {
2023-01-06 13:40:18 +03:00
this.renderUrl();
return;
2023-01-05 18:21:10 +03:00
}
2023-01-06 13:40:18 +03:00
items.forEach((dataItem: any, index: number) => {
let itemInputs: IDataItem = {
ItemValue: dataItem,
// category: dataItem.category,
// categoryItems: dataItem.categoryItems,
2023-01-06 13:40:18 +03:00
};
if (dataItem.category && dataItem.categoryItems) {
this.category = dataItem.category!;
this.itemsSelect.push(this.category);
dataItem.categoryItems.forEach((categoryItem, indexCategory) => {
this.itemsSelect.push(getFormatItem(categoryItem, indexCategory));
});
} else {
this.itemsSelect.push(getFormatItem(itemInputs.ItemValue, index));
}
2023-01-06 13:40:18 +03:00
});
2023-01-05 18:21:10 +03:00
}
2023-01-06 13:40:18 +03:00
/**
* Приватный метод рендера экземпляра класса DropDown
* @protected
2023-01-06 14:07:37 +03:00
* @method render
2023-01-06 13:40:18 +03:00
* @param {string} select необязательный елемент. Передаеться в метод initSelected
* @description Рендер елементов в селекте.
*/
private render(select?: string): void {
const {
styles,
multiselect,
searchMode,
multiselectTag,
darkTheme,
language,
nativeSelectMode,
listDisplayMode,
} = this.options;
const random = Math.random().toString(36).substring(2, 10);
if (select || (select && styles)) {
this.initSelected(select);
customStyles(this.element!, styles);
} else {
this.initSelected();
}
2023-01-06 13:40:18 +03:00
const ulList = document.createElement('ul');
const nativeSelect = createNativeSelect();
let inputSearch: HTMLInputElement;
2023-01-06 13:40:18 +03:00
let textNode: Text;
this.randomId = random;
ulList.classList.add('list');
if (styles) {
const { list } = styles;
customStylesFormat(list!, ulList);
}
if (searchMode) {
if (language === 'ru') {
inputSearch = createInputSearch(random, ru.placeholder);
} else {
inputSearch = createInputSearch(random, en.placeholder);
}
const { search } = styles!;
customStylesFormat(search!, inputSearch);
ulList.appendChild(inputSearch);
}
2023-01-06 13:40:18 +03:00
this.element?.appendChild(ulList);
this.itemsSelect.forEach((dataItem: IItems | any) => {
this.element?.appendChild(nativeSelect);
const liItem = document.createElement('li');
const nativeOption = createNativeSelectOption();
const strongItem = document.createElement('strong');
liItem.classList.add('list__item');
strongItem.classList.add('category');
2023-01-06 21:14:16 +03:00
if (multiselect) {
2023-01-06 13:40:18 +03:00
const checkBox = document.createElement('input');
checkBox.type = 'checkbox';
checkBox.setAttribute('id', `chbox-${dataItem.id}`);
liItem.appendChild(checkBox);
2023-01-06 21:14:16 +03:00
if (multiselectTag) {
2023-01-06 13:40:18 +03:00
checkBox.classList.add('displayHide');
}
nativeSelect.setAttribute('multiple', 'multiple');
}
if (dataItem.title) {
nativeOption.text = dataItem.title;
nativeOption.value = dataItem.title;
textNode = document.createTextNode(dataItem.title);
nativeSelect.appendChild(nativeOption);
liItem.appendChild(textNode);
ulList.appendChild(liItem);
} else {
// Для отрисовки категорий
textNode = document.createTextNode(dataItem);
strongItem.appendChild(textNode);
ulList.appendChild(strongItem);
}
});
this.itemsSelect.filter((item, index) => {
if (typeof item !== 'object') {
this.itemsSelect.splice(index, 1);
}
return item;
});
this.list = this.element!.querySelector('.list');
this.caret = this.element!.querySelector('.caret');
if (darkTheme == false) {
this.checkTheme();
}
if (nativeSelectMode === true) {
this.selectMode(nativeSelectMode);
}
if (listDisplayMode) {
this.displayMode(listDisplayMode);
}
2023-01-06 13:40:18 +03:00
2023-01-06 14:07:37 +03:00
this.addOptionsBehaviour();
}
/**
* Приватный метод рендера экземпляра класса DropDown
* @protected
* @method renderUrl
* @description Рендер елементов в селекте переданных с URL и их настойка
*/
private async renderUrl() {
const { url, items, multiselect, multiselectTag } = this.options;
if (items) {
return;
}
if (!url) {
return;
}
const response = await fetch(url);
const dataUrl = await response.json();
const nativeSelect = createNativeSelect();
dataUrl.forEach((dataItem: IItems, index: number) => {
const item = {
id: dataItem.id,
title: dataItem.title,
value: index,
};
const ulUrl = this.element!.querySelector('.list');
const nativeOption = createNativeSelectOption();
const liUrl = document.createElement('li');
const textUrl = document.createTextNode(item.title);
if (multiselect) {
const checkBox = document.createElement('input');
checkBox.type = 'checkbox';
if (multiselectTag) {
checkBox.classList.add('displayHide');
}
checkBox.setAttribute('id', `chbox-${item.id}`);
nativeSelect.setAttribute('multiple', 'multiple');
liUrl.appendChild(checkBox);
}
liUrl.classList.add('list__item');
nativeOption.value = item.title;
nativeOption.text = item.title;
nativeSelect.appendChild(nativeOption);
liUrl.appendChild(textUrl);
ulUrl!.appendChild(liUrl);
this.itemsSelect.push(item);
});
this.element!.appendChild(nativeSelect);
this.itemsSelect.filter((item, index) => {
if (typeof item !== 'object') {
this.itemsSelect.splice(index, 1);
}
return item;
});
this.addOptionsBehaviour();
}
2023-01-06 13:40:18 +03:00
/**
* Привaтный метод экземпляра класса DropDown
*
2023-01-06 14:07:37 +03:00
* @method initSelected
2023-01-06 13:40:18 +03:00
* @param {string} select необязательный елемент. Используется в методе selectIndex
* @description Отрисовывает и стилизует селект
* @protected
*/
private initSelected(select?: string): void {
const { styles, selected, placeholder, lable, language } = this.options;
if (selected) {
createSelected(this.element, selected);
} else if (placeholder) {
createSelected(this.element, placeholder);
} else {
if (language && language === 'ru') {
// createSelected(this.#element, ru.selectPlaceholder);
} else {
// createSelected(this.#element, en.selectPlaceholder);
}
}
if (select) {
createSelected(this.element, select, styles);
}
if (lable) {
const lableItem = document.createElement('h1');
const textLable = document.createTextNode(lable);
lableItem.appendChild(textLable);
lableItem.classList.add('label');
this.element!.insertAdjacentElement('beforebegin', lableItem);
}
if (styles) {
customStyles(this.element!, styles);
}
}
2023-01-06 13:40:18 +03:00
/**
* Приватный метод экземпляра класса DropDown
* @protected
* @description Открывает и закрывает список по переданному эвенту
* @method initEvent
*/
private 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();
});
}
}
}
2023-01-06 13:40:18 +03:00
/**
* Приватный метод экземпляра класса DropDown
* @protected
* @param {boolean} oneClick необязательный параметр передаваемый из функции buttonControl
* @description Открывает список для выбора элемента
2023-01-06 14:07:37 +03:00
* @method open
2023-01-06 13:40:18 +03:00
*/
private open(oneClick?: boolean): void {
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');
}
}
2023-01-06 14:07:37 +03:00
/**
* Приватный метод экземпляра класса DropDown
* @protected
* @description Закрывает список
* @method close
2023-01-06 14:07:37 +03:00
*/
private close(): void {
2023-01-06 14:07:37 +03:00
this.list?.classList.remove('open');
this.caret?.classList.remove('caret_rotate');
}
/**
* Приватный метод экземпляра класса DropDown
* @protected
* @description Закрывает список по клику вне элемента
2023-01-06 14:57:14 +03:00
* @method closeSelectClick
2023-01-06 14:07:37 +03:00
*/
private closeSelectClick(): void {
2023-01-06 14:07:37 +03:00
const dropdown = document.querySelector(`${this.options.selector}`);
document.addEventListener('click', (e) => {
const withinBoundaries = e.composedPath().includes(dropdown!);
if (!withinBoundaries) {
// if (this.btn) {
// return;
// } else {
// this.#close();
// }
this.close();
}
});
}
/**
* Приватный метод экземпляра класса DropDown
* @protected
* @description Метод реализовывающий выбор элементов в разных режимах. Обычный/Мультиселект/Мультиселект + Мультиселект Таг.
* @method addOptionsBehaviour
*/
private addOptionsBehaviour() {
const {
multiselect,
placeholder,
selected,
multiselectTag,
searchMode,
closeOnSelect,
darkTheme,
} = this.options;
const options = this.element?.querySelectorAll('.list__item');
2023-01-06 21:14:16 +03:00
const select: HTMLElement | null | undefined = this.element?.querySelector('.selected');
2023-01-10 14:33:09 +03:00
const nativeOption = this.element!.querySelectorAll('.nativeSelect__nativeOption');
2023-01-06 14:07:37 +03:00
2023-01-10 14:33:09 +03:00
let selectedItemsClear: ISelectedItems;
2023-01-06 14:07:37 +03:00
const ulMultipul = document.createElement('ul');
2023-01-06 21:14:16 +03:00
if (multiselect) {
this.selectedItems = [];
2023-01-06 14:07:37 +03:00
ulMultipul.classList.add('multiselect-tag');
select?.classList.add('overflow-hidden');
}
if (searchMode) {
this.searchModeSelect(this.randomId);
}
2023-01-06 14:07:37 +03:00
options?.forEach((option: Element, index: number) => {
option.addEventListener('click', (event) => {
2023-01-10 14:33:09 +03:00
if (Array.isArray(this.selectedItems)) {
selectedItemsClear = {
placeholder: placeholder!,
selected: selected!,
selectedItems: this.selectedItems,
indexes: this.indexes,
darkTheme: darkTheme,
multiselectTag: multiselectTag,
};
}
2023-01-06 21:14:16 +03:00
const item: IItems = this.itemsSelect[index];
2023-01-06 14:07:37 +03:00
const checkIndex = this.indexes.indexOf(index);
2023-01-06 21:14:16 +03:00
if (closeOnSelect == false || multiselect) {
event.stopPropagation();
event.preventDefault();
}
if (multiselect) {
option.classList.toggle('active');
const checkBox: HTMLInputElement | null = option.querySelector('input[type="checkbox"]');
if (checkBox) {
if (!(event.target instanceof HTMLInputElement)) {
checkBox.checked = !checkBox.checked;
}
if (checkIndex == -1) {
this.indexes.push(index);
nativeOptionMultiple(nativeOption, item.title, true);
select!.textContent = '';
2023-01-09 17:57:56 +03:00
if (multiselectTag) {
if (Array.isArray(this.selectedItems)) {
const dataBreadCrumb: ICreateBreadCrumb = {
option: this.options,
element: this.element,
indexes: this.indexes,
selectedItems: this.selectedItems,
};
this.selectedItems.push(item.title);
select!.appendChild(ulMultipul);
ulMultipul.appendChild(
2023-01-09 18:01:31 +03:00
createBreadCrumb(dataBreadCrumb, item.title, index, item.id),
2023-01-09 17:57:56 +03:00
);
}
} else {
if (Array.isArray(this.selectedItems)) {
this.selectedItems.push(item.title);
select!.innerText = this.selectedItems.join(',');
}
}
} else {
2023-01-09 17:57:56 +03:00
if (multiselectTag) {
const tagItem = document.getElementById(`tag-${index}-${item.id}`);
ulMultipul.removeChild<Element>(tagItem!);
}
if (Array.isArray(this.selectedItems)) {
this.selectedItems.splice(checkIndex, 1);
2023-01-09 17:57:56 +03:00
this.indexes.splice(checkIndex, 1);
nativeOptionMultiple(nativeOption, item.title, false);
}
}
2023-01-09 17:57:56 +03:00
if (!this.selectedItems.length) {
2023-01-10 14:33:09 +03:00
getSelectText(selectedItemsClear, select);
2023-01-09 17:57:56 +03:00
} else {
if (multiselectTag) {
select!.appendChild(ulMultipul);
} else {
if (Array.isArray(this.selectedItems)) {
select!.innerText = this.selectedItems.join(',');
}
}
}
}
} else {
select!.textContent = item.title;
this.selectedItems = item.title;
nativeOptionOrdinary(nativeOption, item.title);
options.forEach((option) => {
option.classList.remove('active');
});
option.classList.add('active');
}
2023-01-10 14:33:09 +03:00
clearSelect(select!, this.element!, selectedItemsClear);
2023-01-06 14:07:37 +03:00
});
});
}
/**
* Приватный метод рендера экземпляра класса DropDown
* @protected
* @method #checkTheme
* @description Изменяет цветовую схему с темной на светлую.
*/
private checkTheme(): void {
const { darkTheme, searchMode } = this.options;
const select = this.element!.querySelector('.cg-select');
const caret = this.element!.querySelector('.caret');
const list = this.element!.querySelector('ul.list');
const search = this.element!.querySelector('.inputSearch');
if (darkTheme == false) {
select!.classList.add('selectWhite');
caret!.classList.add('caretWhite');
list!.classList.add('listWhite');
if (searchMode == true) {
search!.classList.add('inputWhite');
}
} else if (darkTheme == true || !darkTheme) {
return;
} else {
throw new Error('Styles error or invalid value entered!');
}
}
/**
* Приватный метод экземпляра класса DropDown
* @protected
* @param {boolean} nativeSelectMode параметр отвечающий за добавления нативного селекта.
* @description Изменяет отображение селекта на мобильных устройствах
* @method selectMode
*/
private selectMode(nativeSelectMode: boolean) {
let win = window.outerWidth;
if (nativeSelectMode === true) {
const select = this.element!.querySelector('.cg-select');
const list = this.element!.querySelector('.list');
const nativeSelect = this.element!.querySelector('.nativeSelect');
if (win < 576) {
select!.classList.add('displayHide');
list!.classList.add('displayHide');
nativeSelect!.classList.add('nativeSelectActive');
} else if (win > 576) {
select!.classList.remove('displayHide');
list!.classList.remove('displayHide');
nativeSelect!.classList.remove('nativeSelectActive');
nativeSelect!.classList.add('displayHide');
}
} else {
return;
}
}
/**
* Метод который реализует поиск элементов в селекте
* @protected
* @param {string} random уникальное значение для input элемента.
* @method searchMode
*/
private searchModeSelect(random: string) {
const { language } = this.options;
const input = this.element!.querySelector(`#searchSelect-${random}`) as HTMLInputElement;
const searchSelect = this.element!.querySelectorAll('.list__item');
const result = document.createElement('p');
let textNode: Text;
if (language && language === 'ru') {
textNode = document.createTextNode(`${ru.textInListSearch}`);
} else {
textNode = document.createTextNode(`${en.textInListSearch}`);
}
result.appendChild(textNode);
result.classList.add('displayHide');
result.classList.add('noRezult');
input!.parentElement!.appendChild(result);
input!.addEventListener('click', (e) => {
e.stopPropagation();
});
if (input instanceof HTMLInputElement) {
input!.oninput = function () {
let valueSearch: string = input.value.trim().toLowerCase();
let anyMatch = false;
if (valueSearch != '') {
searchSelect.forEach((elem) => {
let isMatching = new RegExp(valueSearch, 'gi').test(elem.textContent!);
anyMatch = anyMatch || isMatching;
if (elem.textContent!.toLowerCase().search(valueSearch) == -1) {
elem.classList.add('displayHide');
} else {
elem.classList.remove('displayHide');
}
});
result.classList.toggle('displayHide', anyMatch);
} else {
searchSelect.forEach((elem) => {
elem.classList.remove('displayHide');
result.classList.add('displayHide');
});
}
};
}
}
2023-01-11 18:41:33 +03:00
/**
* Приватный метод экземпляра класса DropDown
* @protected
* @param {boolean} listDisplayMode параметр отвечающий за отображение выбора в виде модального окна.
* @description Изменяет отображение листа с выбором в виде модального окна.
* @method displayMode
*/
private displayMode(listDisplayMode: boolean): void {
if (listDisplayMode) {
const modal = document.createElement('div');
const body = document.querySelector('body');
const list = this.list!;
modal.appendChild(list);
this.element!.appendChild(modal);
this.element!.addEventListener('click', () => {
modal.classList.toggle('modal');
list.classList.toggle('listModal');
body!.classList.toggle('overflowHide');
});
} else {
return;
}
}
// Public methods
/**
* Метод экземпляра класса DropDown
* @param {object} language объект в котором находятся поля для подключения языка имеет два обязательных поля placeholder, textInListSearch
* @description метод позволяющий заменить плейсхолдер в поиске и текст который выводится если нет результата
* @method addLanguage
*/
public addLanguage(language: ILanguage) {
const { placeholder, textInListSearch, selectPlaceholder } = language;
const { searchMode } = this.options;
const select = this.element!.querySelector('.selected');
const textNodeSelect = document.createTextNode(selectPlaceholder);
select!.appendChild(textNodeSelect);
if (searchMode) {
const search = this.element!.querySelector('.inputSearch');
const textNoRezult = this.element!.querySelector('.noRezult');
const textNode = document.createTextNode(textInListSearch);
search!.setAttribute('placeholder', placeholder);
search!.setAttribute('placeholder', placeholder);
textNoRezult!.textContent = '';
textNoRezult!.appendChild(textNode);
}
}
/**
* Метод экземпляра класса DropDown
* @param {HTMLInputElement} button - HTML кнопка
* @param {string} method - метод открытия open/close
* @description Метод позволяющий открывать/закрывать селект с помощью кнопок
* @method buttonControl
*/
public buttonControl(button, method: string) {
const { listDisplayMode } = this.options;
if (listDisplayMode === true) {
return;
}
// this.btn = button;
button.addEventListener('click', () => {
if (method.toLowerCase() === 'open') {
this.open(true);
} else if (method.toLowerCase() === 'close') {
this.close();
} else {
return;
}
});
}
2023-01-05 18:21:10 +03:00
}