@cfpb/cfpb-design-system
Version:
CFPB's UI framework
236 lines (202 loc) • 6.53 kB
JavaScript
// Undefined return value for void methods.
let UNDEFINED;
// How many options may be checked.
export const MAX_SELECTIONS = 5;
/**
* Escapes a string.
* @param {string} str - The string to escape.
* @returns {string} The escaped string.
*/
function stringEscape(str) {
return str.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&');
}
/**
* Tests whether a string matches another.
* @param {string} x - The control string.
* @param {string} y - The comparison string.
* @returns {boolean} True if `x` and `y` match, false otherwise.
*/
function stringMatch(x, y) {
return RegExp(stringEscape(y.trim()), 'i').test(x);
}
/**
* @class
* MultiselectModel
* @param {HTMLOptionsCollection} options
* Set of options from a <select> element.
* @param {string} name - a unique name for this multiselect.
* @param {object} config - Customization of Multiselect behavior
*/
function MultiselectModel(options, name, config) {
const _options = options;
const _name = name;
const _max = config?.maxSelections || MAX_SELECTIONS;
let _optionsData = [];
let _selectedIndices = [];
let _filterIndices = [];
/* When the options list is filtered, we store a list of filtered indices
so that when the filter changes we can reset the last matched options. */
let _lastFilterIndices = [];
// Which option is in focus. -1 means the focus is on the search input.
let _index = -1;
/**
* @param {HTMLElement} item - An option HTML node.
* @returns {string} A (hopefully) unique ID.
* If it's not unique, we have a duplicate option value.
*/
function _getOptionId(item) {
return _name + '-' + item.value.trim().replace(/\s+/g, '-').toLowerCase();
}
/**
* @returns {boolean}
* True if the maximum number of options are checked, false otherwise.
*/
function isAtMaxSelections() {
return _selectedIndices.length >= _max;
}
/**
* Cleans up a list of options for saving to memory.
* @param {HTMLOptionsCollection} list - The options from a select element.
* @returns {Array} An array of option objects.
*/
function _formatOptions(list) {
let item;
const cleaned = [];
let isChecked = false;
for (let i = 0, len = list.length; i < len; i++) {
item = list[i];
isChecked = isAtMaxSelections() ? false : item.defaultSelected;
cleaned.push({
id: _getOptionId(item),
value: item.value,
text: item.text,
checked: isChecked,
});
// If an option is initially checked, we need to record it.
if (isChecked) {
_selectedIndices.push(i);
}
}
return cleaned;
}
/**
* @returns {MultiselectModel} An instance.
*/
function init() {
_optionsData = _formatOptions(_options);
return this;
}
/**
* Toggle checked value of an option.
* @param {number} index - The index position of the option in the list.
* @returns {boolean} A value of true is checked and false is unchecked.
*/
function toggleOption(index) {
_optionsData[index].checked = !_optionsData[index].checked;
if (_selectedIndices.length < _max && _optionsData[index].checked) {
_selectedIndices.push(index);
_selectedIndices.sort();
return true;
}
// We're over the max selections, reverse the check of the option.
_optionsData[index].checked = false;
_selectedIndices = _selectedIndices.filter(function (currIndex) {
return currIndex !== index;
});
return false;
}
/**
* Utility function for Array.reduce() used in searchIndices.
* @param {Array} aggregate - The reducer's accumulator.
* @param {object} item - Each item in the collection.
* @param {number} index - The index of item in the collection.
* @param {string} value - The value of item in the collection.
* @returns {Array} The reducer's accumulator.
*/
function _searchAggregator(aggregate, item, index, value) {
if (stringMatch(item.text, value)) {
aggregate.push(index);
}
return aggregate;
}
/**
* Search for a query string in the options text and return the indices of
* the matching positions in the options array.
* @param {string} query - A query string.
* @returns {Array} List of indices of the matching entries from the options.
*/
function filterIndices(query) {
// Convert query to a string if its not.
if (Object.prototype.toString.call(query) !== '[object String]') {
query = '';
}
_lastFilterIndices = _filterIndices;
if (_optionsData.length > 0) {
_filterIndices = _optionsData.reduce(function (acc, item, index) {
return _searchAggregator(acc, item, index, query);
}, []);
}
// Reset index position.
_index = -1;
return _filterIndices;
}
/**
* Retrieve an option object from the options list.
* @param {number} index - The index position in the options list.
* @returns {object} The option object with text, value, and checked value.
*/
function getOption(index) {
return _optionsData[index];
}
/**
* Set the index of the collection (represents the highlighted option).
* @param {number} value - The index to set.
*/
function setIndex(value) {
const filterCount = _filterIndices.length;
const count = filterCount === 0 ? _optionsData.length : filterCount;
if (value < 0) {
_index = -1;
} else if (value >= count) {
_index = count - 1;
} else {
_index = value;
}
}
/**
* @returns {number} The current index (highlighted option).
*/
function getIndex() {
return _index;
}
this.init = init;
// This is used to check an item in the collection.
this.toggleOption = toggleOption;
this.getSelectedIndices = function () {
return _selectedIndices;
};
this.isAtMaxSelections = isAtMaxSelections;
// This is used to search the items in the collection.
this.filterIndices = filterIndices;
this.clearFilter = function () {
_filterIndices = _lastFilterIndices = [];
return UNDEFINED;
};
this.getFilterIndices = function () {
return _filterIndices;
};
this.getLastFilterIndices = function () {
return _lastFilterIndices;
};
// These are used to highlight items in the collection.
this.getIndex = getIndex;
this.setIndex = setIndex;
this.resetIndex = function () {
_index = -1;
return _index;
};
// This is used to retrieve items from the collection.
this.getOption = getOption;
return this;
}
export default MultiselectModel;