@randstad-design/orbit-multitheme
Version:
multitheme Front-end code based on Randstad Human Forward components
522 lines (478 loc) • 17.1 kB
JavaScript
/**
* auto-suggest.js
*
*/
import ElementHelpers from '../helpers/element-helpers';
/**
* Declare constants
*/
const attributeBase = 'data-rs-auto-suggest';
export class AutoSuggest {
constructor(element) {
this.element = element;
this.minimumLength = 1;
this.container = this.element;
this.timeout;
this.numberOfListItems = 0;
this.selectedIndex;
this.selectedValue;
this.previousInput = '';
this.resultList;
// attributes
this.input = ElementHelpers.getElementByAttributeWithinElement(
this.element,
this.attributes.input
);
this.list = ElementHelpers.getElementByAttributeWithinElement(
this.element,
this.attributes.list
);
this.noResultMessage = this.input.getAttribute(
this.attributes.noResultMessage
);
this.searchActionCallback = this.input.getAttribute(
this.attributes.searchActionCallback
);
this.searchActionLabel = this.input.getAttribute(
this.attributes.searchActionLabel
);
this.searchActionIcon = this.input.getAttribute(
this.attributes.searchActionIcon
);
this.addActionCallback = this.input.getAttribute(
this.attributes.addActionCallback
);
this.addActionLabel = this.input.getAttribute(
this.attributes.addActionLabel
);
this.addActionIcon = this.input.getAttribute(
this.attributes.addActionIcon
);
// option to disable the input events RBD-9338
this.preventInputEvent = this.input.getAttribute(
this.attributes.preventInputEvent
);
// ignore default Orbit filter and display all returned data RBR-452
this.useExactValues = this.input.getAttribute(
this.attributes.useExactValues
);
this.separator = this.list.getAttribute(this.attributes.separator);
this.threshold =
Number(this.input.getAttribute(this.attributes.threshold)) || 300;
// nodes
this.submit = element.parentNode.querySelector('[type="submit"]');
// functions
this.dataSource = this.getDataSource();
this.disableNativeAutoComplete();
this.addEventHandlers();
}
/**
* Declare attributes
*/
get attributes() {
return {
addActionLabel: `${attributeBase}-add-action-label`,
addActionCallback: `${attributeBase}-add-action-callback`,
addActionIcon: `${attributeBase}-add-action-icon`,
autocomplete: 'autocomplete',
container: `${attributeBase}-container`,
index: `${attributeBase}-index`,
input: `${attributeBase}-input`,
list: `${attributeBase}-list`,
listValue: `${attributeBase}-list-value`,
noResultMessage: `${attributeBase}-no-result-message`,
preselect: `${attributeBase}-preselect`,
preventInputEvent: `${attributeBase}-prevent-input-event`,
searchActionLabel: `${attributeBase}-search-action-label`,
searchActionCallback: `${attributeBase}-search-action-callback`,
searchActionIcon: `${attributeBase}-search-action-icon`,
selected: `${attributeBase}-selected`,
separator: `${attributeBase}-separator`,
threshold: `${attributeBase}-threshold`,
useExactValues: `${attributeBase}-use-exact-values`,
values: `${attributeBase}-values`
};
}
/**
* Declare classes
*/
get classes() {
return {
autosuggestOpen: 'select-menu--open',
autosuggestItem: 'select-menu__item',
autosuggestAction: 'select-menu__item--action',
autosuggestNoResult: 'select-menu__item select-menu__item--no-result',
autosuggestPreselect: 'select-menu__item--preselect'
};
}
/**
* Declare key codes
*/
get keyCodes() {
return {
Enter: 'Enter',
Up: 'ArrowUp',
Down: 'ArrowDown',
Tab: 'Tab',
Escape: 'Escape',
PageUp: 'PageUp',
PageDown: 'PageDown'
};
}
/**
* Disable native auto complete
* Only Chrome does not support disabling native auto complete
*/
disableNativeAutoComplete() {
this.input.setAttribute(this.attributes.autocomplete, 'off');
}
/**
* Add event handlers
*/
addEventHandlers() {
const events = ['focus', 'keydown'];
if (this.preventInputEvent === null) {
events.push('input');
}
// on input and focus, create list if user input length > minimum length
events.forEach((eventType) => {
this.element.addEventListener(eventType, this.onInput.bind(this), {
capture: true
});
});
document.addEventListener('click', this.clickOutside.bind(this));
}
onInput(event) {
if (event.target.hasAttribute(this.attributes.input)) {
if (event.type === 'input' || event.type === 'focus') {
this.numberOfListItems = 0;
// only process if input is equal to or longer then minimum length
if (this.input.value.length >= this.minimumLength) {
// if previous input length is less then minimum length OR if previous input does not contain current input -> reset the data source
if (
this.previousInput.length < this.minimumLength ||
this.input.value
.toLowerCase()
.indexOf(this.previousInput.toLowerCase()) === -1
) {
this.dataSource = this.getDataSource();
}
this.createList();
} else {
// Close the list if it contains the open class
if (this.container.classList.contains(this.classes.autosuggestOpen)) {
this.closeList();
}
}
this.previousInput = this.input.value;
} else if (event.type === 'keydown') {
// if keydown is tab and item is preselected, select item and close list
if (
ElementHelpers.getKeyCodeOnKeyDownEvent(event) == this.keyCodes.Tab
) {
if (this.selectedIndex > -1) {
this.setSelectedValueOrDoAction(this.selectedIndex);
}
this.closeList();
}
// if keydown is escape, close list and do not select item
if (
ElementHelpers.getKeyCodeOnKeyDownEvent(event) == this.keyCodes.Escape
) {
this.closeList();
}
this.handleButtonKeys(event);
}
}
}
clickOutside(event) {
// if clicked outside input field or list, close the list and do not select item
if (
!this.list.contains(event.target) &&
!this.input.contains(event.target)
) {
if (this.container.classList.contains(this.classes.autosuggestOpen)) {
this.closeList();
}
}
}
/**
* Create list - get matched items from data source and put them in list with li elements
*/
createList() {
let list = '';
let listItemIndex = 0;
window.clearTimeout(this.timeout);
if(this.addActionLabel && this.addActionCallback) {
list = list + this.getActionListItemHTML(`${this.addActionLabel} ${this.input.value}`,this.addActionCallback,this.addActionIcon,listItemIndex);
listItemIndex++;
}
if(this.searchActionLabel && this.searchActionCallback) {
list = list + this.getActionListItemHTML(`${this.searchActionLabel} ${this.input.value}`,this.searchActionCallback,this.searchActionIcon,listItemIndex);
listItemIndex++;
}
// set result list equal to data source
this.resultList = JSON.parse(JSON.stringify(this.dataSource));
this.dataSource.forEach((listItem) => {
// check for each list item in the data source if it matches with input from user
if (
listItem.toLowerCase().indexOf(this.input.value.toLowerCase()) > -1 ||
this.useExactValues !== null
) {
// escape special symbols RBD-8705
const regexSafeValue = this.input.value.replace(
/[-[\]/{}()*+?.\\^$|]/g,
'\\$&'
);
// if item matches, add mark to matched part to create highlighted text
let regex = new RegExp(regexSafeValue, 'gi');
let response = listItem.replace(regex, function (value) {
return `<mark>${value}</mark>`;
});
// create li element for matched item
let li = `<li class="${this.classes.autosuggestItem}" ${this.attributes.listValue} ${this.attributes.index}="${listItemIndex}" >${response}</li>`;
// add li elements to list variable
list = list + li;
this.numberOfListItems++;
listItemIndex++;
} else {
// if list item does not match input from user -> remove list item from result list
let index = this.resultList.indexOf(listItem);
if (index > -1) {
this.resultList.splice(index, 1);
}
}
});
// set data source equal to result list
this.dataSource = JSON.parse(JSON.stringify(this.resultList));
// if number of list items > 0, bind result list
if (this.numberOfListItems > 0) {
this.timeout = setTimeout(
() => this.bindResultList(list),
this.threshold
);
} else {
let noResults = `<li class="${this.classes.autosuggestNoResult}">${
this.noResultMessage || 'no results'
} </li>`;
this.bindResultList(noResults);
}
}
getActionListItemHTML(caption,callbackString,icon,listItemIndex) {
return `<li class="${this.classes.autosuggestItem} ${this.classes.autosuggestAction}" data-action-callback="${callbackString}" ${this.attributes.listValue} ${this.attributes.index}="${listItemIndex}" >${icon ? icon : ''}${caption}</li>`;
}
/**
* bind result list - binds result to list, add eventlisteners for events 'click' and 'mouseenter' to list items
*
* @param {string} list - list with results
*/
bindResultList(list) {
this.list.innerHTML = list;
this.container.classList.add(this.classes.autosuggestOpen);
this.list
.querySelectorAll(`[${this.attributes.listValue}]`)
.forEach((element, index) => {
if (index === 0) {
// set first item as pre selected
element.classList.add(this.classes.autosuggestPreselect);
element.setAttribute(this.attributes.preselect, '');
this.selectedIndex = 0;
this.updatePreselectedItem(this.selectedIndex, null, true);
}
element.addEventListener('click', () => {
this.setSelectedValueOrDoAction(this.selectedIndex);
});
element.addEventListener('mouseenter', (mouseEnterEvent) => {
this.updatePreselectedItem(
element.getAttribute(this.attributes.index),
mouseEnterEvent,
false
);
});
});
}
/**
* Update preselected item, triggers setScrollbar
*
* @param {number} index - index of preselected element
* @param {Event} event - event triggered
* @param {boolean} executeScroll - boolean to trigger scroll
*/
updatePreselectedItem(index, event, executeScroll) {
// get former preselected item, remove class and attribute
let formerPreselectedItem = ElementHelpers.getElementByAttribute(
this.attributes.preselect
);
if (formerPreselectedItem) {
formerPreselectedItem.classList.remove(this.classes.autosuggestPreselect);
formerPreselectedItem.removeAttribute(this.attributes.preselect);
}
// get preselected item, add class and attribute
let preselectedItem = ElementHelpers.getElementByAttribute(
this.attributes.index,
index
);
if (preselectedItem) {
preselectedItem.classList.add(this.classes.autosuggestPreselect);
preselectedItem.setAttribute(this.attributes.preselect, '');
}
this.selectedIndex = index;
this.selectedValue = preselectedItem.innerText;
// only set scrollbar if event is keydown
if (
(typeof event !== 'undefined' &&
event !== null &&
event.type === 'keydown') ||
executeScroll === true
) {
this.setScrollbar(preselectedItem, event);
}
}
/**
* Set selected value from a normal suggestion, or perform action for search and add
*
* @param {number} index - index of selected item
*/
setSelectedValueOrDoAction(index) {
// get preselected item with index, set value to input field and close the list
let preSelectedItem = ElementHelpers.getElementByAttribute(
this.attributes.index,
index
);
if (preSelectedItem != null) {
const callbackString = preSelectedItem.getAttribute('data-action-callback');
if(callbackString) {
const callback = eval(callbackString);
callback && (typeof callback === 'function') && callback(this.input.value);
} else {
this.input.value = preSelectedItem.innerText;
}
this.closeList();
}
}
/**
* Handle button keys - handle key presses down, up, enter, page down and page up
*
* @param {Event} event - event triggered
*/
handleButtonKeys(event) {
if (event != null && this.selectedIndex > -1) {
switch (ElementHelpers.getKeyCodeOnKeyDownEvent(event)) {
case this.keyCodes.Down:
event.preventDefault();
// if preselected item is not last of the list, preselect next item
if (this.selectedIndex < this.numberOfListItems - 1) {
this.selectedIndex++;
this.updatePreselectedItem(this.selectedIndex, event, false);
}
break;
case this.keyCodes.Up:
event.preventDefault();
// if preselected item is not first of the list, preselect previous item
if (this.selectedIndex > 0) {
this.selectedIndex--;
this.updatePreselectedItem(this.selectedIndex, event, false);
}
break;
case this.keyCodes.PageDown:
event.preventDefault();
// if preselected item is not last of the list, preselect last item
if (this.selectedIndex < this.numberOfListItems - 1) {
this.updatePreselectedItem(
Number(this.numberOfListItems) - 1,
event,
false
);
}
break;
case this.keyCodes.PageUp:
event.preventDefault();
// if preselected item is not first of the list, preselect first item
if (this.selectedIndex > 0) {
this.updatePreselectedItem(0, event, false);
}
break;
case this.keyCodes.Enter:
event.preventDefault();
this.setSelectedValueOrDoAction(this.selectedIndex);
this.closeList();
this.input.blur();
// If there is a submit button after enter focus on the submit button
if (this.submit) {
this.submit.focus();
}
break;
}
}
}
/**
* Set scroll bar to correct position
*
* @param {HTMLElement} element - selected element from the list
* @param {Event} event - event triggered
*/
setScrollbar(element, event) {
let elementHeight = element.offsetHeight;
let elementOffsetTop = element.offsetTop;
let offsetTopFirstElement = this.list.children[0].offsetTop;
// set scrollbar on moving down
if (
event != null &&
ElementHelpers.getKeyCodeOnKeyDownEvent(event) == this.keyCodes.Down
) {
// if bottom of preselected element (elementHeight + elementOffsetTop) > bottom of visible list (this.list.offsetHeight + this.list.scrollTop),
// set scrollTop to difference between bottom of preselected element and the height of the list
if (
elementHeight + elementOffsetTop >
this.list.offsetHeight + this.list.scrollTop
) {
this.list.scrollTop =
elementHeight + elementOffsetTop - this.list.offsetHeight;
}
}
// set scrollbar on moving up
else {
// if top of preselected element - margin top) < scroll position OR bottom of preselected element (elementHeight + elementOffsetTop) > bottom of visible list,
// set scrollTop to top op preselected element - margin top
if (
elementOffsetTop - offsetTopFirstElement < this.list.scrollTop ||
elementOffsetTop + elementHeight >
this.list.scrollTop + this.list.offsetHeight
) {
this.list.scrollTop = elementOffsetTop - offsetTopFirstElement;
}
}
}
/**
* Close list - closes the list with suggestions and resets selected index and selected value
*/
closeList() {
// Add custom event to be able to add custom events on the autosuggest close dropdown
let event = new CustomEvent('autoSuggestCloseDropdown');
window.dispatchEvent(event);
this.container.classList.remove(this.classes.autosuggestOpen);
this.list.innerHTML = '';
this.selectedValue = '';
this.selectedIndex = -1;
}
/**
* Get data source
*/
getDataSource() {
let autoSuggestValues = this.list.getAttribute(this.attributes.values);
if (
typeof autoSuggestValues !== 'undefined' &&
autoSuggestValues !== null
) {
return autoSuggestValues.split(this.separator);
} else {
return [];
}
}
/**
* Get selector
*/
static getSelector() {
return `[${attributeBase}]`;
}
}