1
0
mirror of https://github.com/coalaura/whiskr.git synced 2025-09-10 01:39:54 +00:00
Files
whiskr/static/js/dropdown.js

235 lines
4.6 KiB
JavaScript
Raw Normal View History

2025-08-09 21:16:24 +02:00
(() => {
class Dropdown {
#_select;
#_dropdown;
#_selected;
2025-08-09 22:14:35 +02:00
#_search;
2025-08-09 21:16:24 +02:00
2025-08-11 00:15:58 +02:00
#maxTags = false;
2025-08-09 22:14:35 +02:00
#search = false;
2025-08-09 21:16:24 +02:00
#selected = false;
#options = [];
2025-08-11 00:15:58 +02:00
constructor(el, maxTags = false) {
2025-08-09 21:16:24 +02:00
this.#_select = el;
2025-08-11 00:15:58 +02:00
this.#maxTags = maxTags;
2025-08-09 22:14:35 +02:00
this.#search = "searchable" in el.dataset;
2025-08-09 21:16:24 +02:00
this.#_select.querySelectorAll("option").forEach((option) => {
2025-08-10 16:38:02 +02:00
const tags = option.dataset.tags?.trim();
2025-08-09 21:16:24 +02:00
this.#options.push({
value: option.value,
2025-08-09 22:14:35 +02:00
label: option.textContent,
2025-08-10 16:38:02 +02:00
title: option.title || false,
tags: tags ? tags.split(",") : [],
2025-08-09 22:14:35 +02:00
search: searchable(option.textContent),
2025-08-09 21:16:24 +02:00
});
});
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"));
2025-08-09 21:16:24 +02:00
this.#set(value);
},
});
// dropdown
this.#_dropdown = make("div", "dropdown");
// selected item
this.#_selected = make("div", "selected");
2025-08-09 22:14:35 +02:00
this.#_selected.addEventListener("click", () => {
this.#_dropdown.classList.toggle("open");
});
2025-08-09 21:16:24 +02:00
this.#_dropdown.appendChild(this.#_selected);
2025-08-09 22:14:35 +02:00
// content
const _content = make("div", "cont");
this.#_dropdown.appendChild(_content);
2025-08-09 21:16:24 +02:00
// option wrapper
2025-08-09 22:14:35 +02:00
const _options = make("div", "opts");
2025-08-09 21:16:24 +02:00
2025-08-09 22:14:35 +02:00
_content.appendChild(_options);
2025-08-09 21:16:24 +02:00
// options
for (const option of this.#options) {
2025-08-10 16:38:02 +02:00
// option wrapper
2025-08-09 22:14:35 +02:00
const _opt = make("div", "opt");
2025-08-09 21:16:24 +02:00
2025-08-10 16:38:02 +02:00
_opt.title = option.title || "";
2025-08-09 21:16:24 +02:00
_opt.addEventListener("click", () => {
this.#_select.value = option.value;
2025-08-09 22:14:35 +02:00
this.#_dropdown.classList.remove("open");
2025-08-09 21:16:24 +02:00
});
2025-08-10 16:38:02 +02:00
// option label
const _label = make("div", "label");
_label.title = option.label;
_label.textContent = option.label;
_opt.appendChild(_label);
// option tags (optional)
2025-08-11 00:15:58 +02:00
const tags = option.tags;
if (option.tags.length) {
2025-08-10 16:38:02 +02:00
const _tags = make("div", "tags");
2025-08-11 00:15:58 +02:00
_tags.title = `${this.#maxTags ? `${tags.length}/${this.#maxTags}: ` : ""}${tags.join(", ")}`;
if (this.#maxTags && tags.length >= this.#maxTags) {
const _all = make("div", "tag", "all");
2025-08-10 16:38:02 +02:00
2025-08-11 00:15:58 +02:00
_tags.appendChild(_all);
} else {
for (const tag of tags) {
const _tag = make("div", "tag", tag);
2025-08-10 16:38:02 +02:00
2025-08-11 00:15:58 +02:00
_tags.appendChild(_tag);
}
2025-08-10 16:38:02 +02:00
}
_opt.appendChild(_tags);
}
// add to options
2025-08-09 22:14:35 +02:00
_options.appendChild(_opt);
2025-08-09 21:16:24 +02:00
option.el = _opt;
}
2025-08-09 22:14:35 +02:00
// 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);
}
2025-08-09 21:16:24 +02:00
// add to dom
this.#_select.after(this.#_dropdown);
this.#render();
}
#render() {
if (this.#selected === false) {
2025-08-10 16:38:02 +02:00
this.#_selected.innerHTML = "";
2025-08-09 21:16:24 +02:00
return;
}
const selection = this.#options[this.#selected];
2025-08-10 16:38:02 +02:00
this.#_selected.innerHTML = selection.el.innerHTML;
2025-08-09 21:16:24 +02:00
}
2025-08-09 22:14:35 +02:00
#filter() {
if (!this.#_search) {
return;
}
2025-08-09 21:16:24 +02:00
2025-08-09 22:14:35 +02:00
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) {
2025-08-09 21:16:24 +02:00
const index = this.#options.findIndex((option) => option.value === value);
if (this.#selected === index) {
return;
}
this.#selected = index !== -1 ? index : false;
this.#render();
}
}
2025-08-09 22:14:35 +02:00
function searchable(text) {
// lowercase
text = text.toLowerCase();
// only alpha-num
text = text.replace(/[^\w]/g, "");
return text.trim();
}
2025-08-09 21:16:24 +02:00
document.body.addEventListener("click", (event) => {
const clicked = event.target.closest(".dropdown");
document.querySelectorAll(".dropdown").forEach((element) => {
if (element === clicked) {
return;
}
element.classList.remove("open");
});
});
2025-08-11 00:15:58 +02:00
window.dropdown = (el, maxTags = false) => new Dropdown(el, maxTags);
2025-08-09 21:16:24 +02:00
})();