UNPKG

@tomickigrzegorz/autocomplete

Version:

A lightweight, dependency-free autocomplete component in vanilla JavaScript with WAI-ARIA support.

612 lines (606 loc) 23.9 kB
/*! * @name autocomplete * @version 3.0.0 * @author Grzegorz Tomicki * @link https://github.com/tomickigrzegorz/autocomplete * @license MIT */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Autocomplete = factory()); })(this, (function () { 'use strict'; var isObject = function isObject(value) { return value && typeof value === "object" && value.constructor === Object; }; var isPromise = function isPromise(value) { return Boolean(value && typeof value.then === "function"); }; var setAttributes = function setAttributes(el, attributes) { Object.entries(attributes).forEach(function (_ref) { var key = _ref[0], value = _ref[1]; if (key === "addClass" || key === "removeClass") { classList(el, key === "addClass" ? "add" : "remove", value); } else { el.setAttribute(key, value); } }); }; var getFirstElement = function getFirstElement(element) { return (element.firstElementChild || element).textContent.trim(); }; var scrollResultsToTop = function scrollResultsToTop(resultList, resultWrap) { resultList.scrollTop = resultList.offsetTop - resultWrap.offsetHeight; }; var addAriaToAllLiElements = function addAriaToAllLiElements(itemsLi) { itemsLi.forEach(function (item, index) { setAttributes(item, { role: "option", tabindex: "-1", "aria-selected": "false", "aria-setsize": itemsLi.length, "aria-posinset": index + 1 }); }); }; var showBtnToClearData = function showBtnToClearData(clearButton, destroy) { if (!clearButton) return; classList(clearButton, "remove", "hidden"); onEvent(clearButton, "click", destroy); }; var classList = function classList(element, action, className) { return element.classList[action](className); }; var setAriaActivedescendant = function setAriaActivedescendant(root, type) { setAttributes(root, { "aria-activedescendant": type || "" }); }; var getClassGroupHeight = function getClassGroupHeight(outputUl, classGroup) { var allLiElements = document.querySelectorAll("#" + outputUl + " > li:not(." + classGroup + ")"); return Array.from(allLiElements).reduce(function (height, el) { return height + el.offsetHeight; }, 0); }; var followActiveElement = function followActiveElement(target, outputUl, classGroup, resultList) { var previousElement = resultList.previousSibling; var previousElementHeight = previousElement ? previousElement.offsetHeight : 0; if (target.getAttribute("aria-posinset") === "0") { resultList.scrollTop = target.offsetTop - getClassGroupHeight(outputUl, classGroup); } if (target.offsetTop - previousElementHeight < resultList.scrollTop) { resultList.scrollTop = target.offsetTop - previousElementHeight; } else { var offsetBottom = target.offsetTop + target.offsetHeight - previousElementHeight; var scrollBottom = resultList.scrollTop + resultList.offsetHeight; if (offsetBottom > scrollBottom) { resultList.scrollTop = offsetBottom - resultList.offsetHeight; } } }; var output = function output(root, resultList, outputUl, resultWrap, prefix) { setAttributes(resultList, { id: outputUl, role: "listbox" }); setAttributes(resultWrap, { addClass: prefix + "-results-wrapper" }); resultWrap.insertAdjacentElement("beforeend", resultList); root.parentNode.insertBefore(resultWrap, root.nextSibling); }; var createElement = function createElement(type) { return document.createElement(type); }; var select = function select(selector) { return document.querySelector(selector); }; var onEvent = function onEvent(element, action, callback) { element.addEventListener(action, callback); }; var offEvent = function offEvent(element, action, callback) { element.removeEventListener(action, callback); }; var ariaActiveDescendantDefault = function ariaActiveDescendantDefault(id) { return { "aria-owns": id, "aria-expanded": "false", "aria-autocomplete": "list", role: "combobox", removeClass: "auto-expanded" }; }; var KEY_CODES = Object.freeze({ ESC: 27, ENTER: 13, UP: 38, DOWN: 40, TAB: 9 }); var Autocomplete = function Autocomplete(_element, _ref) { var _this = this; var _ref$delay = _ref.delay, _delay = _ref$delay === void 0 ? 500 : _ref$delay, _ref$clearButton = _ref.clearButton, clearButton = _ref$clearButton === void 0 ? true : _ref$clearButton, _ref$clearButtonOnIni = _ref.clearButtonOnInitial, clearButtonOnInitial = _ref$clearButtonOnIni === void 0 ? false : _ref$clearButtonOnIni, _ref$howManyCharacter = _ref.howManyCharacters, howManyCharacters = _ref$howManyCharacter === void 0 ? 1 : _ref$howManyCharacter, _ref$selectFirst = _ref.selectFirst, selectFirst = _ref$selectFirst === void 0 ? false : _ref$selectFirst, _ref$insertToInput = _ref.insertToInput, insertToInput = _ref$insertToInput === void 0 ? false : _ref$insertToInput, _ref$showValuesOnClic = _ref.showValuesOnClick, showValuesOnClick = _ref$showValuesOnClic === void 0 ? false : _ref$showValuesOnClic, _ref$inline = _ref.inline, inline = _ref$inline === void 0 ? false : _ref$inline, _ref$cache = _ref.cache, cache = _ref$cache === void 0 ? false : _ref$cache, _ref$disableCloseOnSe = _ref.disableCloseOnSelect, disableCloseOnSelect = _ref$disableCloseOnSe === void 0 ? false : _ref$disableCloseOnSe, _ref$preventScrollUp = _ref.preventScrollUp, preventScrollUp = _ref$preventScrollUp === void 0 ? false : _ref$preventScrollUp, _ref$removeResultsWhe = _ref.removeResultsWhenInputIsEmpty, removeResultsWhenInputIsEmpty = _ref$removeResultsWhe === void 0 ? false : _ref$removeResultsWhe, _ref$regex = _ref.regex, _regex = _ref$regex === void 0 ? { expression: /[|\\{}()[\]^$+*?]/g, replacement: "\\$&" } : _ref$regex, classGroup = _ref.classGroup, classPreventClosing = _ref.classPreventClosing, classPrefix = _ref.classPrefix, ariaLabelClear = _ref.ariaLabelClear, onSearch = _ref.onSearch, _ref$onResults = _ref.onResults, onResults = _ref$onResults === void 0 ? function () {} : _ref$onResults, _ref$onSubmit = _ref.onSubmit, onSubmit = _ref$onSubmit === void 0 ? function () {} : _ref$onSubmit, _ref$onOpened = _ref.onOpened, onOpened = _ref$onOpened === void 0 ? function () {} : _ref$onOpened, _ref$onReset = _ref.onReset, onReset = _ref$onReset === void 0 ? function () {} : _ref$onReset, _ref$onRender = _ref.onRender, onRender = _ref$onRender === void 0 ? function () {} : _ref$onRender, _ref$onClose = _ref.onClose, onClose = _ref$onClose === void 0 ? function () {} : _ref$onClose, _ref$noResults = _ref.noResults, noResults = _ref$noResults === void 0 ? function () {} : _ref$noResults, _ref$onSelectedItem = _ref.onSelectedItem, onSelectedItem = _ref$onSelectedItem === void 0 ? function () {} : _ref$onSelectedItem; this._initial = function () { _this._clearbutton(); var ariaAcrivedescentDefault = ariaActiveDescendantDefault(_this._outputUl); setAttributes(_this._root, ariaAcrivedescentDefault); output(_this._root, _this._resultList, _this._outputUl, _this._resultWrap, _this._prefix); onEvent(_this._root, "input", _this._handleInput); _this._showValuesOnClick && onEvent(_this._root, "click", _this._handleInput); if (_this._inline) { var config = { root: _this._root, type: "load" }; onEvent(_this._root, "load", _this._handleInput(config)); } _this._onRender({ element: _this._root, results: _this._resultList }); if (_this._clearButtonOnInitial) { showBtnToClearData(_this._clearBtn, _this.destroy); } }; this._cacheAct = function (type, target) { if (!_this._cache) return; if (type === "update") { _this._root.setAttribute(_this._cacheData, target == null ? void 0 : target.value); } else if (type === "remove") { _this._root.removeAttribute(_this._cacheData); } else { _this._root.value = _this._root.getAttribute(_this._cacheData); } }; this._handleInput = function (_ref2) { var _target; var target = _ref2.target, type = _ref2.type; if (_this._root.getAttribute("aria-expanded") === "true" && type === "click") { return; } target = _this._inline ? _this._root : target; var regex = (_target = target) == null ? void 0 : _target.value.replace(_this._regex.expression, _this._regex.replacement); _this._cacheAct("update", target); var delay = _this._showValuesOnClick || _this._inline && type === "load" ? 0 : _this._delay; clearTimeout(_this._timeout); _this._timeout = setTimeout(function () { if (_this._removeResultsWhenInputIsEmpty) { var _target2; if (((_target2 = target) == null ? void 0 : _target2.value.length) === 0) { _this.destroy(); return; } } _this._searchItem(regex == null ? void 0 : regex.trim()); }, delay); }; this._reset = function () { var _this$_matches; classList(_this._resultWrap, "remove", _this._isActive); var ariaAcrivedescentDefault = ariaActiveDescendantDefault(_this._outputUl); var ariaAcrivedescent = _this._preventScrollUp ? ariaAcrivedescentDefault : Object.assign({}, ariaAcrivedescentDefault, { "aria-activedescendant": "" }); setAttributes(_this._root, ariaAcrivedescent); if (!_this._preventScrollUp) { _this._removeAria(select("." + _this._activeList)); _this._index = _this._selectFirst ? 0 : -1; } if (((_this$_matches = _this._matches) == null ? void 0 : _this$_matches.length) == 0 && !_this._toInput || _this._showValuesOnClick) { _this._resultList.textContent = ""; } _this._onClose(); }; this._searchItem = function (value) { _this._value = value; _this._onLoading(true); showBtnToClearData(_this._clearBtn, _this.destroy); if ((!value || (value == null ? void 0 : value.length) === 0) && _this._clearButton && !_this._clearButtonOnInitial) { classList(_this._clearBtn, "add", "hidden"); } if (_this._characters > (value == null ? void 0 : value.length) && !_this._showValuesOnClick && !_this._inline) { _this._onLoading(); return; } _this._onSearch({ currentValue: value, element: _this._root }).then(function (result) { var rootValueLength = _this._root.value.length; var resultLength = result.length; _this._matches = Array.isArray(result) ? result : JSON.parse(JSON.stringify(result)); _this._onLoading(); _this._error(); if (resultLength === 0 && rootValueLength === 0) { classList(_this._clearBtn, "add", "hidden"); } if (resultLength === 0 && rootValueLength) { classList(_this._root, "remove", "auto-expanded"); _this._reset(); _this._noResults({ element: _this._root, currentValue: value, template: _this._results }); _this._events(); } else if (resultLength > 0 || isObject(result)) { _this._index = _this._selectFirst ? 0 : -1; _this._results(); _this._events(); } }).catch(function () { _this._onLoading(); _this._reset(); }); }; this._onLoading = function (type) { return _this._root.parentNode.classList[type ? "add" : "remove"](_this._isLoading); }; this._error = function () { return classList(_this._root, "remove", _this._err); }; this._events = function () { onEvent(_this._root, "keydown", _this._handleKeys); onEvent(_this._root, "click", _this._handleShowItems); if (!_this._inline) { onEvent(document, "click", _this._handleDocClick); } ["mousemove", "click"].map(function (eventType) { onEvent(_this._resultList, eventType, _this._handleMouse); }); }; this._results = function (template) { setAttributes(_this._root, { "aria-expanded": "true", addClass: _this._prefix + "-expanded" }); _this._resultList.textContent = ""; var dataResults = _this._matches.length === 0 ? _this._onResults({ currentValue: _this._value, matches: 0, template: template }) : _this._onResults({ currentValue: _this._value, matches: _this._matches, classGroup: _this._classGroup }); _this._resultList.insertAdjacentHTML("afterbegin", dataResults); classList(_this._resultWrap, "add", _this._isActive); var checkIfClassGroupExist = _this._classGroup ? ":not(." + _this._classGroup + ")" : ""; _this._itemsLi = document.querySelectorAll("#" + _this._outputUl + " > li" + checkIfClassGroupExist); addAriaToAllLiElements(_this._itemsLi); _this._onOpened({ type: "results", element: _this._root, results: _this._resultList }); _this._selectFirstElement(); scrollResultsToTop(_this._resultList, _this._resultWrap); }; this._handleDocClick = function (_ref3) { var target = _ref3.target; var disableClose = null; if (target.closest("ul") && _this._disable || target.closest("." + _this._prevClosing)) { disableClose = true; } if (target.id !== _this._id && !disableClose) { _this._reset(); return; } }; this._selectFirstElement = function () { _this._removeAria(select("." + _this._activeList)); if (!_this._selectFirst) { return; } var firstElementChild = _this._resultList.firstElementChild; var classSelectFirst = _this._classGroup && _this._matches.length > 0 && _this._selectFirst ? firstElementChild.nextElementSibling : firstElementChild; _this._onSelected({ index: _this._index, element: _this._root, object: _this._matches[_this._index], currentValue: _this._root.value }); setAttributes(classSelectFirst, { id: _this._selectedOption + "-0", addClass: _this._activeList, "aria-selected": "true" }); setAriaActivedescendant(_this._root, _this._selectedOption + "-0"); }; this._handleShowItems = function () { if (_this._resultList.textContent.length > 0 && !classList(_this._resultWrap, "contains", _this._isActive)) { setAttributes(_this._root, { "aria-expanded": "true", addClass: _this._prefix + "-expanded" }); classList(_this._resultWrap, "add", _this._isActive); if (!_this._preventScrollUp) { scrollResultsToTop(_this._resultList, _this._resultWrap); _this._selectFirstElement(); } _this._onOpened({ type: "showItems", element: _this._root, results: _this._resultList }); if (!_this._cache) return; _this._cacheAct("update", _this._root); } }; this._handleMouse = function (event) { event.preventDefault(); var target = event.target, type = event.type; var targetClosest = target.closest("li"); var targetClosestRole = targetClosest == null ? void 0 : targetClosest.hasAttribute("role"); var activeClass = _this._activeList; var activeClassElement = select("." + activeClass); if (!targetClosest || !targetClosestRole || target.closest("." + _this._prevClosing)) { return; } if (type === "click") { _this._getTextFromLi(targetClosest); } if (type === "mousemove" && !classList(targetClosest, "contains", activeClass)) { _this._removeAria(activeClassElement); _this._setAria(targetClosest); _this._index = _this._indexLiSelected(targetClosest); _this._onSelected({ index: _this._index, element: _this._root, object: _this._matches[_this._index] }); if (_this._root.value.length > 0) { _this._clearButton && classList(_this._clearBtn, "remove", "hidden"); } } }; this._getTextFromLi = function (element) { if (!element || _this._matches.length === 0) { !_this._disable && _this._reset(); return; } _this._clearButton && classList(_this._clearBtn, "remove", "hidden"); _this._root.value = getFirstElement(element); _this._onSubmit({ index: _this._index, element: _this._root, object: _this._matches[_this._index], results: _this._resultList }); if (!_this._disable) { if (!_this._preventScrollUp) { _this._removeAria(element); } _this._reset(); } _this._cacheAct("remove"); }; this._indexLiSelected = function (target) { return ( Array.prototype.indexOf.call(_this._itemsLi, target) ); }; this._handleKeys = function (event) { var keyCode = event.keyCode; var resultList = classList(_this._resultWrap, "contains", _this._isActive); var matchesLength = _this._matches.length + 1; _this._selectedLi = select("." + _this._activeList); switch (keyCode) { case KEY_CODES.UP: case KEY_CODES.DOWN: event.preventDefault(); if (matchesLength <= 1 && _this._selectFirst || !resultList) { return; } if (keyCode === KEY_CODES.UP) { if (_this._index < 0) { _this._index = matchesLength - 1; } _this._index -= 1; } else { _this._index += 1; if (_this._index >= matchesLength) { _this._index = 0; } } _this._removeAria(_this._selectedLi); if (_this._index >= 0 && _this._index < matchesLength - 1) { var selectedElement = _this._itemsLi[_this._index]; if (_this._toInput && resultList) { _this._root.value = getFirstElement(selectedElement); _this._clearButton && classList(_this._clearBtn, "remove", "hidden"); } _this._onSelected({ index: _this._index, element: _this._root, object: _this._matches[_this._index] }); _this._setAria(selectedElement); } else { _this._cacheAct(); setAriaActivedescendant(_this._root); _this._onSelected({ index: null, element: _this._root, object: null }); } break; case KEY_CODES.ENTER: event.preventDefault(); _this._getTextFromLi(_this._selectedLi); break; case KEY_CODES.TAB: case KEY_CODES.ESC: event.stopPropagation(); if (!_this._inline) { _this._reset(); } break; } }; this._setAria = function (target) { var selectedOption = _this._selectedOption + "-" + _this._indexLiSelected(target); setAttributes(target, { id: selectedOption, "aria-selected": "true", addClass: _this._activeList }); setAriaActivedescendant(_this._root, selectedOption); followActiveElement(target, _this._outputUl, _this._classGroup, _this._resultList); }; this._removeAria = function (element) { if (!element) return; setAttributes(element, { id: "", removeClass: _this._activeList, "aria-selected": "false" }); }; this._clearbutton = function () { if (!_this._clearButton) return; setAttributes(_this._clearBtn, { type: "button", class: _this._prefix + "-clear hidden", title: _this._clearBtnAriLabel, "aria-label": _this._clearBtnAriLabel }); _this._root.insertAdjacentElement("afterend", _this._clearBtn); }; this.rerender = function (inputValue) { var text = inputValue != null && inputValue.trim() ? inputValue.trim() : _this._root.value; if (inputValue != null && inputValue.trim()) { _this._root.value = inputValue.trim(); _this._cacheAct("update", _this._root); } var regexText = text.replace(_this._regex.expression, _this._regex.replacement); _this._searchItem(regexText.trim()); }; this.destroy = function () { _this._clearButton && classList(_this._clearBtn, "add", "hidden"); _this._root.value = ""; _this._root.focus(); _this._resultList.textContent = ""; if (!_this._inline) _this._reset(); if (_this._inline) _this._onClose(); _this._error(); _this._onReset(_this._root); _this._onLoading(); offEvent(_this._root, "keydown", _this._handleKeys); offEvent(_this._root, "click", _this._handleShowItems); offEvent(document, "click", _this._handleDocClick); }; this._id = _element; this._root = document.getElementById(_element); this._onSearch = isPromise(onSearch) ? onSearch : function (_ref4) { var currentValue = _ref4.currentValue, element = _ref4.element; return Promise.resolve(onSearch({ currentValue: currentValue, element: element })); }; this._onResults = onResults; this._onRender = onRender; this._onSubmit = onSubmit; this._onSelected = onSelectedItem; this._onOpened = onOpened; this._onReset = onReset; this._noResults = noResults; this._onClose = onClose; this._delay = _delay; this._characters = howManyCharacters; this._clearButton = clearButton; this._clearButtonOnInitial = clearButtonOnInitial; this._selectFirst = selectFirst; this._toInput = insertToInput; this._showValuesOnClick = showValuesOnClick; this._inline = inline; this._classGroup = classGroup; this._prevClosing = classPreventClosing; this._clearBtnAriLabel = ariaLabelClear ? ariaLabelClear : "clear the search query"; this._prefix = classPrefix ? classPrefix + "-auto" : "auto"; this._disable = disableCloseOnSelect; this._preventScrollUp = preventScrollUp; this._removeResultsWhenInputIsEmpty = removeResultsWhenInputIsEmpty; this._cache = cache; this._timeout = null; this._outputUl = this._prefix + "-" + this._id + "-results"; this._cacheData = "data-cache-auto-" + this._id; this._isLoading = this._prefix + "-is-loading"; this._isActive = this._prefix + "-is-active"; this._activeList = this._prefix + "-selected"; this._selectedOption = this._prefix + "-selected-option"; this._err = this._prefix + "-error"; this._resultWrap = createElement("div"); this._resultList = createElement("ul"); this._clearBtn = createElement("button"); this._regex = Object.assign({}, { expression: /[|\\{}()[\]^$+*?]/g, replacement: "\\$&" }, _regex); if (!this._regex.replacement) { this._regex.replacement = this._defaultExpression.replacement; } if (!this._regex.expression) { this._regex.expression = this._defaultExpression.expression; } this._initial(); }; return Autocomplete; }));