From cc72c92e342ccecfe6cdc2b57b0d9aedfdfd0af3 Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 5 Sep 2023 16:45:43 +0200 Subject: [PATCH] build min file --- tags.min.js | 2 +- tags.min.js.map | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tags.min.js b/tags.min.js index 26d8c69..e22a753 100644 --- a/tags.min.js +++ b/tags.min.js @@ -1,2 +1,2 @@ -var D={items:[],allowNew:!1,showAllSuggestions:!1,badgeStyle:"primary",allowClear:!1,clearEnd:!1,selected:[],regex:"",separator:[],max:0,clearLabel:"Clear",searchLabel:"Type a value",showDropIcon:!0,keepOpen:!1,allowSame:!1,baseClass:"",placeholder:"",addOnBlur:!1,showDisabled:!1,hideNativeValidation:!1,suggestionsThreshold:-1,maximumItems:0,autoselectFirst:!0,updateOnSelect:!1,highlightTyped:!1,highlightClass:"",fullWidth:!0,fixed:!1,fuzzy:!1,startsWith:!1,singleBadge:!1,activeClasses:["bg-primary","text-white"],labelField:"label",valueField:"value",searchFields:["label"],queryParam:"query",server:"",serverMethod:"GET",serverParams:{},serverDataKey:"data",fetchOptions:{},liveServer:!1,noCache:!0,debounceTime:300,notFoundMessage:"",onRenderItem:(o,t,e)=>t,onSelectItem:(o,t)=>{},onClearItem:(o,t)=>{},onCreateItem:(o,t)=>{},onBlur:(o,t)=>{},onFocus:(o,t)=>{},onCanAdd:(o,t,e)=>{},confirmClear:(o,t)=>Promise.resolve(),confirmAdd:(o,t)=>Promise.resolve(),onServerResponse:(o,t)=>o.json()},v="tags-",M="is-loading",N="is-active",g="is-invalid",H="is-max-reached",L="show",b="data-value",C="next",O="prev",R="form-control-focus",q="form-placeholder-shown",P="form-control-disabled",E=new WeakMap,j=0,T=window.bootstrap&&window.bootstrap.Tooltip;function V(o,t=300){let e;return(...i)=>{clearTimeout(e),e=setTimeout(()=>{o.apply(this,i)},t)}}function W(o,t=null){let e=p("span");document.body.appendChild(e),e.style.fontSize=t||"inherit",e.style.height="auto",e.style.width="auto",e.style.position="absolute",e.style.whiteSpace="no-wrap",e.innerHTML=o;let i=Math.ceil(e.clientWidth);return document.body.removeChild(e),i}function _(o){return o.normalize("NFD").replace(/[\u0300-\u036f]/g,"")}function F(o){return o?_(o.toString()).toLowerCase():""}function U(o,t){if(o.indexOf(t)>=0)return!0;let e=0;for(let i=0;i{this.S(!0)},this.e.debounceTime),this.c=!0,this.k(),this.l=p("div"),this.r=p("div"),this.n=p("ul"),this.s=p("input"),this.l.appendChild(this.r),this.i.parentElement.insertBefore(this.l,this.i),this.D(),this.M(),this.N(),this.H(),this.R(),this.resetState(),this.handleEvent=i=>{this.w(i)},this.e.fixed&&(document.addEventListener("scroll",this,!0),window.addEventListener("resize",this)),["focus","blur","input","keydown"].forEach(i=>{this.s.addEventListener(i,this)}),["mousemove","mouseleave"].forEach(i=>{this.n.addEventListener(i,this)}),this.loadData(!0)}static init(t="select[multiple]",e={}){let i=document.querySelectorAll(t);for(let s=0;s{this.s.removeEventListener(t,this)}),["mousemove","mouseleave"].forEach(t=>{this.n.removeEventListener(t,this)}),this.e.fixed&&(document.removeEventListener("scroll",this,!0),window.removeEventListener("resize",this)),this.i.style.display="block",this.l.parentElement.removeChild(this.l),this.parentForm&&this.parentForm.removeEventListener("reset",this),E.delete(this.i)}handleEvent(t){this.w(t)}w(t){["scroll","resize"].includes(t.type)?(this.A&&window.cancelAnimationFrame(this.A),this.A=window.requestAnimationFrame(()=>{this[`on${t.type}`](t)})):this[`on${t.type}`](t)}T(t={}){this.e=Object.assign({},D,{showDropIcon:!!this.u()});let e=this.i.dataset.config?JSON.parse(this.i.dataset.config):{},i={...t,...e,...this.i.dataset};for(let[s,r]of Object.entries(D)){if(s=="config"||i[s]===void 0)continue;let l=i[s];switch(typeof r){case"number":this.e[s]=parseInt(l);break;case"boolean":this.e[s]=B(l);break;case"string":this.e[s]=l.toString();break;case"object":this.e[s]=l,typeof l=="string"&&(["{","["].includes(l[0])?this.e[s]=JSON.parse(l):this.e[s]=l.split(l.includes("|")?"|":","));break;case"function":this.e[s]=typeof l=="string"?l.split(".").reduce((n,a)=>n[a],window):l,this.e[s]||console.error("Invalid function",l);break;default:this.e[s]=l;break}}this.e.placeholder||(this.e.placeholder=this.q()),this.e.suggestionsThreshold==-1&&(this.e.suggestionsThreshold=this.e.liveServer?1:0)}config(t=null){return t?this.e[t]:this.e}setConfig(t,e){this.e[t]=e}k(){for(this.overflowParent=null,this.parentForm=this.i.parentElement;this.parentForm&&(this.parentForm.style.overflow==="hidden"&&(this.overflowParent=this.parentForm),this.parentForm=this.parentForm.parentElement,!(this.parentForm&&this.parentForm.nodeName=="FORM")););this.parentForm&&this.parentForm.addEventListener("reset",this)}q(){if(this.i.hasAttribute("placeholder"))return this.i.getAttribute("placeholder");if(this.i.dataset.placeholder)return this.i.dataset.placeholder;let t=this.i.querySelector("option");return!t||!this.e.autoselectFirst?"":(S(t,"selected"),t.selected=!1,t.value?"":t.textContent)}N(){let t=this.i;this.e.hideNativeValidation?(t.style.position="absolute",t.style.left="-9999px"):t.style.cssText="height:1px;width:1px;opacity:0;padding:0;margin:0;border:0;float:left;flex-basis:100%;",t.tabIndex=-1,t.addEventListener("focus",e=>{this.onclick(e)}),t.addEventListener("invalid",e=>{this.l.classList.add(g)})}R(){let t=this.n;t.classList.add("dropdown-menu",v+"menu"),t.id=v+"menu-"+j,t.setAttribute("role","menu");let e=t.style;e.padding="0",e.maxHeight="280px",this.e.fullWidth||(e.maxWidth="360px"),this.e.fixed&&(e.position="fixed"),e.overflowY="auto",e.overscrollBehavior="contain",e.textAlign="unset",t.addEventListener("mouseenter",i=>{this.h=!1}),this.l.appendChild(t),this.s.setAttribute("aria-controls",t.id)}D(){let t=this.l;t.classList.add("form-control","dropdown"),["form-select-lg","form-select-sm","is-invalid","is-valid"].forEach(e=>{this.i.classList.contains(e)&&t.classList.add(e)}),this.e.suggestionsThreshold==0&&this.e.showDropIcon&&t.classList.add("form-select"),this.overflowParent&&(t.style.position="inherit"),t.style.height="auto",t.addEventListener("click",this)}M(){this.r.addEventListener("click",e=>{this.isDisabled()||this.s.style.visibility!="hidden"&&this.s.focus()});let t=this.r.style;t.display="flex",t.alignItems="center",t.flexWrap="wrap"}H(){let t=this.s;t.type="text",t.autocomplete="off",t.spellcheck=!1,x(t,{"aria-autocomplete":"list","aria-haspopup":"menu","aria-expanded":"false","aria-label":this.e.searchLabel,role:"combobox"}),t.style.cssText="background-color:transparent;color:currentColor;border:0;padding:0;outline:0;max-width:100%",this.resetSearchInput(!0),this.r.appendChild(t),this.P=window.getComputedStyle(t).direction==="rtl"}onfocus(t){this.l.classList.add(R),this.showOrSearch(),this.e.onFocus(t,this)}onblur(t){this.a&&this.a.abort();let e=!0;if(this.e.addOnBlur&&this.s.value&&(e=this.L()),this.l.classList.remove(R),this.hideSuggestions(e),this.c){let i=this.getSelection(),s={selection:i?i.dataset.value:null,input:this.s.value};this.e.onBlur(t,this),this.i.dispatchEvent(new CustomEvent("tags.blur",{bubbles:!0,detail:s}))}}oninput(t){let e=this.s.value;if(e){let i=e.slice(-1);if(this.e.separator.length&&this.e.separator.includes(i)){this.s.value=this.s.value.slice(0,-1);let s=this.s.value,r=s,l={};if(this.e.allowNew)l.new=1;else{let n=this.getSelection();if(!n)return;s=n.getAttribute(b),r=n.dataset.label}this.e.confirmAdd(s,this).then(()=>{this.p(r,s,l)}).catch(()=>{});return}}setTimeout(()=>{this.o()}),this.showOrSearch()}onkeydown(t){let e=t.keyCode||t.key,i=t.target;switch(t.keyCode==229&&(e=i.value.charAt(i.selectionStart-1).charCodeAt(0)),e){case 13:case"Enter":t.preventDefault(),this.L();break;case 38:case"ArrowUp":t.preventDefault(),this.h=!0,this.m(O);break;case 40:case"ArrowDown":t.preventDefault(),this.h=!0,this.isDropdownVisible()?this.m(C):this.showOrSearch(!1);break;case 8:case"Backspace":let s=this.getLastItem();this.s.value.length==0&&s&&this.e.confirmClear(s,this).then(()=>{this.removeLastItem(),this.o(),this.showOrSearch()}).catch(()=>{});break;case 27:case"Escape":this.s.focus(),this.hideSuggestions();break}}onmousemove(t){this.h=!1}onmouseleave(t){this.removeSelection()}onscroll(t){this.g()}onresize(t){this.g()}onclick(t=null){t&&t.preventDefault(),!(!this.isSingle()&&this.isMaxReached())&&this.s.focus()}onreset(t){this.reset()}loadData(t=!1){Object.keys(this.e.items).length>0?this.setData(this.e.items,!0):this.resetSuggestions(!0),this.e.server&&(this.e.liveServer||this.S(!t))}j(){let t=this.i.selectedOptions||[];for(let e=0;e({value:s.getAttribute("value"),label:s.textContent,disabled:s.disabled,selected:s.selected,data:Object.assign({disabled:s.disabled},s.dataset)}),i=Array.from(this.i.children).filter(s=>s.hasAttribute("label")||!s.disabled||this.e.showDisabled).map(s=>s.hasAttribute("label")?{group:s.getAttribute("label"),items:Array.from(s.children).map(r=>e(r))}:e(s));this.setData(i,t)}L(){let t=this.getSelection();if(t)return t.click(),!0;if(this.e.allowNew&&this.s.value){let e=this.s.value;return this.e.confirmAdd(e,this).then(()=>{this.p(e,e,{new:1})}).catch(()=>{}),!0}return!1}S(t=!1){this.a&&this.a.abort(),this.a=new AbortController;let e=this.i.dataset.serverParams||{};typeof e=="string"&&(e=JSON.parse(e));let i=Object.assign({},this.e.serverParams,e);if(i[this.e.queryParam]=this.s.value,this.e.noCache&&(i.t=Date.now()),i.related){let n=document.getElementById(i.related);if(n){i.related=n.value;let a=n.getAttribute("name");a&&(i[a]=n.value)}}let s=new URLSearchParams(i),r=this.e.server,l=Object.assign(this.e.fetchOptions,{method:this.e.serverMethod||"GET",signal:this.a.signal});l.method==="POST"?l.body=s:r+="?"+s.toString(),this.l.classList.add(M),fetch(r,l).then(n=>this.e.onServerResponse(n,this)).then(n=>{let a=n[this.e.serverDataKey]||n;this.setData(a,!t),this.a=null,t&&this.b()}).catch(n=>{n.name==="AbortError"||this.a.signal.aborted||console.error(n)}).finally(n=>{this.l.classList.remove(M)})}p(t,e=null,i={}){if(!this.canAdd(t,i))return null;let s=this.addItem(t,e,i);return this.v(),this.e.keepOpen?this.b():this.resetSearchInput(),s}f(t){if(t.style.display==="none")return!1;let e=t.firstElementChild;return e.tagName==="A"&&!e.classList.contains("disabled")}m(t=C,e=null){let i=this.getSelection();if(i){let s=t===C?"nextSibling":"previousSibling";e=i.parentNode;do e=e[s];while(e&&!this.f(e));e?i.classList.remove(...this.d()):i&&(e=i.parentElement)}else{if(t===O)return e;if(!e)for(e=this.n.firstChild;e&&!this.f(e);)e=e.nextSibling}if(e){let s=e.offsetHeight,r=e.offsetTop,l=e.parentNode,n=l.offsetHeight,a=l.scrollHeight,h=l.offsetTop;if(s===0&&setTimeout(()=>{l.scrollTop=0}),t===O){let f=r-h>10?r-h:0;l.scrollTop=f}else r+s-(n+l.scrollTop)>0&&s>0&&(l.scrollTop=r+s-n+1,l.scrollTop+n>=a-10&&(l.scrollTop=r-h));let c=e.querySelector("a");c.classList.add(...this.d()),this.s.setAttribute("aria-activedescendant",c.id),this.e.updateOnSelect&&(this.s.value=c.dataset.label,this.o())}else this.s.setAttribute("aria-activedescendant","");return e}o(){this.l.classList.remove(q),this.s.value?this.s.size=this.s.value.length:this.getSelectedValues().length?(this.s.placeholder="",this.s.size=1):(this.s.size=this.e.placeholder.length>0?this.e.placeholder.length:1,this.s.placeholder=this.e.placeholder,this.l.classList.add(q));let t=this.s.value||this.s.placeholder,e=window.getComputedStyle(this.l).fontSize,i=W(t,e)+16;this.s.style.width=i+"px"}z(t){for(;this.n.lastChild;)this.n.removeChild(this.n.lastChild);let e=0,i=1;for(let s=0;s',this.n.appendChild(s)}}C(t,e){if(!t[this.e.valueField])return;let i=t[this.e.valueField],s=t[this.e.labelField],r=this.e.onRenderItem(t,s,this),l=p("li");l.setAttribute("role","presentation"),t.group_id&&l.setAttribute("data-group-id",""+t.group_id);let n=p("a");l.append(n),n.id=this.n.id+"-"+e,n.classList.add("dropdown-item","text-truncate"),t.disabled&&n.classList.add("disabled"),n.setAttribute(b,i),n.dataset.label=s,this.e.searchFields.forEach(a=>{l.dataset[a]=t[a]}),n.setAttribute("href","#"),n.innerHTML=r,this.n.appendChild(l),n.addEventListener("mouseenter",a=>{this.h||(this.removeSelection(),l.querySelector("a").classList.add(...this.d()))}),n.addEventListener("mousedown",a=>{a.preventDefault()}),n.addEventListener("click",a=>{a.preventDefault(),a.stopPropagation(),this.e.confirmAdd(i,this).then(()=>{this.p(s,i,t.data),this.e.onSelectItem(t,this)}).catch(()=>{})})}initialOptions(){return this.i.querySelectorAll("option[data-init]")}E(){this.i.querySelectorAll("option").forEach(t=>{S(t,"selected")})}reset(){this.removeAll(),this.c=!1;let t=this.initialOptions();this.E();for(let e=0;ee.value)}getAvailableValues(){let t=this.i.querySelectorAll("option");return Array.from(t).map(e=>e.value)}showOrSearch(t=!0){if(t&&!this.I()){this.hideSuggestions(!1);return}this.e.liveServer?this.F():this.b()}hideSuggestions(t=!0){this.n.classList.remove(L),x(this.s,{"aria-expanded":"false"}),this.removeSelection(),t&&this.l.classList.remove(g)}toggleSuggestions(t=!0,e=!0){this.n.classList.contains(L)?this.hideSuggestions(e):this.showOrSearch(t)}I(){return this.isDisabled()||this.isMaxReached()?!1:this.s.value.length>=this.e.suggestionsThreshold}b(){if(document.activeElement!=this.s||this.s.style.visibility=="hidden")return;let t=F(this.s.value),e={},i=this.n.querySelectorAll("li"),s=0,r=null,l=!1,n={};for(let a=0;a0&&this.e.searchFields.forEach(u=>{let m=F(c.dataset[u]),A=!1;if(this.e.fuzzy)A=U(m,t);else{let k=m.indexOf(t);A=this.e.startsWith?k===0:k>=0}A&&(d=!0)});let w=d||t.length===0;if(f||d?(s++,z(h),h.dataset.groupId&&(n[h.dataset.groupId]=!0),!r&&this.f(h)&&w&&(r=h),this.e.maximumItems>0&&s>this.e.maximumItems&&I(h)):I(h),this.e.highlightTyped){let u=c.textContent,m=F(u).indexOf(t),A=u.substring(0,m)+`${u.substring(m,m+t.length)}`+u.substring(m+t.length,u.length);c.innerHTML=A}this.f(h)&&(l=!0)}if(!this.e.allowNew&&!(t.length===0&&!l)&&this.l.classList.add(g),this.e.allowNew&&this.e.regex&&this.isInvalid()&&this.l.classList.remove(g),Array.from(i).filter(a=>a.dataset.id).forEach(a=>{n[a.dataset.id]===!0&&z(a)}),l&&(this.l.classList.remove(g),r&&this.e.autoselectFirst&&(this.removeSelection(),this.m(C,r))),s===0)if(this.e.notFoundMessage){let a=this.n.querySelector("."+v+"not-found");a.style.display="block";let h=this.e.notFoundMessage.replace("{{tag}}",this.s.value);a.innerHTML=`${h}`,this.O()}else this.hideSuggestions(!1);else this.O()}O(){let t=this.n.classList.contains(L);t||(this.n.classList.add(L),x(this.s,{"aria-expanded":"true"})),this.g(t)}g(t=!1){let e=this.P,i=this.e.fixed,s=this.e.fullWidth,r=this.s.getBoundingClientRect(),l=this.l.getBoundingClientRect(),n=0,a=0;if(i?s?(n=l.x,a=l.y+l.height+2):(n=r.x,a=r.y+r.height):s?(n=0,a=l.height+2):(n=this.s.offsetLeft,a=this.s.offsetHeight+this.s.offsetTop),e&&!s&&(n-=this.n.offsetWidth-r.width),!s){let f=Math.min(window.innerWidth,document.body.offsetWidth),d=e?r.x+r.width-this.n.offsetWidth-1:f-1-(r.x+this.n.offsetWidth);d<0&&(n=e?n-d:n+d)}s&&(this.n.style.width=this.l.offsetWidth+"px"),t||(this.n.style.transform="unset"),Object.assign(this.n.style,{left:n+"px",top:a+"px"});let h=this.n.getBoundingClientRect(),c=window.innerHeight;if(h.y+h.height>c||this.n.style.transform.includes("translateY")){let f=s?l.height+4:r.height;this.n.style.transform="translateY(calc(-100.1% - "+f+"px))"}}B(){let t=5,e=window.jQuery;return e&&e.fn.tooltip&&e.fn.tooltip.Constructor&&(t=parseInt(e.fn.tooltip.Constructor.VERSION.charAt(0))),t}V(t){return!!Array.from(this.i.querySelectorAll("option")).find(s=>s.textContent==t&&s.getAttribute("selected"))}W(t){let i=Array.from(this.i.querySelectorAll("option")).filter(s=>s.textContent==t);return!(i.length>0&&!i.find(r=>!r.getAttribute("selected")))}hasItem(t){for(let e of this.e.items){let i=e.items||[e];for(let s of i)if(s[this.e.labelField]==t)return!0}return!1}_(t){return new RegExp(this.e.regex.trim()).test(t)}getSelection(){return this.n.querySelector("a."+N)}removeSelection(){let t=this.getSelection();t&&t.classList.remove(...this.d())}d(){return[...this.e.activeClasses,N]}getActiveSelection(){return this.getSelection()}removeActiveSelection(){return this.removeSelection()}removeAll(){this.getSelectedValues().forEach(e=>{this.removeItem(e,!0)}),this.o()}removeLastItem(t=!1){let e=this.getLastItem();e&&this.removeItem(e,t)}getLastItem(){let t=this.r.querySelectorAll("span."+v+"badge");return t.length?t[t.length-1].getAttribute(b):void 0}enable(){this.i.setAttribute("disabled",""),this.resetState()}disable(){S(this.i,"disabled"),this.resetState()}isDisabled(){return this.i.hasAttribute("disabled")||this.i.disabled||this.i.hasAttribute("readonly")}isDropdownVisible(){return this.n.classList.contains(L)}isInvalid(){return this.l.classList.contains(g)}isSingle(){return!this.i.hasAttribute("multiple")}isMaxReached(){return this.e.max&&this.getSelectedValues().length>=this.e.max}canAdd(t,e={}){if(!t||e.new&&!this.e.allowNew||!e.new&&!this.hasItem(t)||this.isDisabled())return!1;if(!this.isSingle()&&!this.e.allowSame){if(e.new){if(this.V(t))return!1}else if(!this.W(t))return!1}return this.isMaxReached()?!1:this.e.regex&&e.new&&!this._(t)?(this.l.classList.add(g),!1):this.e.onCanAdd&&this.e.onCanAdd(t,e,this)===!1?(this.l.classList.add(g),!1):!0}getData(){return this.e.items}setData(t,e=!1){Array.isArray(t)||(t=Object.entries(t).map(([i,s])=>({value:i,label:s}))),this.e.items!=t&&(this.e.items=t),e&&(this.E(),t.forEach(i=>{let s=i[this.e.valueField],r=i[this.e.labelField];if(s&&(i.selected||this.e.selected.includes(s))){let l=this.addItem(r,s,i.data);l&&l.setAttribute("data-init","true")}})),this.z(t),this.v()}u(t=null,e="",i=0){let r="option"+(t===null?"":'[value="'+CSS.escape(t)+'"]')+e;return this.i.querySelectorAll(r)[i]||null}addItem(t,e=null,i={}){e||(e=t),this.isSingle()&&this.getSelectedValues().length&&this.removeLastItem(!0);let s=this.u(e,":not([selected])");if(!s){s=p("option"),s.value=e,s.textContent=t;for(let[r,l]of Object.entries(i))s.dataset[r]=l;this.i.appendChild(s),this.e.onCreateItem(s,this)}return s&&(i=Object.assign({title:s.getAttribute("title")},i,s.dataset)),s.setAttribute("selected","selected"),s.selected=!0,this.U(t,e,i),this.c&&this.i.dispatchEvent(new Event("change",{bubbles:!0})),s}v(){let t=this.i.innerHTML;this.i.innerHTML="",this.i.innerHTML=t,this.o()}U(t,e=null,i={}){let s=this.B()===5,r=i.disabled&&B(i.disabled),l=this.e.allowClear&&!r,n=t,a=p("span"),h=[v+"badge"],c=this.isSingle()&&!this.e.singleBadge;if(!c){h.push("badge");let d=this.e.badgeStyle;i.badgeStyle&&(d=i.badgeStyle),i.badgeClass&&h.push(...i.badgeClass.split(" ")),this.e.baseClass?h.push(...this.e.baseClass.split(" ")):s?h=[...h,"bg-"+d,"text-truncate"]:h=[...h,"badge-"+d],a.style.maxWidth="100%"}r&&h.push("disabled","opacity-50");let f=c?0:2;if(a.style.margin=f+"px 6px "+f+"px 0px",a.style.marginBlock=f+"px",a.style.marginInline="0px 6px",a.classList.add(...h),a.setAttribute(b,e),i.title&&a.setAttribute("title",i.title),l){let d=h.includes("text-dark")||c?"btn-close":"btn-close btn-close-white",w="margin-inline: 0px 6px;",u="left";this.e.clearEnd&&(u="right"),u=="right"&&(w="margin-inline: 6px 0px;");let m=s?'':'';n=u=="left"?m+n:n+m}a.innerHTML=n,this.r.insertBefore(a,this.s),i.title&&T&&s&&T.getOrCreateInstance(a),l&&a.querySelector("button").addEventListener("click",d=>{d.preventDefault(),d.stopPropagation(),this.isDisabled()||this.e.confirmClear(e,this).then(()=>{this.removeItem(e),document.activeElement.blur(),this.o()}).catch(()=>{})})}removeItem(t,e=!1){let i=CSS.escape(t),s=this.r.querySelectorAll("span["+b+'="'+i+'"]');if(!s.length)return;let r=s.length-1,l=s[r];l&&(l.dataset.bsOriginalTitle&&T.getOrCreateInstance(l).dispose(),l.remove());let n=this.u(t,"[selected]",r);n&&(S(n,"selected"),n.selected=!1,this.c&&!e&&this.i.dispatchEvent(new Event("change",{bubbles:!0}))),this.s.style.visibility=="hidden"&&!this.isMaxReached()&&(this.s.style.visibility="visible",this.l.classList.remove(H)),e||this.e.onClearItem(t,this)}},$=y;export{$ as default}; +var D={items:[],allowNew:!1,showAllSuggestions:!1,badgeStyle:"primary",allowClear:!1,clearEnd:!1,selected:[],regex:"",separator:[],max:0,clearLabel:"Clear",searchLabel:"Type a value",showDropIcon:!0,keepOpen:!1,allowSame:!1,baseClass:"",placeholder:"",addOnBlur:!1,showDisabled:!1,hideNativeValidation:!1,suggestionsThreshold:-1,maximumItems:0,autoselectFirst:!0,updateOnSelect:!1,highlightTyped:!1,highlightClass:"",fullWidth:!0,fixed:!1,fuzzy:!1,startsWith:!1,singleBadge:!1,activeClasses:["bg-primary","text-white"],labelField:"label",valueField:"value",searchFields:["label"],queryParam:"query",server:"",serverMethod:"GET",serverParams:{},serverDataKey:"data",fetchOptions:{},liveServer:!1,noCache:!0,debounceTime:300,notFoundMessage:"",onRenderItem:(o,t,e)=>t,onSelectItem:(o,t)=>{},onClearItem:(o,t)=>{},onCreateItem:(o,t)=>{},onBlur:(o,t)=>{},onFocus:(o,t)=>{},onCanAdd:(o,t,e)=>{},confirmClear:(o,t)=>Promise.resolve(),confirmAdd:(o,t)=>Promise.resolve(),onServerResponse:(o,t)=>o.json()},v="tags-",M="is-loading",N="is-active",g="is-invalid",H="is-max-reached",L="show",b="data-value",C="next",O="prev",R="form-control-focus",q="form-placeholder-shown",P="form-control-disabled",E=new WeakMap,j=0,T=window.bootstrap&&window.bootstrap.Tooltip;function V(o,t=300){let e;return(...i)=>{clearTimeout(e),e=setTimeout(()=>{o.apply(this,i)},t)}}function W(o,t=null){let e=p("span");document.body.appendChild(e),e.style.fontSize=t||"inherit",e.style.height="auto",e.style.width="auto",e.style.position="absolute",e.style.whiteSpace="no-wrap",e.innerHTML=o;let i=Math.ceil(e.clientWidth);return document.body.removeChild(e),i}function _(o){return o.normalize("NFD").replace(/[\u0300-\u036f]/g,"")}function F(o){return o?_(o.toString()).toLowerCase():""}function U(o,t){if(o.indexOf(t)>=0)return!0;let e=0;for(let i=0;i{this.S(!0)},this.e.debounceTime),this.c=!0,this.k(),this.l=p("div"),this.r=p("div"),this.n=p("ul"),this.s=p("input"),this.l.appendChild(this.r),this.i.parentElement.insertBefore(this.l,this.i),this.D(),this.M(),this.N(),this.H(),this.R(),this.resetState(),this.handleEvent=i=>{this.w(i)},this.e.fixed&&(document.addEventListener("scroll",this,!0),window.addEventListener("resize",this)),["focus","blur","input","keydown"].forEach(i=>{this.s.addEventListener(i,this)}),["mousemove","mouseleave"].forEach(i=>{this.n.addEventListener(i,this)}),this.loadData(!0)}static init(t="select[multiple]",e={}){let i=document.querySelectorAll(t);for(let s=0;s{this.s.removeEventListener(t,this)}),["mousemove","mouseleave"].forEach(t=>{this.n.removeEventListener(t,this)}),this.e.fixed&&(document.removeEventListener("scroll",this,!0),window.removeEventListener("resize",this)),this.i.style.display="block",this.l.parentElement.removeChild(this.l),this.parentForm&&this.parentForm.removeEventListener("reset",this),E.delete(this.i)}handleEvent(t){this.w(t)}w(t){["scroll","resize"].includes(t.type)?(this.A&&window.cancelAnimationFrame(this.A),this.A=window.requestAnimationFrame(()=>{this[`on${t.type}`](t)})):this[`on${t.type}`](t)}T(t={}){this.e=Object.assign({},D,{showDropIcon:!!this.u()});let e=this.i.dataset.config?JSON.parse(this.i.dataset.config):{},i={...t,...e,...this.i.dataset};for(let[s,r]of Object.entries(D)){if(s=="config"||i[s]===void 0)continue;let l=i[s];switch(typeof r){case"number":this.e[s]=parseInt(l);break;case"boolean":this.e[s]=B(l);break;case"string":this.e[s]=l.toString();break;case"object":this.e[s]=l,typeof l=="string"&&(["{","["].includes(l[0])?this.e[s]=JSON.parse(l):this.e[s]=l.split(l.includes("|")?"|":","));break;case"function":this.e[s]=typeof l=="string"?l.split(".").reduce((n,a)=>n[a],window):l,this.e[s]||console.error("Invalid function",l);break;default:this.e[s]=l;break}}this.e.placeholder||(this.e.placeholder=this.q()),this.e.suggestionsThreshold==-1&&(this.e.suggestionsThreshold=this.e.liveServer?1:0)}config(t=null){return t?this.e[t]:this.e}setConfig(t,e){this.e[t]=e}k(){for(this.overflowParent=null,this.parentForm=this.i.parentElement;this.parentForm&&(this.parentForm.style.overflow==="hidden"&&(this.overflowParent=this.parentForm),this.parentForm=this.parentForm.parentElement,!(this.parentForm&&this.parentForm.nodeName=="FORM")););this.parentForm&&this.parentForm.addEventListener("reset",this)}q(){if(this.i.hasAttribute("placeholder"))return this.i.getAttribute("placeholder");if(this.i.dataset.placeholder)return this.i.dataset.placeholder;let t=this.i.querySelector("option");return!t||!this.e.autoselectFirst?"":(S(t,"selected"),t.selected=!1,t.value?"":t.textContent)}N(){let t=this.i;this.e.hideNativeValidation?(t.style.position="absolute",t.style.left="-9999px"):t.style.cssText="height:1px;width:1px;opacity:0;padding:0;margin:0;border:0;float:left;flex-basis:100%;min-height:unset;",t.tabIndex=-1,t.addEventListener("focus",e=>{this.onclick(e)}),t.addEventListener("invalid",e=>{this.l.classList.add(g)})}R(){let t=this.n;t.classList.add("dropdown-menu",v+"menu"),t.id=v+"menu-"+j,t.setAttribute("role","menu");let e=t.style;e.padding="0",e.maxHeight="280px",this.e.fullWidth||(e.maxWidth="360px"),this.e.fixed&&(e.position="fixed"),e.overflowY="auto",e.overscrollBehavior="contain",e.textAlign="unset",t.addEventListener("mouseenter",i=>{this.h=!1}),this.l.appendChild(t),this.s.setAttribute("aria-controls",t.id)}D(){let t=this.l;t.classList.add("form-control","dropdown"),["form-select-lg","form-select-sm","is-invalid","is-valid"].forEach(e=>{this.i.classList.contains(e)&&t.classList.add(e)}),this.e.suggestionsThreshold==0&&this.e.showDropIcon&&t.classList.add("form-select"),this.overflowParent&&(t.style.position="inherit"),t.style.height="auto",t.addEventListener("click",this)}M(){this.r.addEventListener("click",e=>{this.isDisabled()||this.s.style.visibility!="hidden"&&this.s.focus()});let t=this.r.style;t.display="flex",t.alignItems="center",t.flexWrap="wrap"}H(){let t=this.s;t.type="text",t.autocomplete="off",t.spellcheck=!1,x(t,{"aria-autocomplete":"list","aria-haspopup":"menu","aria-expanded":"false","aria-label":this.e.searchLabel,role:"combobox"}),t.style.cssText="background-color:transparent;color:currentColor;border:0;padding:0;outline:0;max-width:100%",this.resetSearchInput(!0),this.r.appendChild(t),this.P=window.getComputedStyle(t).direction==="rtl"}onfocus(t){this.l.classList.add(R),this.showOrSearch(),this.e.onFocus(t,this)}onblur(t){this.a&&this.a.abort();let e=!0;if(this.e.addOnBlur&&this.s.value&&(e=this.L()),this.l.classList.remove(R),this.hideSuggestions(e),this.c){let i=this.getSelection(),s={selection:i?i.dataset.value:null,input:this.s.value};this.e.onBlur(t,this),this.i.dispatchEvent(new CustomEvent("tags.blur",{bubbles:!0,detail:s}))}}oninput(t){let e=this.s.value;if(e){let i=e.slice(-1);if(this.e.separator.length&&this.e.separator.includes(i)){this.s.value=this.s.value.slice(0,-1);let s=this.s.value,r=s,l={};if(this.e.allowNew)l.new=1;else{let n=this.getSelection();if(!n)return;s=n.getAttribute(b),r=n.dataset.label}this.e.confirmAdd(s,this).then(()=>{this.p(r,s,l)}).catch(()=>{});return}}setTimeout(()=>{this.o()}),this.showOrSearch()}onkeydown(t){let e=t.keyCode||t.key,i=t.target;switch(t.keyCode==229&&(e=i.value.charAt(i.selectionStart-1).charCodeAt(0)),e){case 13:case"Enter":t.preventDefault(),this.L();break;case 38:case"ArrowUp":t.preventDefault(),this.h=!0,this.m(O);break;case 40:case"ArrowDown":t.preventDefault(),this.h=!0,this.isDropdownVisible()?this.m(C):this.showOrSearch(!1);break;case 8:case"Backspace":let s=this.getLastItem();this.s.value.length==0&&s&&this.e.confirmClear(s,this).then(()=>{this.removeLastItem(),this.o(),this.showOrSearch()}).catch(()=>{});break;case 27:case"Escape":this.s.focus(),this.hideSuggestions();break}}onmousemove(t){this.h=!1}onmouseleave(t){this.removeSelection()}onscroll(t){this.g()}onresize(t){this.g()}onclick(t=null){t&&t.preventDefault(),!(!this.isSingle()&&this.isMaxReached())&&this.s.focus()}onreset(t){this.reset()}loadData(t=!1){Object.keys(this.e.items).length>0?this.setData(this.e.items,!0):this.resetSuggestions(!0),this.e.server&&(this.e.liveServer||this.S(!t))}j(){let t=this.i.selectedOptions||[];for(let e=0;e({value:s.getAttribute("value"),label:s.textContent,disabled:s.disabled,selected:s.selected,data:Object.assign({disabled:s.disabled},s.dataset)}),i=Array.from(this.i.children).filter(s=>s.hasAttribute("label")||!s.disabled||this.e.showDisabled).map(s=>s.hasAttribute("label")?{group:s.getAttribute("label"),items:Array.from(s.children).map(r=>e(r))}:e(s));this.setData(i,t)}L(){let t=this.getSelection();if(t)return t.click(),!0;if(this.e.allowNew&&this.s.value){let e=this.s.value;return this.e.confirmAdd(e,this).then(()=>{this.p(e,e,{new:1})}).catch(()=>{}),!0}return!1}S(t=!1){this.a&&this.a.abort(),this.a=new AbortController;let e=this.i.dataset.serverParams||{};typeof e=="string"&&(e=JSON.parse(e));let i=Object.assign({},this.e.serverParams,e);if(i[this.e.queryParam]=this.s.value,this.e.noCache&&(i.t=Date.now()),i.related){let n=document.getElementById(i.related);if(n){i.related=n.value;let a=n.getAttribute("name");a&&(i[a]=n.value)}}let s=new URLSearchParams(i),r=this.e.server,l=Object.assign(this.e.fetchOptions,{method:this.e.serverMethod||"GET",signal:this.a.signal});l.method==="POST"?l.body=s:r+="?"+s.toString(),this.l.classList.add(M),fetch(r,l).then(n=>this.e.onServerResponse(n,this)).then(n=>{let a=n[this.e.serverDataKey]||n;this.setData(a,!t),this.a=null,t&&this.b()}).catch(n=>{n.name==="AbortError"||this.a.signal.aborted||console.error(n)}).finally(n=>{this.l.classList.remove(M)})}p(t,e=null,i={}){if(!this.canAdd(t,i))return null;let s=this.addItem(t,e,i);return this.v(),this.e.keepOpen?this.b():this.resetSearchInput(),s}f(t){if(t.style.display==="none")return!1;let e=t.firstElementChild;return e.tagName==="A"&&!e.classList.contains("disabled")}m(t=C,e=null){let i=this.getSelection();if(i){let s=t===C?"nextSibling":"previousSibling";e=i.parentNode;do e=e[s];while(e&&!this.f(e));e?i.classList.remove(...this.d()):i&&(e=i.parentElement)}else{if(t===O)return e;if(!e)for(e=this.n.firstChild;e&&!this.f(e);)e=e.nextSibling}if(e){let s=e.offsetHeight,r=e.offsetTop,l=e.parentNode,n=l.offsetHeight,a=l.scrollHeight,h=l.offsetTop;if(s===0&&setTimeout(()=>{l.scrollTop=0}),t===O){let f=r-h>10?r-h:0;l.scrollTop=f}else r+s-(n+l.scrollTop)>0&&s>0&&(l.scrollTop=r+s-n+1,l.scrollTop+n>=a-10&&(l.scrollTop=r-h));let c=e.querySelector("a");c.classList.add(...this.d()),this.s.setAttribute("aria-activedescendant",c.id),this.e.updateOnSelect&&(this.s.value=c.dataset.label,this.o())}else this.s.setAttribute("aria-activedescendant","");return e}o(){this.l.classList.remove(q),this.s.value?this.s.size=this.s.value.length:this.getSelectedValues().length?(this.s.placeholder="",this.s.size=1):(this.s.size=this.e.placeholder.length>0?this.e.placeholder.length:1,this.s.placeholder=this.e.placeholder,this.l.classList.add(q));let t=this.s.value||this.s.placeholder,e=window.getComputedStyle(this.l).fontSize,i=W(t,e)+16;this.s.style.width=i+"px"}z(t){for(;this.n.lastChild;)this.n.removeChild(this.n.lastChild);let e=0,i=1;for(let s=0;s',this.n.appendChild(s)}}C(t,e){if(!t[this.e.valueField])return;let i=t[this.e.valueField],s=t[this.e.labelField],r=this.e.onRenderItem(t,s,this),l=p("li");l.setAttribute("role","presentation"),t.group_id&&l.setAttribute("data-group-id",""+t.group_id);let n=p("a");l.append(n),n.id=this.n.id+"-"+e,n.classList.add("dropdown-item","text-truncate"),t.disabled&&n.classList.add("disabled"),n.setAttribute(b,i),n.dataset.label=s,this.e.searchFields.forEach(a=>{l.dataset[a]=t[a]}),n.setAttribute("href","#"),n.innerHTML=r,this.n.appendChild(l),n.addEventListener("mouseenter",a=>{this.h||(this.removeSelection(),l.querySelector("a").classList.add(...this.d()))}),n.addEventListener("mousedown",a=>{a.preventDefault()}),n.addEventListener("click",a=>{a.preventDefault(),a.stopPropagation(),this.e.confirmAdd(i,this).then(()=>{this.p(s,i,t.data),this.e.onSelectItem(t,this)}).catch(()=>{})})}initialOptions(){return this.i.querySelectorAll("option[data-init]")}E(){this.i.querySelectorAll("option").forEach(t=>{S(t,"selected")})}reset(){this.removeAll(),this.c=!1;let t=this.initialOptions();this.E();for(let e=0;ee.value)}getAvailableValues(){let t=this.i.querySelectorAll("option");return Array.from(t).map(e=>e.value)}showOrSearch(t=!0){if(t&&!this.I()){this.hideSuggestions(!1);return}this.e.liveServer?this.F():this.b()}hideSuggestions(t=!0){this.n.classList.remove(L),x(this.s,{"aria-expanded":"false"}),this.removeSelection(),t&&this.l.classList.remove(g)}toggleSuggestions(t=!0,e=!0){this.n.classList.contains(L)?this.hideSuggestions(e):this.showOrSearch(t)}I(){return this.isDisabled()||this.isMaxReached()?!1:this.s.value.length>=this.e.suggestionsThreshold}b(){if(document.activeElement!=this.s||this.s.style.visibility=="hidden")return;let t=F(this.s.value),e={},i=this.n.querySelectorAll("li"),s=0,r=null,l=!1,n={};for(let a=0;a0&&this.e.searchFields.forEach(u=>{let m=F(c.dataset[u]),A=!1;if(this.e.fuzzy)A=U(m,t);else{let k=m.indexOf(t);A=this.e.startsWith?k===0:k>=0}A&&(d=!0)});let w=d||t.length===0;if(f||d?(s++,z(h),h.dataset.groupId&&(n[h.dataset.groupId]=!0),!r&&this.f(h)&&w&&(r=h),this.e.maximumItems>0&&s>this.e.maximumItems&&I(h)):I(h),this.e.highlightTyped){let u=c.textContent,m=F(u).indexOf(t),A=u.substring(0,m)+`${u.substring(m,m+t.length)}`+u.substring(m+t.length,u.length);c.innerHTML=A}this.f(h)&&(l=!0)}if(!this.e.allowNew&&!(t.length===0&&!l)&&this.l.classList.add(g),this.e.allowNew&&this.e.regex&&this.isInvalid()&&this.l.classList.remove(g),Array.from(i).filter(a=>a.dataset.id).forEach(a=>{n[a.dataset.id]===!0&&z(a)}),l&&(this.l.classList.remove(g),r&&this.e.autoselectFirst&&(this.removeSelection(),this.m(C,r))),s===0)if(this.e.notFoundMessage){let a=this.n.querySelector("."+v+"not-found");a.style.display="block";let h=this.e.notFoundMessage.replace("{{tag}}",this.s.value);a.innerHTML=`${h}`,this.O()}else this.hideSuggestions(!1);else this.O()}O(){let t=this.n.classList.contains(L);t||(this.n.classList.add(L),x(this.s,{"aria-expanded":"true"})),this.g(t)}g(t=!1){let e=this.P,i=this.e.fixed,s=this.e.fullWidth,r=this.s.getBoundingClientRect(),l=this.l.getBoundingClientRect(),n=0,a=0;if(i?s?(n=l.x,a=l.y+l.height+2):(n=r.x,a=r.y+r.height):s?(n=0,a=l.height+2):(n=this.s.offsetLeft,a=this.s.offsetHeight+this.s.offsetTop),e&&!s&&(n-=this.n.offsetWidth-r.width),!s){let f=Math.min(window.innerWidth,document.body.offsetWidth),d=e?r.x+r.width-this.n.offsetWidth-1:f-1-(r.x+this.n.offsetWidth);d<0&&(n=e?n-d:n+d)}s&&(this.n.style.width=this.l.offsetWidth+"px"),t||(this.n.style.transform="unset"),Object.assign(this.n.style,{left:n+"px",top:a+"px"});let h=this.n.getBoundingClientRect(),c=window.innerHeight;if(h.y+h.height>c||this.n.style.transform.includes("translateY")){let f=s?l.height+4:r.height;this.n.style.transform="translateY(calc(-100.1% - "+f+"px))"}}B(){let t=5,e=window.jQuery;return e&&e.fn.tooltip&&e.fn.tooltip.Constructor&&(t=parseInt(e.fn.tooltip.Constructor.VERSION.charAt(0))),t}V(t){return!!Array.from(this.i.querySelectorAll("option")).find(s=>s.textContent==t&&s.getAttribute("selected"))}W(t){let i=Array.from(this.i.querySelectorAll("option")).filter(s=>s.textContent==t);return!(i.length>0&&!i.find(r=>!r.getAttribute("selected")))}hasItem(t){for(let e of this.e.items){let i=e.items||[e];for(let s of i)if(s[this.e.labelField]==t)return!0}return!1}_(t){return new RegExp(this.e.regex.trim()).test(t)}getSelection(){return this.n.querySelector("a."+N)}removeSelection(){let t=this.getSelection();t&&t.classList.remove(...this.d())}d(){return[...this.e.activeClasses,N]}getActiveSelection(){return this.getSelection()}removeActiveSelection(){return this.removeSelection()}removeAll(){this.getSelectedValues().forEach(e=>{this.removeItem(e,!0)}),this.o()}removeLastItem(t=!1){let e=this.getLastItem();e&&this.removeItem(e,t)}getLastItem(){let t=this.r.querySelectorAll("span."+v+"badge");return t.length?t[t.length-1].getAttribute(b):void 0}enable(){this.i.setAttribute("disabled",""),this.resetState()}disable(){S(this.i,"disabled"),this.resetState()}isDisabled(){return this.i.hasAttribute("disabled")||this.i.disabled||this.i.hasAttribute("readonly")}isDropdownVisible(){return this.n.classList.contains(L)}isInvalid(){return this.l.classList.contains(g)}isSingle(){return!this.i.hasAttribute("multiple")}isMaxReached(){return this.e.max&&this.getSelectedValues().length>=this.e.max}canAdd(t,e={}){if(!t||e.new&&!this.e.allowNew||!e.new&&!this.hasItem(t)||this.isDisabled())return!1;if(!this.isSingle()&&!this.e.allowSame){if(e.new){if(this.V(t))return!1}else if(!this.W(t))return!1}return this.isMaxReached()?!1:this.e.regex&&e.new&&!this._(t)?(this.l.classList.add(g),!1):this.e.onCanAdd&&this.e.onCanAdd(t,e,this)===!1?(this.l.classList.add(g),!1):!0}getData(){return this.e.items}setData(t,e=!1){Array.isArray(t)||(t=Object.entries(t).map(([i,s])=>({value:i,label:s}))),this.e.items!=t&&(this.e.items=t),e&&(this.E(),t.forEach(i=>{let s=i[this.e.valueField],r=i[this.e.labelField];if(s&&(i.selected||this.e.selected.includes(s))){let l=this.addItem(r,s,i.data);l&&l.setAttribute("data-init","true")}})),this.z(t),this.v()}u(t=null,e="",i=0){let r="option"+(t===null?"":'[value="'+CSS.escape(t)+'"]')+e;return this.i.querySelectorAll(r)[i]||null}addItem(t,e=null,i={}){e||(e=t),this.isSingle()&&this.getSelectedValues().length&&this.removeLastItem(!0);let s=this.u(e,":not([selected])");if(!s){s=p("option"),s.value=e,s.textContent=t;for(let[r,l]of Object.entries(i))s.dataset[r]=l;this.i.appendChild(s),this.e.onCreateItem(s,this)}return s&&(i=Object.assign({title:s.getAttribute("title")},i,s.dataset)),s.setAttribute("selected","selected"),s.selected=!0,this.U(t,e,i),this.c&&this.i.dispatchEvent(new Event("change",{bubbles:!0})),s}v(){let t=this.i.innerHTML;this.i.innerHTML="",this.i.innerHTML=t,this.o()}U(t,e=null,i={}){let s=this.B()===5,r=i.disabled&&B(i.disabled),l=this.e.allowClear&&!r,n=t,a=p("span"),h=[v+"badge"],c=this.isSingle()&&!this.e.singleBadge;if(!c){h.push("badge");let d=this.e.badgeStyle;i.badgeStyle&&(d=i.badgeStyle),i.badgeClass&&h.push(...i.badgeClass.split(" ")),this.e.baseClass?h.push(...this.e.baseClass.split(" ")):s?h=[...h,"bg-"+d,"text-truncate"]:h=[...h,"badge-"+d],a.style.maxWidth="100%"}r&&h.push("disabled","opacity-50");let f=c?0:2;if(a.style.margin=f+"px 6px "+f+"px 0px",a.style.marginBlock=f+"px",a.style.marginInline="0px 6px",a.classList.add(...h),a.setAttribute(b,e),i.title&&a.setAttribute("title",i.title),l){let d=h.includes("text-dark")||c?"btn-close":"btn-close btn-close-white",w="margin-inline: 0px 6px;",u="left";this.e.clearEnd&&(u="right"),u=="right"&&(w="margin-inline: 6px 0px;");let m=s?'':'';n=u=="left"?m+n:n+m}a.innerHTML=n,this.r.insertBefore(a,this.s),i.title&&T&&s&&T.getOrCreateInstance(a),l&&a.querySelector("button").addEventListener("click",d=>{d.preventDefault(),d.stopPropagation(),this.isDisabled()||this.e.confirmClear(e,this).then(()=>{this.removeItem(e),document.activeElement.blur(),this.o()}).catch(()=>{})})}removeItem(t,e=!1){let i=CSS.escape(t),s=this.r.querySelectorAll("span["+b+'="'+i+'"]');if(!s.length)return;let r=s.length-1,l=s[r];l&&(l.dataset.bsOriginalTitle&&T.getOrCreateInstance(l).dispose(),l.remove());let n=this.u(t,"[selected]",r);n&&(S(n,"selected"),n.selected=!1,this.c&&!e&&this.i.dispatchEvent(new Event("change",{bubbles:!0}))),this.s.style.visibility=="hidden"&&!this.isMaxReached()&&(this.s.style.visibility="visible",this.l.classList.remove(H)),e||this.e.onClearItem(t,this)}},$=y;export{$ as default}; //# sourceMappingURL=tags.min.js.map diff --git a/tags.min.js.map b/tags.min.js.map index 17ebd4f..5e54f62 100644 --- a/tags.min.js.map +++ b/tags.min.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["tags.js"], - "sourcesContent": ["/**\r\n * Bootstrap 5 (and 4!) tags\r\n *\r\n * Turns your select[multiple] into nice tags lists\r\n *\r\n * Required Bootstrap 5 styles:\r\n * - badge\r\n * - background-color utility\r\n * - text-truncate utility\r\n * - forms\r\n * - dropdown\r\n */\r\n\r\n// #region config\r\n\r\n/**\r\n * @callback EventCallback\r\n * @param {Event} event\r\n * @param {Tags} inst\r\n * @returns {void}\r\n */\r\n\r\n/**\r\n * @callback ServerCallback\r\n * @param {Response} response\r\n * @param {Tags} inst\r\n * @returns {Promise}\r\n */\r\n\r\n/**\r\n * @callback ModalItemCallback\r\n * @param {String} value\r\n * @param {Tags} inst\r\n * @returns {Promise}\r\n */\r\n\r\n/**\r\n * @callback RenderCallback\r\n * @param {Suggestion} item\r\n * @param {String} label\r\n * @param {Tags} inst\r\n * @returns {String}\r\n */\r\n\r\n/**\r\n * @callback ItemCallback\r\n * @param {Suggestion} item\r\n * @param {Tags} inst\r\n * @returns {void}\r\n */\r\n\r\n/**\r\n * @callback ValueCallback\r\n * @param {String} value\r\n * @param {Tags} inst\r\n * @returns {void}\r\n */\r\n\r\n/**\r\n * @callback AddCallback\r\n * @param {String} value\r\n * @param {Object} data\r\n * @param {Tags} inst\r\n * @returns {void|Boolean}\r\n */\r\n\r\n/**\r\n * @callback CreateCallback\r\n * @param {HTMLOptionElement} option\r\n * @param {Tags} inst\r\n * @returns {void}\r\n */\r\n\r\n/**\r\n * @typedef Config\r\n * @property {Array} items Source items\r\n * @property {Boolean} allowNew Allows creation of new tags\r\n * @property {Boolean} showAllSuggestions Show all suggestions even if they don't match. Disables validation.\r\n * @property {String} badgeStyle Color of the badge (color can be configured per option as well)\r\n * @property {Boolean} allowClear Show a clear icon\r\n * @property {Boolean} clearEnd Place clear icon at the end\r\n * @property {Array} selected A list of initially selected values\r\n * @property {String} regex Regex for new tags\r\n * @property {Array|String} separator A list (pipe separated) of characters that should act as separator (default is using enter key)\r\n * @property {Number} max Limit to a maximum of tags (0 = no limit)\r\n * @property {String} placeholder Provides a placeholder if none are provided as the first empty option\r\n * @property {String} clearLabel Text as clear tooltip\r\n * @property {String} searchLabel Default placeholder\r\n * @property {Boolean} showDropIcon Show dropdown icon\r\n * @property {Boolean} keepOpen Keep suggestions open after selection, clear on focus out\r\n * @property {Boolean} allowSame Allow same tags used multiple times\r\n * @property {String} baseClass Customize the class applied to badges\r\n * @property {Boolean} addOnBlur Add new tags on blur (only if allowNew is enabled)\r\n * @property {Boolean} showDisabled Show disabled tags\r\n * @property {Boolean} hideNativeValidation Hide native validation tooltips\r\n * @property {Number} suggestionsThreshold Number of chars required to show suggestions\r\n * @property {Number} maximumItems Maximum number of items to display\r\n * @property {Boolean} autoselectFirst Always select the first item\r\n * @property {Boolean} updateOnSelect Update input value on selection (doesn't play nice with autoselectFirst)\r\n * @property {Boolean} highlightTyped Highlight matched part of the suggestion\r\n * @property {String} highlightClass Class applied to the mark element\r\n * @property {Boolean} fullWidth Match the width on the input field\r\n * @property {Boolean} fixed Use fixed positioning (solve overflow issues)\r\n * @property {Boolean} fuzzy Fuzzy search\r\n * @property {Boolean} startsWith Must start with the string. Defaults to false (it matches any position).\r\n * @property {Boolean} singleBadge Show badge for single elements\r\n * @property {Array} activeClasses By default: [\"bg-primary\", \"text-white\"]\r\n * @property {String} labelField Key for the label\r\n * @property {String} valueField Key for the value\r\n * @property {Array} searchFields Key for the search\r\n * @property {String} queryParam Name of the param passed to endpoint (query by default)\r\n * @property {String} server Endpoint for data provider\r\n * @property {String} serverMethod HTTP request method for data provider, default is GET\r\n * @property {String|Object} serverParams Parameters to pass along to the server. You can specify a \"related\" key with the id of a related field.\r\n * @property {String} serverDataKey By default: data\r\n * @property {Object} fetchOptions Any other fetch options (https://developer.mozilla.org/en-US/docs/Web/API/fetch#syntax)\r\n * @property {Boolean} liveServer Should the endpoint be called each time on input\r\n * @property {Boolean} noCache Prevent caching by appending a timestamp\r\n * @property {Number} debounceTime Debounce time for live server\r\n * @property {String} notFoundMessage Display a no suggestions found message. Leave empty to disable\r\n * @property {RenderCallback} onRenderItem Callback function that returns the suggestion\r\n * @property {ItemCallback} onSelectItem Callback function to call on selection\r\n * @property {ValueCallback} onClearItem Callback function to call on clear\r\n * @property {CreateCallback} onCreateItem Callback function when an item is created\r\n * @property {EventCallback} onBlur Callback function on blur\r\n * @property {EventCallback} onFocus Callback function on focus\r\n * @property {AddCallback} onCanAdd Callback function to validate item. Return false to show validation message.\r\n * @property {ServerCallback} onServerResponse Callback function to process server response. Must return a Promise\r\n * @property {ModalItemCallback} confirmClear Allow modal confirmation of clear. Must return a Promise\r\n * @property {ModalItemCallback} confirmAdd Allow modal confirmation of add. Must return a Promise\r\n */\r\n\r\n/**\r\n * @typedef Suggestion\r\n * @property {String} value Can be overriden by config valueField\r\n * @property {String} label Can be overriden by config labelField\r\n * @property {Boolean} disabled\r\n * @property {Object} data\r\n * @property {Boolean} [selected]\r\n * @property {Number} [group_id]\r\n */\r\n\r\n/**\r\n * @typedef SuggestionGroup\r\n * @property {String} group\r\n * @property {Array} items\r\n */\r\n\r\n/**\r\n * @type {Config}\r\n */\r\nconst DEFAULTS = {\r\n items: [],\r\n allowNew: false,\r\n showAllSuggestions: false,\r\n badgeStyle: \"primary\",\r\n allowClear: false,\r\n clearEnd: false,\r\n selected: [],\r\n regex: \"\",\r\n separator: [],\r\n max: 0,\r\n clearLabel: \"Clear\",\r\n searchLabel: \"Type a value\",\r\n showDropIcon: true,\r\n keepOpen: false,\r\n allowSame: false,\r\n baseClass: \"\",\r\n placeholder: \"\",\r\n addOnBlur: false,\r\n showDisabled: false,\r\n hideNativeValidation: false,\r\n suggestionsThreshold: -1,\r\n maximumItems: 0,\r\n autoselectFirst: true,\r\n updateOnSelect: false,\r\n highlightTyped: false,\r\n highlightClass: \"\",\r\n fullWidth: true,\r\n fixed: false,\r\n fuzzy: false,\r\n startsWith: false,\r\n singleBadge: false,\r\n activeClasses: [\"bg-primary\", \"text-white\"],\r\n labelField: \"label\",\r\n valueField: \"value\",\r\n searchFields: [\"label\"],\r\n queryParam: \"query\",\r\n server: \"\",\r\n serverMethod: \"GET\",\r\n serverParams: {},\r\n serverDataKey: \"data\",\r\n fetchOptions: {},\r\n liveServer: false,\r\n noCache: true,\r\n debounceTime: 300,\r\n notFoundMessage: \"\",\r\n onRenderItem: (item, label, inst) => {\r\n return label;\r\n },\r\n onSelectItem: (item, inst) => {},\r\n onClearItem: (value, inst) => {},\r\n onCreateItem: (option, inst) => {},\r\n onBlur: (event, inst) => {},\r\n onFocus: (event, inst) => {},\r\n onCanAdd: (text, data, inst) => {},\r\n confirmClear: (item, inst) => Promise.resolve(),\r\n confirmAdd: (item, inst) => Promise.resolve(),\r\n onServerResponse: (response, inst) => {\r\n return response.json();\r\n },\r\n};\r\n\r\n// #endregion\r\n\r\n// #region constants\r\n\r\nconst CLASS_PREFIX = \"tags-\";\r\nconst LOADING_CLASS = \"is-loading\";\r\nconst ACTIVE_CLASS = \"is-active\";\r\nconst INVALID_CLASS = \"is-invalid\";\r\nconst MAX_REACHED_CLASS = \"is-max-reached\";\r\nconst SHOW_CLASS = \"show\";\r\nconst VALUE_ATTRIBUTE = \"data-value\";\r\nconst NEXT = \"next\";\r\nconst PREV = \"prev\";\r\nconst FOCUS_CLASS = \"form-control-focus\"; // should match form-control:focus\r\nconst PLACEHOLDER_CLASS = \"form-placeholder-shown\"; // should match :placeholder-shown\r\nconst DISABLED_CLASS = \"form-control-disabled\"; // should match form-control:disabled\r\nconst INSTANCE_MAP = new WeakMap();\r\nlet counter = 0;\r\n//@ts-ignore\r\nlet tooltip = window.bootstrap && window.bootstrap.Tooltip;\r\n\r\n// #endregion\r\n\r\n// #region functions\r\n\r\n/**\r\n * @param {Function} func\r\n * @param {number} timeout\r\n * @returns {Function}\r\n */\r\nfunction debounce(func, timeout = 300) {\r\n let timer;\r\n return (...args) => {\r\n clearTimeout(timer);\r\n timer = setTimeout(() => {\r\n //@ts-ignore\r\n func.apply(this, args);\r\n }, timeout);\r\n };\r\n}\r\n\r\n/**\r\n * @param {string} text\r\n * @param {string} size\r\n * @returns {Number}\r\n */\r\nfunction calcTextWidth(text, size = null) {\r\n const span = ce(\"span\");\r\n document.body.appendChild(span);\r\n span.style.fontSize = size || \"inherit\";\r\n span.style.height = \"auto\";\r\n span.style.width = \"auto\";\r\n span.style.position = \"absolute\";\r\n span.style.whiteSpace = \"no-wrap\";\r\n span.innerHTML = text;\r\n const width = Math.ceil(span.clientWidth);\r\n document.body.removeChild(span);\r\n return width;\r\n}\r\n\r\n/**\r\n * @param {String} str\r\n * @returns {String}\r\n */\r\nfunction removeDiacritics(str) {\r\n return str.normalize(\"NFD\").replace(/[\\u0300-\\u036f]/g, \"\");\r\n}\r\n\r\n/**\r\n * @param {String|Number} str\r\n * @returns {String}\r\n */\r\nfunction normalize(str) {\r\n if (!str) {\r\n return \"\";\r\n }\r\n return removeDiacritics(str.toString()).toLowerCase();\r\n}\r\n\r\n/**\r\n * A simple fuzzy match algorithm that checks if chars are matched\r\n * in order in the target string\r\n *\r\n * @param {String} str\r\n * @param {String} lookup\r\n * @returns {Boolean}\r\n */\r\nfunction fuzzyMatch(str, lookup) {\r\n if (str.indexOf(lookup) >= 0) {\r\n return true;\r\n }\r\n let pos = 0;\r\n for (let i = 0; i < lookup.length; i++) {\r\n const c = lookup[i];\r\n if (c == \" \") continue;\r\n pos = str.indexOf(c, pos) + 1;\r\n if (pos <= 0) {\r\n return false;\r\n }\r\n }\r\n return true;\r\n}\r\n\r\n/**\r\n * @param {HTMLElement} item\r\n */\r\nfunction hideItem(item) {\r\n item.style.display = \"none\";\r\n attrs(item, {\r\n \"aria-hidden\": \"true\",\r\n });\r\n}\r\n\r\n/**\r\n * @param {HTMLElement} item\r\n */\r\nfunction showItem(item) {\r\n item.style.display = \"list-item\";\r\n attrs(item, {\r\n \"aria-hidden\": \"false\",\r\n });\r\n}\r\n\r\n/**\r\n * @param {HTMLElement} el\r\n * @param {Object} attrs\r\n */\r\nfunction attrs(el, attrs) {\r\n for (const [k, v] of Object.entries(attrs)) {\r\n el.setAttribute(k, v);\r\n }\r\n}\r\n\r\n/**\r\n * @param {HTMLElement} el\r\n * @param {string} attr\r\n */\r\nfunction rmAttr(el, attr) {\r\n if (el.hasAttribute(attr)) {\r\n el.removeAttribute(attr);\r\n }\r\n}\r\n\r\n/**\r\n * Allow 1/0, true/false as strings\r\n * @param {any} value\r\n * @returns {Boolean}\r\n */\r\nfunction parseBool(value) {\r\n return [\"true\", \"false\", \"1\", \"0\", true, false].includes(value) && !!JSON.parse(value);\r\n}\r\n\r\n/**\r\n * @template {keyof HTMLElementTagNameMap} K\r\n * @param {K|String} tagName Name of the element\r\n * @returns {*}\r\n */\r\nfunction ce(tagName) {\r\n return document.createElement(tagName);\r\n}\r\n\r\n/**\r\n * @param {HTMLElement} el\r\n * @param {HTMLElement} newEl\r\n * @returns {HTMLElement}\r\n */\r\n// function insertAfter(el, newEl) {\r\n// return el.parentNode.insertBefore(newEl, el.nextSibling);\r\n// }\r\n\r\n// #endregion\r\n\r\nclass Tags {\r\n /**\r\n * @param {HTMLSelectElement} el\r\n * @param {Object|Config} config\r\n */\r\n constructor(el, config = {}) {\r\n if (!(el instanceof HTMLElement)) {\r\n console.error(\"Invalid element\", el);\r\n return;\r\n }\r\n INSTANCE_MAP.set(el, this);\r\n counter++;\r\n this._selectElement = el;\r\n\r\n this._configure(config);\r\n\r\n // private vars\r\n this._keyboardNavigation = false;\r\n this._searchFunc = debounce(() => {\r\n this._loadFromServer(true);\r\n }, this._config.debounceTime);\r\n this._fireEvents = true;\r\n\r\n this._configureParent();\r\n\r\n // Create elements\r\n this._holderElement = ce(\"div\"); // this is the one holding the fake input and the dropmenu\r\n this._containerElement = ce(\"div\"); // this is the one for the fake input (labels + input)\r\n this._dropElement = ce(\"ul\"); // this dropdown list\r\n this._searchInput = ce(\"input\"); // the input element\r\n this._holderElement.appendChild(this._containerElement);\r\n\r\n // insert before select, this helps having native validation tooltips positioned properly\r\n this._selectElement.parentElement.insertBefore(this._holderElement, this._selectElement);\r\n // insertAfter(this._selectElement, this._holderElement);\r\n\r\n // Configure them\r\n this._configureHolderElement();\r\n this._configureContainerElement();\r\n this._configureSelectElement();\r\n this._configureSearchInput();\r\n this._configureDropElement();\r\n this.resetState();\r\n\r\n // Rebind handleEvent to make sure the scope will not change\r\n this.handleEvent = (ev) => {\r\n this._handleEvent(ev);\r\n };\r\n\r\n if (this._config.fixed) {\r\n document.addEventListener(\"scroll\", this, true); // capture input for all scrollables elements\r\n window.addEventListener(\"resize\", this);\r\n }\r\n\r\n // Add listeners (remove then on dispose()). See handleEvent.\r\n [\"focus\", \"blur\", \"input\", \"keydown\"].forEach((type) => {\r\n this._searchInput.addEventListener(type, this);\r\n });\r\n [\"mousemove\", \"mouseleave\"].forEach((type) => {\r\n this._dropElement.addEventListener(type, this);\r\n });\r\n\r\n this.loadData(true);\r\n }\r\n\r\n // #region Core\r\n\r\n /**\r\n * Attach to all elements matched by the selector\r\n * @param {string} selector\r\n * @param {Object} opts\r\n */\r\n static init(selector = \"select[multiple]\", opts = {}) {\r\n /**\r\n * @type {NodeListOf}\r\n */\r\n let list = document.querySelectorAll(selector);\r\n for (let i = 0; i < list.length; i++) {\r\n if (Tags.getInstance(list[i])) {\r\n continue;\r\n }\r\n new Tags(list[i], opts);\r\n }\r\n }\r\n\r\n /**\r\n * @param {HTMLSelectElement} el\r\n */\r\n static getInstance(el) {\r\n if (INSTANCE_MAP.has(el)) {\r\n return INSTANCE_MAP.get(el);\r\n }\r\n }\r\n\r\n dispose() {\r\n [\"focus\", \"blur\", \"input\", \"keydown\"].forEach((type) => {\r\n this._searchInput.removeEventListener(type, this);\r\n });\r\n [\"mousemove\", \"mouseleave\"].forEach((type) => {\r\n this._dropElement.removeEventListener(type, this);\r\n });\r\n\r\n if (this._config.fixed) {\r\n document.removeEventListener(\"scroll\", this, true);\r\n window.removeEventListener(\"resize\", this);\r\n }\r\n\r\n // restore select, remove our custom stuff and unbind parent\r\n this._selectElement.style.display = \"block\";\r\n this._holderElement.parentElement.removeChild(this._holderElement);\r\n if (this.parentForm) {\r\n this.parentForm.removeEventListener(\"reset\", this);\r\n }\r\n\r\n INSTANCE_MAP.delete(this._selectElement);\r\n }\r\n\r\n /**\r\n * event-polyfill compat / handleEvent is expected on class\r\n * @link https://github.com/lifaon74/events-polyfill/issues/10\r\n * @param {Event} event\r\n */\r\n handleEvent(event) {\r\n this._handleEvent(event);\r\n }\r\n\r\n /**\r\n * @link https://gist.github.com/WebReflection/ec9f6687842aa385477c4afca625bbf4#handling-events\r\n * @param {Event} event\r\n */\r\n _handleEvent(event) {\r\n // debounce scroll and resize\r\n const debounced = [\"scroll\", \"resize\"];\r\n if (debounced.includes(event.type)) {\r\n if (this._timer) window.cancelAnimationFrame(this._timer);\r\n this._timer = window.requestAnimationFrame(() => {\r\n this[`on${event.type}`](event);\r\n });\r\n } else {\r\n this[`on${event.type}`](event);\r\n }\r\n }\r\n\r\n /**\r\n * @param {Config|Object} config\r\n */\r\n _configure(config = {}) {\r\n this._config = Object.assign({}, DEFAULTS, {\r\n // Hide icon by default if no value\r\n showDropIcon: this._findOption() ? true : false,\r\n });\r\n\r\n const json = this._selectElement.dataset.config ? JSON.parse(this._selectElement.dataset.config) : {};\r\n // Handle options, using arguments first, then json config and then data attr as override\r\n const o = { ...config, ...json, ...this._selectElement.dataset };\r\n\r\n // Typecast provided options based on defaults types\r\n for (const [key, defaultValue] of Object.entries(DEFAULTS)) {\r\n // Check for undefined keys\r\n if (key == \"config\" || o[key] === void 0) {\r\n continue;\r\n }\r\n const value = o[key];\r\n switch (typeof defaultValue) {\r\n case \"number\":\r\n this._config[key] = parseInt(value);\r\n break;\r\n case \"boolean\":\r\n this._config[key] = parseBool(value);\r\n break;\r\n case \"string\":\r\n this._config[key] = value.toString();\r\n break;\r\n case \"object\":\r\n this._config[key] = value;\r\n if (typeof value === \"string\") {\r\n if ([\"{\", \"[\"].includes(value[0])) {\r\n // JSON like string\r\n this._config[key] = JSON.parse(value);\r\n } else {\r\n // CSV or pipe separated string\r\n this._config[key] = value.split(value.includes(\"|\") ? \"|\" : \",\");\r\n }\r\n }\r\n break;\r\n case \"function\":\r\n // Find a global function with this name\r\n this._config[key] = typeof value === \"string\" ? value.split(\".\").reduce((r, p) => r[p], window) : value;\r\n if (!this._config[key]) {\r\n console.error(\"Invalid function\", value);\r\n }\r\n break;\r\n default:\r\n this._config[key] = value;\r\n break;\r\n }\r\n }\r\n\r\n // Dynamic default values\r\n if (!this._config.placeholder) {\r\n this._config.placeholder = this._getPlaceholder();\r\n }\r\n if (this._config.suggestionsThreshold == -1) {\r\n // if we don't have ajax auto completion, behave like a select by default\r\n this._config.suggestionsThreshold = this._config.liveServer ? 1 : 0;\r\n }\r\n }\r\n\r\n /**\r\n * @param {String} k\r\n * @returns {*}\r\n */\r\n config(k = null) {\r\n return k ? this._config[k] : this._config;\r\n }\r\n\r\n /**\r\n * @param {String} k\r\n * @param {*} v\r\n */\r\n setConfig(k, v) {\r\n this._config[k] = v;\r\n }\r\n\r\n // #endregion\r\n\r\n // #region Html\r\n\r\n /**\r\n * Find overflow parent for positioning\r\n * and bind reset event of the parent form\r\n */\r\n _configureParent() {\r\n this.overflowParent = null;\r\n this.parentForm = this._selectElement.parentElement;\r\n while (this.parentForm) {\r\n if (this.parentForm.style.overflow === \"hidden\") {\r\n this.overflowParent = this.parentForm;\r\n }\r\n this.parentForm = this.parentForm.parentElement;\r\n if (this.parentForm && this.parentForm.nodeName == \"FORM\") {\r\n break;\r\n }\r\n }\r\n if (this.parentForm) {\r\n this.parentForm.addEventListener(\"reset\", this);\r\n }\r\n }\r\n\r\n /**\r\n * @returns {string}\r\n */\r\n _getPlaceholder() {\r\n // Use placeholder and data-placeholder in priority\r\n if (this._selectElement.hasAttribute(\"placeholder\")) {\r\n return this._selectElement.getAttribute(\"placeholder\");\r\n }\r\n if (this._selectElement.dataset.placeholder) {\r\n return this._selectElement.dataset.placeholder;\r\n }\r\n // Fallback to first option if no value\r\n let firstOption = this._selectElement.querySelector(\"option\");\r\n if (!firstOption || !this._config.autoselectFirst) {\r\n return \"\";\r\n }\r\n rmAttr(firstOption, \"selected\");\r\n firstOption.selected = false;\r\n return !firstOption.value ? firstOption.textContent : \"\";\r\n }\r\n\r\n _configureSelectElement() {\r\n const selectEl = this._selectElement;\r\n\r\n // Hiding the select should keep it focusable, otherwise we get this\r\n // An invalid form control with name='...' is not focusable.\r\n // If it's not focusable, we need to remove the native validation attributes\r\n\r\n // If we use display none, we don't get the focus event\r\n // selectEl.style.display = \"none\";\r\n\r\n // If we position it like this, the html5 validation message will not display properly\r\n if (this._config.hideNativeValidation) {\r\n // This position dont break render within input-group and is focusable\r\n selectEl.style.position = \"absolute\";\r\n selectEl.style.left = \"-9999px\";\r\n } else {\r\n // Hide but keep it focusable. If 0 height, no native validation message will show\r\n // It is placed below so that native tooltip is displayed properly\r\n // Flex basis is required for input-group otherwise it breaks the layout\r\n selectEl.style.cssText = `height:1px;width:1px;opacity:0;padding:0;margin:0;border:0;float:left;flex-basis:100%;`;\r\n }\r\n\r\n // Make sure it's not usable using tab\r\n selectEl.tabIndex = -1;\r\n\r\n // No need for custom label click event if select is focusable\r\n // const label = document.querySelector('label[for=\"' + selectEl.getAttribute(\"id\") + '\"]');\r\n // if (label) {\r\n // label.addEventListener(\"click\", this);\r\n // }\r\n\r\n // It can be focused by clicking on the label\r\n selectEl.addEventListener(\"focus\", (event) => {\r\n this.onclick(event);\r\n });\r\n\r\n // When using regular html5 validation, make sure our fake element get the proper class\r\n selectEl.addEventListener(\"invalid\", (event) => {\r\n this._holderElement.classList.add(INVALID_CLASS);\r\n });\r\n }\r\n\r\n /**\r\n * Configure drop element\r\n * Needs to be called after searchInput is created\r\n */\r\n _configureDropElement() {\r\n const dropEl = this._dropElement;\r\n dropEl.classList.add(...[\"dropdown-menu\", CLASS_PREFIX + \"menu\"]);\r\n dropEl.id = CLASS_PREFIX + \"menu-\" + counter;\r\n dropEl.setAttribute(\"role\", \"menu\");\r\n\r\n const dropStyles = dropEl.style;\r\n dropStyles.padding = \"0\"; // avoid ugly space before option\r\n dropStyles.maxHeight = \"280px\";\r\n if (!this._config.fullWidth) {\r\n dropStyles.maxWidth = \"360px\";\r\n }\r\n if (this._config.fixed) {\r\n dropStyles.position = \"fixed\";\r\n }\r\n dropStyles.overflowY = \"auto\";\r\n // Prevent scrolling the menu from scrolling the page\r\n // @link https://developer.mozilla.org/en-US/docs/Web/CSS/overscroll-behavior\r\n dropStyles.overscrollBehavior = \"contain\";\r\n dropStyles.textAlign = \"unset\"; // otherwise RTL is not good\r\n\r\n // If the mouse was outside, entering remove keyboard nav mode\r\n dropEl.addEventListener(\"mouseenter\", (event) => {\r\n this._keyboardNavigation = false;\r\n });\r\n this._holderElement.appendChild(dropEl);\r\n\r\n // include aria-controls with the value of the id of the suggested list of values.\r\n this._searchInput.setAttribute(\"aria-controls\", dropEl.id);\r\n }\r\n\r\n _configureHolderElement() {\r\n const holder = this._holderElement;\r\n holder.classList.add(...[\"form-control\", \"dropdown\"]);\r\n // Reflect size (we must use form-select-xx because we may use form-select) and validation\r\n [\"form-select-lg\", \"form-select-sm\", \"is-invalid\", \"is-valid\"].forEach((className) => {\r\n if (this._selectElement.classList.contains(className)) {\r\n holder.classList.add(className);\r\n }\r\n });\r\n\r\n // It is really more like a dropdown\r\n if (this._config.suggestionsThreshold == 0 && this._config.showDropIcon) {\r\n holder.classList.add(\"form-select\");\r\n }\r\n\r\n // If we have an overflow parent, we can simply inherit styles\r\n if (this.overflowParent) {\r\n holder.style.position = \"inherit\";\r\n }\r\n // Prevent fixed height due to form-control in bs4\r\n holder.style.height = \"auto\";\r\n\r\n // Without this, clicking on a floating label won't always focus properly\r\n holder.addEventListener(\"click\", this);\r\n }\r\n\r\n _configureContainerElement() {\r\n this._containerElement.addEventListener(\"click\", (event) => {\r\n if (this.isDisabled()) {\r\n return;\r\n }\r\n if (this._searchInput.style.visibility != \"hidden\") {\r\n this._searchInput.focus();\r\n }\r\n });\r\n\r\n // Add some extra css to help positioning\r\n const containerStyles = this._containerElement.style;\r\n containerStyles.display = \"flex\";\r\n containerStyles.alignItems = \"center\";\r\n containerStyles.flexWrap = \"wrap\";\r\n }\r\n\r\n _configureSearchInput() {\r\n const searchInput = this._searchInput;\r\n\r\n searchInput.type = \"text\";\r\n searchInput.autocomplete = \"off\";\r\n searchInput.spellcheck = false;\r\n // note: firefox doesn't support the properties so we use attributes\r\n // @link https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-autocomplete\r\n // @link https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-expanded\r\n // use the aria-expanded state on the element with role combobox to communicate that the list is displayed.\r\n // https://developer.mozilla.org/en-US/docs/Web/API/Element/ariaLabel\r\n attrs(searchInput, {\r\n \"aria-autocomplete\": \"list\",\r\n \"aria-haspopup\": \"menu\",\r\n \"aria-expanded\": \"false\",\r\n \"aria-label\": this._config.searchLabel,\r\n role: \"combobox\",\r\n });\r\n searchInput.style.cssText = `background-color:transparent;color:currentColor;border:0;padding:0;outline:0;max-width:100%`;\r\n this.resetSearchInput(true);\r\n\r\n this._containerElement.appendChild(searchInput);\r\n this._rtl = window.getComputedStyle(searchInput).direction === \"rtl\";\r\n }\r\n\r\n // #endregion\r\n\r\n // #region Events\r\n\r\n onfocus(event) {\r\n this._holderElement.classList.add(FOCUS_CLASS);\r\n this.showOrSearch();\r\n this._config.onFocus(event, this);\r\n }\r\n\r\n onblur(event) {\r\n // Cancel any pending request\r\n if (this._abortController) {\r\n this._abortController.abort();\r\n }\r\n let clearValidation = true;\r\n if (this._config.addOnBlur && this._searchInput.value) {\r\n clearValidation = this._enterValue();\r\n }\r\n this._holderElement.classList.remove(FOCUS_CLASS);\r\n this.hideSuggestions(clearValidation);\r\n if (this._fireEvents) {\r\n const sel = this.getSelection();\r\n const data = {\r\n selection: sel ? sel.dataset.value : null,\r\n input: this._searchInput.value,\r\n };\r\n this._config.onBlur(event, this);\r\n this._selectElement.dispatchEvent(new CustomEvent(\"tags.blur\", { bubbles: true, detail: data }));\r\n }\r\n }\r\n\r\n oninput(ev) {\r\n const data = this._searchInput.value;\r\n\r\n // Add item if a separator is used\r\n // On mobile or copy paste, it can pass multiple chars (eg: when pressing space and it formats the string)\r\n if (data) {\r\n const lastChar = data.slice(-1);\r\n if (this._config.separator.length && this._config.separator.includes(lastChar)) {\r\n // Remove separator even if adding is prevented\r\n this._searchInput.value = this._searchInput.value.slice(0, -1);\r\n let value = this._searchInput.value;\r\n let label = value;\r\n let addData = {};\r\n // There is no good reason to use the separator feature without allowNew, but who knows!\r\n if (!this._config.allowNew) {\r\n const sel = this.getSelection();\r\n if (!sel) {\r\n return;\r\n }\r\n value = sel.getAttribute(VALUE_ATTRIBUTE);\r\n label = sel.dataset.label;\r\n } else {\r\n addData.new = 1;\r\n }\r\n this._config\r\n .confirmAdd(value, this)\r\n .then(() => {\r\n this._add(label, value, addData);\r\n })\r\n .catch(() => {});\r\n return;\r\n }\r\n }\r\n\r\n // Adjust input width to current content\r\n setTimeout(() => {\r\n this._adjustWidth();\r\n });\r\n\r\n // Check if we should display suggestions\r\n this.showOrSearch();\r\n }\r\n\r\n /**\r\n * keypress doesn't send arrow keys, so we use keydown\r\n * @param {KeyboardEvent} event\r\n */\r\n onkeydown(event) {\r\n // Keycode reference : https://css-tricks.com/snippets/javascript/javascript-keycodes/\r\n let key = event.keyCode || event.key;\r\n /**\r\n * @type {HTMLInputElement}\r\n */\r\n // @ts-ignore\r\n const target = event.target;\r\n\r\n // Android virtual keyboard might always return 229\r\n if (event.keyCode == 229) {\r\n key = target.value.charAt(target.selectionStart - 1).charCodeAt(0);\r\n }\r\n\r\n // Keyboard keys\r\n switch (key) {\r\n case 13:\r\n case \"Enter\":\r\n event.preventDefault();\r\n this._enterValue();\r\n break;\r\n case 38:\r\n case \"ArrowUp\":\r\n event.preventDefault();\r\n this._keyboardNavigation = true;\r\n this._moveSelection(PREV);\r\n break;\r\n case 40:\r\n case \"ArrowDown\":\r\n event.preventDefault();\r\n this._keyboardNavigation = true;\r\n if (this.isDropdownVisible()) {\r\n this._moveSelection(NEXT);\r\n } else {\r\n // show menu regardless of input length\r\n this.showOrSearch(false);\r\n }\r\n break;\r\n case 8:\r\n case \"Backspace\":\r\n // If the current item is empty, remove the last one\r\n const lastItem = this.getLastItem();\r\n if (this._searchInput.value.length == 0 && lastItem) {\r\n this._config\r\n .confirmClear(lastItem, this)\r\n .then(() => {\r\n this.removeLastItem();\r\n this._adjustWidth();\r\n this.showOrSearch();\r\n })\r\n .catch(() => {});\r\n }\r\n break;\r\n case 27:\r\n case \"Escape\":\r\n this._searchInput.focus();\r\n this.hideSuggestions();\r\n break;\r\n }\r\n }\r\n\r\n onmousemove(e) {\r\n // Moving the mouse means no longer using keyboard\r\n this._keyboardNavigation = false;\r\n }\r\n\r\n onmouseleave(e) {\r\n // remove selection\r\n this.removeSelection();\r\n }\r\n\r\n onscroll(e) {\r\n this._positionMenu();\r\n }\r\n\r\n onresize(e) {\r\n this._positionMenu();\r\n }\r\n\r\n onclick(e = null) {\r\n if (e) {\r\n e.preventDefault();\r\n }\r\n if (!this.isSingle() && this.isMaxReached()) {\r\n return;\r\n }\r\n // Focus on input when clicking on element or focusing select\r\n this._searchInput.focus();\r\n }\r\n\r\n onreset(e) {\r\n this.reset();\r\n }\r\n\r\n // #endregion\r\n\r\n /**\r\n * @param {Boolean} init called during init\r\n */\r\n loadData(init = false) {\r\n if (Object.keys(this._config.items).length > 0) {\r\n this.setData(this._config.items, true);\r\n } else {\r\n // This will setData at the end\r\n this.resetSuggestions(true);\r\n }\r\n\r\n if (this._config.server) {\r\n if (this._config.liveServer) {\r\n // No need to load anything since it will happen when typing\r\n // Initial values are loaded from config items or from provided options\r\n } else {\r\n this._loadFromServer(!init);\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Make sure we have valid selected attributes\r\n */\r\n _setSelectedAttributes() {\r\n // we use selectedOptions because single select can have a selected option without a selected attribute if it's the first value\r\n const selectedOptions = this._selectElement.selectedOptions || [];\r\n for (let j = 0; j < selectedOptions.length; j++) {\r\n // Enforce selected attr for consistency\r\n if (selectedOptions[j].value && !selectedOptions[j].hasAttribute(\"selected\")) {\r\n selectedOptions[j].setAttribute(\"selected\", \"selected\");\r\n }\r\n }\r\n }\r\n\r\n resetState() {\r\n if (this.isDisabled()) {\r\n this._holderElement.setAttribute(\"readonly\", \"\");\r\n this._searchInput.setAttribute(\"disabled\", \"\");\r\n this._holderElement.classList.add(DISABLED_CLASS);\r\n } else {\r\n rmAttr(this._holderElement, \"readonly\");\r\n rmAttr(this._searchInput, \"disabled\");\r\n this._holderElement.classList.remove(DISABLED_CLASS);\r\n }\r\n }\r\n\r\n /**\r\n * Reset suggestions from select element\r\n * Iterates over option children then calls setData\r\n * @param {Boolean} init called during init\r\n */\r\n resetSuggestions(init = false) {\r\n this._setSelectedAttributes();\r\n\r\n const convertOption = (option) => {\r\n return {\r\n value: option.getAttribute(\"value\"),\r\n label: option.textContent,\r\n disabled: option.disabled,\r\n //@ts-ignore\r\n selected: option.selected,\r\n data: Object.assign(\r\n {\r\n disabled: option.disabled, // pass as data as well\r\n },\r\n option.dataset\r\n ),\r\n };\r\n };\r\n\r\n let suggestions = Array.from(this._selectElement.children)\r\n .filter(\r\n /**\r\n * @param {HTMLOptionElement|HTMLOptGroupElement} option\r\n */\r\n (option) => {\r\n return option.hasAttribute(\"label\") || !option.disabled || this._config.showDisabled;\r\n }\r\n )\r\n .map(\r\n /**\r\n * @param {HTMLOptionElement|HTMLOptGroupElement} option\r\n */\r\n (option) => {\r\n if (option.hasAttribute(\"label\")) {\r\n return {\r\n group: option.getAttribute(\"label\"),\r\n items: Array.from(option.children).map((option) => {\r\n return convertOption(option);\r\n }),\r\n };\r\n }\r\n return convertOption(option);\r\n }\r\n );\r\n\r\n this.setData(suggestions, init);\r\n }\r\n\r\n /**\r\n * Try to add the current value\r\n * @returns {Boolean}\r\n */\r\n _enterValue() {\r\n let selection = this.getSelection();\r\n if (selection) {\r\n selection.click();\r\n return true;\r\n } else {\r\n // We use what is typed if not selected and not empty\r\n if (this._config.allowNew && this._searchInput.value) {\r\n let text = this._searchInput.value;\r\n this._config\r\n .confirmAdd(text, this)\r\n .then(() => {\r\n this._add(text, text, { new: 1 });\r\n })\r\n .catch(() => {});\r\n return true;\r\n }\r\n }\r\n return false;\r\n }\r\n\r\n /**\r\n * @param {Boolean} show Show menu after load. False during init\r\n */\r\n _loadFromServer(show = false) {\r\n if (this._abortController) {\r\n this._abortController.abort();\r\n }\r\n this._abortController = new AbortController();\r\n\r\n // Read data params dynamically as well (eg: for vue JS)\r\n let extraParams = this._selectElement.dataset.serverParams || {};\r\n if (typeof extraParams == \"string\") {\r\n extraParams = JSON.parse(extraParams);\r\n }\r\n const params = Object.assign({}, this._config.serverParams, extraParams);\r\n // Pass current value\r\n params[this._config.queryParam] = this._searchInput.value;\r\n // Prevent caching\r\n if (this._config.noCache) {\r\n params.t = Date.now();\r\n }\r\n // We have a related field\r\n if (params.related) {\r\n /**\r\n * @type {HTMLInputElement}\r\n */\r\n //@ts-ignore\r\n const input = document.getElementById(params.related);\r\n if (input) {\r\n params.related = input.value;\r\n const inputName = input.getAttribute(\"name\");\r\n if (inputName) {\r\n params[inputName] = input.value;\r\n }\r\n }\r\n }\r\n\r\n const urlParams = new URLSearchParams(params);\r\n let url = this._config.server;\r\n let fetchOptions = Object.assign(this._config.fetchOptions, {\r\n method: this._config.serverMethod || \"GET\",\r\n signal: this._abortController.signal,\r\n });\r\n\r\n if (fetchOptions.method === \"POST\") {\r\n fetchOptions.body = urlParams;\r\n } else {\r\n url += \"?\" + urlParams.toString();\r\n }\r\n\r\n this._holderElement.classList.add(LOADING_CLASS);\r\n fetch(url, fetchOptions)\r\n .then((r) => this._config.onServerResponse(r, this))\r\n .then((suggestions) => {\r\n const data = suggestions[this._config.serverDataKey] || suggestions;\r\n this.setData(data, !show);\r\n this._abortController = null;\r\n if (show) {\r\n this._showSuggestions();\r\n }\r\n })\r\n .catch((e) => {\r\n // Current version of Firefox rejects the promise with a DOMException\r\n if (e.name === \"AbortError\" || this._abortController.signal.aborted) {\r\n return;\r\n }\r\n console.error(e);\r\n })\r\n .finally((e) => {\r\n this._holderElement.classList.remove(LOADING_CLASS);\r\n });\r\n }\r\n\r\n /**\r\n * Wrapper for the public addItem method that check if the item\r\n * can be added\r\n *\r\n * @param {string} text\r\n * @param {string} value\r\n * @param {object} data\r\n * @returns {HTMLOptionElement|null}\r\n */\r\n _add(text, value = null, data = {}) {\r\n if (!this.canAdd(text, data)) {\r\n return null;\r\n }\r\n const el = this.addItem(text, value, data);\r\n this._resetHtmlState();\r\n if (this._config.keepOpen) {\r\n this._showSuggestions();\r\n } else {\r\n this.resetSearchInput();\r\n }\r\n return el;\r\n }\r\n\r\n /**\r\n * @param {HTMLElement} li\r\n * @returns {Boolean}\r\n */\r\n _isItemEnabled(li) {\r\n if (li.style.display === \"none\") {\r\n return false;\r\n }\r\n const fc = li.firstElementChild;\r\n return fc.tagName === \"A\" && !fc.classList.contains(\"disabled\");\r\n }\r\n\r\n /**\r\n * @param {String} dir\r\n * @param {*|HTMLElement} sel\r\n * @returns {HTMLElement}\r\n */\r\n _moveSelection(dir = NEXT, sel = null) {\r\n const active = this.getSelection();\r\n\r\n // select first li if visible\r\n if (!active) {\r\n // no active selection, cannot go back\r\n if (dir === PREV) {\r\n return sel;\r\n }\r\n // find first enabled item\r\n if (!sel) {\r\n sel = this._dropElement.firstChild;\r\n while (sel && !this._isItemEnabled(sel)) {\r\n sel = sel[\"nextSibling\"];\r\n }\r\n }\r\n } else {\r\n const sibling = dir === NEXT ? \"nextSibling\" : \"previousSibling\";\r\n\r\n // Iterate over visible li\r\n sel = active.parentNode;\r\n do {\r\n sel = sel[sibling];\r\n } while (sel && !this._isItemEnabled(sel));\r\n\r\n // We have a new selection\r\n if (sel) {\r\n // Remove classes from current active\r\n active.classList.remove(...this._activeClasses());\r\n } else if (active) {\r\n // Use active element as selection\r\n sel = active.parentElement;\r\n }\r\n }\r\n\r\n if (sel) {\r\n // Scroll if necessary\r\n const selHeight = sel.offsetHeight;\r\n const selTop = sel.offsetTop;\r\n const parent = sel.parentNode;\r\n const parentHeight = parent.offsetHeight;\r\n const parentScrollHeight = parent.scrollHeight;\r\n const parentTop = parent.offsetTop;\r\n\r\n // Reset scroll, this can happen if menu was scrolled then hidden\r\n if (selHeight === 0) {\r\n setTimeout(() => {\r\n parent.scrollTop = 0;\r\n });\r\n }\r\n\r\n if (dir === PREV) {\r\n // Don't use scrollIntoView as it scrolls the whole window\r\n // Avoid minor top scroll due to headers\r\n const scrollTop = selTop - parentTop > 10 ? selTop - parentTop : 0;\r\n parent.scrollTop = scrollTop;\r\n } else {\r\n // This is the equivalent of scrollIntoView(false) but only for parent node\r\n // Only scroll if the element is not visible\r\n const scrollNeeded = selTop + selHeight - (parentHeight + parent.scrollTop);\r\n if (scrollNeeded > 0 && selHeight > 0) {\r\n parent.scrollTop = selTop + selHeight - parentHeight + 1;\r\n // On last element, make sure we scroll the the bottom\r\n if (parent.scrollTop + parentHeight >= parentScrollHeight - 10) {\r\n parent.scrollTop = selTop - parentTop;\r\n }\r\n }\r\n }\r\n\r\n // Adjust link\r\n const a = sel.querySelector(\"a\");\r\n a.classList.add(...this._activeClasses());\r\n this._searchInput.setAttribute(\"aria-activedescendant\", a.id);\r\n if (this._config.updateOnSelect) {\r\n this._searchInput.value = a.dataset.label;\r\n this._adjustWidth();\r\n }\r\n } else {\r\n this._searchInput.setAttribute(\"aria-activedescendant\", \"\");\r\n }\r\n return sel;\r\n }\r\n\r\n /**\r\n * Adjust the field to fit its content and show/hide placeholder if needed\r\n */\r\n _adjustWidth() {\r\n this._holderElement.classList.remove(PLACEHOLDER_CLASS);\r\n if (this._searchInput.value) {\r\n this._searchInput.size = this._searchInput.value.length;\r\n } else {\r\n // Show the placeholder only if empty\r\n if (this.getSelectedValues().length) {\r\n this._searchInput.placeholder = \"\";\r\n this._searchInput.size = 1;\r\n } else {\r\n this._searchInput.size = this._config.placeholder.length > 0 ? this._config.placeholder.length : 1;\r\n this._searchInput.placeholder = this._config.placeholder;\r\n this._holderElement.classList.add(PLACEHOLDER_CLASS);\r\n }\r\n }\r\n\r\n // If the string contains ascii chars or strange font, input size may be wrong\r\n // We cannot only rely on the size attribute\r\n const v = this._searchInput.value || this._searchInput.placeholder;\r\n const computedFontSize = window.getComputedStyle(this._holderElement).fontSize;\r\n const w = calcTextWidth(v, computedFontSize) + 16;\r\n this._searchInput.style.width = w + \"px\"; // Don't use minWidth as it would prevent using maxWidth\r\n }\r\n\r\n /**\r\n * Add suggestions to the drop element\r\n * @param {Array} suggestions\r\n */\r\n _buildSuggestions(suggestions) {\r\n while (this._dropElement.lastChild) {\r\n this._dropElement.removeChild(this._dropElement.lastChild);\r\n }\r\n let idx = 0;\r\n let groupId = 1; // start at one, because data-id = \"\" + 0 doesn't do anything\r\n for (let i = 0; i < suggestions.length; i++) {\r\n const suggestion = suggestions[i];\r\n\r\n if (!suggestion) {\r\n continue;\r\n }\r\n\r\n // Handle optgroups\r\n if (suggestion[\"group\"] && suggestion[\"items\"]) {\r\n const newChild = ce(\"li\");\r\n newChild.setAttribute(\"role\", \"presentation\");\r\n newChild.dataset.id = \"\" + groupId;\r\n const newChildSpan = ce(\"span\");\r\n newChild.append(newChildSpan);\r\n newChildSpan.classList.add(...[\"dropdown-header\", \"text-truncate\"]);\r\n newChildSpan.innerHTML = suggestion[\"group\"];\r\n this._dropElement.appendChild(newChild);\r\n\r\n if (suggestion[\"items\"]) {\r\n for (let j = 0; j < suggestion[\"items\"].length; j++) {\r\n const groupSuggestion = suggestion[\"items\"][j];\r\n groupSuggestion.group_id = groupId;\r\n this._buildSuggestionsItem(suggestion[\"items\"][j], idx);\r\n idx++;\r\n }\r\n }\r\n\r\n groupId++;\r\n }\r\n\r\n //@ts-ignore\r\n this._buildSuggestionsItem(suggestion, idx);\r\n idx++;\r\n }\r\n\r\n // Create the not found message\r\n if (this._config.notFoundMessage) {\r\n const notFound = ce(\"li\");\r\n notFound.setAttribute(\"role\", \"presentation\");\r\n notFound.classList.add(CLASS_PREFIX + \"not-found\");\r\n // Actual message is refreshed on typing, but we need item for consistency\r\n notFound.innerHTML = ``;\r\n this._dropElement.appendChild(notFound);\r\n }\r\n }\r\n\r\n /**\r\n * @param {Suggestion} suggestion\r\n * @param {Number} i The global counter\r\n */\r\n _buildSuggestionsItem(suggestion, i) {\r\n if (!suggestion[this._config.valueField]) {\r\n return;\r\n }\r\n\r\n const value = suggestion[this._config.valueField];\r\n const label = suggestion[this._config.labelField];\r\n\r\n let textContent = this._config.onRenderItem(suggestion, label, this);\r\n\r\n const newChild = ce(\"li\");\r\n newChild.setAttribute(\"role\", \"presentation\");\r\n if (suggestion.group_id) {\r\n newChild.setAttribute(\"data-group-id\", \"\" + suggestion.group_id);\r\n }\r\n const newChildLink = ce(\"a\");\r\n newChild.append(newChildLink);\r\n newChildLink.id = this._dropElement.id + \"-\" + i;\r\n newChildLink.classList.add(...[\"dropdown-item\", \"text-truncate\"]);\r\n if (suggestion.disabled) {\r\n newChildLink.classList.add(...[\"disabled\"]);\r\n }\r\n newChildLink.setAttribute(VALUE_ATTRIBUTE, value);\r\n newChildLink.dataset.label = label;\r\n this._config.searchFields.forEach((sf) => {\r\n newChild.dataset[sf] = suggestion[sf];\r\n });\r\n newChildLink.setAttribute(\"href\", \"#\");\r\n newChildLink.innerHTML = textContent;\r\n this._dropElement.appendChild(newChild);\r\n\r\n // Hover sets active item\r\n newChildLink.addEventListener(\"mouseenter\", (event) => {\r\n // Don't trigger enter if using arrows\r\n if (this._keyboardNavigation) {\r\n return;\r\n }\r\n this.removeSelection();\r\n newChild.querySelector(\"a\").classList.add(...this._activeClasses());\r\n });\r\n newChildLink.addEventListener(\"mousedown\", (event) => {\r\n // Otherwise searchInput would lose focus and close the menu\r\n event.preventDefault();\r\n });\r\n newChildLink.addEventListener(\"click\", (event) => {\r\n event.preventDefault();\r\n event.stopPropagation();\r\n this._config\r\n .confirmAdd(value, this)\r\n .then(() => {\r\n this._add(label, value, suggestion.data);\r\n this._config.onSelectItem(suggestion, this);\r\n })\r\n .catch(() => {});\r\n });\r\n }\r\n\r\n /**\r\n * @returns {NodeListOf}\r\n */\r\n initialOptions() {\r\n return this._selectElement.querySelectorAll(\"option[data-init]\");\r\n }\r\n\r\n /**\r\n * Call this before looping in a list that calls addItem\r\n * This will make sure addItem will not add incorrectly options to the select\r\n */\r\n _removeSelectedAttrs() {\r\n this._selectElement.querySelectorAll(\"option\").forEach((opt) => {\r\n rmAttr(opt, \"selected\");\r\n });\r\n }\r\n\r\n reset() {\r\n this.removeAll();\r\n\r\n // Reset doesn't fire change event\r\n this._fireEvents = false;\r\n const opts = this.initialOptions();\r\n this._removeSelectedAttrs();\r\n for (let j = 0; j < opts.length; j++) {\r\n const iv = opts[j];\r\n const data = Object.assign(\r\n {},\r\n {\r\n disabled: iv.hasAttribute(\"disabled\"),\r\n },\r\n iv.dataset\r\n );\r\n this.addItem(iv.textContent, iv.value, data);\r\n }\r\n this._resetHtmlState();\r\n this._fireEvents = true;\r\n }\r\n\r\n /**\r\n * @param {Boolean} init Pass true during init\r\n */\r\n resetSearchInput(init = false) {\r\n this._searchInput.value = \"\";\r\n this._adjustWidth();\r\n\r\n if (!init) {\r\n if (!this._shouldShow()) {\r\n this.hideSuggestions();\r\n }\r\n\r\n // Trigger input even to show suggestions if needed when focused\r\n if (this._searchInput === document.activeElement) {\r\n this._searchInput.dispatchEvent(new Event(\"input\"));\r\n }\r\n }\r\n\r\n // We use visibility instead of display to keep layout intact\r\n if (this.isMaxReached()) {\r\n this._holderElement.classList.add(MAX_REACHED_CLASS);\r\n this._searchInput.style.visibility = \"hidden\";\r\n } else {\r\n if (this._searchInput.style.visibility == \"hidden\") {\r\n this._searchInput.style.visibility = \"visible\";\r\n }\r\n }\r\n\r\n if (this.isSingle() && !init) {\r\n //@ts-ignore\r\n document.activeElement.blur();\r\n }\r\n }\r\n\r\n /**\r\n * @returns {Array}\r\n */\r\n getSelectedValues() {\r\n // option[selected] is used rather that selectedOptions as it works more consistently\r\n /**\r\n * @type {NodeListOf}\r\n */\r\n const selected = this._selectElement.querySelectorAll(\"option[selected]\");\r\n return Array.from(selected).map((el) => el.value);\r\n }\r\n\r\n /**\r\n * @returns {Array}\r\n */\r\n getAvailableValues() {\r\n /**\r\n * @type {NodeListOf}\r\n */\r\n const selected = this._selectElement.querySelectorAll(\"option\");\r\n return Array.from(selected).map((el) => el.value);\r\n }\r\n\r\n /**\r\n * Show suggestions or search them depending on live server\r\n * @param {Boolean} check\r\n */\r\n showOrSearch(check = true) {\r\n if (check && !this._shouldShow()) {\r\n // focusing should not clear validation\r\n this.hideSuggestions(false);\r\n return;\r\n }\r\n if (this._config.liveServer) {\r\n this._searchFunc();\r\n } else {\r\n this._showSuggestions();\r\n }\r\n }\r\n\r\n /**\r\n * The element create with buildSuggestions\r\n * @param {Boolean} clearValidation\r\n */\r\n hideSuggestions(clearValidation = true) {\r\n this._dropElement.classList.remove(SHOW_CLASS);\r\n attrs(this._searchInput, {\r\n \"aria-expanded\": \"false\",\r\n });\r\n this.removeSelection();\r\n if (clearValidation) {\r\n this._holderElement.classList.remove(INVALID_CLASS);\r\n }\r\n }\r\n\r\n /**\r\n * Show or hide suggestions\r\n * @param {Boolean} check\r\n * @param {Boolean} clearValidation\r\n */\r\n toggleSuggestions(check = true, clearValidation = true) {\r\n if (this._dropElement.classList.contains(SHOW_CLASS)) {\r\n this.hideSuggestions(clearValidation);\r\n } else {\r\n this.showOrSearch(check);\r\n }\r\n }\r\n\r\n /**\r\n * Do we have enough input to show suggestions ?\r\n * @returns {Boolean}\r\n */\r\n _shouldShow() {\r\n if (this.isDisabled() || this.isMaxReached()) {\r\n return false;\r\n }\r\n return this._searchInput.value.length >= this._config.suggestionsThreshold;\r\n }\r\n\r\n /**\r\n * The element create with buildSuggestions\r\n */\r\n _showSuggestions() {\r\n // It's not focused anymore\r\n if (document.activeElement != this._searchInput) {\r\n return;\r\n }\r\n // Never show suggestions if you cannot add new values\r\n if (this._searchInput.style.visibility == \"hidden\") {\r\n return;\r\n }\r\n\r\n const lookup = normalize(this._searchInput.value);\r\n\r\n const valueCounter = {};\r\n\r\n // Filter the list according to search string\r\n const list = this._dropElement.querySelectorAll(\"li\");\r\n let count = 0;\r\n let firstItem = null;\r\n let hasPossibleValues = false;\r\n let visibleGroups = {};\r\n for (let i = 0; i < list.length; i++) {\r\n /**\r\n * @type {HTMLLIElement}\r\n */\r\n let item = list[i];\r\n /**\r\n * @type {HTMLAnchorElement|HTMLSpanElement}\r\n */\r\n //@ts-ignore\r\n let link = item.firstElementChild;\r\n\r\n // This is the empty result message or a header\r\n if (link instanceof HTMLSpanElement) {\r\n // We will show it later\r\n if (item.dataset.id) {\r\n visibleGroups[item.dataset.id] = false;\r\n }\r\n hideItem(item);\r\n continue;\r\n }\r\n\r\n // Remove previous selection\r\n link.classList.remove(...this._activeClasses());\r\n\r\n // Hide selected values\r\n if (!this._config.allowSame) {\r\n const v = link.getAttribute(VALUE_ATTRIBUTE);\r\n // Find if the matching option is already selected by index to deal with same values\r\n valueCounter[v] = valueCounter[v] || 0;\r\n const opt = this._findOption(link.getAttribute(VALUE_ATTRIBUTE), \"[selected]\", valueCounter[v]++);\r\n if (opt) {\r\n hideItem(item);\r\n continue;\r\n }\r\n }\r\n\r\n // Check search length since we can trigger dropdown with arrow\r\n const showAllSuggestions = this._config.showAllSuggestions || lookup.length === 0;\r\n // Do we find a matching string or do we display immediately ?\r\n let isMatched = lookup.length == 0 && this._config.suggestionsThreshold === 0;\r\n if (!showAllSuggestions && lookup.length > 0) {\r\n // match on any field\r\n this._config.searchFields.forEach((sf) => {\r\n const text = normalize(link.dataset[sf]);\r\n let found = false;\r\n if (this._config.fuzzy) {\r\n found = fuzzyMatch(text, lookup);\r\n } else {\r\n const idx = text.indexOf(lookup);\r\n found = this._config.startsWith ? idx === 0 : idx >= 0;\r\n }\r\n if (found) {\r\n isMatched = true;\r\n }\r\n });\r\n }\r\n\r\n const selectFirst = isMatched || lookup.length === 0;\r\n if (showAllSuggestions || isMatched) {\r\n count++;\r\n showItem(item);\r\n if (item.dataset.groupId) {\r\n visibleGroups[item.dataset.groupId] = true;\r\n }\r\n // Only select as first item if its matching or no lookup\r\n if (!firstItem && this._isItemEnabled(item) && selectFirst) {\r\n firstItem = item;\r\n }\r\n if (this._config.maximumItems > 0 && count > this._config.maximumItems) {\r\n hideItem(item);\r\n }\r\n } else {\r\n hideItem(item);\r\n }\r\n\r\n if (this._config.highlightTyped) {\r\n // using .textContent removes any html that can be present (eg: mark added through highlightTyped)\r\n const textContent = link.textContent;\r\n const idx = normalize(textContent).indexOf(lookup);\r\n const highlighted =\r\n textContent.substring(0, idx) +\r\n `${textContent.substring(idx, idx + lookup.length)}` +\r\n textContent.substring(idx + lookup.length, textContent.length);\r\n link.innerHTML = highlighted;\r\n }\r\n\r\n if (this._isItemEnabled(item)) {\r\n hasPossibleValues = true;\r\n }\r\n }\r\n\r\n // No item and we don't allow new items => error\r\n if (!this._config.allowNew && !(lookup.length === 0 && !hasPossibleValues)) {\r\n this._holderElement.classList.add(INVALID_CLASS);\r\n }\r\n\r\n // If we allow new elements, regex validation should happen on canAdd instead\r\n if (this._config.allowNew && this._config.regex && this.isInvalid()) {\r\n this._holderElement.classList.remove(INVALID_CLASS);\r\n }\r\n\r\n // Show all groups with visible values\r\n Array.from(list)\r\n .filter((li) => {\r\n return li.dataset.id;\r\n })\r\n .forEach((li) => {\r\n if (visibleGroups[li.dataset.id] === true) {\r\n showItem(li);\r\n }\r\n });\r\n\r\n if (hasPossibleValues) {\r\n // Remove validation message if we show selectable values\r\n this._holderElement.classList.remove(INVALID_CLASS);\r\n\r\n // Autoselect first\r\n if (firstItem && this._config.autoselectFirst) {\r\n this.removeSelection();\r\n this._moveSelection(NEXT, firstItem);\r\n }\r\n }\r\n\r\n // Remove dropdown if list is empty\r\n if (count === 0) {\r\n if (this._config.notFoundMessage) {\r\n /**\r\n * @type {HTMLElement}\r\n */\r\n const notFound = this._dropElement.querySelector(\".\" + CLASS_PREFIX + \"not-found\");\r\n notFound.style.display = \"block\";\r\n const notFoundMessage = this._config.notFoundMessage.replace(\"{{tag}}\", this._searchInput.value);\r\n notFound.innerHTML = `${notFoundMessage}`;\r\n this._showDropdown();\r\n } else {\r\n // Remove dropdown if not found (do not clear validation)\r\n this.hideSuggestions(false);\r\n }\r\n } else {\r\n // Or show it if necessary\r\n this._showDropdown();\r\n }\r\n }\r\n\r\n _showDropdown() {\r\n const isVisible = this._dropElement.classList.contains(SHOW_CLASS);\r\n if (!isVisible) {\r\n this._dropElement.classList.add(SHOW_CLASS);\r\n attrs(this._searchInput, {\r\n \"aria-expanded\": \"true\",\r\n });\r\n }\r\n this._positionMenu(isVisible);\r\n }\r\n\r\n /**\r\n * @param {Boolean} wasVisible\r\n */\r\n _positionMenu(wasVisible = false) {\r\n const isRTL = this._rtl;\r\n const fixed = this._config.fixed;\r\n const fullWidth = this._config.fullWidth;\r\n const bounds = this._searchInput.getBoundingClientRect();\r\n const holderBounds = this._holderElement.getBoundingClientRect();\r\n\r\n let left = 0;\r\n let top = 0;\r\n\r\n if (fixed) {\r\n // In full width, use holder as left reference, otherwise use input\r\n if (fullWidth) {\r\n left = holderBounds.x;\r\n top = holderBounds.y + holderBounds.height + 2; // 2px offset\r\n } else {\r\n left = bounds.x;\r\n top = bounds.y + bounds.height;\r\n }\r\n } else {\r\n // When positioning is not fixed, we leave it up to the browser\r\n // it may not work in complex situations with scrollable overflows, etc\r\n if (fullWidth) {\r\n // Stick it at the start\r\n left = 0;\r\n // Move it below\r\n top = holderBounds.height + 2; // 2px offset\r\n } else {\r\n // Position next to input (offsetLeft != bounds.x)\r\n left = this._searchInput.offsetLeft;\r\n top = this._searchInput.offsetHeight + this._searchInput.offsetTop;\r\n }\r\n }\r\n\r\n // Align end\r\n if (isRTL && !fullWidth) {\r\n left -= this._dropElement.offsetWidth - bounds.width;\r\n }\r\n\r\n // Horizontal overflow\r\n if (!fullWidth) {\r\n const w = Math.min(window.innerWidth, document.body.offsetWidth);\r\n const hdiff = isRTL\r\n ? bounds.x + bounds.width - this._dropElement.offsetWidth - 1\r\n : w - 1 - (bounds.x + this._dropElement.offsetWidth);\r\n if (hdiff < 0) {\r\n left = isRTL ? left - hdiff : left + hdiff;\r\n }\r\n }\r\n\r\n // Use full holder width\r\n if (fullWidth) {\r\n this._dropElement.style.width = this._holderElement.offsetWidth + \"px\";\r\n }\r\n\r\n if (!wasVisible) {\r\n // Reset any height overflow adjustement\r\n this._dropElement.style.transform = \"unset\";\r\n }\r\n\r\n Object.assign(this._dropElement.style, {\r\n // Position element\r\n left: left + \"px\",\r\n top: top + \"px\",\r\n });\r\n\r\n // Overflow height\r\n const dropBounds = this._dropElement.getBoundingClientRect();\r\n const h = window.innerHeight;\r\n\r\n // We display above input if it overflows\r\n if (dropBounds.y + dropBounds.height > h || this._dropElement.style.transform.includes(\"translateY\")) {\r\n // We need to add the offset twice\r\n const topOffset = fullWidth ? holderBounds.height + 4 : bounds.height;\r\n // In chrome, we need 100.1% to avoid blurry text\r\n // @link https://stackoverflow.com/questions/32034574/font-looks-blurry-after-translate-in-chrome\r\n this._dropElement.style.transform = \"translateY(calc(-100.1% - \" + topOffset + \"px))\";\r\n }\r\n }\r\n\r\n /**\r\n * @returns {Number}\r\n */\r\n _getBootstrapVersion() {\r\n let ver = 5;\r\n // If we have jQuery and the tooltip plugin for BS4\r\n //@ts-ignore\r\n let jq = window.jQuery;\r\n if (jq && jq.fn.tooltip && jq.fn.tooltip.Constructor) {\r\n ver = parseInt(jq.fn.tooltip.Constructor.VERSION.charAt(0));\r\n }\r\n return ver;\r\n }\r\n\r\n /**\r\n * Find if label is already selected (based on attribute)\r\n * @param {string} text\r\n * @returns {Boolean}\r\n */\r\n _isSelected(text) {\r\n const arr = Array.from(this._selectElement.querySelectorAll(\"option\"));\r\n const selOpt = arr.find((el) => el.textContent == text && el.getAttribute(\"selected\"));\r\n return selOpt ? true : false;\r\n }\r\n\r\n /**\r\n * Find if label is already selectable (based on attribute)\r\n * @param {string} text\r\n * @returns {Boolean}\r\n */\r\n _isSelectable(text) {\r\n const arr = Array.from(this._selectElement.querySelectorAll(\"option\"));\r\n const opts = arr.filter((el) => el.textContent == text);\r\n // Only consider actual