diff --git a/CHANGELOG.md b/CHANGELOG.md index 12a5e04..a00d005 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to this project will be documented in this file. -## [1.2.0] - 2020-10-30 +## [1.2.0] - 2020-10-31 ### Added @@ -13,8 +13,9 @@ All notable changes to this project will be documented in this file. - the function usage of the `source` option can now take a Promise which resolves with the items to render, instead of having to use the provided second argument callback - `onAsyncBeforeSend` callback option, to allow adjustments to the xhr object before it is sent (e.g. adding auth headers) - `onAsyncComplete` callback option, that fires after async call successfully completes and all items have rendered -- `srAutoClear` option that takes a boolean, or number, to allow a delay before automatically clearing the screen reader announcement element +- `srAutoClear` option that takes a boolean, or number, to allow a delay before automatically clearing the screen reader announcement element - defaults to 5 seconds - `deleteAllControl` and `deleteAllText` options to render a button enabling quick deletion of all selected items (when there are at least 2 selected items) +- `create` option to allow adding a results entry for the current search text (if no exact results match is found) - for all async related callbacks, and when the `source` is a function, there is now an additional final param that indicates if it is the first/starting call. - the selected items in multiple mode, and the show all button, will now have their `aria-describedby` set to link them to the control label @@ -24,6 +25,7 @@ All notable changes to this project will be documented in this file. - in multiple mode, when deleting a selected item by clicking it or using enter, move focus to the next available selected item - set the `aria-describedby` attribute on the list container to reference the control's - do not hide the list when focus moves from the input to the show all control +- moved the screen reader announcement element to be before the generated input, so that if users navigate past the input, they will not encounter the announcement element out of context ### Fixed @@ -32,6 +34,7 @@ All notable changes to this project will be documented in this file. - Added a workaround for an IE11 bug where the options were shown on load if the `minLength` was set to 0 on a multi-select autocomplete with starting values. This was due to the input's placeholder being removed, which erroneously triggers the `input` event in IE11. - Edge case errors when destroying the component immediately after certain actions (such as selecting an item, or blurring off of the component). - In multiple mode, moved the selected items to be after the list to fix issue on mobile when navigating by swipe, as it was possible to reach the selected items first, causing the list to disappear. +- Issue with `confirmOnBlur` option not working correctly when a results option did not currently have focus. ## [1.1.4] - 2020-07-05 diff --git a/README.md b/README.md index 4dffdf1..cd221cc 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,10 @@ The full list of options, and their defaults: /** * Specify source. See examples file for more specific usage. * @example ['Afghanistan', 'Albania', 'Algeria', ...more] + * @example [{ label: 'Afghanistan', value: 'AFG' }, ...more] + * @example 'https://some-endpoint.somewhere/available' * @example (query, render, isFirstCall) => render(arrayToUse) + * @example (query) => async () => arrayToUse */ source: string[] | any[] | string | Function; @@ -104,6 +107,12 @@ The full list of options, and their defaults: */ alsoSearchIn: string[] = []; + /** + * If no exact match is found, + * create an entry in the options list for the current search text + */ + create: boolean | ((value: string) => string | object) = false; + /** * Input delay after typing before running a search */ @@ -215,9 +224,9 @@ The full list of options, and their defaults: /** * Automatically clear the screen reader announcement element after the specified delay - * Defaults to 2 seconds if true + * Number is in milliseconds. If true, defaults to 5000. */ - srAutoClear: boolean | number = false; + srAutoClear: boolean | number = 5000; /** * Screen reader text used in multiple mode for element deletion. diff --git a/dist/aria-autocomplete.min.js b/dist/aria-autocomplete.min.js index 346098a..d90942c 100644 --- a/dist/aria-autocomplete.min.js +++ b/dist/aria-autocomplete.min.js @@ -1 +1 @@ -!function(e,t){if("object"==typeof exports&&"object"==typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var i=t();for(var s in i)("object"==typeof exports?exports:e)[s]=i[s]}}(window,(function(){return function(e){var t={};function i(s){if(t[s])return t[s].exports;var n=t[s]={i:s,l:!1,exports:{}};return e[s].call(n.exports,n,n.exports,i),n.l=!0,n.exports}return i.m=e,i.c=t,i.d=function(e,t,s){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:s})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,t){if(1&t&&(e=i(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var s=Object.create(null);if(i.r(s),Object.defineProperty(s,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var n in e)i.d(s,n,function(t){return e[t]}.bind(null,n));return s},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="",i(i.s=2)}([function(e,t,i){window,e.exports=function(e){var t={};function i(s){if(t[s])return t[s].exports;var n=t[s]={i:s,l:!1,exports:{}};return e[s].call(n.exports,n,n.exports,i),n.l=!0,n.exports}return i.m=e,i.c=t,i.d=function(e,t,s){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:s})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,t){if(1&t&&(e=i(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var s=Object.create(null);if(i.r(s),Object.defineProperty(s,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var n in e)i.d(s,n,function(t){return e[t]}.bind(null,n));return s},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="",i(i.s=0)}([function(e,t,i){"use strict";var s;function n(e,t){if(e)for(var i in t){var s="number"==typeof t[i]?t[i]+"px":t[i];e.style[i]=s+""}}i.r(t),i.d(t,"InputAutoWidth",(function(){return r}));var r=function(){function e(e,t){this.cache={},this.options=t,this.input=e,this.trigger(),this.eventHandler=this.trigger.bind(this),this.input.addEventListener("blur",this.eventHandler),this.input.addEventListener("input",this.eventHandler),this.input.addEventListener("keyup",this.eventHandler),this.input.addEventListener("keydown",this.eventHandler)}return e.prototype.measureString=function(e){return e?this.cache&&"number"==typeof this.cache[e]?this.cache[e]:(s||(n(s=document.createElement("span"),{position:"absolute",top:-99999,left:-99999,width:"auto",padding:0,whiteSpace:"pre"}),document.body.appendChild(s)),s.textContent=e,function(e,t,i){if(e&&t){var s=getComputedStyle(e),r={};if(i&&i.length)for(var l=0,o=i.length;l=48&&t<=57||t>=65&&t<=90||t>=96&&t<=111||t>=186&&t<=222||32===t||8===t||46===t){var a=String.fromCharCode(n);s+=a=e.shiftKey?a.toUpperCase():a.toLowerCase()}}!s&&(i=this.input.getAttribute("placeholder"))&&(s=i);var h=this.measureString(s)+4;this.options&&this.options.cache&&this.cache&&(this.cache[s]=h);var u=this.options&&this.options.minWidth;"number"==typeof u&&hc&&(h=c),h!==this.currentWidth&&(this.currentWidth=h,this.input.style.width=h+"px")}},e.prototype.destroy=function(){this.input.removeEventListener("blur",this.eventHandler),this.input.removeEventListener("input",this.eventHandler),this.input.removeEventListener("keyup",this.eventHandler),this.input.removeEventListener("keydown",this.eventHandler),this.input=this.cache=null},e}();t.default=r}])},function(e,t){var i=Element.prototype;i.matches||(i.matches=i.msMatchesSelector||i.webkitMatchesSelector),i.closest||(i.closest=function(e){var t=this;do{if(t.matches(e))return t;t=t.parentElement||t.parentNode}while(null!==t&&1===t.nodeType);return null})},function(e,t,i){"use strict";i.r(t),i.d(t,"AriaAutocomplete",(function(){return B}));var s=i(0),n=i.n(s),r=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;function l(e,t){if(e&&1===e.nodeType&&"string"==typeof t)!function(e,t){for(var i=e.getAttribute&&e.getAttribute("class")||"",s=" "+i+" ",n=0,l=t.split(" "),o=l.length;n-1)&&i.clearAnnouncement("number"==typeof t?t:2e3)}),t)):this.srAnnouncements.textContent=e)},e.prototype.isSelectedElem=function(e){var t=e&&e[c];return!(!this.multiple||"object"!=typeof t)},e.prototype.getSelectedElems=function(){for(var e=[],t=0,i=this.wrapper.childNodes.length;t-1&&this.selected[i]){var r=L(this.selected[i]),l=r.label;k(r.element,!1,this),this.selected.splice(i,1),this.triggerOptionCallback("onDelete",[r]),this.setSourceElementValues(),this.buildMultiSelected(t?i:null),this.triggerAutoGrow(),this.announce(l+" "+this.options.srDeletedText,0)}}},e.prototype.createSelectedElemFrom=function(e,t){var i=e.label,s=this.cssNameSpace,n=s+"__selected",r=document.createElement("span"),l=t?s+"__delete-all "+n+" "+n+"--delete-all":n;return r.setAttribute("aria-describedby",this.ids.LABEL),r.setAttribute("class",l),r.setAttribute("role","button"),r.setAttribute("tabindex","0"),r.textContent=i,t||(r.setAttribute("aria-label",this.options.srDeleteText+" "+i),r[c]=e),r},e.prototype.buildMultiSelected=function(e){var t=this;if(this.multiple){this.multiple&&this.selected.length>=this.options.maxItems?this.disable():this.enable();var i=this.getSelectedElems();if(this.selected.length||i.length){var s=[];i.forEach((function(e){for(var i=e[c],n=0,r=t.selected.length;n=i.length)return this.currentSelectedIndex=i.length-1,void this.setOptionFocus(e,this.currentSelectedIndex);var s=i[t];if(s&&"string"==typeof s.getAttribute("tabindex"))return this.currentSelectedIndex=t,a(s,this.cssNameSpace+"__option--focused focused focus"),s.setAttribute("aria-selected","true"),void s.focus();this.currentSelectedIndex=-1},e.prototype.setSourceElementValues=function(){for(var e=[],t=0,i=this.selected.length;t=this.options.maxItems)&&this.filteredSource.length&&this.filteredSource[t]){for(var s=L(this.filteredSource[t]),n=!1,r=0,l=this.selected.length;r'+b+"")}t.length?(a(this.list,h+"__list--has-results"),l(this.list,h+"__list--no-results")):(l(this.list,h+"__list--has-results"),a(this.list,h+"__list--no-results"));var g=this.options.noResultsText;!t.length&&"string"==typeof g&&g.length&&(r=g,t.push('
  • '+g+"
  • ")),this.cancelFilterPrep(),r||(r=this.triggerOptionCallback("srResultsText",[f])),r&&this.announce(r);var y=t.join("");if(this.currentListHtml!==y?(this.currentListHtml=y,this.list.innerHTML=y):this.resetOptionAttributes(),!t.length)return this.hide(),void(this.forceShowAll=!1);this.show(),this.forceShowAll=!1},e.prototype.handleAsync=function(e,t){var i=this;void 0===t&&(t=!1),this.xhr&&"function"==typeof this.xhr.abort&&this.xhr.abort();var s=new XMLHttpRequest,n=this.forceShowAll,r=t?null:this.api,l=this.multiple?this.selected.length:0,o=n||t||9999===this.options.maxResults,a=this.source+(/\?/.test(this.source)?"&":"?")+encodeURIComponent(this.options.asyncQueryParam)+"="+encodeURIComponent(e)+"&"+encodeURIComponent(this.options.asyncMaxResultsParam)+"="+(o?9999:l+this.options.maxResults),h=this.triggerOptionCallback("onAsyncPrep",[a,s,t],r);h&&"string"==typeof h&&(a=h),s.open("GET",a),s.onload=function(){if(s.readyState===s.DONE&&s.status>=200&&s.status<300){i.forceShowAll=n;var l=F(i.triggerOptionCallback("onAsyncSuccess",[e,s,t],r)||s.responseText,i.options.sourceMapping,!1);t?(i.prepSelectedFromArray(l),i.setInputStartingStates(!1)):i.setListOptions(l),i.triggerOptionCallback("onAsyncComplete",[e,s,t],r)}},s.onerror=function(){i.triggerOptionCallback("onAsyncError",[e,s,t],r)},t||(this.xhr=s),this.triggerOptionCallback("onAsyncBeforeSend",[e,s,t],r),s.send()},e.prototype.filter=function(e){var t=this;if("string"==typeof e){var i=this.forceShowAll;if(!i){var s=this.triggerOptionCallback("onSearch",[e]);"string"==typeof s&&(e=s)}if(this.term=e,"string"==typeof this.source&&this.source.length)return this.handleAsync(e),void(this.forceShowAll=!1);if("function"!=typeof this.source){e||(i=!0);var n=[];if(this.source&&this.source.length){var r=[u];if(!i){e=C(e,!0);var l=this.options.alsoSearchIn;Array.isArray(l)&&l.length&&(r=function(e){var t=[];return e.forEach((function(e){if("string"==typeof e){for(var i=x(e),s="label"!==i,n=0,r=t.length;s&&n-1&&R(n,t,s))return!0}return!1}(t,e,r))&&n.push(t)}))}this.setListOptions(n)}else{var o=function(e){var i=F(e,t.options.sourceMapping);t.setListOptions(i)},a=this.source.call(this.api,this.term,o,!1);a&&"function"==typeof a.then&&a.then((function(e){return o(e)}))}}else this.cancelFilterPrep()},e.prototype.cancelFilterPrep=function(){clearTimeout(this.filterTimer),l(this.wrapper,this.cssNameSpace+"__wrapper--loading loading"),l(this.input,this.cssNameSpace+"__input--loading loading"),this.filtering=!1},e.prototype.filterPrep=function(e,t,i){var s=this;void 0===t&&(t=!1),void 0===i&&(i=!1);var n=this.forceShowAll,r=n||i?0:this.options.delay;this.cancelFilterPrep(),this.filtering=!0,this.filterTimer=setTimeout((function(){var i=s.input.value;if(s.inputPollingValue=i,(n||""===i||t&&!s.multiple&&s.selected.length&&x(s.selected[0].label)===x(i))&&(i=""),!n&&i.length=this.options.minLength)&&this.filterPrep(e)),this.menuOpen&&!this.filtering){var t=this.currentSelectedIndex;"number"!=typeof t||t<0?this.setOptionFocus(e,0):this.setOptionFocus(e,t+1)}},e.prototype.handleEndKey=function(e){if(!this.disabled&&this.menuOpen&&e.target!==this.input){var t=P(this.list);t.length&&(e.preventDefault(),this.setOptionFocus(e,t.length-1))}},e.prototype.handleHomeKey=function(e){!this.disabled&&this.menuOpen&&e.target!==this.input&&(e.preventDefault(),this.setOptionFocus(e,0))},e.prototype.handleEnterKey=function(e){var t=e.target;this.isSelectedElem(t)?this.removeEntryFromSelected(t[c],!0):this.deleteAll&&t===this.deleteAll?this.deleteAllSelected():this.disabled||(this.showAll&&t===this.showAll?this.filterPrepShowAll(e):(this.menuOpen&&(e.preventDefault(),this.currentSelectedIndex>-1&&this.handleOptionSelect(e,this.currentSelectedIndex)),t===this.input&&this.filterPrep(e,!1,!0)))},e.prototype.handleKeyDownDefault=function(e){var t=e.keyCode,i=e.target===this.input;if(t===m&&!i||this.isSelectedElem(e.target)&&t===A)return e.preventDefault(),void this.handleEnterKey(e);if(!this.disabled){var s=this.selected&&this.selected.length;if(this.options.deleteOnBackspace&&t===p&&""===this.input.value&&s&&i&&this.multiple)this.removeEntryFromSelected(this.selected[s-1]);else{var n=function(e){return e>=48&&e<=57||e>=65&&e<=90||e>=96&&e<=111||e>=186&&e<=222||32===e||8===e||46===e}(t),r=!i&&n;r&&this.input.focus(),(r||i&&n)&&this.filterPrep(e)}}},e.prototype.prepKeyDown=function(e){switch(e.keyCode){case g:this.handleUpKey(e);break;case y:this.handleDownKey(e);break;case v:this.handleEndKey(e);break;case b:this.handleHomeKey(e);break;case d:this.handleEnterKey(e);break;case f:this.handleComponentBlur(e,!0);break;default:this.handleKeyDownDefault(e)}},e.prototype.cancelPolling=function(){clearTimeout(this.pollingTimer)},e.prototype.startPolling=function(){var e=this;this.filtering||this.input.value===this.inputPollingValue||this.filterPrep({}),this.pollingTimer=setTimeout((function(){e.startPolling()}),200)},e.prototype.bindEvents=function(){var e=this;this.wrapper.addEventListener("focusout",(function(t){e.handleComponentBlur(t,!1)})),this.wrapper.addEventListener("focusin",(function(t){e.list.contains(t.target)||(e.currentSelectedIndex=-1),e.isFocused||e.triggerOptionCallback("onFocus",[e.wrapper]),e.isFocused=!0})),this.wrapper.addEventListener("keydown",(function(t){e.prepKeyDown(t)})),this.wrapper.addEventListener("click",(function(t){t.target!==e.wrapper?(e.isSelectedElem(t.target)&&e.removeEntryFromSelected(t.target[c],!0),e.deleteAll&&t.target===e.deleteAll&&e.deleteAllSelected()):e.input.focus()}));var t=this.cssNameSpace+"__wrapper--focused focused focus",i=this.cssNameSpace+"__input--focused focused focus";this.input.addEventListener("blur",(function(){l(e.wrapper,t),l(e.input,i),e.cancelPolling()})),this.input.addEventListener("input",(function(t){document.activeElement===e.input&&e.filterPrep(t)})),this.input.addEventListener("click",(function(t){!e.menuOpen&&e.input.value.length>=e.options.minLength&&e.filterPrep(t,!0)})),this.input.addEventListener("focusin",(function(s){a(e.wrapper,t),a(e.input,i),e.startPolling(),e.disabled||e.menuOpen||e.filterPrep(s,!0)})),this.showAll&&this.showAll.addEventListener("click",(function(t){e.filterPrepShowAll(t)})),this.list.addEventListener("mouseenter",(function(t){e.resetOptionAttributes()})),this.list.addEventListener("click",(function(t){if(t.target!==e.list){var i=P(e.list);if(i.length){var s=i.indexOf(t.target);e.handleOptionSelect(t,s)}}})),this.autoGrow&&(this.inputAutoWidth=new n.a(this.input))},e.prototype.prepListSourceCheckboxes=function(){this.multiple=!0,this.source=[];for(var e=this.element.querySelectorAll('input[type="checkbox"]'),t=0,i=e.length;t1&&(this.options.maxItems=1),this.source=[];for(var t=this.element.querySelectorAll("option"),i=0,s=t.length;i-1&&t.selected.push(e[s])}}))}},e.prototype.prepListSourceArray=function(){this.source=F(this.source,this.options.sourceMapping),this.prepSelectedFromArray(this.source)},e.prototype.prepListSourceAsync=function(){var e=this.element;this.elementIsInput&&e.value&&this.handleAsync(e.value,!0)},e.prototype.prepListSourceFunction=function(){var e=this,t=this.element;if(this.elementIsInput&&t.value){var i=function(t){var i=F(t,e.options.sourceMapping);e.prepSelectedFromArray(i),e.setInputStartingStates(!1)},s=this.source.call(void 0,t.value,i,!0);s&&"function"==typeof s.then&&s.then((function(e){return i(e)}))}},e.prototype.prepListSource=function(){return"function"==typeof this.source?this.prepListSourceFunction():"string"==typeof this.source&&this.source.length?this.prepListSourceAsync():Array.isArray(this.source)&&this.source.length?this.prepListSourceArray():this.elementIsSelect?this.prepListSourceDdl():void(this.element.querySelector('input[type="checkbox"]')&&this.prepListSourceCheckboxes())},e.prototype.setInputStartingStates=function(e){if(void 0===e&&(e=!0),e){this.label&&(this.label._ariaAutocompleteLabelOriginallyFor=this.ids.ELEMENT,this.label.setAttribute("for",this.ids.INPUT));var t=this.element.getAttribute("aria-describedby");t&&this.input.setAttribute("aria-describedby",t);var i=this.element.getAttribute("aria-labelledby");i&&this.input.setAttribute("aria-labelledby",i)}this.selected.length&&(this.multiple?(this.buildMultiSelected(),this.triggerAutoGrow()):this.setInputValue(this.selected[0].label||"",!0)),this.element.disabled&&this.disable(!0)},e.prototype.setHtml=function(){var e=this.options,t=this.cssNameSpace,i=e.wrapperClassName?" "+e.wrapperClassName:"",s=['
    '],n=e.name?' name="'+e.name+'"':"",r=e.inputClassName?" "+e.inputClassName:"",l=e.placeholder?' placeholder="'+e.placeholder+'" aria-placeholder="'+e.placeholder+'"':"";s.push('"),e.showAllControl&&s.push('');var o=e.srListLabelText,a=e.listClassName?" "+e.listClassName:"",h=o?' aria-label="'+o+'"':"";s.push('"),s.push('"),s.push('

    '),s.push("
    "),this.element.insertAdjacentHTML("afterend",s.join(""))},e.prototype.destroy=function(){var e=this;this.label&&this.label._ariaAutocompleteLabelOriginallyFor&&(this.label.setAttribute("for",this.label._ariaAutocompleteLabelOriginallyFor),delete this.label._ariaAutocompleteLabelOriginallyFor),this.documentClickBound&&document.removeEventListener("click",this.documentClick),this.autoGrow&&this.inputAutoWidth&&this.inputAutoWidth.destroy(),this.wrapper.parentNode.removeChild(this.wrapper),delete this.element.ariaAutocomplete,this.show(this.element),clearTimeout(this.filterTimer),clearTimeout(this.pollingTimer),clearTimeout(this.showAllPrepTimer),clearTimeout(this.announcementTimer),clearTimeout(this.componentBlurTimer),clearTimeout(this.clearAnnouncementTimer),clearTimeout(this.elementChangeEventTimer),["list","input","label","element","wrapper","showAll","deleteAll","srAssistance","srAnnouncements"].forEach((function(t){return e[t]=null}))},e.prototype.init=function(e,t){this.selected=[],this.element=e,this.label=document.querySelector('[for="'+this.element.id+'"]'),this.ids=new E(this.element.id,this.label?this.label.id:null,t.id),this.elementIsInput="INPUT"===e.nodeName,this.elementIsSelect="SELECT"===e.nodeName,this.options=new h(t),this.label&&!this.label.id&&(this.label.id=this.ids.LABEL),this.source=this.options.source,this.multiple=this.options.multiple,this.autoGrow=this.options.autoGrow,this.cssNameSpace=this.options.cssNameSpace,this.documentClick=this.handleComponentBlur.bind(this),this.setHtml(),this.list=document.getElementById(this.ids.LIST),this.input=document.getElementById(this.ids.INPUT),this.wrapper=document.getElementById(this.ids.WRAPPER),this.showAll=document.getElementById(this.ids.BUTTON),this.srAssistance=document.getElementById(this.ids.SR_ASSISTANCE),this.srAnnouncements=document.getElementById(this.ids.SR_ANNOUNCEMENTS),this.prepListSource();var i=[];this.options.showAllControl&&i.push(this.cssNameSpace+"__wrapper--show-all"),this.autoGrow&&i.push(this.cssNameSpace+"__wrapper--autogrow"),this.multiple&&i.push(this.cssNameSpace+"__wrapper--multiple"),i.length&&a(this.wrapper,i.join(" ")),this.hide(this.list),this.hide(this.element),this.setInputStartingStates(),this.bindEvents(),this.api=new S(this),this.triggerOptionCallback("onReady",[this.wrapper])},e}();function B(e,t){return e&&e.ariaAutocomplete&&e.ariaAutocomplete.open?e.ariaAutocomplete:new D(e,t).api}t.default=B}])})); \ No newline at end of file +!function(e,t){if("object"==typeof exports&&"object"==typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var i=t();for(var s in i)("object"==typeof exports?exports:e)[s]=i[s]}}(window,(function(){return function(e){var t={};function i(s){if(t[s])return t[s].exports;var n=t[s]={i:s,l:!1,exports:{}};return e[s].call(n.exports,n,n.exports,i),n.l=!0,n.exports}return i.m=e,i.c=t,i.d=function(e,t,s){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:s})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,t){if(1&t&&(e=i(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var s=Object.create(null);if(i.r(s),Object.defineProperty(s,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var n in e)i.d(s,n,function(t){return e[t]}.bind(null,n));return s},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="",i(i.s=2)}([function(e,t,i){window,e.exports=function(e){var t={};function i(s){if(t[s])return t[s].exports;var n=t[s]={i:s,l:!1,exports:{}};return e[s].call(n.exports,n,n.exports,i),n.l=!0,n.exports}return i.m=e,i.c=t,i.d=function(e,t,s){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:s})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,t){if(1&t&&(e=i(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var s=Object.create(null);if(i.r(s),Object.defineProperty(s,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var n in e)i.d(s,n,function(t){return e[t]}.bind(null,n));return s},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="",i(i.s=0)}([function(e,t,i){"use strict";var s;function n(e,t){if(e)for(var i in t){var s="number"==typeof t[i]?t[i]+"px":t[i];e.style[i]=s+""}}i.r(t),i.d(t,"InputAutoWidth",(function(){return r}));var r=function(){function e(e,t){this.cache={},this.options=t,this.input=e,this.trigger(),this.eventHandler=this.trigger.bind(this),this.input.addEventListener("blur",this.eventHandler),this.input.addEventListener("input",this.eventHandler),this.input.addEventListener("keyup",this.eventHandler),this.input.addEventListener("keydown",this.eventHandler)}return e.prototype.measureString=function(e){return e?this.cache&&"number"==typeof this.cache[e]?this.cache[e]:(s||(n(s=document.createElement("span"),{position:"absolute",top:-99999,left:-99999,width:"auto",padding:0,whiteSpace:"pre"}),document.body.appendChild(s)),s.textContent=e,function(e,t,i){if(e&&t){var s=getComputedStyle(e),r={};if(i&&i.length)for(var l=0,o=i.length;l=48&&t<=57||t>=65&&t<=90||t>=96&&t<=111||t>=186&&t<=222||32===t||8===t||46===t){var a=String.fromCharCode(n);s+=a=e.shiftKey?a.toUpperCase():a.toLowerCase()}}!s&&(i=this.input.getAttribute("placeholder"))&&(s=i);var h=this.measureString(s)+4;this.options&&this.options.cache&&this.cache&&(this.cache[s]=h);var u=this.options&&this.options.minWidth;"number"==typeof u&&hc&&(h=c),h!==this.currentWidth&&(this.currentWidth=h,this.input.style.width=h+"px")}},e.prototype.destroy=function(){this.input.removeEventListener("blur",this.eventHandler),this.input.removeEventListener("input",this.eventHandler),this.input.removeEventListener("keyup",this.eventHandler),this.input.removeEventListener("keydown",this.eventHandler),this.input=this.cache=null},e}();t.default=r}])},function(e,t){var i=Element.prototype;i.matches||(i.matches=i.msMatchesSelector||i.webkitMatchesSelector),i.closest||(i.closest=function(e){var t=this;do{if(t.matches(e))return t;t=t.parentElement||t.parentNode}while(null!==t&&1===t.nodeType);return null})},function(e,t,i){"use strict";i.r(t),i.d(t,"AriaAutocomplete",(function(){return M}));var s=i(0),n=i.n(s),r=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;function l(e,t){if(e&&1===e.nodeType&&"string"==typeof t)!function(e,t){for(var i=e.getAttribute&&e.getAttribute("class")||"",s=" "+i+" ",n=0,l=t.split(" "),o=l.length;n-1)&&i.clearAnnouncement("number"==typeof t?t:5e3)}),t)):this.srAnnouncements.textContent=e)},e.prototype.isSelectedElem=function(e){var t=e&&e[c];return!(!this.multiple||"object"!=typeof t)},e.prototype.getSelectedElems=function(){for(var e=[],t=0,i=this.wrapper.childNodes.length;t-1&&this.selected[i]){var r=L(this.selected[i]),l=r.label;F(r.element,!1,this),this.selected.splice(i,1),this.triggerOptionCallback("onDelete",[r]),this.setSourceElementValues(),this.buildMultiSelected(t?i:null),this.triggerAutoGrow(),this.announce(l+" "+this.options.srDeletedText,0)}}},e.prototype.createSelectedElemFrom=function(e,t){var i=e.label,s=this.cssNameSpace,n=s+"__selected",r=document.createElement("span"),l=t?s+"__delete-all "+n+" "+n+"--delete-all":n;return r.setAttribute("aria-describedby",this.ids.LABEL),r.setAttribute("class",l),r.setAttribute("role","button"),r.setAttribute("tabindex","0"),r.textContent=i,t||(r.setAttribute("aria-label",this.options.srDeleteText+" "+i),r[c]=e),r},e.prototype.buildMultiSelected=function(e){var t=this;if(this.multiple){this.multiple&&this.selected.length>=this.options.maxItems?this.disable():this.enable();var i=this.getSelectedElems();if(this.selected.length||i.length){var s=[];i.forEach((function(e){for(var i=e[c],n=0,r=t.selected.length;n=i.length)return this.currentSelectedIndex=i.length-1,void this.setOptionFocus(e,this.currentSelectedIndex);var s=i[t];if(s&&"string"==typeof s.getAttribute("tabindex"))return this.currentSelectedIndex=t,a(s,this.cssNameSpace+"__option--focused focused focus"),s.setAttribute("aria-selected","true"),void s.focus();this.currentSelectedIndex=-1},e.prototype.setSourceElementValues=function(){for(var e=[],t=0,i=this.selected.length;t=this.options.maxItems)&&this.filteredSource.length&&this.filteredSource[t]){for(var s=L(this.filteredSource[t]),n=!1,r=0,l=this.selected.length;r-1||this.indexOfValueIn(this.source,i,"label")>-1)){var n,r=this.sourceFromSelect,l=this.sourceFromCheckboxList;if(r){var o=this.element.querySelector("option"),a=o.cloneNode(!0);a.textContent=i,a.value=s,n=a,o.parentNode.insertBefore(a,o)}else if(l){var h=this.element.querySelector('input[type="checkbox"]'),u=h.cloneNode(!0),c=h.closest("label"),p=document.createElement("label");p.textContent=i,u.value=s,n=u,p.appendChild(u);var d=c||h;d.parentNode.insertBefore(p,d)}n&&(e.element=n,n.removeAttribute("id")),this.source.unshift(e)}}},e.prototype.prependCreatedResultsEntry=function(e){var t=this.options.create;if(I(this.term)&&(!0===t||"function"==typeof t)){var i,s=x(this.term),n=this.options.sourceMapping;if(!0===t&&(i=k(s,n)),"function"==typeof t){var r=t(s),l=typeof r;r&&("string"===l||"object"===l&&!Array.isArray(r))&&(i=k(r,n))}i&&i.label&&i.value&&(this.indexOfValueIn(e,i[u],u)>-1||this.indexOfValueIn(e,i.value,"value")>-1||e.unshift(i))}},e.prototype.setListOptions=function(e){this.prependCreatedResultsEntry(e);var t=this.options.sourceMapping,i=this.removeSelectedFromResults(e),s=this.triggerOptionCallback("onResponse",[i]);this.filteredSource=Array.isArray(s)?R(s,t):i;for(var n,r=this.ids.OPTION,o=this.cssNameSpace,h=o+"__option",u=this.filteredSource.length,c="function"==typeof this.options.onItemRender,p=this.forceShowAll?9999:this.options.maxResults,d=p'+b+"")}f.length?(a(this.list,o+"__list--has-results"),l(this.list,o+"__list--no-results")):(l(this.list,o+"__list--has-results"),a(this.list,o+"__list--no-results"));var y=this.options.noResultsText;!f.length&&"string"==typeof y&&y.length&&(n=y,f.push('
  • '+y+"
  • ")),this.cancelFilterPrep(),n||(n=this.triggerOptionCallback("srResultsText",[d])),n&&this.announce(n);var g=f.join("");if(this.currentListHtml!==g?(this.currentListHtml=g,this.list.innerHTML=g):this.resetOptionAttributes(),!f.length)return this.hide(),void(this.forceShowAll=!1);this.show(),this.forceShowAll=!1},e.prototype.handleAsync=function(e,t){var i=this;void 0===t&&(t=!1),this.xhr&&"function"==typeof this.xhr.abort&&this.xhr.abort();var s=new XMLHttpRequest,n=this.forceShowAll,r=t?null:this.api,l=this.multiple?this.selected.length:0,o=n||t||9999===this.options.maxResults,a=this.source+(/\?/.test(this.source)?"&":"?")+encodeURIComponent(this.options.asyncQueryParam)+"="+encodeURIComponent(e)+"&"+encodeURIComponent(this.options.asyncMaxResultsParam)+"="+(o?9999:l+this.options.maxResults),h=this.triggerOptionCallback("onAsyncPrep",[a,s,t],r);h&&"string"==typeof h&&(a=h),s.open("GET",a),s.onload=function(){if(s.readyState===s.DONE&&s.status>=200&&s.status<300){i.forceShowAll=n;var l=R(i.triggerOptionCallback("onAsyncSuccess",[e,s,t],r)||s.responseText,i.options.sourceMapping);t?(i.prepSelectedFromArray(l),i.setInputStartingStates(!1)):i.setListOptions(l),i.triggerOptionCallback("onAsyncComplete",[e,s,t],r)}},s.onerror=function(){i.triggerOptionCallback("onAsyncError",[e,s,t],r)},t||(this.xhr=s),this.triggerOptionCallback("onAsyncBeforeSend",[e,s,t],r),s.send()},e.prototype.filter=function(e){var t=this;if("string"==typeof e){var i=this.forceShowAll;if(!i){var s=this.triggerOptionCallback("onSearch",[e]);"string"==typeof s&&(e=s)}if(this.term=e,"string"==typeof this.source&&this.source.length)return this.handleAsync(e),void(this.forceShowAll=!1);if("function"!=typeof this.source){e||(i=!0);var n=[],r=this.source;if(r&&r.length){var l=[u];if(!i){e=I(e,!0);var o=this.options.alsoSearchIn;Array.isArray(o)&&o.length&&(l=function(e){var t=[];return e.forEach((function(e){if("string"==typeof e){for(var i=x(e),s="label"!==i,n=0,r=t.length;s&&n-1&&D(n,t,s))return!0}return!1}(t,e,l))&&n.push(t)}))}this.setListOptions(n)}else{var a=function(e){var i=R(e,t.options.sourceMapping);t.setListOptions(i)},h=this.source.call(this.api,this.term,a,!1);h&&"function"==typeof h.then&&h.then((function(e){return a(e)}))}}else this.cancelFilterPrep()},e.prototype.cancelFilterPrep=function(){clearTimeout(this.filterTimer),l(this.wrapper,this.cssNameSpace+"__wrapper--loading loading"),l(this.input,this.cssNameSpace+"__input--loading loading"),this.filtering=!1},e.prototype.filterPrep=function(e,t,i){var s=this;void 0===t&&(t=!1),void 0===i&&(i=!1);var n=this.forceShowAll,r=n||i?0:this.options.delay;this.cancelFilterPrep(),this.filtering=!0,this.filterTimer=setTimeout((function(){var i=s.input.value;if(s.inputPollingValue=i,(n||""===i||t&&!s.multiple&&s.selected.length&&x(s.selected[0].label)===x(i))&&(i=""),!n&&i.length=t)&&this.filterPrep(e)}if(this.menuOpen&&!this.filtering){var i=this.currentSelectedIndex;"number"!=typeof i||i<0?this.setOptionFocus(e,0):this.setOptionFocus(e,i+1)}},e.prototype.handleEndKey=function(e){if(!this.disabled&&this.menuOpen&&e.target!==this.input){var t=P(this.list);t.length&&(e.preventDefault(),this.setOptionFocus(e,t.length-1))}},e.prototype.handleHomeKey=function(e){!this.disabled&&this.menuOpen&&e.target!==this.input&&(e.preventDefault(),this.setOptionFocus(e,0))},e.prototype.handleEnterKey=function(e){var t=e.target;this.isSelectedElem(t)?this.removeEntryFromSelected(t[c],!0):this.deleteAll&&t===this.deleteAll?this.deleteAllSelected():this.disabled||(this.showAll&&t===this.showAll?this.filterPrepShowAll(e):(this.menuOpen&&(e.preventDefault(),this.currentSelectedIndex>-1&&this.handleOptionSelect(e,this.currentSelectedIndex)),t===this.input&&this.filterPrep(e,!1,!0)))},e.prototype.handleKeyDownDefault=function(e){var t=e.keyCode,i=e.target===this.input;if(t===m&&!i||this.isSelectedElem(e.target)&&t===A)return e.preventDefault(),void this.handleEnterKey(e);if(!this.disabled){var s=this.selected&&this.selected.length;this.options.deleteOnBackspace&&t===p&&""===this.input.value&&s&&i&&this.multiple&&this.removeEntryFromSelected(this.selected[s-1]);var n=function(e){return e>=48&&e<=57||e>=65&&e<=90||e>=96&&e<=111||e>=186&&e<=222||32===e||8===e||46===e}(t),r=!i&&n;r&&this.input.focus(),(r||i&&n)&&this.filterPrep(e)}},e.prototype.prepKeyDown=function(e){switch(e.keyCode){case y:this.handleUpKey(e);break;case g:this.handleDownKey(e);break;case v:this.handleEndKey(e);break;case b:this.handleHomeKey(e);break;case d:this.handleEnterKey(e);break;case f:this.handleComponentBlur(e,!0);break;default:this.handleKeyDownDefault(e)}},e.prototype.cancelPolling=function(){clearTimeout(this.pollingTimer)},e.prototype.startPolling=function(){var e=this;this.filtering||this.input.value===this.inputPollingValue||this.filterPrep({}),this.pollingTimer=setTimeout((function(){e.startPolling()}),200)},e.prototype.bindEvents=function(){var e=this;this.wrapper.addEventListener("focusout",(function(t){e.handleComponentBlur(t,!1)})),this.wrapper.addEventListener("focusin",(function(t){e.list.contains(t.target)||(e.currentSelectedIndex=-1),e.isFocused||e.triggerOptionCallback("onFocus",[e.wrapper]),e.isFocused=!0})),this.wrapper.addEventListener("keydown",(function(t){e.prepKeyDown(t)})),this.wrapper.addEventListener("click",(function(t){t.target!==e.wrapper?(e.isSelectedElem(t.target)&&e.removeEntryFromSelected(t.target[c],!0),e.deleteAll&&t.target===e.deleteAll&&e.deleteAllSelected()):e.input.focus()}));var t=this.cssNameSpace+"__wrapper--focused focused focus",i=this.cssNameSpace+"__input--focused focused focus";this.input.addEventListener("blur",(function(){l(e.wrapper,t),l(e.input,i),e.cancelPolling()})),this.input.addEventListener("input",(function(t){document.activeElement===e.input&&e.filterPrep(t)})),this.input.addEventListener("click",(function(t){!e.menuOpen&&e.input.value.length>=e.options.minLength&&e.filterPrep(t,!0)})),this.input.addEventListener("focusin",(function(s){a(e.wrapper,t),a(e.input,i),e.startPolling(),e.disabled||e.menuOpen||e.filterPrep(s,!0)})),this.showAll&&this.showAll.addEventListener("click",(function(t){e.filterPrepShowAll(t)})),this.list.addEventListener("mouseenter",(function(t){e.resetOptionAttributes()})),this.list.addEventListener("click",(function(t){if(t.target!==e.list){var i=P(e.list);if(i.length){var s=i.indexOf(t.target);e.handleOptionSelect(t,s)}}})),this.autoGrow&&(this.inputAutoWidth=new n.a(this.input))},e.prototype.prepListSourceCheckboxes=function(){this.multiple=!0,this.source=[];for(var e=this.element.querySelectorAll('input[type="checkbox"]'),t=0,i=e.length;t1&&(this.options.maxItems=1),this.source=[];for(var t=this.element.querySelectorAll("option"),i=0,s=t.length;i-1&&t.selected.push(e[s])}}))}},e.prototype.prepListSourceArray=function(){this.source=R(this.source,this.options.sourceMapping),this.prepSelectedFromArray(this.source)},e.prototype.prepListSourceAsync=function(){var e=this.element;this.elementIsInput&&e.value&&this.handleAsync(e.value,!0)},e.prototype.prepListSourceFunction=function(){var e=this,t=this.element;if(this.elementIsInput&&t.value){var i=function(t){var i=R(t,e.options.sourceMapping);e.prepSelectedFromArray(i),e.setInputStartingStates(!1)},s=this.source.call(void 0,t.value,i,!0);s&&"function"==typeof s.then&&s.then((function(e){return i(e)}))}},e.prototype.prepListSource=function(){return"function"==typeof this.source?this.prepListSourceFunction():"string"==typeof this.source&&this.source.length?this.prepListSourceAsync():Array.isArray(this.source)&&this.source.length?this.prepListSourceArray():(this.sourceFromSelect="SELECT"===this.element.nodeName,this.sourceFromSelect?this.prepListSourceDdl():(this.sourceFromCheckboxList=!!this.element.querySelector('input[type="checkbox"]'),this.sourceFromCheckboxList?this.prepListSourceCheckboxes():void(this.source=[])))},e.prototype.setInputStartingStates=function(e){if(void 0===e&&(e=!0),e){this.label&&(this.label._ariaAutocompleteLabelOriginallyFor=this.ids.ELEMENT,this.label.setAttribute("for",this.ids.INPUT));var t=this.element.getAttribute("aria-describedby");t&&this.input.setAttribute("aria-describedby",t);var i=this.element.getAttribute("aria-labelledby");i&&this.input.setAttribute("aria-labelledby",i)}this.selected.length&&(this.multiple?(this.buildMultiSelected(),this.triggerAutoGrow()):this.setInputValue(this.selected[0].label||"",!0)),this.element.disabled&&this.disable(!0)},e.prototype.setHtml=function(){var e=this.options,t=this.cssNameSpace,i=e.wrapperClassName?" "+e.wrapperClassName:"",s=['
    '];s.push('

    ');var n=e.name?' name="'+e.name+'"':"",r=e.inputClassName?" "+e.inputClassName:"",l=e.placeholder?' placeholder="'+e.placeholder+'" aria-placeholder="'+e.placeholder+'"':"";s.push('"),e.showAllControl&&s.push('');var o=e.srListLabelText,a=e.listClassName?" "+e.listClassName:"",h=o?' aria-label="'+o+'"':"";s.push('"),s.push('"),s.push("
    "),this.element.insertAdjacentHTML("afterend",s.join(""))},e.prototype.destroy=function(){var e=this;this.label&&this.label._ariaAutocompleteLabelOriginallyFor&&(this.label.setAttribute("for",this.label._ariaAutocompleteLabelOriginallyFor),delete this.label._ariaAutocompleteLabelOriginallyFor),this.documentClickBound&&document.removeEventListener("click",this.documentClick),this.autoGrow&&this.inputAutoWidth&&this.inputAutoWidth.destroy(),this.wrapper.parentNode.removeChild(this.wrapper),delete this.element.ariaAutocomplete,this.show(this.element),clearTimeout(this.filterTimer),clearTimeout(this.pollingTimer),clearTimeout(this.showAllPrepTimer),clearTimeout(this.announcementTimer),clearTimeout(this.componentBlurTimer),clearTimeout(this.clearAnnouncementTimer),clearTimeout(this.elementChangeEventTimer),["list","input","label","element","wrapper","showAll","deleteAll","srAssistance","srAnnouncements"].forEach((function(t){return e[t]=null}))},e.prototype.init=function(e,t){this.selected=[],this.element=e,this.label=document.querySelector('[for="'+this.element.id+'"]'),this.ids=new E(this.element.id,this.label?this.label.id:null,t.id),this.elementIsInput="INPUT"===e.nodeName,this.options=new h(t),this.label&&!this.label.id&&(this.label.id=this.ids.LABEL),this.source=this.options.source,this.multiple=this.options.multiple,this.autoGrow=this.options.autoGrow,this.cssNameSpace=this.options.cssNameSpace,this.documentClick=this.handleComponentBlur.bind(this),this.setHtml(),this.list=document.getElementById(this.ids.LIST),this.input=document.getElementById(this.ids.INPUT),this.wrapper=document.getElementById(this.ids.WRAPPER),this.showAll=document.getElementById(this.ids.BUTTON),this.srAssistance=document.getElementById(this.ids.SR_ASSISTANCE),this.srAnnouncements=document.getElementById(this.ids.SR_ANNOUNCEMENTS),this.prepListSource();var i=[];this.options.showAllControl&&i.push(this.cssNameSpace+"__wrapper--show-all"),this.autoGrow&&i.push(this.cssNameSpace+"__wrapper--autogrow"),this.multiple&&i.push(this.cssNameSpace+"__wrapper--multiple"),i.length&&a(this.wrapper,i.join(" ")),this.hide(this.list),this.hide(this.element),this.setInputStartingStates(),this.bindEvents(),this.api=new S(this),this.triggerOptionCallback("onReady",[this.wrapper])},e}();function M(e,t){return e&&e.ariaAutocomplete&&e.ariaAutocomplete.open?e.ariaAutocomplete:new B(e,t).api}t.default=M}])})); \ No newline at end of file diff --git a/dist/index.d.ts b/dist/index.d.ts index 49e27d5..27a5807 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -1,9 +1,10 @@ export interface IAriaAutocompleteOptions { id?: string; name?: string; - source?: string | string[] | any[] | Function; + source?: string | string[] | any[] | Function | Promise; sourceMapping?: any; alsoSearchIn?: string[]; + create?: boolean | ((value: string) => string | any); delay?: number; minLength?: number; maxResults?: number; diff --git a/index.html b/index.html index 70f5991..7601504 100644 --- a/index.html +++ b/index.html @@ -254,7 +254,7 @@

    1. Array as Source, using default options

    2. Progressive Enhancement, using Element(s) as Source

    -

    (With multi-select, autogrow, results limit, and show all control)

    +

    (With result creation, multi-select, autogrow, results limit, and show all control)

    element as the source - if (!this.selected.length && this.elementIsSelect) { + if (!this.selected.length && this.sourceFromSelect) { (this.element as HTMLSelectElement).value = ''; } @@ -641,6 +646,8 @@ export default class Autocomplete { // (re)set values of any DOM elements based on selected array if (!alreadySelected) { + // add entry to the DOM if necessary + this.addResultsEntryToDomAndSource(option); this.selected.push(option); this.setSourceElementValues(); // rebuild multi-selected if needed @@ -686,13 +693,135 @@ export default class Autocomplete { return toReturn; } + /** + * in create mode, if source options were from a dropdown or checkboxlist, + * append the chosen option at list start and update internal source + * @todo: confirm performance and cloned result is as expected in IE9+ + */ + addResultsEntryToDomAndSource(option: any) { + const { create } = this.options; + + // better safe than sorry... + // only applies to create mode, and if the option has a value + // if the source is an endpoint, or function, we can't update it or the DOM + if (!option || !option.value || !create || !Array.isArray(this.source)) { + return; + } + + // if a matching source entry already exists, it does not need to be added; + // use this check to assume a matching element already exists in the DOM as well for performance + // so that we don't need to do any DOM interrogation + const { label, value } = option; + if ( + this.indexOfValueIn(this.source, value, 'value') > -1 || + this.indexOfValueIn(this.source, label, 'label') > -1 + ) { + return; + } + + let element: HTMLOptionElement | HTMLInputElement; + const { sourceFromSelect, sourceFromCheckboxList } = this; + + // dropdown list case + if (sourceFromSelect) { + const existingOption: HTMLOptionElement = this.element.querySelector('option'); + const newOption = existingOption.cloneNode(true) as HTMLOptionElement; + newOption.textContent = label; + newOption.value = value; + element = newOption; + // insert the new option at the beginning of the list + existingOption.parentNode.insertBefore(newOption, existingOption); + } + // checkboxlist case + else if (sourceFromCheckboxList) { + const existingCheckbox = this.element.querySelector('input[type="checkbox"]'); + const newCheckbox = existingCheckbox.cloneNode(true) as HTMLInputElement; + const existingLabel = existingCheckbox.closest('label'); + const newLabel = document.createElement('label'); + newLabel.textContent = label; + newCheckbox.value = value; + element = newCheckbox; + // if the detected existing checkbox in the list had a label parent, + // insert the new label as a sibling, otherwise just insert next to checkbox + newLabel.appendChild(newCheckbox); + const insertNextTo = existingLabel || existingCheckbox; + insertNextTo.parentNode.insertBefore(newLabel, insertNextTo); + } + + // add the element to the option so that it is correctly updated + // within the `setSourceElementValues` method + if (element) { + option.element = element; + // for safety, remove the cloned element id to prevent duplicates + element.removeAttribute('id'); + } + + // update the `source` array so that the option will be available again + // if it's deleted from the selected list; + // place at the beginning to take precedence over existing options + this.source.unshift(option); + } + + /** + * when `create` option is true, or a function that returns a value, + * add an entry to the results for the current search term + */ + prependCreatedResultsEntry(results: any[]) { + const { create } = this.options; + const cleanedValue = cleanString(this.term); + + // if the option is falsy or not a function, or the search value is empty, do nothing + if (!cleanedValue || !(create === true || typeof create === 'function')) { + return; + } + + let entryToAdd: any; + const trimmedValue = trimString(this.term); + const { sourceMapping: mapping } = this.options; + + // simple entry creation when set to true, based on the trimmed value (not cleaned value) + if (create === true) { + entryToAdd = processSourceEntry(trimmedValue, mapping); + } + + // when function, check the result first... + if (typeof create === 'function') { + const result = create(trimmedValue); + const resultType = typeof result; + // check that the result was a string or object + // if devs want to add multiple entries, they can use the `onResponse` callback + if (result && (resultType === 'string' || (resultType === 'object' && !Array.isArray(result)))) { + entryToAdd = processSourceEntry(result, mapping); + } + } + + // only add it if there's actually something to add + if (!entryToAdd || !entryToAdd.label || !entryToAdd.value) { + return; + } + + // if there's an exact label match in the existing results, give original entry precedence + if (this.indexOfValueIn(results, entryToAdd[CLEANED_LABEL_PROP], CLEANED_LABEL_PROP) > -1) { + return; + } + + // also do not proceed if there's an exact value match in the original results + if (this.indexOfValueIn(results, entryToAdd.value, 'value') > -1) { + return; + } + + // finally, add the entry by modifying the original array + results.unshift(entryToAdd); + } + /** * final filtering and render for list options + * @todo add handling for disabled results */ setListOptions(results: any[]) { - const toShow: string[] = []; + this.prependCreatedResultsEntry(results); // now commit to setting the filtered source - const mapping: any = this.options.sourceMapping; + const { sourceMapping: mapping } = this.options; // if in multiple mode, exclude items already in the selected array const updated: any[] = this.removeSelectedFromResults(results); // allow callback to alter the response before rendering @@ -708,6 +837,7 @@ export default class Autocomplete { const maxResults: number = this.forceShowAll ? 9999 : this.options.maxResults; const lengthToUse: number = maxResults < length ? maxResults : length; + const toShow: string[] = []; for (let i = 0; i < lengthToUse; i += 1) { const thisSource: any = this.filteredSource[i]; const callbackResponse = checkCallback && this.triggerOptionCallback('onItemRender', [thisSource]); @@ -730,7 +860,7 @@ export default class Autocomplete { // no results text handling let announce: string; - const noText: string = this.options.noResultsText; + const { noResultsText: noText } = this.options; if (!toShow.length && typeof noText === 'string' && noText.length) { announce = noText; toShow.push(`
  • ${noText}
  • `); @@ -809,7 +939,7 @@ export default class Autocomplete { this.forceShowAll = isShowAll; const response = this.triggerOptionCallback('onAsyncSuccess', [value, xhr, isFirstCall], context); const source = response || xhr.responseText; - const items: any[] = processSourceArray(source, this.options.sourceMapping, false); + const items: any[] = processSourceArray(source, this.options.sourceMapping); if (isFirstCall) { this.prepSelectedFromArray(items); @@ -889,18 +1019,19 @@ export default class Autocomplete { // build up results from static list const toReturn: any[] = []; - if (this.source && this.source.length) { + const source = this.source as any[]; + if (source && source.length) { // build up array of source entry props to search in let toCheck: string[] = [CLEANED_LABEL_PROP]; if (!forceShowAll) { value = cleanString(value, true); - const searchIn: string[] = this.options.alsoSearchIn; + const { alsoSearchIn: searchIn } = this.options; if (Array.isArray(searchIn) && searchIn.length) { toCheck = prepSearchInArray(toCheck.concat(searchIn)); } } // include everything in forceShowAll case - (this.source as any[]).forEach((entry: any) => { + source.forEach((entry: any) => { if (forceShowAll || searchSourceEntryFor(entry, value, toCheck)) { toReturn.push(entry); } @@ -1037,7 +1168,7 @@ export default class Autocomplete { let toUse: number = this.currentSelectedIndex; if (typeof toUse !== 'number' || toUse === -1) { // otherwise check for exact match between current input value and available items - toUse = this.indexOfValueIn.call(this, this.filteredSource); + toUse = this.indexOfValueIn.call(this, this.filteredSource, this.term, 'label'); } this.handleOptionSelect({}, toUse, false); } @@ -1051,7 +1182,7 @@ export default class Autocomplete { if (this.selected.length) { this.removeEntryFromSelected(this.selected[0]); } - const inputOrDdl: boolean = this.elementIsInput || this.elementIsSelect; + const inputOrDdl: boolean = this.elementIsInput || this.sourceFromSelect; const originalElement = this.element as HTMLInputElement | HTMLSelectElement; if (inputOrDdl && originalElement.value !== '') { originalElement.value = ''; @@ -1093,8 +1224,9 @@ export default class Autocomplete { event.preventDefault(); // if closed, and text is long enough, run search if (!this.menuOpen) { - this.forceShowAll = this.options.minLength < 1; - if (this.forceShowAll || this.input.value.length >= this.options.minLength) { + const { minLength } = this.options; + this.forceShowAll = minLength < 1; + if (this.forceShowAll || this.input.value.length >= minLength) { this.filterPrep(event); } } @@ -1204,7 +1336,7 @@ export default class Autocomplete { this.multiple ) { this.removeEntryFromSelected(this.selected[selectedLength - 1]); - return; + // do not return here, to allow the search results to update } // any printable character not on input, return focus to input @@ -1378,20 +1510,18 @@ export default class Autocomplete { if (!checkbox.value) { continue; } - const toPush: any = { element: checkbox, value: checkbox.value }; + const entry: any = { value: checkbox.value }; // label searching let checkboxLabelElem: HTMLElement = checkbox.closest('label'); if (!checkboxLabelElem && checkbox.id) { checkboxLabelElem = document.querySelector('[for="' + checkbox.id + '"]'); } if (checkboxLabelElem) { - toPush.label = checkboxLabelElem.textContent; - } - // if no label so far, re-use value - if (!toPush.label) { - toPush.label = toPush.value; + entry.label = checkboxLabelElem.textContent; } - toPush[CLEANED_LABEL_PROP] = cleanString(toPush.label); + // if there was no label, `processSourceEntry` will re-use the value + const toPush = processSourceEntry(entry); + toPush.element = checkbox; this.source.push(toPush); // add to selected if applicable if (checkbox.checked) { @@ -1423,12 +1553,8 @@ export default class Autocomplete { if (!option.value) { continue; } - const toPush: any = { - element: option, - value: option.value, - label: option.textContent, - }; - toPush[CLEANED_LABEL_PROP] = cleanString(toPush.label); + const toPush = processSourceEntry({ value: option.value, label: option.textContent }); + toPush.element = option; this.source.push(toPush); // add to selected if applicable if (option.selected) { @@ -1444,8 +1570,7 @@ export default class Autocomplete { const value = this.elementIsInput && (this.element as HTMLInputElement).value; if (value && source && source.length) { // account for multiple mode - const multiple: boolean = this.options.multiple; - const separator: string = this.options.multipleSeparator; + const { multiple, multipleSeparator: separator } = this.options; const valueArr: string[] = multiple ? value.split(separator) : [value]; valueArr.forEach((val: string) => { @@ -1520,14 +1645,19 @@ export default class Autocomplete { } // dropdown source - if (this.elementIsSelect) { + this.sourceFromSelect = this.element.nodeName === 'SELECT'; + if (this.sourceFromSelect) { return this.prepListSourceDdl(); } // checkboxlist source - if (this.element.querySelector('input[type="checkbox"]')) { - this.prepListSourceCheckboxes(); + this.sourceFromCheckboxList = !!this.element.querySelector('input[type="checkbox"]'); + if (this.sourceFromCheckboxList) { + return this.prepListSourceCheckboxes(); } + + // defensive fallback + this.source = []; } /** @@ -1580,6 +1710,14 @@ export default class Autocomplete { const wrapperClass = o.wrapperClassName ? ` ${o.wrapperClassName}` : ''; const newHtml = [`
    `]; + // add element for added screen reader announcements + // added before the main input, so that if screen reader users navigate past the input + // they will not encounter this element out of context + newHtml.push( + `

    ` + ); + // add input const name = o.name ? ` name="${o.name}"` : ``; const inputClass = o.inputClassName ? ` ${o.inputClassName}` : ''; @@ -1615,12 +1753,6 @@ export default class Autocomplete { // add the screen reader assistance element newHtml.push(``); - // add element for added screen reader announcements - newHtml.push( - `

    ` - ); - // close all and append newHtml.push(`
    `); this.element.insertAdjacentHTML('afterend', newHtml.join('')); @@ -1685,7 +1817,6 @@ export default class Autocomplete { this.label = document.querySelector('[for="' + this.element.id + '"]'); this.ids = new AutocompleteIds(this.element.id, this.label ? this.label.id : null, options.id); this.elementIsInput = element.nodeName === 'INPUT'; - this.elementIsSelect = element.nodeName === 'SELECT'; this.options = new AutocompleteOptions(options); // ensure label has an id, for use in `aria-describedby` attributes later diff --git a/webpack.config.js b/webpack.config.js index 359837c..4ca3441 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -6,16 +6,16 @@ module.exports = { { test: /\.tsx?$/, use: 'ts-loader', - exclude: /node_modules/ - } - ] + exclude: /node_modules/, + }, + ], }, resolve: { - extensions: ['.tsx', '.ts', '.js'] + extensions: ['.tsx', '.ts', '.js'], }, output: { libraryTarget: 'umd', path: __dirname + '/dist', - filename: 'aria-autocomplete.min.js' - } -}; + filename: 'aria-autocomplete.min.js', + }, +}; \ No newline at end of file