UNPKG

nice-select2

Version:

A lightweight Vanilla JavaScript plugin that replaces native select elements with customizable dropdowns.

548 lines (466 loc) 14.9 kB
import "../scss/nice-select2.scss"; // utility functions function triggerClick(el) { var event = document.createEvent("MouseEvents"); event.initEvent("click", true, false); el.dispatchEvent(event); } function triggerChange(el) { var event = document.createEvent("HTMLEvents"); event.initEvent("change", true, false); el.dispatchEvent(event); } function triggerFocusIn(el) { var event = document.createEvent("FocusEvent"); event.initEvent("focusin", true, false); el.dispatchEvent(event); } function triggerFocusOut(el) { var event = document.createEvent("FocusEvent"); event.initEvent("focusout", true, false); el.dispatchEvent(event); } function triggerModalOpen(el) { var event = document.createEvent("UIEvent"); event.initEvent("modalopen", true, false); el.dispatchEvent(event); } function triggerModalClose(el) { var event = document.createEvent("UIEvent"); event.initEvent("modalclose", true, false); el.dispatchEvent(event); } function triggerValidationMessage(el, type) { if(type == 'invalid'){ addClass(this.dropdown, 'invalid'); removeClass(this.dropdown, 'valid'); }else{ addClass(this.dropdown, 'valid'); removeClass(this.dropdown, 'invalid'); } } function attr(el, key) { if(el[key] != undefined){ return el[key]; } return el.getAttribute(key); } function data(el, key) { return el.getAttribute("data-" + key); } function hasClass(el, className) { if (el){ return el.classList.contains(className); }else{ return false; } } function addClass(el, className) { if (el) return el.classList.add(className); } function removeClass(el, className) { if (el) return el.classList.remove(className); } var defaultOptions = { data: null, searchable: false, showSelectedItems: false }; export default function NiceSelect(element, options) { this.el = element; this.config = Object.assign({}, defaultOptions, options || {}); this.data = this.config.data; this.selectedOptions = []; this.placeholder = attr(this.el, "placeholder") || this.config.placeholder || "Select an option"; this.searchtext = attr(this.el, "searchtext") || this.config.searchtext || "Search"; this.selectedtext = attr(this.el, "selectedtext") || this.config.selectedtext || "selected"; this.dropdown = null; this.multiple = attr(this.el, "multiple"); this.disabled = attr(this.el, "disabled"); this.create(); } NiceSelect.prototype.create = function() { this.el.style.opacity = "0"; this.el.style.width = "0"; this.el.style.padding = "0"; this.el.style.height = "0"; if (this.data) { this.processData(this.data); } else { this.extractData(); } this.renderDropdown(); this.bindEvent(); }; NiceSelect.prototype.processData = function(data) { var options = []; data.forEach(item=> { options.push({ data: item, attributes: { selected: !!item.selected, disabled: !!item.disabled, optgroup: item.value == 'optgroup' } }); }); this.options = options; }; NiceSelect.prototype.extractData = function() { var options = this.el.querySelectorAll("option,optgroup"); var data = []; var allOptions = []; var selectedOptions = []; options.forEach(item => { if(item.tagName == 'OPTGROUP'){ var itemData = { text: item.label, value: 'optgroup' }; }else{ let text = item.innerText; if(item.dataset.display != undefined){ text = item.dataset.display; } var itemData = { text: text, value: item.value, selected: item.getAttribute("selected") != null, disabled: item.getAttribute("disabled") != null }; } var attributes = { selected: item.getAttribute("selected") != null, disabled: item.getAttribute("disabled") != null, optgroup: item.tagName == 'OPTGROUP' }; data.push(itemData); allOptions.push({ data: itemData, attributes: attributes }); }); this.data = data; this.options = allOptions; this.options.forEach(item => { if (item.attributes.selected){ selectedOptions.push(item); } }); this.selectedOptions = selectedOptions; }; NiceSelect.prototype.renderDropdown = function() { var classes = [ "nice-select", attr(this.el, "class") || "", this.disabled ? "disabled" : "", this.multiple ? "has-multiple" : "" ]; let searchHtml = `<div class="nice-select-search-box">`; searchHtml += `<input type="text" class="nice-select-search" placeholder="${this.searchtext}..." title="search"/>`; searchHtml += `</div>`; var html = `<div class="${classes.join(" ")}" tabindex="${this.disabled ? null : 0}">`; html += `<span class="${this.multiple ? "multiple-options" : "current"}"></span>`; html += `<div class="nice-select-dropdown">`; html += `${this.config.searchable ? searchHtml : ""}`; html += `<ul class="list"></ul>`; html += `</div>`; html += `</div>`; this.el.insertAdjacentHTML("afterend", html); this.dropdown = this.el.nextElementSibling; this._renderSelectedItems(); this._renderItems(); }; NiceSelect.prototype._renderSelectedItems = function() { if (this.multiple) { var selectedHtml = ""; if(this.config.showSelectedItems || this.config.showSelectedItems || window.getComputedStyle(this.dropdown).width == 'auto' || this.selectedOptions.length < 2){ this.selectedOptions.forEach(function(item) { selectedHtml += `<span class="current">${item.data.text}</span>`; }); selectedHtml = selectedHtml == "" ? this.placeholder : selectedHtml; }else{ selectedHtml = this.selectedOptions.length+' '+this.selectedtext; } this.dropdown.querySelector(".multiple-options").innerHTML = selectedHtml; } else { var html = this.selectedOptions.length > 0 ? this.selectedOptions[0].data.text : this.placeholder; this.dropdown.querySelector(".current").innerHTML = html; } }; NiceSelect.prototype._renderItems = function() { var ul = this.dropdown.querySelector("ul"); this.options.forEach(item => { ul.appendChild(this._renderItem(item)); }); }; NiceSelect.prototype._renderItem = function(option) { var el = document.createElement("li"); el.innerHTML = option.data.text; if(option.attributes.optgroup){ addClass(el, 'optgroup'); }else{ el.setAttribute("data-value", option.data.value); var classList = [ "option", option.attributes.selected ? "selected" : null, option.attributes.disabled ? "disabled" : null, ]; el.addEventListener("click", this._onItemClicked.bind(this, option)); el.classList.add(...classList); } option.element = el; return el; }; NiceSelect.prototype.update = function() { this.extractData(); if (this.dropdown) { var open = hasClass(this.dropdown, "open"); this.dropdown.parentNode.removeChild(this.dropdown); this.create(); if (open) { triggerClick(this.dropdown); } } if(attr(this.el, "disabled")) { this.disable(); } else { this.enable(); } }; NiceSelect.prototype.disable = function() { if (!this.disabled) { this.disabled = true; addClass(this.dropdown, "disabled"); } }; NiceSelect.prototype.enable = function() { if (this.disabled) { this.disabled = false; removeClass(this.dropdown, "disabled"); } }; NiceSelect.prototype.clear = function() { this.resetSelectValue(); this.selectedOptions = []; this._renderSelectedItems(); this.update(); triggerChange(this.el); }; NiceSelect.prototype.destroy = function() { if (this.dropdown) { this.dropdown.parentNode.removeChild(this.dropdown); this.el.style.display = ""; } }; NiceSelect.prototype.bindEvent = function() { var $this = this; this.dropdown.addEventListener("click", this._onClicked.bind(this)); this.dropdown.addEventListener("keydown", this._onKeyPressed.bind(this)); this.dropdown.addEventListener("focusin", triggerFocusIn.bind(this, this.el)); this.dropdown.addEventListener("focusout", triggerFocusOut.bind(this, this.el)); this.el.addEventListener("invalid", triggerValidationMessage.bind(this, this.el, 'invalid')); window.addEventListener("click", this._onClickedOutside.bind(this)); if (this.config.searchable) { this._bindSearchEvent(); } }; NiceSelect.prototype._bindSearchEvent = function() { var searchBox = this.dropdown.querySelector(".nice-select-search"); if (searchBox){ searchBox.addEventListener("click", function(e) { e.stopPropagation(); return false; }); } searchBox.addEventListener("input", this._onSearchChanged.bind(this)); }; NiceSelect.prototype._onClicked = function(e) { e.preventDefault(); if (!hasClass(this.dropdown, "open") ) { addClass(this.dropdown, "open"); triggerModalOpen(this.el); } else { if (this.multiple) { if (e.target == this.dropdown.querySelector('.multiple-options')) { removeClass(this.dropdown, "open"); triggerModalClose(this.el); } } else { removeClass(this.dropdown, "open"); triggerModalClose(this.el); } } if (hasClass(this.dropdown, "open")) { var search = this.dropdown.querySelector(".nice-select-search"); if (search) { search.value = ""; search.focus(); } var t = this.dropdown.querySelector(".focus"); removeClass(t, "focus"); t = this.dropdown.querySelector(".selected"); addClass(t, "focus"); this.dropdown.querySelectorAll("ul li").forEach(function(item) { item.style.display = ""; }); } else { this.dropdown.focus(); } }; NiceSelect.prototype._onItemClicked = function(option, e) { var optionEl = e.target; if (!hasClass(optionEl, "disabled")) { if (this.multiple) { if (hasClass(optionEl, "selected")) { removeClass(optionEl, "selected"); this.selectedOptions.splice(this.selectedOptions.indexOf(option), 1); this.el.querySelector(`option[value="${optionEl.dataset.value}"]`).removeAttribute('selected'); }else{ addClass(optionEl, "selected"); this.selectedOptions.push(option); } } else { this.options.forEach(function (item) { removeClass(item.element, "selected"); }); this.selectedOptions.forEach(function (item) { removeClass(item.element, "selected"); }); addClass(optionEl, "selected"); this.selectedOptions = [option]; } this._renderSelectedItems(); this.updateSelectValue(); } }; NiceSelect.prototype.updateSelectValue = function() { if (this.multiple) { var select = this.el; this.selectedOptions.forEach(function(item) { var el = select.querySelector(`option[value="${item.data.value}"]`); if (el){ el.setAttribute("selected", true); } }); } else if (this.selectedOptions.length > 0) { this.el.value = this.selectedOptions[0].data.value; } triggerChange(this.el); }; NiceSelect.prototype.resetSelectValue = function() { if (this.multiple) { var select = this.el; this.selectedOptions.forEach(function(item) { var el = select.querySelector(`option[value="${item.data.value}"]`); if (el){ el.removeAttribute("selected"); } }); } else if (this.selectedOptions.length > 0) { this.el.selectedIndex = -1; } triggerChange(this.el); }; NiceSelect.prototype._onClickedOutside = function(e) { if (!this.dropdown.contains(e.target)) { removeClass(this.dropdown, "open"); triggerModalClose(this.el); } }; NiceSelect.prototype._onKeyPressed = function(e) { // Keyboard events var focusedOption = this.dropdown.querySelector(".focus"); var open = hasClass(this.dropdown, "open"); // Enter if (e.keyCode == 13) { if (open) { triggerClick(focusedOption); } else { triggerClick(this.dropdown); } } else if (e.keyCode == 40) { // Down if (!open) { triggerClick(this.dropdown); } else { var next = this._findNext(focusedOption); if (next) { var t = this.dropdown.querySelector(".focus"); removeClass(t, "focus"); addClass(next, "focus"); } } e.preventDefault(); } else if (e.keyCode == 38) { // Up if (!open) { triggerClick(this.dropdown); } else { var prev = this._findPrev(focusedOption); if (prev) { var t = this.dropdown.querySelector(".focus"); removeClass(t, "focus"); addClass(prev, "focus"); } } e.preventDefault(); } else if (e.keyCode == 27 && open) { // Esc triggerClick(this.dropdown); } else if(e.keyCode === 32 && open) { // Space return false; } return false; }; NiceSelect.prototype._findNext = function(el) { if (el) { el = el.nextElementSibling; } else { el = this.dropdown.querySelector(".list .option"); } while (el) { if (!hasClass(el, "disabled") && el.style.display != "none") { return el; } el = el.nextElementSibling; } return null; }; NiceSelect.prototype._findPrev = function(el) { if (el) { el = el.previousElementSibling; } else { el = this.dropdown.querySelector(".list .option:last-child"); } while (el) { if (!hasClass(el, "disabled") && el.style.display != "none") { return el; } el = el.previousElementSibling; } return null; }; NiceSelect.prototype._onSearchChanged = function(e) { var open = hasClass(this.dropdown, "open"); var text = e.target.value; text = text.toLowerCase(); if (text == "") { this.options.forEach(function(item) { item.element.style.display = ""; }); } else if (open) { var matchReg = new RegExp(text); this.options.forEach(function(item) { var optionText = item.data.text.toLowerCase(); var matched = matchReg.test(optionText); item.element.style.display = matched ? "" : "none"; }); } this.dropdown.querySelectorAll(".focus").forEach(function(item) { removeClass(item, "focus"); }); var firstEl = this._findNext(null); addClass(firstEl, "focus"); }; export function bind(el, options) { return new NiceSelect(el, options); }