(() => { class Dropdown { #_select; #_dropdown; #_selected; #_search; #search = false; #selected = false; #options = []; constructor(el) { this.#_select = el; this.#search = "searchable" in el.dataset; this.#_select.querySelectorAll("option").forEach((option) => { this.#options.push({ value: option.value, label: option.textContent, search: searchable(option.textContent), }); }); this.#build(); if (this.#options.length) { this.#set(this.#options[0].value); } } #build() { // prepare and hide original select this.#_select.style.display = "none"; const descriptor = Object.getOwnPropertyDescriptor( HTMLSelectElement.prototype, "value", ); Object.defineProperty(this.#_select, "value", { get: () => { return descriptor.get.call(this.#_select); }, set: (value) => { descriptor.set.call(this.#_select, value); this.#_select.dispatchEvent(new Event("change")); this.#set(value); }, }); // dropdown this.#_dropdown = make("div", "dropdown"); // selected item this.#_selected = make("div", "selected"); this.#_selected.addEventListener("click", () => { this.#_dropdown.classList.toggle("open"); }); this.#_dropdown.appendChild(this.#_selected); // content const _content = make("div", "cont"); this.#_dropdown.appendChild(_content); // option wrapper const _options = make("div", "opts"); _content.appendChild(_options); // options for (const option of this.#options) { const _opt = make("div", "opt"); _opt.textContent = option.label; _opt.addEventListener("click", () => { this.#_select.value = option.value; this.#_dropdown.classList.remove("open"); }); _options.appendChild(_opt); option.el = _opt; } // live search (if enabled) if (this.#search) { this.#_search = make("input", "search"); this.#_search.type = "text"; this.#_search.placeholder = "Search..."; this.#_search.addEventListener("input", () => { this.#filter(); }); this.#_search.addEventListener("keydown", (event) => { if (event.key !== "Escape") { return; } if (this.#_search.value) { this.#_search.value = ""; this.#_search.dispatchEvent(new Event("input")); return; } this.#_dropdown.classList.remove("open"); }); _content.appendChild(this.#_search); } // add to dom this.#_select.after(this.#_dropdown); this.#render(); } #render() { if (this.#selected === false) { this.#_selected.textContent = ""; return; } const selection = this.#options[this.#selected]; this.#_selected.textContent = selection.label; } #filter() { if (!this.#_search) { return; } const query = searchable(this.#_search.value); for (const option of this.#options) { if (query && !option.search.includes(query)) { option.el.classList.add("filtered"); } else { option.el.classList.remove("filtered"); } } } #set(value) { const index = this.#options.findIndex((option) => option.value === value); if (this.#selected === index) { return; } this.#selected = index !== -1 ? index : false; this.#render(); } } function searchable(text) { // lowercase text = text.toLowerCase(); // only alpha-num text = text.replace(/[^\w]/g, ""); return text.trim(); } document.body.addEventListener("click", (event) => { const clicked = event.target.closest(".dropdown"); document.querySelectorAll(".dropdown").forEach((element) => { if (element === clicked) { return; } element.classList.remove("open"); }); }); window.dropdown = (el) => new Dropdown(el); })();