irisrad-ui
Version:
UI elements developered for IRIS R&D Group Inc
202 lines (176 loc) • 6.49 kB
JavaScript
let debounceTimeout;
let searchTerm = "";
let onListBlurred;
let onKeyDown;
let onLabelClicked;
let onListItemClicked;
export default class Select {
constructor(element) {
this.selectEle = element;
this.optionObjects = getFormattedOptions(
element.querySelector("ul").childNodes
);
this.customSelectContainer = element.querySelector(
".iris-custom-select-container"
);
this.customSelectLabel = element.querySelector(".iris-custom-select-label");
this.optionList = element.querySelector(".iris-custom-select-list");
this.registerEvents();
setupCustomElement(this);
}
/**
* get the selected default option
*/
get selectedOption() {
const target = this.optionObjects.find((option) => option.selected);
if (target) {
return target;
} else {
const firstOption = this.optionObjects[0];
firstOption.selected = true;
firstOption.element.classList.add("selected");
if (this.selectEle.onChange) {
this.selectEle.onChange(firstOption.value);
}
return firstOption;
}
}
/**
* get the index of the selected option
*/
get selectedOptionIndex() {
return this.optionObjects.indexOf(this.selectedOption);
}
setSelectedValue(value) {
// find previously selected option;
const prevSelectedOption = this.selectedOption;
// set its selected state as false
prevSelectedOption.selected = false;
// remove selected css style from the option
// this.optionList
// .querySelector(`[data-value="${prevSelectedOption.value}"]`)
prevSelectedOption.element.classList.remove("selected");
// find the reference of the selected option
const newSelectedOption = this.optionObjects.find((option) => {
return option.value === value;
});
// set its selected state as true
newSelectedOption.selected = true;
// set the label's innerText with the one of the selected option
this.customSelectLabel.innerText = newSelectedOption.label;
// add stelected css style on the selected option
newSelectedOption.element.classList.add("selected");
// scroll to the selected option if necessary
newSelectedOption.element.scrollIntoView({ block: "center" });
// call the onchange callback function which is passed to the original element (select)
if (value !== prevSelectedOption.value && this?.selectEle?.onChange) {
this.selectEle.onChange(value);
}
}
registerEvents() {
onListBlurred = handlOnListBlurred.bind(this);
onKeyDown = handlOnKeyDown.bind(this);
onLabelClicked = handleOnLabelClicked.bind(this);
onListItemClicked = handleListItemClick.bind(this);
this.optionObjects.forEach((option) => {
option.element.addEventListener("click", onListItemClicked);
});
this.customSelectContainer.addEventListener("blur", onListBlurred);
this.customSelectContainer.addEventListener("keydown", onKeyDown);
this.customSelectContainer.addEventListener("click", onLabelClicked);
}
unregisterListeners() {
this.customSelectContainer.removeEventListener("blur", onListBlurred);
this.customSelectContainer.removeEventListener("keydown", onKeyDown);
this.customSelectContainer.removeEventListener("click", onLabelClicked);
this.optionObjects.forEach((option) => {
option.element.removeEventListener("click", onListItemClicked);
});
}
}
function handlOnKeyDown(event) {
switch (event.code) {
case "Space":
this.optionList.classList.toggle("show");
break;
case "ArrowUp": {
const prevOption = this.optionObjects[this.selectedOptionIndex - 1];
if (prevOption) {
this.setSelectedValue(prevOption.value);
}
break;
}
case "ArrowDown": {
const nextOption = this.optionObjects[this.selectedOptionIndex + 1];
if (nextOption) {
this.setSelectedValue(nextOption.value);
}
break;
}
case "Enter":
this.optionList.classList.toggle("show");
break;
case "Escape":
this.optionList.classList.remove("show");
break;
default: {
clearTimeout(debounceTimeout);
searchTerm += event.key;
debounceTimeout = setTimeout(() => {
searchTerm = "";
}, 500);
const searchedOption = this.optionObjects.find((option) => {
return option.label.toLowerCase().startsWith(searchTerm);
});
if (searchedOption) {
this.setSelectedValue(searchedOption.value);
}
}
}
}
function handlOnListBlurred() {
this.optionList.classList.remove("show");
}
// click listener for each list option
function handleListItemClick(event) {
event.stopPropagation();
const optionValue = event.target.getAttribute("value");
this.setSelectedValue(optionValue);
this.optionList.classList.remove("show");
}
function handleOnLabelClicked() {
if (!this.optionList.classList.contains("show")) {
this.optionList.classList.toggle("show");
}
this.selectedOption.element.scrollIntoView({ block: "center" });
}
function setupCustomElement(select) {
const customSelectContainer = select.customSelectContainer;
const customSelectLabel = select.customSelectLabel;
customSelectContainer.tabIndex = 0;
customSelectLabel.innerText = select.selectedOption.label;
}
/**
* @description given a nodelist of html default options, generate an array of objects which containes the following properties:
* value: option's value of property [value]
* label: the html innerText of the option element
* selected: the boolean value of property [selected]
* element: the option node itself
*
* @param {Array} optionElements an array of default html option elements (NodeList)
* @returns an array of objecs, in the shape of {value: string, label: string, selected: boolean, element: the_option_node_itself}
*/
function getFormattedOptions(optionElements) {
return [...optionElements].map((optionElement) => {
optionElement.classList.add("iris-custom-select-option");
if (optionElement.selected) {
optionElement.classList.add("selected");
}
return {
value: optionElement.getAttribute("value"),
label: optionElement.innerText,
selected: optionElement.selected,
element: optionElement,
};
});
}