mirror of
https://github.com/Theodor-Springmann-Stiftung/musenalm.git
synced 2025-10-28 16:55:32 +00:00
672 lines
28 KiB
JavaScript
672 lines
28 KiB
JavaScript
// --- Class Name Constants for MultiSelectSimple ---
|
|
const MSS_COMPONENT_WRAPPER_CLASS = "mss-component-wrapper";
|
|
const MSS_SELECTED_ITEMS_CONTAINER_CLASS = "mss-selected-items-container";
|
|
const MSS_SELECTED_ITEM_PILL_CLASS = "mss-selected-item-pill";
|
|
const MSS_SELECTED_ITEM_TEXT_CLASS = "mss-selected-item-text";
|
|
const MSS_SELECTED_ITEM_PILL_DETAIL_CLASS = "mss-selected-item-pill-detail"; // New class for pill detail
|
|
const MSS_SELECTED_ITEM_DELETE_BTN_CLASS = "mss-selected-item-delete-btn";
|
|
const MSS_INPUT_CONTROLS_CONTAINER_CLASS = "mss-input-controls-container";
|
|
const MSS_INPUT_WRAPPER_CLASS = "mss-input-wrapper";
|
|
const MSS_INPUT_WRAPPER_FOCUSED_CLASS = "mss-input-wrapper-focused";
|
|
const MSS_TEXT_INPUT_CLASS = "mss-text-input";
|
|
const MSS_CREATE_NEW_BUTTON_CLASS = "mss-create-new-button";
|
|
const MSS_OPTIONS_LIST_CLASS = "mss-options-list";
|
|
const MSS_OPTION_ITEM_CLASS = "mss-option-item";
|
|
const MSS_OPTION_ITEM_NAME_CLASS = "mss-option-item-name";
|
|
const MSS_OPTION_ITEM_DETAIL_CLASS = "mss-option-item-detail";
|
|
const MSS_HIGHLIGHTED_OPTION_CLASS = "mss-option-item-highlighted";
|
|
const MSS_HIDDEN_SELECT_CLASS = "mss-hidden-select";
|
|
const MSS_NO_ITEMS_TEXT_CLASS = "mss-no-items-text";
|
|
|
|
// State classes for MultiSelectSimple
|
|
const MSS_STATE_NO_SELECTION = "mss-state-no-selection";
|
|
const MSS_STATE_HAS_SELECTION = "mss-state-has-selection";
|
|
const MSS_STATE_LIST_OPEN = "mss-state-list-open";
|
|
|
|
// --- MultiSelectSimple Element ---
|
|
export class MultiSelectSimple extends HTMLElement {
|
|
static formAssociated = true;
|
|
|
|
constructor() {
|
|
super();
|
|
this.internals_ = this.attachInternals();
|
|
this._value = [];
|
|
this._options = [
|
|
{ id: "abk", name: "Abchasisch" },
|
|
{ id: "aar", name: "Afar" },
|
|
{ id: "afr", name: "Afrikaans" },
|
|
{ id: "aka", name: "Akan" },
|
|
{ id: "alb", name: "Albanisch" },
|
|
{ id: "amh", name: "Amharisch" },
|
|
{ id: "ara", name: "Arabisch" },
|
|
{ id: "arg", name: "Aragonesisch" },
|
|
{ id: "arm", name: "Armenisch" },
|
|
{ id: "asm", name: "Assamesisch" },
|
|
{ id: "ava", name: "Awarisch" },
|
|
{ id: "ave", name: "Avestisch" },
|
|
{ id: "aym", name: "Aymara" },
|
|
{ id: "aze", name: "Aserbaidschanisch" },
|
|
{ id: "bam", name: "Bambara" },
|
|
{ id: "bak", name: "Baschkirisch" },
|
|
{ id: "baq", name: "Baskisch" },
|
|
{ id: "bel", name: "Belarussisch" },
|
|
{ id: "ben", name: "Bengalisch" },
|
|
{ id: "bis", name: "Bislama" },
|
|
{ id: "bos", name: "Bosnisch" },
|
|
{ id: "bre", name: "Bretonisch" },
|
|
{ id: "bul", name: "Bulgarisch" },
|
|
{ id: "bur", name: "Birmanisch" },
|
|
{ id: "cat", name: "Katalanisch" },
|
|
{ id: "cha", name: "Chamorro" },
|
|
{ id: "che", name: "Tschetschenisch" },
|
|
{ id: "nya", name: "Nyanja" },
|
|
{ id: "chi", name: "Chinesisch" },
|
|
{ id: "chu", name: "Kirchenslawisch" },
|
|
{ id: "chv", name: "Tschuwaschisch" },
|
|
{ id: "cor", name: "Kornisch" },
|
|
{ id: "cos", name: "Korsisch" },
|
|
{ id: "cre", name: "Cree" },
|
|
{ id: "hrv", name: "Kroatisch" },
|
|
{ id: "cze", name: "Tschechisch" },
|
|
{ id: "dan", name: "Dänisch" },
|
|
{ id: "div", name: "Dhivehi" },
|
|
{ id: "dut", name: "Niederländisch" },
|
|
{ id: "dzo", name: "Dzongkha" },
|
|
{ id: "eng", name: "Englisch" },
|
|
{ id: "epo", name: "Esperanto" },
|
|
{ id: "est", name: "Estnisch" },
|
|
{ id: "ewe", name: "Ewe" },
|
|
{ id: "fao", name: "Färöisch" },
|
|
{ id: "fij", name: "Fidschianisch" },
|
|
{ id: "fin", name: "Finnisch" },
|
|
{ id: "fre", name: "Französisch" },
|
|
{ id: "fry", name: "Westfriesisch" },
|
|
{ id: "ful", name: "Ful" },
|
|
{ id: "gla", name: "Schottisch-Gälisch" },
|
|
{ id: "glg", name: "Galicisch" },
|
|
{ id: "lug", name: "Ganda" },
|
|
{ id: "geo", name: "Georgisch" },
|
|
{ id: "ger", name: "Deutsch" },
|
|
{ id: "gre", name: "Griechisch" },
|
|
{ id: "kal", name: "Kalaallisut" },
|
|
{ id: "grn", name: "Guaraní" },
|
|
{ id: "guj", name: "Gujarati" },
|
|
{ id: "hat", name: "Haitianisch-Kreolisch" },
|
|
{ id: "hau", name: "Hausa" },
|
|
{ id: "heb", name: "Hebräisch" },
|
|
{ id: "her", name: "Herero" },
|
|
{ id: "hin", name: "Hindi" },
|
|
{ id: "hmo", name: "Hiri Motu" },
|
|
{ id: "hun", name: "Ungarisch" },
|
|
{ id: "ice", name: "Isländisch" },
|
|
{ id: "ido", name: "Ido" },
|
|
{ id: "ibo", name: "Igbo" },
|
|
{ id: "ind", name: "Indonesisch" },
|
|
{ id: "ina", name: "Interlingua" },
|
|
{ id: "ile", name: "Interlingue" },
|
|
{ id: "iku", name: "Inuktitut" },
|
|
{ id: "ipk", name: "Inupiaq" },
|
|
{ id: "gle", name: "Irisch" },
|
|
{ id: "ita", name: "Italienisch" },
|
|
{ id: "jpn", name: "Japanisch" },
|
|
{ id: "jav", name: "Javanisch" },
|
|
{ id: "kan", name: "Kannada" },
|
|
{ id: "kau", name: "Kanuri" },
|
|
{ id: "kas", name: "Kashmiri" },
|
|
{ id: "kaz", name: "Kasachisch" },
|
|
{ id: "khm", name: "Khmer" },
|
|
{ id: "kik", name: "Kikuyu" },
|
|
{ id: "kin", name: "Kinyarwanda" },
|
|
{ id: "kir", name: "Kirgisisch" },
|
|
{ id: "kom", name: "Komi" },
|
|
{ id: "kon", name: "Kongo" },
|
|
{ id: "kor", name: "Koreanisch" },
|
|
{ id: "kua", name: "Kwanyama" },
|
|
{ id: "kur", name: "Kurdisch" },
|
|
{ id: "lao", name: "Laotisch" },
|
|
{ id: "lat", name: "Latein" },
|
|
{ id: "lav", name: "Lettisch" },
|
|
{ id: "lim", name: "Limburgisch" },
|
|
{ id: "lin", name: "Lingala" },
|
|
{ id: "lit", name: "Litauisch" },
|
|
{ id: "lub", name: "Luba-Katanga" },
|
|
{ id: "ltz", name: "Luxemburgisch" },
|
|
{ id: "mac", name: "Mazedonisch" },
|
|
{ id: "mlg", name: "Malagasy" },
|
|
{ id: "may", name: "Malaiisch" },
|
|
{ id: "mal", name: "Malayalam" },
|
|
{ id: "mlt", name: "Maltesisch" },
|
|
{ id: "glv", name: "Manx" },
|
|
{ id: "mao", name: "Maori" },
|
|
{ id: "mar", name: "Marathi" },
|
|
{ id: "mah", name: "Marshallesisch" },
|
|
{ id: "mon", name: "Mongolisch" },
|
|
{ id: "nau", name: "Nauruisch" },
|
|
{ id: "nav", name: "Navajo" },
|
|
{ id: "nde", name: "Nord-Ndebele" },
|
|
{ id: "nbl", name: "Süd-Ndebele" },
|
|
{ id: "ndo", name: "Ndonga" },
|
|
{ id: "nep", name: "Nepali" },
|
|
{ id: "nor", name: "Norwegisch" },
|
|
{ id: "nob", name: "Norwegisch Bokmål" },
|
|
{ id: "nno", name: "Norwegisch Nynorsk" },
|
|
{ id: "oci", name: "Okzitanisch" },
|
|
{ id: "oji", name: "Ojibwa" },
|
|
{ id: "ori", name: "Oriya" },
|
|
{ id: "orm", name: "Oromo" },
|
|
{ id: "oss", name: "Ossetisch" },
|
|
{ id: "pli", name: "Pali" },
|
|
{ id: "pus", name: "Paschtu" },
|
|
{ id: "per", name: "Persisch" },
|
|
{ id: "pol", name: "Polnisch" },
|
|
{ id: "por", name: "Portugiesisch" },
|
|
{ id: "pan", name: "Panjabi" },
|
|
{ id: "que", name: "Quechua" },
|
|
{ id: "rum", name: "Rumänisch" },
|
|
{ id: "roh", name: "Rätoromanisch" },
|
|
{ id: "run", name: "Rundi" },
|
|
{ id: "rus", name: "Russisch" },
|
|
{ id: "sme", name: "Nordsamisch" },
|
|
{ id: "smo", name: "Samoanisch" },
|
|
{ id: "sag", name: "Sango" },
|
|
{ id: "san", name: "Sanskrit" },
|
|
{ id: "srd", name: "Sardisch" },
|
|
{ id: "srp", name: "Serbisch" },
|
|
{ id: "sna", name: "Shona" },
|
|
{ id: "snd", name: "Sindhi" },
|
|
{ id: "sin", name: "Singhalesisch" },
|
|
{ id: "slo", name: "Slowakisch" },
|
|
{ id: "slv", name: "Slowenisch" },
|
|
{ id: "som", name: "Somali" },
|
|
{ id: "sot", name: "Süd-Sotho" },
|
|
{ id: "spa", name: "Spanisch" },
|
|
{ id: "sun", name: "Sundanesisch" },
|
|
{ id: "swa", name: "Swahili" },
|
|
{ id: "ssw", name: "Swazi" },
|
|
{ id: "swe", name: "Schwedisch" },
|
|
{ id: "tgl", name: "Tagalog" },
|
|
{ id: "tah", name: "Tahitisch" },
|
|
{ id: "tgk", name: "Tadschikisch" },
|
|
{ id: "tam", name: "Tamil" },
|
|
{ id: "tat", name: "Tatarisch" },
|
|
{ id: "tel", name: "Telugu" },
|
|
{ id: "tha", name: "Thailändisch" },
|
|
{ id: "tib", name: "Tibetisch" },
|
|
{ id: "tir", name: "Tigrinya" },
|
|
{ id: "ton", name: "Tongaisch" },
|
|
{ id: "tso", name: "Tsonga" },
|
|
{ id: "tsn", name: "Tswana" },
|
|
{ id: "tur", name: "Türkisch" },
|
|
{ id: "tuk", name: "Turkmenisch" },
|
|
{ id: "twi", name: "Twi" },
|
|
{ id: "uig", name: "Uigurisch" },
|
|
{ id: "ukr", name: "Ukrainisch" },
|
|
{ id: "urd", name: "Urdu" },
|
|
{ id: "uzb", name: "Usbekisch" },
|
|
{ id: "ven", name: "Venda" },
|
|
{ id: "vie", name: "Vietnamesisch" },
|
|
{ id: "vol", name: "Volapük" },
|
|
{ id: "wln", name: "Wallonisch" },
|
|
{ id: "wel", name: "Walisisch" },
|
|
{ id: "wol", name: "Wolof" },
|
|
{ id: "xho", name: "Xhosa" },
|
|
{ id: "iii", name: "Sichuan Yi" },
|
|
{ id: "yid", name: "Jiddisch" },
|
|
{ id: "yor", name: "Yoruba" },
|
|
{ id: "zha", name: "Zhuang" },
|
|
{ id: "zul", name: "Zulu" },
|
|
];
|
|
this._filteredOptions = [];
|
|
this._highlightedIndex = -1;
|
|
this._isOptionsListVisible = false;
|
|
|
|
this._placeholder = this.getAttribute("placeholder") || "Search items...";
|
|
this._showCreateButton = this.getAttribute("show-create-button") !== "false";
|
|
|
|
this._setupTemplates();
|
|
this._bindEventHandlers();
|
|
}
|
|
|
|
_setupTemplates() {
|
|
this.optionTemplate = document.createElement("template");
|
|
this.optionTemplate.innerHTML = `
|
|
<li role="option" class="${MSS_OPTION_ITEM_CLASS}">
|
|
<span data-ref="nameEl" class="${MSS_OPTION_ITEM_NAME_CLASS}"></span>
|
|
<span data-ref="detailEl" class="${MSS_OPTION_ITEM_DETAIL_CLASS}"></span>
|
|
</li>
|
|
`;
|
|
|
|
this.selectedItemTemplate = document.createElement("template");
|
|
// Apply new MSS_SELECTED_ITEM_PILL_DETAIL_CLASS to the detail span
|
|
this.selectedItemTemplate.innerHTML = `
|
|
<span class="${MSS_SELECTED_ITEM_PILL_CLASS} flex items-center">
|
|
<span data-ref="textEl" class="${MSS_SELECTED_ITEM_TEXT_CLASS}"></span>
|
|
<span data-ref="detailEl" class="${MSS_SELECTED_ITEM_PILL_DETAIL_CLASS} hidden"></span>
|
|
<button type="button" data-ref="deleteBtn" class="${MSS_SELECTED_ITEM_DELETE_BTN_CLASS}">×</button>
|
|
</span>
|
|
`;
|
|
}
|
|
|
|
_bindEventHandlers() {
|
|
this._handleInput = this._handleInput.bind(this);
|
|
this._handleKeyDown = this._handleKeyDown.bind(this);
|
|
this._handleFocus = this._handleFocus.bind(this);
|
|
this._handleBlur = this._handleBlur.bind(this);
|
|
this._handleOptionMouseDown = this._handleOptionMouseDown.bind(this);
|
|
this._handleOptionClick = this._handleOptionClick.bind(this);
|
|
this._handleCreateNewButtonClick = this._handleCreateNewButtonClick.bind(this);
|
|
this._handleSelectedItemsContainerClick = this._handleSelectedItemsContainerClick.bind(this);
|
|
}
|
|
|
|
_getItemById(id) {
|
|
return this._options.find((opt) => opt.id === id);
|
|
}
|
|
|
|
get placeholder() {
|
|
return this._placeholder;
|
|
}
|
|
set placeholder(value) {
|
|
this._placeholder = value;
|
|
if (this.inputElement) this.inputElement.placeholder = this._placeholder;
|
|
this.setAttribute("placeholder", value);
|
|
}
|
|
|
|
get showCreateButton() {
|
|
return this._showCreateButton;
|
|
}
|
|
set showCreateButton(value) {
|
|
const boolValue = String(value).toLowerCase() !== "false" && value !== false;
|
|
if (this._showCreateButton === boolValue) return;
|
|
this._showCreateButton = boolValue;
|
|
if (this.createNewButton) this.createNewButton.classList.toggle("hidden", !this._showCreateButton);
|
|
this.setAttribute("show-create-button", this._showCreateButton ? "true" : "false");
|
|
}
|
|
|
|
setOptions(newOptions) {
|
|
if (Array.isArray(newOptions) && newOptions.every((o) => o && typeof o.id === "string" && typeof o.name === "string")) {
|
|
this._options = [...newOptions];
|
|
const validValues = this._value.filter((id) => this._getItemById(id));
|
|
if (validValues.length !== this._value.length) this.value = validValues;
|
|
else if (this.selectedItemsContainer) this._renderSelectedItems();
|
|
this._filteredOptions = [];
|
|
this._highlightedIndex = -1;
|
|
if (this.inputElement && this.inputElement.value) this._handleInput({ target: this.inputElement });
|
|
else this._hideOptionsList();
|
|
} else console.error("setOptions expects an array of objects with id and name properties.");
|
|
}
|
|
|
|
get value() {
|
|
return this._value;
|
|
}
|
|
set value(newVal) {
|
|
const oldValString = JSON.stringify(this._value.sort());
|
|
if (Array.isArray(newVal)) {
|
|
this._value = [...new Set(newVal.filter((id) => typeof id === "string" && this._getItemById(id)))];
|
|
} else if (typeof newVal === "string" && newVal.trim() !== "") {
|
|
const singleId = newVal.trim();
|
|
if (this._getItemById(singleId) && !this._value.includes(singleId)) this._value = [singleId];
|
|
else if (!this._getItemById(singleId)) this._value = this._value.filter((id) => id !== singleId);
|
|
} else this._value = [];
|
|
const newValString = JSON.stringify(this._value.sort());
|
|
if (oldValString !== newValString) {
|
|
this._updateFormValue();
|
|
if (this.selectedItemsContainer) this._renderSelectedItems();
|
|
this._updateRootElementStateClasses();
|
|
this.dispatchEvent(new Event("change", { bubbles: true }));
|
|
}
|
|
}
|
|
|
|
get name() {
|
|
return this.getAttribute("name");
|
|
}
|
|
set name(value) {
|
|
this.setAttribute("name", value);
|
|
if (this.hiddenSelect) this.hiddenSelect.name = value;
|
|
}
|
|
|
|
connectedCallback() {
|
|
this._render();
|
|
this.inputControlsContainer = this.querySelector(`.${MSS_INPUT_CONTROLS_CONTAINER_CLASS}`);
|
|
this.inputWrapper = this.querySelector(`.${MSS_INPUT_WRAPPER_CLASS}`);
|
|
this.inputElement = this.querySelector(`.${MSS_TEXT_INPUT_CLASS}`);
|
|
this.createNewButton = this.querySelector(`.${MSS_CREATE_NEW_BUTTON_CLASS}`);
|
|
this.optionsListElement = this.querySelector(`.${MSS_OPTIONS_LIST_CLASS}`);
|
|
this.selectedItemsContainer = this.querySelector(`.${MSS_SELECTED_ITEMS_CONTAINER_CLASS}`);
|
|
this.hiddenSelect = this.querySelector(`.${MSS_HIDDEN_SELECT_CLASS}`);
|
|
|
|
this.placeholder = this.getAttribute("placeholder") || "Search items...";
|
|
this.showCreateButton = this.getAttribute("show-create-button") !== "false";
|
|
|
|
if (this.name && this.hiddenSelect) this.hiddenSelect.name = this.name;
|
|
|
|
this.inputElement.addEventListener("input", this._handleInput);
|
|
this.inputElement.addEventListener("keydown", this._handleKeyDown);
|
|
this.inputElement.addEventListener("focus", this._handleFocus);
|
|
this.inputElement.addEventListener("blur", this._handleBlur);
|
|
this.optionsListElement.addEventListener("mousedown", this._handleOptionMouseDown);
|
|
this.optionsListElement.addEventListener("click", this._handleOptionClick);
|
|
this.createNewButton.addEventListener("click", this._handleCreateNewButtonClick);
|
|
this.selectedItemsContainer.addEventListener("click", this._handleSelectedItemsContainerClick);
|
|
|
|
this._updateRootElementStateClasses();
|
|
if (this.hasAttribute("value")) {
|
|
const attrValue = this.getAttribute("value");
|
|
try {
|
|
this.value = JSON.parse(attrValue);
|
|
} catch (e) {
|
|
this.value = attrValue
|
|
.split(",")
|
|
.map((s) => s.trim())
|
|
.filter(Boolean);
|
|
}
|
|
} else {
|
|
this._renderSelectedItems();
|
|
this._synchronizeHiddenSelect();
|
|
}
|
|
if (this.hasAttribute("disabled")) this.disabledCallback(true);
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
if (this.inputElement) {
|
|
this.inputElement.removeEventListener("input", this._handleInput);
|
|
this.inputElement.removeEventListener("keydown", this._handleKeyDown);
|
|
this.inputElement.removeEventListener("focus", this._handleFocus);
|
|
this.inputElement.removeEventListener("blur", this._handleBlur);
|
|
}
|
|
if (this.optionsListElement) {
|
|
this.optionsListElement.removeEventListener("mousedown", this._handleOptionMouseDown);
|
|
this.optionsListElement.removeEventListener("click", this._handleOptionClick);
|
|
}
|
|
if (this.createNewButton) this.createNewButton.removeEventListener("click", this._handleCreateNewButtonClick);
|
|
if (this.selectedItemsContainer) this.selectedItemsContainer.removeEventListener("click", this._handleSelectedItemsContainerClick);
|
|
clearTimeout(this._blurTimeout);
|
|
}
|
|
|
|
static get observedAttributes() {
|
|
return ["disabled", "name", "value", "placeholder", "show-create-button"];
|
|
}
|
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
if (oldValue === newValue) return;
|
|
if (name === "disabled") this.disabledCallback(this.hasAttribute("disabled"));
|
|
else if (name === "name" && this.hiddenSelect) this.hiddenSelect.name = newValue;
|
|
else if (name === "value" && this.inputElement) {
|
|
try {
|
|
this.value = JSON.parse(newValue);
|
|
} catch (e) {
|
|
this.value = newValue
|
|
.split(",")
|
|
.map((s) => s.trim())
|
|
.filter(Boolean);
|
|
}
|
|
} else if (name === "placeholder") this.placeholder = newValue;
|
|
else if (name === "show-create-button") this.showCreateButton = newValue;
|
|
}
|
|
|
|
formAssociatedCallback(form) {}
|
|
formDisabledCallback(disabled) {
|
|
this.disabledCallback(disabled);
|
|
}
|
|
formResetCallback() {
|
|
this.value = [];
|
|
this._hideOptionsList();
|
|
if (this.inputElement) this.inputElement.value = "";
|
|
this.placeholder = this.getAttribute("placeholder") || "Search items...";
|
|
this.showCreateButton = this.getAttribute("show-create-button") !== "false";
|
|
this._updateRootElementStateClasses();
|
|
this._renderSelectedItems();
|
|
}
|
|
formStateRestoreCallback(state, mode) {
|
|
this.value = Array.isArray(state) ? state : [];
|
|
this._updateRootElementStateClasses();
|
|
}
|
|
|
|
_synchronizeHiddenSelect() {
|
|
if (!this.hiddenSelect) return;
|
|
this.hiddenSelect.innerHTML = "";
|
|
this._value.forEach((id) => {
|
|
const option = document.createElement("option");
|
|
option.value = id;
|
|
const itemData = this._getItemById(id);
|
|
option.textContent = itemData ? itemData.name : id;
|
|
option.selected = true;
|
|
this.hiddenSelect.appendChild(option);
|
|
});
|
|
}
|
|
_updateFormValue() {
|
|
this.internals_.setFormValue(null);
|
|
this._synchronizeHiddenSelect();
|
|
}
|
|
disabledCallback(disabled) {
|
|
if (this.inputElement) this.inputElement.disabled = disabled;
|
|
if (this.createNewButton) this.createNewButton.disabled = disabled;
|
|
this.toggleAttribute("disabled", disabled);
|
|
this.querySelectorAll(`.${MSS_SELECTED_ITEM_DELETE_BTN_CLASS}`).forEach((btn) => (btn.disabled = disabled));
|
|
if (this.hiddenSelect) this.hiddenSelect.disabled = disabled;
|
|
if (disabled) this._hideOptionsList();
|
|
}
|
|
_updateRootElementStateClasses() {
|
|
this.classList.toggle(MSS_STATE_NO_SELECTION, this._value.length === 0);
|
|
this.classList.toggle(MSS_STATE_HAS_SELECTION, this._value.length > 0);
|
|
this.classList.toggle(MSS_STATE_LIST_OPEN, this._isOptionsListVisible);
|
|
}
|
|
_render() {
|
|
const componentId = this.id || `mss-${crypto.randomUUID().slice(0, 8)}`;
|
|
if (!this.id) this.setAttribute("id", componentId);
|
|
this.innerHTML = `
|
|
<style>
|
|
.${MSS_HIDDEN_SELECT_CLASS} { display: block !important; visibility: hidden !important; position: absolute !important; width: 0px !important; height: 0px !important; opacity: 0 !important; pointer-events: none !important; margin: -1px !important; padding: 0 !important; border: 0 !important; overflow: hidden !important; clip: rect(0, 0, 0, 0) !important; white-space: nowrap !important; }
|
|
</style>
|
|
<div class="${MSS_COMPONENT_WRAPPER_CLASS} relative">
|
|
<div class="${MSS_SELECTED_ITEMS_CONTAINER_CLASS} flex flex-wrap gap-1 mb-1 min-h-[38px]" aria-live="polite" tabindex="-1"></div>
|
|
<div class="${MSS_INPUT_CONTROLS_CONTAINER_CLASS} flex items-center space-x-2">
|
|
<div class="${MSS_INPUT_WRAPPER_CLASS} relative rounded-md flex items-center flex-grow">
|
|
<input type="text"
|
|
class="${MSS_TEXT_INPUT_CLASS} w-full outline-none bg-transparent text-sm"
|
|
placeholder="${this.placeholder}"
|
|
aria-autocomplete="list"
|
|
aria-expanded="${this._isOptionsListVisible}"
|
|
aria-controls="options-list-${componentId}"
|
|
autocomplete="off" autocorrect="off" autocapitalize="none" spellcheck="false" role="combobox" />
|
|
</div>
|
|
<button type="button" class="${MSS_CREATE_NEW_BUTTON_CLASS} ${!this.showCreateButton ? "hidden" : ""}" title="Create new item from input">+</button>
|
|
</div>
|
|
<ul id="options-list-${componentId}" role="listbox" class="${MSS_OPTIONS_LIST_CLASS} absolute z-20 w-full max-h-60 overflow-y-auto mt-1 hidden"></ul>
|
|
<select multiple name="${this.getAttribute("name") || "mss_default_name"}" id="hidden-select-${componentId}" class="${MSS_HIDDEN_SELECT_CLASS}" aria-hidden="true"></select>
|
|
</div>
|
|
`;
|
|
}
|
|
_createSelectedItemElement(itemId) {
|
|
const itemData = this._getItemById(itemId);
|
|
if (!itemData) return null;
|
|
const fragment = this.selectedItemTemplate.content.cloneNode(true);
|
|
const pillEl = fragment.firstElementChild;
|
|
const textEl = pillEl.querySelector('[data-ref="textEl"]');
|
|
const detailEl = pillEl.querySelector('[data-ref="detailEl"]'); // This now uses MSS_SELECTED_ITEM_PILL_DETAIL_CLASS
|
|
const deleteBtn = pillEl.querySelector('[data-ref="deleteBtn"]');
|
|
textEl.textContent = itemData.name;
|
|
if (itemData.additional_data) {
|
|
detailEl.textContent = `(${itemData.additional_data})`;
|
|
detailEl.classList.remove("hidden"); // Toggle visibility via JS
|
|
} else {
|
|
detailEl.classList.add("hidden"); // Toggle visibility via JS
|
|
}
|
|
deleteBtn.setAttribute("aria-label", `Remove ${itemData.name}`);
|
|
deleteBtn.dataset.id = itemId;
|
|
deleteBtn.disabled = this.hasAttribute("disabled");
|
|
deleteBtn.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
this._handleDeleteSelectedItem(itemId);
|
|
});
|
|
return pillEl;
|
|
}
|
|
_renderSelectedItems() {
|
|
if (!this.selectedItemsContainer) return;
|
|
this.selectedItemsContainer.innerHTML = "";
|
|
if (this._value.length === 0) {
|
|
this.selectedItemsContainer.innerHTML = `<span class="${MSS_NO_ITEMS_TEXT_CLASS}">Keine Sprachen ausgewählt...</span>`;
|
|
} else {
|
|
this._value.forEach((id) => {
|
|
const pillEl = this._createSelectedItemElement(id);
|
|
if (pillEl) this.selectedItemsContainer.appendChild(pillEl);
|
|
});
|
|
}
|
|
this._updateRootElementStateClasses();
|
|
}
|
|
_createOptionElement(itemData, index) {
|
|
const fragment = this.optionTemplate.content.cloneNode(true);
|
|
const li = fragment.firstElementChild;
|
|
const nameEl = li.querySelector('[data-ref="nameEl"]');
|
|
const detailEl = li.querySelector('[data-ref="detailEl"]');
|
|
nameEl.textContent = itemData.name;
|
|
detailEl.textContent = itemData.additional_data ? `(${itemData.additional_data})` : "";
|
|
li.dataset.id = itemData.id;
|
|
li.setAttribute("aria-selected", String(index === this._highlightedIndex));
|
|
const optionElementId = `option-${this.id || "mss"}-${itemData.id}`;
|
|
li.id = optionElementId;
|
|
if (index === this._highlightedIndex) {
|
|
li.classList.add(MSS_HIGHLIGHTED_OPTION_CLASS);
|
|
if (this.inputElement) this.inputElement.setAttribute("aria-activedescendant", optionElementId);
|
|
}
|
|
return li;
|
|
}
|
|
_renderOptionsList() {
|
|
if (!this.optionsListElement || !this.inputElement) return;
|
|
this.optionsListElement.innerHTML = "";
|
|
this.inputElement.removeAttribute("aria-activedescendant");
|
|
if (this._filteredOptions.length === 0 || !this._isOptionsListVisible) {
|
|
this.optionsListElement.classList.add("hidden");
|
|
this.inputElement.setAttribute("aria-expanded", "false");
|
|
} else {
|
|
this.optionsListElement.classList.remove("hidden");
|
|
this.inputElement.setAttribute("aria-expanded", "true");
|
|
this._filteredOptions.forEach((item, index) => {
|
|
const optionEl = this._createOptionElement(item, index);
|
|
this.optionsListElement.appendChild(optionEl);
|
|
});
|
|
const highlightedElement = this.optionsListElement.querySelector(`.${MSS_HIGHLIGHTED_OPTION_CLASS}`);
|
|
if (highlightedElement) {
|
|
highlightedElement.scrollIntoView({ block: "nearest" });
|
|
this.inputElement.setAttribute("aria-activedescendant", highlightedElement.id);
|
|
}
|
|
}
|
|
this._updateRootElementStateClasses();
|
|
}
|
|
_handleSelectedItemsContainerClick(event) {
|
|
if (event.target === this.selectedItemsContainer && this.inputElement && !this.inputElement.disabled) {
|
|
this.inputElement.focus();
|
|
}
|
|
}
|
|
_handleCreateNewButtonClick() {
|
|
if (this.hasAttribute("disabled") || !this.showCreateButton) return;
|
|
const inputValue = this.inputElement ? this.inputElement.value.trim() : "";
|
|
this.dispatchEvent(
|
|
new CustomEvent("createnew", {
|
|
detail: { value: inputValue },
|
|
bubbles: true,
|
|
composed: true,
|
|
}),
|
|
);
|
|
}
|
|
_handleInput(event) {
|
|
const searchTerm = event.target.value;
|
|
if (searchTerm.length === 0) {
|
|
this._filteredOptions = [];
|
|
this._isOptionsListVisible = false;
|
|
} else {
|
|
const searchTermLower = searchTerm.toLowerCase();
|
|
this._filteredOptions = this._options.filter((item) => {
|
|
if (this._value.includes(item.id)) return false;
|
|
const nameMatch = item.name.toLowerCase().includes(searchTermLower);
|
|
const additionalDataMatch = item.additional_data && item.additional_data.toLowerCase().includes(searchTermLower);
|
|
return nameMatch || additionalDataMatch;
|
|
});
|
|
this._isOptionsListVisible = this._filteredOptions.length > 0;
|
|
}
|
|
this._highlightedIndex = this._filteredOptions.length > 0 ? 0 : -1;
|
|
this._renderOptionsList();
|
|
}
|
|
_handleKeyDown(event) {
|
|
if (this.inputElement.disabled) return;
|
|
// Removed: Backspace on empty input to delete last item
|
|
// if (event.key === "Backspace" && this.inputElement.value === "" && this._value.length > 0) {
|
|
// event.preventDefault();
|
|
// const lastItemId = this._value[this._value.length - 1];
|
|
// this._handleDeleteSelectedItem(lastItemId);
|
|
// return;
|
|
// }
|
|
if (!this._isOptionsListVisible || this._filteredOptions.length === 0) {
|
|
if (event.key === "Enter" && this.inputElement.value.length > 0) {
|
|
event.preventDefault();
|
|
}
|
|
if (event.key === "Escape") this._hideOptionsList();
|
|
if ((event.key === "ArrowDown" || event.key === "ArrowUp") && this.inputElement.value.length > 0) {
|
|
this._handleInput({ target: this.inputElement });
|
|
}
|
|
return;
|
|
}
|
|
switch (event.key) {
|
|
case "ArrowDown":
|
|
event.preventDefault();
|
|
this._highlightedIndex = (this._highlightedIndex + 1) % this._filteredOptions.length;
|
|
this._renderOptionsList();
|
|
break;
|
|
case "ArrowUp":
|
|
event.preventDefault();
|
|
this._highlightedIndex = (this._highlightedIndex - 1 + this._filteredOptions.length) % this._filteredOptions.length;
|
|
this._renderOptionsList();
|
|
break;
|
|
case "Enter":
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
if (this._highlightedIndex > -1 && this._filteredOptions[this._highlightedIndex]) {
|
|
this._selectItem(this._filteredOptions[this._highlightedIndex].id);
|
|
}
|
|
break;
|
|
case "Escape":
|
|
event.preventDefault();
|
|
this._hideOptionsList();
|
|
break;
|
|
case "Tab":
|
|
this._hideOptionsList();
|
|
break;
|
|
}
|
|
}
|
|
_hideOptionsList() {
|
|
this._isOptionsListVisible = false;
|
|
this._highlightedIndex = -1;
|
|
if (this.optionsListElement) this._renderOptionsList();
|
|
}
|
|
_handleFocus() {
|
|
if (this.inputElement.disabled) return;
|
|
if (this.inputWrapper) this.inputWrapper.classList.add(MSS_INPUT_WRAPPER_FOCUSED_CLASS);
|
|
if (this.inputElement.value.length > 0) this._handleInput({ target: this.inputElement });
|
|
this._updateRootElementStateClasses();
|
|
}
|
|
_blurTimeout = null;
|
|
_handleBlur() {
|
|
if (this.inputWrapper) this.inputWrapper.classList.remove(MSS_INPUT_WRAPPER_FOCUSED_CLASS);
|
|
this._blurTimeout = setTimeout(() => {
|
|
if (!this.contains(document.activeElement)) this._hideOptionsList();
|
|
}, 150);
|
|
}
|
|
_handleOptionMouseDown(event) {
|
|
event.preventDefault();
|
|
}
|
|
_handleOptionClick(event) {
|
|
const li = event.target.closest("li[data-id]");
|
|
if (li && li.dataset.id) this._selectItem(li.dataset.id);
|
|
}
|
|
_selectItem(itemId) {
|
|
if (itemId && !this._value.includes(itemId)) this.value = [...this._value, itemId];
|
|
if (this.inputElement) this.inputElement.value = "";
|
|
this._filteredOptions = [];
|
|
this._hideOptionsList();
|
|
if (this.inputElement && !this.hasAttribute("disabled")) this.inputElement.focus();
|
|
}
|
|
_handleDeleteSelectedItem(itemId) {
|
|
this.value = this._value.filter((id) => id !== itemId);
|
|
if (this.inputElement && this.inputElement.value) this._handleInput({ target: this.inputElement });
|
|
if (this.inputElement && !this.hasAttribute("disabled")) this.inputElement.focus();
|
|
}
|
|
}
|