UNPKG

@randstad-design/orbit-multitheme

Version:

multitheme Front-end code based on Randstad Human Forward components

522 lines (478 loc) 17.1 kB
/** * 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}]`; } }