+Reihen relations, small UX stuff

This commit is contained in:
Simon Martens
2026-01-08 14:36:18 +01:00
parent 53eab6a779
commit 1656f60ac4
12 changed files with 1732 additions and 643 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -17,9 +17,7 @@ type AlmanachResult struct {
}
-->
<script type="module">
FormLoad(document.getElementById("changealmanachform"));
</script>
<almanach-edit-page>
<div class="flex container-normal bg-slate-100 mx-auto px-8">
<div class="flex flex-row w-full justify-between">
@@ -279,11 +277,276 @@ type AlmanachResult struct {
<i class="ri-links-line"></i>
<span>Normdaten &amp; Verknüpfungen</span>
</div>
<button type="button" id="agents-add-toggle" class="text-gray-700 hover:text-gray-900">
<i class="ri-add-line"></i> Akteur hinzufügen
</button>
<div class="flex items-center gap-3">
<button type="button" id="series-add-toggle" class="text-gray-700 hover:text-gray-900">
<i class="ri-add-line"></i> Reihe hinzufügen
</button>
<button type="button" id="agents-add-toggle" class="text-gray-700 hover:text-gray-900">
<i class="ri-add-line"></i> Akteur hinzufügen
</button>
</div>
</div>
<hr class="border-slate-400 mt-2 mb-3" />
<div class="mt-3">
<relations-editor data-prefix="entries_series" data-link-base="/reihe/" data-new-label="(Neu)" data-add-toggle-id="series-add-toggle">
<div class="inputwrapper">
<label class="inputlabel" for="series-section">Reihen</label>
<div id="series-section" class="flex flex-col gap-2 mt-2">
{{- if $model.result.Series -}}
{{- range $i, $s := $model.result.Series -}}
{{- $rel := index $model.result.EntriesSeries $s.Id -}}
{{- if $rel -}}
<div data-rel-row class="entries-series-row border border-stone-200 rounded-xs bg-stone-50 px-3 py-2">
<div class="grid grid-cols-[1fr_14rem_8rem_7rem] gap-3 items-center">
<div data-rel-strike class="relation-strike flex flex-col min-w-0">
<a data-rel-link href="/reihe/{{ $s.MusenalmID }}" class="text-base font-bold text-gray-800 no-underline hover:text-slate-900 truncate">
<span data-rel-name>{{- $s.Title -}}</span>
</a>
{{- if $s.Pseudonyms -}}
<div data-rel-detail-container class="text-xs text-gray-600 truncate"><span data-rel-detail>{{- $s.Pseudonyms -}}</span></div>
{{- end -}}
</div>
<div data-rel-strike class="relation-strike">
<select name="entries_series_type[{{ $rel.Id }}]" id="entries_series_type_{{ $rel.Id }}" autocomplete="off" class="inputselect font-bold w-full">
{{- range $t := $model.series_relations -}}
<option value="{{- $t -}}" {{ if eq $rel.Type $t }}selected{{ end }}>{{- $t -}}</option>
{{- end -}}
</select>
</div>
<div data-rel-strike class="relation-strike flex items-center gap-2">
<input
type="checkbox"
name="entries_series_uncertain[{{ $rel.Id }}]"
id="entries_series_uncertain_{{ $rel.Id }}"
{{ if $rel.Uncertain }}checked{{ end }} />
<label for="entries_series_uncertain_{{ $rel.Id }}" class="text-sm text-gray-700">Unsicher</label>
</div>
<div class="flex justify-end">
<button
type="button"
class="text-sm text-red-700 hover:text-red-900"
data-delete-toggle="entries_series_delete_{{ $rel.Id }}">
<i class="ri-delete-bin-line mr-1"></i>
<span class="no-underline" data-delete-label data-delete-default="Entfernen" data-delete-active="Wird entfernt" data-delete-hover="Rückgängig">Entfernen</span>
</button>
<input type="checkbox" class="hidden" name="entries_series_delete[{{ $rel.Id }}]" id="entries_series_delete_{{ $rel.Id }}" />
</div>
</div>
<input type="hidden" name="entries_series_id[{{ $rel.Id }}]" value="{{ $rel.Id }}" />
<input type="hidden" name="entries_series_series[{{ $rel.Id }}]" value="{{ $rel.Series }}" />
</div>
{{- end -}}
{{- end -}}
{{- else -}}
<div class="text-sm text-gray-600">Keine Reihen verknüpft.</div>
{{- end -}}
</div>
<div data-role="relation-add-row" class="mt-2"></div>
<div data-role="relation-add-panel" class="mt-2 hidden">
<div class="border border-stone-200 rounded-xs bg-stone-50 px-3 py-2">
<div class="grid grid-cols-[1fr_14rem_8rem_7rem] gap-3 items-center">
<div class="min-w-0">
<label for="series-add-select" class="sr-only">Reihe suchen</label>
<single-select-remote
id="series-add-select"
data-role="relation-add-select"
name="entries_series_new_id"
placeholder="Reihe suchen..."
data-endpoint="/api/series/search"
data-result-key="series"
data-minchars="1"
data-limit="15">
</single-select-remote>
</div>
<div>
<label for="entries_series_new_type" class="sr-only">Beziehung</label>
<select data-role="relation-type-select" name="entries_series_new_type" id="entries_series_new_type" autocomplete="off" class="inputselect font-bold w-full">
{{- range $t := $model.series_relations -}}
<option value="{{- $t -}}">{{- $t -}}</option>
{{- end -}}
</select>
</div>
<div class="flex items-center gap-2">
<input data-role="relation-uncertain" type="checkbox" name="entries_series_new_uncertain" id="entries_series_new_uncertain" />
<label for="entries_series_new_uncertain" class="text-sm text-gray-700">Unsicher</label>
</div>
<div class="flex justify-end">
<div class="flex items-center gap-3 text-lg">
<button type="button" data-role="relation-add-apply" class="text-gray-700 hover:text-gray-900" id="series-add-apply" aria-label="Reihe hinzufügen">
<i class="ri-check-line"></i>
</button>
<button type="button" data-role="relation-add-close" class="text-gray-700 hover:text-gray-900" id="series-add-close" aria-label="Ausblenden">
<i class="ri-close-line"></i>
</button>
</div>
</div>
</div>
<div data-role="relation-add-error" class="text-xs text-red-700 mt-2 hidden">Bitte Reihe auswählen.</div>
</div>
</div>
<template data-role="relation-new-template">
<div data-rel-row class="border border-stone-200 rounded-xs bg-stone-50 px-3 py-2">
<div class="grid grid-cols-[1fr_14rem_8rem_7rem] gap-3 items-center">
<div data-rel-strike class="relation-strike flex flex-col min-w-0">
<div class="text-base font-bold text-gray-800 truncate">
<a data-rel-link class="no-underline hover:text-slate-900">
<span data-rel-name></span>
</a>
<em data-rel-new class="text-sm text-gray-600 ml-1"></em>
</div>
<div data-rel-detail-container class="text-xs text-gray-600 truncate"><span data-rel-detail></span></div>
</div>
<div data-rel-strike class="relation-strike">
<select data-rel-input="type" class="inputselect font-bold w-full"></select>
</div>
<div data-rel-strike class="relation-strike flex items-center gap-2">
<input data-rel-input="uncertain" type="checkbox" />
<label data-rel-uncertain-label class="text-sm text-gray-700">Unsicher</label>
</div>
<div class="flex justify-end">
<button type="button" class="text-sm text-red-700 hover:text-red-900" data-role="relation-new-delete">
<i class="ri-delete-bin-line mr-1"></i> Entfernen
</button>
</div>
</div>
<input type="hidden" data-rel-input="id" />
</div>
</template>
</div>
</relations-editor>
</div>
<div class="mt-3">
<relations-editor data-prefix="entries_agents" data-link-base="/person/" data-new-label="(Neu)" data-add-toggle-id="agents-add-toggle">
<div class="inputwrapper">
<label class="inputlabel" for="agents-section">Personen &amp; Körperschaften</label>
<div id="agents-section" class="flex flex-col gap-2 mt-2">
{{- if $model.result.EntriesAgents -}}
{{- range $i, $r := $model.result.EntriesAgents -}}
{{- $a := index $model.result.Agents $r.Agent -}}
<div data-rel-row class="entries-agent-row border border-stone-200 rounded-xs bg-stone-50 px-3 py-2">
<div class="grid grid-cols-[1fr_14rem_8rem_7rem] gap-3 items-center">
<div data-rel-strike class="relation-strike flex flex-col min-w-0">
{{- if $a -}}
<a data-rel-link href="/person/{{ $a.Id }}" class="text-base font-bold text-gray-800 no-underline hover:text-slate-900 truncate">
<span data-rel-name>{{- $a.Name -}}</span>
</a>
{{- if $a.BiographicalData -}}
<div data-rel-detail-container class="text-xs text-gray-600 truncate"><span data-rel-detail>{{- $a.BiographicalData -}}</span></div>
{{- end -}}
{{- else -}}
<div class="text-base font-bold text-gray-800">Unbekannte Person</div>
{{- end -}}
</div>
<div data-rel-strike class="relation-strike">
<select name="entries_agents_type[{{ $r.Id }}]" id="entries_agents_type_{{ $r.Id }}" autocomplete="off" class="inputselect font-bold w-full">
{{- range $t := $model.agent_relations -}}
<option value="{{- $t -}}" {{ if eq $r.Type $t }}selected{{ end }}>{{- $t -}}</option>
{{- end -}}
</select>
</div>
<div data-rel-strike class="relation-strike flex items-center gap-2">
<input
type="checkbox"
name="entries_agents_uncertain[{{ $r.Id }}]"
id="entries_agents_uncertain_{{ $r.Id }}"
{{ if $r.Uncertain }}checked{{ end }} />
<label for="entries_agents_uncertain_{{ $r.Id }}" class="text-sm text-gray-700">Unsicher</label>
</div>
<div class="flex justify-end">
<button
type="button"
class="text-sm text-red-700 hover:text-red-900"
data-delete-toggle="entries_agents_delete_{{ $r.Id }}">
<i class="ri-delete-bin-line mr-1"></i>
<span class="no-underline" data-delete-label data-delete-default="Entfernen" data-delete-active="Wird entfernt" data-delete-hover="Rückgängig">Entfernen</span>
</button>
<input type="checkbox" class="hidden" name="entries_agents_delete[{{ $r.Id }}]" id="entries_agents_delete_{{ $r.Id }}" />
</div>
</div>
<input type="hidden" name="entries_agents_id[{{ $r.Id }}]" value="{{ $r.Id }}" />
<input type="hidden" name="entries_agents_agent[{{ $r.Id }}]" value="{{ $r.Agent }}" />
</div>
{{- end -}}
{{- else -}}
<div class="text-sm text-gray-600">Keine Personen verknüpft.</div>
{{- end -}}
</div>
<div data-role="relation-add-row" class="mt-2"></div>
<div data-role="relation-add-panel" class="mt-2 hidden">
<div class="border border-stone-200 rounded-xs bg-stone-50 px-3 py-2">
<div class="grid grid-cols-[1fr_14rem_8rem_7rem] gap-3 items-center">
<div class="min-w-0">
<label for="agents-add-select" class="sr-only">Akteur suchen</label>
<single-select-remote
id="agents-add-select"
data-role="relation-add-select"
name="entries_agents_new_id"
placeholder="Akteur suchen..."
data-endpoint="/api/agents/search"
data-result-key="agents"
data-minchars="1"
data-limit="15">
</single-select-remote>
</div>
<div>
<label for="entries_agents_new_type" class="sr-only">Beziehung</label>
<select data-role="relation-type-select" name="entries_agents_new_type" id="entries_agents_new_type" autocomplete="off" class="inputselect font-bold w-full">
{{- range $t := $model.agent_relations -}}
<option value="{{- $t -}}">{{- $t -}}</option>
{{- end -}}
</select>
</div>
<div class="flex items-center gap-2">
<input data-role="relation-uncertain" type="checkbox" name="entries_agents_new_uncertain" id="entries_agents_new_uncertain" />
<label for="entries_agents_new_uncertain" class="text-sm text-gray-700">Unsicher</label>
</div>
<div class="flex justify-end">
<div class="flex items-center gap-3 text-lg">
<button type="button" data-role="relation-add-apply" class="text-gray-700 hover:text-gray-900" id="agents-add-apply" aria-label="Akteur hinzufügen">
<i class="ri-check-line"></i>
</button>
<button type="button" data-role="relation-add-close" class="text-gray-700 hover:text-gray-900" id="agents-add-close" aria-label="Ausblenden">
<i class="ri-close-line"></i>
</button>
</div>
</div>
</div>
<div data-role="relation-add-error" class="text-xs text-red-700 mt-2 hidden">Bitte Akteur auswählen.</div>
</div>
</div>
<template data-role="relation-new-template">
<div data-rel-row class="border border-stone-200 rounded-xs bg-stone-50 px-3 py-2">
<div class="grid grid-cols-[1fr_14rem_8rem_7rem] gap-3 items-center">
<div data-rel-strike class="relation-strike flex flex-col min-w-0">
<div class="text-base font-bold text-gray-800 truncate">
<a data-rel-link class="no-underline hover:text-slate-900">
<span data-rel-name></span>
</a>
<em data-rel-new class="text-sm text-gray-600 ml-1"></em>
</div>
<div data-rel-detail-container class="text-xs text-gray-600 truncate"><span data-rel-detail></span></div>
</div>
<div data-rel-strike class="relation-strike">
<select data-rel-input="type" class="inputselect font-bold w-full"></select>
</div>
<div data-rel-strike class="relation-strike flex items-center gap-2">
<input data-rel-input="uncertain" type="checkbox" />
<label data-rel-uncertain-label class="text-sm text-gray-700">Unsicher</label>
</div>
<div class="flex justify-end">
<button type="button" class="text-sm text-red-700 hover:text-red-900" data-role="relation-new-delete">
<i class="ri-delete-bin-line mr-1"></i> Entfernen
</button>
</div>
</div>
<input type="hidden" data-rel-input="id" />
</div>
</template>
</div>
</relations-editor>
</div>
<div class="flex flex-col gap-4 mt-4">
<div class="inputwrapper">
<label for="places" class="inputlabel">Erscheinungs- und Verlagsorte</label>
@@ -291,268 +554,20 @@ type AlmanachResult struct {
id="places"
name="places[]"
placeholder="Orte suchen..."
data-toggle-label='<i class="ri-add-circle-line"></i>'
data-empty-text="Keine Orte ausgewählt..."
show-create-button="false"
data-endpoint="/api/places/search"
data-result-key="places"
data-minchars="1"
data-limit="15">
data-limit="15"
data-initial-options='[{{- range $i, $place := $model.result.Places -}}{{- if $i }},{{ end -}}{"id":"{{ $place.Id }}","name":{{ printf "%q" $place.Name }},"additional_data":{{ printf "%q" $place.Pseudonyms }}}}{{- end -}}]'
data-initial-values='[{{- range $i, $place := $model.result.Places -}}{{- if $i }},{{ end -}}"{{ $place.Id }}"{{- end -}}]'>
</multi-select-simple>
</div>
</div>
</div>
<script type="module">
const placesSelect = document.getElementById("places");
if (placesSelect) {
const initialPlaces = [
{{- range $i, $place := $model.result.Places }}
{ id: "{{ $place.Id }}", name: {{ printf "%q" $place.Name }}, additional_data: {{ printf "%q" $place.Pseudonyms }} },
{{- end -}}
];
const initialPlaceIds = [
{{- range $i, $place := $model.result.Places -}}
{{- if $i }},{{ end }}"{{ $place.Id }}"
{{- end -}}
];
if (initialPlaces.length > 0) {
placesSelect.setOptions(initialPlaces);
}
placesSelect.value = initialPlaceIds;
}
</script>
<div class="mt-3">
<div class="text-base font-semibold text-gray-700">
<span>Personen &amp; Körperschaften</span>
</div>
<div class="flex flex-col gap-2 mt-4">
{{- if $model.result.EntriesAgents -}}
{{- range $i, $r := $model.result.EntriesAgents -}}
{{- $a := index $model.result.Agents $r.Agent -}}
<div class="border border-stone-200 rounded-xs bg-stone-50 px-3 py-2">
<div class="grid grid-cols-[1fr_14rem_8rem_7rem] gap-3 items-center">
<div class="flex flex-col min-w-0">
{{- if $a -}}
<a href="/person/{{ $a.Id }}" class="text-base font-bold text-gray-800 no-underline hover:text-slate-900 truncate">
{{- $a.Name -}}
</a>
{{- if $a.BiographicalData -}}
<div class="text-xs text-gray-600 truncate">{{- $a.BiographicalData -}}</div>
{{- end -}}
{{- else -}}
<div class="text-base font-bold text-gray-800">Unbekannte Person</div>
{{- end -}}
</div>
<div>
<select name="entries_agents_type[{{ $r.Id }}]" id="entries_agents_type_{{ $r.Id }}" autocomplete="off" class="inputselect font-bold w-full">
{{- range $t := $model.agent_relations -}}
<option value="{{- $t -}}" {{ if eq $r.Type $t }}selected{{ end }}>{{- $t -}}</option>
{{- end -}}
</select>
</div>
<div class="flex items-center gap-2">
<input
type="checkbox"
name="entries_agents_uncertain[{{ $r.Id }}]"
id="entries_agents_uncertain_{{ $r.Id }}"
{{ if $r.Uncertain }}checked{{ end }} />
<label for="entries_agents_uncertain_{{ $r.Id }}" class="text-sm text-gray-700">Unsicher</label>
</div>
<div class="flex justify-end">
<button
type="button"
class="text-sm text-red-700 hover:text-red-900"
data-delete-toggle="entries_agents_delete_{{ $r.Id }}">
<i class="ri-delete-bin-line mr-1"></i> Entfernen
</button>
<input type="checkbox" class="hidden" name="entries_agents_delete[{{ $r.Id }}]" id="entries_agents_delete_{{ $r.Id }}" />
</div>
</div>
<input type="hidden" name="entries_agents_id[{{ $r.Id }}]" value="{{ $r.Id }}" />
<input type="hidden" name="entries_agents_agent[{{ $r.Id }}]" value="{{ $r.Agent }}" />
</div>
{{- end -}}
{{- else -}}
<div class="text-sm text-gray-600">Keine Personen verknüpft.</div>
{{- end -}}
</div>
<div id="agents-add-row" class="mt-2"></div>
<div id="agents-add-panel" class="mt-2 hidden">
<div class="border border-stone-200 rounded-xs bg-stone-50 px-3 py-2">
<div class="grid grid-cols-[1fr_14rem_8rem_7rem] gap-3 items-center">
<div class="min-w-0">
<label for="agents-add-select" class="sr-only">Akteur suchen</label>
<single-select-remote
id="agents-add-select"
name="entries_agents_new_id"
placeholder="Akteur suchen..."
data-endpoint="/api/agents/search"
data-result-key="agents"
data-minchars="1"
data-limit="15">
</single-select-remote>
</div>
<div>
<label for="entries_agents_new_type" class="sr-only">Beziehung</label>
<select name="entries_agents_new_type" id="entries_agents_new_type" autocomplete="off" class="inputselect font-bold w-full">
{{- range $t := $model.agent_relations -}}
<option value="{{- $t -}}">{{- $t -}}</option>
{{- end -}}
</select>
</div>
<div class="flex items-center gap-2">
<input type="checkbox" name="entries_agents_new_uncertain" id="entries_agents_new_uncertain" />
<label for="entries_agents_new_uncertain" class="text-sm text-gray-700">Unsicher</label>
</div>
<div class="flex justify-end">
<div class="flex items-center gap-3 text-lg">
<button type="button" class="text-gray-700 hover:text-gray-900" id="agents-add-apply" aria-label="Akteur hinzufügen">
<i class="ri-check-line"></i>
</button>
<button type="button" class="text-gray-700 hover:text-gray-900" id="agents-add-close" aria-label="Ausblenden">
<i class="ri-close-line"></i>
</button>
</div>
</div>
</div>
<div id="agents-add-error" class="text-xs text-red-700 mt-2 hidden">Bitte Akteur auswählen.</div>
</div>
</div>
<script type="module">
const agentsAddToggle = document.getElementById("agents-add-toggle");
const agentsAddPanel = document.getElementById("agents-add-panel");
const agentsAddClose = document.getElementById("agents-add-close");
const agentsAddApply = document.getElementById("agents-add-apply");
const agentsAddError = document.getElementById("agents-add-error");
const agentsAddRow = document.getElementById("agents-add-row");
const agentsAddSelect = document.getElementById("agents-add-select");
const relationSelect = document.getElementById("entries_agents_new_type");
const uncertainCheckbox = document.getElementById("entries_agents_new_uncertain");
let pendingAgent = null;
if (agentsAddToggle && agentsAddPanel) {
agentsAddToggle.addEventListener("click", () => {
agentsAddPanel.classList.toggle("hidden");
});
}
if (agentsAddClose && agentsAddPanel) {
agentsAddClose.addEventListener("click", () => {
agentsAddPanel.classList.add("hidden");
});
}
const clearAddPanel = () => {
if (agentsAddSelect) {
const clearButton = agentsAddSelect.querySelector(".ssr-clear-button");
if (clearButton) {
clearButton.click();
}
}
if (relationSelect) {
relationSelect.selectedIndex = 0;
}
if (uncertainCheckbox) {
uncertainCheckbox.checked = false;
}
};
const removeNewRow = () => {
if (agentsAddRow) {
agentsAddRow.innerHTML = "";
}
pendingAgent = null;
clearAddPanel();
};
if (agentsAddApply && agentsAddPanel) {
agentsAddApply.addEventListener("click", () => {
const idInput = agentsAddPanel.querySelector("input[name='entries_agents_new_id']");
const hasSelection = idInput && idInput.value.trim().length > 0;
if (!hasSelection) {
if (agentsAddError) {
agentsAddError.classList.remove("hidden");
}
return;
}
if (agentsAddError) {
agentsAddError.classList.add("hidden");
}
if (!pendingAgent || !agentsAddRow) {
return;
}
const row = document.createElement("div");
row.className = "border border-stone-200 rounded-xs bg-stone-50 px-3 py-2";
row.innerHTML = `
<div class="grid grid-cols-[1fr_14rem_8rem_7rem] gap-3 items-center">
<div class="flex flex-col min-w-0">
<div class="text-base font-bold text-gray-800 truncate">
<a href="/person/${pendingAgent.id}" class="no-underline hover:text-slate-900">
${pendingAgent.name || ""}
</a>
<em class="text-sm text-gray-600 ml-1">(Neu)</em>
</div>
${pendingAgent.bio ? `<div class="text-xs text-gray-600 truncate">${pendingAgent.bio}</div>` : ""}
</div>
<div>
<select name="entries_agents_new_type" class="inputselect font-bold w-full"></select>
</div>
<div class="flex items-center gap-2">
<input type="checkbox" name="entries_agents_new_uncertain" id="entries_agents_new_uncertain_row" />
<label for="entries_agents_new_uncertain_row" class="text-sm text-gray-700">Unsicher</label>
</div>
<div class="flex justify-end">
<button type="button" class="text-sm text-red-700 hover:text-red-900" data-new-delete>
<i class="ri-delete-bin-line mr-1"></i> Entfernen
</button>
</div>
</div>
<input type="hidden" name="entries_agents_new_id" value="${pendingAgent.id}" />
`;
const rowSelect = row.querySelector("select[name='entries_agents_new_type']");
if (rowSelect && relationSelect) {
rowSelect.innerHTML = relationSelect.innerHTML;
rowSelect.value = relationSelect.value;
}
const rowUncertain = row.querySelector("input[name='entries_agents_new_uncertain']");
if (rowUncertain && uncertainCheckbox) {
rowUncertain.checked = uncertainCheckbox.checked;
}
const deleteButton = row.querySelector("[data-new-delete]");
if (deleteButton) {
deleteButton.addEventListener("click", () => {
removeNewRow();
if (agentsAddPanel) {
agentsAddPanel.classList.remove("hidden");
}
});
}
agentsAddRow.innerHTML = "";
agentsAddRow.appendChild(row);
agentsAddPanel.classList.add("hidden");
clearAddPanel();
});
}
if (agentsAddSelect) {
agentsAddSelect.addEventListener("ssrchange", (event) => {
pendingAgent = event.detail?.item || null;
if (pendingAgent && agentsAddError) {
agentsAddError.classList.add("hidden");
}
});
}
document.querySelectorAll("[data-delete-toggle]").forEach((button) => {
button.addEventListener("click", () => {
const targetId = button.getAttribute("data-delete-toggle");
const checkbox = document.getElementById(targetId);
if (!checkbox) {
return;
}
checkbox.checked = !checkbox.checked;
button.classList.toggle("opacity-60", checkbox.checked);
});
});
</script>
</div>
</div>
<!-- End Left Column -->
@@ -638,11 +653,13 @@ type AlmanachResult struct {
<!-- Languages -->
<div class="inputwrapper">
<label for="languages" class="inputlabel">Sprachen</label>
<multi-select-simple id="languages" show-create-button="false" placeholder="Sprachen suchen..."></multi-select-simple>
<script type="module">
const smlang = document.getElementById("languages");
smlang.value = {{ $model.result.Entry.Language }};
</script>
<multi-select-simple
id="languages"
show-create-button="false"
placeholder="Sprachen suchen..."
data-toggle-label='<i class="ri-add-circle-line"></i>'
data-empty-text="Keine Sprachen ausgewählt..."
value='[{{- range $i, $lang := $model.result.Entry.Language -}}{{- if $i }},{{ end -}}"{{ $lang }}"{{- end -}}]'></multi-select-simple>
</div>
<!-- Nachweise - Always visible -->
@@ -675,11 +692,12 @@ type AlmanachResult struct {
<input type="hidden" name="items_id[]" value="{{ $item.Id }}" />
<div class="items-summary flex flex-col gap-2">
<div class="flex items-center justify-between bg-stone-100 px-3 py-2 rounded-xs">
<div class="text-base font-bold" data-summary-container>
<span data-summary-field="owner" data-summary-hide-empty="true">{{ $item.Owner }}</span>
<div class="text-base font-bold" data-summary-container>
<span data-items-strike data-summary-field="owner" data-summary-hide-empty="true">{{ $item.Owner }}</span>
<span data-new-badge class="items-new-badge hidden text-sm text-gray-600 ml-1">(Neu)</span>
</div>
<div class="px-2 py-0.5 bg-stone-200 text-sm font-bold rounded-sm" data-summary-container>
<span data-summary-field="identifier" data-summary-hide-empty="true">{{ $item.Identifier }}</span>
<span data-items-strike data-summary-field="identifier" data-summary-hide-empty="true">{{ $item.Identifier }}</span>
</div>
</div>
<div class="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-4 px-3
@@ -688,23 +706,23 @@ type AlmanachResult struct {
<div class="flex flex-col gap-1 text-base">
<div class="grid grid-cols-[6rem_1fr] gap-x-4 items-baseline" data-summary-container>
<span class="text-xs uppercase tracking-wide text-gray-500">Standort</span>
<span class="items-summary-value" data-summary-field="location" data-summary-hide-empty="true">{{ $item.Location }}</span>
<span class="items-summary-value" data-items-strike data-summary-field="location" data-summary-hide-empty="true">{{ $item.Location }}</span>
</div>
<div class="grid grid-cols-[6rem_1fr] gap-x-4 items-baseline" data-summary-container>
<span class="text-xs uppercase tracking-wide text-gray-500">Vorhanden als</span>
<span class="items-summary-value" data-summary-field="media" data-summary-hide-empty="true">
<span class="items-summary-value" data-items-strike data-summary-field="media" data-summary-hide-empty="true">
{{- range $j, $m := $item.Media -}}{{- if $j -}}, {{- end -}}{{- $m -}}{{- end -}}
</span>
</div>
<div class="grid grid-cols-[6rem_1fr] gap-x-4 items-baseline" data-summary-container>
<span class="text-xs uppercase tracking-wide text-gray-500">URL</span>
<span class="items-summary-value" data-summary-field="uri" data-summary-hide-empty="true">
<span class="items-summary-value" data-items-strike data-summary-field="uri" data-summary-hide-empty="true">
<a class="no-underline" data-summary-link href="{{ $item.Uri }}" target="_blank" rel="noopener">{{ $item.Uri }}</a>
</span>
</div>
<div class="grid grid-cols-[6rem_1fr] gap-x-4 items-baseline" data-summary-container>
<span class="text-xs uppercase tracking-wide text-gray-500">Annotationen</span>
<span class="items-summary-value" data-summary-field="annotation" data-summary-hide-empty="true">{{ $item.Annotation }}</span>
<span class="items-summary-value" data-items-strike data-summary-field="annotation" data-summary-hide-empty="true">{{ $item.Annotation }}</span>
</div>
</div>
</div>
@@ -716,6 +734,7 @@ type AlmanachResult struct {
</button>
<button type="button" class="items-remove-button text-red-700 hover:text-red-900" aria-label="Entfernen">
<i class="ri-delete-bin-line"></i>
<span class="ml-1 text-sm no-underline" data-delete-label data-delete-default="Entfernen" data-delete-active="Wird entfernt" data-delete-hover="Rückgängig">Entfernen</span>
</button>
</div>
</div>
@@ -725,19 +744,19 @@ type AlmanachResult struct {
<div class="flex flex-col gap-3 mt-3">
<div class="inputwrapper">
<label class="inputlabel" data-field-label="owner">Besitzer</label>
<input class="inputinput" data-field="owner" name="items_owner[]" autocomplete="off" value="{{ $item.Owner }}" />
<input class="inputinput" data-items-strike data-field="owner" name="items_owner[]" autocomplete="off" value="{{ $item.Owner }}" />
</div>
<div class="inputwrapper">
<label class="inputlabel" data-field-label="identifier">Signatur</label>
<input class="inputinput" data-field="identifier" name="items_identifier[]" autocomplete="off" value="{{ $item.Identifier }}" />
<input class="inputinput" data-items-strike data-field="identifier" name="items_identifier[]" autocomplete="off" value="{{ $item.Identifier }}" />
</div>
<div class="inputwrapper">
<label class="inputlabel" data-field-label="location">Standort</label>
<input class="inputinput" data-field="location" name="items_location[]" autocomplete="off" value="{{ $item.Location }}" />
<input class="inputinput" data-items-strike data-field="location" name="items_location[]" autocomplete="off" value="{{ $item.Location }}" />
</div>
<div class="inputwrapper">
<label class="inputlabel" data-field-label="media">Vorhanden als</label>
<select class="inputselect" data-field="media" name="items_media[]" autocomplete="off">
<select class="inputselect" data-items-strike data-field="media" name="items_media[]" autocomplete="off">
<option value=""></option>
{{- range $t := $model.item_types -}}
<option value="{{- $t -}}" {{ if Contains $item.Media $t }}selected{{ end }}>{{- $t -}}</option>
@@ -746,11 +765,11 @@ type AlmanachResult struct {
</div>
<div class="inputwrapper">
<label class="inputlabel" data-field-label="annotation">Annotationen</label>
<textarea class="inputtextarea min-h-[8rem]" data-field="annotation" name="items_annotation[]" autocomplete="off">{{- $item.Annotation -}}</textarea>
<textarea class="inputtextarea min-h-[8rem]" data-items-strike data-field="annotation" name="items_annotation[]" autocomplete="off">{{- $item.Annotation -}}</textarea>
</div>
<div class="inputwrapper">
<label class="inputlabel" data-field-label="uri">URI</label>
<input class="inputinput" data-field="uri" name="items_uri[]" autocomplete="off" value="{{ $item.Uri }}" />
<input class="inputinput" data-items-strike data-field="uri" name="items_uri[]" autocomplete="off" value="{{ $item.Uri }}" />
</div>
</div>
<div class="flex justify-end gap-3 mt-3 px-3 py-2">
@@ -761,7 +780,8 @@ type AlmanachResult struct {
<i class="ri-close-line mr-2"></i> Abbrechen
</button>
<button type="button" class="items-remove-button resetbutton w-auto px-2 py-1 text-base text-red-700 hover:text-red-900">
<i class="ri-delete-bin-line mr-2"></i> Entfernen
<i class="ri-delete-bin-line mr-2"></i>
<span class="no-underline" data-delete-label data-delete-default="Entfernen" data-delete-active="Wird entfernt" data-delete-hover="Rückgängig">Entfernen</span>
</button>
</div>
</div>
@@ -775,10 +795,11 @@ type AlmanachResult struct {
<div class="items-summary hidden flex flex-col gap-2">
<div class="flex items-center justify-between bg-stone-100 px-3 py-2 rounded-xs">
<div class="text-base font-bold" data-summary-container>
<span data-summary-field="owner" data-summary-hide-empty="true">—</span>
<span data-items-strike data-summary-field="owner" data-summary-hide-empty="true">—</span>
<span data-new-badge class="items-new-badge hidden text-sm text-gray-600 ml-1">(Neu)</span>
</div>
<div class="px-2 py-0.5 bg-stone-200 text-sm font-bold rounded-sm" data-summary-container>
<span data-summary-field="identifier" data-summary-hide-empty="true">—</span>
<span data-items-strike data-summary-field="identifier" data-summary-hide-empty="true">—</span>
</div>
</div>
<div class="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-4 px-3 py-1">
@@ -786,21 +807,21 @@ type AlmanachResult struct {
<div class="flex flex-col gap-1 text-base">
<div class="grid grid-cols-[6rem_1fr] gap-x-4 items-baseline" data-summary-container>
<span class="text-xs uppercase tracking-wide text-gray-500">Standort</span>
<span class="items-summary-value" data-summary-field="location" data-summary-hide-empty="true">—</span>
<span class="items-summary-value" data-items-strike data-summary-field="location" data-summary-hide-empty="true">—</span>
</div>
<div class="grid grid-cols-[6rem_1fr] gap-x-4 items-baseline" data-summary-container>
<span class="text-xs uppercase tracking-wide text-gray-500">Vorh. als</span>
<span class="items-summary-value" data-summary-field="media" data-summary-hide-empty="true">—</span>
<span class="items-summary-value" data-items-strike data-summary-field="media" data-summary-hide-empty="true">—</span>
</div>
<div class="grid grid-cols-[6rem_1fr] gap-x-4 items-baseline" data-summary-container>
<span class="text-xs uppercase tracking-wide text-gray-500">URL</span>
<span class="items-summary-value" data-summary-field="uri" data-summary-hide-empty="true">
<span class="items-summary-value" data-items-strike data-summary-field="uri" data-summary-hide-empty="true">
<a class="no-underline" data-summary-link href="#" target="_blank" rel="noopener">—</a>
</span>
</div>
<div class="grid grid-cols-[6rem_1fr] gap-x-4 items-baseline" data-summary-container>
<span class="text-xs uppercase tracking-wide text-gray-500">Annotationen</span>
<span class="items-summary-value" data-summary-field="annotation" data-summary-hide-empty="true">—</span>
<span class="items-summary-value" data-items-strike data-summary-field="annotation" data-summary-hide-empty="true">—</span>
</div>
</div>
</div>
@@ -812,6 +833,7 @@ type AlmanachResult struct {
</button>
<button type="button" class="items-remove-button text-red-700 hover:text-red-900" aria-label="Entfernen">
<i class="ri-delete-bin-line"></i>
<span class="ml-1 text-sm no-underline" data-delete-label data-delete-default="Entfernen" data-delete-active="Wird entfernt" data-delete-hover="Rückgängig">Entfernen</span>
</button>
</div>
</div>
@@ -820,19 +842,19 @@ type AlmanachResult struct {
<div class="flex flex-col gap-3 mt-3">
<div class="inputwrapper">
<label class="inputlabel" data-field-label="owner">Besitzer</label>
<input class="inputinput" data-field="owner" name="items_owner[]" autocomplete="off" value="" />
<input class="inputinput" data-items-strike data-field="owner" name="items_owner[]" autocomplete="off" value="" />
</div>
<div class="inputwrapper">
<label class="inputlabel" data-field-label="identifier">Signatur</label>
<input class="inputinput" data-field="identifier" name="items_identifier[]" autocomplete="off" value="" />
<input class="inputinput" data-items-strike data-field="identifier" name="items_identifier[]" autocomplete="off" value="" />
</div>
<div class="inputwrapper">
<label class="inputlabel" data-field-label="location">Standort</label>
<input class="inputinput" data-field="location" name="items_location[]" autocomplete="off" value="" />
<input class="inputinput" data-items-strike data-field="location" name="items_location[]" autocomplete="off" value="" />
</div>
<div class="inputwrapper">
<label class="inputlabel" data-field-label="media">Vorhanden als</label>
<select class="inputselect" data-field="media" name="items_media[]" autocomplete="off">
<select class="inputselect" data-items-strike data-field="media" name="items_media[]" autocomplete="off">
<option value=""></option>
{{- range $t := $model.item_types -}}
<option value="{{- $t -}}">{{- $t -}}</option>
@@ -841,11 +863,11 @@ type AlmanachResult struct {
</div>
<div class="inputwrapper">
<label class="inputlabel" data-field-label="annotation">Annotationen</label>
<textarea class="inputtextarea min-h-[8rem]" data-field="annotation" name="items_annotation[]" autocomplete="off"></textarea>
<textarea class="inputtextarea min-h-[8rem]" data-items-strike data-field="annotation" name="items_annotation[]" autocomplete="off"></textarea>
</div>
<div class="inputwrapper">
<label class="inputlabel" data-field-label="uri">URI</label>
<input class="inputinput" data-field="uri" name="items_uri[]" autocomplete="off" value="" />
<input class="inputinput" data-items-strike data-field="uri" name="items_uri[]" autocomplete="off" value="" />
</div>
</div>
<div class="flex justify-end gap-3 mt-3 px-3 py-1">
@@ -867,3 +889,4 @@ type AlmanachResult struct {
<!-- End Right Column -->
</form>
</div>
</almanach-edit-page>

View File

@@ -0,0 +1,50 @@
export class AlmanachEditPage extends HTMLElement {
constructor() {
super();
this._pendingAgent = null;
}
connectedCallback() {
this._initForm();
this._initPlaces();
}
_initForm() {
const form = this.querySelector("#changealmanachform");
if (form && typeof window.FormLoad === "function") {
window.FormLoad(form);
}
}
_parseJSONAttr(element, name) {
if (!element) {
return null;
}
const raw = element.getAttribute(name);
if (!raw) {
return null;
}
try {
return JSON.parse(raw);
} catch {
return null;
}
}
_initPlaces() {
const placesSelect = this.querySelector("#places");
if (!placesSelect) {
return;
}
const initialPlaces = this._parseJSONAttr(placesSelect, "data-initial-options") || [];
const initialPlaceIds = this._parseJSONAttr(placesSelect, "data-initial-values") || [];
if (initialPlaces.length > 0 && typeof placesSelect.setOptions === "function") {
placesSelect.setOptions(initialPlaces);
}
if (initialPlaceIds.length > 0) {
placesSelect.value = initialPlaceIds;
}
}
}

View File

@@ -41,6 +41,23 @@
@apply relative inline-block;
}
.relation-strike,
.entries-agent-strike {
position: relative;
}
.relation-strike.is-removed::after,
.entries-agent-strike.is-removed::after {
content: "";
position: absolute;
left: 0;
right: 0;
top: 50%;
transform: translateY(-50%);
border-top: 2px solid #dc2626;
pointer-events: none;
}
/* Multi-Select-Role example styles */
.msr-selected-items-container {
@apply rounded-md;
@@ -180,16 +197,16 @@
}
.mss-selected-items-container {
@apply py-2 rounded;
@apply py-1 rounded;
/* Tailwind classes from component: flex flex-wrap gap-1 mb-1 min-h-[38px] */
}
.mss-no-items-text {
@apply italic text-xs text-gray-500 w-full; /* Adjusted font size slightly to match 'xs' */
@apply italic text-sm text-gray-500 w-full;
}
.mss-selected-item-pill {
@apply bg-gray-200 text-gray-800 py-0.5 px-2 rounded leading-5 shadow-xs hover:shadow select-none; /* Adjusted font size and padding */
@apply bg-stone-50 text-stone-900 py-0.5 px-2 border rounded leading-5 shadow-xs hover:shadow select-none; /* Adjusted font size and padding */
/* Tailwind classes from component: flex items-center */
}
@@ -212,7 +229,7 @@
}
.mss-input-wrapper {
@apply border border-gray-300 rounded;
@apply border border-gray-300 rounded bg-stone-50;
/* Tailwind classes from component: relative flex items-center flex-grow */
}
.mss-input-wrapper-focused {
@@ -220,7 +237,7 @@
}
.mss-text-input {
@apply py-1.5 px-2;
@apply py-1 px-1.5;
/* Tailwind classes from component: w-full outline-none bg-transparent */
}
.mss-text-input::placeholder {
@@ -234,6 +251,10 @@
@apply !hidden; /* Ensure it hides */
}
.mss-toggle-button {
@apply text-gray-700 hover:text-gray-900 font-semibold text-lg px-2 py-0.5 rounded-xs border border-transparent bg-transparent whitespace-nowrap leading-none;
}
.mss-options-list {
@apply bg-white border border-gray-300 rounded shadow-md list-none m-0; /* Using shadow-md as a softer default */
/* Tailwind classes from component: absolute z-20 w-full max-h-60 overflow-y-auto mt-1 hidden */

View File

@@ -9,6 +9,7 @@ const CLOSE_BUTTON_CLASS = "items-close-button";
const SUMMARY_CLASS = "items-summary";
const EDIT_PANEL_CLASS = "items-edit-panel";
const REMOVED_INPUT_NAME = "items_removed[]";
const REMOVED_ROW_STATE = "data-items-removed";
export class ItemsEditor extends HTMLElement {
constructor() {
@@ -76,15 +77,8 @@ export class ItemsEditor extends HTMLElement {
if (!row) {
return;
}
const idInput = row.querySelector('input[name="items_id[]"]');
const itemId = idInput ? idInput.value.trim() : "";
if (itemId) {
this._ensureRemovalInput(itemId);
}
row.remove();
this._refreshRowIds();
const isRemoved = row.getAttribute(REMOVED_ROW_STATE) === "true";
this._setRowRemoved(row, !isRemoved);
}
_wireRemoveButtons(root = this) {
@@ -97,6 +91,47 @@ export class ItemsEditor extends HTMLElement {
event.preventDefault();
this.removeItem(btn);
});
btn.addEventListener("mouseenter", () => {
const row = btn.closest(`.${ROW_CLASS}`);
if (!row || row.getAttribute(REMOVED_ROW_STATE) !== "true") {
return;
}
const label = btn.querySelector("[data-delete-label]");
if (label) {
label.textContent = label.getAttribute("data-delete-hover") || "Rückgängig";
label.classList.add("text-orange-700");
}
const icon = btn.querySelector("i");
if (icon) {
icon.classList.remove("hidden");
icon.classList.add("ri-arrow-go-back-line");
icon.classList.remove("ri-delete-bin-line");
}
});
btn.addEventListener("mouseleave", () => {
const row = btn.closest(`.${ROW_CLASS}`);
const label = btn.querySelector("[data-delete-label]");
if (!label) {
return;
}
label.classList.remove("text-orange-700");
if (row && row.getAttribute(REMOVED_ROW_STATE) === "true") {
label.textContent = label.getAttribute("data-delete-active") || "Wird entfernt";
} else {
label.textContent = label.getAttribute("data-delete-default") || "Entfernen";
}
const icon = btn.querySelector("i");
if (icon) {
if (row && row.getAttribute(REMOVED_ROW_STATE) === "true") {
icon.classList.add("hidden");
icon.classList.remove("ri-delete-bin-line", "ri-arrow-go-back-line");
} else {
icon.classList.remove("hidden");
icon.classList.add("ri-delete-bin-line");
icon.classList.remove("ri-arrow-go-back-line");
}
}
});
});
}
@@ -159,6 +194,48 @@ export class ItemsEditor extends HTMLElement {
this._setRowMode(row, "summary");
}
_setRowRemoved(row, removed) {
row.setAttribute(REMOVED_ROW_STATE, removed ? "true" : "false");
row.classList.toggle("bg-red-50", removed);
row.querySelectorAll("[data-items-strike]").forEach((el) => {
el.classList.toggle("line-through", removed);
el.classList.toggle("decoration-2", removed);
el.classList.toggle("decoration-red-600", removed);
el.classList.toggle("text-gray-500", removed);
});
row.querySelectorAll("[data-delete-label]").forEach((label) => {
const nextLabel = removed
? label.getAttribute("data-delete-active") || "Wird entfernt"
: label.getAttribute("data-delete-default") || "Entfernen";
label.textContent = nextLabel;
});
row.querySelectorAll(`.${REMOVE_BUTTON_CLASS} i`).forEach((icon) => {
if (removed) {
icon.classList.add("hidden");
icon.classList.remove("ri-delete-bin-line", "ri-arrow-go-back-line");
} else {
icon.classList.remove("hidden");
icon.classList.add("ri-delete-bin-line");
icon.classList.remove("ri-arrow-go-back-line");
}
});
const idInput = row.querySelector('input[name="items_id[]"]');
const itemId = idInput ? idInput.value.trim() : "";
if (itemId) {
if (removed) {
this._ensureRemovalInput(itemId);
} else {
this._removeRemovalInput(itemId);
}
}
row.querySelectorAll("[data-field]").forEach((field) => {
field.disabled = removed;
});
}
_setRowMode(row, mode) {
const summary = row.querySelector(`.${SUMMARY_CLASS}`);
const editor = row.querySelector(`.${EDIT_PANEL_CLASS}`);
@@ -238,6 +315,7 @@ export class ItemsEditor extends HTMLElement {
this.querySelectorAll(`.${ROW_CLASS}`).forEach((row) => {
this._wireSummarySync(row);
this._syncSummary(row);
this._syncNewBadge(row);
});
}
@@ -285,6 +363,15 @@ export class ItemsEditor extends HTMLElement {
}
}
});
this._syncNewBadge(row);
}
_syncNewBadge(row) {
const idInput = row.querySelector('input[name="items_id[]"]');
const itemId = idInput ? idInput.value.trim() : "";
row.querySelectorAll("[data-new-badge]").forEach((badge) => {
badge.classList.toggle("hidden", itemId !== "");
});
}
_setSummaryContent(summaryField, value) {
@@ -336,4 +423,13 @@ export class ItemsEditor extends HTMLElement {
hidden.value = itemId;
this.appendChild(hidden);
}
_removeRemovalInput(itemId) {
const inputs = Array.from(this.querySelectorAll(`input[name="${REMOVED_INPUT_NAME}"]`));
for (const input of inputs) {
if (input.value === itemId) {
input.remove();
}
}
}
}

View File

@@ -16,6 +16,8 @@ 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";
import { AlmanachEditPage } from "./almanach-edit.js";
import { RelationsEditor } from "./relations-editor.js";
const FILTER_LIST_ELEMENT = "filter-list";
const SCROLL_BUTTON_ELEMENT = "scroll-button";
@@ -32,6 +34,8 @@ 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";
const ALMANACH_EDIT_PAGE_ELEMENT = "almanach-edit-page";
const RELATIONS_EDITOR_ELEMENT = "relations-editor";
customElements.define(INT_LINK_ELEMENT, IntLink);
customElements.define(ABBREV_TOOLTIPS_ELEMENT, AbbreviationTooltips);
@@ -48,6 +52,8 @@ 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);
customElements.define(ALMANACH_EDIT_PAGE_ELEMENT, AlmanachEditPage);
customElements.define(RELATIONS_EDITOR_ELEMENT, RelationsEditor);
function PathPlusQuery() {
const path = window.location.pathname;
@@ -283,4 +289,4 @@ window.PathPlusQuery = PathPlusQuery;
window.HookupRBChange = HookupRBChange;
window.FormLoad = FormLoad;
export { FilterList, ScrollButton, AbbreviationTooltips, MultiSelectSimple, MultiSelectRole, ToolTip, PopupImage, TabList, FilterPill, ImageReel, IntLink, ItemsEditor, SingleSelectRemote };
export { FilterList, ScrollButton, AbbreviationTooltips, MultiSelectSimple, MultiSelectRole, ToolTip, PopupImage, TabList, FilterPill, ImageReel, IntLink, ItemsEditor, SingleSelectRemote, AlmanachEditPage, RelationsEditor };

View File

@@ -10,6 +10,8 @@ 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_TOGGLE_BUTTON_CLASS = "mss-toggle-button";
const MSS_INLINE_ROW_CLASS = "mss-inline-row";
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";
@@ -36,6 +38,11 @@ export class MultiSelectSimple extends HTMLElement {
super();
this.internals_ = this.attachInternals();
this._value = [];
this._initialValue = [];
this._initialOrder = [];
this._removedIds = new Set();
this._initialCaptured = false;
this._allowInitialCapture = true;
this._options = [
{ id: "abk", name: "Abchasisch" },
{ id: "aar", name: "Afar" },
@@ -234,6 +241,9 @@ export class MultiSelectSimple extends HTMLElement {
this._placeholder = this.getAttribute("placeholder") || "Search items...";
this._showCreateButton = this.getAttribute("show-create-button") !== "false";
this._toggleLabel = this.getAttribute("data-toggle-label") || "";
this._toggleInput = this._toggleLabel !== "";
this._inputCollapsed = this._toggleInput;
this._setupTemplates();
this._bindEventHandlers();
@@ -268,6 +278,7 @@ export class MultiSelectSimple extends HTMLElement {
this._handleOptionClick = this._handleOptionClick.bind(this);
this._handleCreateNewButtonClick = this._handleCreateNewButtonClick.bind(this);
this._handleSelectedItemsContainerClick = this._handleSelectedItemsContainerClick.bind(this);
this._handleToggleClick = this._handleToggleClick.bind(this);
}
_getItemById(id) {
@@ -325,6 +336,16 @@ export class MultiSelectSimple extends HTMLElement {
else if (!this._getItemById(singleId)) this._value = this._value.filter((id) => id !== singleId);
} else this._value = [];
const newValString = JSON.stringify(this._value.sort());
if (!this._initialCaptured && this._allowInitialCapture && this._value.length > 0) {
this._initialValue = [...this._value];
this._initialOrder = [...this._value];
this._initialCaptured = true;
}
this._value.forEach((id) => {
if (this._removedIds.has(id)) {
this._removedIds.delete(id);
}
});
if (oldValString !== newValString) {
this._updateFormValue();
if (this.selectedItemsContainer) this._renderSelectedItems();
@@ -347,12 +368,16 @@ export class MultiSelectSimple extends HTMLElement {
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.toggleButton = this.querySelector(`.${MSS_TOGGLE_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";
this._toggleLabel = this.getAttribute("data-toggle-label") || "";
this._toggleInput = this._toggleLabel !== "";
this._inputCollapsed = this._toggleInput;
this._remoteEndpoint = this.getAttribute("data-endpoint") || null;
this._remoteResultKey = this.getAttribute("data-result-key") || "items";
this._remoteMinChars = this._parsePositiveInt(this.getAttribute("data-minchars"), MSS_REMOTE_DEFAULT_MIN_CHARS);
@@ -368,6 +393,9 @@ export class MultiSelectSimple extends HTMLElement {
this.optionsListElement.addEventListener("click", this._handleOptionClick);
this.createNewButton.addEventListener("click", this._handleCreateNewButtonClick);
this.selectedItemsContainer.addEventListener("click", this._handleSelectedItemsContainerClick);
if (this.toggleButton) {
this.toggleButton.addEventListener("click", this._handleToggleClick);
}
this._updateRootElementStateClasses();
if (this.hasAttribute("value")) {
@@ -385,6 +413,15 @@ export class MultiSelectSimple extends HTMLElement {
this._synchronizeHiddenSelect();
}
if (this.hasAttribute("disabled")) this.disabledCallback(true);
if (this._toggleInput) {
this._hideInputControls();
}
this._allowInitialCapture = false;
if (!this._initialCaptured) {
this._initialValue = [...this._value];
this._initialOrder = [...this._value];
this._initialCaptured = true;
}
}
disconnectedCallback() {
@@ -400,6 +437,7 @@ export class MultiSelectSimple extends HTMLElement {
}
if (this.createNewButton) this.createNewButton.removeEventListener("click", this._handleCreateNewButtonClick);
if (this.selectedItemsContainer) this.selectedItemsContainer.removeEventListener("click", this._handleSelectedItemsContainerClick);
if (this.toggleButton) this.toggleButton.removeEventListener("click", this._handleToggleClick);
clearTimeout(this._blurTimeout);
if (this._remoteFetchTimeout) {
clearTimeout(this._remoteFetchTimeout);
@@ -409,7 +447,18 @@ export class MultiSelectSimple extends HTMLElement {
}
static get observedAttributes() {
return ["disabled", "name", "value", "placeholder", "show-create-button", "data-endpoint", "data-result-key", "data-minchars", "data-limit"];
return [
"disabled",
"name",
"value",
"placeholder",
"show-create-button",
"data-endpoint",
"data-result-key",
"data-minchars",
"data-limit",
"data-toggle-label",
];
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) return;
@@ -430,6 +479,10 @@ export class MultiSelectSimple extends HTMLElement {
else if (name === "data-result-key") this._remoteResultKey = newValue || "items";
else if (name === "data-minchars") this._remoteMinChars = this._parsePositiveInt(newValue, MSS_REMOTE_DEFAULT_MIN_CHARS);
else if (name === "data-limit") this._remoteLimit = this._parsePositiveInt(newValue, MSS_REMOTE_DEFAULT_LIMIT);
else if (name === "data-toggle-label") {
this._toggleLabel = newValue || "";
this._toggleInput = this._toggleLabel !== "";
}
}
formAssociatedCallback(form) {}
@@ -444,6 +497,9 @@ export class MultiSelectSimple extends HTMLElement {
this.showCreateButton = this.getAttribute("show-create-button") !== "false";
this._updateRootElementStateClasses();
this._renderSelectedItems();
if (this._toggleInput) {
this._hideInputControls();
}
}
formStateRestoreCallback(state, mode) {
this.value = Array.isArray(state) ? state : [];
@@ -482,23 +538,29 @@ export class MultiSelectSimple extends HTMLElement {
_render() {
const componentId = this.id || `mss-${crypto.randomUUID().slice(0, 8)}`;
if (!this.id) this.setAttribute("id", componentId);
const toggleLabel = this.getAttribute("data-toggle-label") || "";
const toggleInput = toggleLabel !== "";
const inputHiddenClass = toggleInput ? "hidden" : "";
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-4">
<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"
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 class="${MSS_INLINE_ROW_CLASS} flex flex-wrap items-center gap-2">
<div class="${MSS_SELECTED_ITEMS_CONTAINER_CLASS} flex flex-wrap items-center gap-1 min-h-[30px]" aria-live="polite" tabindex="-1"></div>
${toggleInput ? `<button type="button" class="${MSS_TOGGLE_BUTTON_CLASS}">${toggleLabel}</button>` : ""}
<div class="${MSS_INPUT_CONTROLS_CONTAINER_CLASS} flex items-center gap-2 ${inputHiddenClass}">
<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"
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>
<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>
@@ -522,9 +584,22 @@ export class MultiSelectSimple extends HTMLElement {
detailEl.textContent = "";
detailEl.classList.add("hidden"); // Toggle visibility via JS
}
deleteBtn.setAttribute("aria-label", `Remove ${itemData.name}`);
const isRemoved = this._removedIds.has(itemId);
const isNew = !this._initialValue.includes(itemId);
if (isNew) {
const newBadge = document.createElement("span");
newBadge.className = "ml-1 text-xs text-gray-600";
newBadge.textContent = "(Neu)";
textEl.appendChild(newBadge);
}
if (isRemoved) {
textEl.classList.add("line-through", "decoration-2", "decoration-red-600", "text-gray-500");
detailEl.classList.add("line-through", "decoration-2", "decoration-red-600", "text-gray-500");
}
deleteBtn.setAttribute("aria-label", isRemoved ? `Undo remove ${itemData.name}` : `Remove ${itemData.name}`);
deleteBtn.dataset.id = itemId;
deleteBtn.disabled = this.hasAttribute("disabled");
deleteBtn.innerHTML = isRemoved ? '<span class="text-xs inline-flex items-center"><i class="ri-arrow-go-back-line"></i></span>' : "&times;";
deleteBtn.addEventListener("click", (e) => {
e.stopPropagation();
this._handleDeleteSelectedItem(itemId);
@@ -534,10 +609,13 @@ export class MultiSelectSimple extends HTMLElement {
_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>`;
const removedInOrder = this._initialOrder.filter((id) => this._removedIds.has(id) && !this._value.includes(id));
const displayIds = [...this._value, ...removedInOrder];
if (displayIds.length === 0) {
const emptyText = this.getAttribute("data-empty-text") || "Keine Auswahl...";
this.selectedItemsContainer.innerHTML = `<span class="${MSS_NO_ITEMS_TEXT_CLASS}">${emptyText}</span>`;
} else {
this._value.forEach((id) => {
displayIds.forEach((id) => {
const pillEl = this._createSelectedItemElement(id);
if (pillEl) this.selectedItemsContainer.appendChild(pillEl);
});
@@ -664,6 +742,9 @@ export class MultiSelectSimple extends HTMLElement {
case "Escape":
event.preventDefault();
this._hideOptionsList();
if (this._toggleInput) {
this._hideInputControls();
}
break;
case "Tab":
this._hideOptionsList();
@@ -685,7 +766,12 @@ export class MultiSelectSimple extends HTMLElement {
_handleBlur() {
if (this.inputWrapper) this.inputWrapper.classList.remove(MSS_INPUT_WRAPPER_FOCUSED_CLASS);
this._blurTimeout = setTimeout(() => {
if (!this.contains(document.activeElement)) this._hideOptionsList();
if (!this.contains(document.activeElement)) {
this._hideOptionsList();
if (this._toggleInput && (!this.inputElement || this.inputElement.value.trim() === "")) {
this._hideInputControls();
}
}
}, 150);
}
_handleOptionMouseDown(event) {
@@ -700,14 +786,75 @@ export class MultiSelectSimple extends HTMLElement {
if (this.inputElement) this.inputElement.value = "";
this._filteredOptions = [];
this._hideOptionsList();
if (this.inputElement && !this.hasAttribute("disabled")) this.inputElement.focus();
if (this._toggleInput) {
this._hideInputControls();
} else if (this.inputElement && !this.hasAttribute("disabled")) {
this.inputElement.focus();
}
}
_handleDeleteSelectedItem(itemId) {
if (this._removedIds.has(itemId)) {
this._removedIds.delete(itemId);
if (!this._value.includes(itemId)) {
this.value = [...this._value, itemId];
} else {
this._renderSelectedItems();
}
return;
}
if (this._initialValue.includes(itemId)) {
this._removedIds.add(itemId);
this.value = this._value.filter((id) => id !== itemId);
return;
}
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();
}
_handleToggleClick(event) {
event.preventDefault();
this._showInputControls();
}
_showInputControls() {
if (!this.inputControlsContainer) {
return;
}
this.inputControlsContainer.classList.remove("hidden");
if (this.toggleButton) {
this.toggleButton.classList.add("hidden");
}
if (this._value.length === 0 && this.selectedItemsContainer) {
const emptyText = this.selectedItemsContainer.querySelector(`.${MSS_NO_ITEMS_TEXT_CLASS}`);
if (emptyText) {
emptyText.classList.add("hidden");
}
}
if (this.inputElement && !this.hasAttribute("disabled")) {
this.inputElement.focus();
}
this._inputCollapsed = false;
}
_hideInputControls() {
if (!this.inputControlsContainer) {
return;
}
this.inputControlsContainer.classList.add("hidden");
if (this.toggleButton) {
this.toggleButton.classList.remove("hidden");
}
if (this._value.length === 0 && this.selectedItemsContainer) {
const emptyText = this.selectedItemsContainer.querySelector(`.${MSS_NO_ITEMS_TEXT_CLASS}`);
if (emptyText) {
emptyText.classList.remove("hidden");
}
}
this._hideOptionsList();
this._inputCollapsed = true;
}
_parsePositiveInt(value, fallback) {
if (!value) return fallback;
const parsed = parseInt(value, 10);

View File

@@ -0,0 +1,289 @@
const ROLE_ADD_TOGGLE = "[data-role='relation-add-toggle']";
const ROLE_ADD_PANEL = "[data-role='relation-add-panel']";
const ROLE_ADD_CLOSE = "[data-role='relation-add-close']";
const ROLE_ADD_APPLY = "[data-role='relation-add-apply']";
const ROLE_ADD_ERROR = "[data-role='relation-add-error']";
const ROLE_ADD_ROW = "[data-role='relation-add-row']";
const ROLE_ADD_SELECT = "[data-role='relation-add-select']";
const ROLE_TYPE_SELECT = "[data-role='relation-type-select']";
const ROLE_UNCERTAIN = "[data-role='relation-uncertain']";
const ROLE_NEW_TEMPLATE = "template[data-role='relation-new-template']";
const ROLE_NEW_DELETE = "[data-role='relation-new-delete']";
const ROLE_REL_ROW = "[data-rel-row]";
const ROLE_REL_STRIKE = "[data-rel-strike]";
export class RelationsEditor extends HTMLElement {
constructor() {
super();
this._pendingItem = null;
this._pendingApply = false;
}
connectedCallback() {
this._prefix = this.getAttribute("data-prefix") || "";
this._linkBase = this.getAttribute("data-link-base") || "";
this._newLabel = this.getAttribute("data-new-label") || "(Neu)";
this._addToggleId = this.getAttribute("data-add-toggle-id") || "";
this._setupAddPanel();
this._setupDeleteToggles();
}
_setupAddPanel() {
this._addToggle = this.querySelector(ROLE_ADD_TOGGLE);
if (this._addToggleId) {
const externalToggle = document.getElementById(this._addToggleId);
if (externalToggle) {
this._addToggle = externalToggle;
}
}
this._addPanel = this.querySelector(ROLE_ADD_PANEL);
this._addClose = this.querySelector(ROLE_ADD_CLOSE);
this._addApply = this.querySelector(ROLE_ADD_APPLY);
this._addError = this.querySelector(ROLE_ADD_ERROR);
this._addRow = this.querySelector(ROLE_ADD_ROW);
this._addSelect = this.querySelector(ROLE_ADD_SELECT);
this._typeSelect = this.querySelector(ROLE_TYPE_SELECT);
this._uncertain = this.querySelector(ROLE_UNCERTAIN);
this._template = this.querySelector(ROLE_NEW_TEMPLATE);
this._addInput = this._addSelect ? this._addSelect.querySelector(".ssr-input") : null;
if (!this._addPanel || !this._addRow || !this._addSelect || !this._typeSelect || !this._uncertain || !this._template) {
return;
}
if (this._addToggle) {
this._addToggle.addEventListener("click", () => {
this._addPanel.classList.toggle("hidden");
});
}
if (this._addClose) {
this._addClose.addEventListener("click", () => {
this._addPanel.classList.add("hidden");
});
}
if (this._addInput) {
this._addInput.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
this._pendingApply = true;
}
});
}
if (this._addApply) {
this._addApply.addEventListener("click", () => {
this._pendingApply = false;
const idInput = this._addPanel.querySelector(`input[name='${this._prefix}_new_id']`);
const hasSelection = idInput && idInput.value.trim().length > 0;
if (!hasSelection) {
if (this._addError) {
this._addError.classList.remove("hidden");
}
return;
}
if (this._addError) {
this._addError.classList.add("hidden");
}
if (!this._pendingItem) {
return;
}
this._insertNewRow();
});
}
this._addSelect.addEventListener("ssrchange", (event) => {
this._pendingItem = event.detail?.item || null;
if (this._pendingItem && this._addError) {
this._addError.classList.add("hidden");
}
if (this._pendingApply && this._pendingItem && this._addApply) {
this._pendingApply = false;
this._addApply.click();
}
});
}
_clearAddPanel() {
if (this._addSelect) {
const clearButton = this._addSelect.querySelector(".ssr-clear-button");
if (clearButton) {
clearButton.click();
}
}
if (this._typeSelect) {
this._typeSelect.selectedIndex = 0;
}
if (this._uncertain) {
this._uncertain.checked = false;
}
if (this._addError) {
this._addError.classList.add("hidden");
}
}
_insertNewRow() {
const fragment = this._template.content.cloneNode(true);
const row = fragment.querySelector(ROLE_REL_ROW) || fragment.firstElementChild;
if (!row) {
return;
}
const link = fragment.querySelector("[data-rel-link]");
if (link) {
link.setAttribute("href", `${this._linkBase}${this._pendingItem.id}`);
}
const nameEl = fragment.querySelector("[data-rel-name]");
if (nameEl) {
nameEl.textContent = this._pendingItem.name || "";
}
const detailEl = fragment.querySelector("[data-rel-detail]");
const detailContainer = fragment.querySelector("[data-rel-detail-container]");
const detailText = this._pendingItem.detail || this._pendingItem.bio || "";
if (detailEl && detailText) {
detailEl.textContent = detailText;
} else if (detailContainer) {
detailContainer.remove();
}
const newBadge = fragment.querySelector("[data-rel-new]");
if (newBadge) {
newBadge.textContent = this._newLabel;
}
const typeSelect = fragment.querySelector("[data-rel-input='type']");
if (typeSelect && this._typeSelect) {
typeSelect.innerHTML = this._typeSelect.innerHTML;
typeSelect.value = this._typeSelect.value;
typeSelect.name = `${this._prefix}_new_type`;
}
const uncertain = fragment.querySelector("[data-rel-input='uncertain']");
if (uncertain && this._uncertain) {
uncertain.checked = this._uncertain.checked;
uncertain.name = `${this._prefix}_new_uncertain`;
const uncertainId = `${this._prefix}_new_uncertain_row`;
uncertain.id = uncertainId;
const uncertainLabel = fragment.querySelector("[data-rel-uncertain-label]");
if (uncertainLabel) {
uncertainLabel.setAttribute("for", uncertainId);
}
}
const hiddenId = fragment.querySelector("[data-rel-input='id']");
if (hiddenId) {
hiddenId.name = `${this._prefix}_new_id`;
hiddenId.value = this._pendingItem.id;
}
const deleteButton = fragment.querySelector(ROLE_NEW_DELETE);
if (deleteButton) {
deleteButton.addEventListener("click", () => {
this._addRow.innerHTML = "";
this._pendingItem = null;
this._clearAddPanel();
if (this._addPanel) {
this._addPanel.classList.add("hidden");
}
});
}
this._addRow.innerHTML = "";
this._addRow.appendChild(fragment);
this._pendingItem = null;
this._clearAddPanel();
if (this._addPanel) {
this._addPanel.classList.add("hidden");
}
}
_setupDeleteToggles() {
this.querySelectorAll("[data-delete-toggle]").forEach((button) => {
button.addEventListener("click", () => {
const targetId = button.getAttribute("data-delete-toggle");
const checkbox = this.querySelector(`#${CSS.escape(targetId)}`);
if (!checkbox) {
return;
}
checkbox.checked = !checkbox.checked;
button.classList.toggle("opacity-60", checkbox.checked);
const row = button.closest(ROLE_REL_ROW);
if (row) {
row.classList.toggle("bg-red-50", checkbox.checked);
row.querySelectorAll(ROLE_REL_STRIKE).forEach((el) => {
el.classList.toggle("is-removed", checkbox.checked);
el.classList.toggle("text-gray-500", checkbox.checked);
});
}
const label = button.querySelector("[data-delete-label]");
if (label) {
label.textContent = checkbox.checked
? label.getAttribute("data-delete-active") || "Wird entfernt"
: label.getAttribute("data-delete-default") || "Entfernen";
}
const icon = button.querySelector("i");
if (icon) {
if (checkbox.checked) {
icon.classList.add("hidden");
icon.classList.remove("ri-delete-bin-line", "ri-arrow-go-back-line");
} else {
icon.classList.remove("hidden");
icon.classList.add("ri-delete-bin-line");
icon.classList.remove("ri-arrow-go-back-line");
}
}
});
button.addEventListener("mouseenter", () => {
const targetId = button.getAttribute("data-delete-toggle");
const checkbox = this.querySelector(`#${CSS.escape(targetId)}`);
if (!checkbox || !checkbox.checked) {
return;
}
const label = button.querySelector("[data-delete-label]");
if (label) {
label.textContent = label.getAttribute("data-delete-hover") || "Rückgängig";
label.classList.add("text-orange-700");
label.classList.remove("text-gray-500");
}
const icon = button.querySelector("i");
if (icon) {
icon.classList.remove("hidden");
icon.classList.add("ri-arrow-go-back-line");
icon.classList.remove("ri-delete-bin-line");
}
});
button.addEventListener("mouseleave", () => {
const targetId = button.getAttribute("data-delete-toggle");
const checkbox = this.querySelector(`#${CSS.escape(targetId)}`);
const label = button.querySelector("[data-delete-label]");
if (!label) {
return;
}
label.classList.remove("text-orange-700");
if (checkbox && checkbox.checked) {
label.textContent = label.getAttribute("data-delete-active") || "Wird entfernt";
} else {
label.textContent = label.getAttribute("data-delete-default") || "Entfernen";
}
const icon = button.querySelector("i");
if (icon) {
if (checkbox && checkbox.checked) {
icon.classList.add("hidden");
icon.classList.remove("ri-delete-bin-line", "ri-arrow-go-back-line");
} else {
icon.classList.remove("hidden");
icon.classList.add("ri-delete-bin-line");
icon.classList.remove("ri-arrow-go-back-line");
}
}
});
});
}
}

View File

@@ -22,6 +22,7 @@ export class SingleSelectRemote extends HTMLElement {
this._placeholder = "Search...";
this._options = [];
this._selected = null;
this._highlightedIndex = -1;
this._fetchTimeout = null;
this._fetchController = null;
this._listVisible = false;
@@ -93,6 +94,7 @@ export class SingleSelectRemote extends HTMLElement {
_handleInput(event) {
const value = event.target.value.trim();
this._selected = null;
this._highlightedIndex = -1;
this._syncHiddenInput();
if (value.length < this._minChars) {
@@ -114,6 +116,35 @@ export class SingleSelectRemote extends HTMLElement {
_handleKeyDown(event) {
if (event.key === "Escape") {
this._hideList();
return;
}
if (event.key === "ArrowDown") {
event.preventDefault();
this._moveHighlight(1);
return;
}
if (event.key === "ArrowUp") {
event.preventDefault();
this._moveHighlight(-1);
return;
}
if (event.key === "Home") {
event.preventDefault();
this._setHighlight(0);
return;
}
if (event.key === "End") {
event.preventDefault();
this._setHighlight(this._options.length - 1);
return;
}
if (event.key === "Enter") {
if (this._options.length === 0) {
return;
}
event.preventDefault();
const index = this._highlightedIndex >= 0 ? this._highlightedIndex : 0;
this._selectOption(this._options[index]);
}
}
@@ -164,6 +195,7 @@ export class SingleSelectRemote extends HTMLElement {
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._highlightedIndex = this._options.length > 0 ? 0 : -1;
this._renderOptions();
if (this._options.length > 0) {
this._showList();
@@ -186,10 +218,16 @@ export class SingleSelectRemote extends HTMLElement {
this._options.forEach((item) => {
const option = document.createElement("button");
option.type = "button";
option.setAttribute("data-index", String(this._options.indexOf(item)));
option.className = [
SSR_OPTION_CLASS,
"w-full text-left px-3 py-2 hover:bg-slate-100 transition-colors",
].join(" ");
const optionIndex = this._options.indexOf(item);
const isHighlighted = optionIndex === this._highlightedIndex;
option.classList.toggle("bg-slate-100", isHighlighted);
option.classList.toggle("text-gray-900", isHighlighted);
option.setAttribute("aria-selected", isHighlighted ? "true" : "false");
const nameEl = document.createElement("div");
nameEl.className = [SSR_OPTION_NAME_CLASS, "text-sm font-semibold text-gray-800"].join(" ");
@@ -218,6 +256,41 @@ export class SingleSelectRemote extends HTMLElement {
});
}
_setHighlight(index) {
if (this._options.length === 0) {
this._highlightedIndex = -1;
return;
}
const nextIndex = Math.max(0, Math.min(index, this._options.length - 1));
this._highlightedIndex = nextIndex;
this._renderOptions();
this._scrollHighlightedIntoView();
this._showList();
}
_moveHighlight(delta) {
if (this._options.length === 0) {
this._highlightedIndex = -1;
return;
}
const startIndex = this._highlightedIndex >= 0 ? this._highlightedIndex : 0;
const nextIndex = Math.max(0, Math.min(startIndex + delta, this._options.length - 1));
this._highlightedIndex = nextIndex;
this._renderOptions();
this._scrollHighlightedIntoView();
this._showList();
}
_scrollHighlightedIntoView() {
if (!this._list || this._highlightedIndex < 0) {
return;
}
const option = this._list.querySelector(`[data-index="${this._highlightedIndex}"]`);
if (option) {
option.scrollIntoView({ block: "nearest" });
}
}
_selectOption(item) {
this._selected = item;
if (this._input) {