Newer
Older
gnexus-ui-kit / src / js / components / advanced-select.js
@root root 5 hours ago 6 KB Tech
function scrollToElementInFocus(container) {
	const focus = container.querySelector(".focus");
	if (!focus) return;

	const container_rect = container.getBoundingClientRect();
	const focus_rect = focus.getBoundingClientRect();

	if (focus_rect.top < container_rect.top) {
		container.scrollTop -= (container_rect.top - focus_rect.top);
	} else if (focus_rect.bottom > container_rect.bottom) {
		container.scrollTop += (focus_rect.bottom - container_rect.bottom);
	}
}

function autoSetState(container) {
	const totalViewed = container.advancedSelect.optionsElements.length - container.querySelectorAll(".option.hide").length;
	if(totalViewed == 0) {
		container.advancedSelect.showState("not-found");
	} else {
		container.advancedSelect.showState("options");
	}
}

function firstVisibleOption(container) {
	return container.querySelector(".option:not(.hide)");
}

function lastVisibleOption(container) {
	return container.querySelector(".option:not(.hide):last-child");
}

function selectOption(input, container, option) {
	if(!option) {
		return;
	}

	input.value = option.dataset.displayValue;
	input.blur();
	input.dispatchEvent(new Event("input", { bubbles: true }));
	input.dispatchEvent(new Event("change", { bubbles: true }));
	container.advancedSelect.dispatchEvent("selected");
	container.advancedSelect.closeList();
}

export default function advancedSelect(input, options, notFoundText) {
	const container = document.createElement("div");
	container.classList.add("advanced-select");

	const popup = document.createElement("div");
	popup.className = "popup-options-container";

	const notFound = document.createElement("div");
	notFound.className = "not-found";
	notFound.textContent = notFoundText ?? "Nothing found";

	const optionsContainer = document.createElement("div");
	optionsContainer.className = "options";

	for(let optionValue in options) {
		const option = document.createElement("div");
		option.className = "option";
		option.dataset.value = optionValue;
		option.dataset.displayValue = options[optionValue];
		option.textContent = options[optionValue];
		optionsContainer.append(option);
	}

	popup.append(notFound, optionsContainer);
	container.append(popup);

	const existsOption = (value, options) => {
		for(let optionValue in options) {
			if(options[optionValue] == value) {
				const ret = {};
				ret[optionValue] = options[optionValue];
				return ret;
			}
		}

		return false;
	}

	container.advancedSelect = {
		isOpened: false,
		options: options,
		eventsHandlers: {
			openList: [],
			closeList: [],
			selected: [],
			changed: [],
		},
		openList: () => {
			container.advancedSelect.isOpened = true;
			container.classList.add("a-show");
			autoSetState(container);
			container.advancedSelect.dispatchEvent("openList");
		},
		closeList: () => {
			container.advancedSelect.isOpened = false;
			container.classList.remove("a-show");
			autoSetState(container);
			container.advancedSelect.dispatchEvent("closeList");
		},
		showState: stateName => {
			if(stateName == "options") {
				container.querySelector(".options").classList.add("show");
				container.querySelector(".not-found").classList.remove("show");
			} else if(stateName == "not-found") {
				container.querySelector(".options").classList.remove("show");
				container.querySelector(".not-found").classList.add("show");
			}
		},
		optionsElements: container.querySelectorAll(".option"),
		value: () => {
			const option = existsOption(input.value, options);

			return { 
				inputValue: input.value,
				isOption: option ? true : false,
				option 
			};
		},
		addEventListener: (name, handler) => {
			if(typeof container.advancedSelect.eventsHandlers[name] != "undefined") {
				return container.advancedSelect.eventsHandlers[name].push(handler);
			}

			console.error("Advanced Select component.", "addEventListener()", "Invalid event name");
		},
		dispatchEvent: name => {
			if(typeof container.advancedSelect.eventsHandlers[name] == "undefined") {
				return console.error("Advanced Select component.", "dispatchEvent()", "Invalid event name");
			}
			
			for(let eventHandler of container.advancedSelect.eventsHandlers[name]) {
				eventHandler(container);
			}
		}
	};

	input.setAttribute("autocomplete", "nope");

	input.advancedSelect = {
		value: () => container.advancedSelect.value()
	}

	input.addEventListener("focus", e => {
		container.advancedSelect.openList();
	});

	input.addEventListener("blur", e => {
		requestAnimationFrame(() => {
			if(!container.matches(":hover")) {
				container.advancedSelect.closeList();
			}
		});
	});

	input.addEventListener("keydown", e => {
		if(e.key === "ArrowUp") {
			e.preventDefault();
			// up
			const current = container.querySelector(".option.focus");
			if(current) {
				current.classList.remove("focus");
				let prev = current.previousElementSibling;

				while (prev) {
					if (!prev.classList.contains("hide")) {
						break;
					}
					prev = prev.previousElementSibling;
				}

				if(!prev) {
					prev = firstVisibleOption(container);
				}

				prev?.classList.add("focus");
			} else {
				lastVisibleOption(container)?.classList.add("focus");
			}

			scrollToElementInFocus(container);
		} else if(e.key === "ArrowDown") {
			e.preventDefault();
			// down
			const current = container.querySelector(".option.focus");
			if(current) {
				current.classList.remove("focus");
				let next = current.nextElementSibling;

				while (next) {
					if (!next.classList.contains("hide")) {
						break;
					}
					next = next.nextElementSibling;
				}

				if(!next) {
					next = firstVisibleOption(container);
				}

				next?.classList.add("focus");
			} else {
				firstVisibleOption(container)?.classList.add("focus");
			}

			scrollToElementInFocus(container);
		} else if(e.key === "Enter") {
			e.preventDefault();
			let selected = container.querySelector(".option.focus");
			selectOption(input, container, selected);
		} else if(e.key === "Escape") {
			container.advancedSelect.closeList();
			input.blur();
		}
	});

	input.addEventListener("input", e => {
		const val = e.currentTarget.value.toLowerCase();
		if(val == "") {
			container.advancedSelect.optionsElements.forEach(i => i.classList.remove("hide"));
		} else {
			[ ...container.advancedSelect.optionsElements ]
				.filter(i => i.dataset.displayValue.toLowerCase().indexOf(val) != -1)
				.forEach(i => i.classList.remove("hide"));

			[ ...container.advancedSelect.optionsElements ]
				.filter(i => i.dataset.displayValue.toLowerCase().indexOf(val) == -1)
				.forEach(i => i.classList.add("hide"));

			autoSetState(container);

			container.querySelector(".option.focus")?.classList.remove("focus");
		}
	});

	input.addEventListener("change", e => {
		container.advancedSelect.dispatchEvent("changed");
	});

	[ ...container.advancedSelect.optionsElements ].forEach(option => {
		option.addEventListener("pointerdown", e => {
			e.preventDefault();
			selectOption(input, container, e.currentTarget);
		});
	});

	return container;
}