(() => { class Dropdown { #_select; #_dropdown; #_selected; #_search; #maxTags = false; #search = false; #selected = false; #options = []; constructor(el, maxTags = false) { this.#_select = el; this.#maxTags = maxTags; this.#search = "searchable" in el.dataset; this.#_select.querySelectorAll("option").forEach((option) => { const tags = option.dataset.tags?.trim(); this.#options.push({ value: option.value, label: option.textContent, title: option.title || false, tags: tags ? tags.split(",") : [], 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) { // option wrapper const _opt = make("div", "opt"); _opt.title = option.title || ""; _opt.addEventListener("click", () => { this.#_select.value = option.value; this.#_dropdown.classList.remove("open"); }); // option label const _label = make("div", "label"); _label.title = option.label; _label.textContent = option.label; _opt.appendChild(_label); // option tags (optional) const tags = option.tags; if (option.tags.length) { const _tags = make("div", "tags"); _tags.title = `${this.#maxTags ? `${tags.length}/${this.#maxTags}: ` : ""}${tags.join(", ")}`; if (this.#maxTags && tags.length >= this.#maxTags) { const _all = make("div", "tag", "all"); _tags.appendChild(_all); } else { for (const tag of tags) { const _tag = make("div", "tag", tag); _tags.appendChild(_tag); } } _opt.appendChild(_tags); } // add to options _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.innerHTML = ""; return; } const selection = this.#options[this.#selected]; this.#_selected.innerHTML = selection.el.innerHTML; } #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, maxTags = false) => new Dropdown(el, maxTags); })();