mirror of
				https://github.com/Theodor-Springmann-Stiftung/musenalm.git
				synced 2025-10-30 17:55:31 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			532 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			532 lines
		
	
	
		
			22 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: "marie_curie", name: "Marie Curie", additional_data: "Physicist and Chemist" },
 | |
| 			{ id: "leonardo_da_vinci", name: "Leonardo da Vinci", additional_data: "Polymath" },
 | |
| 			{ id: "albert_einstein", name: "Albert Einstein", additional_data: "Theoretical Physicist" },
 | |
| 			{ id: "ada_lovelace", name: "Ada Lovelace", additional_data: "Mathematician and Writer" },
 | |
| 			{ id: "isaac_newton", name: "Isaac Newton", additional_data: "Mathematician and Physicist" },
 | |
| 			{
 | |
| 				id: "galileo_galilei",
 | |
| 				name: "Galileo Galilei",
 | |
| 				additional_data: "Astronomer and Physicist",
 | |
| 			},
 | |
| 			{ id: "charles_darwin", name: "Charles Darwin", additional_data: "Naturalist" },
 | |
| 			{ id: "nikola_tesla", name: "Nikola Tesla", additional_data: "Inventor and Engineer" },
 | |
| 			{ id: "jane_austen", name: "Jane Austen", additional_data: "Novelist" },
 | |
| 			{
 | |
| 				id: "william_shakespeare",
 | |
| 				name: "William Shakespeare",
 | |
| 				additional_data: "Playwright and Poet",
 | |
| 			},
 | |
| 		];
 | |
| 		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}">No items selected</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 === "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.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();
 | |
| 	}
 | |
| }
 | 
