irisrad-ui
Version:
UI elements developered for IRIS R&D Group Inc
272 lines (249 loc) • 8.56 kB
JavaScript
let debounceTimeout;
let searchTerm = "";
let onListBlurred;
let onKeyDown;
let onLabelClicked;
let onListItemClicked;
export default class MultiSelect {
constructor(element) {
this.multiSelectWrapper = element;
this.values = element.values || [];
this.onChange = element.onChange;
this.label = element.querySelectorAll(".iris-multi-select-text-area")[0];
this.list = element.querySelector(".iris-custom-select-list");
this.optionObjects = getOptionObjects(
element.querySelector("[iris-multi-select-list]"),
this.values
);
this.multiSelectWrapper.tabIndex = 0;
this.registerEventListeners();
}
toggleList(isShown) {
this.list.classList.toggle("show", isShown);
}
/**
* @description if the values array contains the value, remove the value from the array
* otherwise, add the value to the array
*
* @param {String} value value of the option
*/
toggleValues(value) {
const index = this.values.indexOf(value);
if (index < 0) {
this.values.push(value);
} else {
this.values.splice(index, 1);
}
// this.label.value = this.values.join(", ");
this.label.scrollTo(0, this.label.scrollHeight);
// this.label.innerText = this.values.join(", ");
if (this.onChange) {
this.onChange(this.values);
}
const labels = [];
this.optionObjects.map((option) => {
if (this.values.indexOf(option.value) >= 0) {
labels.push(option.label);
}
});
this.label.innerText = labels.join(", ");
}
/**
* @description loop through all the options within the optionObjects, if the value of the option
* matches the one from the passed in option, removed the css class that highlighed the option,
* otherwise, add the highlighted css class to it.
*
* @param {HTMLElement} option the option element that contains the input checkbox
*/
highLightOption(option) {
this.searchedOption = option;
const { element, label } = option;
element.scrollIntoView({ block: "center" });
this.optionObjects.map((option) => {
const { element } = option;
const { classList } = element;
classList.toggle("searched", option.label === label);
});
}
/**
* @description this method would be called when user presses the "enter" key while an
* option is highlighted.
*
* It toggle the checked state of the option's element's input. Also, the values within the
* property values of the class would also be updated.
*
* @see toggleValues
*/
toggleSearchedOption() {
if (this.searchedOption) {
const { value, isChecked, element } = this.searchedOption;
element.querySelector("input").checked = !isChecked;
this.searchedOption.isChecked = !isChecked;
this.toggleValues(value);
}
}
/**
* @description this method should be called when the option list component is closed
* or when a user click on one of the options on the list.
*
* While the user act one of the above options, if there was searched option which is
* highlighted, the option should be unhighlighted the the referece of the searchedOption
* should be removed from the class.
*
*/
clearSearchedOption() {
if (this.searchedOption) {
const { element } = this.searchedOption;
element.classList.remove("searched");
this.searchedOption = undefined;
}
}
/**
* @returns index of the searchedOption within the optionObjects array.
*/
get searchedOptionIndex() {
if (this.searchedOption === undefined) {
return -1;
} else {
return this.optionObjects.findIndex(
(option) => option.value === this.searchedOption.value
);
}
}
registerEventListeners() {
onListBlurred = handleOnBlur.bind(this);
onLabelClicked = handleOnLableClick.bind(this);
onKeyDown = handleKeyDown.bind(this);
onListItemClicked = handleListItemClick.bind(this);
this.multiSelectWrapper.addEventListener("blur", onListBlurred);
this.multiSelectWrapper.addEventListener("click", onLabelClicked);
this.multiSelectWrapper.addEventListener("keydown", onKeyDown);
this.optionObjects.map((option) => {
option.element.addEventListener("click", onListItemClicked);
});
}
unRegisterEventListeners() {
this.multiSelectWrapper.removeEventListener("blur", onListBlurred);
this.multiSelectWrapper.removeEventListener("click", onLabelClicked);
this.multiSelectWrapper.removeEventListener("keydown", onKeyDown);
this.optionObjects.map((option) => {
option.element.removeEventListener("click", onListItemClicked);
});
}
}
/**
* @description
* set up the blur event listener for the wrapper so that when it is blur, the shown muti select list should be dismissed
* set up keydown event listeners, for instance, space, escape to toggle the show or dismiss of the multi select list
* with the miti select list is shown, arrow up or arrow down to navigate between options, text string to search and jump
* to corresponding option
*
* set up click event for each options within the optionObjects array so the checkbox within each of them would be toggled
* properly and corresponding value could be added or removed from the values array of the multSeelctEle object.
*
* @param {HTMLElement} multiSelectEle the HTMLElement that contains the wrapper, the toggle
* and the multi select list components
*/
/**
* @description given an array of HTMLElement in the shape of
* <li>
* <input
* type="checkbox"
* name="brilliance"
* value="value"
* />
* <span for="brilliance">inner_text_value</span>
* </li>
*
*
* return an array of objects in the shape of
* {
* value: value,
* label: inner_text_value,
* isChecked: whether_check_box_is_checked,
* element: the_HTMLElement_itself,
* };
*
* so that the access and manipulation of each HTMLElement would
* be easier and the code could be more concise.
*
* @param {Array} options an array of HTMLElements (LI)
* @param {Array} values an array of strings
* @returns
*/
function getOptionObjects(options, values) {
const elements = options.querySelectorAll("li");
return [...elements].map((element) => {
const value = element.querySelector("input").value;
const shouldCheck = values.indexOf(value) >= 0;
element.querySelector("input").checked = shouldCheck;
const label = element.querySelector("span").innerText;
return {
value,
label,
isChecked: shouldCheck,
element,
};
});
}
function handleOnBlur() {
// alert("blurred");
this.list.classList.remove("show");
}
function handleOnLableClick() {
this.list.classList.toggle("show");
}
function handleKeyDown(event) {
switch (event.code.toLowerCase()) {
case "space":
// open list
this.toggleList(true);
break;
case "escape":
// close list
this.list.classList.remove("show");
this.clearSearchedOption();
break;
case "enter":
// check or uncheck currently highlighted option
this.toggleSearchedOption();
break;
case "arrowup": {
const index = this.searchedOptionIndex;
if (index - 1 >= 0) {
this.highLightOption(this.optionObjects[index - 1]);
}
break;
}
case "arrowdown": {
const index = this.searchedOptionIndex;
if (index + 1 < this.optionObjects.length) {
this.highLightOption(this.optionObjects[index + 1]);
}
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.highLightOption(searchedOption);
}
}
}
}
function handleListItemClick(event) {
event.stopPropagation();
const label = event.target.innerText;
const ele = this.optionObjects.find((object) => object.label === label);
const { value, isChecked } = ele;
this.clearSearchedOption();
this.toggleValues(value);
ele.element.querySelector("input").checked = !isChecked;
ele.isChecked = !isChecked;
}