/*
 * HSSelect
 * @version: 4.2.0
 * @author: Preline Labs Ltd.
 * @license: Licensed under MIT and Preline UI Fair Use License (https://preline.co/docs/license.html)
 * Copyright 2024 Preline Labs Ltd.
 */

import {
	afterTransition,
	classToClassList,
	debounce,
	dispatch,
	htmlToElement,
	isEnoughSpace,
} from '../../utils';

import {
	IApiFieldMap,
	ISelect,
	ISelectOptions,
	ISingleOption,
	ISingleOptionOptions,
} from '../select/interfaces';

import HSBasePlugin from '../base-plugin';
import { ICollectionItem } from '../../interfaces';
import { IAccessibilityComponent } from '../accessibility-manager/interfaces';
import HSAccessibilityObserver from '../accessibility-manager';

import { POSITIONS } from '../../constants';

class HSSelect extends HSBasePlugin<ISelectOptions> implements ISelect {
	private static globalListenersInitialized = false;

	private accessibilityComponent: IAccessibilityComponent;

	value: string | string[] | null;
	private readonly placeholder: string | null;
	private readonly hasSearch: boolean;
	private readonly minSearchLength: number;
	private readonly preventSearchFocus: boolean;
	private readonly preventSearchInsideDescription: boolean;
	private readonly mode: string | null;
	private readonly viewport: HTMLElement | null;
	private readonly scrollToSelected: boolean;

	private _isOpened: boolean | null;
	isMultiple: boolean | null;
	isDisabled: boolean | null;
	selectedItems: string[];

	private readonly apiUrl: string | null;
	private readonly apiQuery: string | null;
	private readonly apiOptions: RequestInit | null;
	private readonly apiDataPart: string | null;
	private readonly apiSearchQueryKey: string | null;
	private readonly apiLoadMore:
		| boolean
		| {
				perPage: number;
				scrollThreshold: number;
		  };
	private readonly apiFieldsMap: IApiFieldMap | null;
	private readonly apiIconTag: string | null;
	private readonly apiSelectedValues: string | string[] | null;

	private readonly toggleTag: string | null;
	private readonly toggleClasses: string | null;
	private readonly toggleSeparators: {
		items?: string;
		betweenItemsAndCounter?: string;
	} | null;
	private readonly toggleCountText: string | null;
	private readonly toggleCountTextPlacement:
		| 'postfix'
		| 'prefix'
		| 'postfix-no-space'
		| 'prefix-no-space';
	private readonly toggleCountTextMinItems: number | null;
	private readonly toggleCountTextMode: string | null;
	private readonly wrapperClasses: string | null;
	private readonly tagsItemTemplate: string | null;
	private readonly tagsItemClasses: string | null;
	private readonly tagsInputId: string | null;
	private readonly tagsInputClasses: string | null;
	private readonly dropdownTag: string | null;
	private readonly dropdownClasses: string | null;
	private readonly dropdownDirectionClasses: {
		top?: string;
		bottom?: string;
	} | null;
	public dropdownSpace: number | null;
	public readonly dropdownPlacement: string | null;
	private readonly dropdownAutoPlacement: boolean;
	public readonly dropdownVerticalFixedPlacement: 'top' | 'bottom' | null;
	public readonly dropdownScope: 'window' | 'parent';
	private readonly searchTemplate: string | null;
	private readonly searchWrapperTemplate: string | null;
	private readonly searchPlaceholder: string | null;
	private readonly searchId: string | null;
	private readonly searchLimit: number | typeof Infinity;
	private readonly isSearchDirectMatch: boolean;
	private readonly searchMatchMode:
		| 'substring'
		| 'chars-sequence'
		| 'token-all'
		| 'hybrid';
	private readonly searchClasses: string | null;
	private readonly searchWrapperClasses: string | null;
	private readonly searchNoResultTemplate: string | null;
	private readonly searchNoResultText: string | null;
	private readonly searchNoResultClasses: string | null;
	private readonly optionAllowEmptyOption: boolean;
	private readonly optionTag: string | null;
	private readonly optionTemplate: string | null;
	private readonly optionClasses: string | null;
	private readonly optgroupTag: string | null;
	private readonly optgroupClasses: string | null;
	private readonly descriptionClasses: string | null;
	private readonly iconClasses: string | null;

	private animationInProcess: boolean;
	private currentPage: number;
	private isLoading: boolean;
	private hasMore: boolean;
	private hasOptgroup: boolean;

	private wrapper: HTMLElement | null;
	private toggle: HTMLElement | null;
	private toggleTextWrapper: HTMLElement | null;
	private tagsInput: HTMLElement | null;
	private dropdown: HTMLElement | null;
	private floatingUIInstance: any;
	private searchWrapper: HTMLElement | null;
	private search: HTMLInputElement | null;
	private searchNoResult: HTMLElement | null;
	private selectOptions: ISingleOption[] | [];
	private staticOptions: ISingleOption[] | [];
	private extraMarkup: string | string[] | Element | null;

	private readonly isAddTagOnEnter: boolean;

	private tagsInputHelper: HTMLElement | null;

	private remoteOptions: unknown[];
	private disabledObserver: MutationObserver | null = null;
	private remoteSearchAbortController: AbortController | null = null;
	private loadMoreAbortController: AbortController | null = null;
	private requestId: number = 0;
	private lastQuery: string = '';
	private readonly apiPageStart?: number;
	private readonly apiTotalPath?: string | null;
	private isLoadEventFired: boolean = false;

	private optionId = 0;

	private onWrapperClickListener: (evt: Event) => void;
	private onToggleClickListener: () => void;
	private onTagsInputFocusListener: () => void;
	private onTagsInputInputListener: () => void;
	private onTagsInputInputSecondListener: (evt: InputEvent) => void;
	private onTagsInputKeydownListener: (evt: KeyboardEvent) => void;
	private onSearchInputListener: (evt: InputEvent) => void;

	private readonly isSelectedOptionOnTop: boolean;

	constructor(el: HTMLElement, options?: ISelectOptions) {
		super(el, options);

		const data = el.getAttribute('data-hs-select');
		const dataOptions: ISelectOptions = data ? JSON.parse(data) : {};
		const concatOptions = {
			...dataOptions,
			...options,
		};

		this.value =
			concatOptions?.value || (this.el as HTMLSelectElement).value || null;
		this.placeholder = concatOptions?.placeholder || 'Select...';
		this.hasSearch = concatOptions?.hasSearch || false;
		this.minSearchLength = concatOptions?.minSearchLength ?? 0;
		this.preventSearchFocus = concatOptions?.preventSearchFocus || false;
		this.preventSearchInsideDescription =
			concatOptions?.preventSearchInsideDescription || false;
		this.mode = concatOptions?.mode || 'default';
		this.viewport =
			typeof concatOptions?.viewport !== 'undefined'
				? document.querySelector(concatOptions?.viewport)
				: null;
		this.scrollToSelected =
			typeof concatOptions?.scrollToSelected !== 'undefined'
				? concatOptions?.scrollToSelected
				: false;
		this._isOpened = Boolean(concatOptions?.isOpened) || false;
		this.isMultiple = this.el.hasAttribute('multiple') || false;
		this.isDisabled = this.el.hasAttribute('disabled') || false;
		this.selectedItems = [];

		this.apiUrl = concatOptions?.apiUrl || null;
		this.apiQuery = concatOptions?.apiQuery || null;
		this.apiOptions = concatOptions?.apiOptions || null;
		this.apiSearchQueryKey = concatOptions?.apiSearchQueryKey || null;
		this.apiDataPart = concatOptions?.apiDataPart || null;
		this.apiLoadMore =
			concatOptions?.apiLoadMore === true
				? {
						perPage: 10,
						scrollThreshold: 100,
					}
				: typeof concatOptions?.apiLoadMore === 'object' &&
					  concatOptions?.apiLoadMore !== null
					? {
							perPage: concatOptions.apiLoadMore.perPage || 10,
							scrollThreshold: concatOptions.apiLoadMore.scrollThreshold || 100,
						}
					: false;
		this.apiPageStart =
			typeof concatOptions?.apiPageStart === 'number'
				? concatOptions.apiPageStart
				: undefined;
		this.apiTotalPath =
			typeof concatOptions?.apiTotalPath === 'string'
				? concatOptions.apiTotalPath
				: null;

		this.apiFieldsMap = concatOptions?.apiFieldsMap || null;
		this.apiIconTag = concatOptions?.apiIconTag || null;
		this.apiSelectedValues = concatOptions?.apiSelectedValues || null;

		this.currentPage = 0;
		this.isLoading = false;
		this.hasMore = true;

		this.wrapperClasses = concatOptions?.wrapperClasses || null;
		this.toggleTag = concatOptions?.toggleTag || null;
		this.toggleClasses = concatOptions?.toggleClasses || null;
		this.toggleCountText =
			typeof concatOptions?.toggleCountText === undefined
				? null
				: concatOptions.toggleCountText;
		this.toggleCountTextPlacement =
			concatOptions?.toggleCountTextPlacement || 'postfix';
		this.toggleCountTextMinItems = concatOptions?.toggleCountTextMinItems || 1;
		this.toggleCountTextMode =
			concatOptions?.toggleCountTextMode || 'countAfterLimit';
		this.toggleSeparators = {
			items: concatOptions?.toggleSeparators?.items || ', ',
			betweenItemsAndCounter:
				concatOptions?.toggleSeparators?.betweenItemsAndCounter || 'and',
		};
		this.tagsItemTemplate = concatOptions?.tagsItemTemplate || null;
		this.tagsItemClasses = concatOptions?.tagsItemClasses || null;
		this.tagsInputId = concatOptions?.tagsInputId || null;
		this.tagsInputClasses = concatOptions?.tagsInputClasses || null;
		this.dropdownTag = concatOptions?.dropdownTag || null;
		this.dropdownClasses = concatOptions?.dropdownClasses || null;
		this.dropdownDirectionClasses =
			concatOptions?.dropdownDirectionClasses || null;
		this.dropdownSpace = concatOptions?.dropdownSpace || 10;
		this.dropdownPlacement = concatOptions?.dropdownPlacement || null;
		this.dropdownVerticalFixedPlacement =
			concatOptions?.dropdownVerticalFixedPlacement || null;
		this.dropdownScope = concatOptions?.dropdownScope || 'parent';
		this.dropdownAutoPlacement = concatOptions?.dropdownAutoPlacement || false;
		this.searchTemplate = concatOptions?.searchTemplate || null;
		this.searchWrapperTemplate = concatOptions?.searchWrapperTemplate || null;
		this.searchWrapperClasses =
			concatOptions?.searchWrapperClasses || 'bg-white p-2 sticky top-0';
		this.searchId = concatOptions?.searchId || null;
		this.searchLimit = concatOptions?.searchLimit || Infinity;
		this.isSearchDirectMatch =
			typeof concatOptions?.isSearchDirectMatch !== 'undefined'
				? concatOptions.isSearchDirectMatch
				: true;
		this.searchMatchMode =
			concatOptions?.searchMatchMode ||
			(this.isSearchDirectMatch ? 'substring' : 'chars-sequence');
		this.searchClasses =
			concatOptions?.searchClasses ||
			'block w-[calc(100%-32px)] text-sm border-gray-200 rounded-md focus:border-blue-500 focus:ring-blue-500 dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-400 py-2 px-3 my-2 mx-4';
		this.searchPlaceholder = concatOptions?.searchPlaceholder || 'Search...';
		this.searchNoResultTemplate =
			concatOptions?.searchNoResultTemplate || '<span></span>';
		this.searchNoResultText =
			concatOptions?.searchNoResultText || 'No results found';
		this.searchNoResultClasses =
			concatOptions?.searchNoResultClasses ||
			'px-4 text-sm text-gray-800 dark:text-neutral-200';
		this.optionAllowEmptyOption =
			typeof concatOptions?.optionAllowEmptyOption !== 'undefined'
				? concatOptions?.optionAllowEmptyOption
				: false;
		this.optionTemplate = concatOptions?.optionTemplate || null;
		this.optionTag = concatOptions?.optionTag || null;
		this.optionClasses = concatOptions?.optionClasses || null;
		this.optgroupTag = concatOptions?.optgroupTag || null;
		this.optgroupClasses = concatOptions?.optgroupClasses || null;
		this.extraMarkup = concatOptions?.extraMarkup || null;
		this.descriptionClasses = concatOptions?.descriptionClasses || null;
		this.iconClasses = concatOptions?.iconClasses || null;
		this.isAddTagOnEnter = concatOptions?.isAddTagOnEnter ?? true;
		this.isSelectedOptionOnTop = concatOptions?.isSelectedOptionOnTop ?? false;

		this.animationInProcess = false;

		this.selectOptions = [];
		this.staticOptions = [];
		this.remoteOptions = [];

		this.tagsInputHelper = null;

		this.disabledObserver = new MutationObserver((muts) => {
			if (muts.some((m) => m.attributeName === 'disabled')) {
				this.setDisabledState(this.el.hasAttribute('disabled'));
			}
		});
		this.disabledObserver.observe(this.el, {
			attributes: true,
			attributeFilter: ['disabled'],
		});

		this.init();
	}

	private wrapperClick(evt: Event) {
		if (
			!(evt.target as HTMLElement).closest('[data-hs-select-dropdown]') &&
			!(evt.target as HTMLElement).closest('[data-tag-value]')
		) {
			this.tagsInput.focus();
		}
	}

	private toggleClick() {
		if (this.isDisabled) return false;

		this.toggleFn();
	}

	private tagsInputFocus() {
		if (!this._isOpened) this.open();
	}

	private tagsInputInput() {
		this.calculateInputWidth();
	}

	private tagsInputInputSecond(evt: InputEvent) {
		if (!this.apiUrl) {
			this.searchOptions((evt.target as HTMLInputElement).value);
		}
	}

	private tagsInputKeydown(evt: KeyboardEvent) {
		if (evt.key === 'Enter' && this.isAddTagOnEnter) {
			const val = (evt.target as HTMLInputElement).value;

			if (this.selectOptions.find((el: ISingleOption) => el.val === val)) {
				return false;
			}

			this.addSelectOption(val, val);
			this.buildOption(val, val);
			this.buildOriginalOption(val, val);
			(
				this.dropdown.querySelector(`[data-value="${val}"]`) as HTMLElement
			).click();

			this.resetTagsInputField();
		}
	}

	private searchInput(evt: InputEvent) {
		const newVal = (evt.target as HTMLInputElement).value;
		this.lastQuery = newVal;

		if (this.apiUrl) this.remoteSearch(newVal);
		else this.searchOptions(newVal);
	}

	public setValue(val: string | string[]) {
		this.value = val;

		this.clearSelections();

		if (Array.isArray(val)) {
			if (this.mode === 'tags') {
				this.unselectMultipleItems();
				this.selectMultipleItems();

				this.selectedItems = [];

				const existingTags = this.wrapper.querySelectorAll('[data-tag-value]');
				existingTags.forEach((tag) => tag.remove());

				this.setTagsItems();
				this.reassignTagsInputPlaceholder(
					this.hasValue() ? '' : this.placeholder,
				);
			} else {
				this.toggleTextWrapper.innerHTML = this.hasValue()
					? this.stringFromValue()
					: this.placeholder;

				this.unselectMultipleItems();
				this.selectMultipleItems();
			}
		} else {
			this.setToggleTitle();

			if (this.toggle.querySelector('[data-icon]')) this.setToggleIcon();
			if (this.toggle.querySelector('[data-title]')) this.setToggleTitle();

			this.selectSingleItem();
		}
	}

	private setDisabledState(isDisabled: boolean): void {
		this.isDisabled = isDisabled;

		const target = this.mode === 'tags' ? this.wrapper : this.toggle;

		target?.classList.toggle('disabled', isDisabled);

		if (isDisabled && this.isOpened()) this.close();
	}

	private hasValue(): boolean {
		if (!this.isMultiple) {
			return (
				this.value !== null && this.value !== undefined && this.value !== ''
			);
		}
		return (
			Array.isArray(this.value) &&
			this.value.length > 0 &&
			this.value.some((val) => val !== null && val !== undefined && val !== '')
		);
	}

	private init() {
		HSSelect.ensureGlobalHandlers();
		this.createCollection(window.$hsSelectCollection, this);

		this.build();

		if (typeof window !== 'undefined') {
			if (!window.HSAccessibilityObserver) {
				window.HSAccessibilityObserver = new HSAccessibilityObserver();
			}
			this.setupAccessibility();
		}
	}

	private build() {
		this.el.style.display = 'none';
		const options = this.el.querySelectorAll('option');

		if (options.length) this.setOptions();

		if (this.optionAllowEmptyOption && !this.value) {
			this.value = '';
		}

		if (this.isMultiple) {
			const options = this.el.querySelectorAll('option');
			const selectedOptions = Array.from(options).filter(
				(el: HTMLOptionElement) => el.selected,
			);

			const values: string[] = [];

			selectedOptions.forEach((el: HTMLOptionElement) => {
				values.push(el.value);
			});

			this.value = values;
		}

		this.buildWrapper();
		if (this.mode === 'tags') this.buildTags();
		else this.buildToggle();
		this.buildDropdown();
		if (this.extraMarkup) this.buildExtraMarkup();

		if (!this.apiUrl) this.fireLoadEvent();
	}

	private fireLoadEvent() {
		if (this.isLoadEventFired) return;

		this.isLoadEventFired = true;

		setTimeout(() => {
			this.fireEvent('load', this.el);
			dispatch('load.hs.select', this.el, this.el);
		});
	}

	private setOptions() {
		const options = this.el.querySelectorAll('option');

		Array.from(options)
			.filter(
				(el: HTMLOptionElement) =>
					this.optionAllowEmptyOption ||
					(!this.optionAllowEmptyOption && el.value && el.value !== ''),
			)
			.forEach((el: HTMLOptionElement) => {
				const data = el.getAttribute('data-hs-select-option');

				const optionData: ISingleOption = {
					title: el.textContent,
					val: el.value,
					disabled: el.disabled,
					options: data && data !== 'undefined' ? JSON.parse(data) : null,
					optgroupName:
						el.parentElement?.tagName === 'OPTGROUP'
							? (el.parentElement as HTMLOptGroupElement).label
							: null,
				};

				this.selectOptions = [...this.selectOptions, optionData];

				if (this.apiUrl) {
					this.staticOptions = [...this.staticOptions, optionData];
					el.setAttribute('data-static', 'true');
				}
			});
	}

	private buildWrapper() {
		this.wrapper = document.createElement('div');
		this.wrapper.classList.add('hs-select', 'relative');

		this.setDisabledState(this.isDisabled);

		if (this.mode === 'tags') {
			this.onWrapperClickListener = (evt) => this.wrapperClick(evt);

			this.wrapper.addEventListener('click', this.onWrapperClickListener);
		}

		if (this.wrapperClasses) {
			classToClassList(this.wrapperClasses, this.wrapper);
		}

		this.el.before(this.wrapper);

		this.wrapper.append(this.el);
	}

	private buildExtraMarkup() {
		const appendMarkup = (markup: string): HTMLElement => {
			const el = htmlToElement(markup);

			this.wrapper.append(el);

			return el;
		};
		const clickHandle = (el: HTMLElement) => {
			if (!el.classList.contains('--prevent-click')) {
				el.addEventListener('click', (evt: Event) => {
					evt.stopPropagation();

					if (!this.isDisabled) this.toggleFn();
				});
			}
		};

		if (Array.isArray(this.extraMarkup)) {
			this.extraMarkup.forEach((el) => {
				const newEl = appendMarkup(el);

				clickHandle(newEl);
			});
		} else {
			const newEl = appendMarkup(this.extraMarkup as string);

			clickHandle(newEl);
		}
	}

	private buildToggle() {
		let icon, title;
		this.toggleTextWrapper = document.createElement('span');
		this.toggleTextWrapper.classList.add('truncate');
		this.toggle = htmlToElement(this.toggleTag || '<div></div>');
		icon = this.toggle.querySelector('[data-icon]');
		title = this.toggle.querySelector('[data-title]');

		if (!this.isMultiple && icon) this.setToggleIcon();
		if (!this.isMultiple && title) this.setToggleTitle();
		if (this.isMultiple) {
			this.toggleTextWrapper.innerHTML = this.hasValue()
				? this.stringFromValue()
				: this.placeholder;
		} else {
			this.toggleTextWrapper.innerHTML =
				this.getItemByValue(this.value as string)?.title || this.placeholder;
		}
		if (!title) this.toggle.append(this.toggleTextWrapper);
		if (this.toggleClasses) classToClassList(this.toggleClasses, this.toggle);
		if (this.isDisabled) this.toggle.classList.add('disabled');
		if (this.wrapper) this.wrapper.append(this.toggle);

		if (this.toggle?.ariaExpanded) {
			if (this._isOpened) this.toggle.ariaExpanded = 'true';
			else this.toggle.ariaExpanded = 'false';
		}

		this.onToggleClickListener = () => this.toggleClick();

		this.toggle.addEventListener('click', this.onToggleClickListener);
	}

	private setToggleIcon() {
		const item = this.getItemByValue(this.value as string) as ISingleOption &
			IApiFieldMap;
		const icon = this.toggle.querySelector('[data-icon]');

		if (icon) {
			icon.innerHTML = '';

			const staticIconSrc = item?.options?.apiFields?.icon;
			const remoteIconSrc = item?.[this.apiFieldsMap?.icon];
			const directIconSrc = item?.options?.icon;

			const img = htmlToElement(
				(this.apiUrl || staticIconSrc) && this.apiIconTag
					? this.apiIconTag || ''
					: directIconSrc || '',
			) as HTMLImageElement;

			if (this.value) {
				if (staticIconSrc) {
					img.src = staticIconSrc as string;
				} else if (this.apiUrl && this.apiIconTag && remoteIconSrc) {
					img.src = remoteIconSrc as string;
				} else if (
					directIconSrc &&
					typeof directIconSrc === 'string' &&
					!directIconSrc.trim().startsWith('<')
				) {
					img.src = directIconSrc;
				}
			}

			icon.append(img);

			if (img instanceof HTMLImageElement ? !img.src : !img) {
				icon.classList.add('hidden');
			} else {
				icon.classList.remove('hidden');
			}
		}
	}

	private setToggleTitle() {
		const title = this.toggle.querySelector('[data-title]');
		let value = this.placeholder;

		if (this.optionAllowEmptyOption && this.value === '') {
			const emptyOption = this.selectOptions.find(
				(el: ISingleOption) => el.val === '',
			);
			value = emptyOption?.title || this.placeholder;
		} else if (this.value) {
			if (this.apiUrl) {
				const staticOption = this.staticOptions.find(
					(el: ISingleOption) => el.val === this.value,
				);
				if (staticOption) {
					value = staticOption.title;
				} else {
					const selectedOption = (this.remoteOptions as IApiFieldMap[]).find(
						(el) =>
							`${el[this.apiFieldsMap.val]}` === this.value ||
							`${el[this.apiFieldsMap.title]}` === this.value,
					);
					if (selectedOption) {
						value = selectedOption[this.apiFieldsMap.title] as string;
					}
				}
			} else {
				const selectedOption = this.selectOptions.find(
					(el: ISingleOption) => el.val === this.value,
				);

				if (selectedOption) {
					value = selectedOption.title;
				}
			}
		}

		if (title) {
			title.innerHTML = value;
			title.classList.add('truncate');
			this.toggle.append(title);
		} else {
			this.toggleTextWrapper.innerHTML = value;
		}
	}

	private buildTags() {
		if (this.isDisabled) this.wrapper.classList.add('disabled');
		this.wrapper.setAttribute('tabindex', '0');
		this.buildTagsInput();
		this.setTagsItems();
	}

	private reassignTagsInputPlaceholder(placeholder: string) {
		(this.tagsInput as HTMLInputElement).placeholder = placeholder;
		this.tagsInputHelper.innerHTML = placeholder;
		this.calculateInputWidth();
	}

	private buildTagsItem(val: string) {
		const item = this.getItemByValue(val) as ISingleOption & IApiFieldMap;

		let template, title, remove, icon: null | HTMLElement;

		const newItem = document.createElement('div');
		newItem.setAttribute('data-tag-value', val);
		if (this.tagsItemClasses) classToClassList(this.tagsItemClasses, newItem);

		if (this.tagsItemTemplate) {
			template = htmlToElement(this.tagsItemTemplate);

			newItem.append(template);
		}

		// Icon
		if (item?.options?.icon || this.apiIconTag) {
			const img = htmlToElement(
				this.apiUrl && this.apiIconTag ? this.apiIconTag : item?.options?.icon,
			) as HTMLImageElement;

			if (this.apiUrl && this.apiIconTag) {
				const iconSrc =
					(item[this.apiFieldsMap.icon] as string) ||
					(item?.options?.apiFields?.icon as string) ||
					(item?.options?.icon as string) ||
					'';
				if (iconSrc) img.src = iconSrc;
			}

			icon = template
				? template.querySelector('[data-icon]')
				: document.createElement('span');

			icon.append(img);

			if (!template) newItem.append(icon);
		}

		if (
			template &&
			template.querySelector('[data-icon]') &&
			!item?.options?.icon &&
			!this.apiUrl &&
			!this.apiIconTag &&
			!item[this.apiFieldsMap?.icon]
		) {
			template.querySelector('[data-icon]').classList.add('hidden');
		}

		// Title
		title = template
			? template.querySelector('[data-title]')
			: document.createElement('span');

		if (
			this.apiUrl &&
			this.apiFieldsMap?.title &&
			item[this.apiFieldsMap.title]
		) {
			title.textContent = item[this.apiFieldsMap.title] as string;
		} else {
			title.textContent = item.title || '';
		}

		if (!template) newItem.append(title);

		// Remove
		if (template) {
			remove = template.querySelector('[data-remove]');
		} else {
			remove = document.createElement('span');
			remove.textContent = 'X';

			newItem.append(remove);
		}

		remove.addEventListener('click', () => {
			this.value = (this.value as string[]).filter((el) => el !== val);
			this.selectedItems = this.selectedItems.filter((el) => el !== val);

			if (!this.hasValue()) {
				this.reassignTagsInputPlaceholder(this.placeholder);
			}

			this.unselectMultipleItems();
			this.selectMultipleItems();

			newItem.remove();

			this.triggerChangeEventForNativeSelect();
		});

		this.wrapper.append(newItem);
	}

	private getItemByValue(val: string) {
		if (this.apiUrl) {
			const staticValue = this.staticOptions.find(
				(el: ISingleOption) => el.val === val,
			);
			if (staticValue) return staticValue;

			return (this.remoteOptions as (ISingleOption & IApiFieldMap)[]).find(
				(el) =>
					`${el[this.apiFieldsMap.val]}` === val ||
					el[this.apiFieldsMap.title] === val,
			);
		}

		return this.selectOptions.find((el: ISingleOption) => el.val === val);
	}

	private setTagsItems() {
		if (this.value) {
			const values = Array.isArray(this.value)
				? this.value
				: this.value != null
					? [this.value]
					: [];

			values.forEach((val) => {
				if (!this.selectedItems.includes(val)) this.buildTagsItem(val);

				this.selectedItems = !this.selectedItems.includes(val)
					? [...this.selectedItems, val]
					: this.selectedItems;
			});
		}

		if (this._isOpened && this.floatingUIInstance) {
			this.floatingUIInstance.update();
		}
	}

	private buildTagsInput() {
		this.tagsInput = document.createElement('input');

		if (this.tagsInputId) this.tagsInput.id = this.tagsInputId;
		if (this.tagsInputClasses) {
			classToClassList(this.tagsInputClasses, this.tagsInput);
		}
		this.tagsInput.setAttribute('tabindex', '-1');

		this.onTagsInputFocusListener = () => this.tagsInputFocus();
		this.onTagsInputInputListener = () => this.tagsInputInput();
		this.onTagsInputInputSecondListener = debounce((evt: InputEvent) =>
			this.tagsInputInputSecond(evt),
		);
		this.onTagsInputKeydownListener = (evt) => this.tagsInputKeydown(evt);

		this.tagsInput.addEventListener('focus', this.onTagsInputFocusListener);
		this.tagsInput.addEventListener('input', this.onTagsInputInputListener);
		this.tagsInput.addEventListener(
			'input',
			this.onTagsInputInputSecondListener,
		);
		this.tagsInput.addEventListener('keydown', this.onTagsInputKeydownListener);

		this.wrapper.append(this.tagsInput);

		setTimeout(() => {
			this.adjustInputWidth();
			this.reassignTagsInputPlaceholder(
				this.hasValue() ? '' : this.placeholder,
			);
		});
	}

	private buildDropdown() {
		this.dropdown = htmlToElement(this.dropdownTag || '<div></div>');
		this.dropdown.setAttribute('data-hs-select-dropdown', '');

		if (this.dropdownScope === 'parent') {
			this.dropdown.classList.add('absolute');
			if (!this.dropdownVerticalFixedPlacement) {
				this.dropdown.classList.add('top-full');
			}
		}
		this.dropdown.role = 'listbox';
		this.dropdown.tabIndex = -1;
		this.dropdown.ariaOrientation = 'vertical';

		if (!this._isOpened) this.dropdown.classList.add('hidden');

		if (this.dropdownClasses) {
			classToClassList(this.dropdownClasses, this.dropdown);
		}
		if (this.wrapper) this.wrapper.append(this.dropdown);
		if (this.dropdown && this.hasSearch) this.buildSearch();
		if (this.selectOptions) {
			let optgroupName = '';

			this.selectOptions.forEach((props: ISingleOption, i) => {
				if (props.optgroupName && props.optgroupName !== optgroupName) {
					this.hasOptgroup = true;
					optgroupName = props.optgroupName;
					this.buildOptgroup(optgroupName);
				}

				this.buildOption(
					props.title,
					props.val,
					props.disabled,
					props.selected,
					props.options,
					`${i}`,
					undefined,
					!!this.apiUrl,
				);
			});

			optgroupName = '';
		}

		if (this.apiUrl) {
			this.optionsFromRemoteData().then(() => this.fireLoadEvent());
		}

		if (!this.apiUrl) {
			this.sortElements(this.el, 'option');
			this.sortElements(this.dropdown, '[data-value]');
		}

		if (this.dropdownScope === 'window') this.buildFloatingUI();

		if (this.dropdown && this.apiLoadMore) this.setupInfiniteScroll();
	}

	private buildOptgroup(name: string) {
		const optgroup = htmlToElement(this.optgroupTag || '<div></div>');
		optgroup.textContent = name;
		optgroup.setAttribute('data-optgroup', '');
		if (this.optgroupClasses) classToClassList(this.optgroupClasses, optgroup);
		this.dropdown.append(optgroup);
	}

	private setupInfiniteScroll() {
		this.dropdown.addEventListener('scroll', this.handleScroll.bind(this));
	}

	private async handleScroll() {
		if (!this.dropdown || this.isLoading || !this.hasMore || !this.apiLoadMore)
			return;

		const { scrollTop, scrollHeight, clientHeight } = this.dropdown;
		const scrollThreshold =
			typeof this.apiLoadMore === 'object'
				? this.apiLoadMore.scrollThreshold
				: 100;
		const isNearBottom =
			scrollHeight - scrollTop - clientHeight < scrollThreshold;

		if (isNearBottom) await this.loadMore();
	}

	private async loadMore() {
		if (!this.apiUrl || this.isLoading || !this.hasMore || !this.apiLoadMore) {
			return;
		}

		const tempRequestId = this.requestId;
		this.isLoading = true;

		try {
			const url = new URL(this.apiUrl);
			const tempQuery = (this.lastQuery ?? '').trim().toLowerCase();
			const tempQueryParams = new URLSearchParams(this.apiQuery ?? '');
			const tempSearchQuery = this.apiSearchQueryKey ?? 'q';
			if (tempQuery !== '') tempQueryParams.set(tempSearchQuery, tempQuery);
			url.search = tempQueryParams.toString();

			const paginationParam = (this.apiFieldsMap?.page ||
				this.apiFieldsMap?.offset ||
				'page') as string;
			const isOffsetBased = !!this.apiFieldsMap?.offset;
			const perPage =
				typeof this.apiLoadMore === 'object' ? this.apiLoadMore.perPage : 10;
			const nextPage = this.currentPage + 1;

			if (isOffsetBased) {
				const offset = this.currentPage * perPage;

				url.searchParams.set(paginationParam, String(offset));
			} else {
				url.searchParams.set(paginationParam, String(nextPage));
			}

			url.searchParams.set(
				this.apiFieldsMap?.limit || 'limit',
				String(perPage),
			);

			if (this.loadMoreAbortController) {
				try {
					this.loadMoreAbortController.abort();
				} catch {}
			}

			this.loadMoreAbortController = new AbortController();
			const apiOpts = {
				...(this.apiOptions || {}),
				signal: this.loadMoreAbortController.signal,
			};
			const response = await fetch(url.toString(), apiOpts);
			const data = await response.json();

			if (tempRequestId !== this.requestId) {
				this.isLoading = false;
				return;
			}

			const items = this.apiDataPart
				? (data[this.apiDataPart] ?? data.results ?? data)
				: (data.results ?? data);
			const getByPath = (obj: any, path: string) =>
				path.split('.').reduce((o, k) => (o ? o[k] : undefined), obj);
			let total: number | null = null;

			if (this.apiTotalPath) {
				const val = getByPath(data, this.apiTotalPath);
				total = typeof val === 'number' ? val : null;
			} else {
				total =
					(typeof data.count === 'number' ? data.count : null) ??
					(typeof data.total === 'number' ? data.total : null) ??
					(data.info && typeof data.info.count === 'number'
						? data.info.count
						: null);
			}

			if (items && items.length > 0) {
				this.remoteOptions = [...(this.remoteOptions || []), ...items];

				this.buildOptionsFromRemoteData(items);

				if (typeof total === 'number') {
					const consumed = nextPage * perPage;
					this.hasMore = consumed < total;
				} else {
					this.hasMore = items.length === perPage;
				}

				this.currentPage = nextPage;
			} else {
				this.hasMore = false;
			}
		} catch (error) {
			this.hasMore = false;
			console.error('Error loading more options:', error);
		} finally {
			this.isLoading = false;
		}
	}

	/**
	 * Positions the dropdown using Floating UI when `dropdownScope` is set to `"window"`.
	 *
	 * Requires `@floating-ui/dom` to be loaded on the page (e.g. via CDN or npm).
	 * Used by: `dropdownScope: "window"`, `dropdownPlacement`, `dropdownAutoPlacement`.
	 *
	 * @see https://floating-ui.com
	 */
	private buildFloatingUI() {
		if (typeof FloatingUIDOM !== 'undefined' && FloatingUIDOM.computePosition) {
			document.body.appendChild(this.dropdown);

			const reference = this.mode === 'tags' ? this.wrapper : this.toggle;
			const middleware = [FloatingUIDOM.offset([0, 5])];

			if (
				this.dropdownAutoPlacement &&
				typeof FloatingUIDOM.flip === 'function'
			) {
				middleware.push(
					FloatingUIDOM.flip({
						fallbackPlacements: [
							'bottom-start',
							'bottom-end',
							'top-start',
							'top-end',
						],
					}),
				);
			}

			const options = {
				placement: POSITIONS[this.dropdownPlacement] || 'bottom',
				strategy: 'fixed',
				middleware,
			};

			const update = () => {
				Object.assign(this.dropdown.style, {
					marginLeft: '',
					marginTop: '',
					marginRight: '',
					marginBottom: '',
				});

				FloatingUIDOM.computePosition(reference, this.dropdown, options).then(
					({ x, y, placement: computedPlacement }) => {
						Object.assign(this.dropdown.style, {
							position: 'fixed',
							left: `${x}px`,
							top: `${y}px`,
							[`margin${
								computedPlacement.startsWith('bottom') ||
								computedPlacement.startsWith('top')
									? 'Top'
									: computedPlacement.startsWith('right')
										? 'Left'
										: 'Right'
							}`]: `${
								computedPlacement.startsWith('top') ? '-' : ''
							}${this.dropdownSpace}px`,
						});

						this.dropdown.setAttribute('data-placement', computedPlacement);
					},
				);
			};

			update();

			const cleanup = FloatingUIDOM.autoUpdate(
				reference,
				this.dropdown,
				update,
			);

			this.floatingUIInstance = {
				update,
				destroy: cleanup,
			};
		} else {
			console.error('FloatingUIDOM not found! Please enable it on the page.');
		}
	}

	private updateDropdownWidth() {
		const toggle = this.mode === 'tags' ? this.wrapper : this.toggle;

		this.dropdown.style.width = `${toggle.clientWidth}px`;
	}

	private buildSearch() {
		if (!this.hasSearch) return;

		if (this.mode === 'tags') {
			const tagsInput =
				((this as any).tagsInput as HTMLInputElement) ??
				(this.wrapper?.querySelector(
					':scope input',
				) as HTMLInputElement | null) ??
				(this.el.querySelector(':scope input') as HTMLInputElement | null);

			if (tagsInput) {
				this.search = tagsInput;
				if (this.searchPlaceholder) {
					this.search.placeholder = this.searchPlaceholder;
				}
				if (this.searchId) this.search.id = this.searchId;

				if (this.searchClasses) {
					classToClassList(this.searchClasses, this.search);
				}

				if (this.apiUrl) {
					this.onSearchInputListener = debounce((evt: InputEvent) =>
						this.searchInput(evt),
					);

					this.search.addEventListener('input', this.onSearchInputListener);
				}

				return;
			}

			return;
		}

		let input;
		this.searchWrapper = htmlToElement(
			this.searchWrapperTemplate || '<div></div>',
		);

		if (this.searchWrapperClasses) {
			classToClassList(this.searchWrapperClasses, this.searchWrapper);
		}

		input = this.searchWrapper.querySelector('[data-input]');

		const search = htmlToElement(this.searchTemplate || '<input type="text">');
		this.search = (
			search.tagName === 'INPUT' ? search : search.querySelector(':scope input')
		) as HTMLInputElement;
		this.search.placeholder = this.searchPlaceholder;

		if (this.searchClasses) classToClassList(this.searchClasses, this.search);
		if (this.searchId) this.search.id = this.searchId;

		this.onSearchInputListener = debounce((evt: InputEvent) =>
			this.searchInput(evt),
		);
		this.search.addEventListener('input', this.onSearchInputListener);

		if (input) input.append(search);
		else this.searchWrapper.append(search);

		this.dropdown.append(this.searchWrapper);
	}

	private buildOption(
		title: string,
		val: string,
		disabled: boolean = false,
		selected: boolean = false,
		options?: ISingleOptionOptions,
		index: string = '1',
		id?: string,
		isStatic: boolean = false,
	) {
		let template: HTMLElement | null = null;
		let titleWrapper: HTMLElement | null = null;
		let iconWrapper: HTMLElement | null = null;
		let descriptionWrapper: HTMLElement | null = null;

		const option = htmlToElement(this.optionTag || '<div></div>');
		option.setAttribute('data-value', val);
		option.setAttribute('data-title-value', title);
		option.setAttribute('tabIndex', index);
		option.classList.add('cursor-pointer');
		option.setAttribute('data-id', id || `${this.optionId}`);
		if (isStatic) option.setAttribute('data-static', 'true');
		if (!id) this.optionId++;
		if (disabled) option.classList.add('disabled');
		if (selected) {
			if (this.isMultiple) this.value = [...(this.value as []), val];
			else this.value = val;
		}
		if (this.optionTemplate) {
			template = htmlToElement(this.optionTemplate);

			option.append(template);
		}
		if (template) {
			titleWrapper = template.querySelector('[data-title]');
			titleWrapper.textContent = title || '';
		} else {
			option.textContent = title || '';
		}
		if (options) {
			const iconValue = options.apiFields?.icon ?? options.icon;
			const descriptionValue =
				options.apiFields?.description ?? options.description;

			if (iconValue) {
				const img = htmlToElement(this.apiIconTag ?? (iconValue as string));
				img.classList.add('max-w-full');

				if (this.apiUrl || options.apiFields) {
					img.setAttribute('alt', title);
					img.setAttribute('src', iconValue as string);
				}

				if (template) {
					iconWrapper = template.querySelector('[data-icon]');
					iconWrapper.append(img);
				} else {
					const icon = htmlToElement('<div></div>');
					if (this.iconClasses) classToClassList(this.iconClasses, icon);

					icon.append(img);
					option.append(icon);
				}
			}
			if (Array.isArray(options.additionalClasses)) {
				options.additionalClasses.forEach(([selector, classes]) => {
					const element = selector ? option.querySelector(selector) : option;

					if (element) classes.forEach((cl) => element.classList.add(cl));
				});
			}
			if (descriptionValue) {
				option.dataset.description = descriptionValue as string;

				if (template) {
					descriptionWrapper = template.querySelector('[data-description]');
					if (descriptionWrapper) {
						descriptionWrapper.append(descriptionValue as string);
					}
				} else {
					const description = htmlToElement('<div></div>');
					description.textContent = descriptionValue as string;
					if (this.descriptionClasses) {
						classToClassList(this.descriptionClasses, description);
					}

					option.append(description);
				}
			}
		}
		if (
			template &&
			template.querySelector('[data-icon]') &&
			!options &&
			!options?.icon
		) {
			template.querySelector('[data-icon]').classList.add('hidden');
		}

		if (
			this.value &&
			(this.isMultiple ? this.value.includes(val) : this.value === val)
		) {
			option.classList.add('selected');
		}

		if (!disabled) {
			option.addEventListener('click', () => this.onSelectOption(val));
		}

		if (this.optionClasses) classToClassList(this.optionClasses, option);
		if (this.dropdown) this.dropdown.append(option);
		if (selected) this.setNewValue();
	}

	private buildOptionFromRemoteData(
		title: string,
		val: string,
		disabled: boolean = false,
		selected: boolean = false,
		index: string = '1',
		id: string | null,
		options?: ISingleOptionOptions,
	) {
		if (index) {
			this.buildOption(title, val, disabled, selected, options, index, id);
		} else {
			alert(
				'ID parameter is required for generating remote options! Please check your API endpoint have it.',
			);
		}
	}

	private buildOptionsFromRemoteData(data: []) {
		data.forEach((el: IApiFieldMap, i) => {
			let id = null;
			let title = '';
			let value = '';
			const options: IApiFieldMap & { rest: { [key: string]: unknown } } = {
				id: '',
				val: '',
				title: '',
				icon: null,
				description: null,
				rest: {},
			};

			Object.keys(el).forEach((key: string) => {
				if (el[this.apiFieldsMap.id]) {
					id = el[this.apiFieldsMap.id];
					options.id = `${id}`;
				}
				if (el[this.apiFieldsMap.val]) {
					value = `${el[this.apiFieldsMap.val]}`;
					options.val = value;
				}
				if (el[this.apiFieldsMap.title]) {
					title = el[this.apiFieldsMap.title] as string;
					options.title = title;
					if (!el[this.apiFieldsMap.val]) {
						value = title;
						options.val = value;
					}
				}
				if (el[this.apiFieldsMap.icon]) {
					options.icon = el[this.apiFieldsMap.icon] as string;
				}
				if (el[this.apiFieldsMap?.description]) {
					options.description = el[this.apiFieldsMap.description] as string;
				}
				options.rest[key] = el[key];
			});

			// Check if option already exists in static options
			const staticOptionIndex = this.staticOptions.findIndex(
				(staticOpt: ISingleOption) => staticOpt.val === value,
			);

			if (staticOptionIndex !== -1) {
				this.mergeRemoteDataIntoStaticOption(
					staticOptionIndex,
					value,
					options,
					i,
				);
				return;
			}

			const existingOption = this.dropdown.querySelector(
				`[data-value="${value}"]`,
			);

			if (!existingOption) {
				const isSelected = this.apiSelectedValues
					? Array.isArray(this.apiSelectedValues)
						? this.apiSelectedValues.includes(value)
						: this.apiSelectedValues === value
					: false;

				this.buildOriginalOption(
					title,
					value,
					id,
					false,
					isSelected,
					options as ISingleOptionOptions & IApiFieldMap,
				);

				this.buildOptionFromRemoteData(
					title,
					value,
					false,
					isSelected,
					`${i}`,
					id,
					options as ISingleOptionOptions & IApiFieldMap,
				);

				if (isSelected) {
					if (this.isMultiple) {
						if (!this.value) this.value = [];
						if (Array.isArray(this.value)) {
							this.value = [...this.value, value];
						}
					} else {
						this.value = value;
					}

					if (this.toggle) {
						if (this.toggle.querySelector('[data-title]'))
							this.setToggleTitle();
						if (this.toggle.querySelector('[data-icon]')) this.setToggleIcon();
					}
				}
			}
		});

		this.sortElements(this.el, 'option');
		this.sortElements(this.dropdown, '[data-value]');
	}

	private mergeRemoteDataIntoStaticOption(
		staticOptionIndex: number,
		value: string,
		remoteData: IApiFieldMap & { rest: { [key: string]: unknown } },
		remoteIndex: number,
	) {
		const staticOption = this.staticOptions[staticOptionIndex];
		if (!staticOption.options) {
			staticOption.options = {};
		}
		if (!staticOption.options.apiFields) {
			staticOption.options.apiFields = {};
		}

		if (remoteData.title) {
			staticOption.title = remoteData.title as string;
		}

		if (remoteData.icon) {
			staticOption.options.apiFields.icon = remoteData.icon;
		}
		if (remoteData.description) {
			staticOption.options.apiFields.description = remoteData.description;
		}
		Object.keys(remoteData.rest).forEach((key) => {
			staticOption.options.apiFields[key] = remoteData.rest[key];
		});

		const dropdownOption = this.dropdown.querySelector(
			`[data-value="${value}"][data-static]`,
		) as HTMLElement;

		if (dropdownOption) {
			dropdownOption.setAttribute('tabIndex', `${remoteIndex}`);

			if (remoteData.id) {
				dropdownOption.setAttribute('data-id', `${remoteData.id}`);
			}

			if (remoteData.title) {
				dropdownOption.setAttribute(
					'data-title-value',
					remoteData.title as string,
				);
				const titleWrapper = dropdownOption.querySelector('[data-title]');
				if (titleWrapper) {
					titleWrapper.textContent = remoteData.title as string;
				}
			}

			if (remoteData.icon) {
				const iconWrapper = dropdownOption.querySelector('[data-icon]');
				if (iconWrapper) {
					iconWrapper.innerHTML = '';
					const img = htmlToElement(
						this.apiIconTag || '<img />',
					) as HTMLImageElement;
					img.classList.add('max-w-full');
					img.setAttribute('alt', staticOption.title);
					img.setAttribute('src', remoteData.icon as string);
					iconWrapper.append(img);
				}
			}

			if (remoteData.description) {
				dropdownOption.dataset.description = remoteData.description as string;
				const descWrapper = dropdownOption.querySelector('[data-description]');
				if (descWrapper) {
					descWrapper.textContent = remoteData.description as string;
				}
			}
		}

		const originalOption = this.el.querySelector(
			`option[value="${value}"][data-static]`,
		) as HTMLOptionElement;

		if (originalOption) {
			if (remoteData.id) {
				originalOption.setAttribute('data-id', `${remoteData.id}`);
			}

			if (remoteData.title) {
				originalOption.textContent = remoteData.title as string;
			}

			const optionData = {
				id: remoteData.id || '',
				val: remoteData.val || '',
				title: remoteData.title || '',
				icon: remoteData.icon || null,
				description: remoteData.description || null,
				rest: remoteData.rest,
			};
			originalOption.setAttribute(
				'data-hs-select-option',
				JSON.stringify(optionData),
			);
		}

		const selectOptionIndex = this.selectOptions.findIndex(
			(opt: ISingleOption) => opt.val === value,
		);
		if (selectOptionIndex !== -1) {
			this.selectOptions[selectOptionIndex] = staticOption;
		}

		const isCurrentlySelected = this.isMultiple
			? Array.isArray(this.value) && this.value.includes(value)
			: this.value === value;

		if (isCurrentlySelected && this.toggle) {
			if (remoteData.title) {
				if (this.toggle.querySelector('[data-title]')) {
					this.setToggleTitle();
				} else if (this.toggleTextWrapper) {
					if (this.isMultiple) {
						this.toggleTextWrapper.innerHTML = this.stringFromValue();
					} else {
						this.toggleTextWrapper.innerHTML = remoteData.title as string;
					}
				}
			}

			if (remoteData.icon && this.toggle.querySelector('[data-icon]')) {
				this.setToggleIcon();
			}
		}
	}

	private async optionsFromRemoteData(val = '') {
		const res = (await this.apiRequest(val)) || [];
		this.remoteOptions = res;

		if (res.length) this.buildOptionsFromRemoteData(this.remoteOptions as []);
		else console.log('There is no data were responded!');
	}

	private async apiRequest(val = '', signal?: AbortSignal): Promise<any> {
		try {
			const url = new URL(this.apiUrl);
			const queryParams = new URLSearchParams(this.apiQuery ?? '');
			const options = this.apiOptions ?? {};
			const tempOptions = { ...(options as any) } as RequestInit;
			if (signal) tempOptions.signal = signal;
			const key = this.apiSearchQueryKey ?? 'q';
			const trimmed = (val ?? '').trim().toLowerCase();

			if (trimmed !== '') queryParams.set(key, trimmed);

			if (this.apiLoadMore) {
				const perPage =
					typeof this.apiLoadMore === 'object' ? this.apiLoadMore.perPage : 10;
				const pageKey =
					this.apiFieldsMap?.page ?? this.apiFieldsMap?.offset ?? 'page';
				const limitKey = this.apiFieldsMap?.limit ?? 'limit';
				const isOffset = Boolean(this.apiFieldsMap?.offset);
				const defaultStart = isOffset ? 0 : 1;
				const pageStart =
					typeof this.apiPageStart === 'number'
						? this.apiPageStart
						: defaultStart;

				queryParams.delete(pageKey);
				queryParams.delete(limitKey);

				queryParams.set(pageKey, String(pageStart));
				queryParams.set(limitKey, String(perPage));
			}

			url.search = queryParams.toString();
			const res = await fetch(url.toString(), tempOptions);
			const json = await res.json();

			return this.apiDataPart ? json[this.apiDataPart] : json;
		} catch (err) {
			console.error(err);
		}
	}

	private sortElements(container: HTMLElement, selector: string): void {
		if (this.hasOptgroup) return;

		const items = Array.from(container.querySelectorAll(selector));

		if (this.isSelectedOptionOnTop) {
			items.sort((a, b) => {
				const isASelected =
					a.classList.contains('selected') || a.hasAttribute('selected');
				const isBSelected =
					b.classList.contains('selected') || b.hasAttribute('selected');

				if (isASelected && !isBSelected) return -1;
				if (!isASelected && isBSelected) return 1;

				return 0;
			});
		}

		items.forEach((item, index) => {
			container.appendChild(item);
			if (item.hasAttribute('tabindex')) {
				item.setAttribute('tabIndex', `${index}`);
			}
		});
	}

	private async remoteSearch(val: string) {
		this.requestId++;

		if (this.remoteSearchAbortController) {
			try {
				this.remoteSearchAbortController.abort();
			} catch (_) {}
		}

		if (this.loadMoreAbortController) {
			try {
				this.loadMoreAbortController.abort();
			} catch {}
		}

		this.remoteSearchAbortController = new AbortController();
		this.currentPage = 0;
		this.hasMore = true;
		this.isLoading = false;

		this.filterStaticOptions(val);

		if (val.length <= this.minSearchLength) {
			const res = await this.apiRequest(
				'',
				this.remoteSearchAbortController?.signal,
			);

			if (!res) return false;

			const isOffset = Boolean(this.apiFieldsMap?.offset);
			const defaultStart = isOffset ? 0 : 1;
			const pageStart =
				typeof this.apiPageStart === 'number'
					? this.apiPageStart
					: defaultStart;
			this.currentPage = pageStart;
			const currentlySelectedValsOnClear = Array.isArray(this.value)
				? this.value
				: this.value
					? [this.value]
					: [];
			const prevSelectedRemoteOnClear = (
				this.remoteOptions as IApiFieldMap[]
			).filter((el) =>
				currentlySelectedValsOnClear.includes(`${el[this.apiFieldsMap?.val]}`),
			);
			const resValSetOnClear = new Set(
				res.map((el: IApiFieldMap) => `${el[this.apiFieldsMap?.val]}`),
			);
			this.remoteOptions = [
				...res,
				...prevSelectedRemoteOnClear.filter(
					(el) => !resValSetOnClear.has(`${el[this.apiFieldsMap?.val]}`),
				),
			];

			Array.from(
				this.dropdown.querySelectorAll('[data-value]:not([data-static])'),
			).forEach((el: HTMLElement) => {
				if (
					!currentlySelectedValsOnClear.includes(el.getAttribute('data-value'))
				)
					el.remove();
			});
			Array.from(
				this.el.querySelectorAll(
					'option[value][data-hs-select-option]:not([data-static])',
				),
			).forEach((el: HTMLOptionElement) => {
				if (!currentlySelectedValsOnClear.includes(el.value)) el.remove();
			});

			if (res.length) this.buildOptionsFromRemoteData(res);
			else console.log('No data responded!');

			return false;
		}

		const res = await this.apiRequest(
			val,
			this.remoteSearchAbortController?.signal,
		);

		if (!res) return;

		const isOffset = Boolean(this.apiFieldsMap?.offset);
		const defaultStart = isOffset ? 0 : 1;
		const pageStart =
			typeof this.apiPageStart === 'number' ? this.apiPageStart : defaultStart;
		this.currentPage = pageStart;
		const currentlySelectedVals = Array.isArray(this.value)
			? this.value
			: this.value
				? [this.value]
				: [];
		const prevSelectedRemote = (this.remoteOptions as IApiFieldMap[]).filter(
			(el) => currentlySelectedVals.includes(`${el[this.apiFieldsMap?.val]}`),
		);
		const resValSet = new Set(
			res.map((el: IApiFieldMap) => `${el[this.apiFieldsMap?.val]}`),
		);
		this.remoteOptions = [
			...res,
			...prevSelectedRemote.filter(
				(el) => !resValSet.has(`${el[this.apiFieldsMap?.val]}`),
			),
		];
		let newIds = res.map((item: { id: string }) => `${item.id}`);
		let restOptions = null;
		const pseudoOptions = this.dropdown.querySelectorAll(
			'[data-value]:not([data-static])',
		);
		const options = this.el.querySelectorAll(
			'[data-hs-select-option]:not([data-static])',
		);

		options.forEach((el: HTMLOptionElement) => {
			const dataId = el.getAttribute('data-id');
			if (!newIds.includes(dataId) && !this.value?.includes(el.value)) {
				this.destroyOriginalOption(el.value);
			}
		});

		pseudoOptions.forEach((el: HTMLElement) => {
			const dataId = el.getAttribute('data-id');
			if (
				!newIds.includes(dataId) &&
				!this.value?.includes(el.getAttribute('data-value'))
			) {
				this.destroyOption(el.getAttribute('data-value'));
			} else newIds = newIds.filter((item: string) => item !== dataId);
		});

		restOptions = res.filter((item: { id: string }) =>
			newIds.includes(`${item.id}`),
		);

		if (restOptions.length) this.buildOptionsFromRemoteData(restOptions as []);
		else console.log('No data responded!');
	}

	private filterStaticOptions(val: string) {
		const staticOptions = this.dropdown.querySelectorAll(
			'[data-value][data-static]',
		);
		const searchVal = val.trim().toLowerCase();

		staticOptions.forEach((el: HTMLElement) => {
			if (searchVal.length <= this.minSearchLength) {
				el.classList.remove('hidden');
			} else {
				const optionVal =
					el.getAttribute('data-title-value')?.toLowerCase() || '';
				const description = el.dataset.description?.toLowerCase() || '';
				const matches = this.optionMatchesQuery(
					searchVal,
					optionVal,
					description,
				);

				if (matches) {
					el.classList.remove('hidden');
				} else {
					el.classList.add('hidden');
				}
			}
		});
	}

	private normalizeSearchText(val: string = '') {
		return val.toLocaleLowerCase().replace(/\s+/g, ' ').trim();
	}

	private tokenizeSearchQuery(query: string): string[] {
		return this.normalizeSearchText(query).split(' ').filter(Boolean);
	}

	private charsSequenceMatch(query: string, target: string): boolean {
		const regexSafeVal = query
			.split('')
			.map((char) => (/\w/.test(char) ? `${char}[\\W_]*` : '\\W*'))
			.join('');
		const regex = new RegExp(regexSafeVal, 'i');

		return regex.test(target);
	}

	private optionMatchesQuery(
		queryRaw: string,
		titleRaw: string,
		descriptionRaw: string = '',
	): boolean {
		const query = this.normalizeSearchText(queryRaw);

		if (!query) return true;

		const title = this.normalizeSearchText(titleRaw);
		const description = this.normalizeSearchText(descriptionRaw);
		const targets = this.preventSearchInsideDescription
			? [title]
			: [title, description];

		if (this.searchMatchMode === 'token-all') {
			const tokens = this.tokenizeSearchQuery(query);

			return targets.some((target) =>
				tokens.every((token) => target.includes(token)),
			);
		}

		if (this.searchMatchMode === 'hybrid') {
			const tokens = this.tokenizeSearchQuery(query);

			return targets.some((target) => {
				const tokenAll = tokens.every((token) => target.includes(token));
				const charsSequence = this.charsSequenceMatch(query, target);

				return tokenAll || charsSequence;
			});
		}

		if (this.searchMatchMode === 'chars-sequence') {
			return targets.some((target) => this.charsSequenceMatch(query, target));
		}

		return targets.some((target) => target.includes(query));
	}

	private destroyOption(val: string) {
		const option = this.dropdown.querySelector(`[data-value="${val}"]`);

		if (!option) return false;

		option.remove();
	}

	private buildOriginalOption(
		title: string,
		val: string,
		id?: string | null,
		disabled?: boolean,
		selected?: boolean,
		options?: ISingleOptionOptions,
	) {
		const option = htmlToElement('<option></option>');
		option.setAttribute('value', val);
		if (disabled) option.setAttribute('disabled', 'disabled');
		if (selected) option.setAttribute('selected', 'selected');
		if (id) option.setAttribute('data-id', id);
		option.setAttribute('data-hs-select-option', JSON.stringify(options));
		option.innerText = title;

		this.el.append(option);
	}

	private destroyOriginalOption(val: string) {
		const option = this.el.querySelector(`[value="${val}"]`);

		if (!option) return false;

		option.remove();
	}

	private buildTagsInputHelper() {
		this.tagsInputHelper = document.createElement('span');
		this.tagsInputHelper.style.fontSize = window.getComputedStyle(
			this.tagsInput,
		).fontSize;
		this.tagsInputHelper.style.fontFamily = window.getComputedStyle(
			this.tagsInput,
		).fontFamily;
		this.tagsInputHelper.style.fontWeight = window.getComputedStyle(
			this.tagsInput,
		).fontWeight;
		this.tagsInputHelper.style.letterSpacing = window.getComputedStyle(
			this.tagsInput,
		).letterSpacing;
		this.tagsInputHelper.style.visibility = 'hidden';
		this.tagsInputHelper.style.whiteSpace = 'pre';
		this.tagsInputHelper.style.position = 'absolute';

		this.wrapper.appendChild(this.tagsInputHelper);
	}

	private calculateInputWidth() {
		this.tagsInputHelper.textContent =
			(this.tagsInput as HTMLInputElement).value ||
			(this.tagsInput as HTMLInputElement).placeholder;

		const inputPadding =
			parseInt(window.getComputedStyle(this.tagsInput).paddingLeft) +
			parseInt(window.getComputedStyle(this.tagsInput).paddingRight);
		const inputBorder =
			parseInt(window.getComputedStyle(this.tagsInput).borderLeftWidth) +
			parseInt(window.getComputedStyle(this.tagsInput).borderRightWidth);
		const newWidth =
			this.tagsInputHelper.offsetWidth + inputPadding + inputBorder;
		const maxWidth =
			this.wrapper.offsetWidth -
			(parseInt(window.getComputedStyle(this.wrapper).paddingLeft) +
				parseInt(window.getComputedStyle(this.wrapper).paddingRight));

		(this.tagsInput as HTMLInputElement).style.width = `${
			Math.min(newWidth, maxWidth) + 2
		}px`;
	}

	private adjustInputWidth() {
		this.buildTagsInputHelper();
		this.calculateInputWidth();
	}

	private onSelectOption(val: string) {
		this.clearSelections();

		if (this.isMultiple) {
			if (!Array.isArray(this.value)) this.value = [];

			this.value = this.value.includes(val)
				? this.value.filter((el) => el !== val)
				: [...this.value, val];

			this.selectMultipleItems();
			this.setNewValue();
		} else {
			this.value = val;
			this.selectSingleItem();
			this.setNewValue();
		}

		this.fireEvent('change', this.value);

		if (this.mode === 'tags') {
			const intersection = this.selectedItems.filter(
				(x) => !(this.value as string[]).includes(x),
			);
			if (intersection.length) {
				intersection.forEach((el) => {
					this.selectedItems = this.selectedItems.filter((elI) => elI !== el);
					this.wrapper.querySelector(`[data-tag-value="${el}"]`).remove();
				});
			}

			this.resetTagsInputField();
		}

		if (!this.isMultiple) {
			if (this.toggle.querySelector('[data-icon]')) this.setToggleIcon();
			if (this.toggle.querySelector('[data-title]')) this.setToggleTitle();
			this.close(true);
		}

		if (!this.hasValue() && this.mode === 'tags') {
			this.reassignTagsInputPlaceholder(this.placeholder);
		}

		if (this._isOpened && this.mode === 'tags' && this.tagsInput) {
			this.tagsInput.focus();
		}

		this.triggerChangeEventForNativeSelect();
	}

	private triggerChangeEventForNativeSelect() {
		const selectChangeEvent = new Event('change', { bubbles: true });
		(this.el as HTMLSelectElement).dispatchEvent(selectChangeEvent);

		dispatch('change.hs.select', this.el, this.value);
	}

	private addSelectOption(
		title: string,
		val: string,
		disabled?: boolean,
		selected?: boolean,
		options?: ISingleOptionOptions,
	) {
		this.selectOptions = [
			...this.selectOptions,
			{
				title,
				val,
				disabled,
				selected,
				options,
			},
		];
	}

	private removeSelectOption(val: string, isArray = false) {
		const hasOption = !!this.selectOptions.some(
			(el: ISingleOption) => el.val === val,
		);

		if (!hasOption) return false;

		this.selectOptions = this.selectOptions.filter(
			(el: ISingleOption) => el.val !== val,
		);

		this.value = isArray
			? (this.value as string[]).filter((item: string) => item !== val)
			: val;
	}

	private resetTagsInputField() {
		(this.tagsInput as HTMLInputElement).value = '';

		this.reassignTagsInputPlaceholder('');
		this.searchOptions('');
	}

	private clearSelections() {
		const options = this.el.querySelectorAll('option');

		Array.from(this.dropdown.children).forEach((el) => {
			if (el.classList.contains('selected')) el.classList.remove('selected');
		});
		Array.from(options).forEach((el) => {
			if ((el as HTMLOptionElement).selected) {
				(el as HTMLOptionElement).selected = false;
			}
		});
	}

	private setNewValue() {
		if (this.mode === 'tags') {
			this.setTagsItems();
		} else {
			if (this.optionAllowEmptyOption && this.value === '') {
				const emptyOption = this.selectOptions.find(
					(el: ISingleOption) => el.val === '',
				);
				this.toggleTextWrapper.innerHTML =
					emptyOption?.title || this.placeholder;
			} else {
				if (this.hasValue()) {
					if (this.apiUrl) {
						const selectedItem = this.dropdown.querySelector(
							`[data-value="${this.value}"]`,
						);
						if (selectedItem) {
							this.toggleTextWrapper.innerHTML =
								selectedItem.getAttribute('data-title-value') ||
								this.placeholder;
						} else {
							const staticOption = this.staticOptions.find(
								(el: ISingleOption) => el.val === this.value,
							);
							if (staticOption) {
								this.toggleTextWrapper.innerHTML = staticOption.title;
							} else {
								const selectedOption = (
									this.remoteOptions as IApiFieldMap[]
								).find((el) => {
									const val = el[this.apiFieldsMap.val]
										? `${el[this.apiFieldsMap.val]}`
										: (el[this.apiFieldsMap.title] as string);
									return val === this.value;
								});
								this.toggleTextWrapper.innerHTML = selectedOption
									? `${selectedOption[this.apiFieldsMap.title]}`
									: this.stringFromValue();
							}
						}
					} else {
						this.toggleTextWrapper.innerHTML = this.stringFromValue();
					}
				} else {
					this.toggleTextWrapper.innerHTML = this.placeholder;
				}
			}
		}
	}

	private stringFromValueBasic(options: ISingleOption[]) {
		const value: string[] = [];
		let title = '';

		options.forEach((el: ISingleOption) => {
			if (this.isMultiple) {
				if (Array.isArray(this.value) && this.value.includes(el.val)) {
					value.push(el.title);
				}
			} else {
				if (this.value === el.val) value.push(el.title);
			}
		});

		if (
			this.toggleCountText !== undefined &&
			this.toggleCountText !== null &&
			value.length >= this.toggleCountTextMinItems
		) {
			if (this.toggleCountTextMode === 'nItemsAndCount') {
				const nItems = value.slice(0, this.toggleCountTextMinItems - 1);
				const tempTitle = [nItems.join(this.toggleSeparators.items)];
				const count = `${value.length - nItems.length}`;

				if (this?.toggleSeparators?.betweenItemsAndCounter) {
					tempTitle.push(this.toggleSeparators.betweenItemsAndCounter);
				}
				if (this.toggleCountText) {
					switch (this.toggleCountTextPlacement) {
						case 'postfix-no-space':
							tempTitle.push(`${count}${this.toggleCountText}`);
							break;
						case 'prefix-no-space':
							tempTitle.push(`${this.toggleCountText}${count}`);
							break;
						case 'prefix':
							tempTitle.push(`${this.toggleCountText} ${count}`);
							break;
						default:
							tempTitle.push(`${count} ${this.toggleCountText}`);
							break;
					}
				}

				title = tempTitle.join(' ');
			} else {
				title = `${value.length} ${this.toggleCountText}`;
			}
		} else {
			title = value.join(this.toggleSeparators.items);
		}

		return title;
	}

	private stringFromValueRemoteData() {
		if (!this.dropdown) {
			return this.stringFromValueBasic(this.selectOptions);
		}

		const options = this.dropdown.querySelectorAll('[data-title-value]');
		const value: string[] = [];
		let title = '';

		options.forEach((el: HTMLElement) => {
			const dataValue = el.getAttribute('data-value');
			const dataTitleValue = el.getAttribute('data-title-value');

			if (this.isMultiple) {
				if (Array.isArray(this.value) && this.value.includes(dataValue)) {
					value.push(dataTitleValue);
				}
			} else {
				if (this.value === dataValue) value.push(dataTitleValue);
			}
		});

		if (
			this.toggleCountText &&
			this.toggleCountText !== '' &&
			value.length >= this.toggleCountTextMinItems
		) {
			if (this.toggleCountTextMode === 'nItemsAndCount') {
				const nItems = value.slice(0, this.toggleCountTextMinItems - 1);

				title = `${nItems.join(
					this.toggleSeparators.items,
				)} ${this.toggleSeparators.betweenItemsAndCounter} ${
					value.length - nItems.length
				} ${this.toggleCountText}`;
			} else {
				title = `${value.length} ${this.toggleCountText}`;
			}
		} else {
			title = value.join(this.toggleSeparators.items);
		}

		return title;
	}

	private stringFromValue() {
		const result = this.apiUrl
			? this.stringFromValueRemoteData()
			: this.stringFromValueBasic(this.selectOptions);

		return result;
	}

	private selectSingleItem() {
		const options = this.el.querySelectorAll('option');
		const selectedOption = Array.from(options).find(
			(el) => this.value === (el as HTMLOptionElement).value,
		);
		(selectedOption as HTMLOptionElement).selected = true;

		const selectedItem = Array.from(this.dropdown.children).find(
			(el) =>
				this.value === (el as HTMLOptionElement).getAttribute('data-value'),
		);

		if (selectedItem) selectedItem.classList.add('selected');

		this.sortElements(this.el, 'option');
		this.sortElements(this.dropdown, '[data-value]');
	}

	private selectMultipleItems() {
		if (!Array.isArray(this.value)) return;
		const options = this.el.querySelectorAll('option');

		Array.from(this.dropdown.children)
			.filter((el) => this.value.includes(el.getAttribute('data-value')))
			.forEach((el) => el.classList.add('selected'));

		Array.from(options)
			.filter((el) => this.value.includes((el as HTMLOptionElement).value))
			.forEach((el) => ((el as HTMLOptionElement).selected = true));

		this.sortElements(this.el, 'option');
		this.sortElements(this.dropdown, '[data-value]');
	}

	private unselectMultipleItems() {
		const options = this.el.querySelectorAll('option');

		Array.from(this.dropdown.children).forEach((el) =>
			el.classList.remove('selected'),
		);
		Array.from(options).forEach(
			(el) => ((el as HTMLOptionElement).selected = false),
		);

		this.sortElements(this.el, 'option');
		this.sortElements(this.dropdown, '[data-value]');
	}

	private searchOptions(val: string) {
		if (val.length <= this.minSearchLength) {
			if (this.searchNoResult) {
				this.searchNoResult.remove();
				this.searchNoResult = null;
			}

			const options = this.dropdown.querySelectorAll('[data-value]');

			options.forEach((el) => {
				el.classList.remove('hidden');
			});

			return false;
		}

		if (this.searchNoResult) {
			this.searchNoResult.remove();
			this.searchNoResult = null;
		}

		this.searchNoResult = htmlToElement(this.searchNoResultTemplate);
		this.searchNoResult.innerText = this.searchNoResultText;
		classToClassList(this.searchNoResultClasses, this.searchNoResult);

		const options = this.dropdown.querySelectorAll('[data-value]');
		let hasItems = false;
		let countLimit: number;

		if (this.searchLimit) countLimit = 0;

		options.forEach((el) => {
			const optionVal = el.getAttribute('data-title-value') || '';
			const description = (el as HTMLElement)?.dataset?.description || '';
			const matches = this.optionMatchesQuery(val, optionVal, description);
			const condition =
				!matches || (this.searchLimit && countLimit >= this.searchLimit);

			if (condition) {
				el.classList.add('hidden');
			} else {
				el.classList.remove('hidden');

				hasItems = true;

				if (this.searchLimit) countLimit++;
			}
		});

		if (!hasItems) this.dropdown.append(this.searchNoResult);
	}

	private eraseToggleIcon() {
		const icon = this.toggle.querySelector('[data-icon]');

		if (icon) {
			icon.innerHTML = null;
			icon.classList.add('hidden');
		}
	}

	private eraseToggleTitle() {
		const title = this.toggle.querySelector('[data-title]');

		if (title) {
			title.innerHTML = this.placeholder;
		} else {
			this.toggleTextWrapper.innerHTML = this.placeholder;
		}
	}

	private toggleFn() {
		if (this._isOpened) this.close();
		else this.open();
	}

	// Accessibility methods
	private setupAccessibility(): void {
		this.accessibilityComponent =
			window.HSAccessibilityObserver.registerComponent(
				this.wrapper,
				{
					onEnter: () => {
						if (!this._isOpened) {
							this.open();
						} else {
							const highlighted = this.dropdown.querySelector(
								'.hs-select-option-highlighted',
							);
							if (highlighted) {
								this.onSelectOption(
									highlighted.getAttribute('data-value') || '',
								);
								if (this._isOpened) {
									(highlighted as HTMLElement).focus();
								}
							}
						}
					},
					onSpace: () => {
						if (!this._isOpened) {
							this.open();
						} else {
							const highlighted = this.dropdown.querySelector(
								'.hs-select-option-highlighted',
							);
							if (highlighted) {
								this.onSelectOption(
									highlighted.getAttribute('data-value') || '',
								);
								if (this._isOpened) {
									(highlighted as HTMLElement).focus();
								}
							}
						}
					},
					onEsc: () => {
						if (this._isOpened) {
							this.close(true);
						}
					},
					onArrow: (evt: KeyboardEvent) => {
						if (evt.metaKey) return;

						if (!this._isOpened && evt.key === 'ArrowDown') {
							this.open();
							return;
						}

						if (this._isOpened) {
							switch (evt.key) {
								case 'ArrowDown':
									this.focusMenuItem('next');
									break;
								case 'ArrowUp':
									this.focusMenuItem('prev');
									break;
								case 'Home':
									this.onStartEnd(true);
									break;
								case 'End':
									this.onStartEnd(false);
									break;
							}
						}
					},
					onHome: () => {
						if (this._isOpened) this.onStartEnd(true);
					},
					onEnd: () => {
						if (this._isOpened) this.onStartEnd(false);
					},
					onTab: () => {
						if (this._isOpened) this.close();
					},
				},
				this._isOpened,
				'Select',
				'.hs-select',
				this.dropdown,
			);
	}

	private focusMenuItem(direction: 'next' | 'prev'): void {
		const options = Array.from(
			this.dropdown.querySelectorAll(':scope > *:not(.hidden)'),
		).filter(
			(el: any) =>
				!el.classList.contains('disabled') && !el.hasAttribute('data-optgroup'),
		);

		if (!options.length) return;

		const current = this.dropdown.querySelector(
			'.hs-select-option-highlighted',
		);
		const currentIndex = current ? options.indexOf(current) : -1;
		const nextIndex =
			direction === 'next'
				? (currentIndex + 1) % options.length
				: (currentIndex - 1 + options.length) % options.length;

		if (current) current.classList.remove('hs-select-option-highlighted');
		options[nextIndex].classList.add('hs-select-option-highlighted');
		(options[nextIndex] as HTMLElement).focus();
	}

	private onStartEnd(isStart = true): void {
		if (!this.dropdown) return;

		const options = Array.from(
			this.dropdown.querySelectorAll(':scope > *:not(.hidden)'),
		).filter((el: any) => !el.classList.contains('disabled'));

		if (!options.length) return;

		const current = this.dropdown.querySelector(
			'.hs-select-option-highlighted',
		);
		if (current) current.classList.remove('hs-select-option-highlighted');

		const index = isStart ? 0 : options.length - 1;
		options[index].classList.add('hs-select-option-highlighted');
		(options[index] as HTMLElement).focus();
	}

	// Public methods
	public destroy() {
		// Remove listeners
		if (this.wrapper) {
			this.wrapper.removeEventListener('click', this.onWrapperClickListener);
		}
		if (this.toggle) {
			this.toggle.removeEventListener('click', this.onToggleClickListener);
		}
		if (this.tagsInput) {
			this.tagsInput.removeEventListener(
				'focus',
				this.onTagsInputFocusListener,
			);
			this.tagsInput.removeEventListener(
				'input',
				this.onTagsInputInputListener,
			);
			this.tagsInput.removeEventListener(
				'input',
				this.onTagsInputInputSecondListener,
			);
			this.tagsInput.removeEventListener(
				'keydown',
				this.onTagsInputKeydownListener,
			);
		}

		if (this.search) {
			this.search.removeEventListener('input', this.onSearchInputListener);
		}

		const parent = this.el.parentElement.parentElement;

		this.el.classList.add('hidden');
		this.el.style.display = '';
		parent.prepend(this.el);
		parent.querySelector('.hs-select').remove();
		this.wrapper = null;
		this.disabledObserver?.disconnect();
		this.disabledObserver = null;

		window.$hsSelectCollection = window.$hsSelectCollection.filter(
			({ element }) => element.el !== this.el,
		);
	}

	public open() {
		const currentlyOpened =
			window?.$hsSelectCollection?.find((el) => el.element.isOpened()) || null;

		if (currentlyOpened) currentlyOpened.element.close();
		if (this.animationInProcess) return false;

		this.animationInProcess = true;

		if (this.dropdownScope === 'window') {
			this.dropdown.classList.add('invisible');
		}
		this.dropdown.classList.remove('hidden');

		if (this.dropdownScope !== 'window') this.recalculateDirection();

		setTimeout(() => {
			if (this?.toggle?.ariaExpanded) this.toggle.ariaExpanded = 'true';
			this.wrapper.classList.add('active');
			this.dropdown.classList.add('opened');
			if (
				this.dropdown.classList.contains('w-full') &&
				this.dropdownScope === 'window'
			) {
				this.updateDropdownWidth();
			}

			if (this.floatingUIInstance && this.dropdownScope === 'window') {
				this.floatingUIInstance.update();
				this.dropdown.classList.remove('invisible');
			}
			if (this.hasSearch && !this.preventSearchFocus) this.search.focus();

			this.animationInProcess = false;

			if (this.scrollToSelected && this.dropdown) {
				const selectedOption = this.dropdown.querySelector('.selected');

				if (selectedOption) {
					const dropdownHeight = this.dropdown.clientHeight;
					const optionTop = (selectedOption as HTMLElement).offsetTop;
					const optionHeight = (selectedOption as HTMLElement).clientHeight;
					const scrollPosition =
						optionTop - dropdownHeight / 2 + optionHeight / 2;

					this.dropdown.scrollTop = scrollPosition;
				}
			}
		});

		this._isOpened = true;

		if (window.HSAccessibilityObserver && this.accessibilityComponent) {
			window.HSAccessibilityObserver.updateComponentState(
				this.accessibilityComponent,
				this._isOpened,
			);
		}
	}

	public close(forceFocus = false) {
		if (this.animationInProcess) return false;

		this.animationInProcess = true;

		if (this?.toggle?.ariaExpanded) this.toggle.ariaExpanded = 'false';
		this.wrapper.classList.remove('active');
		this.dropdown.classList.remove('opened', 'bottom-full', 'top-full');
		if (this.dropdownDirectionClasses?.bottom) {
			this.dropdown.classList.remove(this.dropdownDirectionClasses.bottom);
		}
		if (this.dropdownDirectionClasses?.top) {
			this.dropdown.classList.remove(this.dropdownDirectionClasses.top);
		}
		this.dropdown.style.marginTop = '';
		this.dropdown.style.marginBottom = '';

		afterTransition(this.dropdown, () => {
			this.dropdown.classList.add('hidden');
			if (this.hasSearch && this.mode !== 'tags') {
				this.search.value = '';

				if (!this.apiUrl) {
					this.search.dispatchEvent(new Event('input', { bubbles: true }));
				} else {
					this.filterStaticOptions('');
				}

				this.search.blur();
			}

			if (forceFocus) {
				if (this.mode?.includes('tags')) this.wrapper.focus();
				else this.toggle.focus();
			}

			this.animationInProcess = false;
		});

		this.dropdown
			.querySelector('.hs-select-option-highlighted')
			?.classList.remove('hs-select-option-highlighted');
		this._isOpened = false;

		if (window.HSAccessibilityObserver && this.accessibilityComponent) {
			window.HSAccessibilityObserver.updateComponentState(
				this.accessibilityComponent,
				this._isOpened,
			);
		}
	}

	public addOption(items: ISingleOption | ISingleOption[]) {
		let i = `${this.selectOptions.length}`;
		const addOption = (option: ISingleOption) => {
			const { title, val, disabled, selected, options } = option;
			const hasOption = !!this.selectOptions.some(
				(el: ISingleOption) => el.val === val,
			);

			if (!hasOption) {
				this.addSelectOption(title, val, disabled, selected, options);
				this.buildOption(title, val, disabled, selected, options, i);
				this.buildOriginalOption(title, val, null, disabled, selected, options);

				if (selected && !this.isMultiple) this.onSelectOption(val);
			}
		};

		if (Array.isArray(items)) {
			items.forEach((option) => {
				addOption(option);
			});
		} else {
			addOption(items);
		}

		this.sortElements(this.el, 'option');
		this.sortElements(this.dropdown, '[data-value]');
	}

	public removeOption(values: string | string[]) {
		const removeOption = (val: string, isArray = false) => {
			const hasOption = !!this.selectOptions.some(
				(el: ISingleOption) => el.val === val,
			);

			if (hasOption) {
				this.removeSelectOption(val, isArray);
				this.destroyOption(val);
				this.destroyOriginalOption(val);

				if (this.value === val) {
					this.value = null;

					this.eraseToggleTitle();
					this.eraseToggleIcon();
				}
			}
		};

		if (Array.isArray(values)) {
			values.forEach((val) => {
				removeOption(val, this.isMultiple);
			});
		} else {
			removeOption(values, this.isMultiple);
		}

		this.setNewValue();

		this.sortElements(this.el, 'option');
		this.sortElements(this.dropdown, '[data-value]');
	}

	public recalculateDirection() {
		if (
			this?.dropdownVerticalFixedPlacement &&
			(this.dropdown.classList.contains('bottom-full') ||
				this.dropdown.classList.contains('top-full'))
		)
			return false;

		if (this?.dropdownVerticalFixedPlacement === 'top') {
			this.dropdown.classList.add('bottom-full');
			this.dropdown.style.marginBottom = `${this.dropdownSpace}px`;
		} else if (this?.dropdownVerticalFixedPlacement === 'bottom') {
			this.dropdown.classList.add('top-full');
			this.dropdown.style.marginTop = `${this.dropdownSpace}px`;
		} else if (
			isEnoughSpace(
				this.dropdown,
				this.toggle || this.tagsInput,
				'bottom',
				this.dropdownSpace,
				this.viewport,
			)
		) {
			this.dropdown.classList.remove('bottom-full');
			if (this.dropdownDirectionClasses?.bottom) {
				this.dropdown.classList.remove(this.dropdownDirectionClasses.bottom);
			}
			this.dropdown.style.marginBottom = '';
			this.dropdown.classList.add('top-full');
			if (this.dropdownDirectionClasses?.top) {
				this.dropdown.classList.add(this.dropdownDirectionClasses.top);
			}
			this.dropdown.style.marginTop = `${this.dropdownSpace}px`;
		} else {
			this.dropdown.classList.remove('top-full');
			if (this.dropdownDirectionClasses?.top) {
				this.dropdown.classList.remove(this.dropdownDirectionClasses.top);
			}
			this.dropdown.style.marginTop = '';
			this.dropdown.classList.add('bottom-full');
			if (this.dropdownDirectionClasses?.bottom) {
				this.dropdown.classList.add(this.dropdownDirectionClasses.bottom);
			}
			this.dropdown.style.marginBottom = `${this.dropdownSpace}px`;
		}
	}

	public isOpened(): boolean {
		return this._isOpened || false;
	}

	public containsElement(element: HTMLElement): boolean {
		return this.wrapper?.contains(element) || false;
	}

	public containsDropdownElement(element: HTMLElement): boolean {
		return this.dropdown?.contains(element) || false;
	}

	// Static methods
	private static findInCollection(
		target: HSSelect | HTMLElement | string,
	): ICollectionItem<HSSelect> | null {
		HSSelect.ensureGlobalHandlers();

		return (
			window.$hsSelectCollection.find((el) => {
				if (target instanceof HSSelect) return el.element.el === target.el;
				else if (typeof target === 'string') {
					return el.element.el === document.querySelector(target);
				} else return el.element.el === target;
			}) || null
		);
	}

	static getInstance(target: HTMLElement | string, isInstance?: boolean) {
		HSSelect.ensureGlobalHandlers();

		const elInCollection = window.$hsSelectCollection.find(
			(el) =>
				el.element.el ===
				(typeof target === 'string' ? document.querySelector(target) : target),
		);

		return elInCollection
			? isInstance
				? elInCollection
				: elInCollection.element
			: null;
	}

	static autoInit() {
		HSSelect.ensureGlobalHandlers();

		if (window.$hsSelectCollection) {
			window.$hsSelectCollection = window.$hsSelectCollection.filter(
				({ element }) => document.contains(element.el),
			);
		}

		document
			.querySelectorAll('[data-hs-select]:not(.--prevent-on-load-init)')
			.forEach((el: HTMLElement) => {
				if (
					!window.$hsSelectCollection.find(
						(elC) => (elC?.element?.el as HTMLElement) === el,
					)
				) {
					const data = el.getAttribute('data-hs-select');
					const options: ISelectOptions = data ? JSON.parse(data) : {};

					new HSSelect(el, options);
				}
			});
	}

	private static ensureGlobalHandlers() {
		if (typeof window === 'undefined') return;

		if (!window.$hsSelectCollection) window.$hsSelectCollection = [];
		if (HSSelect.globalListenersInitialized) return;

		HSSelect.globalListenersInitialized = true;

		window.addEventListener('click', (evt) => {
			const evtTarget = evt.target;

			HSSelect.closeCurrentlyOpened(evtTarget as HTMLElement);
		});
	}

	static open(target: HSSelect | HTMLElement | string) {
		const instance = HSSelect.findInCollection(target);

		if (instance && !instance.element.isOpened()) instance.element.open();
	}

	static close(target: HSSelect | HTMLElement | string) {
		const instance = HSSelect.findInCollection(target);

		if (instance && instance.element.isOpened()) instance.element.close();
	}

	static closeCurrentlyOpened(evtTarget: HTMLElement | null = null) {
		if (
			!evtTarget.closest('.hs-select.active') &&
			!evtTarget.closest('[data-hs-select-dropdown].opened')
		) {
			const currentlyOpened =
				window.$hsSelectCollection.filter((el) => el.element.isOpened()) ||
				null;

			if (currentlyOpened) {
				currentlyOpened.forEach((el) => {
					el.element.close();
				});
			}
		}
	}
}

export default HSSelect;
