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;
}