/**
* The controller for a drop-down multi selector.
*
* Below is an implementation example. It shows the HTML and JS needed to create the component.
*
* HTML:
JavaScript:
new MmuiDropSelect({
id: "product_version",
placeholder: "Select a product",
onSelection: this.selectionHandler,
});
*/
/**
* Dropdown Multi-select JavaScript
* @deprecated Use MmuiDropChoiceComponent
*/
export class MmuiDropSelect {
options = {
id: null,
onOpen: null,
onClose: null,
onSelection: null,
placeholder: 'Select',
enableSearchBox: true,
searchBoxSelector: '#search-filter',
isNested: false, // control if the dropdown is nested or not
};
baseElement;
button;
buttonTitle;
selectBody;
searchFilter;
constructor(options) {
this.options = Object.assign({}, this.options, options);
this.baseElement = document.getElementById(this.options.id);
this.button = this.baseElement.querySelector('.btn');
this.button.addEventListener('click', this.onBtnClick);
this.buttonTitle = this.baseElement.querySelector(
'.mmui-drop-select-title'
);
this.selectBody = this.baseElement.querySelector('.mmui-drop-select-body');
const checkboxes = this.getCheckboxes();
checkboxes.forEach((item) => {
item.addEventListener('click', this.onSelection);
});
if (this.options.enableSearchBox) {
//searchBoxSelector should have prefix '#'
this.searchFilter = this.baseElement.querySelector(
this.options.searchBoxSelector
);
this.searchFilter.addEventListener('input', this.onSearchInput);
}
this.setTitle(this.getSelectedNames());
this.close();
}
open() {
if (!this.isOpen()) {
this.selectBody.classList.remove('d-none');
window.addEventListener('click', this.onBlur);
}
}
close() {
if (this.isOpen()) {
this.selectBody.classList.add('d-none');
window.removeEventListener('click', this.onBlur);
}
}
isOpen() {
return !this.selectBody.classList.contains('d-none');
}
setTitle(title) {
if (Array.isArray(title)) {
if (title.length == 0) {
this.buttonTitle.innerHTML = this.options.placeholder;
} else if (title.length == 1) {
this.buttonTitle.innerHTML = title[0];
} else {
this.buttonTitle.innerHTML = title[0] + ' + ' + (title.length - 1);
}
} else {
this.buttonTitle.innerHTML = title;
}
}
getCheckboxes() {
return this.baseElement.querySelectorAll('input[type="checkbox"]');
}
getSelectedValues() {
const selectedValues = [];
this.getCheckboxes().forEach(function (item) {
if (item.checked) {
selectedValues.push(item.value);
}
});
return selectedValues;
}
getSelectedNames() {
const selectedNames = [];
// if nested dropdown box, only count the children
if (this.options.isNested) {
this.getCheckboxes().forEach(function (item) {
if (item.checked && !item.dataset.isparent) {
selectedNames.push(item.getAttribute('data-display'));
}
});
} else {
this.getCheckboxes().forEach(function (item) {
if (item.checked) {
selectedNames.push(item.getAttribute('data-display'));
}
});
}
return selectedNames;
}
onBlur = (evt) => {
const mX = evt.clientX;
const mY = evt.clientY;
const rect = this.selectBody.getBoundingClientRect();
const inRect =
rect.left <= mX &&
mX <= rect.right &&
rect.top <= mY &&
mY <= rect.bottom;
if (this.isOpen() && !inRect) {
this.close();
}
};
onBtnClick = (evt) => {
evt.stopPropagation();
if (this.isOpen()) {
this.close();
if (this.options.onClose) {
this.options.onClose();
}
} else {
this.open();
if (this.options.onOpen) {
this.options.onOpen();
}
}
};
getCheckBoxByValue(value) {
let checkBox;
this.getCheckboxes().forEach(function (item) {
// suppose value is unique
if (item.value == value) {
checkBox = item;
return false;
}
});
return checkBox;
}
onSelection = (evt) => {
if (this.options.onSelection) {
this.options.onSelection(this.getSelectedValues());
}
// use this.options.isNested to determine if the parent and children checkbox will affect each other or not
if (this.options.isNested) {
// scenario 1: if parent is checked, check all children, if parent is unchecked, uncheck all children
// attention: the value of child node should use parent's value as prefix, followed by '_'
if (evt.target.dataset.isparent) {
this.getCheckboxes().forEach(function (item) {
if (item.value.startsWith(evt.target.value + '_')) {
item.checked = evt.target.checked;
}
});
}
// scenario 2: if children is selected, check its parent's all children to determine if parent should be checked
// or indeterminate. If all children are unselected, unchecked the parent
else {
// get parent_id
let parentId = evt.target.dataset.parentid,
isAllChecked = true,
isAllUnchecked = true;
// there is a chance that a check box has no nested children, it will not have parentid attribute
if (parentId != null) {
this.getCheckboxes().forEach(function (item) {
// find all siblings
if (item.dataset.parentid == parentId) {
isAllChecked = isAllChecked && item.checked;
isAllUnchecked = isAllUnchecked && !item.checked;
}
});
// determine the parent status
const parentCheckBox = this.getCheckBoxByValue(parentId);
this.statusCheck(parentCheckBox, isAllChecked, isAllUnchecked);
}
}
}
// define what to display when boxes are selected
this.setTitle(this.getSelectedNames());
};
onSearchInput = (evt) => {
let query = evt.currentTarget.value,
parentSelectors,
inputChoices,
inputChoice;
inputChoices = this.baseElement.querySelectorAll('input:disabled');
for (inputChoice of inputChoices) {
inputChoice.disabled = false;
}
if (query) {
query = query.trim().toLowerCase();
if (query.length > 0) {
let parent,
itemName,
choiceParentId,
isPossibleMatch,
nodeList = this.getCheckboxes(),
parentChoicesMatchChildren = [];
for (const item of nodeList) {
if (item.disabled) {
item.disabled = false;
}
itemName = item.dataset.display.toLowerCase();
isPossibleMatch = itemName.indexOf(query) >= 0;
parent = item.parentElement;
if (isPossibleMatch) {
if (parent.classList.contains('d-none')) {
parent.classList.remove('d-none');
}
choiceParentId = item.dataset.parentid;
if (choiceParentId) {
parentChoicesMatchChildren.push(choiceParentId);
}
} else {
if (!parent.classList.contains('d-none')) {
parent.classList.add('d-none');
}
}
}
let selectorArray = [],
parentElmnts;
for (choiceParentId of parentChoicesMatchChildren) {
selectorArray.push(`div[data-id='${choiceParentId}']`);
}
if (selectorArray.length > 0) {
parentSelectors = selectorArray.join(',');
parentElmnts = this.baseElement.querySelectorAll(parentSelectors);
for (parent of parentElmnts) {
if (parent.classList.contains('d-none')) {
parent.classList.remove('d-none');
parent.querySelector('input').disabled = true;
}
}
}
}
} else {
const nodeList = this.getCheckboxes();
for (const element of nodeList) {
const parent = element.parentElement;
if (parent.classList.contains('d-none')) {
parent.classList.remove('d-none');
}
}
}
};
filterBy(data) {
// Select the relevant metric types
const checkboxes = this.getCheckboxes();
for (let i = 0; i < checkboxes.length; i++) {
const element = checkboxes[i];
if (data.length == 0) {
const parent = element.parentElement;
if (parent.classList.contains('d-none')) {
parent.classList.remove('d-none');
}
} else {
const data_filter = element.getAttribute('data-filter');
const parent = element.parentElement;
let hasIt = false;
for (let j = 0; j < data.length; j++) {
if (data_filter.indexOf(data[j]) >= 0) {
hasIt = true;
}
}
if (!hasIt) {
if (!parent.classList.contains('d-none')) {
parent.classList.add('d-none');
}
element.checked = false;
} else {
if (parent.classList.contains('d-none')) {
parent.classList.remove('d-none');
}
}
}
}
}
/**
* the parentCheckBoxStatus() only works for nested drop down box,
* used case:
* when the drop down value is submitted, instead of using web app framework's template language to check if the box
* is checked or not, using this method to set the parent status to :indeterminate, checked, or unchecked
*/
parentCheckBoxStatus() {
const nestedCheckBox = document.getElementById(this.options.id);
const parentCheckBox = [].slice.call(
nestedCheckBox.querySelectorAll("input[data-isparent='1']")
);
// convert nodeoflist into array to get rid of the foreach error
parentCheckBox.forEach((parentItem) => {
const childCheckBox =
parentItem.parentElement.nextElementSibling.querySelectorAll('input'); // next sibling is class=pl-5, all chidlren are div
let isAllChecked = true,
isAllUnchecked = true;
childCheckBox.forEach(function (childItem) {
isAllChecked = isAllChecked && childItem.checked;
isAllUnchecked = isAllUnchecked && !childItem.checked;
});
this.statusCheck(parentItem, isAllChecked, isAllUnchecked);
});
}
/**
* a private function. created for reducing the code duplication
* @param item: the checkbox which is a parent checkbox with children checkbox
* @param isAllChecked: boolean, is all children checkbox all checked
* @param isAllUnchecked: boolean, is all children checkbox all unchecked
*/
private statusCheck(item, isAllChecked, isAllUnchecked) {
if (isAllChecked) {
item.indeterminate = false;
item.checked = true;
} else if (isAllUnchecked) {
item.indeterminate = false;
item.checked = false;
} else {
item.indeterminate = true;
}
}
}