as-select3
Version:
Modern JavaScript Select Library with advanced features and HTML rendering support
9 lines • 20 kB
JavaScript
/*!
* As-Select3 - Modern JavaScript Select Library
* Version: 1.5.0
* Author: Sunil Kumar
* Repository: https://github.com/sunil4587/As-select3
* License: MIT
*/
!function(e){"use strict";const t={placeholder:"Choose an option...",searchable:!0,selectAll:!0,clearAll:!0,maxSelection:null,remote:null,searchDelay:300,noResultsText:"No results found",loadingText:"Loading...",searchPlaceholder:"Search options...",selectAllText:"Select All",clearAllText:"Clear All",defaultIconClass:"as-select3-arrow-down",iconPrefix:null,allowHtml:!0,fuzzySearch:!0,highlightMatches:!0,virtualScrolling:!1,itemHeight:32,maxHeight:300,escapeMarkup:e=>e,templateResult:null,templateSelection:null,matcher:null};class s{constructor(s,i={}){if(!e(s).length)throw new Error(`As-Select3: Element "${s}" not found`);this.$element=e(s),this.element=this.$element[0],this.isMultiple=void 0!==this.$element.attr("multiple"),this.options=Object.assign({},t,{placeholder:this.isMultiple?"Select options...":t.placeholder,selectAll:this.isMultiple&&t.selectAll,searchable:!1!==this.$element.data("search")},i),this._initContainer(),this._initState(),this.init()}_initContainer(){let t=this.$element.closest(".as-select3-container");t.length||(t=e('<div class="as-select3-container"></div>'),this.$element.before(t),t.append(this.$element)),this.$container=t}_initState(){this.isOpen=!1,this.focusIndex=-1,this.selectedValues=this.isMultiple?[]:this.$element.find("option:selected").val()||null,this.isLoading=!1,this.searchTimer=null,this.searchCache=new Map,this.boundHandlers={}}init(){this.createUI(),this.bindEvents(),this.updateFromSelect(),this.storeOriginalOptions()}createUI(){const t=`\n <div class="as-select3-trigger" tabindex="0" role="combobox" aria-expanded="false">\n <div class="as-select3-selection">\n <span class="as-select3-placeholder">${this.options.placeholder}</span>\n </div>\n <span class="${this.options.defaultIconClass} as-select3-arrow"></span>\n </div>\n `;this.$trigger=e(t),this.$selection=this.$trigger.find(".as-select3-selection"),this.$placeholder=this.$trigger.find(".as-select3-placeholder"),this.$arrow=this.$trigger.find(".as-select3-arrow"),this.$container.append(this.$trigger),this.$element.addClass("d-none"),this.createDropdown()}createDropdown(){this.$dropdown=e('<div class="as-select3-dropdown" role="listbox"></div>'),this.options.searchable&&this.createSearch(),this.createOptionsContainer(),this.isMultiple&&(this.shouldShowSelectAll()||this.options.clearAll)&&this.createActions(),this.$container.append(this.$dropdown)}createSearch(){const t=`\n <div class="as-select3-search">\n <div class="position-relative w-100">\n <input type="text" class="form-control form-control-sm" \n role="searchbox" placeholder="${this.options.searchPlaceholder}">\n <span class="as-select3-close as-select3-search-clear"></span>\n </div>\n </div>\n `,s=e(t);this.$searchInput=s.find("input"),this.$dropdown.append(s)}createOptionsContainer(){this.$optionsContainer=e('<ul class="as-select3-options"></ul>'),this.$element.find("option").each(((e,t)=>{this.$optionsContainer.append(this.createOptionElement(t,e))})),this.$dropdown.append(this.$optionsContainer)}createOptionElement(t,s){const i=t instanceof HTMLOptionElement?{value:t.value,text:t.text,selected:t.selected,icon:e(t).data("icon")||e(t).attr("data-icon"),html:e(t).data("html")||e(t).attr("data-html")||e(t).html(),disabled:t.disabled}:t,n=e('<li class="as-select3-option" role="option" tabindex="-1"></li>').addClass(i.selected?"selected":"").addClass(i.disabled?"disabled":"").attr({"data-value":i.value,"data-index":s,"data-text":i.text,"data-icon":i.icon||"","data-html":i.html||"","aria-selected":i.selected.toString()}),l=e('<div class="as-select3-option-left"></div>');if(i.icon){const t=e('<div class="as-select3-option-icon"></div>');this.addIcon(t,i.icon),l.append(t)}return l.append(e('<span class="as-select3-option-text"></span>').text(i.text)),n.append(l),this.isMultiple&&n.append(e('<input type="checkbox" class="form-check-input">').prop("checked",i.selected)),n}addIcon(t,s,i="16px"){if(s)if(s.match(/^(https?:|data:|\.?\/)/))t.append(e("<img>").attr({src:s,alt:""}).css({width:i,height:i,objectFit:"cover"}));else if(this.isIconClass(s)){const i=e("<i></i>").addClass(s);t.append(i)}else t.html(s)}isIconClass(e){return["bi-","fa-","fas-","far-","fab-","material-icons","icon-"].some((t=>e.includes(t)))||this.options.iconPrefix&&e.startsWith(this.options.iconPrefix)}shouldShowSelectAll(){if(!this.options.selectAll)return!1;if(!this.options.maxSelection)return!0;const e=this.$element.find("option:not(:disabled)").length;return this.options.maxSelection>=e}createActions(){if(!this.isMultiple)return;const e=`\n <div class="as-select3-actions">\n ${this.shouldShowSelectAll()?`<button type="button" class="btn btn-sm btn-outline-primary select-all">${this.options.selectAllText}</button>`:""}\n ${this.options.clearAll?`<button type="button" class="btn btn-sm btn-outline-secondary clear-all">${this.options.clearAllText}</button>`:""}\n </div>\n `;this.$dropdown.append(e)}bindEvents(){this.boundHandlers={triggerClick:this.handleTriggerClick.bind(this),documentClick:this.handleDocumentClick.bind(this),keyDown:this.handleKeyDown.bind(this),optionClick:this.handleOptionClick.bind(this),searchInput:this.debounce(this.handleSearchInput.bind(this),this.options.searchDelay),clearClick:this.handleClearClick.bind(this),actionClick:this.handleActionClick.bind(this)},this.$trigger.on("click",this.boundHandlers.triggerClick),this.$trigger.on("keydown",this.boundHandlers.keyDown),this.$container.on("click",".as-select3-option",this.boundHandlers.optionClick),this.$container.on("click",".as-select3-clear, .as-select3-tag-remove, .as-select3-search-clear",this.boundHandlers.clearClick),this.$container.on("click",".select-all, .clear-all",this.boundHandlers.actionClick),this.$searchInput&&this.$searchInput.on("input",this.boundHandlers.searchInput),e(document).on("click.asSelect3",this.boundHandlers.documentClick)}handleTriggerClick(t){e(t.target).hasClass("as-select3-clear")||(t.preventDefault(),this.toggle())}handleDocumentClick(e){this.$container.is(e.target)||this.$container.has(e.target).length||this.close()}handleKeyDown(e){const t={Enter:()=>this.isOpen?this.selectFocused():this.open()," ":()=>this.isOpen?this.selectFocused():this.open(),Escape:()=>this.isOpen&&this.close(),ArrowDown:()=>this.isOpen?this.focusNext():this.open(),ArrowUp:()=>this.isOpen?this.focusPrevious():this.open()};t[e.key]&&(e.preventDefault(),t[e.key]())}handleOptionClick(t){const s=e(t.currentTarget);s.hasClass("disabled")||this.toggleOption(s)}handleSearchInput(e){const t=e.target.value;this.options.remote&&"function"==typeof this.options.remote?this.remoteSearch(t):this.search(t)}handleClearClick(t){t.stopPropagation();const s=e(t.target);if(s.hasClass("as-select3-search-clear"))this.$searchInput.val("").trigger("input").focus();else if(s.hasClass("as-select3-tag-remove")){const e=s.closest(".as-select3-tag").data("value");this.removeTag(e)}else s.hasClass("as-select3-clear")&&this.clearSingle()}handleActionClick(t){const s=e(t.target);s.hasClass("select-all")?this.selectAll():s.hasClass("clear-all")&&this.clearAll()}toggle(){this.isOpen?this.close():this.open()}open(){this.isOpen||(this.isOpen=!0,this.$dropdown.addClass("show"),this.$trigger.addClass("active").attr("aria-expanded","true"),this.$container.addClass("active"),this.$searchInput&&requestAnimationFrame((()=>this.$searchInput.focus())),this.$element.trigger("asSelect3:open"))}close(){this.isOpen&&(this.isOpen=!1,this.$dropdown.removeClass("show"),this.$trigger.removeClass("active").attr("aria-expanded","false"),this.$container.removeClass("active"),this.focusIndex=-1,this.$searchInput&&(this.$searchInput.val(""),this.search("")),this.$element.trigger("asSelect3:close"))}toggleOption(e){if(!e?.length||e.hasClass("disabled"))return;const t=e.data("value");e.hasClass("selected")?this.deselectOption(e,t):this.selectOption(e,t),this.updateSelection(),this.$element.trigger("change").trigger("asSelect3:change",{value:this.getValue()})}selectOption(e,t){if(this.isMultiple){if(this.options.maxSelection&&this.selectedValues.length>=this.options.maxSelection)return void this.$element.trigger("asSelect3:maxselection",{max:this.options.maxSelection});this.selectedValues.push(t),e.addClass("selected").attr("aria-selected","true"),e.find('input[type="checkbox"]').prop("checked",!0),this.updateOptionStates()}else this.$optionsContainer.find(".as-select3-option").removeClass("selected").attr("aria-selected","false"),this.$element.find("option").prop("selected",!1),this.selectedValues=t,e.addClass("selected").attr("aria-selected","true"),this.close();this.$element.find(`option[value="${t}"]`).prop("selected",!0)}updateOptionStates(){if(this.isMultiple&&this.options.maxSelection){const t=this.selectedValues.length>=this.options.maxSelection;this.$optionsContainer.find(".as-select3-option").each(((s,i)=>{const n=e(i),l=n.hasClass("selected"),a=n.find('input[type="checkbox"]');t&&!l?(n.addClass("disabled").attr("aria-disabled","true"),a.prop("disabled",!0)):(n.removeClass("disabled").attr("aria-disabled","false"),a.prop("disabled",!1))}))}}deselectOption(e,t){e.removeClass("selected").attr("aria-selected","false"),this.$element.find(`option[value="${t}"]`).prop("selected",!1),this.isMultiple?(e.find('input[type="checkbox"]').prop("checked",!1),this.selectedValues=this.selectedValues.filter((e=>e!==t)),this.updateOptionStates()):this.selectedValues=null}search(t){if(this.searchCache.has(t))return void this.applySearchResults(this.searchCache.get(t));const s=this.$optionsContainer.find(".as-select3-option"),i=[];let n=!1;s.each(((s,l)=>{const a=e(l),o=a.data("text")||a.text(),c=this.matchText(o,t);c&&(n=!0,i.push(a),this.options.highlightMatches&&t&&this.highlightText(a,o,t)),a.toggle(c)})),this.searchCache.set(t,i),this.toggleNoResults(!n&&t.length>0)}applySearchResults(t){this.$optionsContainer.find(".as-select3-option").each(((s,i)=>{const n=e(i),l=t.some((e=>e.is(n)));n.toggle(l)}))}matchText(e,t){if(!t)return!0;const s=e.toLowerCase(),i=t.toLowerCase();return s.includes(i)}highlightText(e,t,s){const i=e.find(".as-select3-option-text");if(!s||!i.length)return;const n=new RegExp(`(${this.escapeRegex(s)})`,"gi"),l=t.replace(n,'<mark class="as-select3-highlight">$1</mark>');i.html(l)}escapeRegex(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}async remoteSearch(e){if(this.options.remote&&"function"==typeof this.options.remote){this.showLoading();try{const t=await this.options.remote(e);Array.isArray(t)?this.populateOptions(t):t&&t.options&&Array.isArray(t.options)?this.populateOptions(t.options):t&&t.data&&Array.isArray(t.data)?this.populateOptions(t.data):(console.warn("Invalid remote response:",t),this.showNoResults())}catch(e){console.error("Remote search error:",e),this.showNoResults(),this.$element.trigger("asSelect3:error",{error:e})}finally{this.hideLoading()}}}showLoading(){this.isLoading=!0,this.hideNoResults(),this.$optionsContainer.find(".as-select3-option").hide(),this.$optionsContainer.find(".as-select3-loading").length||this.$optionsContainer.append(`\n <li class="as-select3-loading">\n <div class="spinner-border spinner-border-sm me-2"></div>\n ${this.options.loadingText}\n </li>\n `)}hideLoading(){this.isLoading=!1,this.$optionsContainer.find(".as-select3-loading").remove(),this.$optionsContainer.find(".as-select3-option").show()}showNoResults(){this.toggleNoResults(!0)}hideNoResults(){this.toggleNoResults(!1)}populateOptions(t=[]){this.$optionsContainer.find(".as-select3-option").remove(),this.$optionsContainer.find(".as-select3-no-results, .as-select3-loading").remove(),this.options.remote&&this.$element.empty(),t&&0!==t.length?(t.forEach(((t,s)=>{const i={value:t.value||t.id,text:t.text||t.label||t.name||t.title,selected:!!t.selected,disabled:!!t.disabled,icon:t.icon,html:t.html},n=this.createOptionElement(i,s);this.$optionsContainer.append(n);const l=e("<option></option>").val(i.value).text(i.text).prop("selected",i.selected);i.icon&&l.attr("data-icon",i.icon),i.html&&l.attr("data-html",i.html),i.disabled&&l.prop("disabled",!0),this.$element.append(l)})),this.hideNoResults(),this.$element.trigger("asSelect3:optionsPopulated",{options:t})):this.showNoResults()}toggleNoResults(e){this.$optionsContainer.find(".as-select3-no-results").remove(),e&&this.$optionsContainer.append(`\n <li class="as-select3-no-results">\n ${this.options.noResultsText}\n </li>\n `)}focusNext(){const e=this.$optionsContainer.find(".as-select3-option:visible:not(.disabled)");e.length&&(this.$optionsContainer.find(".focused").removeClass("focused"),this.focusIndex=this.focusIndex<e.length-1?this.focusIndex+1:0,e.eq(this.focusIndex).addClass("focused"))}focusPrevious(){const e=this.$optionsContainer.find(".as-select3-option:visible:not(.disabled)");e.length&&(this.$optionsContainer.find(".focused").removeClass("focused"),this.focusIndex=this.focusIndex>0?this.focusIndex-1:e.length-1,e.eq(this.focusIndex).addClass("focused"))}selectFocused(){const e=this.$optionsContainer.find(".focused");e.length&&this.toggleOption(e)}updateSelection(){if(this.$selection.empty(),this.isMultiple)this.selectedValues.length>0?this.selectedValues.forEach((e=>{const t=this.$element.find(`option[value="${e}"]`),s=this.$optionsContainer.find(`[data-value="${e}"]`);if(t.length){const i=t.text(),n=t.attr("data-icon")||s.data("icon");this.$selection.append(this.createTag(i,e,n))}})):this.$selection.append(this.$placeholder);else if(this.selectedValues){const e=this.$element.find(`option[value="${this.selectedValues}"]`),t=this.$optionsContainer.find(`[data-value="${this.selectedValues}"]`);if(e.length){const s=e.text(),i=e.attr("data-icon")||t.data("icon");this.$selection.append(this.createSingleValue(s,i))}}else this.$selection.append(this.$placeholder)}createTag(t,s,i){const n=e('<span class="as-select3-tag"></span>').data("value",s);if(i){const t=e('<div class="as-select3-tag-icon"></div>');this.addIcon(t,i),n.append(t)}return n.append(e('<span class="as-select3-tag-text"></span>').text(t).attr("title",t)),n.append(e('<span class="as-select3-close as-select3-tag-remove"></span>')),n}createSingleValue(t,s){const i=e('<div class="as-select3-single-container"></div>'),n=e('<span class="as-select3-single-value"></span>');if(s){const t=e('<div class="as-select3-single-icon"></div>');this.addIcon(t,s),n.append(t)}return n.append(e("<span></span>").text(t)),i.append(n),i.append(e('<span class="as-select3-close as-select3-clear"></span>')),i}selectAll(){this.isMultiple&&(this.$element.find("option:not(:disabled)").prop("selected",!0),this.selectedValues=this.$element.find("option:selected").map(((e,t)=>t.value)).get(),this.$optionsContainer.find(".as-select3-option:not(.disabled)").addClass("selected").attr("aria-selected","true").find('input[type="checkbox"]').prop("checked",!0),this.updateSelection(),this.$element.trigger("change").trigger("asSelect3:selectall"))}clearAll(){this.$element.find("option").prop("selected",!1),this.selectedValues=this.isMultiple?[]:null,this.$optionsContainer.find(".as-select3-option").removeClass("selected").attr("aria-selected","false").find('input[type="checkbox"]').prop("checked",!1),this.updateSelection(),this.$element.trigger("change").trigger("asSelect3:clearall")}clearSingle(){this.selectedValues=null,this.$element.find("option").prop("selected",!1),this.$optionsContainer.find(".as-select3-option").removeClass("selected").attr("aria-selected","false"),this.updateSelection(),this.$element.trigger("change").trigger("asSelect3:cleared")}removeTag(e){this.isMultiple&&(this.selectedValues=this.selectedValues.filter((t=>t!==e)),this.$element.find(`option[value="${e}"]`).prop("selected",!1),this.$optionsContainer.find(`[data-value="${e}"]`).removeClass("selected").attr("aria-selected","false").find('input[type="checkbox"]').prop("checked",!1),this.updateSelection(),this.$element.trigger("change").trigger("asSelect3:tagremoved",{value:e}))}updateFromSelect(){this.isMultiple?this.selectedValues=this.$element.find("option:selected").map(((e,t)=>t.value)).get():this.selectedValues=this.$element.find("option:selected").val()||null,this.updateSelection()}storeOriginalOptions(){this.originalOptions=this.$element.find("option").map((function(){return{value:this.value,text:this.text,selected:this.selected}})).get()}getValue(){return this.selectedValues}setValue(e){Array.isArray(e)&&this.isMultiple?(this.$element.find("option").prop("selected",!1),e.forEach((e=>{this.$element.find(`option[value="${e}"]`).prop("selected",!0)}))):Array.isArray(e)||this.isMultiple||(this.$element.find("option").prop("selected",!1),this.$element.find(`option[value="${e}"]`).prop("selected",!0)),this.updateFromSelect(),this.$element.trigger("change").trigger("asSelect3:valuechanged",{value:this.getValue()})}debounce(e,t){let s;return function(...i){clearTimeout(s),s=setTimeout((()=>{clearTimeout(s),e(...i)}),t)}}addOption(t){const s=e("<option></option>").val(t.value).text(t.text||t.label).prop("selected",!!t.selected);return t.icon&&s.attr("data-icon",t.icon),t.html&&s.attr("data-html",t.html),t.disabled&&s.prop("disabled",!!t.disabled),this.$element.append(s),this.$optionsContainer.append(this.createOptionElement(t,this.$element.find("option").length-1)),t.selected&&this.updateFromSelect(),this.$element.trigger("asSelect3:optionadded",{option:t}),s}removeOption(e){this.$element.find(`option[value="${e}"]`).remove(),this.$optionsContainer.find(`[data-value="${e}"]`).remove(),this.isMultiple?this.selectedValues=this.selectedValues.filter((t=>t!==e)):this.selectedValues===e&&(this.selectedValues=null),this.updateSelection(),this.$element.trigger("asSelect3:optionremoved",{value:e})}enable(){this.$container.removeClass("disabled"),this.$trigger.attr("tabindex","0"),this.$element.prop("disabled",!1)}disable(){this.close(),this.$container.addClass("disabled"),this.$trigger.attr("tabindex","-1"),this.$element.prop("disabled",!0)}refresh(){this.$optionsContainer.empty(),this.$element.find("option").each(((e,t)=>{this.$optionsContainer.append(this.createOptionElement(t,e))})),this.updateFromSelect()}reinitializeFromDOM(){const e=this.getValue(),t=this.options;this.destroy();const i=new s(this.element,t);return this.element._asSelect3=i,e&&setTimeout((()=>{try{i.setValue(e)}catch(e){console.warn("As-Select3: Could not restore value after reinitialize")}}),10),i}destroy(){return this.searchTimer&&clearTimeout(this.searchTimer),this.mutationObserver&&(this.mutationObserver.disconnect(),this.mutationObserver=null),this.close(),Object.values(this.boundHandlers).forEach((e=>{this.$trigger.off("click",e),this.$trigger.off("keydown",e),this.$container.off("click",e),this.$searchInput&&this.$searchInput.off("input",e)})),e(document).off("click.asSelect3",this.boundHandlers.documentClick),this.searchCache.clear(),this.$element.removeClass("d-none").insertAfter(this.$container),this.$container.remove(),delete this.element._asSelect3,this.$element}}e.fn.asSelect3=function(e){return this.each((function(){this._asSelect3||(this._asSelect3=new s(this,e))}))},s.autoInit=function(t=".as-select3-container select"){return e(t).filter((function(){return!this._asSelect3})).map((function(){const e=new s(this);return this._asSelect3=e,e})).get()},window.AsSelect3=s}(jQuery);
//# sourceMappingURL=as-select3.min.js.map