Newer
Older
smart-home-server / webclient / src / js / components / advanced-select.js
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");
	}
}

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

	let optionsList = ``;
	for(let optionValue in options) {
		optionsList += `<div class="option" data-value="${optionValue}" data-display-value="${options[optionValue]}">${options[optionValue]}</div>`;
	}

	let html = `
		<div class="popup-options-container">
			<div class="not-found">${notFoundText}</div>
			<div class="options">${optionsList}</div>
		</div>
	`;

	container.innerHTML = html;

	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 => {
		setTimeout(() => container.advancedSelect.closeList(), 20);
	});

	input.addEventListener("keydown", e => {
		if(e.keyCode == 38) {
			// 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 = container.querySelector(".option:not(.hide)");
				}

				prev.classList.add("focus");
			} else {
				container.querySelector(".option:not(.hide):last-child").classList.add("focus");
			}

			scrollToElementInFocus(container);
		} else if(e.keyCode == 40) {
			// 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 = container.querySelector(".option:not(.hide)");
				}

				next.classList.add("focus");
			} else {
				container.querySelector(".option:not(.hide)").classList.add("focus");
			}

			scrollToElementInFocus(container);
		} else if(e.keyCode == 13) {
			let selected = container.querySelector(".option.focus");
			if(!selected) return;
			input.value = selected.dataset.displayValue;
			input.blur();
			input.dispatchEvent(new Event("input", { bubbles: true }));
			input.dispatchEvent(new Event("change", { bubbles: true }));
			container.advancedSelect.dispatchEvent("selected");
		}
	});

	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("click", e => {
			input.value = e.currentTarget.dataset.displayValue;
			input.blur();
			input.dispatchEvent(new Event("input", { bubbles: true }));
			input.dispatchEvent(new Event("change", { bubbles: true }));
			container.advancedSelect.dispatchEvent("selected");
		});
	});

	return container;
}