-
-
+
+
+
-
+
-
+
`;
@@ -1011,7 +1011,7 @@ class dt extends HTMLElement {
_renderStagedPillOrInput() {
if (!(!this.stagedItemPillContainer || !this.inputElement || !this.inputAreaWrapper)) {
if (this.stagedItemPillContainer.innerHTML = "", this._stagedItem && this._stagedItem.item) {
- this.inputAreaWrapper.classList.remove(L), this.inputAreaWrapper.classList.add(w);
+ this.inputAreaWrapper.classList.remove(v), this.inputAreaWrapper.classList.add(w);
const t = this._createStagedItemPillElement(this._stagedItem.item);
this.stagedItemPillContainer.appendChild(t);
const e = this._getAvailableRolesForItem(this._stagedItem.item.id), s = this._createStagedRoleSelectElement(
@@ -1022,7 +1022,7 @@ class dt extends HTMLElement {
const n = this._createStagedCancelButtonElement(this._stagedItem.item.name);
this.stagedItemPillContainer.appendChild(n), this.inputElement.classList.add("hidden"), this.inputElement.value = "", this.inputElement.removeAttribute("aria-activedescendant"), this.inputElement.setAttribute("aria-expanded", "false");
} else
- this.inputAreaWrapper.classList.add(L), this.inputAreaWrapper.classList.remove(w), this.inputElement.classList.remove("hidden");
+ this.inputAreaWrapper.classList.add(v), this.inputAreaWrapper.classList.remove(w), this.inputElement.classList.remove("hidden");
this._updateAddButtonState(), this._updatePreAddButtonVisibility(), this._updateRootElementStateClasses();
}
}
@@ -1041,15 +1041,15 @@ class dt extends HTMLElement {
const e = this._getItemById(t.itemId);
if (!e) return null;
const n = this.selectedItemTemplate.content.cloneNode(!0).firstElementChild, r = n.querySelector('[data-ref="textEl"]');
- let l = `
${e.name}`, o = e.additional_data ? `
(${e.additional_data})` : "", h = `
${t.role}`;
+ let l = `
${e.name}`, o = e.additional_data ? `
(${e.additional_data})` : "", h = `
${t.role}`;
r.innerHTML = `${l}${o}${h}`;
const u = n.querySelector('[data-ref="deleteBtn"]');
- return u.setAttribute("aria-label", `Entferne ${e.name} als ${t.role}`), u.dataset.instanceId = t.instanceId, u.disabled = this.hasAttribute("disabled"), u.addEventListener("click", (ft) => {
- ft.stopPropagation(), this._handleDeleteSelectedItem(t.instanceId);
+ return u.setAttribute("aria-label", `Entferne ${e.name} als ${t.role}`), u.dataset.instanceId = t.instanceId, u.disabled = this.hasAttribute("disabled"), u.addEventListener("click", (Lt) => {
+ Lt.stopPropagation(), this._handleDeleteSelectedItem(t.instanceId);
}), n;
}
_renderSelectedItems() {
- this.selectedItemsContainer && (this.selectedItemsContainer.innerHTML = "", this._value.length === 0 ? this.selectedItemsContainer.innerHTML = `
${this.placeholderNoSelection}` : this._value.forEach((t) => {
+ this.selectedItemsContainer && (this.selectedItemsContainer.innerHTML = "", this._value.length === 0 ? this.selectedItemsContainer.innerHTML = `
${this.placeholderNoSelection}` : this._value.forEach((t) => {
const e = this._createSelectedItemElement(t);
e && this.selectedItemsContainer.appendChild(e);
}), this._updateRootElementStateClasses());
@@ -1062,7 +1062,7 @@ class dt extends HTMLElement {
}
_createOptionElement(t, e) {
const n = this.optionTemplate.content.cloneNode(!0).firstElementChild;
- return n.querySelector('[data-ref="nameEl"]').textContent = t.name, n.querySelector('[data-ref="detailEl"]').textContent = t.additional_data ? `(${t.additional_data})` : "", n.dataset.id = t.id, n.setAttribute("aria-selected", String(e === this._highlightedIndex)), n.id = `${this.id || "msr"}-option-${t.id}`, e === this._highlightedIndex && n.classList.add(j), n;
+ return n.querySelector('[data-ref="nameEl"]').textContent = t.name, n.querySelector('[data-ref="detailEl"]').textContent = t.additional_data ? `(${t.additional_data})` : "", n.dataset.id = t.id, n.setAttribute("aria-selected", String(e === this._highlightedIndex)), n.id = `${this.id || "msr"}-option-${t.id}`, e === this._highlightedIndex && n.classList.add(J), n;
}
_renderOptionsList() {
if (!(!this.optionsListElement || !this.inputElement)) {
@@ -1074,7 +1074,7 @@ class dt extends HTMLElement {
this.optionsListElement.appendChild(n);
});
const t = this.optionsListElement.querySelector(
- `.${j}`
+ `.${J}`
);
t ? (t.scrollIntoView({ block: "nearest" }), this.inputElement.setAttribute("aria-activedescendant", t.id)) : this.inputElement.removeAttribute("aria-activedescendant");
}
@@ -1123,7 +1123,7 @@ class dt extends HTMLElement {
if (!this.hasAttribute("disabled")) {
if (t.key === "Enter" && this._stagedItem && this._stagedItem.item) {
const s = document.activeElement, n = (e = this.stagedItemPillContainer) == null ? void 0 : e.querySelector(
- `.${z}`
+ `.${V}`
);
if (s === n) {
t.preventDefault(), this._handleCancelStagedItem(t);
@@ -1161,7 +1161,7 @@ class dt extends HTMLElement {
}
_handleFocus() {
if (!(this.hasAttribute("disabled") || this.inputElement && this.inputElement.disabled || this._stagedItem)) {
- if (!this._stagedItem && this.inputAreaWrapper && (this.inputAreaWrapper.classList.add(L), this.inputAreaWrapper.classList.remove(w)), this.inputElement && this.inputElement.value.length > 0) {
+ if (!this._stagedItem && this.inputAreaWrapper && (this.inputAreaWrapper.classList.add(v), this.inputAreaWrapper.classList.remove(w)), this.inputElement && this.inputElement.value.length > 0) {
const t = this.inputElement.value.toLowerCase();
this._filteredOptions = this._options.filter((e) => this._getAvailableRolesForItem(e.id).length === 0 ? !1 : e.name.toLowerCase().includes(t) || e.additional_data && e.additional_data.toLowerCase().includes(t)), this._filteredOptions.length > 0 ? (this._isOptionsListVisible = !0, this._highlightedIndex = 0, this._renderOptionsList()) : this._hideOptionsList();
} else
@@ -1180,7 +1180,7 @@ class dt extends HTMLElement {
}
_handleOptionClick(t) {
if (this.hasAttribute("disabled")) return;
- const e = t.target.closest(`li[data-id].${W}`);
+ const e = t.target.closest(`li[data-id].${Q}`);
if (e) {
const s = e.dataset.id, n = this._filteredOptions.find((r) => r.id === s);
n && this._stageItem(n);
@@ -1190,9 +1190,9 @@ class dt extends HTMLElement {
this.hasAttribute("disabled") || (this._value = this._value.filter((e) => e.instanceId !== t), this._updateFormValue(), this._renderSelectedItems(), this._stagedItem && this._stagedItem.item && this._renderStagedPillOrInput(), this.inputElement && this.inputElement.focus(), this._updatePreAddButtonVisibility());
}
}
-p(dt, "formAssociated", !0);
-const Vt = "mss-component-wrapper", Q = "mss-selected-items-container", Kt = "mss-selected-item-pill", Gt = "mss-selected-item-text", Wt = "mss-selected-item-pill-detail", J = "mss-selected-item-delete-btn", X = "mss-input-controls-container", Y = "mss-input-wrapper", Z = "mss-input-wrapper-focused", tt = "mss-text-input", et = "mss-create-new-button", it = "mss-options-list", jt = "mss-option-item", Qt = "mss-option-item-name", Jt = "mss-option-item-detail", st = "mss-option-item-highlighted", O = "mss-hidden-select", Xt = "mss-no-items-text", nt = "mss-loading", k = 1, R = 10, Yt = 250, Zt = "mss-state-no-selection", te = "mss-state-has-selection", ee = "mss-state-list-open";
-class ct extends HTMLElement {
+p(ft, "formAssociated", !0);
+const Jt = "mss-component-wrapper", X = "mss-selected-items-container", Xt = "mss-selected-item-pill", Yt = "mss-selected-item-text", Zt = "mss-selected-item-pill-detail", Y = "mss-selected-item-delete-btn", Z = "mss-input-controls-container", tt = "mss-input-wrapper", et = "mss-input-wrapper-focused", it = "mss-text-input", st = "mss-create-new-button", nt = "mss-options-list", te = "mss-option-item", ee = "mss-option-item-name", ie = "mss-option-item-detail", at = "mss-option-item-highlighted", O = "mss-hidden-select", se = "mss-no-items-text", rt = "mss-loading", k = 1, R = 10, ne = 250, ae = "mss-state-no-selection", re = "mss-state-has-selection", le = "mss-state-list-open";
+class gt extends HTMLElement {
constructor() {
super();
p(this, "_blurTimeout", null);
@@ -1384,15 +1384,15 @@ class ct extends HTMLElement {
}
_setupTemplates() {
this.optionTemplate = document.createElement("template"), this.optionTemplate.innerHTML = `
-
-
-
+
+
+
`, this.selectedItemTemplate = document.createElement("template"), this.selectedItemTemplate.innerHTML = `
-
-
-
-
+
+
+
+
`;
}
@@ -1446,7 +1446,7 @@ class ct extends HTMLElement {
this.setAttribute("name", t), this.hiddenSelect && (this.hiddenSelect.name = t);
}
connectedCallback() {
- if (this._render(), this.inputControlsContainer = this.querySelector(`.${X}`), this.inputWrapper = this.querySelector(`.${Y}`), this.inputElement = this.querySelector(`.${tt}`), this.createNewButton = this.querySelector(`.${et}`), this.optionsListElement = this.querySelector(`.${it}`), this.selectedItemsContainer = this.querySelector(`.${Q}`), this.hiddenSelect = this.querySelector(`.${O}`), this.placeholder = this.getAttribute("placeholder") || "Search items...", this.showCreateButton = this.getAttribute("show-create-button") !== "false", this._remoteEndpoint = this.getAttribute("data-endpoint") || null, this._remoteResultKey = this.getAttribute("data-result-key") || "items", this._remoteMinChars = this._parsePositiveInt(this.getAttribute("data-minchars"), k), this._remoteLimit = this._parsePositiveInt(this.getAttribute("data-limit"), R), 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(), this.hasAttribute("value")) {
+ if (this._render(), this.inputControlsContainer = this.querySelector(`.${Z}`), this.inputWrapper = this.querySelector(`.${tt}`), this.inputElement = this.querySelector(`.${it}`), this.createNewButton = this.querySelector(`.${st}`), this.optionsListElement = this.querySelector(`.${nt}`), this.selectedItemsContainer = this.querySelector(`.${X}`), this.hiddenSelect = this.querySelector(`.${O}`), this.placeholder = this.getAttribute("placeholder") || "Search items...", this.showCreateButton = this.getAttribute("show-create-button") !== "false", this._remoteEndpoint = this.getAttribute("data-endpoint") || null, this._remoteResultKey = this.getAttribute("data-result-key") || "items", this._remoteMinChars = this._parsePositiveInt(this.getAttribute("data-minchars"), k), this._remoteLimit = this._parsePositiveInt(this.getAttribute("data-limit"), R), 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(), this.hasAttribute("value")) {
const t = this.getAttribute("value");
try {
this.value = JSON.parse(t);
@@ -1498,10 +1498,10 @@ class ct extends HTMLElement {
this.internals_.setFormValue(null), this._synchronizeHiddenSelect();
}
disabledCallback(t) {
- this.inputElement && (this.inputElement.disabled = t), this.createNewButton && (this.createNewButton.disabled = t), this.toggleAttribute("disabled", t), this.querySelectorAll(`.${J}`).forEach((e) => e.disabled = t), this.hiddenSelect && (this.hiddenSelect.disabled = t), t && this._hideOptionsList();
+ this.inputElement && (this.inputElement.disabled = t), this.createNewButton && (this.createNewButton.disabled = t), this.toggleAttribute("disabled", t), this.querySelectorAll(`.${Y}`).forEach((e) => e.disabled = t), this.hiddenSelect && (this.hiddenSelect.disabled = t), t && this._hideOptionsList();
}
_updateRootElementStateClasses() {
- this.classList.toggle(Zt, this._value.length === 0), this.classList.toggle(te, this._value.length > 0), this.classList.toggle(ee, this._isOptionsListVisible);
+ this.classList.toggle(ae, this._value.length === 0), this.classList.toggle(re, this._value.length > 0), this.classList.toggle(le, this._isOptionsListVisible);
}
_render() {
const t = this.id || `mss-${crypto.randomUUID().slice(0, 8)}`;
@@ -1509,21 +1509,21 @@ class ct extends HTMLElement {
-
-
-
-
+
`;
@@ -1539,7 +1539,7 @@ class ct extends HTMLElement {
}), n;
}
_renderSelectedItems() {
- this.selectedItemsContainer && (this.selectedItemsContainer.innerHTML = "", this._value.length === 0 ? this.selectedItemsContainer.innerHTML = `
Keine Sprachen ausgewählt...` : this._value.forEach((t) => {
+ this.selectedItemsContainer && (this.selectedItemsContainer.innerHTML = "", this._value.length === 0 ? this.selectedItemsContainer.innerHTML = `
Keine Sprachen ausgewählt...` : this._value.forEach((t) => {
const e = this._createSelectedItemElement(t);
e && this.selectedItemsContainer.appendChild(e);
}), this._updateRootElementStateClasses());
@@ -1550,7 +1550,7 @@ class ct extends HTMLElement {
const o = this._normalizeText(t.additional_data);
l.textContent = o ? `(${o})` : "", n.dataset.id = t.id, n.setAttribute("aria-selected", String(e === this._highlightedIndex));
const h = `option-${this.id || "mss"}-${t.id}`;
- return n.id = h, e === this._highlightedIndex && (n.classList.add(st), this.inputElement && this.inputElement.setAttribute("aria-activedescendant", h)), n;
+ return n.id = h, e === this._highlightedIndex && (n.classList.add(at), this.inputElement && this.inputElement.setAttribute("aria-activedescendant", h)), n;
}
_renderOptionsList() {
if (!(!this.optionsListElement || !this.inputElement)) {
@@ -1561,7 +1561,7 @@ class ct extends HTMLElement {
const n = this._createOptionElement(e, s);
this.optionsListElement.appendChild(n);
});
- const t = this.optionsListElement.querySelector(`.${st}`);
+ const t = this.optionsListElement.querySelector(`.${at}`);
t && (t.scrollIntoView({ block: "nearest" }), this.inputElement.setAttribute("aria-activedescendant", t.id));
}
this._updateRootElementStateClasses();
@@ -1628,10 +1628,10 @@ class ct extends HTMLElement {
this._isOptionsListVisible = !1, this._highlightedIndex = -1, this.optionsListElement && this._renderOptionsList();
}
_handleFocus() {
- this.inputElement.disabled || (this.inputWrapper && this.inputWrapper.classList.add(Z), this.inputElement.value.length > 0 && this._handleInput({ target: this.inputElement }), this._updateRootElementStateClasses());
+ this.inputElement.disabled || (this.inputWrapper && this.inputWrapper.classList.add(et), this.inputElement.value.length > 0 && this._handleInput({ target: this.inputElement }), this._updateRootElementStateClasses());
}
_handleBlur() {
- this.inputWrapper && this.inputWrapper.classList.remove(Z), this._blurTimeout = setTimeout(() => {
+ this.inputWrapper && this.inputWrapper.classList.remove(et), this._blurTimeout = setTimeout(() => {
this.contains(document.activeElement) || this._hideOptionsList();
}, 150);
}
@@ -1660,14 +1660,14 @@ class ct extends HTMLElement {
}
this._remoteFetchTimeout = setTimeout(() => {
this._fetchRemoteOptions(t);
- }, Yt);
+ }, ne);
}
_cancelRemoteFetch() {
this._remoteFetchController && (this._remoteFetchController.abort(), this._remoteFetchController = null);
}
async _fetchRemoteOptions(t) {
if (!this._remoteEndpoint) return;
- this._cancelRemoteFetch(), this.classList.add(nt);
+ this._cancelRemoteFetch(), this.classList.add(rt);
const e = new AbortController();
this._remoteFetchController = e;
try {
@@ -1690,7 +1690,7 @@ class ct extends HTMLElement {
return;
console.error("MultiSelectSimple remote fetch error:", s), this._filteredOptions = [], this._isOptionsListVisible = !1, this._renderOptionsList();
} finally {
- this._remoteFetchController === e && (this._remoteFetchController = null), this.classList.remove(nt);
+ this._remoteFetchController === e && (this._remoteFetchController = null), this.classList.remove(rt);
}
}
_extractRemoteOptions(t) {
@@ -1724,9 +1724,9 @@ class ct extends HTMLElement {
return (s === '"' && n === '"' || s === "'" && n === "'") && (e = e.slice(1, -1).trim(), !e) ? "" : e;
}
}
-p(ct, "formAssociated", !0);
-const ie = "rbi-button", se = "rbi-icon";
-class ne extends HTMLElement {
+p(gt, "formAssociated", !0);
+const oe = "rbi-button", he = "rbi-icon";
+class de extends HTMLElement {
constructor() {
super(), this.initialStates = /* @__PURE__ */ new Map(), this._controlledElements = [], this.button = null, this.lastOverallModifiedState = null, this.handleInputChange = this.handleInputChange.bind(this), this.handleReset = this.handleReset.bind(this);
}
@@ -1735,10 +1735,10 @@ class ne extends HTMLElement {
}
connectedCallback() {
const i = `
-
-
-
-
Normdaten & Verknüpfungen
+
+
+
+ Normdaten & Verknüpfungen
+
+
+ Akteur hinzufügen
+
@@ -291,7 +296,7 @@ type AlmanachResult struct {
data-result-key="places"
data-minchars="1"
data-limit="15">
-
+
@@ -316,6 +321,238 @@ type AlmanachResult struct {
}
+
+
+ Personen & Körperschaften
+
+
+ {{- if $model.result.EntriesAgents -}}
+ {{- range $i, $r := $model.result.EntriesAgents -}}
+ {{- $a := index $model.result.Agents $r.Agent -}}
+
+
+
+ {{- if $a -}}
+
+ {{- $a.Name -}}
+
+ {{- if $a.BiographicalData -}}
+
{{- $a.BiographicalData -}}
+ {{- end -}}
+ {{- else -}}
+
Unbekannte Person
+ {{- end -}}
+
+
+
+
+
+
+
+
+
+
+ Entfernen
+
+
+
+
+
+
+
+ {{- end -}}
+ {{- else -}}
+
Keine Personen verknüpft.
+ {{- end -}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Bitte Akteur auswählen.
+
+
+
+
+
diff --git a/views/transform/main.js b/views/transform/main.js
index 2468c65..4c6dbcb 100644
--- a/views/transform/main.js
+++ b/views/transform/main.js
@@ -15,6 +15,7 @@ import { MultiSelectSimple } from "./multi-select-simple.js";
import { ResetButton } from "./reset-button.js";
import { DivManager } from "./div-menu.js";
import { ItemsEditor } from "./items-editor.js";
+import { SingleSelectRemote } from "./single-select-remote.js";
const FILTER_LIST_ELEMENT = "filter-list";
const SCROLL_BUTTON_ELEMENT = "scroll-button";
@@ -27,6 +28,7 @@ const FILTER_PILL_ELEMENT = "filter-pill";
const IMAGE_REEL_ELEMENT = "image-reel";
const MULTI_SELECT_ROLE_ELEMENT = "multi-select-places";
const MULTI_SELECT_SIMPLE_ELEMENT = "multi-select-simple";
+const SINGLE_SELECT_REMOTE_ELEMENT = "single-select-remote";
const RESET_BUTTON_ELEMENT = "reset-button";
const DIV_MANAGER_ELEMENT = "div-manager";
const ITEMS_EDITOR_ELEMENT = "items-editor";
@@ -42,6 +44,7 @@ customElements.define(FILTER_PILL_ELEMENT, FilterPill);
customElements.define(IMAGE_REEL_ELEMENT, ImageReel);
customElements.define(MULTI_SELECT_ROLE_ELEMENT, MultiSelectRole);
customElements.define(MULTI_SELECT_SIMPLE_ELEMENT, MultiSelectSimple);
+customElements.define(SINGLE_SELECT_REMOTE_ELEMENT, SingleSelectRemote);
customElements.define(RESET_BUTTON_ELEMENT, ResetButton);
customElements.define(DIV_MANAGER_ELEMENT, DivManager);
customElements.define(ITEMS_EDITOR_ELEMENT, ItemsEditor);
@@ -280,4 +283,4 @@ window.PathPlusQuery = PathPlusQuery;
window.HookupRBChange = HookupRBChange;
window.FormLoad = FormLoad;
-export { FilterList, ScrollButton, AbbreviationTooltips, MultiSelectSimple, MultiSelectRole, ToolTip, PopupImage, TabList, FilterPill, ImageReel, IntLink, ItemsEditor };
+export { FilterList, ScrollButton, AbbreviationTooltips, MultiSelectSimple, MultiSelectRole, ToolTip, PopupImage, TabList, FilterPill, ImageReel, IntLink, ItemsEditor, SingleSelectRemote };
diff --git a/views/transform/single-select-remote.js b/views/transform/single-select-remote.js
new file mode 100644
index 0000000..eba3b4a
--- /dev/null
+++ b/views/transform/single-select-remote.js
@@ -0,0 +1,286 @@
+const SSR_WRAPPER_CLASS = "ssr-wrapper";
+const SSR_INPUT_CLASS = "ssr-input";
+const SSR_LIST_CLASS = "ssr-list";
+const SSR_OPTION_CLASS = "ssr-option";
+const SSR_OPTION_NAME_CLASS = "ssr-option-name";
+const SSR_OPTION_DETAIL_CLASS = "ssr-option-detail";
+const SSR_OPTION_BIO_CLASS = "ssr-option-bio";
+const SSR_HIDDEN_INPUT_CLASS = "ssr-hidden-input";
+const SSR_CLEAR_BUTTON_CLASS = "ssr-clear-button";
+
+const SSR_DEFAULT_MIN_CHARS = 1;
+const SSR_DEFAULT_LIMIT = 10;
+const SSR_FETCH_DEBOUNCE_MS = 250;
+
+export class SingleSelectRemote extends HTMLElement {
+ constructor() {
+ super();
+ this._endpoint = "";
+ this._resultKey = "items";
+ this._minChars = SSR_DEFAULT_MIN_CHARS;
+ this._limit = SSR_DEFAULT_LIMIT;
+ this._placeholder = "Search...";
+ this._options = [];
+ this._selected = null;
+ this._fetchTimeout = null;
+ this._fetchController = null;
+ this._listVisible = false;
+ this._boundHandleInput = this._handleInput.bind(this);
+ this._boundHandleFocus = this._handleFocus.bind(this);
+ this._boundHandleKeyDown = this._handleKeyDown.bind(this);
+ this._boundHandleClear = this._handleClear.bind(this);
+ this._boundHandleClickOutside = this._handleClickOutside.bind(this);
+ }
+
+ static get observedAttributes() {
+ return ["data-endpoint", "data-result-key", "data-minchars", "data-limit", "placeholder", "name"];
+ }
+
+ connectedCallback() {
+ this._render();
+ this._input = this.querySelector(`.${SSR_INPUT_CLASS}`);
+ this._list = this.querySelector(`.${SSR_LIST_CLASS}`);
+ this._hiddenInput = this.querySelector(`.${SSR_HIDDEN_INPUT_CLASS}`);
+ this._clearButton = this.querySelector(`.${SSR_CLEAR_BUTTON_CLASS}`);
+
+ this._endpoint = this.getAttribute("data-endpoint") || "";
+ this._resultKey = this.getAttribute("data-result-key") || "items";
+ this._minChars = this._parsePositiveInt(this.getAttribute("data-minchars"), SSR_DEFAULT_MIN_CHARS);
+ this._limit = this._parsePositiveInt(this.getAttribute("data-limit"), SSR_DEFAULT_LIMIT);
+ this._placeholder = this.getAttribute("placeholder") || "Search...";
+
+ if (this._input) {
+ this._input.placeholder = this._placeholder;
+ this._input.addEventListener("input", this._boundHandleInput);
+ this._input.addEventListener("focus", this._boundHandleFocus);
+ this._input.addEventListener("keydown", this._boundHandleKeyDown);
+ }
+
+ if (this._clearButton) {
+ this._clearButton.addEventListener("click", this._boundHandleClear);
+ }
+
+ document.addEventListener("click", this._boundHandleClickOutside);
+ }
+
+ disconnectedCallback() {
+ document.removeEventListener("click", this._boundHandleClickOutside);
+ if (this._input) {
+ this._input.removeEventListener("input", this._boundHandleInput);
+ this._input.removeEventListener("focus", this._boundHandleFocus);
+ this._input.removeEventListener("keydown", this._boundHandleKeyDown);
+ }
+ if (this._clearButton) {
+ this._clearButton.removeEventListener("click", this._boundHandleClear);
+ }
+ }
+
+ attributeChangedCallback(name, oldValue, newValue) {
+ if (oldValue === newValue) {
+ return;
+ }
+ if (name === "data-endpoint") this._endpoint = newValue || "";
+ if (name === "data-result-key") this._resultKey = newValue || "items";
+ if (name === "data-minchars") this._minChars = this._parsePositiveInt(newValue, SSR_DEFAULT_MIN_CHARS);
+ if (name === "data-limit") this._limit = this._parsePositiveInt(newValue, SSR_DEFAULT_LIMIT);
+ if (name === "placeholder") {
+ this._placeholder = newValue || "Search...";
+ if (this._input) this._input.placeholder = this._placeholder;
+ }
+ if (name === "name" && this._hiddenInput) this._hiddenInput.name = newValue || "";
+ }
+
+ _handleInput(event) {
+ const value = event.target.value.trim();
+ this._selected = null;
+ this._syncHiddenInput();
+
+ if (value.length < this._minChars) {
+ this._options = [];
+ this._renderOptions();
+ this._hideList();
+ return;
+ }
+
+ this._debouncedFetch(value);
+ }
+
+ _handleFocus() {
+ if (this._options.length > 0) {
+ this._showList();
+ }
+ }
+
+ _handleKeyDown(event) {
+ if (event.key === "Escape") {
+ this._hideList();
+ }
+ }
+
+ _handleClear(event) {
+ event.preventDefault();
+ this._selected = null;
+ this._options = [];
+ if (this._input) this._input.value = "";
+ this._syncHiddenInput();
+ this._renderOptions();
+ this._hideList();
+ this.dispatchEvent(new CustomEvent("ssrchange", { bubbles: true, detail: { item: null } }));
+ }
+
+ _handleClickOutside(event) {
+ if (!this.contains(event.target)) {
+ this._hideList();
+ }
+ }
+
+ _debouncedFetch(query) {
+ if (this._fetchTimeout) {
+ clearTimeout(this._fetchTimeout);
+ }
+ this._fetchTimeout = setTimeout(() => {
+ this._fetchOptions(query);
+ }, SSR_FETCH_DEBOUNCE_MS);
+ }
+
+ async _fetchOptions(query) {
+ if (!this._endpoint) {
+ return;
+ }
+ if (this._fetchController) {
+ this._fetchController.abort();
+ }
+ this._fetchController = new AbortController();
+ const url = new URL(this._endpoint, window.location.origin);
+ url.searchParams.set("q", query);
+ if (this._limit > 0) {
+ url.searchParams.set("limit", String(this._limit));
+ }
+ try {
+ const resp = await fetch(url.toString(), { signal: this._fetchController.signal });
+ if (!resp.ok) {
+ return;
+ }
+ const data = await resp.json();
+ const items = Array.isArray(data?.[this._resultKey]) ? data[this._resultKey] : [];
+ this._options = items.filter((item) => item && item.id && item.name);
+ this._renderOptions();
+ if (this._options.length > 0) {
+ this._showList();
+ } else {
+ this._hideList();
+ }
+ } catch (err) {
+ if (err?.name === "AbortError") {
+ return;
+ }
+ }
+ }
+
+ _renderOptions() {
+ if (!this._list) {
+ return;
+ }
+ this._list.innerHTML = "";
+
+ this._options.forEach((item) => {
+ const option = document.createElement("button");
+ option.type = "button";
+ option.className = [
+ SSR_OPTION_CLASS,
+ "w-full text-left px-3 py-2 hover:bg-slate-100 transition-colors",
+ ].join(" ");
+
+ const nameEl = document.createElement("div");
+ nameEl.className = [SSR_OPTION_NAME_CLASS, "text-sm font-semibold text-gray-800"].join(" ");
+ nameEl.textContent = item.name;
+ option.appendChild(nameEl);
+
+ if (item.detail) {
+ const detailEl = document.createElement("div");
+ detailEl.className = [SSR_OPTION_DETAIL_CLASS, "text-xs text-gray-600"].join(" ");
+ detailEl.textContent = item.detail;
+ option.appendChild(detailEl);
+ }
+
+ if (item.bio) {
+ const bioEl = document.createElement("div");
+ bioEl.className = [SSR_OPTION_BIO_CLASS, "text-xs text-gray-500"].join(" ");
+ bioEl.textContent = item.bio;
+ option.appendChild(bioEl);
+ }
+
+ option.addEventListener("click", () => {
+ this._selectOption(item);
+ });
+
+ this._list.appendChild(option);
+ });
+ }
+
+ _selectOption(item) {
+ this._selected = item;
+ if (this._input) {
+ this._input.value = item.name || "";
+ }
+ this._syncHiddenInput();
+ this._hideList();
+ this.dispatchEvent(new CustomEvent("ssrchange", { bubbles: true, detail: { item } }));
+ this.dispatchEvent(new Event("change", { bubbles: true }));
+ }
+
+ _syncHiddenInput() {
+ if (!this._hiddenInput) {
+ return;
+ }
+ this._hiddenInput.value = this._selected?.id || "";
+ }
+
+ _showList() {
+ if (!this._list || this._listVisible) {
+ return;
+ }
+ this._list.classList.remove("hidden");
+ this._listVisible = true;
+ }
+
+ _hideList() {
+ if (!this._list || !this._listVisible) {
+ return;
+ }
+ this._list.classList.add("hidden");
+ this._listVisible = false;
+ }
+
+ _parsePositiveInt(value, fallback) {
+ const parsed = parseInt(value || "", 10);
+ if (Number.isNaN(parsed) || parsed <= 0) {
+ return fallback;
+ }
+ return parsed;
+ }
+
+ _render() {
+ const inputName = this.getAttribute("name") || "";
+ this.innerHTML = `
+
+ `;
+ }
+}