diff --git a/README.md b/README.md index 6adcaf2..f633afe 100644 --- a/README.md +++ b/README.md @@ -47,13 +47,13 @@ autoComplete.js is a simple pure vanilla Javascript library that's progressively `JS` ```html - + ``` `CSS` ```html - + ``` #### Package Manager diff --git a/dist/js/autoComplete.js b/dist/js/autoComplete.js index 2969284..d6d88f9 100644 --- a/dist/js/autoComplete.js +++ b/dist/js/autoComplete.js @@ -79,6 +79,18 @@ return obj; } + function _toConsumableArray(arr) { + return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); + } + + function _arrayWithoutHoles(arr) { + if (Array.isArray(arr)) return _arrayLikeToArray(arr); + } + + function _iterableToArray(iter) { + if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); + } + function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); @@ -96,6 +108,10 @@ return arr2; } + function _nonIterableSpread() { + throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); + } + function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; @@ -154,11 +170,12 @@ } var prepareInputField = (function (config) { - config.inputField.setAttribute("role", "combobox"); - config.inputField.setAttribute("aria-haspopup", true); - config.inputField.setAttribute("aria-expanded", false); - config.inputField.setAttribute("aria-controls", config.resultsList.idName); - config.inputField.setAttribute("aria-autocomplete", "both"); + var input = config.inputField; + input.setAttribute("role", "combobox"); + input.setAttribute("aria-haspopup", true); + input.setAttribute("aria-expanded", false); + input.setAttribute("aria-controls", config.resultsList.idName); + input.setAttribute("aria-autocomplete", "both"); }); var eventEmitter = (function (target, detail, name) { @@ -172,12 +189,13 @@ var ariaActive$2 = "aria-activedescendant"; var ariaExpanded$1 = "aria-expanded"; var closeList = function closeList(config, target) { + var inputField = config.inputField; var list = document.getElementById(config.resultsList.idName); - if (list && target !== config.inputField) { + if (list && target !== inputField) { list.remove(); - config.inputField.removeAttribute(ariaActive$2); - config.inputField.setAttribute(ariaExpanded$1, false); - eventEmitter(config.inputField, null, "close"); + inputField.removeAttribute(ariaActive$2); + inputField.setAttribute(ariaExpanded$1, false); + eventEmitter(inputField, null, "close"); } }; @@ -207,6 +225,7 @@ var navigation = (function (config, dataFeedback) { config.inputField.removeEventListener(keyboardEvent, config.nav); var cursor = -1; + var classList = config.resultItem.selected.className.split(" "); var update = function update(event, list, state) { event.preventDefault(); if (list.length) { @@ -225,17 +244,19 @@ } }; var removeActive = function removeActive(list) { - for (var index = 0; index < list.length; index++) { - list[index].removeAttribute(ariaSelected); - if (config.resultItem.selected.className) list[index].classList.remove(config.resultItem.selected.className); - } + Array.from(list).forEach(function (item) { + var _item$classList; + item.removeAttribute(ariaSelected); + if (classList) (_item$classList = item.classList).remove.apply(_item$classList, _toConsumableArray(classList)); + }); }; var addActive = function addActive(list) { + var _list$cursor$classLis; removeActive(list); if (cursor >= list.length) cursor = 0; if (cursor < 0) cursor = list.length - 1; list[cursor].setAttribute(ariaSelected, true); - if (config.resultItem.selected.className) list[cursor].classList.add(config.resultItem.selected.className); + if (classList) (_list$cursor$classLis = list[cursor].classList).add.apply(_list$cursor$classLis, _toConsumableArray(classList)); }; config.nav = function (event) { var list = document.getElementById(config.resultsList.idName); @@ -271,18 +292,19 @@ var ariaExpanded = "aria-expanded"; var ariaActive = "aria-activedescendant"; var resultsList = (function (config, data) { - data.input; - var query = data.query, + var query = data.query, matches = data.matches, results = data.results; + var input = config.inputField; + var resultsList = config.resultsList; var list = document.getElementById(config.resultsList.idName); if (list) { list.innerHTML = ""; - config.inputField.removeAttribute(ariaActive); + input.removeAttribute(ariaActive); } else { list = createList(config); - config.inputField.setAttribute(ariaExpanded, true); - eventEmitter(config.inputField, data, "open"); + input.setAttribute(ariaExpanded, true); + eventEmitter(input, data, "open"); } if (matches.length) { results.forEach(function (item, index) { @@ -300,52 +322,53 @@ list.appendChild(resultItem); }); } else { - if (!config.resultsList.noResults) { + if (!resultsList.noResults) { closeList(config); - config.inputField.setAttribute(ariaExpanded, false); + input.setAttribute(ariaExpanded, false); } else { - config.resultsList.noResults(list, query); + resultsList.noResults(list, query); } } - if (config.resultsList.container) config.resultsList.container(list, data); - config.resultsList.navigation ? config.resultsList.navigation(list) : navigation(config, data); + if (resultsList.container) resultsList.container(list, data); + resultsList.navigation ? resultsList.navigation(list) : navigation(config, data); document.addEventListener(clickEvent, function (event) { return closeList(config, event.target); }); }); + var formatRawInputValue = function formatRawInputValue(value, config) { + value = value.toLowerCase(); + return config.diacritics ? value.normalize("NFD").replace(/[\u0300-\u036f]/g, "").normalize("NFC") : value; + }; var getInputValue = function getInputValue(inputField) { - return inputField instanceof HTMLInputElement || inputField instanceof HTMLTextAreaElement ? inputField.value.toLowerCase() : inputField.innerHTML.toLowerCase(); + return inputField instanceof HTMLInputElement || inputField instanceof HTMLTextAreaElement ? inputField.value : inputField.innerHTML; }; - var prepareQueryValue = function prepareQueryValue(inputValue, config) { - return config.query && config.query.manipulate ? config.query.manipulate(inputValue) : config.diacritics ? inputValue.normalize("NFD").replace(/[\u0300-\u036f]/g, "").normalize("NFC") : inputValue; + var prepareQuery = function prepareQuery(input, config) { + return config.query && config.query.manipulate ? config.query.manipulate(input) : config.diacritics ? formatRawInputValue(input, config) : formatRawInputValue(input, config); }; - var item = function item(className, value) { + var highlightChar = function highlightChar(className, value) { return "").concat(value, ""); }; var searchEngine = (function (query, record, config) { - var recordLowerCase = config.diacritics ? record.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").normalize("NFC") : record.toLowerCase(); + var formattedRecord = formatRawInputValue(record, config); if (config.searchEngine === "loose") { query = query.replace(/ /g, ""); - var match = []; - var searchPosition = 0; - for (var number = 0; number < recordLowerCase.length; number++) { - var recordChar = record[number]; - if (searchPosition < query.length && recordLowerCase[number] === query[searchPosition]) { - recordChar = config.resultItem.highlight.render ? item(config.resultItem.highlight.className, recordChar) : recordChar; - searchPosition++; + var queryLength = query.length; + var cursor = 0; + var match = Array.from(record).map(function (character, index) { + if (cursor < queryLength && formattedRecord[index] === query[cursor]) { + character = config.resultItem.highlight.render ? highlightChar(config.resultItem.highlight.className, character) : character; + cursor++; } - match.push(recordChar); - } - if (searchPosition === query.length) { - return match.join(""); - } + return character; + }).join(""); + if (cursor === queryLength) return match; } else { - if (recordLowerCase.includes(query)) { + if (formattedRecord.includes(query)) { var pattern = new RegExp(query.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"), "i"); query = pattern.exec(record); - var _match = config.resultItem.highlight.render ? record.replace(query, item(config.resultItem.highlight.className, query)) : record; + var _match = config.resultItem.highlight.render ? record.replace(query, highlightChar(config.resultItem.highlight.className, query)) : record; return _match; } } @@ -358,23 +381,15 @@ data.store.forEach(function (record, index) { var search = function search(key) { var recordValue = (key ? record[key] : record).toString(); - if (recordValue) { - var match = typeof customSearchEngine === "function" ? customSearchEngine(query, recordValue) : searchEngine(query, recordValue, config); - if (match && key) { - results.push({ - key: key, - index: index, - match: match, - value: record - }); - } else if (match && !key) { - results.push({ - index: index, - match: match, - value: record - }); - } - } + var match = typeof customSearchEngine === "function" ? customSearchEngine(query, recordValue) : searchEngine(query, recordValue, config); + if (!match) return; + var result = { + index: index, + match: match, + value: record + }; + if (key) result.key = key; + results.push(result); }; if (data.key) { var _iterator = _createForOfIteratorHelper(data.key), @@ -396,8 +411,8 @@ return results; }); - var checkTriggerCondition = (function (config, event, queryValue) { - return config.trigger.condition ? config.trigger.condition(event, queryValue) : queryValue.length >= config.threshold && queryValue.replace(/ /g, "").length; + var checkTriggerCondition = (function (config, event, query) { + return config.trigger.condition ? config.trigger.condition(event, query) : query.length >= config.threshold && query.replace(/ /g, "").length; }); var debouncer = (function (callback, delay) { @@ -542,7 +557,7 @@ value: function dataStore() { var _this = this; return new Promise(function ($return, $error) { - if (_this.data.cache && _this.data.store) return $return(null); + if (_this.data.cache && _this.data.store) return $return(); return new Promise(function ($return, $error) { if (typeof _this.data.src === "function") { return _this.data.src().then($return, $error); @@ -566,7 +581,7 @@ return new Promise(function ($return, $error) { var input, query, triggerCondition; input = getInputValue(_this2.inputField); - query = prepareQueryValue(input, _this2); + query = prepareQuery(input, _this2); triggerCondition = checkTriggerCondition(_this2, event, query); if (triggerCondition) { return _this2.dataStore().then(function ($await_6) { @@ -604,10 +619,6 @@ key: "preInit", value: function preInit() { var _this4 = this; - var config = { - childList: true, - subtree: true - }; var callback = function callback(mutations, observer) { mutations.forEach(function (mutation) { if (_this4.inputField) { @@ -617,7 +628,10 @@ }); }; var observer = new MutationObserver(callback); - observer.observe(document, config); + observer.observe(document, { + childList: true, + subtree: true + }); } }, { key: "unInit", diff --git a/dist/js/autoComplete.js.gz b/dist/js/autoComplete.js.gz index 040be51..005a538 100644 Binary files a/dist/js/autoComplete.js.gz and b/dist/js/autoComplete.js.gz differ diff --git a/dist/js/autoComplete.min.js b/dist/js/autoComplete.min.js index e11caf9..a9d74df 100644 --- a/dist/js/autoComplete.min.js +++ b/dist/js/autoComplete.min.js @@ -1 +1 @@ -var e,t;e=this,t=function(){"use strict";function e(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);t&&(i=i.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,i)}return n}function t(t){for(var n=1;ne.length)&&(t=e.length);for(var n=0,i=new Array(t);n=e.length?{done:!0}:{done:!1,value:e[i++]}},e:function(e){throw e},f:s}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var a,o=!0,u=!1;return{s:function(){n=n.call(e)},n:function(){var e=n.next();return o=e.done,e},e:function(e){u=!0,a=e},f:function(){try{o||null==n.return||n.return()}finally{if(u)throw a}}}}var a=function(e,t,n){e.dispatchEvent(new CustomEvent(n,{bubbles:!0,detail:t,cancelable:!0}))},o=function(e,t){var n=document.getElementById(e.resultsList.idName);n&&t!==e.inputField&&(n.remove(),e.inputField.removeAttribute("aria-activedescendant"),e.inputField.setAttribute("aria-expanded",!1),a(e.inputField,null,"close"))},u="keydown",l="aria-selected",c=function(e,n){e.inputField.removeEventListener(u,e.nav);var i=-1,r=function(r,o,u){r.preventDefault(),o.length&&(u?i++:i--,s(o),e.inputField.setAttribute("aria-activedescendant",o[i].id),a(r.srcElement,t(t({event:r},n),{},{selection:n.results[i]}),"navigate"))},s=function(t){!function(t){for(var n=0;n=t.length&&(i=0),i<0&&(i=t.length-1),t[i].setAttribute(l,!0),e.resultItem.selected.className&&t[i].classList.add(e.resultItem.selected.className)};e.nav=function(t){var n=document.getElementById(e.resultsList.idName);if(n)switch(n=n.getElementsByTagName(e.resultItem.element),t.keyCode){case 40:r(t,n,1);break;case 38:r(t,n);break;case 27:e.inputField.value="",o(e);break;case 13:t.preventDefault(),i>=0&&n[i].click();break;case 9:o(e)}},e.inputField.addEventListener(u,e.nav)},d="click",h="aria-expanded",f=function(e,n){n.input;var i=n.query,r=n.matches,s=n.results,u=document.getElementById(e.resultsList.idName);u?(u.innerHTML="",e.inputField.removeAttribute("aria-activedescendant")):(u=function(e){var t=document.createElement(e.resultsList.element);return t.setAttribute("id",e.resultsList.idName),t.setAttribute("class",e.resultsList.className),t.setAttribute("role","listbox"),("string"==typeof e.resultsList.destination?document.querySelector(e.resultsList.destination):e.resultsList.destination()).insertAdjacentElement(e.resultsList.position,t),t}(e),e.inputField.setAttribute(h,!0),a(e.inputField,n,"open")),r.length?s.forEach((function(i,r){var s=function(e,t,n){var i=document.createElement(n.resultItem.element);return i.setAttribute("id","".concat(n.resultItem.idName,"_").concat(t)),i.setAttribute("class",n.resultItem.className),i.setAttribute("role","option"),i.innerHTML=e.match,n.resultItem.content&&n.resultItem.content(e,i),i}(i,r,e);s.addEventListener(d,(function(s){var a=t(t({event:s},n),{},{selection:t(t({},i),{},{index:r})});e.onSelection&&e.onSelection(a)})),u.appendChild(s)})):e.resultsList.noResults?e.resultsList.noResults(u,i):(o(e),e.inputField.setAttribute(h,!1)),e.resultsList.container&&e.resultsList.container(u,n),e.resultsList.navigation?e.resultsList.navigation(u):c(e,n),document.addEventListener(d,(function(t){return o(e,t.target)}))},m=function(e,t){return'').concat(t,"")},p=function(e,t){var n=e.data,i=e.searchEngine,r=[];return n.store.forEach((function(a,o){var u=function(n){var s=(n?a[n]:a).toString();if(s){var u="function"==typeof i?i(t,s):function(e,t,n){var i=n.diacritics?t.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g,"").normalize("NFC"):t.toLowerCase();if("loose"===n.searchEngine){e=e.replace(/ /g,"");for(var r=[],s=0,a=0;a=e.threshold&&n.replace(/ /g,"").length}(t,e,s)?t.dataStore().then((function(e){try{return t.start(r,s),c.call(t)}catch(e){return i(e)}}),i):(o(t),c.call(t));function c(){return n()}}))}},{key:"init",value:function(){var e,t,n,i,r=this;(e=this).inputField.setAttribute("role","combobox"),e.inputField.setAttribute("aria-haspopup",!0),e.inputField.setAttribute("aria-expanded",!1),e.inputField.setAttribute("aria-controls",e.resultsList.idName),e.inputField.setAttribute("aria-autocomplete","both"),this.placeHolder&&this.inputField.setAttribute("placeholder",this.placeHolder),this.hook=(t=function(e){r.compose(e)},n=this.debounce,function(){var e=this,r=arguments;clearTimeout(i),i=setTimeout((function(){return t.apply(e,r)}),n)}),this.trigger.event.forEach((function(e){r.inputField.addEventListener(e,r.hook)})),a(this.inputField,null,"init")}},{key:"preInit",value:function(){var e=this;new MutationObserver((function(t,n){t.forEach((function(t){e.inputField&&(n.disconnect(),e.init())}))})).observe(document,{childList:!0,subtree:!0})}},{key:"unInit",value:function(){var e=this;this.trigger.event.forEach((function(t){e.inputField.removeEventListener(t,e.hook)})),a(this.inputField,null,"unInit")}}])&&n(t.prototype,i),r&&n(t,r),e}()},"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).autoComplete=t(); +var e,t;e=this,t=function(){"use strict";function e(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function t(t){for(var n=1;ne.length)&&(t=e.length);for(var n=0,r=new Array(t);n=e.length&&(r=0),r<0&&(r=e.length-1),e[r].setAttribute(l,!0),o&&(t=e[r].classList).add.apply(t,i(o))};e.nav=function(t){var n=document.getElementById(e.resultsList.idName);if(n)switch(n=n.getElementsByTagName(e.resultItem.element),t.keyCode){case 40:a(t,n,1);break;case 38:a(t,n);break;case 27:e.inputField.value="",u(e);break;case 13:t.preventDefault(),r>=0&&n[r].click();break;case 9:u(e)}},e.inputField.addEventListener(c,e.nav)},f="click",h="aria-expanded",m=function(e,n){var r=n.query,i=n.matches,o=n.results,a=e.inputField,c=e.resultsList,l=document.getElementById(e.resultsList.idName);l?(l.innerHTML="",a.removeAttribute("aria-activedescendant")):(l=function(e){var t=document.createElement(e.resultsList.element);return t.setAttribute("id",e.resultsList.idName),t.setAttribute("class",e.resultsList.className),t.setAttribute("role","listbox"),("string"==typeof e.resultsList.destination?document.querySelector(e.resultsList.destination):e.resultsList.destination()).insertAdjacentElement(e.resultsList.position,t),t}(e),a.setAttribute(h,!0),s(a,n,"open")),i.length?o.forEach((function(r,i){var o=function(e,t,n){var r=document.createElement(n.resultItem.element);return r.setAttribute("id","".concat(n.resultItem.idName,"_").concat(t)),r.setAttribute("class",n.resultItem.className),r.setAttribute("role","option"),r.innerHTML=e.match,n.resultItem.content&&n.resultItem.content(e,r),r}(r,i,e);o.addEventListener(f,(function(o){var a=t(t({event:o},n),{},{selection:t(t({},r),{},{index:i})});e.onSelection&&e.onSelection(a)})),l.appendChild(o)})):c.noResults?c.noResults(l,r):(u(e),a.setAttribute(h,!1)),c.container&&c.container(l,n),c.navigation?c.navigation(l):d(e,n),document.addEventListener(f,(function(t){return u(e,t.target)}))},v=function(e,t){return e=e.toLowerCase(),t.diacritics?e.normalize("NFD").replace(/[\u0300-\u036f]/g,"").normalize("NFC"):e},p=function(e,t){return'').concat(t,"")},b=function(e,t){var n=e.data,r=e.searchEngine,i=[];return n.store.forEach((function(a,s){var u=function(n){var o=(n?a[n]:a).toString(),u="function"==typeof r?r(t,o):function(e,t,n){var r=v(t,n);if("loose"===n.searchEngine){var i=(e=e.replace(/ /g,"")).length,o=0,a=Array.from(t).map((function(t,a){return o=e.length?{done:!0}:{done:!1,value:e[r++]}},e:function(e){throw e},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var a,s=!0,u=!1;return{s:function(){n=n.call(e)},n:function(){var e=n.next();return s=e.done,e},e:function(e){u=!0,a=e},f:function(){try{s||null==n.return||n.return()}finally{if(u)throw a}}}}(n.key);try{for(l.s();!(c=l.n()).done;)u(c.value)}catch(e){l.e(e)}finally{l.f()}}else u()})),i};return function(){function e(t){!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e);var n=t.selector,r=void 0===n?"#autoComplete":n,i=t.placeHolder,o=t.observer,a=t.data,s=a.src,u=a.key,c=a.cache,l=a.store,d=a.results,f=t.query,h=t.trigger,m=(h=void 0===h?{}:h).event,v=void 0===m?["input"]:m,p=h.condition,b=t.threshold,y=void 0===b?1:b,g=t.debounce,E=void 0===g?0:g,A=t.diacritics,L=t.searchEngine,k=t.feedback,w=t.resultsList,I=(w=void 0===w?{}:w).render,N=void 0===I||I,O=w.container,j=w.destination,F=void 0===j?r:j,S=w.position,x=void 0===S?"afterend":S,T=w.element,C=void 0===T?"ul":T,P=w.idName,q=void 0===P?"autoComplete_list":P,H=w.className,R=w.maxResults,D=void 0===R?5:R,M=w.navigation,_=w.noResults,B=t.resultItem,$=(B=void 0===B?{}:B).content,z=B.element,U=void 0===z?"li":z,G=B.idName,J=B.className,K=void 0===J?"autoComplete_result":J,Q=B.highlight,V=(Q=void 0===Q?{}:Q).render,W=Q.className,X=void 0===W?"autoComplete_highlighted":W,Y=B.selected,Z=(Y=void 0===Y?{}:Y).className,ee=void 0===Z?"autoComplete_selected":Z,te=t.onSelection;this.selector=r,this.observer=o,this.placeHolder=i,this.data={src:s,key:u,cache:c,store:l,results:d},this.query=f,this.trigger={event:v,condition:p},this.threshold=y,this.debounce=E,this.diacritics=A,this.searchEngine=L,this.feedback=k,this.resultsList={render:N,container:O,destination:F,position:x,element:C,idName:q,className:H,maxResults:D,navigation:M,noResults:_},this.resultItem={content:$,element:U,idName:G,className:K,highlight:{render:V,className:X},selected:{className:ee}},this.onSelection=te,this.inputField="string"==typeof this.selector?document.querySelector(this.selector):this.selector(),this.observer?this.preInit():this.init()}var t,r,i;return t=e,(r=[{key:"start",value:function(e,t){var n=this.data.results?this.data.results(b(this,t)):b(this,t),r={input:e,query:t,matches:n,results:n.slice(0,this.resultsList.maxResults)};if(s(this.inputField,r,"results"),!this.resultsList.render)return this.feedback(r);m(this,r)}},{key:"dataStore",value:function(){var e=this;return new Promise((function(t,n){return e.data.cache&&e.data.store?t():new Promise((function(t,n){return"function"==typeof e.data.src?e.data.src().then(t,n):t(e.data.src)})).then((function(r){try{return e.data.store=r,s(e.inputField,e.data.store,"fetch"),t()}catch(e){return n(e)}}),n)}))}},{key:"compose",value:function(e){var t=this;return new Promise((function(n,r){var i,o,a;return a=t.inputField,i=a instanceof HTMLInputElement||a instanceof HTMLTextAreaElement?a.value:a.innerHTML,o=function(e,t){return t.query&&t.query.manipulate?t.query.manipulate(e):(t.diacritics,v(e,t))}(i,t),function(e,t,n){return e.trigger.condition?e.trigger.condition(t,n):n.length>=e.threshold&&n.replace(/ /g,"").length}(t,e,o)?t.dataStore().then((function(e){try{return t.start(i,o),s.call(t)}catch(e){return r(e)}}),r):(u(t),s.call(t));function s(){return n()}}))}},{key:"init",value:function(){var e,t,n,r,i,o=this;(t=(e=this).inputField).setAttribute("role","combobox"),t.setAttribute("aria-haspopup",!0),t.setAttribute("aria-expanded",!1),t.setAttribute("aria-controls",e.resultsList.idName),t.setAttribute("aria-autocomplete","both"),this.placeHolder&&this.inputField.setAttribute("placeholder",this.placeHolder),this.hook=(n=function(e){o.compose(e)},r=this.debounce,function(){var e=this,t=arguments;clearTimeout(i),i=setTimeout((function(){return n.apply(e,t)}),r)}),this.trigger.event.forEach((function(e){o.inputField.addEventListener(e,o.hook)})),s(this.inputField,null,"init")}},{key:"preInit",value:function(){var e=this;new MutationObserver((function(t,n){t.forEach((function(t){e.inputField&&(n.disconnect(),e.init())}))})).observe(document,{childList:!0,subtree:!0})}},{key:"unInit",value:function(){var e=this;this.trigger.event.forEach((function(t){e.inputField.removeEventListener(t,e.hook)})),s(this.inputField,null,"unInit")}}])&&n(t.prototype,r),i&&n(t,i),e}()},"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).autoComplete=t(); diff --git a/dist/js/autoComplete.min.js.gz b/dist/js/autoComplete.min.js.gz index 323cddf..b74fb97 100644 Binary files a/dist/js/autoComplete.min.js.gz and b/dist/js/autoComplete.min.js.gz differ diff --git a/docs/demo/index.html b/docs/demo/index.html index 5fe8727..96083ad 100644 --- a/docs/demo/index.html +++ b/docs/demo/index.html @@ -151,7 +151,7 @@

mode

- + diff --git a/docs/index.html b/docs/index.html index c44b61a..521f8d6 100644 --- a/docs/index.html +++ b/docs/index.html @@ -192,7 +192,7 @@ mustache: { data: ["../package.json", { minVersion: "9.1", - version: "9.1.0" + version: "9.1.1" }] } } @@ -206,7 +206,7 @@ - + \ No newline at end of file diff --git a/docs/release-notes.md b/docs/release-notes.md index cb6c784..6c02eed 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -34,7 +34,19 @@ For more information on semantic versioning, please visit . *** -### v9.1.0 ✨ +### v9.1.1 ✨ + >
Important Note: + > + >> Starting next major release: + > 1. "noResults" API will accept "Boolean" instead of function to be replaced with "resultsList.container" + > 2. "fetch" eventEmitter will be renamed to "response" + > 3. All "className" APIs will be renamed to "classList" + + - 🔧 Fixed: Data feedback `inputField` value was in lowerCase instead of raw + - 🔧 Fixed: `resultItem.className` did not accept except one class instead of multiple + - 🔝 Updated: Code with deep refactoring & cleanup (Thanks 👍 @Pirulax) #210 + +### v9.1.0 >
Important Note: > >> Starting next major release: diff --git a/package.json b/package.json index 2e8dc99..485a516 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tarekraafat/autocomplete.js", - "version": "9.1.0", + "version": "9.1.1", "description": "Simple autocomplete pure vanilla Javascript library.", "keywords": [ "simple", diff --git a/src/autoComplete.js b/src/autoComplete.js index 474d05a..cb6389a 100644 --- a/src/autoComplete.js +++ b/src/autoComplete.js @@ -1,7 +1,7 @@ import prepareInputField from "./components/Input"; import { closeList } from "./controllers/listController"; import resultsList from "./controllers/resultsController"; -import { getInputValue, prepareQueryValue } from "./controllers/inputController"; +import { getInputValue, prepareQuery } from "./controllers/inputController"; import findMatches from "./controllers/dataController"; import checkTriggerCondition from "./controllers/triggerController"; import debouncer from "./utils/debouncer"; @@ -84,46 +84,41 @@ export default class autoComplete { }; this.onSelection = onSelection; - // Assign the inputField selector + // Assign the "inputField" selector this.inputField = typeof this.selector === "string" ? document.querySelector(this.selector) : this.selector(); - // Invoke preInit if enabled + // Invoke preInit function if enabled // or initiate autoComplete instance directly - if (this.observer) - this.preInit() - else - this.init(); + this.observer ? this.preInit() : this.init(); } /** * Run autoComplete processes * - * @param {String} - Raw inputField value as a string + * @param {String} - Raw "inputField" value as a string * @param {Object} config - autoComplete configurations * * @return {void} */ start(input, query) { - const results = this.data.results - ? this.data.results(findMatches(this, query)) - : findMatches(this, query); + const results = this.data.results ? this.data.results(findMatches(this, query)) : findMatches(this, query); // Prepare data feedback object const dataFeedback = { input, query, matches: results, results: results.slice(0, this.resultsList.maxResults) }; /** - * @emit {results} Emit Event on search response with results + * @emit {results} event on search response with results **/ eventEmitter(this.inputField, dataFeedback, "results"); - // If resultsList set not to render + // If "resultsList" NOT active if (!this.resultsList.render) return this.feedback(dataFeedback); - // Generate & Render results list + // Generate & Render "resultsList" resultsList(this, dataFeedback); } async dataStore() { - if (this.data.cache && this.data.store) return null; + if (this.data.cache && this.data.store) return; this.data.store = typeof this.data.src === "function" ? await this.data.src() : this.data.src; /** - * @emit {fetch} Emit Event on data request + * @emit {fetch} event on data request **/ eventEmitter(this.inputField, this.data.store, "fetch"); } @@ -131,20 +126,20 @@ export default class autoComplete { /** * Run autoComplete composer * - * @param {Object} event - The trigger event Object + * @param {Object} event - Trigger event Object * * @return {void} */ async compose(event) { - // Prepare raw inputField value + // Prepare raw "inputField" value const input = getInputValue(this.inputField); - // Prepare manipulated query inputField value - const query = prepareQueryValue(input, this); - // Get trigger condition value + // Prepare manipulated query value + const query = prepareQuery(input, this); + // Get trigger decision const triggerCondition = checkTriggerCondition(this, event, query); // Validate trigger condition if (triggerCondition) { - // Prepare the data + // Prepare data await this.dataStore(); // Start autoComplete engine this.start(input, query); @@ -156,7 +151,7 @@ export default class autoComplete { // Initialization stage init() { - // Set inputField attributes + // Set "inputField" attributes prepareInputField(this); // Set placeholder attribute value if (this.placeHolder) this.inputField.setAttribute("placeholder", this.placeHolder); @@ -166,13 +161,13 @@ export default class autoComplete { this.compose(event); }, this.debounce); /** - * @listen {input} Listen to all set `inputField` events + * @listen {input} events of "inputField" **/ this.trigger.event.forEach((eventType) => { this.inputField.addEventListener(eventType, this.hook); }); /** - * @emit {init} Emit Event on Initialization + * @emit {init} event on Initialization **/ eventEmitter(this.inputField, null, "init"); } @@ -181,9 +176,8 @@ export default class autoComplete { preInit() { // Callback function to execute when mutations are observed const callback = (mutations, observer) => { - // Go though each mutation mutations.forEach((mutation) => { - // Check if inputField was added to the DOM + // Check if "inputField" added to the DOM if (this.inputField) { // If yes disconnect the observer observer.disconnect(); @@ -192,21 +186,21 @@ export default class autoComplete { } }); }; - // Create an observer instance linked to the callback function + // Create mutation observer instance const observer = new MutationObserver(callback); - // Start observing the entire DOM until inputField is present + // Start observing the entire DOM until "inputField" is present // The entire document will be observed for all changes observer.observe(document, { childList: true, subtree: true }); } // Un-initialize autoComplete unInit() { - // Remove all autoComplete inputField eventListeners + // Remove all autoComplete "inputField" eventListeners this.trigger.event.forEach((eventType) => { this.inputField.removeEventListener(eventType, this.hook); }); /** - * @emit {unInit} Emit Event on inputField eventListener detachment + * @emit {unInit} event on "inputField" detachment **/ eventEmitter(this.inputField, null, "unInit"); } diff --git a/src/components/Input.js b/src/components/Input.js index f61eff3..a496279 100644 --- a/src/components/Input.js +++ b/src/components/Input.js @@ -2,16 +2,15 @@ * InputField element * * @param {Object} config - autoComplete configurations - * + * * @return {void} */ export default (config) => { - // General attributes + const input = config.inputField; // ARIA attributes - let inf = config.inputField; - inf.setAttribute("role", "combobox"); - inf.setAttribute("aria-haspopup", true); - inf.setAttribute("aria-expanded", false); - inf.setAttribute("aria-controls", config.resultsList.idName); - inf.setAttribute("aria-autocomplete", "both"); + input.setAttribute("role", "combobox"); + input.setAttribute("aria-haspopup", true); + input.setAttribute("aria-expanded", false); + input.setAttribute("aria-controls", config.resultsList.idName); + input.setAttribute("aria-autocomplete", "both"); }; diff --git a/src/components/Item.js b/src/components/Item.js index a01c719..30fbd5c 100644 --- a/src/components/Item.js +++ b/src/components/Item.js @@ -8,13 +8,14 @@ * @return {Element} - The created result item element */ export default (data, index, config) => { - // Create a DIV element for each matching result item const item = document.createElement(config.resultItem.element); + // Setting item element attributes item.setAttribute("id", `${config.resultItem.idName}_${index}`); item.setAttribute("class", config.resultItem.className); item.setAttribute("role", "option"); + item.innerHTML = data.match; - // If custom content set pass params + // If custom content is active pass params if (config.resultItem.content) config.resultItem.content(data, item); return item; diff --git a/src/components/List.js b/src/components/List.js index f215071..13bb215 100644 --- a/src/components/List.js +++ b/src/components/List.js @@ -6,15 +6,16 @@ * @return {Element} - The created list element */ export default (config) => { - // Create a DIV element that will contain the results items const list = document.createElement(config.resultsList.element); + // Setting list element attributes list.setAttribute("id", config.resultsList.idName); list.setAttribute("class", config.resultsList.className); list.setAttribute("role", "listbox"); // List rendering destination - const destination = "string" === typeof config.resultsList.destination - ? document.querySelector(config.resultsList.destination) - : config.resultsList.destination(); + const destination = + "string" === typeof config.resultsList.destination + ? document.querySelector(config.resultsList.destination) + : config.resultsList.destination(); // Append the DIV element as a child of autoComplete container destination.insertAdjacentElement(config.resultsList.position, list); diff --git a/src/controllers/dataController.js b/src/controllers/dataController.js index eb58f0b..3964646 100644 --- a/src/controllers/dataController.js +++ b/src/controllers/dataController.js @@ -1,9 +1,9 @@ import searchEngine from "../services/search"; /** - * Find matches to `query` + * Find matches to "query" * - * @param {Object} config - The search engine configurations + * @param {Object} config - Search engine configurations * @param {String} query - User's search query string * * @return {Array} - Matches @@ -13,15 +13,16 @@ export default (config, query) => { const results = []; - // Get matches in data source + // Find matches from data source data.store.forEach((record, index) => { const search = (key) => { const recordValue = (key ? record[key] : record).toString(); - if (!recordValue) return; - - const match = typeof customSearchEngine === "function" + + const match = + typeof customSearchEngine === "function" ? customSearchEngine(query, recordValue) : searchEngine(query, recordValue, config); + if (!match) return; let result = { @@ -29,9 +30,12 @@ export default (config, query) => { match, value: record, }; - if (key) result.key = key + + if (key) result.key = key; + results.push(result); }; + if (data.key) { for (const key of data.key) { search(key); diff --git a/src/controllers/inputController.js b/src/controllers/inputController.js index 803a017..ca10f10 100644 --- a/src/controllers/inputController.js +++ b/src/controllers/inputController.js @@ -1,33 +1,47 @@ /** - * Gets the inputField search value "query" + * Format raw input value * - * @param {Element} inputField - autoComplete inputField or textarea element + * @param {String} inputValue - User's raw search query value + * @param {Object} config - autoComplete configurations * - * @return {String} - Raw inputField value as a string + * @return {String} - Raw "inputField" value as a string */ -const getInputValue = (inputField) => { - return inputField instanceof HTMLInputElement || inputField instanceof HTMLTextAreaElement - ? inputField.value.toLowerCase() - : inputField.innerHTML.toLowerCase(); +const formatRawInputValue = (value, config) => { + value = value.toLowerCase(); + + return config.diacritics + ? value + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .normalize("NFC") + : value; }; +/** + * Get the "inputField" search value + * + * @param {Element} inputField - autoComplete "inputField" or textarea element + * + * @return {String} - Raw "inputField" value as a string + */ +const getInputValue = (inputField) => + inputField instanceof HTMLInputElement || inputField instanceof HTMLTextAreaElement + ? inputField.value + : inputField.innerHTML; + /** * Intercept query value * - * @param {String} inputValue - User's raw search query value + * @param {String} input - User's raw search input value * @param {Object} config - autoComplete configurations * - * @return {String} - Manipulated Query Value + * @return {String} - Manipulated Query */ -const prepareQueryValue = (inputValue, config) => { - if (config.query && config.query.manipulate) - return config.query.manipulate(inputValue); - else - { - return config.diacritics - ? inputValue.normalize("NFD").replace(/[\u0300-\u036f]/g, "").normalize("NFC") - : inputValue - } -}; +const prepareQuery = (input, config) => + config.query && config.query.manipulate + ? config.query.manipulate(input) + : config.diacritics + ? formatRawInputValue(input, config) + : formatRawInputValue(input, config); -export { getInputValue, prepareQueryValue }; +export { getInputValue, prepareQuery, formatRawInputValue }; diff --git a/src/controllers/listController.js b/src/controllers/listController.js index 7aaa7f4..9648668 100644 --- a/src/controllers/listController.js +++ b/src/controllers/listController.js @@ -8,24 +8,26 @@ const ariaExpanded = "aria-expanded"; * Close open list * * @param {Object} config - autoComplete configurations - * @param {String} target - autoComplete inputField ID + * @param {String} target - autoComplete "inputField" element * * @return {void} */ const closeList = (config, target) => { + const inputField = config.inputField; + // Get autoComplete list const list = document.getElementById(config.resultsList.idName); - if (list && target !== config.inputField) { + if (list && target !== inputField) { // Remove open list list.remove(); // Remove active descendant - config.inputField.removeAttribute(ariaActive); + inputField.removeAttribute(ariaActive); // Set list to closed - config.inputField.setAttribute(ariaExpanded, false); + inputField.setAttribute(ariaExpanded, false); /** - * @emit {close} Emit Event on list close + * @emit {close} event after "resultsList" is closed **/ - eventEmitter(config.inputField, null, "close"); + eventEmitter(inputField, null, "close"); } }; diff --git a/src/controllers/navigationController.js b/src/controllers/navigationController.js index 7c0a2b3..c3442f1 100644 --- a/src/controllers/navigationController.js +++ b/src/controllers/navigationController.js @@ -10,87 +10,89 @@ const ariaActive = "aria-activedescendant"; * List navigation function initializer * * @param {Object} config - autoComplete configurations - * @param {Object} dataFeedback - The available data object + * @param {Object} dataFeedback - Data object * */ export default (config, dataFeedback) => { // Remove previous keyboard Event listener config.inputField.removeEventListener(keyboardEvent, config.nav); - // Reset cursor state + // Reset cursor let cursor = -1; + const classList = config.resultItem.selected.className.split(" "); + /** * Update list item state * - * @param {Object} event - The `keydown` event Object - * @param {Array } list - The array of list items - * @param {Boolean} state - Of the arrow Down/Up + * @param {Object} event - "keydown" event Object + * @param {Array} list - Array of list items + * @param {Boolean} state - arrow Down/Up * */ const update = (event, list, state) => { - // prevent default behaviour + // Prevent default behaviour event.preventDefault(); // If list is NOT empty if (list.length) { - // If the arrow `DOWN` key is pressed + // If arrow "DOWN" key pressed if (state) { - // increase the cursor + // Move cursor forward cursor++; } else { - // Else if the arrow `UP` key is pressed - // decrease the cursor + // Else if arrow "UP" key pressed + // Move cursor backwards cursor--; } - // and add "active" class to the list item + // Add "active/selected" class to the list item addActive(list); + // Set "aria-activedescendant" value to the selected item config.inputField.setAttribute(ariaActive, list[cursor].id); /** - * @emit {navigate} Emit Event on results list navigation + * @emit {navigate} event on results list navigation **/ eventEmitter(event.srcElement, { event, ...dataFeedback, selection: dataFeedback.results[cursor] }, "navigate"); } }; /** - * Remove list item active state + * Remove "active" class from all list items * - * @param {Array } list - The array of list items + * @param {Array} list - Array of list items * */ const removeActive = (list) => { - // Remove "active" class from all list items - for (let index = 0; index < list.length; index++) { - // Remove "active" class from the item - list[index].removeAttribute(ariaSelected); + Array.from(list).forEach((item) => { + // Remove "aria-selected" attribute from the item + item.removeAttribute(ariaSelected); // Remove "selected" class from the item - if (config.resultItem.selected.className) list[index].classList.remove(config.resultItem.selected.className); - } + if (classList) item.classList.remove(...classList); + }); }; /** - * Add active state to list item + * Add "selected" class to the selected item * - * @param {Array } list - The array of list items + * @param {Array} list - Array of list items * */ const addActive = (list) => { // Remove "active" class from all list items removeActive(list); - // Reset selection to first item + // Reset cursor to first item if (cursor >= list.length) cursor = 0; - // Move selection to the next item + // Move cursor to the next item if (cursor < 0) cursor = list.length - 1; - // Add "active" class to the current item + // Set "aria-selected" value to true list[cursor].setAttribute(ariaSelected, true); - // Add "selected" class to the current item - if (config.resultItem.selected.className) list[cursor].classList.add(config.resultItem.selected.className); + // Add "selected" class to the selected item + if (classList) list[cursor].classList.add(...classList); }; /** * List Navigation Function * - * @param {Object} event - The `keydown` event Object + * @param {Object} event - "keydown" event Object * * @return {void} */ @@ -104,25 +106,30 @@ export default (config, dataFeedback) => { // Check pressed key switch (event.keyCode) { - case 40: // Down arrow + // Down arrow + case 40: update(event, list, 1); break; - case 38: // Up arrow + // Up arrow + case 38: update(event, list); break; - case 27: // Esc + // Esc + case 27: config.inputField.value = ""; closeList(config); break; - case 13: // Enter + // Enter + case 13: event.preventDefault(); // If cursor moved if (cursor >= 0) { - // Simulate a click on the selected "active" item + // Simulate a click event on the selected "active" item list[cursor].click(); } break; - case 9: // Tab + // Tab + case 9: closeList(config); break; } @@ -130,7 +137,7 @@ export default (config, dataFeedback) => { }; /** - * @listen {keydown} Listen to all `keydown` events on the inputField + * @listen {keydown} events of inputField **/ config.inputField.addEventListener(keyboardEvent, config.nav); }; diff --git a/src/controllers/resultsController.js b/src/controllers/resultsController.js index de4d437..2143106 100644 --- a/src/controllers/resultsController.js +++ b/src/controllers/resultsController.js @@ -13,36 +13,39 @@ const ariaActive = "aria-activedescendant"; * List all matching results * * @param {Object} config - autoComplete configurations - * @param {Object|Array} data - The available data object + * @param {Object|Array} data - Data object * - * @return {Component} - The matching results list component + * @return {Component} - Results list component */ export default (config, data) => { - const { input, query, matches, results } = data; + const { query, matches, results } = data; + const input = config.inputField; + const resultsList = config.resultsList; + // Results list element let list = document.getElementById(config.resultsList.idName); // Check if there is a rendered list if (list) { list.innerHTML = ""; - // Remove active descendant - config.inputField.removeAttribute(ariaActive); + // Remove "aria-activedescendant" attribute + input.removeAttribute(ariaActive); } else { // Create new list list = createList(config); // Set list to opened - config.inputField.setAttribute(ariaExpanded, true); + input.setAttribute(ariaExpanded, true); /** - * @emit {open} Emit Event after results list is opened + * @emit {open} event after results list is opened **/ - eventEmitter(config.inputField, data, "open"); + eventEmitter(input, data, "open"); } if (matches.length) { results.forEach((item, index) => { const resultItem = createItem(item, index, config); resultItem.addEventListener(clickEvent, (event) => { - // Prepare onSelection feedback data object + // Prepare onSelection data feedback object const dataFeedback = { event, ...data, @@ -51,36 +54,33 @@ export default (config, data) => { index, }, }; - // Returns the selected value onSelection if set + // Return selected value if onSelection is active if (config.onSelection) config.onSelection(dataFeedback); }); list.appendChild(resultItem); }); } else { // Check if there are NO results - if (!config.resultsList.noResults) { + if (!resultsList.noResults) { closeList(config); // Set list to closed - config.inputField.setAttribute(ariaExpanded, false); + input.setAttribute(ariaExpanded, false); } else { - // Run noResults action function - config.resultsList.noResults(list, query); + // Run "noResults" action function + resultsList.noResults(list, query); } } - // If custom container is set pass the list - if (config.resultsList.container) config.resultsList.container(list, data); + // Run custom container function if active + if (resultsList.container) resultsList.container(list, data); // Initialize list navigation controls - if (config.resultsList.navigation) - config.resultsList.navigation(list) - else - navigation(config, data); + resultsList.navigation ? resultsList.navigation(list) : navigation(config, data); /** * @desc - * Listen for all `click` events in the document - * and close list if clicked outside the list and inputField - * @listen {click} Listen to all `click` events on the document + * Listen for all "click" events in the document + * and close list if clicked outside "resultsList" or "inputField" + * @listen {click} events of the entire document **/ document.addEventListener(clickEvent, (event) => closeList(config, event.target)); }; diff --git a/src/controllers/triggerController.js b/src/controllers/triggerController.js index 95181e8..54ba29c 100644 --- a/src/controllers/triggerController.js +++ b/src/controllers/triggerController.js @@ -1,14 +1,13 @@ /** - * App triggering condition + * autoComplete triggering condition * - * @param {Object} config - Trigger Object with the Trigger function - * @param {Object} event - The trigger event Object - * @param {String} queryValue - User's search query value string after manipulation + * @param {Object} config - autoComplete configurations + * @param {Object} event - Trigger event Object + * @param {String} query - User's manipulated search query value * - * @return {Boolean} triggerCondition - For autoComplete trigger decision + * @return {Boolean} triggerCondition - For autoComplete to run */ -export default (config, event, queryValue) => { - if (config.trigger.condition) - return config.trigger.condition(event, queryValue); - return queryValue.length >= config.threshold && queryValue.replace(/ /g, "").length; -}; +export default (config, event, query) => + config.trigger.condition + ? config.trigger.condition(event, query) + : query.length >= config.threshold && query.replace(/ /g, "").length; diff --git a/src/services/search.js b/src/services/search.js index d680a28..f7afac8 100644 --- a/src/services/search.js +++ b/src/services/search.js @@ -1,61 +1,56 @@ -// Result item constructor -const item = (className, value) => `${value}`; +import { formatRawInputValue } from "../controllers/inputController"; + +// Highlighted result item constructor +const highlightChar = (className, value) => `${value}`; /** - * Search common characters within record + * Find matching characters in record * - * @param {String} query - User's search query value after manipulation - * @param {String} record - The data item string to be compared - * @param {Object {searchEngine: String, highlight: Boolean}} config - The search engine configurations + * @param {String} query - User's manipulated search query value + * @param {String} record - Data record string to be compared + * @param {Object} config - autoComplete configurations * - * @return {String} - The matched data item string + * @return {String} - Matched data record string */ export default (query, record, config) => { - const recordLowerCase = config.diacritics - ? record - .toLowerCase() - .normalize("NFD") - .replace(/[\u0300-\u036f]/g, "") - .normalize("NFC") - : record.toLowerCase(); + const formattedRecord = formatRawInputValue(record, config); if (config.searchEngine === "loose") { - // Search query string sanitized & normalized + // Query string with no spaces query = query.replace(/ /g, ""); - // Array of matching characters - const match = []; - // Query character position based on success - let searchPosition = 0; - // Iterate over record characters - for (let number = 0; number < recordLowerCase.length; number++) { - // Holds current record character - let recordChar = record[number]; - // Matching case - if (searchPosition < query.length && recordLowerCase[number] === query[searchPosition]) { - // Highlight matching character - recordChar = config.resultItem.highlight.render - ? item(config.resultItem.highlight.className, recordChar) - : recordChar; - searchPosition++; - } - // Adds matching character to the matching list - match.push(recordChar); - } - // Non-Matching case - if (searchPosition === query.length) { - // Return the joined match - return match.join(""); - } - - } else { // Strict mode - if (recordLowerCase.includes(query)) { - // Regular Expression Query Pattern Ignores caseSensitive + const queryLength = query.length; + // Query character cursor position based on match + let cursor = 0; + // Matching characters + const match = Array.from(record) + .map((character, index) => { + // Matching case + if (cursor < queryLength && formattedRecord[index] === query[cursor]) { + // Highlight matching character if active + character = config.resultItem.highlight.render + ? highlightChar(config.resultItem.highlight.className, character) + : character; + // Move cursor position + cursor++; + } + + return character; + }) + .join(""); + // If record is fully scanned + if (cursor === queryLength) return match; + } else { + // Strict mode + if (formattedRecord.includes(query)) { + // Ignore special characters & caseSensitivity const pattern = new RegExp(query.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"), "i"); - // Search for a match Query in Record query = pattern.exec(record); - if (config.resultItem.highlight.render) - return record.replace(query, item(config.resultItem.highlight.className, query)); - return record; + // Highlight matching characters if active + const match = config.resultItem.highlight.render + ? record.replace(query, highlightChar(config.resultItem.highlight.className, query)) + : record; + + return match; } } }; diff --git a/src/utils/debouncer.js b/src/utils/debouncer.js index aed4b21..0265d16 100644 --- a/src/utils/debouncer.js +++ b/src/utils/debouncer.js @@ -1,10 +1,10 @@ /** * Debouncer * - * @param {Function} callback - The callback function - * @param {Number} delay - The delay number value + * @param {Function} callback - Callback function + * @param {Number} delay - Delay number value * - * @return {Function} - The callback function wrapped in `setTimeout` function + * @return {Function} - Debouncer function */ export default (callback, delay) => { let inDebounce; diff --git a/src/utils/eventEmitter.js b/src/utils/eventEmitter.js index 87ee080..1f37112 100644 --- a/src/utils/eventEmitter.js +++ b/src/utils/eventEmitter.js @@ -1,10 +1,10 @@ /** - * Event emitter/dispatcher `Function` + * Event emitter/dispatcher function + * + * @param {Element} target - Target element to listen for + * @param {Object|null} detail - Data object with relevant information or null + * @param {String} name - Name of fired event * - * @param {Element} target - The target element to listen for - * @param {Object} detail - The detail object containing relevant information or null - * @param {String} name - The name of the event fired - * * @return {void} */ export default (target, detail, name) => {