@gpaol/bootstrap-multiselect
Version:
Bootstrap 5 MultiSelect component with search, filtering, grouped options, pagination, and comprehensive localization support
18 lines • 22.4 kB
JavaScript
/*!
* Bootstrap MultiSelect - jQuery Plugin v1.2.0
*
* A powerful multi-select dropdown plugin built with Bootstrap 5
* Supports declarative configuration via data attributes and programmatic control via JavaScript API
*
* This plugin transforms standard HTML select elements or input fields into feature-rich
* multi-select dropdowns with search, filtering, select all/deselect all functionality,
* maximum selection limits, disabled options, optgroup support, pagination, and more.
*
* Authors: Paolo Gaetano
* Copyright (c) 2026 Paolo Gaetano
* Released under the MIT license
*
* @requires jQuery >= 3.6
* @requires Bootstrap >= 5.0
*/
!function(t){"use strict";t.fn.bootstrapMultiSelect=t.fn.bootstrapMultiSelect||{},t.fn.bootstrapMultiSelect.langs=t.fn.bootstrapMultiSelect.langs||{en:{placeholder:"Select items...",searchPlaceholder:"Search...",selectAllText:"Select All",deselectAllText:"Deselect All",noResultsText:"No results found",itemsSelectedText:"items selected",paginationPrevText:"Previous",paginationNextText:"Next",paginationInfoText:"Page {current} of {total}"}},t.fn.bootstrapMultiSelect.lang=t.fn.bootstrapMultiSelect.lang||"en";const e={placeholder:"Select items...",maxSelection:0,selectAll:!0,search:!0,width:"100%",theme:"primary",data:[],valueField:"id",textField:"text",selectedValues:[],closeOnSelect:!1,enableClickableOptGroups:!1,buttonClass:"btn-outline-secondary",dropdownClass:"",searchPlaceholder:"Search...",selectAllText:"Select All",deselectAllText:"Deselect All",noResultsText:"No results found",maxHeight:"300px",itemsSelectedText:"items selected",enablePagination:!1,itemsPerPage:10,paginationPrevText:"Previous",paginationNextText:"Next",paginationInfoText:"Page {current} of {total}",paginationPosition:"bottom",lang:null};class s{constructor(e,s){this.$element=t(e),this.options=this._getOptions(s),this.id=this._generateId(),this.isOpen=!1,this.selectedItems=[],this.currentPage=1,this.filteredData=[],this._init()}_getOptions(s){const i={placeholder:this.$element.data("placeholder"),maxSelection:this.$element.data("max-selection"),selectAll:this.$element.data("select-all"),search:this.$element.data("search"),width:this.$element.data("width"),theme:this.$element.data("theme"),valueField:this.$element.data("value-field"),textField:this.$element.data("text-field"),closeOnSelect:this.$element.data("close-on-select"),buttonClass:this.$element.data("button-class"),dropdownClass:this.$element.data("dropdown-class"),searchPlaceholder:this.$element.data("search-placeholder"),selectAllText:this.$element.data("select-all-text"),deselectAllText:this.$element.data("deselect-all-text"),noResultsText:this.$element.data("no-results-text"),maxHeight:this.$element.data("max-height"),itemsSelectedText:this.$element.data("items-selected-text"),enablePagination:this.$element.data("enable-pagination"),itemsPerPage:this.$element.data("items-per-page"),paginationPrevText:this.$element.data("pagination-prev-text"),paginationNextText:this.$element.data("pagination-next-text"),paginationInfoText:this.$element.data("pagination-info-text"),paginationPosition:this.$element.data("pagination-position"),lang:this.$element.data("lang")};Object.keys(i).forEach(t=>{void 0===i[t]&&delete i[t]});const n=t.extend(!0,{},e,i,s);if(t.fn.bootstrapMultiSelect&&t.fn.bootstrapMultiSelect.langs){let e;e=void 0!==i.lang?i.lang:s&&void 0!==s.lang?s.lang:t.fn.bootstrapMultiSelect.lang||"en";const l=t.fn.bootstrapMultiSelect.langs[e];l&&(i.placeholder||s?.placeholder||(n.placeholder=l.placeholder),i.searchPlaceholder||s?.searchPlaceholder||(n.searchPlaceholder=l.searchPlaceholder),i.selectAllText||s?.selectAllText||(n.selectAllText=l.selectAllText),i.deselectAllText||s?.deselectAllText||(n.deselectAllText=l.deselectAllText),i.noResultsText||s?.noResultsText||(n.noResultsText=l.noResultsText),i.itemsSelectedText||s?.itemsSelectedText||(n.itemsSelectedText=l.itemsSelectedText),i.paginationPrevText||s?.paginationPrevText||(n.paginationPrevText=l.paginationPrevText),i.paginationNextText||s?.paginationNextText||(n.paginationNextText=l.paginationNextText),i.paginationInfoText||s?.paginationInfoText||(n.paginationInfoText=l.paginationInfoText))}return n}_generateId(){return"bs-multiselect-"+Math.random().toString(36).substr(2,9)}_init(){this.$element.css({position:"absolute",left:"-9999px",width:"1px",height:"1px"}),this._loadData(),this._buildDropdown(),this._setInitialSelection(),this._bindEvents()}_loadData(){this.options.data&&Array.isArray(this.options.data)&&this.options.data.length>0||this.$element.is("select")&&(this.options.data=this._parseSelectOptions())}_parseSelectOptions(){const e=[],s=this;return this.$element.find("option, optgroup").each(function(){const i=t(this);if(i.is("optgroup")){const n={label:i.attr("label"),children:[]};i.find("option").each(function(){const e=t(this);n.children.push({[s.options.valueField]:e.val(),[s.options.textField]:e.text(),selected:e.prop("selected"),disabled:e.prop("disabled")})}),e.push(n)}else i.parent().is("optgroup")||e.push({[s.options.valueField]:i.val(),[s.options.textField]:i.text(),selected:i.prop("selected"),disabled:i.prop("disabled")})}),e}_buildDropdown(){const e=this.options.buttonClass||`btn-outline-${this.options.theme}`;this.$container=t(`\n <div class="bs-multiselect-container dropdown" style="width: ${this.options.width}">\n <button class="btn ${e} dropdown-toggle w-100 text-start d-flex justify-content-between align-items-center" \n type="button" \n id="${this.id}-button" \n data-bs-toggle="dropdown" \n aria-expanded="false">\n <span class="bs-multiselect-placeholder">${this.options.placeholder}</span>\n <span class="bs-multiselect-badge badge bg-${this.options.theme} ms-2" style="display: none;">0</span>\n </button>\n <div class="dropdown-menu w-100 ${this.options.dropdownClass}" \n aria-labelledby="${this.id}-button" \n style="max-height: ${this.options.maxHeight}; overflow-y: auto;">\n ${this._buildDropdownContent()}\n </div>\n </div>\n `),this.$element.after(this.$container),this.$button=this.$container.find(".dropdown-toggle"),this.$dropdownMenu=this.$container.find(".dropdown-menu"),this.$placeholder=this.$container.find(".bs-multiselect-placeholder"),this.$badge=this.$container.find(".bs-multiselect-badge"),this.$searchInput=this.$container.find(".bs-multiselect-search"),this.$selectAllBtn=this.$container.find(".bs-multiselect-select-all"),this.$deselectAllBtn=this.$container.find(".bs-multiselect-deselect-all"),this.$optionsList=this.$container.find(".bs-multiselect-options"),this.$noResults=this.$container.find(".bs-multiselect-no-results")}_buildDropdownContent(){let t='<div class="p-2">';return this.options.search&&(t+=`\n <div class="mb-2">\n <input type="text" \n class="form-control form-control-sm bs-multiselect-search" \n placeholder="${this.options.searchPlaceholder}">\n </div>\n `),this.options.selectAll&&(t+=`\n <div class="mb-2 d-flex gap-2">\n <button type="button" class="btn btn-sm btn-outline-primary bs-multiselect-select-all flex-fill">\n ${this.options.selectAllText}\n </button>\n <button type="button" class="btn btn-sm btn-outline-secondary bs-multiselect-deselect-all flex-fill">\n ${this.options.deselectAllText}\n </button>\n </div>\n `),t+='<hr class="my-2">',!this.options.enablePagination||"top"!==this.options.paginationPosition&&"both"!==this.options.paginationPosition||(t+=this._buildPaginationControls("top")),t+='<div class="bs-multiselect-options">',t+=this._buildOptions(),t+="</div>",t+=`\n <div class="bs-multiselect-no-results text-muted text-center py-2" style="display: none;">\n <small>${this.options.noResultsText}</small>\n </div>\n `,!this.options.enablePagination||"bottom"!==this.options.paginationPosition&&"both"!==this.options.paginationPosition||(t+=this._buildPaginationControls("bottom")),t+="</div>",t}_buildPaginationControls(t="top"){const e=this._getTotalPages(),s=this.options.paginationInfoText.replace("{current}",this.currentPage).replace("{total}",e);return`\n <div class="bs-multiselect-pagination d-flex justify-content-between align-items-center ${"bottom"===t?"mt-2":"mb-2"}">\n <button type="button" class="btn btn-sm btn-outline-secondary bs-multiselect-prev-page" ${1===this.currentPage?"disabled":""}>\n ${this.options.paginationPrevText}\n </button>\n <span class="bs-multiselect-page-info text-muted small">\n ${s}\n </span>\n <button type="button" class="btn btn-sm btn-outline-secondary bs-multiselect-next-page" ${this.currentPage===e?"disabled":""}>\n ${this.options.paginationNextText}\n </button>\n </div>\n `}_getTotalPages(){const t=this.filteredData.length>0?this.filteredData:this.options.data,e=this._getFlatItems(t);return Math.max(1,Math.ceil(e.length/this.options.itemsPerPage))}_getFlatItems(t){const e=[];return t.forEach(t=>{t.label&&t.children?e.push(...t.children):e.push(t)}),e}_getPaginatedItems(){if(!this.options.enablePagination)return this.options.data;const t=this.filteredData.length>0?this.filteredData:this.options.data,e=this._getFlatItems(t),s=(this.currentPage-1)*this.options.itemsPerPage,i=s+this.options.itemsPerPage;return e.slice(s,i)}_buildOptions(){let t="";return(this.options.enablePagination?this._getPaginatedItems():this.options.data).forEach((e,s)=>{e.label&&e.children?t+=this._buildOptGroup(e,s):t+=this._buildOption(e,s)}),t}_buildOptGroup(t,e){let s=`\n <div class="bs-multiselect-optgroup" data-group-index="${e}">\n <div class="fw-bold text-muted small px-2 py-1">${t.label}</div>\n <div class="ps-3">\n `;return t.children.forEach((t,i)=>{s+=this._buildOption(t,`${e}-${i}`,!0)}),s+="</div></div>",s}_buildOption(t,e,s=!1){const i=t[this.options.valueField],n=t[this.options.textField],l=t.disabled?"disabled":"",o=t.disabled?'data-original-disabled="true"':"";return`\n <div class="form-check bs-multiselect-option" data-value="${i}" data-index="${e}">\n <input class="form-check-input" \n type="checkbox" \n value="${i}" \n id="${this.id}-option-${e}" \n ${l}\n ${o}>\n <label class="form-check-label w-100" for="${this.id}-option-${e}">\n ${n}\n </label>\n </div>\n `}_bindEvents(){const e=this;this.$dropdownMenu.on("click",function(t){e.options.closeOnSelect||t.stopPropagation()}),this.$dropdownMenu.on("change",".form-check-input",function(s){s.stopPropagation(),e._onOptionChange(t(this))}),this.$selectAllBtn.on("click",function(t){t.preventDefault(),t.stopPropagation(),e._selectAll()}),this.$deselectAllBtn.on("click",function(t){t.preventDefault(),t.stopPropagation(),e._deselectAll()}),this.options.search&&this.$searchInput.on("input",function(){e._filterOptions(t(this).val())}),this.options.enablePagination&&(this.$dropdownMenu.on("click",".bs-multiselect-prev-page",function(t){t.preventDefault(),t.stopPropagation(),e._goToPreviousPage()}),this.$dropdownMenu.on("click",".bs-multiselect-next-page",function(t){t.preventDefault(),t.stopPropagation(),e._goToNextPage()})),this.$container.on("shown.bs.dropdown",function(){e.isOpen=!0,e.options.search&&e.$searchInput.focus()}),this.$container.on("hidden.bs.dropdown",function(){e.isOpen=!1,e.options.search&&(e.$searchInput.val(""),e._filterOptions(""))});const s=this.$element.closest("form");s.length>0&&(s.on("reset.bs-multiselect",function(t){setTimeout(function(){e.reset()},0)}),s.on("submit.bs-multiselect",function(i){t.validator&&s.data("validator")&&setTimeout(function(){const t=s.data("validator"),i=e.$element.attr("name");if(t.settings.rules&&t.settings.rules[i]){const s=t.invalid&&t.invalid[i];e._updateValidationState(!s)}},0)})),t.validator&&(this.$element.on("invalid-form.validate",function(){e._updateValidationState(!1)}),this.$element.on("change",function(){setTimeout(function(){const t=e.$element.closest("form");if(t.length&&t.data("validator")){const s=t.data("validator"),i=e.$element.attr("name");if(s.settings.rules&&s.settings.rules[i]){const t=s.invalid&&s.invalid[i];e._updateValidationState(!t)}}},100)}))}_onOptionChange(t){const e=t.val();if(t.prop("checked")){if(this.options.maxSelection>0&&this.selectedItems.length>=this.options.maxSelection)return t.prop("checked",!1),void this._showMaxSelectionWarning();this.selectedItems.push(e)}else{const t=this.selectedItems.indexOf(e);t>-1&&this.selectedItems.splice(t,1)}this._updateDisplay(),this._updateValue(),this._triggerChange(),this._updateMaxSelectionState()}_selectAll(){const e=this;if(this.options.enablePagination){const s=this._getPaginatedItems().map(t=>t[this.options.valueField]);if(this.options.maxSelection>0){const t=this.options.maxSelection-this.selectedItems.length;if(t<=0)return void this._showMaxSelectionWarning();let e=0;s.forEach(s=>{!this.selectedItems.includes(s)&&e<t&&(this.selectedItems.push(s),e++)}),e<s.filter(t=>!this.selectedItems.includes(t)).length&&this._showMaxSelectionWarning()}else s.forEach(t=>{this.selectedItems.includes(t)||this.selectedItems.push(t)});this.$optionsList.find(".form-check-input").each(function(){const s=t(this),i=s.val();e.selectedItems.includes(i)&&s.prop("checked",!0)})}else{const s=this.$optionsList.find(".bs-multiselect-option:visible .form-check-input").filter(function(){return!t(this).data("original-disabled")});if(this.options.maxSelection>0){const i=this.options.maxSelection-this.selectedItems.length;if(i<=0)return void this._showMaxSelectionWarning();let n=0;s.each(function(){const s=t(this);!s.prop("checked")&&n<i&&(s.prop("checked",!0),e.selectedItems.push(s.val()),n++)}),n<s.filter(":not(:checked)").length&&this._showMaxSelectionWarning()}else s.each(function(){const s=t(this);s.prop("checked")||(s.prop("checked",!0),e.selectedItems.push(s.val()))})}this.selectedItems=[...new Set(this.selectedItems)],this._updateDisplay(),this._updateValue(),this._triggerChange(),this._updateMaxSelectionState()}_deselectAll(){this.$optionsList.find(".form-check-input:checked").prop("checked",!1),this.selectedItems=[],this._updateDisplay(),this._updateValue(),this._triggerChange(),this._updateMaxSelectionState()}_filterOptions(e){const s=e.toLowerCase().trim();if(!s)return this.filteredData=[],this.currentPage=1,this.options.enablePagination?this._refreshPagination():this.$optionsList.find(".bs-multiselect-option, .bs-multiselect-optgroup").show(),void this.$noResults.hide();if(this.options.enablePagination)this.filteredData=this.options.data.filter(t=>{if(t.label&&t.children){return t.children.filter(t=>t[this.options.textField].toLowerCase().includes(s)).length>0}return t[this.options.textField].toLowerCase().includes(s)}),this.filteredData=this.filteredData.map(t=>t.label&&t.children?{...t,children:t.children.filter(t=>t[this.options.textField].toLowerCase().includes(s))}:t),this.currentPage=1,this._refreshPagination(),0===this._getFlatItems(this.filteredData).length?this.$noResults.show():this.$noResults.hide();else{let e=0;this.$optionsList.find(".bs-multiselect-option").each(function(){const i=t(this);i.find(".form-check-label").text().toLowerCase().includes(s)?(i.show(),e++):i.hide()}),this.$optionsList.find(".bs-multiselect-optgroup").each(function(){const e=t(this);e.find(".bs-multiselect-option:visible").length>0?e.show():e.hide()}),0===e?this.$noResults.show():this.$noResults.hide()}}_goToPreviousPage(){this.currentPage>1&&(this.currentPage--,this._refreshPagination())}_goToNextPage(){const t=this._getTotalPages();this.currentPage<t&&(this.currentPage++,this._refreshPagination())}_refreshPagination(){this.$optionsList.html(this._buildOptions());const t=this._getTotalPages(),e=this.options.paginationInfoText.replace("{current}",this.currentPage).replace("{total}",t);this.$dropdownMenu.find(".bs-multiselect-page-info").text(e),this.$dropdownMenu.find(".bs-multiselect-prev-page").prop("disabled",1===this.currentPage),this.$dropdownMenu.find(".bs-multiselect-next-page").prop("disabled",this.currentPage===t);this.selectedItems.forEach(t=>{this.$optionsList.find(`.form-check-input[value="${t}"]`).prop("checked",!0)})}_updateDisplay(){const t=this.selectedItems.length;if(0===t)this.$placeholder.text(this.options.placeholder),this.$badge.hide(),this.$badge.text("0");else{const e=[];this.selectedItems.forEach(t=>{const s=this.$optionsList.find(`.form-check-input[value="${t}"]`).next(".form-check-label").text();e.push(s)}),t<=3?this.$placeholder.text(e.join(", ")):this.$placeholder.text(`${t} ${this.options.itemsSelectedText}`),this.$badge.text(t).show()}}_updateValue(){this.$element.is("select")?(this.$element.find("option").prop("selected",!1),this.selectedItems.length>0&&this.selectedItems.forEach(t=>{this.$element.find(`option[value="${t}"]`).prop("selected",!0)}),this.$element.val(this.selectedItems.length>0?this.selectedItems:null)):this.$element.val(this.selectedItems.length>0?JSON.stringify(this.selectedItems):""),this.$element.trigger("change")}_triggerChange(){this.$element.trigger("change.bs-multiselect",[{selectedValues:this.selectedItems}]);const e=this.$element.closest("form");if(t.validator&&e.length>0&&e.data("validator")){const t=this,s=t.$element.attr("name"),i=e.data("validator");if(i.settings.rules&&i.settings.rules[s]){t.$element.valid();setTimeout(function(){const e=i.invalid&&i.invalid[s];t._updateValidationState(!e)},50)}}}_updateValidationState(t){t?(this.$button.removeClass("is-invalid border-danger"),this.$button.addClass("is-valid")):(this.$button.removeClass("is-valid"),this.$button.addClass("is-invalid border-danger"))}_setInitialSelection(){const e=this;if(this.$element.is("select"))this.$element.find("option[selected]").each(function(){const s=t(this).val();s&&!e.selectedItems.includes(s)&&e.selectedItems.push(s)});else if(this.options.selectedValues&&Array.isArray(this.options.selectedValues)&&this.options.selectedValues.length>0)this.selectedItems=[...this.options.selectedValues];else if(this.options.data&&Array.isArray(this.options.data))this.options.data.forEach(t=>{if(t.children)t.children.forEach(t=>{if(t.selected){const s=t[this.options.valueField];s&&!e.selectedItems.includes(s)&&e.selectedItems.push(s)}});else if(t.selected){const s=t[this.options.valueField];s&&!this.selectedItems.includes(s)&&e.selectedItems.push(s)}});else if(this.$element.val())try{const t=JSON.parse(this.$element.val());Array.isArray(t)&&(this.selectedItems=t)}catch(t){}this.initialSelectedItems=[...this.selectedItems],this.selectedItems.forEach(t=>{this.$optionsList.find(`.form-check-input[value="${t}"]`).prop("checked",!0)}),this._updateDisplay(),this._updateValue(),this._updateMaxSelectionState()}_updateMaxSelectionState(){if(this.options.maxSelection>0){const e=this.selectedItems.length>=this.options.maxSelection;this.$optionsList.find(".form-check-input").each(function(){const s=t(this),i=s.prop("checked");!0===s.data("original-disabled")||(e&&!i?(s.prop("disabled",!0),s.closest(".bs-multiselect-option").addClass("disabled")):(s.prop("disabled",!1),s.closest(".bs-multiselect-option").removeClass("disabled")))});const s=this.$dropdownMenu.find(".bs-multiselect-select-all");e?s.prop("disabled",!0).addClass("disabled"):s.prop("disabled",!1).removeClass("disabled")}}_showMaxSelectionWarning(){console.warn(`Maximum selection limit reached (${this.options.maxSelection})`)}getSelectedValues(){return[...this.selectedItems]}setSelectedValues(t){if(!Array.isArray(t))return;this._deselectAll();const e=this;t.forEach(t=>{const s=this.$optionsList.find(`.form-check-input[value="${t}"]`);s.length&&!s.prop("disabled")&&(s.prop("checked",!0),e.selectedItems.push(t))}),this._updateDisplay(),this._updateValue(),this._triggerChange()}refresh(){const t=[...this.selectedItems];this.$container.remove(),this._init(),this.setSelectedValues(t)}destroy(){const e=this.$element.closest("form");e.length>0&&(e.off("reset.bs-multiselect"),e.off("submit.bs-multiselect")),t.validator&&this.$element.off("invalid-form.validate"),this.$container.remove(),this.$element.css({position:"",left:"",width:"",height:""}).show(),this.$element.removeData("bootstrapMultiSelect"),this.$element.removeData("bs-multiselect")}enable(){this.$button.prop("disabled",!1)}disable(){this.$button.prop("disabled",!0)}reset(){this.$optionsList.find(".form-check-input:checked").prop("checked",!1),this.selectedItems=this.initialSelectedItems?[...this.initialSelectedItems]:[],this.selectedItems.forEach(t=>{this.$optionsList.find(`.form-check-input[value="${t}"]`).prop("checked",!0)}),this.$element.is("select")&&(this.$element.find("option").prop("selected",!1),this.selectedItems.forEach(t=>{this.$element.find(`option[value="${t}"]`).prop("selected",!0)})),this._updateDisplay(),this._updateValue(),this._updateMaxSelectionState(),this._triggerChange()}}t.fn.bootstrapMultiSelect=function(e){const i=Array.prototype.slice.call(arguments,1);let n;return this.each(function(){const l=t(this);let o=l.data("bootstrapMultiSelect");if(o?"object"==typeof e&&e.data&&Array.isArray(e.data)&&(o.options.data=e.data,o.refresh()):(o=new s(this,"object"==typeof e?e:{}),l.data("bootstrapMultiSelect",o),l.data("bs-multiselect",o)),"string"==typeof e&&"function"==typeof o[e]){const t=o[e].apply(o,i);void 0!==t&&(n=t)}}),void 0!==n?n:this},t(document).ready(function(){t('[data-toggle="bootstrap-multiselect"]').bootstrapMultiSelect()})}(jQuery);