From 6334c399971849ecb21bab4c239d1a7223f62387 Mon Sep 17 00:00:00 2001 From: Tomas Kirda Date: Wed, 20 May 2026 11:34:46 -0500 Subject: [PATCH] fix: fire onSearchComplete before onSelect when auto-selecting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a search returns a single suggestion whose value exactly matches the current query and `triggerSelectOnValidInput` is true (the default), `suggest()` calls `select(0)` which fires `onSelect`. Previously, the three response paths (function lookup, cached/local, ajax) all fired `onSearchComplete` after `suggest()`, so consumers received the events in the wrong order — onSelect before onSearchComplete — making any cleanup logic in onSearchComplete clobber state set during onSelect. Move `onSearchComplete` ahead of `suggest()` (and `processResponse()` on the ajax path, which contains `suggest()`) so the events fire in the natural order: search-complete, then any resulting select. Closes #852. Co-Authored-By: Claude Opus 4.7 (1M context) --- dist/jquery.autocomplete.esm.js | 6 +++--- dist/jquery.autocomplete.js | 6 +++--- dist/jquery.autocomplete.min.js | 2 +- src/Autocomplete.ts | 8 +++++--- test/autocomplete.test.js | 21 +++++++++++++++++++++ 5 files changed, 33 insertions(+), 10 deletions(-) diff --git a/dist/jquery.autocomplete.esm.js b/dist/jquery.autocomplete.esm.js index 6156385..5f9570c 100644 --- a/dist/jquery.autocomplete.esm.js +++ b/dist/jquery.autocomplete.esm.js @@ -400,8 +400,8 @@ var _Autocomplete = class _Autocomplete { if (typeof options.lookup === "function") { options.lookup(q, (data) => { this.suggestions = data.suggestions; - this.suggest(); options.onSearchComplete.call(this.element, q, data.suggestions); + this.suggest(); }); return; } @@ -416,8 +416,8 @@ var _Autocomplete = class _Autocomplete { } if (response && Array.isArray(response.suggestions)) { this.suggestions = response.suggestions; - this.suggest(); options.onSearchComplete.call(this.element, q, response.suggestions); + this.suggest(); } else if (!this.isBadQuery(q)) { this.abortAjax(); const ajaxSettings = { @@ -430,8 +430,8 @@ var _Autocomplete = class _Autocomplete { this.currentRequest = $.ajax(ajaxSettings).done((data) => { this.currentRequest = null; const result = options.transformResult(data, q); - this.processResponse(result, q, cacheKey); options.onSearchComplete.call(this.element, q, result.suggestions); + this.processResponse(result, q, cacheKey); }).fail((jqXHR, textStatus, errorThrown) => { options.onSearchError.call(this.element, q, jqXHR, textStatus, errorThrown); }); diff --git a/dist/jquery.autocomplete.js b/dist/jquery.autocomplete.js index 05c89a8..2e143dc 100644 --- a/dist/jquery.autocomplete.js +++ b/dist/jquery.autocomplete.js @@ -408,8 +408,8 @@ if (typeof options.lookup === "function") { options.lookup(q, (data) => { this.suggestions = data.suggestions; - this.suggest(); options.onSearchComplete.call(this.element, q, data.suggestions); + this.suggest(); }); return; } @@ -424,8 +424,8 @@ } if (response && Array.isArray(response.suggestions)) { this.suggestions = response.suggestions; - this.suggest(); options.onSearchComplete.call(this.element, q, response.suggestions); + this.suggest(); } else if (!this.isBadQuery(q)) { this.abortAjax(); const ajaxSettings = { @@ -438,8 +438,8 @@ this.currentRequest = $2.ajax(ajaxSettings).done((data) => { this.currentRequest = null; const result = options.transformResult(data, q); - this.processResponse(result, q, cacheKey); options.onSearchComplete.call(this.element, q, result.suggestions); + this.processResponse(result, q, cacheKey); }).fail((jqXHR, textStatus, errorThrown) => { options.onSearchError.call(this.element, q, jqXHR, textStatus, errorThrown); }); diff --git a/dist/jquery.autocomplete.min.js b/dist/jquery.autocomplete.min.js index 6a03616..231a9b3 100644 --- a/dist/jquery.autocomplete.min.js +++ b/dist/jquery.autocomplete.min.js @@ -15,6 +15,6 @@ factory(jQuery); } })(function ($) { -"use strict";(()=>{var l=null;function C(r){l=r}var v={escapeRegExChars(r){return r.replace(/[|\\{}()[\]^$+*?.]/g,"\\$&")},createNode(r){let t=document.createElement("div");return t.className=r,t.style.position="absolute",t.style.display="none",t}},h={ESC:27,TAB:9,RETURN:13,LEFT:37,UP:38,RIGHT:39,DOWN:40};function x(r,t,e){return r.value.toLowerCase().indexOf(e)!==-1}function T(r){return typeof r=="string"?JSON.parse(r):r}function w(r,t){if(!t)return r.value;let e="("+v.escapeRegExChars(t)+")";return r.value.replace(new RegExp(e,"gi"),"$1").replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/<(\/?strong)>/g,"<$1>")}function R(r,t){return'
'+t+"
"}var b=()=>{},A={ajaxSettings:{},autoSelectFirst:!1,appendTo:"body",width:"auto",minChars:1,maxHeight:300,deferRequestBy:0,params:{},formatResult:w,formatGroup:R,zIndex:9999,type:"GET",noCache:!1,onSearchStart:b,onSearchComplete:b,onSearchError:b,preserveInput:!1,containerClass:"autocomplete-suggestions",tabDisabled:!1,dataType:"text",triggerSelectOnValidInput:!0,preventBadQueries:!0,lookupFilter:x,paramName:"query",transformResult:T,showNoSuggestionNotice:!1,noSuggestionNotice:"No results",orientation:"bottom",forceFixPosition:!1};var p=class p{constructor(t,e){this.suggestions=[];this.badQueries=[];this.selectedIndex=-1;this.cachedResponse={};this.onChangeTimeout=null;this.isLocal=!1;this.classes={selected:"autocomplete-selected",suggestion:"autocomplete-suggestion"};this.hint=null;this.hintValue="";this.selection=null;this.currentRequest=null;this.element=t,this.el=l(t),this.currentValue=t.value,this.options=l.extend(!0,{},p.defaults,e),this.initialize(),this.setOptions(e)}initialize(){let t=this,e=`.${this.classes.suggestion}`,s=this.classes.selected,i=this.options;this.element.setAttribute("autocomplete","off"),this.$noSuggestionsContainer=l('
').html(i.noSuggestionNotice),this.noSuggestionsContainer=this.$noSuggestionsContainer.get(0),this.suggestionsContainer=p.utils.createNode(i.containerClass),this.$container=l(this.suggestionsContainer),this.$container.appendTo(i.appendTo||"body"),i.width!=="auto"&&this.$container.css("width",i.width);let o=this.$container;o.on("mouseover.autocomplete",e,function(){t.activate(l(this).data("index"))}),o.on("click.autocomplete",e,function(){t.select(l(this).data("index"))}),o.on("mouseout.autocomplete",()=>{this.selectedIndex=-1,o.children(`.${s}`).removeClass(s)}),o.on("click.autocomplete",()=>{this.blurTimeoutId!==void 0&&clearTimeout(this.blurTimeoutId)}),this.fixPositionCapture=()=>{this.visible&&this.fixPosition()},l(window).on("resize.autocomplete",this.fixPositionCapture),this.el.on("keydown.autocomplete",n=>this.onKeyPress(n)),this.el.on("keyup.autocomplete",n=>this.onKeyUp(n)),this.el.on("blur.autocomplete",()=>this.onBlur()),this.el.on("focus.autocomplete",()=>this.onFocus()),this.el.on("change.autocomplete",n=>this.onKeyUp(n)),this.el.on("input.autocomplete",n=>this.onKeyUp(n))}onFocus(){this.disabled||(this.fixPosition(),this.el.val().length>=this.options.minChars&&this.onValueChange())}onBlur(){let t=this.options,e=this.getQuery(this.el.val());this.blurTimeoutId=setTimeout(()=>{this.hide(),this.selection&&this.currentValue!==e&&t.onInvalidateSelection?.call(this.element)},200)}abortAjax(){this.currentRequest&&(this.currentRequest.abort(),this.currentRequest=null)}setOptions(t){let e={...this.options,...t};this.isLocal=Array.isArray(e.lookup),this.isLocal&&(e.lookup=this.verifySuggestionsFormat(e.lookup)),e.orientation=this.validateOrientation(e.orientation,"bottom"),this.$container.css({"max-height":`${e.maxHeight}px`,width:`${e.width}px`,"z-index":e.zIndex}),this.options=e}clearCache(){this.cachedResponse={},this.badQueries=[]}clear(){this.clearCache(),this.currentValue="",this.suggestions=[]}disable(){this.disabled=!0,this.onChangeTimeout&&clearTimeout(this.onChangeTimeout),this.abortAjax()}enable(){this.disabled=!1}fixPosition(){let t=this.$container,e=t.parent().get(0);if(e!==document.body&&!this.options.forceFixPosition)return;let s=this.options.orientation,i=t.outerHeight()??0,o=this.el.outerHeight()??0,n=this.el.offset()??{top:0,left:0},a={top:n.top,left:n.left};if(s==="auto"){let u=l(window).height()??0,c=l(window).scrollTop()??0,g=-c+n.top-i,y=c+u-(n.top+o+i);s=Math.max(g,y)===g?"top":"bottom"}if(a.top+=s==="top"?-i:o,e!==document.body&&e!==void 0){let u=t.css("opacity");this.visible||t.css("opacity",0).show();let c=t.offsetParent().offset()??{top:0,left:0};a.top-=c.top,a.top+=e.scrollTop,a.left-=c.left,this.visible||t.css("opacity",u).hide()}this.options.width==="auto"&&(a.width=`${this.el.outerWidth()??0}px`),t.css(a)}isCursorAtEnd(){let t=this.el.val().length,{selectionStart:e}=this.element;return typeof e=="number"?e===t:!0}onKeyPress(t){if(!this.disabled&&!this.visible&&t.which===h.DOWN&&this.currentValue){this.suggest();return}if(!(this.disabled||!this.visible)){switch(t.which){case h.ESC:this.el.val(this.currentValue),this.hide();break;case h.RIGHT:if(this.hint&&this.options.onHint&&this.isCursorAtEnd()){this.selectHint();break}return;case h.TAB:if(this.hint&&this.options.onHint){this.selectHint();return}if(this.selectedIndex===-1){this.hide();return}if(this.select(this.selectedIndex),this.options.tabDisabled===!1)return;break;case h.RETURN:if(this.selectedIndex===-1){this.hide();return}this.select(this.selectedIndex);break;case h.UP:this.moveUp();break;case h.DOWN:this.moveDown();break;default:return}t.stopImmediatePropagation(),t.preventDefault()}}onKeyUp(t){this.disabled||t.which===h.UP||t.which===h.DOWN||(this.onChangeTimeout&&clearTimeout(this.onChangeTimeout),this.currentValue!==this.el.val()&&(this.findBestHint(),this.options.deferRequestBy>0?this.onChangeTimeout=setTimeout(()=>this.onValueChange(),this.options.deferRequestBy):this.onValueChange()))}onValueChange(){if(this.ignoreValueChange){this.ignoreValueChange=!1;return}let t=this.options,e=this.el.val(),s=this.getQuery(e);if(this.selection&&this.currentValue!==s&&(this.selection=null,t.onInvalidateSelection?.call(this.element)),this.onChangeTimeout&&clearTimeout(this.onChangeTimeout),this.currentValue=e,this.selectedIndex=-1,t.triggerSelectOnValidInput&&this.isExactMatch(s)){this.select(0);return}s.lengthi(u,t,s));return{suggestions:o&&a.length>o?a.slice(0,o):a}}getSuggestions(t){let e=this.options,s=e.serviceUrl,i,o;if(e.params[e.paramName]=t,e.onSearchStart.call(this.element,e.params)===!1)return;let n=e.ignoreParams?null:e.params;if(typeof e.lookup=="function"){e.lookup(t,a=>{this.suggestions=a.suggestions,this.suggest(),e.onSearchComplete.call(this.element,t,a.suggestions)});return}if(this.isLocal?i=this.getSuggestionsLocal(t):(typeof s=="function"&&(s=s.call(this.element,t)),o=`${s}?${l.param(n??{})}`,i=this.cachedResponse[o]),i&&Array.isArray(i.suggestions))this.suggestions=i.suggestions,this.suggest(),e.onSearchComplete.call(this.element,t,i.suggestions);else if(this.isBadQuery(t))e.onSearchComplete.call(this.element,t,[]);else{this.abortAjax();let a={url:s,data:n??void 0,type:e.type,dataType:e.dataType,...e.ajaxSettings};this.currentRequest=l.ajax(a).done(u=>{this.currentRequest=null;let c=e.transformResult(u,t);this.processResponse(c,t,o),e.onSearchComplete.call(this.element,t,c.suggestions)}).fail((u,c,g)=>{e.onSearchError.call(this.element,t,u,c,g)})}}isBadQuery(t){return this.options.preventBadQueries?this.badQueries.some(e=>t.indexOf(e)===0):!1}hide(){this.options.onHide&&this.visible&&this.options.onHide.call(this.element,this.$container),this.visible=!1,this.selectedIndex=-1,this.onChangeTimeout&&clearTimeout(this.onChangeTimeout),this.$container.hide(),this.onHint(null)}groupSuggestionsByCategory(t,e){let s=new Map;for(let i of t){let o=i.data[e],n=s.get(o);n?n.push(i):s.set(o,[i])}return Array.from(s.values()).flat()}suggest(){if(!this.suggestions.length){this.options.showNoSuggestionNotice?this.noSuggestions():this.hide();return}let t=this.options,{groupBy:e,formatResult:s,beforeRender:i}=t,o=this.getQuery(this.currentValue),n=this.classes.suggestion,a=this.classes.selected,u=this.$container;if(t.triggerSelectOnValidInput&&this.isExactMatch(o)){this.select(0);return}e&&(this.suggestions=this.groupSuggestionsByCategory(this.suggestions,e));let c,g=d=>{let f=d.data[e];return c===f?"":(c=f,t.formatGroup(d,c))},y=this.suggestions.map((d,f)=>`${e?g(d):""}
${s(d,o,f)}
`).join("");this.adjustContainerWidth(),this.$noSuggestionsContainer.detach(),u.html(y),i?.call(this.element,u,this.suggestions),this.fixPosition(),u.show(),t.autoSelectFirst&&(this.selectedIndex=0,u.scrollTop(0),u.children(`.${n}`).first().addClass(a)),this.visible=!0,this.findBestHint()}noSuggestions(){let{beforeRender:t}=this.options,e=this.$container;this.adjustContainerWidth(),this.$noSuggestionsContainer.detach(),e.empty().append(this.$noSuggestionsContainer),t?.call(this.element,e,this.suggestions),this.fixPosition(),e.show(),this.visible=!0}adjustContainerWidth(){let{width:t}=this.options;if(t==="auto"){let e=this.el.outerWidth()??0;this.$container.css("width",e>0?e:300)}else t==="flex"&&this.$container.css("width","")}findBestHint(){let t=this.el.val().toLowerCase();if(!t)return;let e=this.suggestions.find(s=>s.value.toLowerCase().indexOf(t)===0)??null;this.onHint(e)}onHint(t){let{onHint:e}=this.options,s=t?this.currentValue+t.value.substr(this.currentValue.length):"";this.hintValue!==s&&(this.hintValue=s,this.hint=t,e?.call(this.element,s))}verifySuggestionsFormat(t){return t.length&&typeof t[0]=="string"?t.map(e=>({value:e,data:null})):t}validateOrientation(t,e){let s=(t||"").trim().toLowerCase();return s==="auto"||s==="top"||s==="bottom"?s:e}processResponse(t,e,s){let i=this.options;t.suggestions=this.verifySuggestionsFormat(t.suggestions),i.noCache||(this.cachedResponse[s]=t,i.preventBadQueries&&!t.suggestions.length&&this.badQueries.push(e)),e===this.getQuery(this.currentValue)&&(this.suggestions=t.suggestions,this.suggest())}activate(t){let e=this.classes.selected,s=this.$container,i=s.find(`.${this.classes.suggestion}`);if(s.find(`.${e}`).removeClass(e),this.selectedIndex=t,this.selectedIndex!==-1&&i.length>this.selectedIndex){let o=i.get(this.selectedIndex);return l(o).addClass(e),o}return null}selectHint(){this.select(this.suggestions.indexOf(this.hint))}select(t){this.hide(),this.onSelect(t)}moveUp(){if(this.selectedIndex!==-1){if(this.selectedIndex===0){this.$container.children(`.${this.classes.suggestion}`).first().removeClass(this.classes.selected),this.selectedIndex=-1,this.ignoreValueChange=!1,this.el.val(this.currentValue),this.findBestHint();return}this.adjustScroll(this.selectedIndex-1)}}moveDown(){this.selectedIndex!==this.suggestions.length-1&&this.adjustScroll(this.selectedIndex+1)}adjustScroll(t){let e=this.activate(t);if(!e)return;let s=l(e).outerHeight()??0,i=e.offsetTop,o=this.$container,n=o.scrollTop()??0,a=n+this.options.maxHeight-s;ia&&o.scrollTop(i-this.options.maxHeight+s),this.options.preserveInput||(this.ignoreValueChange=!0,this.el.val(this.getValue(this.suggestions[t].value))),this.onHint(null)}onSelect(t){let e=this.options.onSelect,s=this.suggestions[t];this.currentValue=this.getValue(s.value),this.currentValue!==this.el.val()&&!this.options.preserveInput&&this.el.val(this.currentValue),this.onHint(null),this.suggestions=[],this.selection=s,e?.call(this.element,s)}getValue(t){let{delimiter:e}=this.options;if(!e)return t;let s=this.currentValue,i=s.split(e);return i.length===1?t:s.substr(0,s.length-i[i.length-1].length)+t}dispose(){this.el.off(".autocomplete").removeData("autocomplete"),this.fixPositionCapture&&l(window).off("resize.autocomplete",this.fixPositionCapture),this.$container.remove()}};p.defaults=A,p.utils=v;var m=p;var S="autocomplete";function k(r){C(r),r.Autocomplete=m,r.fn.devbridgeAutocomplete=function(t,e){return arguments.length?this.each(function(){let s=r(this),i=s.data(S);typeof t=="string"?i&&typeof i[t]=="function"&&i[t](e):(i&&i.dispose&&i.dispose(),i=new m(this,t),s.data(S,i))}):this.first().data(S)},r.fn.autocomplete||(r.fn.autocomplete=r.fn.devbridgeAutocomplete)}k($);})(); +"use strict";(()=>{var l=null;function C(r){l=r}var v={escapeRegExChars(r){return r.replace(/[|\\{}()[\]^$+*?.]/g,"\\$&")},createNode(r){let t=document.createElement("div");return t.className=r,t.style.position="absolute",t.style.display="none",t}},h={ESC:27,TAB:9,RETURN:13,LEFT:37,UP:38,RIGHT:39,DOWN:40};function x(r,t,e){return r.value.toLowerCase().indexOf(e)!==-1}function T(r){return typeof r=="string"?JSON.parse(r):r}function w(r,t){if(!t)return r.value;let e="("+v.escapeRegExChars(t)+")";return r.value.replace(new RegExp(e,"gi"),"$1").replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/<(\/?strong)>/g,"<$1>")}function R(r,t){return'
'+t+"
"}var b=()=>{},A={ajaxSettings:{},autoSelectFirst:!1,appendTo:"body",width:"auto",minChars:1,maxHeight:300,deferRequestBy:0,params:{},formatResult:w,formatGroup:R,zIndex:9999,type:"GET",noCache:!1,onSearchStart:b,onSearchComplete:b,onSearchError:b,preserveInput:!1,containerClass:"autocomplete-suggestions",tabDisabled:!1,dataType:"text",triggerSelectOnValidInput:!0,preventBadQueries:!0,lookupFilter:x,paramName:"query",transformResult:T,showNoSuggestionNotice:!1,noSuggestionNotice:"No results",orientation:"bottom",forceFixPosition:!1};var p=class p{constructor(t,e){this.suggestions=[];this.badQueries=[];this.selectedIndex=-1;this.cachedResponse={};this.onChangeTimeout=null;this.isLocal=!1;this.classes={selected:"autocomplete-selected",suggestion:"autocomplete-suggestion"};this.hint=null;this.hintValue="";this.selection=null;this.currentRequest=null;this.element=t,this.el=l(t),this.currentValue=t.value,this.options=l.extend(!0,{},p.defaults,e),this.initialize(),this.setOptions(e)}initialize(){let t=this,e=`.${this.classes.suggestion}`,s=this.classes.selected,i=this.options;this.element.setAttribute("autocomplete","off"),this.$noSuggestionsContainer=l('
').html(i.noSuggestionNotice),this.noSuggestionsContainer=this.$noSuggestionsContainer.get(0),this.suggestionsContainer=p.utils.createNode(i.containerClass),this.$container=l(this.suggestionsContainer),this.$container.appendTo(i.appendTo||"body"),i.width!=="auto"&&this.$container.css("width",i.width);let o=this.$container;o.on("mouseover.autocomplete",e,function(){t.activate(l(this).data("index"))}),o.on("click.autocomplete",e,function(){t.select(l(this).data("index"))}),o.on("mouseout.autocomplete",()=>{this.selectedIndex=-1,o.children(`.${s}`).removeClass(s)}),o.on("click.autocomplete",()=>{this.blurTimeoutId!==void 0&&clearTimeout(this.blurTimeoutId)}),this.fixPositionCapture=()=>{this.visible&&this.fixPosition()},l(window).on("resize.autocomplete",this.fixPositionCapture),this.el.on("keydown.autocomplete",n=>this.onKeyPress(n)),this.el.on("keyup.autocomplete",n=>this.onKeyUp(n)),this.el.on("blur.autocomplete",()=>this.onBlur()),this.el.on("focus.autocomplete",()=>this.onFocus()),this.el.on("change.autocomplete",n=>this.onKeyUp(n)),this.el.on("input.autocomplete",n=>this.onKeyUp(n))}onFocus(){this.disabled||(this.fixPosition(),this.el.val().length>=this.options.minChars&&this.onValueChange())}onBlur(){let t=this.options,e=this.getQuery(this.el.val());this.blurTimeoutId=setTimeout(()=>{this.hide(),this.selection&&this.currentValue!==e&&t.onInvalidateSelection?.call(this.element)},200)}abortAjax(){this.currentRequest&&(this.currentRequest.abort(),this.currentRequest=null)}setOptions(t){let e={...this.options,...t};this.isLocal=Array.isArray(e.lookup),this.isLocal&&(e.lookup=this.verifySuggestionsFormat(e.lookup)),e.orientation=this.validateOrientation(e.orientation,"bottom"),this.$container.css({"max-height":`${e.maxHeight}px`,width:`${e.width}px`,"z-index":e.zIndex}),this.options=e}clearCache(){this.cachedResponse={},this.badQueries=[]}clear(){this.clearCache(),this.currentValue="",this.suggestions=[]}disable(){this.disabled=!0,this.onChangeTimeout&&clearTimeout(this.onChangeTimeout),this.abortAjax()}enable(){this.disabled=!1}fixPosition(){let t=this.$container,e=t.parent().get(0);if(e!==document.body&&!this.options.forceFixPosition)return;let s=this.options.orientation,i=t.outerHeight()??0,o=this.el.outerHeight()??0,n=this.el.offset()??{top:0,left:0},a={top:n.top,left:n.left};if(s==="auto"){let u=l(window).height()??0,c=l(window).scrollTop()??0,g=-c+n.top-i,y=c+u-(n.top+o+i);s=Math.max(g,y)===g?"top":"bottom"}if(a.top+=s==="top"?-i:o,e!==document.body&&e!==void 0){let u=t.css("opacity");this.visible||t.css("opacity",0).show();let c=t.offsetParent().offset()??{top:0,left:0};a.top-=c.top,a.top+=e.scrollTop,a.left-=c.left,this.visible||t.css("opacity",u).hide()}this.options.width==="auto"&&(a.width=`${this.el.outerWidth()??0}px`),t.css(a)}isCursorAtEnd(){let t=this.el.val().length,{selectionStart:e}=this.element;return typeof e=="number"?e===t:!0}onKeyPress(t){if(!this.disabled&&!this.visible&&t.which===h.DOWN&&this.currentValue){this.suggest();return}if(!(this.disabled||!this.visible)){switch(t.which){case h.ESC:this.el.val(this.currentValue),this.hide();break;case h.RIGHT:if(this.hint&&this.options.onHint&&this.isCursorAtEnd()){this.selectHint();break}return;case h.TAB:if(this.hint&&this.options.onHint){this.selectHint();return}if(this.selectedIndex===-1){this.hide();return}if(this.select(this.selectedIndex),this.options.tabDisabled===!1)return;break;case h.RETURN:if(this.selectedIndex===-1){this.hide();return}this.select(this.selectedIndex);break;case h.UP:this.moveUp();break;case h.DOWN:this.moveDown();break;default:return}t.stopImmediatePropagation(),t.preventDefault()}}onKeyUp(t){this.disabled||t.which===h.UP||t.which===h.DOWN||(this.onChangeTimeout&&clearTimeout(this.onChangeTimeout),this.currentValue!==this.el.val()&&(this.findBestHint(),this.options.deferRequestBy>0?this.onChangeTimeout=setTimeout(()=>this.onValueChange(),this.options.deferRequestBy):this.onValueChange()))}onValueChange(){if(this.ignoreValueChange){this.ignoreValueChange=!1;return}let t=this.options,e=this.el.val(),s=this.getQuery(e);if(this.selection&&this.currentValue!==s&&(this.selection=null,t.onInvalidateSelection?.call(this.element)),this.onChangeTimeout&&clearTimeout(this.onChangeTimeout),this.currentValue=e,this.selectedIndex=-1,t.triggerSelectOnValidInput&&this.isExactMatch(s)){this.select(0);return}s.lengthi(u,t,s));return{suggestions:o&&a.length>o?a.slice(0,o):a}}getSuggestions(t){let e=this.options,s=e.serviceUrl,i,o;if(e.params[e.paramName]=t,e.onSearchStart.call(this.element,e.params)===!1)return;let n=e.ignoreParams?null:e.params;if(typeof e.lookup=="function"){e.lookup(t,a=>{this.suggestions=a.suggestions,e.onSearchComplete.call(this.element,t,a.suggestions),this.suggest()});return}if(this.isLocal?i=this.getSuggestionsLocal(t):(typeof s=="function"&&(s=s.call(this.element,t)),o=`${s}?${l.param(n??{})}`,i=this.cachedResponse[o]),i&&Array.isArray(i.suggestions))this.suggestions=i.suggestions,e.onSearchComplete.call(this.element,t,i.suggestions),this.suggest();else if(this.isBadQuery(t))e.onSearchComplete.call(this.element,t,[]);else{this.abortAjax();let a={url:s,data:n??void 0,type:e.type,dataType:e.dataType,...e.ajaxSettings};this.currentRequest=l.ajax(a).done(u=>{this.currentRequest=null;let c=e.transformResult(u,t);e.onSearchComplete.call(this.element,t,c.suggestions),this.processResponse(c,t,o)}).fail((u,c,g)=>{e.onSearchError.call(this.element,t,u,c,g)})}}isBadQuery(t){return this.options.preventBadQueries?this.badQueries.some(e=>t.indexOf(e)===0):!1}hide(){this.options.onHide&&this.visible&&this.options.onHide.call(this.element,this.$container),this.visible=!1,this.selectedIndex=-1,this.onChangeTimeout&&clearTimeout(this.onChangeTimeout),this.$container.hide(),this.onHint(null)}groupSuggestionsByCategory(t,e){let s=new Map;for(let i of t){let o=i.data[e],n=s.get(o);n?n.push(i):s.set(o,[i])}return Array.from(s.values()).flat()}suggest(){if(!this.suggestions.length){this.options.showNoSuggestionNotice?this.noSuggestions():this.hide();return}let t=this.options,{groupBy:e,formatResult:s,beforeRender:i}=t,o=this.getQuery(this.currentValue),n=this.classes.suggestion,a=this.classes.selected,u=this.$container;if(t.triggerSelectOnValidInput&&this.isExactMatch(o)){this.select(0);return}e&&(this.suggestions=this.groupSuggestionsByCategory(this.suggestions,e));let c,g=d=>{let f=d.data[e];return c===f?"":(c=f,t.formatGroup(d,c))},y=this.suggestions.map((d,f)=>`${e?g(d):""}
${s(d,o,f)}
`).join("");this.adjustContainerWidth(),this.$noSuggestionsContainer.detach(),u.html(y),i?.call(this.element,u,this.suggestions),this.fixPosition(),u.show(),t.autoSelectFirst&&(this.selectedIndex=0,u.scrollTop(0),u.children(`.${n}`).first().addClass(a)),this.visible=!0,this.findBestHint()}noSuggestions(){let{beforeRender:t}=this.options,e=this.$container;this.adjustContainerWidth(),this.$noSuggestionsContainer.detach(),e.empty().append(this.$noSuggestionsContainer),t?.call(this.element,e,this.suggestions),this.fixPosition(),e.show(),this.visible=!0}adjustContainerWidth(){let{width:t}=this.options;if(t==="auto"){let e=this.el.outerWidth()??0;this.$container.css("width",e>0?e:300)}else t==="flex"&&this.$container.css("width","")}findBestHint(){let t=this.el.val().toLowerCase();if(!t)return;let e=this.suggestions.find(s=>s.value.toLowerCase().indexOf(t)===0)??null;this.onHint(e)}onHint(t){let{onHint:e}=this.options,s=t?this.currentValue+t.value.substr(this.currentValue.length):"";this.hintValue!==s&&(this.hintValue=s,this.hint=t,e?.call(this.element,s))}verifySuggestionsFormat(t){return t.length&&typeof t[0]=="string"?t.map(e=>({value:e,data:null})):t}validateOrientation(t,e){let s=(t||"").trim().toLowerCase();return s==="auto"||s==="top"||s==="bottom"?s:e}processResponse(t,e,s){let i=this.options;t.suggestions=this.verifySuggestionsFormat(t.suggestions),i.noCache||(this.cachedResponse[s]=t,i.preventBadQueries&&!t.suggestions.length&&this.badQueries.push(e)),e===this.getQuery(this.currentValue)&&(this.suggestions=t.suggestions,this.suggest())}activate(t){let e=this.classes.selected,s=this.$container,i=s.find(`.${this.classes.suggestion}`);if(s.find(`.${e}`).removeClass(e),this.selectedIndex=t,this.selectedIndex!==-1&&i.length>this.selectedIndex){let o=i.get(this.selectedIndex);return l(o).addClass(e),o}return null}selectHint(){this.select(this.suggestions.indexOf(this.hint))}select(t){this.hide(),this.onSelect(t)}moveUp(){if(this.selectedIndex!==-1){if(this.selectedIndex===0){this.$container.children(`.${this.classes.suggestion}`).first().removeClass(this.classes.selected),this.selectedIndex=-1,this.ignoreValueChange=!1,this.el.val(this.currentValue),this.findBestHint();return}this.adjustScroll(this.selectedIndex-1)}}moveDown(){this.selectedIndex!==this.suggestions.length-1&&this.adjustScroll(this.selectedIndex+1)}adjustScroll(t){let e=this.activate(t);if(!e)return;let s=l(e).outerHeight()??0,i=e.offsetTop,o=this.$container,n=o.scrollTop()??0,a=n+this.options.maxHeight-s;ia&&o.scrollTop(i-this.options.maxHeight+s),this.options.preserveInput||(this.ignoreValueChange=!0,this.el.val(this.getValue(this.suggestions[t].value))),this.onHint(null)}onSelect(t){let e=this.options.onSelect,s=this.suggestions[t];this.currentValue=this.getValue(s.value),this.currentValue!==this.el.val()&&!this.options.preserveInput&&this.el.val(this.currentValue),this.onHint(null),this.suggestions=[],this.selection=s,e?.call(this.element,s)}getValue(t){let{delimiter:e}=this.options;if(!e)return t;let s=this.currentValue,i=s.split(e);return i.length===1?t:s.substr(0,s.length-i[i.length-1].length)+t}dispose(){this.el.off(".autocomplete").removeData("autocomplete"),this.fixPositionCapture&&l(window).off("resize.autocomplete",this.fixPositionCapture),this.$container.remove()}};p.defaults=A,p.utils=v;var m=p;var S="autocomplete";function k(r){C(r),r.Autocomplete=m,r.fn.devbridgeAutocomplete=function(t,e){return arguments.length?this.each(function(){let s=r(this),i=s.data(S);typeof t=="string"?i&&typeof i[t]=="function"&&i[t](e):(i&&i.dispose&&i.dispose(),i=new m(this,t),s.data(S,i))}):this.first().data(S)},r.fn.autocomplete||(r.fn.autocomplete=r.fn.devbridgeAutocomplete)}k($);})(); }); diff --git a/src/Autocomplete.ts b/src/Autocomplete.ts index 36a445a..e23eb90 100644 --- a/src/Autocomplete.ts +++ b/src/Autocomplete.ts @@ -416,8 +416,10 @@ export class Autocomplete { if (typeof options.lookup === "function") { (options.lookup as LookupCallback)(q, (data) => { this.suggestions = data.suggestions; - this.suggest(); + // Fire onSearchComplete before suggest() so consumers see + // "search complete" before any auto-select fires onSelect. options.onSearchComplete.call(this.element, q, data.suggestions); + this.suggest(); }); return; } @@ -434,8 +436,8 @@ export class Autocomplete { if (response && Array.isArray(response.suggestions)) { this.suggestions = response.suggestions; - this.suggest(); options.onSearchComplete.call(this.element, q, response.suggestions); + this.suggest(); } else if (!this.isBadQuery(q)) { this.abortAjax(); @@ -451,8 +453,8 @@ export class Autocomplete { .done((data) => { this.currentRequest = null; const result = options.transformResult(data, q); - this.processResponse(result, q, cacheKey!); options.onSearchComplete.call(this.element, q, result.suggestions); + this.processResponse(result, q, cacheKey!); }) .fail((jqXHR, textStatus, errorThrown) => { options.onSearchError.call(this.element, q, jqXHR, textStatus, errorThrown); diff --git a/test/autocomplete.test.js b/test/autocomplete.test.js index 12b38e4..fd183d7 100644 --- a/test/autocomplete.test.js +++ b/test/autocomplete.test.js @@ -708,6 +708,27 @@ describe("Autocomplete", () => { }); }); +describe("Autocomplete event ordering", () => { + afterEach(() => { + $(".autocomplete-suggestions").remove(); + }); + + it("fires onSearchComplete before onSelect when a single match auto-selects", () => { + const input = document.createElement("input"); + const calls = []; + const autocomplete = new $.Autocomplete(input, { + lookup: [{ value: "Apple", data: 1 }], + onSearchComplete: () => calls.push("searchComplete"), + onSelect: () => calls.push("select"), + }); + + input.value = "Apple"; + autocomplete.onValueChange(); + + expect(calls).toEqual(["searchComplete", "select"]); + }); +}); + describe("Autocomplete groupBy", () => { afterEach(() => { $(".autocomplete-suggestions").remove();