mirror of
https://github.com/Theodor-Springmann-Stiftung/musenalm.git
synced 2026-02-04 02:25:30 +00:00
Some frontend validation logic
This commit is contained in:
@@ -67,6 +67,7 @@ func (p *SeriesAPI) searchHandler(app core.App) HandleFunc {
|
|||||||
"id": series.Id,
|
"id": series.Id,
|
||||||
"name": series.Title(),
|
"name": series.Title(),
|
||||||
"detail": series.Pseudonyms(),
|
"detail": series.Pseudonyms(),
|
||||||
|
"musenalm_id": strconv.Itoa(series.MusenalmID()),
|
||||||
})
|
})
|
||||||
if limit > 0 && len(response) >= limit {
|
if limit > 0 && len(response) >= limit {
|
||||||
break
|
break
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -17,6 +17,22 @@ type AlmanachResult struct {
|
|||||||
}
|
}
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
{{- $preferredSeriesTitle := "" -}}
|
||||||
|
{{- $preferredSeriesId := "" -}}
|
||||||
|
{{- $preferredSeriesMusenalmID := "" -}}
|
||||||
|
{{- $preferredRelationId := "" -}}
|
||||||
|
{{- if and $model.result $model.result.Series -}}
|
||||||
|
{{- range $i, $s := $model.result.Series -}}
|
||||||
|
{{- $rel := index $model.result.EntriesSeries $s.Id -}}
|
||||||
|
{{- if and $rel (eq $rel.Type "Bevorzugter Reihentitel") -}}
|
||||||
|
{{- $preferredSeriesTitle = $s.Title -}}
|
||||||
|
{{- $preferredSeriesId = $s.Id -}}
|
||||||
|
{{- $preferredSeriesMusenalmID = $s.MusenalmID -}}
|
||||||
|
{{- $preferredRelationId = $rel.Id -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
<almanach-edit-page>
|
<almanach-edit-page>
|
||||||
<duplicate-warning-checker></duplicate-warning-checker>
|
<duplicate-warning-checker></duplicate-warning-checker>
|
||||||
|
|
||||||
@@ -187,18 +203,62 @@ type AlmanachResult struct {
|
|||||||
<div class="inputwrapper">
|
<div class="inputwrapper">
|
||||||
<div class="inputlabelrow">
|
<div class="inputlabelrow">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<label for="preferred_title" class="inputlabel">Kurztitel</label>
|
<label for="preferred-title-field-input" class="inputlabel">Kurztitel</label>
|
||||||
<tool-tip position="top" class="!inline">
|
<tool-tip position="top" class="!inline">
|
||||||
<div class="data-tip">{{ help "entries" "preferred_title" }}</div>
|
<div class="data-tip">{{ help "entries" "preferred_title" }}</div>
|
||||||
<i class="ri-question-line"></i>
|
<i class="ri-question-line"></i>
|
||||||
</tool-tip>
|
</tool-tip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<textarea name="preferred_title" id="preferred_title" class="inputinput no-enter" placeholder="" required autocomplete="off" rows="1" data-duplicate-check data-duplicate-endpoint="/api/entries/search" data-duplicate-result-key="entries" data-duplicate-current-id="{{ if not $model.is_new }}{{ $model.result.Entry.Id }}{{ end }}">{{- $model.result.Entry.PreferredTitle -}}</textarea>
|
<lookup-field
|
||||||
<div class="duplicate-warning hidden" data-duplicate-warning-for="preferred_title">
|
id="preferred-title-field"
|
||||||
<i class="ri-information-line"></i>
|
name="preferred_title"
|
||||||
<span data-duplicate-count></span>
|
data-multiline="true"
|
||||||
|
data-no-enter="true"
|
||||||
|
data-required="true"
|
||||||
|
data-autocomplete="false"
|
||||||
|
data-valid-fn="lookupRequiredText"
|
||||||
|
data-dup-endpoint="/api/entries/search"
|
||||||
|
data-dup-result-key="entries"
|
||||||
|
data-dup-current-id="{{ if not $model.is_new }}{{ $model.result.Entry.Id }}{{ end }}"
|
||||||
|
value="{{- $model.result.Entry.PreferredTitle -}}">
|
||||||
|
</lookup-field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="inputwrapper">
|
||||||
|
<div class="inputlabelrow">
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<label for="preferred-series-field-input" class="inputlabel">Reihentitel</label>
|
||||||
|
<tool-tip position="top" class="!inline">
|
||||||
|
<div class="data-tip">{{ helpOr "entries" "series" "Bevorzugter Reihentitel für diesen Almanach." }}</div>
|
||||||
|
<i class="ri-question-line"></i>
|
||||||
|
</tool-tip>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3 ml-auto pr-2">
|
||||||
|
<a href="/reihen/new/" class="text-sm font-bold text-gray-700 hover:text-slate-950 no-underline" target="_blank" rel="noreferrer">
|
||||||
|
<i class="ri-add-line"></i> Neue Reihe anlegen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<lookup-field
|
||||||
|
id="preferred-series-field"
|
||||||
|
name="preferred_series_id"
|
||||||
|
data-value-name="preferred_series_id"
|
||||||
|
data-text-name=""
|
||||||
|
data-valid-fn="lookupRequiredId"
|
||||||
|
data-endpoint="/api/series/search"
|
||||||
|
data-result-key="series"
|
||||||
|
data-minchars="1"
|
||||||
|
data-limit="15"
|
||||||
|
data-link-fn="lookupSeriesLink"
|
||||||
|
data-value-fn="lookupSeriesValue"
|
||||||
|
data-required="true"
|
||||||
|
data-initial-id="{{ $preferredSeriesId }}"
|
||||||
|
data-initial-name="{{ $preferredSeriesTitle }}"
|
||||||
|
data-initial-musenalm-id="{{ $preferredSeriesMusenalmID }}"
|
||||||
|
data-preferred-relation-id="{{ $preferredRelationId }}"
|
||||||
|
data-preferred-series-id="{{ $preferredSeriesId }}">
|
||||||
|
</lookup-field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
@@ -438,7 +498,7 @@ type AlmanachResult struct {
|
|||||||
{{- if $model.result.Series -}}
|
{{- if $model.result.Series -}}
|
||||||
{{- range $i, $s := $model.result.Series -}}
|
{{- range $i, $s := $model.result.Series -}}
|
||||||
{{- $rel := index $model.result.EntriesSeries $s.Id -}}
|
{{- $rel := index $model.result.EntriesSeries $s.Id -}}
|
||||||
{{- if $rel -}}
|
{{- if and $rel (ne $rel.Type "Bevorzugter Reihentitel") -}}
|
||||||
<div data-rel-row class="entries-series-row rel-row">
|
<div data-rel-row class="entries-series-row rel-row">
|
||||||
<div class="rel-grid">
|
<div class="rel-grid">
|
||||||
<div data-rel-strike class="relation-strike rel-name-col">
|
<div data-rel-strike class="relation-strike rel-name-col">
|
||||||
@@ -452,8 +512,10 @@ type AlmanachResult struct {
|
|||||||
<div data-rel-strike class="relation-strike">
|
<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">
|
<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 -}}
|
{{- range $t := $model.series_relations -}}
|
||||||
|
{{- if ne $t "Bevorzugter Reihentitel" -}}
|
||||||
<option value="{{- $t -}}" {{ if eq $rel.Type $t }}selected{{ end }}>{{- $t -}}</option>
|
<option value="{{- $t -}}" {{ if eq $rel.Type $t }}selected{{ end }}>{{- $t -}}</option>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div data-rel-strike class="relation-strike rel-uncertain-container">
|
<div data-rel-strike class="relation-strike rel-uncertain-container">
|
||||||
@@ -505,8 +567,10 @@ type AlmanachResult struct {
|
|||||||
<label for="entries_series_new_type" class="sr-only">Beziehung</label>
|
<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">
|
<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 -}}
|
{{- range $t := $model.series_relations -}}
|
||||||
|
{{- if ne $t "Bevorzugter Reihentitel" -}}
|
||||||
<option value="{{- $t -}}">{{- $t -}}</option>
|
<option value="{{- $t -}}">{{- $t -}}</option>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="rel-uncertain-container">
|
<div class="rel-uncertain-container">
|
||||||
@@ -1142,9 +1206,9 @@ type AlmanachResult struct {
|
|||||||
</div>
|
</div>
|
||||||
<!-- End Right Column -->
|
<!-- End Right Column -->
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full flex items-end justify-between gap-4 mt-6 flex-wrap">
|
<div class="w-full flex flex-col gap-3 mt-6 items-end">
|
||||||
<p id="almanach-save-feedback" class="text-sm text-gray-600" aria-live="polite"></p>
|
<p id="almanach-save-feedback" class="save-feedback hidden text-right" aria-live="polite"></p>
|
||||||
<div class="flex items-center gap-3 self-end flex-wrap">
|
<div class="flex items-center gap-3 flex-wrap justify-end">
|
||||||
<a href="{{ if $model.is_new }}/suche/baende{{ else }}/almanach/{{ $model.result.Entry.MusenalmID }}{{ end }}" class="resetbutton w-40 flex items-center gap-2 justify-center">
|
<a href="{{ if $model.is_new }}/suche/baende{{ else }}/almanach/{{ $model.result.Entry.MusenalmID }}{{ end }}" class="resetbutton w-40 flex items-center gap-2 justify-center">
|
||||||
<i class="ri-close-line"></i>
|
<i class="ri-close-line"></i>
|
||||||
<span>Abbrechen</span>
|
<span>Abbrechen</span>
|
||||||
|
|||||||
@@ -12,31 +12,28 @@
|
|||||||
<div data-role="content-item" class="relative odd:bg-stone-100" data-content-id="{{ $contentID }}">
|
<div data-role="content-item" class="relative odd:bg-stone-100" data-content-id="{{ $contentID }}">
|
||||||
<div data-role="content-view">
|
<div data-role="content-view">
|
||||||
<div class="bg-transparent overflow-visible">
|
<div class="bg-transparent overflow-visible">
|
||||||
<div class="flex items-center justify-between gap-3 bg-transparent px-2 py-0 flex-nowrap whitespace-nowrap" data-content-header="true">
|
<div class="flex items-center justify-between gap-2 bg-transparent px-2 py-0.5 flex-nowrap whitespace-nowrap" data-content-header="true">
|
||||||
<div class="flex items-center gap-2 text-sm font-bold text-gray-800 flex-1 min-w-0 flex-nowrap whitespace-nowrap">
|
<div class="flex items-center gap-1.5 text-sm font-bold text-gray-800 flex-1 min-w-0 flex-nowrap whitespace-nowrap">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="text-slate-600 rounded-sm px-2 py-1 text-sm cursor-grab"
|
class="text-slate-600 rounded-sm px-1 py-0.5 text-xs cursor-grab"
|
||||||
data-role="content-drag-handle"
|
data-role="content-drag-handle"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
aria-label="Beitrag verschieben">
|
aria-label="Beitrag verschieben">
|
||||||
<i class="ri-draggable"></i>
|
<i class="ri-draggable text-xs"></i>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="text-slate-600 rounded-sm px-2 py-1 text-sm transition-colors hover:bg-stone-300" data-role="content-move-up" aria-label="Beitrag nach oben">
|
<button type="button" class="text-slate-600 rounded-sm px-1 py-0.5 text-xs transition-colors hover:bg-stone-300" data-role="content-move-up" aria-label="Beitrag nach oben">
|
||||||
<i class="ri-arrow-up-line"></i>
|
<i class="ri-arrow-up-line text-xs"></i>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="text-slate-600 rounded-sm px-2 py-1 text-sm transition-colors hover:bg-stone-300" data-role="content-move-down" aria-label="Beitrag nach unten">
|
<button type="button" class="text-slate-600 rounded-sm px-1 py-0.5 text-xs transition-colors hover:bg-stone-300" data-role="content-move-down" aria-label="Beitrag nach unten">
|
||||||
<i class="ri-arrow-down-line"></i>
|
<i class="ri-arrow-down-line text-xs"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{{- if $content.Extent -}}
|
|
||||||
<span class="content-search-text bg-slate-200 text-slate-900 px-1.5 py-0.5 rounded text-sm font-semibold shadow-sm shrink-0" data-role="content-page-pill">S. {{- $content.Extent -}}</span>
|
|
||||||
{{- end -}}
|
|
||||||
{{- if $content.MusenalmType -}}
|
{{- if $content.MusenalmType -}}
|
||||||
<span class="flex flex-nowrap gap-1 text-gray-700 font-normal overflow-hidden">
|
<span class="flex flex-nowrap gap-1 text-gray-700 font-normal overflow-hidden">
|
||||||
{{- range $i, $t := $content.MusenalmType -}}
|
{{- range $i, $t := $content.MusenalmType -}}
|
||||||
<span class="content-search-text bg-slate-200 text-slate-900 px-1.5 py-0.5 rounded text-base font-semibold shadow-sm" data-role="content-type-pill">{{- $t -}}</span>
|
<span class="content-search-text bg-slate-200 text-slate-900 px-1.5 py-0.5 rounded text-xs font-semibold shadow-sm" data-role="content-type-pill">{{- $t -}}</span>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
</span>
|
</span>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
@@ -52,28 +49,42 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 flex-nowrap whitespace-nowrap shrink-0">
|
<div class="flex items-center gap-1.5 flex-nowrap whitespace-nowrap shrink-0">
|
||||||
<span
|
{{- $scans := $content.ImagePaths -}}
|
||||||
id="content-{{ $content.Id }}-images-count"
|
{{- if $scans -}}
|
||||||
class="inline-flex items-center gap-1 text-sm font-semibold text-slate-600 mr-2.5 {{ if eq (len $content.Scans) 0 }}hidden{{ end }}">
|
<span class="inline-flex items-center gap-1 mr-2.5">
|
||||||
<i class="ri-image-line"></i>
|
{{- range $i, $scan := $scans -}}
|
||||||
<span>{{ len $content.Scans }}</span>
|
{{- if lt $i 3 -}}
|
||||||
|
<popup-image data-image-url="{{- $scan -}}" class="inline-flex items-center">
|
||||||
|
<img
|
||||||
|
src="{{- $scan -}}"
|
||||||
|
alt="Scan Vorschau"
|
||||||
|
class="h-5 w-5 rounded-full object-cover border border-slate-500 shadow-sm" />
|
||||||
|
</popup-image>
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
</span>
|
</span>
|
||||||
<span class="status-badge text-xs shadow-sm" data-status="{{ $content.EditState }}">
|
{{- end -}}
|
||||||
<i class="status-icon {{- if eq $content.EditState "Edited" }} ri-checkbox-circle-line{{- else if eq $content.EditState "Seen" }} ri-information-line{{- else if eq $content.EditState "Review" }} ri-search-line{{- else if eq $content.EditState "ToDo" }} ri-list-check{{- else }} ri-forbid-2-line{{- end }}"></i>
|
{{- if or $content.Extent (not $scans) -}}
|
||||||
|
<span class="content-search-text {{ if $content.Extent }}bg-blue-900 text-white shadow-sm{{ else }}bg-transparent text-transparent shadow-none{{ end }} px-1.5 py-0.5 rounded-xs text-xs font-semibold shrink-0 inline-flex items-center justify-end min-w-[5ch]" data-role="content-page-pill">
|
||||||
|
{{- if $content.Extent -}}S. {{- $content.Extent -}}{{- else -}} {{- end -}}
|
||||||
|
</span>
|
||||||
|
{{- end -}}
|
||||||
|
<span class="status-badge text-xs shadow-sm ml-2" data-status="{{ $content.EditState }}">
|
||||||
|
<i class="status-icon text-xs {{- if eq $content.EditState "Edited" }} ri-checkbox-circle-line{{- else if eq $content.EditState "Seen" }} ri-information-line{{- else if eq $content.EditState "Review" }} ri-search-line{{- else if eq $content.EditState "ToDo" }} ri-list-check{{- else }} ri-forbid-2-line{{- end }}"></i>
|
||||||
</span>
|
</span>
|
||||||
<a
|
<a
|
||||||
href="/almanach/{{ $entry.MusenalmID }}/contents/{{ $content.MusenalmID }}/edit"
|
href="/almanach/{{ $entry.MusenalmID }}/contents/{{ $content.MusenalmID }}/edit"
|
||||||
class="resetbutton w-9 h-9 flex items-center justify-center rounded-sm cursor-pointer hover:bg-stone-300"
|
class="resetbutton w-8 h-8 flex items-center justify-center rounded-sm cursor-pointer hover:bg-stone-300"
|
||||||
aria-label="Beitrag bearbeiten">
|
aria-label="Beitrag bearbeiten">
|
||||||
<i class="ri-edit-2-line"></i>
|
<i class="ri-edit-2-line text-sm"></i>
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="resetbutton w-9 h-9 flex items-center justify-center text-red-700 hover:text-red-900 hover:bg-red-100 rounded-sm"
|
class="resetbutton w-8 h-8 flex items-center justify-center text-red-700 hover:text-red-900 hover:bg-red-100 rounded-sm"
|
||||||
data-role="content-delete-view"
|
data-role="content-delete-view"
|
||||||
aria-label="Beitrag löschen">
|
aria-label="Beitrag löschen">
|
||||||
<i class="ri-delete-bin-line"></i>
|
<i class="ri-delete-bin-line text-sm"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -130,11 +130,19 @@
|
|||||||
</tool-tip>
|
</tool-tip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<textarea name="name" id="name" class="inputinput no-enter" autocomplete="off" rows="1" data-duplicate-check data-duplicate-endpoint="/api/places/search" data-duplicate-result-key="places" data-duplicate-current-id="{{ if not $model.is_new }}{{ $place.Id }}{{ end }}">{{- $place.Name -}}</textarea>
|
<lookup-field
|
||||||
<div class="duplicate-warning hidden" data-duplicate-warning-for="name">
|
id="place-name-field"
|
||||||
<i class="ri-information-line"></i>
|
name="name"
|
||||||
<span data-duplicate-count></span>
|
data-multiline="true"
|
||||||
</div>
|
data-no-enter="true"
|
||||||
|
data-required="true"
|
||||||
|
data-autocomplete="false"
|
||||||
|
data-valid-fn="lookupRequiredText"
|
||||||
|
data-dup-endpoint="/api/places/search"
|
||||||
|
data-dup-result-key="places"
|
||||||
|
data-dup-current-id="{{ if not $model.is_new }}{{ $place.Id }}{{ end }}"
|
||||||
|
value="{{- $place.Name -}}">
|
||||||
|
</lookup-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="inputwrapper">
|
<div class="inputwrapper">
|
||||||
<div class="inputlabelrow">
|
<div class="inputlabelrow">
|
||||||
|
|||||||
@@ -132,11 +132,19 @@
|
|||||||
</tool-tip>
|
</tool-tip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<textarea name="name" id="name" class="inputinput no-enter" autocomplete="off" rows="1" data-duplicate-check data-duplicate-endpoint="/api/agents/search" data-duplicate-result-key="agents" data-duplicate-current-id="{{ if not $model.is_new }}{{ $agent.Id }}{{ end }}">{{- $agent.Name -}}</textarea>
|
<lookup-field
|
||||||
<div class="duplicate-warning hidden" data-duplicate-warning-for="name">
|
id="person-name-field"
|
||||||
<i class="ri-information-line"></i>
|
name="name"
|
||||||
<span data-duplicate-count></span>
|
data-multiline="true"
|
||||||
</div>
|
data-no-enter="true"
|
||||||
|
data-required="true"
|
||||||
|
data-autocomplete="false"
|
||||||
|
data-valid-fn="lookupRequiredText"
|
||||||
|
data-dup-endpoint="/api/agents/search"
|
||||||
|
data-dup-result-key="agents"
|
||||||
|
data-dup-current-id="{{ if not $model.is_new }}{{ $agent.Id }}{{ end }}"
|
||||||
|
value="{{- $agent.Name -}}">
|
||||||
|
</lookup-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="inputwrapper">
|
<div class="inputwrapper">
|
||||||
<div class="inputlabelrow">
|
<div class="inputlabelrow">
|
||||||
|
|||||||
@@ -132,11 +132,19 @@
|
|||||||
</tool-tip>
|
</tool-tip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<textarea name="title" id="title" class="inputinput no-enter" autocomplete="off" rows="1" data-duplicate-check data-duplicate-endpoint="/api/series/search" data-duplicate-result-key="series" data-duplicate-current-id="{{ if not $model.is_new }}{{ $series.Id }}{{ end }}">{{- $series.Title -}}</textarea>
|
<lookup-field
|
||||||
<div class="duplicate-warning hidden" data-duplicate-warning-for="title">
|
id="series-title-field"
|
||||||
<i class="ri-information-line"></i>
|
name="title"
|
||||||
<span data-duplicate-count></span>
|
data-multiline="true"
|
||||||
</div>
|
data-no-enter="true"
|
||||||
|
data-required="true"
|
||||||
|
data-autocomplete="false"
|
||||||
|
data-valid-fn="lookupRequiredText"
|
||||||
|
data-dup-endpoint="/api/series/search"
|
||||||
|
data-dup-result-key="series"
|
||||||
|
data-dup-current-id="{{ if not $model.is_new }}{{ $series.Id }}{{ end }}"
|
||||||
|
value="{{- $series.Title -}}">
|
||||||
|
</lookup-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row gap-3">
|
<div class="flex flex-row gap-3">
|
||||||
<div class="inputwrapper grow">
|
<div class="inputwrapper grow">
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ export class AlmanachEditPage extends HTMLElement {
|
|||||||
this._saveEndpoint = "";
|
this._saveEndpoint = "";
|
||||||
this._deleteEndpoint = "";
|
this._deleteEndpoint = "";
|
||||||
this._isSaving = false;
|
this._isSaving = false;
|
||||||
|
this._preferredSeriesRelationId = "";
|
||||||
|
this._preferredSeriesSeriesId = "";
|
||||||
this._handleSaveClick = this._handleSaveClick.bind(this);
|
this._handleSaveClick = this._handleSaveClick.bind(this);
|
||||||
this._handleResetClick = this._handleResetClick.bind(this);
|
this._handleResetClick = this._handleResetClick.bind(this);
|
||||||
this._handleDeleteClick = this._handleDeleteClick.bind(this);
|
this._handleDeleteClick = this._handleDeleteClick.bind(this);
|
||||||
@@ -27,6 +29,7 @@ export class AlmanachEditPage extends HTMLElement {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this._initForm();
|
this._initForm();
|
||||||
this._initPlaces();
|
this._initPlaces();
|
||||||
|
this._initPreferredSeries();
|
||||||
this._initSaveHandling();
|
this._initSaveHandling();
|
||||||
this._initStatusSelect();
|
this._initStatusSelect();
|
||||||
}, 0);
|
}, 0);
|
||||||
@@ -171,6 +174,16 @@ export class AlmanachEditPage extends HTMLElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_initPreferredSeries() {
|
||||||
|
const preferredSelect = this.querySelector("#preferred-series-field");
|
||||||
|
if (!preferredSelect) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._preferredSeriesRelationId = preferredSelect.getAttribute("data-preferred-relation-id") || "";
|
||||||
|
this._preferredSeriesSeriesId = preferredSelect.getAttribute("data-preferred-series-id") || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
_teardownSaveHandling() {
|
_teardownSaveHandling() {
|
||||||
if (this._saveButton) {
|
if (this._saveButton) {
|
||||||
this._saveButton.removeEventListener("click", this._handleSaveClick);
|
this._saveButton.removeEventListener("click", this._handleSaveClick);
|
||||||
@@ -383,6 +396,54 @@ export class AlmanachEditPage extends HTMLElement {
|
|||||||
targetField: "series",
|
targetField: "series",
|
||||||
});
|
});
|
||||||
const newSeriesRelations = this._collectNewRelations("entries_series");
|
const newSeriesRelations = this._collectNewRelations("entries_series");
|
||||||
|
const preferredSeriesId = this._readValue(formData, "preferred_series_id");
|
||||||
|
if (!preferredSeriesId) {
|
||||||
|
throw new Error("Reihentitel ist erforderlich.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyPreferred = (relation) => {
|
||||||
|
relation.type = PREFERRED_SERIES_RELATION;
|
||||||
|
relation.uncertain = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let preferredApplied = false;
|
||||||
|
seriesRelations.forEach((relation) => {
|
||||||
|
if (relation.target_id === preferredSeriesId) {
|
||||||
|
applyPreferred(relation);
|
||||||
|
preferredApplied = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
newSeriesRelations.forEach((relation) => {
|
||||||
|
if (relation.target_id === preferredSeriesId) {
|
||||||
|
applyPreferred(relation);
|
||||||
|
preferredApplied = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!preferredApplied) {
|
||||||
|
if (this._preferredSeriesRelationId && this._preferredSeriesSeriesId === preferredSeriesId) {
|
||||||
|
seriesRelations.push({
|
||||||
|
id: this._preferredSeriesRelationId,
|
||||||
|
target_id: preferredSeriesId,
|
||||||
|
type: PREFERRED_SERIES_RELATION,
|
||||||
|
uncertain: false,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
newSeriesRelations.push({
|
||||||
|
target_id: preferredSeriesId,
|
||||||
|
type: PREFERRED_SERIES_RELATION,
|
||||||
|
uncertain: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this._preferredSeriesRelationId &&
|
||||||
|
this._preferredSeriesSeriesId &&
|
||||||
|
this._preferredSeriesSeriesId !== preferredSeriesId &&
|
||||||
|
!deletedSeriesRelationIds.includes(this._preferredSeriesRelationId)
|
||||||
|
) {
|
||||||
|
deletedSeriesRelationIds.push(this._preferredSeriesRelationId);
|
||||||
|
}
|
||||||
const preferredCount = [...seriesRelations, ...newSeriesRelations].filter(
|
const preferredCount = [...seriesRelations, ...newSeriesRelations].filter(
|
||||||
(relation) => relation.type === PREFERRED_SERIES_RELATION,
|
(relation) => relation.type === PREFERRED_SERIES_RELATION,
|
||||||
).length;
|
).length;
|
||||||
@@ -572,7 +633,8 @@ export class AlmanachEditPage extends HTMLElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._statusEl.textContent = "";
|
this._statusEl.textContent = "";
|
||||||
this._statusEl.classList.remove("text-red-700", "text-green-700");
|
this._statusEl.classList.remove("text-red-700", "text-green-700", "save-feedback-error", "save-feedback-success");
|
||||||
|
this._statusEl.classList.add("hidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
_showStatus(message, type) {
|
_showStatus(message, type) {
|
||||||
@@ -581,10 +643,11 @@ export class AlmanachEditPage extends HTMLElement {
|
|||||||
}
|
}
|
||||||
this._clearStatus();
|
this._clearStatus();
|
||||||
this._statusEl.textContent = message;
|
this._statusEl.textContent = message;
|
||||||
|
this._statusEl.classList.remove("hidden");
|
||||||
if (type === "success") {
|
if (type === "success") {
|
||||||
this._statusEl.classList.add("text-green-700");
|
this._statusEl.classList.add("text-green-700", "save-feedback-success");
|
||||||
} else if (type === "error") {
|
} else if (type === "error") {
|
||||||
this._statusEl.classList.add("text-red-700");
|
this._statusEl.classList.add("text-red-700", "save-feedback-error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -166,6 +166,42 @@
|
|||||||
@apply inline-flex items-center px-1 text-gray-500;
|
@apply inline-flex items-center px-1 text-gray-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.save-feedback {
|
||||||
|
@apply text-sm font-semibold px-3 py-2 rounded-xs border bg-stone-50 text-gray-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-feedback-error {
|
||||||
|
@apply bg-red-50 border-red-200 text-red-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-feedback-success {
|
||||||
|
@apply bg-green-50 border-green-200 text-green-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lf-warn-icon {
|
||||||
|
@apply ml-1 mr-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lf-link-button {
|
||||||
|
@apply ml-1 mr-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lf-clear-button {
|
||||||
|
@apply ml-1 mr-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lf-dup-warning {
|
||||||
|
@apply mt-0 mb-1 pl-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferred-series-select .ssr-clear-button {
|
||||||
|
@apply text-sm ml-1 mr-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferred-series-select .ssr-wrapper > .flex {
|
||||||
|
@apply gap-3;
|
||||||
|
}
|
||||||
|
|
||||||
.dbform div-menu {
|
.dbform div-menu {
|
||||||
@apply relative inline-block;
|
@apply relative inline-block;
|
||||||
}
|
}
|
||||||
|
|||||||
590
views/transform/lookup-field.js
Normal file
590
views/transform/lookup-field.js
Normal file
@@ -0,0 +1,590 @@
|
|||||||
|
const LF_WRAPPER_CLASS = "lookup-field";
|
||||||
|
const LF_INPUT_CLASS = "lf-input";
|
||||||
|
const LF_LIST_CLASS = "lf-list";
|
||||||
|
const LF_OPTION_CLASS = "lf-option";
|
||||||
|
const LF_HIDDEN_INPUT_CLASS = "lf-hidden-input";
|
||||||
|
const LF_CLEAR_BUTTON_CLASS = "lf-clear-button";
|
||||||
|
const LF_LINK_BUTTON_CLASS = "lf-link-button";
|
||||||
|
const LF_WARN_ICON_CLASS = "lf-warn-icon";
|
||||||
|
const LF_DUP_WARNING_CLASS = "lf-dup-warning";
|
||||||
|
|
||||||
|
const LF_DEFAULT_MIN_CHARS = 1;
|
||||||
|
const LF_DEFAULT_LIMIT = 10;
|
||||||
|
const LF_FETCH_DEBOUNCE_MS = 250;
|
||||||
|
|
||||||
|
export class LookupField extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this._endpoint = "";
|
||||||
|
this._resultKey = "items";
|
||||||
|
this._minChars = LF_DEFAULT_MIN_CHARS;
|
||||||
|
this._limit = LF_DEFAULT_LIMIT;
|
||||||
|
this._autocomplete = true;
|
||||||
|
this._placeholder = "";
|
||||||
|
this._required = false;
|
||||||
|
this._multiline = false;
|
||||||
|
this._valueName = "";
|
||||||
|
this._textName = "";
|
||||||
|
this._valueFn = null;
|
||||||
|
this._linkFn = null;
|
||||||
|
this._validFn = null;
|
||||||
|
|
||||||
|
this._dupEndpoint = "";
|
||||||
|
this._dupResultKey = "";
|
||||||
|
this._dupCurrentId = "";
|
||||||
|
this._dupExact = true;
|
||||||
|
|
||||||
|
this._options = [];
|
||||||
|
this._selected = null;
|
||||||
|
this._highlightedIndex = -1;
|
||||||
|
this._fetchTimeout = null;
|
||||||
|
this._fetchController = null;
|
||||||
|
this._dupTimeout = null;
|
||||||
|
this._listVisible = false;
|
||||||
|
|
||||||
|
this._input = null;
|
||||||
|
this._hiddenInput = null;
|
||||||
|
this._list = null;
|
||||||
|
this._clearButton = null;
|
||||||
|
this._linkButton = null;
|
||||||
|
this._warnIcon = null;
|
||||||
|
this._dupWarning = null;
|
||||||
|
|
||||||
|
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 [
|
||||||
|
"name",
|
||||||
|
"value",
|
||||||
|
"placeholder",
|
||||||
|
"data-endpoint",
|
||||||
|
"data-result-key",
|
||||||
|
"data-minchars",
|
||||||
|
"data-limit",
|
||||||
|
"data-autocomplete",
|
||||||
|
"data-required",
|
||||||
|
"data-multiline",
|
||||||
|
"data-value-name",
|
||||||
|
"data-text-name",
|
||||||
|
"data-value-fn",
|
||||||
|
"data-link-fn",
|
||||||
|
"data-valid-fn",
|
||||||
|
"data-dup-endpoint",
|
||||||
|
"data-dup-result-key",
|
||||||
|
"data-dup-current-id",
|
||||||
|
"data-dup-exact",
|
||||||
|
"data-initial-id",
|
||||||
|
"data-initial-name",
|
||||||
|
"data-initial-musenalm-id",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this._render();
|
||||||
|
this._bindElements();
|
||||||
|
this._syncFromAttributes();
|
||||||
|
this._applyInitialValue();
|
||||||
|
this._updateValidity();
|
||||||
|
this._maybeCheckDuplicates(this._input?.value || "");
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (!this._input) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._syncFromAttributes();
|
||||||
|
if (name === "value") {
|
||||||
|
this._applyInitialValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_render() {
|
||||||
|
const isMultiline = this.getAttribute("data-multiline") === "true";
|
||||||
|
const hasTextName = this.hasAttribute("data-text-name");
|
||||||
|
const textName = hasTextName ? this.getAttribute("data-text-name") || "" : "";
|
||||||
|
const valueName = this.getAttribute("data-value-name") || "";
|
||||||
|
const placeholder = this.getAttribute("placeholder") || "";
|
||||||
|
const inputId = this.getAttribute("id") ? `${this.getAttribute("id")}-input` : "";
|
||||||
|
const initialValue = this.getAttribute("value") || "";
|
||||||
|
|
||||||
|
const noEnter = this.getAttribute("data-no-enter") === "true";
|
||||||
|
const extraClass = noEnter ? " no-enter" : "";
|
||||||
|
const fallbackName = this.getAttribute("name") || "";
|
||||||
|
const finalTextName = hasTextName ? textName : fallbackName;
|
||||||
|
const textNameAttr = finalTextName ? ` name="${finalTextName}"` : "";
|
||||||
|
const inputMarkup = isMultiline
|
||||||
|
? `<textarea id="${inputId}" class="${LF_INPUT_CLASS} inputinput w-full${extraClass}" rows="1" placeholder="${placeholder}"${textNameAttr}>${initialValue}</textarea>`
|
||||||
|
: `<input id="${inputId}" type="text" class="${LF_INPUT_CLASS} inputinput w-full${extraClass}" placeholder="${placeholder}" value="${initialValue}"${textNameAttr} />`;
|
||||||
|
const hiddenInputMarkup = valueName ? `<input type="hidden" class="${LF_HIDDEN_INPUT_CLASS}" name="${valueName}" value="" />` : "";
|
||||||
|
|
||||||
|
this.innerHTML = `
|
||||||
|
<div class="${LF_WRAPPER_CLASS} relative">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
${inputMarkup.replace(/(class="[^"]*)"/, `$1" ${textNameAttr}`)}
|
||||||
|
<a class="${LF_LINK_BUTTON_CLASS} hidden text-sm text-gray-600 hover:text-gray-900 no-underline" aria-label="Auswahl öffnen" target="_blank" rel="noopener">
|
||||||
|
<i class="ri-external-link-line"></i>
|
||||||
|
</a>
|
||||||
|
<span class="${LF_WARN_ICON_CLASS} hidden text-red-700 text-lg" aria-hidden="true">
|
||||||
|
<i class="ri-error-warning-line"></i>
|
||||||
|
</span>
|
||||||
|
<button type="button" class="${LF_CLEAR_BUTTON_CLASS} text-sm text-gray-600 hover:text-gray-900" aria-label="Eingabe löschen">
|
||||||
|
<i class="ri-close-line"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
${hiddenInputMarkup}
|
||||||
|
<div class="${LF_LIST_CLASS} absolute left-0 right-0 mt-1 border border-stone-200 rounded-xs bg-white shadow-sm z-10 hidden max-h-64 overflow-auto"></div>
|
||||||
|
<div class="${LF_DUP_WARNING_CLASS} hidden text-sm text-blue-700 mt-1 flex items-center gap-2">
|
||||||
|
<i class="ri-information-line"></i>
|
||||||
|
<span data-role="dup-text"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
_bindElements() {
|
||||||
|
this._input = this.querySelector(`.${LF_INPUT_CLASS}`);
|
||||||
|
this._hiddenInput = this.querySelector(`.${LF_HIDDEN_INPUT_CLASS}`);
|
||||||
|
this._list = this.querySelector(`.${LF_LIST_CLASS}`);
|
||||||
|
this._clearButton = this.querySelector(`.${LF_CLEAR_BUTTON_CLASS}`);
|
||||||
|
this._linkButton = this.querySelector(`.${LF_LINK_BUTTON_CLASS}`);
|
||||||
|
this._warnIcon = this.querySelector(`.${LF_WARN_ICON_CLASS}`);
|
||||||
|
this._dupWarning = this.querySelector(`.${LF_DUP_WARNING_CLASS}`);
|
||||||
|
|
||||||
|
if (this._input) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
_syncFromAttributes() {
|
||||||
|
this._endpoint = this.getAttribute("data-endpoint") || "";
|
||||||
|
this._resultKey = this.getAttribute("data-result-key") || "items";
|
||||||
|
this._minChars = this._parsePositiveInt(this.getAttribute("data-minchars"), LF_DEFAULT_MIN_CHARS);
|
||||||
|
this._limit = this._parsePositiveInt(this.getAttribute("data-limit"), LF_DEFAULT_LIMIT);
|
||||||
|
this._autocomplete = this.getAttribute("data-autocomplete") !== "false";
|
||||||
|
this._placeholder = this.getAttribute("placeholder") || "";
|
||||||
|
this._required = this.getAttribute("data-required") === "true";
|
||||||
|
this._multiline = this.getAttribute("data-multiline") === "true";
|
||||||
|
this._valueName = this.getAttribute("data-value-name") || "";
|
||||||
|
this._textName = this.hasAttribute("data-text-name") ? this.getAttribute("data-text-name") || "" : "";
|
||||||
|
this._valueFn = this._getFn(this.getAttribute("data-value-fn"));
|
||||||
|
this._linkFn = this._getFn(this.getAttribute("data-link-fn"));
|
||||||
|
this._validFn = this._getFn(this.getAttribute("data-valid-fn"));
|
||||||
|
this._dupEndpoint = this.getAttribute("data-dup-endpoint") || "";
|
||||||
|
this._dupResultKey = this.getAttribute("data-dup-result-key") || "";
|
||||||
|
this._dupCurrentId = this.getAttribute("data-dup-current-id") || "";
|
||||||
|
this._dupExact = this.getAttribute("data-dup-exact") !== "false";
|
||||||
|
const initialName = this.getAttribute("data-initial-name") || "";
|
||||||
|
const initialId = this.getAttribute("data-initial-id") || "";
|
||||||
|
const initialMusenalmId = this.getAttribute("data-initial-musenalm-id") || "";
|
||||||
|
if (initialId && initialName && !this._selected) {
|
||||||
|
this._selected = { id: initialId, name: initialName, musenalm_id: initialMusenalmId || undefined };
|
||||||
|
this._syncHiddenInput();
|
||||||
|
if (this._input && !this._input.value) {
|
||||||
|
this._input.value = initialName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this._input) {
|
||||||
|
this._input.placeholder = this._placeholder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_getFn(name) {
|
||||||
|
if (!name) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const fn = window[name];
|
||||||
|
return typeof fn === "function" ? fn : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_applyInitialValue() {
|
||||||
|
const valueAttr = this.getAttribute("value") || "";
|
||||||
|
if (this._input && valueAttr && !this._input.value) {
|
||||||
|
this._input.value = valueAttr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleInput(event) {
|
||||||
|
const value = event.target.value.trim();
|
||||||
|
this._selected = null;
|
||||||
|
this._highlightedIndex = -1;
|
||||||
|
this._syncHiddenInput();
|
||||||
|
this._updateValidity();
|
||||||
|
this._maybeCheckDuplicates(value);
|
||||||
|
if (!this._autocomplete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleClear(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (this._input) {
|
||||||
|
this._input.value = "";
|
||||||
|
}
|
||||||
|
this._selected = null;
|
||||||
|
this._options = [];
|
||||||
|
this._syncHiddenInput();
|
||||||
|
this._updateValidity();
|
||||||
|
this._renderOptions();
|
||||||
|
this._hideList();
|
||||||
|
this._maybeCheckDuplicates("");
|
||||||
|
this.dispatchEvent(new CustomEvent("lfchange", { bubbles: true, detail: { item: null } }));
|
||||||
|
this.dispatchEvent(new Event("change", { bubbles: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleClickOutside(event) {
|
||||||
|
if (!this.contains(event.target)) {
|
||||||
|
this._hideList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_debouncedFetch(query) {
|
||||||
|
if (this._fetchTimeout) {
|
||||||
|
clearTimeout(this._fetchTimeout);
|
||||||
|
}
|
||||||
|
this._fetchTimeout = setTimeout(() => {
|
||||||
|
this._fetchOptions(query);
|
||||||
|
}, LF_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)
|
||||||
|
.map((item) => {
|
||||||
|
if ("musenalm_id" in item && item.musenalm_id) {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
const musenalmId = item.MusenalmID || item.musenalmId || item.musenalmID || "";
|
||||||
|
return musenalmId ? { ...item, musenalm_id: musenalmId } : item;
|
||||||
|
});
|
||||||
|
this._highlightedIndex = this._options.length > 0 ? 0 : -1;
|
||||||
|
this._maybeAutoSelectExactMatch(query);
|
||||||
|
this._renderOptions();
|
||||||
|
if (this._options.length > 0) {
|
||||||
|
if (this._options.length === 1 && this._isExactMatch(query, this._options[0]?.name || "")) {
|
||||||
|
this._hideList();
|
||||||
|
} else {
|
||||||
|
this._showList();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this._hideList();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (err?.name === "AbortError") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderOptions() {
|
||||||
|
if (!this._list) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._list.innerHTML = "";
|
||||||
|
this._options.forEach((item, idx) => {
|
||||||
|
const option = document.createElement("button");
|
||||||
|
option.type = "button";
|
||||||
|
option.setAttribute("data-index", String(idx));
|
||||||
|
option.className = `${LF_OPTION_CLASS} w-full text-left px-3 py-2 hover:bg-slate-100 transition-colors`;
|
||||||
|
const isHighlighted = idx === this._highlightedIndex;
|
||||||
|
option.classList.toggle("bg-slate-100", isHighlighted);
|
||||||
|
option.setAttribute("aria-selected", isHighlighted ? "true" : "false");
|
||||||
|
|
||||||
|
const nameEl = document.createElement("div");
|
||||||
|
nameEl.className = "text-sm font-semibold text-gray-800";
|
||||||
|
nameEl.textContent = item.name;
|
||||||
|
option.appendChild(nameEl);
|
||||||
|
|
||||||
|
if (item.detail) {
|
||||||
|
const detailEl = document.createElement("div");
|
||||||
|
detailEl.className = "text-xs text-gray-600";
|
||||||
|
detailEl.textContent = item.detail;
|
||||||
|
option.appendChild(detailEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
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._updateValidity();
|
||||||
|
this._hideList();
|
||||||
|
this.dispatchEvent(new CustomEvent("lfchange", { bubbles: true, detail: { item } }));
|
||||||
|
this.dispatchEvent(new Event("change", { bubbles: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
_isExactMatch(query, name) {
|
||||||
|
const lhs = (query || "").trim().toLowerCase();
|
||||||
|
const rhs = (name || "").trim().toLowerCase();
|
||||||
|
return lhs !== "" && lhs === rhs;
|
||||||
|
}
|
||||||
|
|
||||||
|
_maybeAutoSelectExactMatch(query) {
|
||||||
|
const match = this._options.find((item) => this._isExactMatch(query, item?.name || ""));
|
||||||
|
if (!match) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const prevId = this._selected?.id || "";
|
||||||
|
this._selected = match;
|
||||||
|
this._syncHiddenInput();
|
||||||
|
this._updateValidity();
|
||||||
|
if (match.id !== prevId) {
|
||||||
|
this.dispatchEvent(new CustomEvent("lfchange", { bubbles: true, detail: { item: match } }));
|
||||||
|
this.dispatchEvent(new Event("change", { bubbles: true }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_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" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_syncHiddenInput() {
|
||||||
|
if (!this._hiddenInput) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let nextValue = "";
|
||||||
|
if (this._valueFn && this._selected) {
|
||||||
|
nextValue = String(this._valueFn({ item: this._selected, displayValue: this._input?.value || "" }) || "");
|
||||||
|
} else if (this._selected?.id) {
|
||||||
|
nextValue = this._selected.id;
|
||||||
|
}
|
||||||
|
this._hiddenInput.value = nextValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateValidity() {
|
||||||
|
const displayValue = (this._input?.value || "").trim();
|
||||||
|
const hiddenValue = (this._hiddenInput?.value || "").trim();
|
||||||
|
let isValid = true;
|
||||||
|
if (this._validFn) {
|
||||||
|
isValid = Boolean(this._validFn({ value: hiddenValue || displayValue, displayValue, hiddenValue, item: this._selected }));
|
||||||
|
} else if (this._required) {
|
||||||
|
isValid = (hiddenValue || displayValue).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const linkUrl = this._linkFn ? this._linkFn({ item: this._selected, value: hiddenValue || displayValue }) : "";
|
||||||
|
if (this._warnIcon && this._linkButton) {
|
||||||
|
if (!isValid) {
|
||||||
|
this._warnIcon.classList.remove("hidden");
|
||||||
|
this._linkButton.classList.add("hidden");
|
||||||
|
} else if (linkUrl) {
|
||||||
|
this._warnIcon.classList.add("hidden");
|
||||||
|
this._linkButton.classList.remove("hidden");
|
||||||
|
this._linkButton.setAttribute("href", linkUrl);
|
||||||
|
} else {
|
||||||
|
this._warnIcon.classList.add("hidden");
|
||||||
|
this._linkButton.classList.add("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._clearButton) {
|
||||||
|
this._clearButton.classList.toggle("hidden", displayValue.length === 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_maybeCheckDuplicates(value) {
|
||||||
|
if (!this._dupEndpoint || !this._dupResultKey || !this._dupWarning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this._dupTimeout) {
|
||||||
|
clearTimeout(this._dupTimeout);
|
||||||
|
}
|
||||||
|
this._dupTimeout = setTimeout(() => {
|
||||||
|
this._checkDuplicates(value);
|
||||||
|
}, LF_FETCH_DEBOUNCE_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _checkDuplicates(value) {
|
||||||
|
if (!this._dupEndpoint || !this._dupResultKey || !this._dupWarning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const trimmed = (value || "").trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
this._dupWarning.classList.add("hidden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const url = new URL(this._dupEndpoint, window.location.origin);
|
||||||
|
url.searchParams.set("q", trimmed);
|
||||||
|
url.searchParams.set("limit", "100");
|
||||||
|
const resp = await fetch(url.toString());
|
||||||
|
if (!resp.ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = await resp.json();
|
||||||
|
const results = data[this._dupResultKey] || [];
|
||||||
|
let filtered = results;
|
||||||
|
if (this._dupCurrentId) {
|
||||||
|
filtered = results.filter((item) => item.id !== this._dupCurrentId);
|
||||||
|
}
|
||||||
|
const matches = this._dupExact
|
||||||
|
? filtered.filter((item) => item.name && item.name.toLowerCase() === trimmed.toLowerCase())
|
||||||
|
: filtered;
|
||||||
|
if (matches.length > 0) {
|
||||||
|
const textEl = this._dupWarning.querySelector("[data-role='dup-text']");
|
||||||
|
if (textEl) {
|
||||||
|
textEl.textContent = `Der Name ist bereits vorhanden (${matches.length} Treffer)`;
|
||||||
|
}
|
||||||
|
this._dupWarning.classList.remove("hidden");
|
||||||
|
} else {
|
||||||
|
this._dupWarning.classList.add("hidden");
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
this._dupWarning.classList.add("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_parsePositiveInt(value, fallback) {
|
||||||
|
const parsed = parseInt(value || "", 10);
|
||||||
|
if (Number.isNaN(parsed) || parsed <= 0) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
_showList() {
|
||||||
|
if (!this._list) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._listVisible = true;
|
||||||
|
this._list.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
_hideList() {
|
||||||
|
if (!this._list) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._listVisible = false;
|
||||||
|
this._list.classList.add("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ import { EditPage } from "./edit-page.js";
|
|||||||
import { FabMenu } from "./fab-menu.js";
|
import { FabMenu } from "./fab-menu.js";
|
||||||
import { DuplicateWarningChecker } from "./duplicate-warning.js";
|
import { DuplicateWarningChecker } from "./duplicate-warning.js";
|
||||||
import { ContentImages } from "./content-images.js";
|
import { ContentImages } from "./content-images.js";
|
||||||
|
import { LookupField } from "./lookup-field.js";
|
||||||
|
|
||||||
const FILTER_LIST_ELEMENT = "filter-list";
|
const FILTER_LIST_ELEMENT = "filter-list";
|
||||||
const FAB_MENU_ELEMENT = "fab-menu";
|
const FAB_MENU_ELEMENT = "fab-menu";
|
||||||
@@ -51,6 +52,12 @@ const RELATIONS_EDITOR_ELEMENT = "relations-editor";
|
|||||||
const EDIT_PAGE_ELEMENT = "edit-page";
|
const EDIT_PAGE_ELEMENT = "edit-page";
|
||||||
const DUPLICATE_WARNING_ELEMENT = "duplicate-warning-checker";
|
const DUPLICATE_WARNING_ELEMENT = "duplicate-warning-checker";
|
||||||
const CONTENT_IMAGES_ELEMENT = "content-images";
|
const CONTENT_IMAGES_ELEMENT = "content-images";
|
||||||
|
const LOOKUP_FIELD_ELEMENT = "lookup-field";
|
||||||
|
|
||||||
|
window.lookupSeriesValue = ({ item }) => item?.id || "";
|
||||||
|
window.lookupSeriesLink = ({ item }) => (item?.musenalm_id ? `/reihe/${item.musenalm_id}` : "");
|
||||||
|
window.lookupRequiredText = ({ displayValue }) => Boolean((displayValue || "").trim());
|
||||||
|
window.lookupRequiredId = ({ hiddenValue }) => Boolean((hiddenValue || "").trim());
|
||||||
|
|
||||||
customElements.define(INT_LINK_ELEMENT, IntLink);
|
customElements.define(INT_LINK_ELEMENT, IntLink);
|
||||||
customElements.define(ABBREV_TOOLTIPS_ELEMENT, AbbreviationTooltips);
|
customElements.define(ABBREV_TOOLTIPS_ELEMENT, AbbreviationTooltips);
|
||||||
@@ -73,6 +80,7 @@ customElements.define(EDIT_PAGE_ELEMENT, EditPage);
|
|||||||
customElements.define(FAB_MENU_ELEMENT, FabMenu);
|
customElements.define(FAB_MENU_ELEMENT, FabMenu);
|
||||||
customElements.define(DUPLICATE_WARNING_ELEMENT, DuplicateWarningChecker);
|
customElements.define(DUPLICATE_WARNING_ELEMENT, DuplicateWarningChecker);
|
||||||
customElements.define(CONTENT_IMAGES_ELEMENT, ContentImages);
|
customElements.define(CONTENT_IMAGES_ELEMENT, ContentImages);
|
||||||
|
customElements.define(LOOKUP_FIELD_ELEMENT, LookupField);
|
||||||
|
|
||||||
function PathPlusQuery() {
|
function PathPlusQuery() {
|
||||||
const path = window.location.pathname;
|
const path = window.location.pathname;
|
||||||
@@ -469,4 +477,4 @@ window.HookupRBChange = HookupRBChange;
|
|||||||
window.FormLoad = FormLoad;
|
window.FormLoad = FormLoad;
|
||||||
window.TextareaAutoResize = TextareaAutoResize;
|
window.TextareaAutoResize = TextareaAutoResize;
|
||||||
|
|
||||||
export { FilterList, ScrollButton, AbbreviationTooltips, MultiSelectSimple, MultiSelectRole, ToolTip, PopupImage, TabList, FilterPill, ImageReel, IntLink, ItemsEditor, SingleSelectRemote, AlmanachEditPage, RelationsEditor, EditPage, FabMenu };
|
export { FilterList, ScrollButton, AbbreviationTooltips, MultiSelectSimple, MultiSelectRole, ToolTip, PopupImage, TabList, FilterPill, ImageReel, IntLink, ItemsEditor, SingleSelectRemote, AlmanachEditPage, RelationsEditor, EditPage, FabMenu, LookupField };
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ export class SingleSelectRemote extends HTMLElement {
|
|||||||
this._fetchTimeout = null;
|
this._fetchTimeout = null;
|
||||||
this._fetchController = null;
|
this._fetchController = null;
|
||||||
this._listVisible = false;
|
this._listVisible = false;
|
||||||
|
this._linkBase = "";
|
||||||
|
this._linkTarget = "_blank";
|
||||||
|
this._linkButton = null;
|
||||||
|
this._showWarningIcon = false;
|
||||||
|
this._linkField = "id";
|
||||||
this._boundHandleInput = this._handleInput.bind(this);
|
this._boundHandleInput = this._handleInput.bind(this);
|
||||||
this._boundHandleFocus = this._handleFocus.bind(this);
|
this._boundHandleFocus = this._handleFocus.bind(this);
|
||||||
this._boundHandleKeyDown = this._handleKeyDown.bind(this);
|
this._boundHandleKeyDown = this._handleKeyDown.bind(this);
|
||||||
@@ -34,7 +39,19 @@ export class SingleSelectRemote extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static get observedAttributes() {
|
static get observedAttributes() {
|
||||||
return ["data-endpoint", "data-result-key", "data-minchars", "data-limit", "placeholder", "name"];
|
return [
|
||||||
|
"data-endpoint",
|
||||||
|
"data-result-key",
|
||||||
|
"data-minchars",
|
||||||
|
"data-limit",
|
||||||
|
"placeholder",
|
||||||
|
"name",
|
||||||
|
"data-link-base",
|
||||||
|
"data-link-target",
|
||||||
|
"data-link-field",
|
||||||
|
"data-initial-link-id",
|
||||||
|
"data-show-warning-icon",
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
@@ -49,6 +66,13 @@ export class SingleSelectRemote extends HTMLElement {
|
|||||||
this._minChars = this._parsePositiveInt(this.getAttribute("data-minchars"), SSR_DEFAULT_MIN_CHARS);
|
this._minChars = this._parsePositiveInt(this.getAttribute("data-minchars"), SSR_DEFAULT_MIN_CHARS);
|
||||||
this._limit = this._parsePositiveInt(this.getAttribute("data-limit"), SSR_DEFAULT_LIMIT);
|
this._limit = this._parsePositiveInt(this.getAttribute("data-limit"), SSR_DEFAULT_LIMIT);
|
||||||
this._placeholder = this.getAttribute("placeholder") || "Search...";
|
this._placeholder = this.getAttribute("placeholder") || "Search...";
|
||||||
|
const initialId = this.getAttribute("data-initial-id") || "";
|
||||||
|
const initialName = this.getAttribute("data-initial-name") || "";
|
||||||
|
const initialLinkId = this.getAttribute("data-initial-link-id") || "";
|
||||||
|
this._linkBase = this.getAttribute("data-link-base") || "";
|
||||||
|
this._linkTarget = this.getAttribute("data-link-target") || "_blank";
|
||||||
|
this._linkField = this.getAttribute("data-link-field") || "id";
|
||||||
|
this._showWarningIcon = this.getAttribute("data-show-warning-icon") === "true";
|
||||||
|
|
||||||
if (this._input) {
|
if (this._input) {
|
||||||
this._input.placeholder = this._placeholder;
|
this._input.placeholder = this._placeholder;
|
||||||
@@ -57,10 +81,21 @@ export class SingleSelectRemote extends HTMLElement {
|
|||||||
this._input.addEventListener("keydown", this._boundHandleKeyDown);
|
this._input.addEventListener("keydown", this._boundHandleKeyDown);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._linkButton = this.querySelector("[data-role='ssr-open-link']");
|
||||||
|
|
||||||
if (this._clearButton) {
|
if (this._clearButton) {
|
||||||
this._clearButton.addEventListener("click", this._boundHandleClear);
|
this._clearButton.addEventListener("click", this._boundHandleClear);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (initialId && initialName) {
|
||||||
|
this._selected = { id: initialId, name: initialName, linkId: initialLinkId };
|
||||||
|
if (this._input) {
|
||||||
|
this._input.value = initialName;
|
||||||
|
}
|
||||||
|
this._syncHiddenInput();
|
||||||
|
}
|
||||||
|
this._updateLinkButton();
|
||||||
|
|
||||||
document.addEventListener("click", this._boundHandleClickOutside);
|
document.addEventListener("click", this._boundHandleClickOutside);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +124,10 @@ export class SingleSelectRemote extends HTMLElement {
|
|||||||
if (this._input) this._input.placeholder = this._placeholder;
|
if (this._input) this._input.placeholder = this._placeholder;
|
||||||
}
|
}
|
||||||
if (name === "name" && this._hiddenInput) this._hiddenInput.name = newValue || "";
|
if (name === "name" && this._hiddenInput) this._hiddenInput.name = newValue || "";
|
||||||
|
if (name === "data-link-base") this._linkBase = newValue || "";
|
||||||
|
if (name === "data-link-target") this._linkTarget = newValue || "_blank";
|
||||||
|
if (name === "data-link-field") this._linkField = newValue || "id";
|
||||||
|
if (name === "data-show-warning-icon") this._showWarningIcon = newValue === "true";
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleInput(event) {
|
_handleInput(event) {
|
||||||
@@ -154,6 +193,7 @@ export class SingleSelectRemote extends HTMLElement {
|
|||||||
this._options = [];
|
this._options = [];
|
||||||
if (this._input) this._input.value = "";
|
if (this._input) this._input.value = "";
|
||||||
this._syncHiddenInput();
|
this._syncHiddenInput();
|
||||||
|
this._updateLinkButton();
|
||||||
this._renderOptions();
|
this._renderOptions();
|
||||||
this._hideList();
|
this._hideList();
|
||||||
this.dispatchEvent(new CustomEvent("ssrchange", { bubbles: true, detail: { item: null } }));
|
this.dispatchEvent(new CustomEvent("ssrchange", { bubbles: true, detail: { item: null } }));
|
||||||
@@ -208,9 +248,14 @@ export class SingleSelectRemote extends HTMLElement {
|
|||||||
|
|
||||||
this._options = filteredItems;
|
this._options = filteredItems;
|
||||||
this._highlightedIndex = this._options.length > 0 ? 0 : -1;
|
this._highlightedIndex = this._options.length > 0 ? 0 : -1;
|
||||||
|
this._maybeAutoSelectExactMatch(query);
|
||||||
this._renderOptions();
|
this._renderOptions();
|
||||||
if (this._options.length > 0) {
|
if (this._options.length > 0) {
|
||||||
|
if (this._options.length === 1 && this._isExactMatch(query, this._options[0]?.name || "")) {
|
||||||
|
this._hideList();
|
||||||
|
} else {
|
||||||
this._showList();
|
this._showList();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this._hideList();
|
this._hideList();
|
||||||
}
|
}
|
||||||
@@ -221,6 +266,30 @@ export class SingleSelectRemote extends HTMLElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_isExactMatch(query, name) {
|
||||||
|
const lhs = (query || "").trim().toLowerCase();
|
||||||
|
const rhs = (name || "").trim().toLowerCase();
|
||||||
|
return lhs !== "" && lhs === rhs;
|
||||||
|
}
|
||||||
|
|
||||||
|
_maybeAutoSelectExactMatch(query) {
|
||||||
|
if (!query) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const match = this._options.find((item) => this._isExactMatch(query, item?.name || ""));
|
||||||
|
if (!match) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const prevId = this._selected?.id || "";
|
||||||
|
this._selected = match;
|
||||||
|
this._syncHiddenInput();
|
||||||
|
this._updateLinkButton();
|
||||||
|
if (match.id !== prevId) {
|
||||||
|
this.dispatchEvent(new CustomEvent("ssrchange", { bubbles: true, detail: { item: match } }));
|
||||||
|
this.dispatchEvent(new Event("change", { bubbles: true }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_renderOptions() {
|
_renderOptions() {
|
||||||
if (!this._list) {
|
if (!this._list) {
|
||||||
return;
|
return;
|
||||||
@@ -309,6 +378,7 @@ export class SingleSelectRemote extends HTMLElement {
|
|||||||
this._input.value = item.name || "";
|
this._input.value = item.name || "";
|
||||||
}
|
}
|
||||||
this._syncHiddenInput();
|
this._syncHiddenInput();
|
||||||
|
this._updateLinkButton();
|
||||||
this._hideList();
|
this._hideList();
|
||||||
this.dispatchEvent(new CustomEvent("ssrchange", { bubbles: true, detail: { item } }));
|
this.dispatchEvent(new CustomEvent("ssrchange", { bubbles: true, detail: { item } }));
|
||||||
this.dispatchEvent(new Event("change", { bubbles: true }));
|
this.dispatchEvent(new Event("change", { bubbles: true }));
|
||||||
@@ -359,6 +429,14 @@ export class SingleSelectRemote extends HTMLElement {
|
|||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
placeholder="${this._placeholder}"
|
placeholder="${this._placeholder}"
|
||||||
/>
|
/>
|
||||||
|
<a
|
||||||
|
class="ssr-open-link hidden text-sm text-gray-600 hover:text-gray-900 no-underline"
|
||||||
|
data-role="ssr-open-link"
|
||||||
|
aria-label="Auswahl öffnen"
|
||||||
|
target="${this._linkTarget}"
|
||||||
|
rel="noreferrer">
|
||||||
|
<i data-role="ssr-open-link-icon" class="ri-external-link-line"></i>
|
||||||
|
</a>
|
||||||
<button type="button" class="${SSR_CLEAR_BUTTON_CLASS} text-sm text-gray-600 hover:text-gray-900">
|
<button type="button" class="${SSR_CLEAR_BUTTON_CLASS} text-sm text-gray-600 hover:text-gray-900">
|
||||||
<i class="ri-close-line"></i>
|
<i class="ri-close-line"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -368,4 +446,34 @@ export class SingleSelectRemote extends HTMLElement {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_updateLinkButton() {
|
||||||
|
if (!this._linkButton) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const linkValue = this._selected?.[this._linkField] || this._selected?.linkId || this._selected?.id;
|
||||||
|
const icon = this._linkButton.querySelector("[data-role='ssr-open-link-icon']");
|
||||||
|
if (!linkValue || !this._linkBase) {
|
||||||
|
if (this._showWarningIcon) {
|
||||||
|
this._linkButton.classList.remove("hidden");
|
||||||
|
this._linkButton.removeAttribute("href");
|
||||||
|
this._linkButton.classList.add("ssr-open-link-warning");
|
||||||
|
this._linkButton.setAttribute("aria-label", "Auswahl fehlt");
|
||||||
|
if (icon) {
|
||||||
|
icon.className = "ri-error-warning-line";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this._linkButton.classList.add("hidden");
|
||||||
|
this._linkButton.removeAttribute("href");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._linkButton.classList.remove("hidden");
|
||||||
|
this._linkButton.classList.remove("ssr-open-link-warning");
|
||||||
|
this._linkButton.setAttribute("href", `${this._linkBase}${linkValue}`);
|
||||||
|
this._linkButton.setAttribute("aria-label", "Auswahl öffnen");
|
||||||
|
if (icon) {
|
||||||
|
icon.className = "ri-external-link-line";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user