/**
* advancedSelect browser helper.
*
* Mirrors gnexus-ui-kit advancedSelect public surface.
*
* @param {HTMLInputElement} input - Text input to enhance
* @param {object} options - Map of value => displayValue
* @param {string} [notFoundText='Nothing found'] - Empty state message
* @returns {HTMLElement} Advanced select container
*/
function scrollToElementInFocus(container) {
const focus = container.querySelector(".option.focus");
if(!focus) return;
const containerRect = container.getBoundingClientRect();
const focusRect = focus.getBoundingClientRect();
if(focusRect.top < containerRect.top) {
container.scrollTop -= (containerRect.top - focusRect.top);
} else if(focusRect.bottom > containerRect.bottom) {
container.scrollTop += (focusRect.bottom - containerRect.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) {
const options = container.querySelectorAll(".option:not(.hide)");
return options[options.length - 1] || null;
}
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();
}
function existsOption(value, options) {
for(const optionValue in options) {
if(options[optionValue] === value) {
return { [optionValue]: options[optionValue] };
}
}
return false;
}
export default function advancedSelect(input, options, notFoundText) {
const wrapper = document.createElement("div");
wrapper.className = "advanced-select-container";
input.parentElement?.insertBefore(wrapper, input);
if(!input.classList.contains("input")) {
input.classList.add("input");
}
wrapper.append(input);
const container = document.createElement("div");
container.className = "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(const 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);
wrapper.append(container);
container.advancedSelect = {
isOpened: false,
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") {
container.advancedSelect.eventsHandlers[name].push(handler);
return;
}
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(const eventHandler of container.advancedSelect.eventsHandlers[name]) {
eventHandler(container);
}
}
};
input.setAttribute("autocomplete", "nope");
input.advancedSelect = {
value: () => container.advancedSelect.value()
};
input.addEventListener("focus", () => {
container.advancedSelect.openList();
});
input.addEventListener("blur", () => {
requestAnimationFrame(() => {
if(!container.matches(":hover")) {
container.advancedSelect.closeList();
}
});
});
input.addEventListener("keydown", event => {
if(event.key === "ArrowUp") {
event.preventDefault();
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 = lastVisibleOption(container);
}
prev?.classList.add("focus");
} else {
lastVisibleOption(container)?.classList.add("focus");
}
scrollToElementInFocus(container);
} else if(event.key === "ArrowDown") {
event.preventDefault();
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(event.key === "Enter") {
event.preventDefault();
const selected = container.querySelector(".option.focus");
selectOption(input, container, selected);
} else if(event.key === "Escape") {
container.advancedSelect.closeList();
input.blur();
}
});
input.addEventListener("input", event => {
const val = event.currentTarget.value.toLowerCase();
if(val === "") {
container.advancedSelect.optionsElements.forEach(i => i.classList.remove("hide"));
} else {
[...container.advancedSelect.optionsElements]
.filter(i => i.dataset.displayValue.toLowerCase().includes(val))
.forEach(i => i.classList.remove("hide"));
[...container.advancedSelect.optionsElements]
.filter(i => !i.dataset.displayValue.toLowerCase().includes(val))
.forEach(i => i.classList.add("hide"));
}
autoSetState(container);
container.querySelector(".option.focus")?.classList.remove("focus");
});
input.addEventListener("change", () => {
container.advancedSelect.dispatchEvent("changed");
});
[...container.advancedSelect.optionsElements].forEach(option => {
option.addEventListener("pointerdown", event => {
event.preventDefault();
selectOption(input, container, event.currentTarget);
});
});
return container;
}